BatchNode.batch() in a new Thread? Or Better way to display a set of tiles?

So I’m intending to use JME3 to make a 2D top-down RTS, I have the following code:
TileMap.java
[java]/*

  • To change this template, choose Tools | Templates
  • and open the template in the editor.
    */

import com.jme3.asset.AssetManager;
import com.jme3.material.Material;
import com.jme3.math.Transform;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.scene.BatchNode;
import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh;
import com.jme3.scene.Node;
import com.jme3.texture.Texture;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Set;
import org.json.JSONObject;
import com.jme3.scene.VertexBuffer.Type;
import com.jme3.util.BufferUtils;

public class TileMap extends Node {

HashMap<String, Integer> nameToIndex=new HashMap<String, Integer>();

ArrayList<Tile> tiles=new ArrayList<Tile>();

Mesh mymesh = new Mesh();

Material tileSheet;

int rows = 1;
int cols = 1;
float tileSize;

int mapSize;
Tile[] map;
BatchNode myBatch;
public TileMap(String name, float tileSize, AssetManager manager, String tilemap) {
    super(name);
    myBatch = new BatchNode(name + "-Batch");
    myBatch.setLocalTransform(Transform.IDENTITY);
    this.tileSize=tileSize;
    generateMesh(tileSize);
    tileSheet = new Material(manager, "/MatDefs/SpriteMaterial.j3md");
	
	//<SNIP LOADING CODE> PSUEDOCODE FOLLOWS
	//get texture name from asset
	tileSheet.setTexture("ColorMap",manager.loadTexture(texName));
	//load asset from tilemap string
	//set rows, cols from asset
	//for each tile type in the asset
		//load tile name, index, terrain data, and material index from asset
		Tile t = new Tile(name,tileIndex,terrainstuff, tileSheet.clone());
		t.material.setInt("Index", tileIndex);
		nameToIndex.put(name,tileIndex);
		tiles.add(t);
	
}

public int getIndexFromName(String tileName){
    return nameToIndex.get(tileName);
}

public int getIndex(int x, int y){
    return x*mapSize+y;
}

public void generateTerrain(/*<SNIP PARAMS>*/){
    mapSize=128;//<SNIPPED PARAM REFERENCE>
    map=new Tile[mapSize*mapSize];
    for(int x = 0; x<mapSize; x++){
        for(int y = 0; y<mapSIZE; y++){
            //<SNIPPED ACTUAL IMPL>
            int tile = (int)(Math.random()*getNumTiles());//<NOT REAL IMPL>
            setTile(x,y,tile,false);
        }
    }
    myBatch.batch();
}

public void setTile(int x, int y, int tile) {
    setTile(x,y,tile,true);
}
public void setTile(int x, int y, int tile,boolean rebatch) {
    Geometry cur =(Geometry) myBatch.getChild(getTileName(x, y));
    if(cur==null){
        cur = new Geometry(getTileName(x, y), mymesh);
        cur.setMaterial(tiles.get(tile).mat);
        cur.getMaterial().setInt("Index", tile);
        cur.setUserData("myTile", tile);
        myBatch.attachChild(cur);
        cur.setLocalTranslation(x*tileSize, 0, y*tileSize);
    } else {
        myBatch.detachChild(cur);
        cur.setMaterial(tiles.get(tile).mat);
        cur.setUserData("myTile", tile);
        myBatch.attachChild(cur);
    }
    map[getIndex(x,y)]=tiles.get(tile);
    if(rebatch) {
        rebatch();
    }
}

public void rebatch(){
    myBatch.batch();
}

private String getTileName(int x, int y) {
    return "tile("+x+","+y+")";
}

private void generateMesh(float len) {
    mymesh = new Mesh();
    float hlen = len/2;
    final float EPS = 0.01f;
    hlen*=(1+EPS);//REMOVES SEAMS VISIBLE AT SOME ZOOM LEVELS
    Vector3f[] verts = {
        new Vector3f(-hlen,0,-hlen),
        new Vector3f(-hlen,0,hlen),
        new Vector3f(hlen,0,hlen),
        new Vector3f(hlen,0,-hlen)
    };
	//<SNIP BOILER PLATE UV'S NORMS AND BUFFER GENERATION>
}

public int getSize() {
    return mapSize;
}

public int getNumTiles(){
    return tiles.size();
}

}
[/java]
/MatDefs/SpriteMaterial.j3md
[java]MaterialDef SpriteMaterial {

MaterialParameters {
    Texture2D ColorMap
    Int Index
    Int Cols
    Int Rows
}

Technique {
    VertexShader GLSL100:   /Shaders/SpriteVertShader.vert
    FragmentShader GLSL100: /Shaders/SpriteFragShader.frag

    WorldParameters {
        WorldViewProjectionMatrix
    }
    RenderState
    {
        Blend Alpha
    }
}

}
[/java]
both shaders:
[java]//////////////////////////////////////
//Shaders/SpriteVertShader.vert
//////////////////////////////////////
uniform mat4 g_WorldViewProjectionMatrix;
uniform int m_Cols;
uniform int m_Rows;uniform int m_Index;
attribute vec3 inPosition;
attribute vec2 inTexCoord;
varying vec2 texCoord1;void main(){

int xInd= int(mod(m_Index, m_Cols));
int yInd= m_Index / m_Cols;    float xCoord = inTexCoord.x/m_Cols;
float yCoord = inTexCoord.y/m_Rows;
xCoord = xCoord + (xInd * (1.0/float(m_Cols)));
yCoord = yCoord + (yInd * (1.0/float(m_Rows)));
xCoord = xCoord;
yCoord = 1-yCoord;    texCoord1 = vec2(xCoord,yCoord);




gl_Position = g_WorldViewProjectionMatrix * vec4(inPosition, 1.0);

}

//////////////////////////////////////////////////////
//Shaders/SpriteFragShader.frag
////////////////////////////////////////////////////////

uniform sampler2D m_ColorMap;
varying vec2 texCoord1;void main(){
vec4 color = vec4(1.0); color *= texture2D(m_ColorMap, texCoord1); gl_FragColor = color;
}[/java]
within an event handler:
[java]
for(int i = 0; i<30; i++){
int x = FastMath.nextRandomInt(0, map.getSize()-1);
int y = FastMath.nextRandomInt(0, map.getSize()-1);
int n = FastMath.nextRandomInt(0, map.getNumTiles()-1);
map.setTile(x, y, n, false);
}
map.rebatch();
[/java]

This will run just fine currently using 128 as number of tiles per side of map at ~200 fps with all in view, but when the event handler changes a tile it causes a massive frame rate drop and stutter. (without batching was getting <10 fps) Is there a better way to do a tile-map, or is there a way to offload the BatchNode.batch() method to another thread to avoid the frame-rate drop?

Am I going in an entirely wrong direction for this problem?

NOTE: I’m using the same j3md material for my animated sprites which animate by changing the Index into the sprite map.
NOTE 2: I know I’m not overriding the save/load methods, and do intend to do so, but I wanted to get it working nicely first.

You can do this in the background, but ONLY IF the node is not attached.

You can try to get it working, but I would suggest spending more time on ‘proper’ terrain mesh. You can do a lot better job than BatchNode if you know your data set exactly.

@matrixpeckham said: This will run just fine currently using 128 as number of tiles per side of map at ~200 fps with all in view, but when the event handler changes a tile it causes a massive frame rate drop and stutter. (without batching was getting <10 fps) Is there a better way to do a tile-map, or is there a way to offload the BatchNode.batch() method to another thread to avoid the frame-rate drop?

Am I going in an entirely wrong direction for this problem?

NOTE: I’m using the same j3md material for my animated sprites which animate by changing the Index into the sprite map.
NOTE 2: I know I’m not overriding the save/load methods, and do intend to do so, but I wanted to get it working nicely first.

You may want to consider a custom mesh since the size of the mesh will never be that large at any given time.

You could set the mesh up as a number of unattached quads, and update the buffers to move the “quads” to the location needed with the new texture coordinates. You could manage multiple height layers, impostors, etc all in the same mesh. you’d probably be looking at a max of 1000 verts? Maybe a bit more.

Either way, updating the buffers every frame is going to be a minimal performance hit compared to rebatching 100 individual objects… even on a separate thread.

What abies and t0neg0d said. You’d best make your own mesh.

If you absolutely want to keep the BatchNode for this, you could have your scene split up in chunks and just rebatch subparts (it would be faster).
Or do what Empire suggest, have 2 batch Nodes one in the scene graph and one detached from the scene graph. You batch the detached one in a thread en then enqueue its attachment to the app. Then you switch the 2 batchNodes. That’s BatchNode double buffering…that’s quite the gymnastic…
Or a combination of the 2 options…

Thanks for the fast replies.

@t0neg0d If I understand you correctly, I should make a mesh and controller, and update the mesh with only visible tiles. This seems like an ideal solution for my main view once I add the limit on zooming out.

When I said 128, with all visible, I meant that it was 128x128 tiles all showing at once 16384 quads. How would this custom mesh be any different in this case? While your case will make the addition of Fog Of War a bit easier. Also how does that technique work when I want to render a minimap?

@nehon How would one keep the two BatchNodes in sync? Example, change the tile at (1,1) from index 1 to 2. in another thread take secondary, non-attached, BatchNode, remove 1,1 change index of the node, re-attach it to the secondary batch, call batch() queue the swap. in main thread, find queued swap, swap BatchNodes then… our new buffer, the BatchNode that was being used is now out-of-date, how would I duplicate the changes in the other BatchedNode (this would not need to be re-batched, because the next change will do it)?

t0neg0d’s plan seems to be the best for my main view, but what would the best way to allow the whole map to be in view, or a minimap of the whole map. Also what would be a good way to implement fog of war.

off topic: BatchNode will throw a NullPointerException if any of the meshes don’t have vertex normals. Is this undocumented but intended or a bug?

@matrixpeckham said: When I said 128, with all visible, I meant that it was 128x128 tiles all showing at once 16384 quads. How would this custom mesh be any different in this case? While your case will make the addition of Fog Of War a bit easier. Also how does that technique work when I want to render a minimap?

16384 is not that many quads. So if you go with abies/tonegod’s approach then you could do it all as one mesh.

If it were me, I still might try to break it up into a few smaller ones. Either 4 64x64 or 16 32x32 ones. It will just make it easier/faster to update them. It would also give you a clean and convenient place to attach things on the map so that they get frustum culled with the tile instead of individually.

@pspeed said: 16384 is not that many quads. So if you go with abies/tonegod's approach then you could do it all as one mesh.

If it were me, I still might try to break it up into a few smaller ones. Either 4 64x64 or 16 32x32 ones. It will just make it easier/faster to update them. It would also give you a clean and convenient place to attach things on the map so that they get frustum culled with the tile instead of individually.

Breaking into chunks would probably be a good idea, truth be told, my plan is to increase the size dramatically once i get it working will with smaller sizes my plan is to use a much larger map size 1024x1024 tiles (1048576 tiles) and the chunks seem to be a good way to get that without issue, especially if combined with the possible custom mesh, but the minimap still concerns me. I know I don’t have to draw the minimap nearly as often as everything else. What do you recommend I do for that?

@matrixpeckham said: Breaking into chunks would probably be a good idea, truth be told, my plan is to increase the size dramatically once i get it working will with smaller sizes my plan is to use a much larger map size 1024x1024 tiles (1048576 tiles) and the chunks seem to be a good way to get that without issue, especially if combined with the possible custom mesh, but the minimap still concerns me. I know I don't have to draw the minimap nearly as often as everything else. What do you recommend I do for that?

How big do you want it? Should be it be just representative or do you actually want an overhead camera view?

Approach 1: just use an ImageRaster to paint a texture that is a mini-map version of your tiles (just representative)

Approach 2: use a pre-ViewPort to render your map from above and then take that texture to put on a minimap object in the HUD.

@pspeed said: How big do you want it? Should be it be just representative or do you actually want an overhead camera view?

Approach 1: just use an ImageRaster to paint a texture that is a mini-map version of your tiles (just representative)

Approach 2: use a pre-ViewPort to render your map from above and then take that texture to put on a minimap object in the HUD.

I was thinking representative, the problem I see with just painting a RasterImage would be on of possibly displaying known unit locations on it. If a player can see a unit through the fog of war the minimap should have some indication that there is an enemy unit in that area of the map. Translating that to a raster image would probably be faster than rendering the entire world to a texture. Also the minimap will need to be updated, but the representation of the map itself shouldn’t need to be changed unless the map is altered.

I took a quick look at ImageRaster, is there an existing wrapper or utility class that has convenience methods? (fill/draw rectangle, drawLine etc.)? edit: or do I have to re-live Intro to Computer graphics and implement bresenham’s line algorithm sigh memories

There is an ImagePainter plugin if you check in the plugins in the SDK.

Wow, what a difference, I changed my class it now looks like this:

[java]/*

  • To change this template, choose Tools | Templates
  • and open the template in the editor.
    */

import com.jme3.asset.AssetManager;
import com.jme3.material.Material;
import com.jme3.math.Transform;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.scene.BatchNode;
import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh;
import com.jme3.scene.Node;
import com.jme3.texture.Texture;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Set;
import org.json.JSONObject;
import com.jme3.scene.VertexBuffer.Type;
import com.jme3.util.BufferUtils;

public class TileMap extends Node {

HashMap&lt;String, Integer&gt; nameToIndex=new HashMap&lt;String, Integer&gt;();

ArrayList&lt;Tile&gt; tiles=new ArrayList&lt;Tile&gt;();

Mesh mymesh = new Mesh();

Material tileSheet;

int rows = 1;
int cols = 1;
float tileSize;

int mapSize;
Tile[] map;
Geometry geom
public TileMap(String name, float tileSize, AssetManager manager, String tilemap) {
    super(name);
    myBatch = new BatchNode(name + “-Batch”);
    myBatch.setLocalTransform(Transform.IDENTITY);
    this.tileSize=tileSize;
    generateMesh(tileSize);
    tileSheet = new Material(manager, “/MatDefs/SpriteMaterial.j3md”);

	//&lt;SNIP LOADING CODE&gt; PSUEDOCODE FOLLOWS
	//get texture name from asset
	tileSheet.setTexture(“ColorMap”,manager.loadTexture(texName));
	//load asset from tilemap string
	//set rows, cols from asset
	//for each tile type in the asset
		//load tile name, index, terrain data, and material index from asset
		Tile t = new Tile(name,tileIndex,terrainstuff, tileSheet.clone());
		t.material.setInt(“Index”, tileIndex);
		nameToIndex.put(name,tileIndex);
		tiles.add(t);
	geom = new Geometry("mapgeom", mymesh);
	geom.setMaterial(tileSheet);
	attachChild(geom);
}

public int getIndexFromName(String tileName){
    return nameToIndex.get(tileName);
}

public int getIndex(int x, int y){
    return x*mapSize+y;
}

public void generateTerrain(/*&lt;SNIP PARAMS&gt;*/){
    mapSize=128;//&lt;SNIPPED PARAM REFERENCE&gt;
    map=new Tile[mapSize*mapSize];
    for(int x = 0; x&lt;mapSize; x++){
        for(int y = 0; y&lt;mapSIZE; y++){
            //&lt;SNIPPED ACTUAL IMPL&gt;
            int tile = (int)(Math.random()*getNumTiles());//&lt;NOT REAL IMPL&gt;
            setTile(x,y,tile,false);
        }
    }
    myBatch.batch();
}

public void setTile(int x, int y, int tile) {
    int index = getIndex(x, y);
    index *= 4;//tile index of vert
    index *= 2;//index of x coord
    FloatBuffer uvbuf = (FloatBuffer)mymesh.getBuffer(Type.TexCoord).getData();
    
    int xInd = tile % cols;
    int yInd = tile / cols;
    
    for(int i = 0; i&lt;4; i++){
        Vector2f inTexCoord = uvs[i];
        float xCoord = inTexCoord.x / cols;
        float yCoord = inTexCoord.y / rows;
        xCoord = xCoord + (xInd * (1.0f / cols));
        yCoord = yCoord + (yInd * (1.0f / rows));
        //xCoord = xCoord;
        yCoord = 1 - yCoord;
        uvbuf.put(index, xCoord);
        uvbuf.put(index+1, yCoord);
        index+=2;
    }
    mymesh.getBuffer(Type.TexCoord).setUpdateNeeded();
}

private String getTileName(int x, int y) {
    return “tile(“+x+”,”+y+”)”;
}

Vector2f[] uvs = {
    new Vector2f(0, 0),
    new Vector2f(0, 1),
    new Vector2f(1, 1),
    new Vector2f(1, 0)
};

private void generateMesh(float len) {
    mymesh = new Mesh();
    float hlen = len / 2;
    final float EPS = 0.01f;

// hlen*=(1+EPS);
Vector3f[] verts = {
new Vector3f(-hlen, 0, -hlen),
new Vector3f(-hlen, 0, hlen),
new Vector3f(hlen, 0, hlen),
new Vector3f(hlen, 0, -hlen)
};
Vector3f[] norms = {
new Vector3f(0, 1, 0),
new Vector3f(0, 1, 0),
new Vector3f(0, 1, 0),
new Vector3f(0, 1, 0)
};
int[] inds = {
0, 1, 2,
0, 2, 3
};
ArrayList<Vector3f> vertlist = new ArrayList<Vector3f>();
ArrayList<Vector3f> normlist = new ArrayList<Vector3f>();
ArrayList<Vector2f> uvlist = new ArrayList<Vector2f>();
ArrayList<Integer> indlist = new ArrayList<Integer>();
int tile = 0;
Vector3f trans = new Vector3f();
Vector2f uv = new Vector2f(1f / cols, 1f / rows);
for (int x = 0; x < mapSize; x++) {
for (int y = 0; y < mapSize; y++) {
trans.x = x * tileSize;
trans.z = y * tileSize;
for (int i = 0; i < 4; i++) {
vertlist.add(verts[i].add(trans));
normlist.add(norms[i]);
Vector2f nuv = new Vector2f(uvs[i]);
nuv.x *= uv.x;
nuv.y *= uv.y;
uvlist.add(nuv);
}
int indAdd = tile * 4;
for (int i = 0; i < 6; i++) {
indlist.add(inds[i] + indAdd);
}
tile++;
}
}

    int[] indarr = new int[indlist.size()];
    for (int i = 0; i &lt; indlist.size(); i++) {
        indarr[i] = indlist.get(i);
    }
    Vector3f[] vertarr = new Vector3f[vertlist.size()];
    Vector3f[] normarr = new Vector3f[normlist.size()];
    Vector2f[] texarr = new Vector2f[uvlist.size()];
    mymesh.setBuffer(Type.Position, 3, BufferUtils.createFloatBuffer((vertlist.toArray(vertarr))));
    mymesh.setBuffer(Type.TexCoord, 2, BufferUtils.createFloatBuffer(uvlist.toArray(texarr)));
    mymesh.setBuffer(Type.Index, 3, BufferUtils.createIntBuffer(indarr));
    mymesh.setBuffer(Type.Normal, 3, BufferUtils.createFloatBuffer(normlist.toArray(normarr)));
    mymesh.updateBound();
}

public int getSize() {
    return mapSize;
}

public int getNumTiles(){
    return tiles.size();
}

}[/java]

With this code the changes in tiles don’t cause a stutter and my frame rate is much higher, ~900, with the same whole map. I also had to change the min filter for the texture to prevent some artifacts caused by automatic mip mapping causing tiles to bleed into each other. now i’m using NearestNoMip for minfilter and Nearest for magfilter.

I tried it with a mapSize of 1024, and it does slow down to ~100 fps, but the startup time is more of a bother at that point, another reason to go chunky and generate visible chunks first. then do the rest in the background.

is there a better way for me to implement the generateMesh method? I suppose I could directly allocate plain arrays instead of the ArrayLists, as the method is only ever called once the performance really isn’t a concern.

@matrixpeckham said: is there a better way for me to implement the generateMesh method? I suppose I could directly allocate plain arrays instead of the ArrayLists, as the method is only ever called once the performance really isn't a concern.

Well, as it stands now, a bunch of copies are made with your current approach.

First you make an ArrayList of Vector3fs that you will throw away and then you make arrays of Vector3fs that get thrown away. The actual buffers are FloatBuffers.

So the “best” way from a performance/memory perspective would be to avoid the Vector3f objects completely and just put your x,y,z directly into some empty, appropriately sized, float buffers. Whether it’s worth it or not depends, I guess.

Yes, I know I’m wasting a lot of resources. I wanted to get it working first and foremost, and worry about efficiency later. As it stands I’ll probably wait to do that until I port it over to using several chunks. I doubt it would be a noticeable improvement, but I’ll definitely fix it before i consider it “production worthy.”

Thanks for the help, everyone. As my initial question has been answered I’ll start another if I have more problems. Is there a way to mark the topic as solved? (sorry for the newbie question, this was my first topic here)