[SOLVED] Loading a Texture Object from an Image file in computer memory (not disk)

Hello there!
I have recently been working on random map generation using Height Maps to generate a scene. However, instead of hand-drawing the maps, I have written an algorithm that writes a BufferedImage in memory. Once the BufferedImage is fully written, it exports .png image. These are then passed to the AssetManager to produce a Texture as in Hello Terrain, etc. That all works well.

I have recently, however, wanted to try to build a dynamic landscape–one that slowly shifts between the landscapes produced by two height maps of the same size. To to this, I am thinking of splitting my one initial height map into many smaller ones, and update each individually over time with the correct replacements. However, at the current point I am unaware of any way to use AssetManager to load a BufferedImage–I have to write to disk first. Unfortunately, this takes a good deal of time (read-write disk access for one 1024 x 1024 pixel image takes .234 seconds) and would create a large amount of files on disk. This time may also be faster than what many users will experience, as I am on an SSD, not spinning disk.

My question is this: is there any way I can pass my BufferedImage (or some other Image-type object) in computer memory, not from a disk file, to AssetManager to load a scene?

In case you need, here are the relevant parts of my code. Note that ColorVector is a class I wrote that stores and allows easy modification of color data until I make a BufferedImage. I recorded the time it took to write to disk, which was .232 seconds.

//Write to disk
        System.out.println(System.currentTimeMillis());
        try 
        {
            File outputfile = new File(dir);
            absolutePath = outputfile.getAbsolutePath();
            absoluteDir = absolutePath.substring(0, absolutePath.length() - Runner.HeightMapName().length());
            ImageIO.write(bufferedImage, "png", outputfile);
        } 
        catch (IOException e) 
        {
            System.out.println("The heightmap at " + dir + "failed to load:");
            System.out.println(e.getMessage());
        }
        finally
        {
            System.out.println(System.currentTimeMillis());
        }
//Loading the generated images as textures, as in Hello Terrain. Runner is my class with main() that has references to generated images, and gui is an instance of an object I created for on-screen documentation. The time it took to load the image from disk was .02 seconds.
        gui.updateStatus("Load height map");
        assetManager.registerLocator(Runner.heightMap().absoluteDir(), FileLocator.class);

        System.out.println(System.currentTimeMillis());
        Texture heightMapImage = assetManager.loadTexture(Runner.HeightMapName());
        System.out.println(System.currentTimeMillis());

        gui.updateStatus("Loading alpha map");
        Material mat_terrain = new Material(assetManager, "Common/MatDefs/Terrain/TerrainLighting.j3md");
        mat_terrain.setTexture("AlphaMap", assetManager.loadTexture(Runner.AlphaMapName()));
        
        gui.updateStatus("Set up texturing");
        //Red  layer in alphamap
        Texture grass = assetManager.loadTexture("Textures/Terrain/splat/grass.jpg");
        grass.setWrap(WrapMode.Repeat);
        mat_terrain.setTexture("DiffuseMap", grass);
        mat_terrain.setFloat("DiffuseMap_0_scale", 64f);

        //Green layer in alphamap
        Texture dirt = assetManager.loadTexture("Textures/Terrain/splat/dirt.jpg");
        dirt.setWrap(WrapMode.Repeat);
        mat_terrain.setTexture("DiffuseMap_1", dirt);
        mat_terrain.setFloat("DiffuseMap_1_scale", 32f);

        //Blue layer in alphamap
        Texture rock = assetManager.loadTexture("Textures/Terrain/splat/road.jpg");
        rock.setWrap(WrapMode.Repeat);
        mat_terrain.setTexture("DiffuseMap_2", rock);
        mat_terrain.setFloat("DiffuseMap_2_scale", 128f);
    
        gui.updateStatus("Generate map from height image");
        AbstractHeightMap heightmap = null;
        heightmap = new ImageBasedHeightMap(heightMapImage.getImage());
        heightmap.load();

        TerrainQuad terrain;
        int patchSize = 65;
        terrain = new TerrainQuad("my terrain", patchSize, 1025, heightmap.getHeightMap());
    
        gui.updateStatus("Apply scale ratios");
        terrain.setMaterial(mat_terrain);
        terrain.setLocalTranslation(0, -305 * (COMPRESS * verticalScalar), 0);
        terrain.setLocalScale(COMPRESS, verticalScalar, COMPRESS);
        
        gui.updateStatus("Add terrain to rootNode");
        geography.attachChild(terrain);
        rootNode.attachChild(geography);

        gui.updateStatus("Calculate camera info");
        TerrainLodControl control = new TerrainLodControl(terrain, getCamera());
        terrain.addControl(control);
        
        gui.updateStatus("Load map collision shape");
        CollisionShape sceneShape = CollisionShapeFactory.createMeshShape((Node) terrain);
        RigidBodyControl mapBody = new RigidBodyControl(sceneShape, 0);
        terrain.addControl(mapBody);
        bulletAppState.getPhysicsSpace().add(mapBody);
1 Like

look into this jmonkeyengine/ImageToAwt.java at master · jMonkeyEngine/jmonkeyengine · GitHub

It converts a BufferedImage into a byteBuffer.
Then create a jme3 Image and use the setData method with the bytebuffer.
Then create a texture2D and set the image.
You’ll have to properly choose your image format but from your post RGBA8 is what you need

2 Likes

(a) Yes, it’s absolutely possible to load any asset you like from RAM. See http://javadoc.jmonkeyengine.org/com/jme3/asset/AssetManager.html#loadAssetFromStream-com.jme3.asset.AssetKey-java.io.InputStream-.

Edit: For textures, @nehon’s method will be more efficient than this.

(b) It’s very inefficient to do (a) since you’re generating the heightmaps yourself. I believe that you can do what you want by generating your heights into a float array and passing that to the RawHeightMap constructor: http://javadoc.jmonkeyengine.org/com/jme3/terrain/heightmap/RawHeightMap.html#RawHeightMap-float:A-. (Make sure you call load() after constructing the heightmap). This saves you from all the overhead of writing to a buffered image only to load it again - just generate your height values as floats in the array and hand it off to the heightmap. You can even have the heightmap scale things for you.

2 Likes

Thank you both very much!

I have a question about the method @nehon described. How do I determine how large my ByteBuffer must be when I allocate its memory? (I am using ByteBuffer byteBuffer = ByteBuffer.allocate(/* some int */) to construct the ByteBuffer).

Thanks again!

1 Like

The number of bytes your texture will use (uncompressed).
for example a 1024x1024 texture in RGBA8 is 1024 * 1024 * 4 (4 channels) * 1 (nb byte per channel).

1 Like

(humorous… well may be subjective)

Maybe you can write the image to a file, print it out, then scan it back in again, convert the PDF to a jpg with a screen capture app, then save it to disk as a jpg, read it back again as a texture, then use it as your height map…

(humorous over)

…or as danielp suggests, why even use an image at all for something that is just going to be height data? To me the round trips and flips you take are not that much different than the ones I jokingly describe above.

But if you really have your heart set on using BufferedImage, then @nehon’s approach is actually the “extra work” version. AWTLoader will even allocate the buffer and image for you.

Image img = new AWTLoader().load(bufferedImage, false);

But in the end you will probably be happiest bypassing all of the interpretation and copying necessary with this approach and just go straight from algorithm to height data without involving images.

1 Like

Thank you all!

I agree with the float array method, will definitely use the it for the height map. I only am interested in the bufferedImage-based loading because I also have alpha map generation based on my height map, and I need to conserve color data for it.

That brings me to one more question: I can pass my float array to my rawHeightMap, but when I call load() it throws a RuntimeException with the message “Must supply valid stream and size (> 0)”. I looked through the RawHeightMap source code, and can tell that the size has been assigned automatically based on my array length. (And I have verified that the float array I pass to RawHeightMap indeed has a non-zero length, 1048576). Thus, I believe the stream is the issue. Yet I cannot seem to find a proper input stream for a float array.

How should create a stream to pass to the constructor?

1 Like

Please don’t bother to mention exceptions without a stack trace.

1 Like

Why are you using input streams? The RawHeightMap class has a float array constructor. Once you generate the heights, you can just pass the array straight to the heightmap without any extra twiddling.

1 Like

He says it threw an exception that way, I guess… of course no info was included with the exception so it’s nearly useless information.

Hopefully, the important information is forthcoming.

1 Like

Oops, read through too quickly and missed that. I thought he was supplying the stream himself.

1 Like

Sorry about that!

Here is the stacktrace:

Grave: null
java.lang.RuntimeException: Must supply valid stream and size (> 0)
	at com.jme3.terrain.heightmap.RawHeightMap.load(RawHeightMap.java:149)
	at mygame.HeightMap.writeImage(HeightMap.java:193)
	at mygame.Runner.main(Runner.java:29)

From this code

        rawHeightMap = new RawHeightMap(heightData); //heightData is my float array, rawHeightMap is an instance variable
        rawHeightMap.setSize(SIZE);
        rawHeightMap.load(); //This is mygame.HeightMap.writeImage(HeightMap.java:193)
1 Like

What are you passing for size? It should be the size of ONE side of your heightmap grid (I.e., the grid of height values is size x size). Since the exception indicates a bad stream/size, the size you’re passing is a likely culprit.

1 Like

I am passing the value 1024 through the constant SIZE, which is the size of one side of the grid.
I verified this by testing that Math.sqrt(heightData.length) was equal to 1024 (this was true), and by printing SIZE, which also gave 1024.

EDIT:
When I ran rawHeightMap.setSize(1024); , as well as when I omitted that line, I got the same error from the same line of code.

1 Like

I started to say that the condition the load method is wrong but it turns out that you shouldn’t be calling load() at all if you supplied a height array already.

You also don’t need to supply the size.

Change this:

        rawHeightMap = new RawHeightMap(heightData); //heightData is my float array, rawHeightMap is an instance variable
        rawHeightMap.setSize(SIZE);
        rawHeightMap.load(); //This is mygame.HeightMap.writeImage(HeightMap.java:193)

To just this:

        rawHeightMap = new RawHeightMap(heightData); //heightData is my float array, rawHeightMap is an instance variable

I personally don’t like how this RawHeightMap class was written… it’s kind of weird voodoo to call it properly, I guess.

2 Likes

Thank you very much, that worked perfectly! :slight_smile:

1 Like

I am just writing here for anyone else who has this problem. The code to achieve the desired effect is:

        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(SIZE * SIZE * 4);
        ImageToAwt.convert(bufferedImage, Image.Format.RGBA8, byteBuffer);
        Image image = new Image();
        image.setFormat(Image.Format.RGBA8);
        image.setData(byteBuffer);
        texture2D = new Texture2D();
        texture2D.setImage(image);

You should use allocateDirect,(int bytes) not allocate(int bytes), so it is stored properly in memory (see java - ByteBuffer.allocate() vs. ByteBuffer.allocateDirect() - Stack Overflow for a detailed explanation).
Also, be sure to set the Image format in both the call to convert() and on the Image object itself.

1 Like

You should use BufferUtils :wink:

1 Like

Also, you can use

Image image = new AWTLoader().load(bufferedImage, false);
texture2D = new Texture2D();
texture2D.setImage(image);

…which will basically do exactly the same thing but is less fragile.

2 Likes