Creating Your Own 3D World Editor with JMonkeyEngine 2.0 (Part 4) – Paint Your World
At the last article, we have our terrain deformed by using the tools we have created. Then if we look back at the second part, we have prepared the terrain so it can have multiple detail texture layer with their alpha map. But we ony have one texture for our terrain now. In this article I will explain how to add more detail textures and then paint the terrain with the textures. I need to remind you this part is the hard one. It will need you to modify the existing TerrainPass to make it work. You will also need to maintain the list of texture layers which is quite complicated. As in the second article, maybe I won’t tell you much about the classes design. You can always design your classes as you want, just make sure they can be used properly.
More importantly, this article is the hard one but not that hard. It is hard for me because when I created that, I have nothing to guide or to tell me about it. I have to think and do some experiments to find how to do things I present in this article. But I’m sure it will be easier for you because I will tell you everything you need to add to make this work. If you have some difficulty following this article, feel free to ask me.
Adding New Tools
This is the easiest part of this article. We only need to add more tools to support us. For my world editor, I add a JList for showing the existing texture layer on my terrain, two buttons to add and remove texture layer, two toggle buttons for paint and erase action. I will also add two sliders for the brush radius and for the texture scale. And before I forget, for the sliders, you can always change them to textfield or spinner for more precise number or you can combine them. Do what you what to make it better. Here is my user interface
Modifying Existing Code
First of all, we need to modify the existing TerrainPass code. Because we will enable the texture editing feature of our world editor, we need the TerrainPass to be able to recalculate the texture layer, removing detail texture, and also changing the texture’s scale. The existing TerrainPass doesn’t have those functionality, or at least I can’t find them. So we have to add new methods to the TerrainPass to make it work.
First we need a method to refresh the terrain’s texture layer. The actual TerrainPass only generate the texture information once, that is the first time it is rendered. So if we make some changes to the texture, we won’t see the result immadiately unless we make the TerrainPass to regenerate the texture information. Here is the code snippet that we have to add in TerrainPass class.
We need to clear the existing passList inside TerrainPass because when TerrainPass render the scene, it will check whether the passList is empty or not. If it is empty, TerrainPass will generate the texture information.
The next method to add is to remove the detail texture layer. The actual TerrainPass has a method to add the detail, but has no method to remove it. So we have to add it. The detail texture that I talk about are not included the base texture (we add it by passing null alpha map to addDetail() method). The base texture can’t be removed cause it will create an error. I have attempted to remove base texture then make the next layer became base layer. It works but it isn’t rendered properly. So I will only remove the detail layer. Here is the code.
We also have to call refresh() at the end of addDetail() method from TerrainPass so it will refresh the texture each time a new layer is added.
The next one is to rescale the texture layer. When we add a detail layer, we can also specify the scale of the layer, that is how many times the texture will be tiled over the terrain. If we only use one scale for every layer, we will find that some textures may not appear as we want. It may to small or to big. So we need a way to rescale the layer after we add the detail. Here is the code snippet.
But it won’t work immediately. I found that at addLayer() method in SplatEnv, every time a layer is added, the scale is set to default scale. I don’t know why they make it like that. So I have to remove some lines of code.
Maybe that’s all of the modification we have to do. Now lets move to the next part.
Adding and Removing Texture Layer
This seems to be an easy task. But it can become complicated. If you look at the other applications that use more than one layer, for example Photoshop or Flash, you will find that the top layer usually drawn at the top. No other layers will cover the top layer. If we want to create that texture list with JList, we have to put the top layer at the first index at the list. But the layer list in the TerrainPass works differently. The top layer will be at the last index of the list, not the first one. So we have to maintain two different list, one at the TerrainPass and the other for the JList. Sometimes it can cause confusion. It is better to be careful when accessing the each list.
Adding a new texture layer consist of some steps. First we have to create a new image for the new alpha map. You can create it with BufferedImage and draw a fully transparent rectangle all over the new image. Then you can store all of the needed information (For me, I store the path of detail texture, the path of newly created alpha map, and the scale of the layer). Finally, you can call addDetail() method from TerrainPass. Your new texture should be visible on the terrain. The truth is you can’t actually see the texture because you make a transparent alpha map for the new texture. But you can change the alpha map transparency to see the new texture layer.
As for removing the layer, we can do the reverse version of adding new layer. We have to remove the layer information which we store when we add new layer. Optionally, we can also delete the alpha map file. Then remove the detail from TerrainPass by using the new method we create above, removeDetail(). Make sure you use correct index when adding and removing the layer.
I can’t show you any screenshot of what the result would looked like because at this point the new layers aren’t visible to out eyes. But you can check if you have properly doing this by drawing something opaque on each alpha map. Make sure you do everything alright before you move to the next part.
But before we move to the next part, maybe you can try to add more than 4 layers (this includes base layer). There’s something strange when you add the fifth layer. You will see then terrain color will change drastically. Actually it’s not the color that is changing, but the lightning. I don’t know why but the texture on the first until fourth layer don’t receive any light anymore. But the fifth layer and the next layers after that will meet no problem. This thing isn’t happening if we use lightmap (which we don’t). For now, I will assume four layers of texture are enough to make a good looking terrain.
Painting the Textures
This part works in similar way as deforming the terrain. Actually, I use the same method that we created at the previous article. I use mouseClick() method to call the painting method. I believe you still remember the method, if you have forgotten about it, try to re-read the article.
The process of painting the texture and deforming the terrain are similar. The difference is when we deform the terrain, we modify the height map value of the terrain, but when we paint the texture, we modify the alpha map transparency. But what we have is the Instances of jME Texture, not BufferedImage. So we can’t easily draw new pixels to them. Fortunately, we can obtain the texture image data in jME’s Image class format. That class has some ways to change the pixel data values.
The first statement in that method is just to get the alpha map that we want to change. So you can use your own way to get the required texture. As for the next part, we take the image data from the texture. Actually it is in the form of ByteBuffer. To navigate the pixel data, we can use position() method. Then after that, we have to put the color value one by one. put() method will put the value into the buffer position and then moving to the next byte. After that the position must be moved back to the first position.
We can call the method above from the mouseClick() method. It is just like terrain deformation. If you try that, you won’t find the difference on the terrain, the texture doesn’t change at all. This is because we only change the actual texture data. jME mainly uses LWJGL as the renderer. LWJGL maintains the textures that are rendered to the screen. So we also need to update the texture data maintained by LWJGL. We can do that by using this method.
We can call that method after we finish the painting loop. You have to remember that this method is an expensive one, so make sure you only call it if you need to update the texture.
For erasing the texture, we can use changeTexturePixel() method. Just give 0 to its value. It will create transparent pixel on the alpha map.
That’s it. With that you should be able to paint you terrain. This is the example of the painted terrain that I made. I gave them some types of sand as the texture.