How to create a 2D drawing canvas overlay

I need to make a complex graphical representiation of same data in my game. To do so I want to create an overlay image that I can draw on.

How can I achieve this overlay? Is there a library that gives me some drawing commands to create circles, lines and text?

And would it be possible to integrate this image in a Lemur -or other- GUI?

My first idea would be a bitmap image, but an SVG that I can access and update would be even better.

Is what you are looking for: a dynamic texture that you can draw pixels onto that you can then put onto a quad (Lemur works fine with spatials so that should be fine there). I did something like that for a radar display.

If so this may help. You can call setPixel (circles were important to me so there is a method for that as well) then when you are finished building it up call getTexture() to get a JMonkeyEngine Texture you can use as normal. (You can also get an antiAliased texture)

/**
 * @author Richard Tingle
 * License: public domain
 */
import com.jme3.math.ColorRGBA;
import com.jme3.texture.Image;
import com.jme3.texture.Image.Format;
import com.jme3.texture.Texture;
import com.jme3.texture.Texture.MagFilter;
import com.jme3.texture.Texture2D;
import com.jme3.util.BufferUtils;
import java.nio.ByteBuffer;
  
public class DrawableTexture{
  
    public static int ENTIES_PER_PIXEL=4;
    
    private enum ColorEntry{
        RED,
        GREEN,
        BLUE,
        ALPHA
    }

    protected byte[] data;
    protected byte[] antiAliasedData;
    protected Image image;
    protected Texture2D texture;
    int sizeX;
    int sizeY;
  
    public DrawableTexture(int width, int height) {
  
        this.sizeX = width;
        this.sizeY = height;
  
        // create black image
        data = new byte[width * height * 4];
        antiAliasedData=new byte[data.length];
        
        setBackground (new ColorRGBA(0,0,0,255));
  
        // set data to texture
        ByteBuffer buffer = BufferUtils.createByteBuffer(data);
        image = new Image(Format.RGBA8, width, height, buffer);
        
        texture = new Texture2D(image);
        texture.setMagFilter(Texture.MagFilter.Nearest);
  
    }
  
    /**
     * used for any drawable texture that can be rotated by the video block
     * when rotated the edge gets stretched to the end, so it needs to be
     * transparent
     */
    public void createTransparentEdge(){
        ColorRGBA transparent =new ColorRGBA(0,0,0,0);
        for(int i=0;i<sizeX;i++){
            setPixel(i, 0,transparent);
            setPixel(i, sizeY-1,transparent);
        }
        for(int i=0;i<sizeY;i++){
            setPixel(0, i, transparent);
            setPixel(sizeX-1, i, transparent);
        }
    }
    
    public Texture getTexture() {
        ByteBuffer buffer = BufferUtils.createByteBuffer(data);
        image.setData(buffer);
        return texture;
    }
    
    public Texture getTexture_antiAliased(){

        for(int i=0;i<sizeX;i++){
            for(int j=0;j<sizeY;j++){
                int index = (i + j * sizeX) * ENTIES_PER_PIXEL;
                    antiAliasedData[index] = getAntiAliasedEntry(i, j, ColorEntry.RED);
                    antiAliasedData[index  + 1] = getAntiAliasedEntry(i, j, ColorEntry.GREEN);
                    antiAliasedData[index  + 2] = getAntiAliasedEntry(i, j, ColorEntry.BLUE);
                    antiAliasedData[index  + 3] =  getAntiAliasedEntry(i, j, ColorEntry.ALPHA);
                
            }
        }
        ByteBuffer buffer = BufferUtils.createByteBuffer(antiAliasedData);
        image.setData(buffer);
        return texture;
        
    }

    private byte getAntiAliasedEntry(int x, int y, ColorEntry element){      
        int returnValue=(int)(
        0.5*getEntry(x,y,element) + 
        (1.0/12)*getEntry(x+1,y,element) + 
        (1.0/12)*getEntry(x,y+1,element) +
        (1.0/12)*getEntry(x-1,y,element) +
        (1.0/12)*getEntry(x,y-1,element) +
        (1.0/24)*getEntry(x+1,y+1,element) +
        (1.0/24)*getEntry(x+1,y-1,element) +  
        (1.0/24)*getEntry(x-1,y+1,element) +      
        (1.0/24)*getEntry(x-1,y-1,element)
        );
        
        if (returnValue>127){
            returnValue-=256;
        }

        
        return (byte)returnValue;      
    }
    
    private int getEntry(int x, int y, ColorEntry element){
        if (x>=sizeX || y>=sizeY || x<0 || y<0){
            return 0; //outside the image
        }
        
        int i = (x + y * sizeX) * 4;
        
        byte dataValue;
        
        switch(element){
            case RED:
                dataValue=data[i];
                break;
            case GREEN:
                dataValue=data[i+1];
                break;
            case BLUE:
                dataValue=data[i+2];
                break;
            case ALPHA:
                dataValue=data[i+3];
                break;
            default:
                throw new RuntimeException("Element does not exist");
        }
        
        return (dataValue<0?dataValue+256:dataValue);
    }
    private void setEntry(int x, int y, ColorEntry element, int newValue){
        if (x>=sizeX || y>=sizeY || x<0 || y<0){
            return; //outside the image
        }
        
        int i = (x + y * sizeX) * 4;
        
        byte dataValue=(byte)(newValue>127?newValue-256:newValue);
        
        switch(element){
            case RED:
                data[i]=dataValue;
                break;
            case GREEN:
                data[i+1]=dataValue;
                break;
            case BLUE:
                data[i+2]=dataValue;
                break;
            case ALPHA:
                data[i+3]=dataValue;
                break;
            default:
                throw new RuntimeException("Element does not exist");
        }
    }

    public void setPixel(int x, int y, ColorRGBA color) {
        if (x>=sizeX || y>=sizeY || x<0 || y<0){
            return; //outside the image
        }

        int i = (x + y * sizeX) * 4;
        
        //color.asBytesRGBA() extracted to avoid temporary byte[]
        data[i] = (byte) ((int) (color.r * 255) & 0xFF); // r
        data[i + 1] = (byte) ((int) (color.g * 255) & 0xFF); // g
        data[i + 2] = (byte) ((int) (color.b * 255) & 0xFF); // b
        data[i + 3] = (byte) ((int) (color.a * 255) & 0xFF); // a
    }
    public void setPixel(int x, int y, byte[] color) {
        if (x>=sizeX || y>=sizeY || x<0 || y<0){
            return; //outside the image
        }

        int i = (x + y * sizeX) * 4;
        
        data[i] = color[0]; // r
        data[i + 1] = color[1]; // g
        data[i + 2] = color[2]; // b
        data[i + 3] = color[3]; // a
    }
    
    public ColorRGBA getPixel(int x, int y) {
        if (x>=sizeX || y>=sizeY || x<0 || y<0){
            return new ColorRGBA(0,0,0,0); //outside the image
        }
        ColorRGBA color=new ColorRGBA();
        int i = (x + y * sizeX) * 4;
        color.set(data[i], data[i + 1], data[i + 2], data[i + 3]);
        return color;
    }

    public void setCircle(int x, int y, double radius, ColorRGBA color){
        byte[] byteColor=color.asBytesRGBA();
        
        for(int i=(int)(x-radius-1);i<x+radius+1;i++){
            //see http://stackoverflow.com/questions/14285358/find-all-integer-coordinates-in-a-given-radius
            //-sqrt(r^2 - x^2) to sqrt(r^2 - x^2)
            final int minJ=(int)-Math.sqrt(radius*radius-(i-x)*(i-x))+y;
            final int maxJ=(int)+Math.sqrt(radius*radius-(i-x)*(i-x))+y;
            
            for(int j=minJ;j<maxJ;j++){
                setPixel(i,j, byteColor);
            }
        }
    }
    
    public final void setBackground(ColorRGBA color) {
  
        byte[] colorBytes=color.asBytesRGBA();
        for (int i = 0; i < sizeX * sizeY * 4; i += 4) {
            data[i] = colorBytes[0]; // r
            data[i + 1] = colorBytes[1]; // g
            data[i + 2] = colorBytes[2]; // b
            data[i + 3] = colorBytes[3]; // a
  
        }
    }
  
    public void setMagFilter(MagFilter filter) {
        texture.setMagFilter(filter);
    }

    public int getSizeX() {
        return sizeX;
    }

    public int getSizeY() {
        return sizeY;
    }

}

If you’re updating this at a high frame rate you may want to look at alternatives (that don’t fully push a new buffer each time) but for occasionally refreshed dynamic textures this has worked well for me

2 Likes

This is interesting, if I could add a text function and a way to draw lines, then for now it would be what I need.