Terrain texture questions

I am digging around the terrain examples and have a few questions:


  1. Is it possible to have two textures at the same height? I ask this because I would like to create terrain similar to that found in Civ 2 and 3 where there are different terrain types (like desert, grassland etc) which are mostly at the same height.


  2. I have been playing with ProceduralTextureGenerator and cant figure out how to make a sharp transition between one texture and another. E.G. where water meets land I would like the transistion to be quite sharp so water doesnt seem to creep up steep cliffs at the waters edge.


  3. Are there plans (or is the feature there already) to animate the textures like waves on water or swishing grassland?



    Cheers,



    Chad.

You are looking in the right area.



I used the code in ProceduralTextureGenerator to generate my terrain textures based not on height but one geographic layout. I used data from another source to create the terrain textures.



You can do the same. Instead of using height, use your x,y position in your map. For each section on your grid, the entire section is one type of terrain. Do the same kind of blending that’s used for the different ‘layers’ right now. Except make the transition much more dramatic so that you have distinct lines b/t terrrain sections.



I don’t have the code where I’m at right now, but when I get home this evening I’ll go find it and post as an example. In my stuff I used a european map of vegetation types to texturize my terrain mesh(s) and used the heightmap (from satelite imagery) to create the mesh.

That sounds brilliant! The more I look at this terrain stuff the more I love it. I look forward to seeing some example code cause I’m a complete 3D noobie. Thanks guurk.

I tried to give this a go but I’m not getting it. If you could post some sample code that would be great.



Basically I created two very simple images, one for the heightmap and the other for the vegetation etc.









And here is the code I tried:



   private void complexTerrain() {
      
      // This grayscale image will be our terrain
      URL grayScale = HelloTerrain.class.getClassLoader().getResource("images/tropical.jpg");
      URL vegetation = HelloTerrain.class.getClassLoader().getResource("images/tropical_v.jpg");
      
      // These will be the textures of our terrain.
      URL waterImage = HelloTerrain.class.getClassLoader().getResource("images/water.png");
      URL dirtImage  = HelloTerrain.class.getClassLoader().getResource("images/dirt.jpg");
      URL sandImage  = HelloTerrain.class.getClassLoader().getResource("images/sand.jpg");
      URL grassImage  = HelloTerrain.class.getClassLoader().getResource("images/grass.jpg");
      URL highest    = HelloTerrain.class.getClassLoader().getResource("images/highest.jpg");
      
      // Create an image height map based on the gray scale of our image.
      ImageBasedHeightMap ib = new ImageBasedHeightMap(new ImageIcon(grayScale).getImage());
      ImageBasedHeightMap vb = new ImageBasedHeightMap(new ImageIcon(vegetation).getImage());
      
      // Create a terrain block from the image's grey scale
      TerrainBlock tb = new TerrainBlock(
         "image icon",
         ib.getSize(),
         new Vector3f(1.5f,.05f,1.5f),
         ib.getHeightMap(),
         new Vector3f(0,0,0),
         true
      );


      // Create an object to generate textured terrain from the
      // image based height map.
      ProceduralTextureGenerator pg = new ProceduralTextureGenerator(vb);
      
      
      // colour scale in vegetation image is 0-6

      pg.addTexture(new ImageIcon(waterImage),0,32,64);
      pg.addTexture(new ImageIcon(sandImage),50,70,96);
      pg.addTexture(new ImageIcon(dirtImage),80,110,156);
      pg.addTexture(new ImageIcon(grassImage),140,180,200);
      pg.addTexture(new ImageIcon(highest),180,220,255);
      
      // Tell pg to create a texture from the ImageIcon's it has recieved.
      pg.createTexture(256);
      TextureState ts = display.getRenderer().createTextureState();
      // Load the texture and assign it.
      ts.setTexture(
         TextureManager.loadTexture(
            pg.getImageIcon().getImage(),
            Texture.MM_LINEAR_LINEAR,
            Texture.FM_LINEAR,
            true,
            true
         )
      );
      tb.setRenderState(ts);
      // Give the terrain a bounding box tb.setModelBound(new BoundingBox());
      
      tb.updateModelBound();
      // Move the terrain in front of the camera
      tb.setLocalTranslation(new Vector3f(0,0,-50));
      // Attach the terrain to our rootNode.
      rootNode.attachChild(tb);
   }



I'm a bit confused about the values sent to addTexture. I figured out that a scale of 0-255 is always applicable even if you only have a few scales of gray in your image. This makes getting the right values for the right terrain difficult.

Is there a golden rule or trick I am missing?

Cheers, Chad.

I didn’t forget about you! I’m actually trying to tack down my old code… :stuck_out_tongue:



What info I can give you I will.



First, from what I understand of the result that you wish to get I don’t think that what you are wanting is to use is another ‘veg map’.



Rather it seems that you want to divide your landscape into squares, where each square in the grid is a different type of terrain; water, dirt, sand, grass, mountain…etc.



So first you need to figure out for your end texture(s) where you are within your ‘game map’.



Take a look at the two loops within ProceduralTextureGenerator.createTexture(). It’s looping through the x and y axises of the end terrain texture that’s being created.



So, if each of your grid squares is say 30 pixels by 30 pixels then you can figure out which grid square you are in as the loops pass by.



Then you’ll want a variation of the getTextureScale + interpolateHeight methods where, instead of basing the scalar on the height value at any particular location, it’s based on the poximity to the grid and the assigned terrain value of that grid.



Let’s take an example:



grids are 30x30



x = 20

y = 10



You know you are in grid 1x1 or 0x0 depending on how you number them… let’s say this grid is supposed to be sand. Now you will get the corresponding color value from your sand texture at the scaled location. So, if your sand texture is 128x128 then the rgb value you want from your source texture will be the pixel at (128 * 20/30, 128 * 10/30) in your source sand pixel.



This all works very nicely if you want very clean and distinct lines between your grids. If however, you want to gradually transision from one grid to another then you have to average the ending rgb values of two different types of land.



For this I’d allow a certain amount of overlap. You want to average based on distance from the line between grid squares, not from the center of each grid… otherwise you’d get circular paterns in your squares, and some grey areas.



In this case you’d have to check if you were within your ‘gradual’ area, say within 4 - 5 pixels of the edge of squares. If you were then you’d get the source values of the two connecting landscape textures. You’d assume that now your source pixel location is offset by the negative overlap. Then you’d average the two rgb values while wieghting the ‘stronger’ rgb with the proximity to the middle line.



Bottom line, I’d create my own version of the ProceduralTextureGenerator.



I hope this helps.

The key as Guurk mentions is you are going to have to create your own modified texture generator. The one that’s in the terrain’s sole job is to blend textures smoothly, which is exactly what you don’t want. It is also completely based on height, so certain textures are applied at certain amounts at certain heights. So, working from that class is not going to give you the results you are looking for.

Thanks for the pointers guurk, I’ve had a look at ProceduralTextureGenerator and things look straight forward enough. I have started building my own version and definately want a gradual transition between grids. I will need to look at more than one neighboring grid though to account for blending with more than one terrain type as seen in the central tile in this isometirc engine.







I hope to be posting some code within a few days - either working or severaly broken :smiley:



Have you built a similar type of generator already?

Hey this looks great! :slight_smile: Keep it up.

I noticed in examples the sizes chosen for the final texture size are always the magic numbers like 64, 256 etc. Is there some significance to this? In creating my own flavour of the procedural texture generator I will invariably have texture sizes that are not these numbers and I was wondering if there is a performance penalty or something.

Yes, there is significance in this. The dimensions of texture images (ie. height and width) should be powers of two due to issues in the rendering hardware. Others might work, but need not work correctly on all hardware. We’ve had several reports about that already in this forum. So better make sure you scale to a power of two somehow.

And if you want to support really old cards like 3dfx’s voodoo, you’ll need to make them square too, and (I think) <= 512.

Ok thanks. I will have to put a restriction that the tile map that I am generating the texture for has to be a power of two as well. That way I will always get a whole number for the size of each tile.



Now that I have a much better idea of what I want I am getting a little concerned that the heightmap approach might be too memory intensive.



For example if I have a world map of 256x256 tiles the heightmap will have to be a lot bigger than that so that I can get a decent representation for each tile. So if one of those tiles is a mountain I would want at least 32x32 to get the bumpyness etc. For plains or grassland tiles that would be overkill but I would be limited to having to have a consistent number of height vertexes per tile. In this case the hieghtmap would be a staggering 8192x8192!! I am also concerned that for a nice level of detail the final texture would also have to be ludicrously large. I have thought about generating new sections of the map on-the-fly but the procedural texture generation is quite slow and cpu intensive so I dont know how practical that is.



I will carry on regardless just to get this finished and see what the results look like - good practice anyway - but I’m thinking I may end up having to go back to hand sewing a map together to save on resources. The up side is that all this work I am doing on procedural texture generation would not be lost as I would still need to create a bunch of transition textures (to save me 400 hours in photoshop XD )

Just hit this one myself for something entirely different, you want to keep <= 256 for voodoos (and actually the relatively modern G400 on linux).

:( Personally I wouldn't bother supporting them then - too much hassle, plus there's probably not so many gamers who has got them anyway :)

256 is pretty limiting as well given the resolution of most monitors. You could handle it with a lores and hires texture pack but then you probably don’t have an army of artists at your beck and call :slight_smile: (Anyone have a free artist army they can lend me?)

Should I place all the tiles into the scenegraph or only the ones that are visible and generate and add them when the user scrolls or clicks to a different part of the map?


Your main limitiation is memory here. Those not visible will be culled, so handling invisible tiles yourself is not needed. But if you were to load all 90,000 tiles with and average of 256 vertices you'll be approaching a memory limitation (combination of partly our fault and partly Java).

If its best to add all tiles should I add all 90,000 to the root of the scenegraph or is there a better way of organising them?


Adding all to the root will be the scene graph as inefficient as possible. Earlier I mentioned organizing your tiles in a quad tree. Take four adjacent tiles (starting at a corner) and add them to a node. After you have four nodes that contain 16 adjacent tiles add those four nodes to another node, do this until you reach the root.


Even so, just looking at the vertexes storage, thats something like 60Mb which seems insane to me...


Not insane, but it is alot. You'll be running up against the default memory allocation of the JVM, so you'll need to allow the jvm to use more memory. I would give 128MB.

And if the player only sees 30x30 tiles at one time when fully zoomed out (and they all happen to be mountain tiles) thats a maximum of 92,000 vertexes on screen before any other detail (cities, units etc.)


I would create 3 detail level for each time. Except ocean since they are already flat. But you'll have your basic terrain at high detail (32 verts), medium detail (8) and low detail (4). Then you can use the Discrete Level of Detail to switch out the models as you get further away. But, guess what, this speed comes at the price of... you know it... memory. :)

Ok, other things you can do... MANY of your tiles are probably going to be the exact same, correct? If not most? Make use of the SharedMesh. This will only load they ONE tile model and just render it multiple times. This will probably save you.

I think the combination of SharedMesh, Discrete LOD and a quadtree layout will provide you with what you need. Note that SharedMesh is a new addition and you might run into issues, just let me know and they will be taken care of.



Layout of the quadtree would be like the following:



AABB

AABB

CCDD

CCDD



So, A’s would go into a node, B’s, C’s, etc. Recursively follow this until you have a single node parent (the root node).









This method will allow for very efficient culling and scene handling.



Texture animation doesn’t natively exist yet, but enough people are asking about it, that it might get higher priority. What I would suggest is applying a texture to the tile, and altering the texture coordinates during the update phase to “slide” the texture across the tile. Please note, that if you do this using a shared node the textures will slide at the same time/rate (you can have different textures though).

Thanks Mojo I think I have my recursive buildNode function almost in a working state :). I also looked on gamedev.net and found a good article on there on quadtrees and also another site with loads of applets that demostrated how they work.



I am using SharedMeshes and was wondering whats the best way to move them into the right position seeing as I create the real mesh around 0,0,0?



I’m also wondering if I can still build a quadtree with a map that is not perfectly square…

I am using SharedMeshes and was wondering whats the best way to move them into the right position seeing as I create the real mesh around 0,0,0?


Just move the ShareMeshes like you normally would. The theory is you build a shared mesh using a TriMesh and then operate on the shared mesh. So build your 5,000 shared mesh grass tiles for example, then move them where they need to be (don't do anything to the trimesh that you used to build the shared mesh).

Don't know if that makes sense. Also, if you run into issues with the SharedMesh it may not be you. It's a new feature, and might have some warts.

I'm also wondering if I can still build a quadtree with a map that is not perfectly square...


Well, you can do like a hybrid. Make it a couple quadtrees that meet up at the top level node. That is, make them quadtrees until there are no longer enough nodes, then switch to binary. Something like that.

Well I’d like to say thanks a million Mojomonk for all your help, I wouldn’t have got so far so quickly without it. I realise I have a long way to go but seeing my tiles rendering on screen all properly setup in a quadtree just makes me want to scream with excitement - but I dont want the neighbors coming round asking if everything is ok :?. I will post some screenies in the showcase forum when it actually starts to look like something but until then its onto the remaining 483 tasks on my list XD

Here is my code. I am sure I am doing silly things in places. Preliminary tests with big maps aren’t promising either with me needing to allocate 120Mb to the jvm just to draw a 128x128 map using tiles of only 4 vertices each.



The FPS on a 128x128 map drops to 17 even on my beastly machine as opposed to around 590 FPS for a 32x32 map so I am not sure if the clipping or the qaudtree is being built correctly. Do I need to be putting bounding boxes around the Nodes? I’m also not sure if I am handling the TriMeshes and SharedMeshes and thier textures, render states and bounding boxes properly.



Please let me know if you see any mistakes.


import java.net.URL;
import java.util.HashMap;
import com.jme.app.SimpleGame;
import com.jme.scene.Node;
import com.jme.scene.TriMesh;
import com.jme.scene.SharedMesh;
import com.jme.bounding.BoundingBox;
import com.jme.math.Vector3f;
import com.jme.math.Vector2f;
import com.jme.image.Image;
import com.jme.image.Texture;
import com.jme.util.TextureManager;
import com.jme.scene.state.TextureState;
import com.jme.system.JmeException;
import com.jme.input.*;

public class QuadTree extends SimpleGame {

   private final int tileSize = 1;
   private int xStart;
   private int zStart;
   int[][] testMap = new int[64][64];

   URL waterImage;
   URL grassImage;
   
   private HashMap sharedTiles = new HashMap();
   
   public static void main(String[] args) {
      QuadTree app = new QuadTree();            
      app.setDialogBehaviour(SimpleGame.ALWAYS_SHOW_PROPS_DIALOG);
      app.start();
   }
   
   public QuadTree() {
      int tilesInXDirection = testMap[0].length;
      int tilesInZDirection = testMap.length;
      
      for (int x = 0; x < tilesInXDirection; x++) {
         for (int z = 0; z < tilesInZDirection; z++) {
            testMap[z][x] = 2;
         }
      }
      
      xStart = (int) (0 - ((tileSize * tilesInXDirection) / 2));
      zStart = (int) (0 - ((tileSize * (tilesInZDirection)) / 2));   
      waterImage = QuadTree.class.getClassLoader().getResource("img/water.png");
      grassImage = QuadTree.class.getClassLoader().getResource("img/grass.jpg");
      
   }
      
   private SharedMesh generateTile(int tileX, int tileZ) {
      int tileType = testMap[tileZ][tileX];
      SharedMesh sharedMesh;
       TriMesh realMesh;

      if (sharedTiles.containsKey(String.valueOf(tileType))) {
         realMesh = (TriMesh) sharedTiles.get(String.valueOf(tileType));
      } else {
         realMesh = new TriMesh(String.valueOf(tileType));
         sharedTiles.put(String.valueOf(tileType), realMesh);         

         Vector3f[] vertexes={
            new Vector3f(0,0,0),
            new Vector3f(tileSize,0,0),
            new Vector3f(0,0,tileSize),
            new Vector3f(tileSize,0,tileSize)
         };

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

         int[] indexes = {0, 1, 2, 1, 2, 3};

         realMesh.reconstruct(vertexes, null, null, texCoords, indexes);
         
         realMesh.setModelBound(new BoundingBox());
         realMesh.updateModelBound();
               
         TextureState ts = display.getRenderer().createTextureState();
         URL useTexture;
         if (tileType == 1) {
            useTexture = waterImage;
         } else {
            useTexture = grassImage;
         }
         Texture t = TextureManager.loadTexture(useTexture, Texture.MM_LINEAR_LINEAR, Texture.FM_LINEAR, Image.GUESS_FORMAT_NO_S3TC, 1.0f, true);
         
         ts.setTexture(t);
         realMesh.setRenderState(ts);   
      }
      
      sharedMesh = new SharedMesh(String.valueOf(tileX) + "," + String.valueOf(tileZ), realMesh);
      int realX = xStart + (tileX * tileSize);
      int realZ = zStart + (tileZ * tileSize);
      sharedMesh.setLocalTranslation(new Vector3f(realX, 0, realZ));
      
      return sharedMesh;
   }
   
   private Node buildNode(int x1, int z1, int x2, int z2) {
      int Dx = x2 - x1;
      int Dz = z2 - z1;
      Node thisNode = new Node(x1 + "," + x2 + "x" + z1 + "," + z2);

      if ((Dx / 2 > 1) && (Dz / 2 > 1)) {
         // create 4 holding nodes
         thisNode.attachChild(buildNode(x1, z1, x1 + Dx / 2, z1 + Dz / 2));
         thisNode.attachChild(buildNode(x1, z1 + Dz / 2, x1 + Dx / 2, z2));
         thisNode.attachChild(buildNode(x1 + Dx / 2, z1, x2, z1 + Dz / 2));
         thisNode.attachChild(buildNode(x1 + Dx / 2, z1 + Dz / 2, x2, z2));
      } else {
         // create leaf nodes (the tiles)
         thisNode.attachChild(generateTile(x1, z1));
         thisNode.attachChild(generateTile(x1, z1 + 1));
         thisNode.attachChild(generateTile(x1 + 1, z1));
         thisNode.attachChild(generateTile(x1 + 1, z1 + 1));         

      }
      return thisNode;
   }
   
   protected void simpleInitGame() {

      rootNode.attachChild(buildNode(0, 0, testMap[0].length, testMap.length));

      lightState.setEnabled(false);
      
      try {
         cam = display.getRenderer().createCamera(display.getWidth(), display.getHeight());
      } catch (JmeException e) {
         e.printStackTrace();
         System.exit(1);
      }

      cam.setFrustumPerspective(45.0f, (float) display.getWidth() / (float) display.getHeight(), 1, 1000);
      Vector3f loc = new Vector3f(0.0f, 90.0f, 0.0f);
      Vector3f left = new Vector3f(-1.0f, 0.0f, 0.0f);
      Vector3f up = new Vector3f(0.0f, 0.0f, -1.0f);
      Vector3f dir = new Vector3f(0.0f, -1.0f, 0.0f);

      cam.setFrame(loc, left, up, dir);
      cam.update();
      display.getRenderer().setCamera(cam);
      input = new FirstPersonHandler(this, cam, properties.getRenderer());
      input.setKeySpeed(50f);
      input.setMouseSpeed(1f);
      
   }
}