Draw to Texture

I am creating a Scrabble-like game, with a game board that has a grid and various bonus squares (see image below).



Snug Scrabulous board



I want these bonus squares to be positioned based on game data, not baked onto the board texture itself. Using JMonkeyEngine 3, how can I draw the images for the bonus squares on the board texture?

:o Is this a trivia? Can we win something?

:affe:

I imagine you’ll use Nifty for that.



I’ll assume so and that you’ll use Nifty’s drag 'n drop feature.



When you’ll setup your board make sure that you’ll put the multiplicator tiles in the proper panels like this:



The example below is taken from my inventory system.



[xml]

<panel height=“47” width=“49” childLayout=“absolute” id=“inv2” x=“51” y=“1” >

<!-- Put the multiplicator tile here so it’s not dragged. -->

<image filename=“Textures/GUI/multx4.png” x=“0” y=“0” />

<control id=“invDrp02” name=“droppable” width=“100%” height=“100%” childLayout=“absolute” x=“0” y="-1" >

<control id=“invDrg02” name=“draggable” childLayout=“absolute” x=“0” y="-1">

</control>

</control>

</panel>

[/xml]

1 Like

I think the OP means the squares on the board that give x2 for the letter or x3 for the word etc. (correct me if I’m wrong).

I’m not that knowledgeable with shaders, but I’m sure this could be done with a shader based on your game data positions fairly easily if you know how to work with shaders, adding colour data in the right places to produce the texture for your level.

Otherwise you could just create a static image externally with the bonus squares, as an overlay to use in the game.

This is how I did it:

http://hub.jmonkeyengine.org/groups/gui/forum/topic/drawing-into-textures-once-instead-of-every-frame/

@normen: Let's see what's behind door number one! ... It's a brand new upvote! :D

@madjack, @zarch: Hmmm... interesting. I hadn't thought of using Nifty. It may work for this situation simply because what I'm trying to do is so simple (draw one image on top of another). I will definitely use this technique for the level editor. :)

@Tumaini: You are correct. I am trying to draw the bonus squares. Ideally, I would generate the texture for the board once at the beginning of the game, since the squares won't move mid-game, but so far I haven't found any way to do this.

I am not very experienced with shaders, but I know that they basically let you define code to be run at key points of the rendering pipeline. If I used a shader, though, the image for the board would still be rendered each frame, yes?

I found a couple examples that use java.awt.BufferedImage to do the drawing (via the Graphics object), but they all have to copy the result pixel-by-pixel into a new com.jme3.texture.Image at the end (which seems incredibly inefficient).



I know there’s a jME class/method for converting between the two Image formats:[java]ImageToAWT.convert(BufferedImage image, Format format, ByteBuffer buf)[/java]but I can’t figure out how to use it. The method doesn’t return a jME Image like I expected, and where am I supposed to get the ByteBuffer object?

My example uses nifty to draw into the texture once (or whenever you need to change it) after that the same texture is just used repeatedly.



Since you are only doing it once at startup it doesn’t need to be super fast (although in fact the nifty rendering is fast anyway and one texture copy doesn’t change that).



All textures need copying across to the graphics card anyway so a certain amount of copying around is unavoidable.

cool game! :slight_smile:





http://hub.jmonkeyengine.org/wp-includes/images/smilies/affe.gif I like that :smiley:

@zarch: The solution you proposed is valid, but the limitation is that you have to use GUI elements to do all the drawing. I was more interested in finding out whether it was possible to draw directly to a Texture in code. For example: drawing lines and beziers onto a texture to generate a random maze in the shape of a circle. (This would be very hard to do using only GUI elements.)



So far, I have found no way to draw to a Texture directly. As @zarch mentioned, all texture data has to be copied to the graphics card, so I imagine to manipulate this data directly you would have to use shaders.



However, it is possible to generate an AWT Image, draw to it using a Graphics2D object, and then convert it to a jME Image that can be used with a Texture. The class to use when converting an AWT Image to a jME Image is com.jme3.texture.plugins.AWTLoader.



Here are the results of a simple test I did: a large plane with a texture I generated completely in code:



AWT Image to Texture 2



Here is the code itself:

[java]

// Create a plane.

Quad boardShape = new Quad(10.0f, 10.0f);

Geometry board = new Geometry(“Board”, boardShape);



// Create the board image.

BufferedImage boardImage = new BufferedImage(512, 512, BufferedImage.TYPE_INT_RGB);

Graphics2D boardGraphics = boardImage.createGraphics();



for(int y = 0; y < 16; y++)

{

for(int x = 0; x < 16; x++)

{

Color color = new Color(Color.HSBtoRGB((float)(x + y) / 32f, 1.0f, 1.0f));

boardGraphics.setColor(color);

boardGraphics.fillRect((x * 32) - 1, (y * 32) - 1, 30, 30);

}

}



Image image = new AWTLoader().load(boardImage, false);

Texture2D boardTexture = new Texture2D(image);



Material material = new Material(assetManager, “Common/MatDefs/Misc/Unshaded.j3md”);

material.setTexture(“ColorMap”, boardTexture);



board.setMaterial(material);



rootNode.attachChild(board);

[/java]



NOTE: Because the image data must be copied from the AWT Image into the jME Image, this process is slow. This is not something you should do every frame. If you plan to use this method, generate the Image once at the beginning of the game (when all the assets are being loaded). At the least, make sure you only re-generate the image when it changes.

Seems to work for you. Just be aware that all the Java BufferedImage stuff is not available on Android so if you use it you won’t be portable to Android (I don’t know if that’s a requirement for you but it was one reason I went the nifty route).

P.S. Java Graphics2D is sloooooow. It wouldn’t surprise me if you put some timings in you found that a fair amount of your time is just spent running through the loop drawing into the image.



It’s not too bad using the operations available (i.e. coping images, drawing primitives etc) but if you try and do anything down at the pixel level it’s terrible.

@zarch: Noted. Yeah, this really only works because I’m generating the image once during load and never changing it after that. If I was doing this every frame, I’m sure the game would be unplayable.



Does anyone know of a way to do something like this easily with a pixel shader? I want to exhaust all of my options.

Hey i have an android project where i draw directly on a surface. For this i have a ImageUtil class that allow you to draw lines in a texture.

It’s vastly inspired from the ImageToAwt jme class but i removed the conversion to not have any reference on AWT (because that was for android)

Any way i guess it can help



look for the drawLine method it takes a start and end coordinates, color, the format of the image (usually RGBA8), the byteBuffer to write to and the size of the image (only square images, but it looks like it’s ok for you).



the byteBuffer is the data of a texture2D (myTexture.getImage().getData(0)). Then you can just feed a material with this texture

mat.setTexture(“Colormap”,myTexture);



hope that helps.



EDIT : don’t mind the javadoc of the methods it just a blunt copy/paste of the first method

[java]



import com.jme3.math.ColorRGBA;

import com.jme3.math.Vector2f;

import com.jme3.texture.Image.Format;

import java.nio.ByteBuffer;

import java.util.EnumMap;



public class ImageUtils {



private static final EnumMap<Format, DecodeParams> params = new EnumMap<Format, DecodeParams>(Format.class);



private static class DecodeParams {



final int bpp, am, rm, gm, bm, as, rs, gs, bs, im, is, ra, rg, rb, rr;



public DecodeParams(int bpp, int am, int rm, int gm, int bm, int as, int rs, int gs, int bs, int im, int is) {

this.bpp = bpp;

this.am = am;

this.rm = rm;

this.gm = gm;

this.bm = bm;

this.as = as;

this.rs = rs;

this.gs = gs;

this.bs = bs;

this.im = im;

this.is = is;

ra = 8 - Integer.bitCount(am);

rr = 8 - Integer.bitCount(rm);

rg = 8 - Integer.bitCount(gm);

rb = 8 - Integer.bitCount(bm);



}



public DecodeParams(int bpp, int rm, int rs, int im, int is, boolean alpha) {

this.bpp = bpp;

if (alpha) {

this.am = rm;

this.as = rs;

this.rm = 0;

this.rs = 0;

} else {

this.rm = rm;

this.rs = rs;

this.am = 0;

this.as = 0;

}



this.gm = 0;

this.bm = 0;

this.gs = 0;

this.bs = 0;

this.im = im;

this.is = is;

ra = 8 - Integer.bitCount(am);

rr = 8 - Integer.bitCount(rm);

rg = 8 - Integer.bitCount(gm);

rb = 8 - Integer.bitCount(bm);

}



public DecodeParams(int bpp, int rm, int rs, int im, int is) {

this(bpp, rm, rs, im, is, false);

}

}



static {

final int mx___ = 0xff000000;

final int m_x__ = 0x00ff0000;

final int m__x_ = 0x0000ff00;

final int m___x = 0x000000ff;

final int sx___ = 24;

final int s_x__ = 16;

final int s__x_ = 8;

final int s___x = 0;

final int mxxxx = 0xffffffff;

final int sxxxx = 0;



final int m4x___ = 0xf000;

final int m4_x__ = 0x0f00;

final int m4__x_ = 0x00f0;

final int m4___x = 0x000f;

final int s4x___ = 12;

final int s4_x__ = 8;

final int s4__x_ = 4;

final int s4___x = 0;



final int m5___ = 0xf800;

final int m_5__ = 0x07c0;

final int m__5_ = 0x003e;

final int m___1 = 0x0001;



final int s5___ = 11;

final int s_5__ = 6;

final int s__5_ = 1;

final int s___1 = 0;



final int m5__ = 0xf800;

final int m_6_ = 0x07e0;

final int m__5 = 0x001f;



final int s5__ = 11;

final int s_6_ = 5;

final int s__5 = 0;



final int mxx__ = 0xffff0000;

final int sxx__ = 32;

final int m__xx = 0x0000ffff;

final int s__xx = 0;



// note: compressed, depth, or floating point formats not included here…



params.put(Format.ABGR8, new DecodeParams(4, mx___, m___x, m__x_, m_x__,

sx___, s___x, s__x_, s_x__,

mxxxx, sxxxx));

params.put(Format.ARGB4444, new DecodeParams(2, m4x___, m4_x__, m4__x_, m4___x,

s4x___, s4_x__, s4__x_, s4___x,

mxxxx, sxxxx));

params.put(Format.Alpha16, new DecodeParams(2, mxxxx, sxxxx, mxxxx, sxxxx, true));

params.put(Format.Alpha8, new DecodeParams(1, mxxxx, sxxxx, mxxxx, sxxxx, true));

params.put(Format.BGR8, new DecodeParams(3, 0, m___x, m__x_, m_x__,

0, s___x, s__x_, s_x__,

mxxxx, sxxxx));

params.put(Format.Luminance16, new DecodeParams(2, mxxxx, sxxxx, mxxxx, sxxxx, false));

params.put(Format.Luminance8, new DecodeParams(1, mxxxx, sxxxx, mxxxx, sxxxx, false));

params.put(Format.Luminance16Alpha16, new DecodeParams(4, m__xx, mxx__, 0, 0,

s__xx, sxx__, 0, 0,

mxxxx, sxxxx));

params.put(Format.Luminance16F, new DecodeParams(2, mxxxx, sxxxx, mxxxx, sxxxx, false));

params.put(Format.Luminance16FAlpha16F, new DecodeParams(4, m__xx, mxx__, 0, 0,

s__xx, sxx__, 0, 0,

mxxxx, sxxxx));

params.put(Format.Luminance32F, new DecodeParams(4, mxxxx, sxxxx, mxxxx, sxxxx, false));

params.put(Format.Luminance8, new DecodeParams(1, mxxxx, sxxxx, mxxxx, sxxxx, false));

params.put(Format.RGB5A1, new DecodeParams(2, m___1, m5___, m_5__, m__5_,

s___1, s5___, s_5__, s__5_,

mxxxx, sxxxx));

params.put(Format.RGB565, new DecodeParams(2, 0, m5__, m_6_, m__5,

0, s5__, s_6_, s__5,

mxxxx, sxxxx));

params.put(Format.RGB8, new DecodeParams(3, 0, m_x__, m__x_, m___x,

0, s_x__, s__x_, s___x,

mxxxx, sxxxx));

params.put(Format.RGBA8, new DecodeParams(4, m___x, mx___, m_x__, m__x_,

s___x, sx___, s_x__, s__x_,

mxxxx, sxxxx));

}



private static int Ix(int x, int y, int w) {

return y * w + x;

}



private static int readPixel(ByteBuffer buf, int idx, int bpp) {

buf.position(idx);

int original = buf.get() & 0xff;

while ((–bpp) > 0) {

original = (original << 8) | (buf.get() & 0xff);

}

return original;

}



private static void writePixel(ByteBuffer buf, int idx, int pixel, int bpp) {

buf.position(idx);

while ((–bpp) >= 0) {

// pixel = pixel >> 8;

byte bt = (byte) ((pixel >> (bpp * 8)) & 0xff);

// buf.put( (byte) (pixel & 0xff) );

buf.put(bt);

}

}



/**

  • Convert an ColorRBGA array image to jME image.

    */

    public static void convert(ColorRGBA[] image, Format format, ByteBuffer buf) {

    DecodeParams p = params.get(format);

    if (p == null) {

    throw new UnsupportedOperationException("Image format " + format + " is not supported");

    }



    for (int x = 0; x < image.length; x++) {



    int outputPixel = encodeColor(image[x], p);

    int i = (x * p.bpp);

    writePixel(buf, i, outputPixel, p.bpp);

    }



    }



    /**
  • Convert an ColorRBGA array image to jME image.

    */

    public static void clear(ColorRGBA color, Format format, ByteBuffer buf, int size) {

    DecodeParams p = params.get(format);

    if (p == null) {

    throw new UnsupportedOperationException("Image format " + format + " is not supported");

    }



    for (int x = 0; x < size * size; x++) {



    int outputPixel = encodeColor(color, p);

    int i = (x * p.bpp);

    writePixel(buf, i, outputPixel, p.bpp);

    }



    }



    /**
  • Convert an ColorRBGA array image to jME image.

    */

    public static void draw(Vector2f pixel, ColorRGBA color, Format format, ByteBuffer buf, int size) {

    DecodeParams p = params.get(format);

    if (p == null) {

    throw new UnsupportedOperationException("Image format " + format + " is not supported");

    }



    int x = (int) (pixel.x * size);

    int y = (int) (pixel.y * size);

    x += y * size;



    int outputPixel = encodeColor(color, p);

    int i = (x * p.bpp);

    writePixel(buf, i, outputPixel, p.bpp);



    }



    /**
  • Convert an ColorRBGA array image to jME image.

    */

    public static void drawLine(Vector2f start, Vector2f end, ColorRGBA color, Format format, ByteBuffer buf, int size) {

    DecodeParams p = params.get(format);

    if (p == null) {

    throw new UnsupportedOperationException("Image format " + format + " is not supported");

    }

    int outputPixel = encodeColor(color, p);



    int x0 = (int) Math.round(start.x * size);

    int y0 = (int) Math.round(start.y * size);

    int x1 = (int) Math.round(end.x * size);

    int y1 = (int) Math.round(end.y * size);



    int dx = Math.abs(x1 - x0);

    int dy = Math.abs(y1 - y0);

    int sx = -1;

    int sy = -1;

    if (x0 < x1) {

    sx = 1;

    }

    if (y0 < y1) {

    sy = 1;

    }

    int err = dx - dy;



    while (x0 != x1 || y0 != y1) {



    writePixel(buf, (x0 + y0 * size) * p.bpp, outputPixel, p.bpp);

    int e2 = 2 * err;

    if (e2 > -dy) {

    err = err - dy;

    x0 = x0 + sx;

    }

    if (e2 < dx) {

    err = err + dx;

    y0 = y0 + sy;

    }

    }





    }



    private static int encodeColor(ColorRGBA color, DecodeParams p) {

    // Get ARGB

    int argb = color.asIntARGB();

    // Extract color components

    int a = (argb & 0xff000000) >> 24;

    int r = (argb & 0x00ff0000) >> 16;

    int g = (argb & 0x0000ff00) >> 8;

    int b = (argb & 0x000000ff);

    // Remove anything after 8 bits

    a = a & 0xff;

    r = r & 0xff;

    g = g & 0xff;

    b = b & 0xff;

    // Do bit reduction, assumes proper rounding has already been

    // done.

    a = a >> p.ra;

    r = r >> p.rr;

    g = g >> p.rg;

    b = b >> p.rb;

    // Put components into appropriate positions

    a = (a << p.as) & p.am;

    r = (r << p.rs) & p.rm;

    g = (g << p.gs) & p.gm;

    b = (b << p.bs) & p.bm;

    int outputPixel = ((a | r | g | b) << p.is) & p.im;

    return outputPixel;

    }

    }



    [/java]