Terrain editor Tweak / Feature request

Master Sploreg Sir!



I have a request and a question on the TerraMonkey Terrain editor.



Can you add the ability to set Specular value on a per texture basis in the terrain editor?



And the question I have is, With the nice work that @androlo has made with the vegetation system, would this also be a possibility in a future release of the terrain editor to allow editing a terrain to also propagate vegetation?





Thanks for the Already really nice groovy terrain tools! Just offering up some feedback!

It’s technically possible to do and both and they are something I really want to add. As for time, I don’t have much to make major changes like that until the summer (too busy at my day job at the moment). :confused:

The specular layers aren’t that hard to add in. The material would have to change, but I would like to improve the terrain material some more to allow for even more textures if I were to add per-layer specularity. Moving to 3d textures or texture arrays.

However, if you would like to tackle the task, I would be glad to help you out or at least steer you in the right direction until I can free up more time for myself.

@Sploreg said:
I don't have much to make major changes like that until the summer (too busy at my day job at the moment). :/


I know the feeling. And I'm patient :) Just wanted to ask!

Thanks Sploreg!

Besides drawing a texture on the terrain should remove the other textures at the same position. Otherwise normalmaps will not be shown correctly. (Or you need to change the terrain shader but Im not sure if this is a good solution because it would make the normalmap calculation more complicated… )

@ogerlord said:
Besides drawing a texture on the terrain should remove the other textures at the same position. Otherwise normalmaps will not be shown correctly. (Or you need to change the terrain shader but Im not sure if this is a good solution because it would make the normalmap calculation more complicated... )

You should erase any underlying textures there if you are painting over a surface, it is very easy to do in the editor, and much more difficult and definitely slower to do in the shader.

I have modified the PaintTerrainToolAction for my own Editor, here are the changes:



-When you draw a texture other textures at the position were removed

-Undo works (in the SDK undo has some problems)

-The toolWeight change the border of the brush (low value: smooth, high value hard)



Here is my code, maybe it helps:

[java]import com.jme3.material.MatParam;

import com.jme3.math.ColorRGBA;

import com.jme3.math.Vector2f;

import com.jme3.math.Vector3f;

import com.jme3.scene.Node;

import com.jme3.terrain.Terrain;

import com.jme3.terrain.geomipmap.TerrainQuad;

import com.jme3.texture.Image;

import com.jme3.texture.Texture;

import java.nio.ByteBuffer;

import java.util.List;

import java.util.ArrayList;



import de.jpenguin.editor.engine.Undo;



/**

  • Paint the texture at the specified location.

    *
  • @author Brent Owens and Oger-Lord

    /

    public class PaintTerrainToolAction extends Undo {



    private Vector3f worldLoc;

    private float radius;

    private float weight;

    private int selectedTextureIndex;



    List<Vector2f> undoLocs;

    List<ColorRGBA[]> undoColors;





    public PaintTerrainToolAction(Vector3f markerLocation, float radius, float weight, int selectedTextureIndex) {

    this.worldLoc = markerLocation.clone();

    this.radius = radius;

    this.weight = weight;

    this.selectedTextureIndex = selectedTextureIndex;

    // name = "Paint terrain";

    }



    // @Override

    protected Object doApplyTool() {

    Terrain terrain = null;// Terrain terrain = getTerrain(rootNode.getLookup().lookup(Node.class));

    if (terrain == null)

    return null;

    paintTexture(terrain, worldLoc, radius, weight, selectedTextureIndex);

    return terrain;

    }



    // @Override



    public void doUndoTool(Object undoObject) {

    if (undoObject == null)

    return;

    if (undoLocs == null || undoColors == null)

    return;

    resetColor((Terrain)undoObject, undoLocs, undoColors,selectedTextureIndex);

    }



    public void paintTexture(Terrain terrain, Vector3f markerLocation, float toolRadius, float toolWeight, int selectedTextureIndex) {

    if (selectedTextureIndex < 0 || markerLocation == null)

    return;



    int alphaIdx = selectedTextureIndex/4; // 4 = rgba = 4 textures



    Vector2f UV = getPointPercentagePosition(terrain, markerLocation);



    // get the radius of the brush in pixel-percent

    float brushSize = toolRadius/(terrain.getTerrainSize()
    ((Node)terrain).getLocalScale().x);

    float weightSize = Math.abs(brushSizetoolWeight);//(toolRadiustoolWeight)/(terrain.getTerrainSize()*((Node)terrain).getLocalScale().x);

    int texIndex = selectedTextureIndex - ((selectedTextureIndex/4)4); // selectedTextureIndex/4 is an int floor, do not simplify the equation

    boolean erase = toolWeight<0;

    if (erase)

    toolWeight = -1;



    doPaintAction(texIndex, terrain, alphaIdx,UV, true, brushSize, weightSize);



    }



    public Vector2f getPointPercentagePosition(Terrain terrain, Vector3f worldLoc) {

    Vector2f uv = new Vector2f(worldLoc.x,-worldLoc.z);

    float scale = ((Node)terrain).getLocalScale().x;



    uv.subtractLocal(((Node)terrain).getWorldTranslation().x
    scale, ((Node)terrain).getWorldTranslation().z
    scale); // center it on 0,0

    float scaledSize = terrain.getTerrainSize()*scale;

    uv.addLocal(scaledSize/2, scaledSize/2); // shift the bottom left corner up to 0,0

    uv.divideLocal(scaledSize); // get the location as a percentage



    return uv;

    }



    private Image[] getAlphaImages(Terrain terrain) {

    Image[] images=null;



    MatParam matParam = null;

    matParam = terrain.getMaterial(null).getParam("AlphaMap_2");

    if(matParam != null)

    {

    images = new Image[3];

    images[2] = ((Texture) matParam.getValue()).getImage();

    }



    matParam = terrain.getMaterial(null).getParam("AlphaMap_1");

    if(matParam != null)

    {

    if(images == null)

    {

    images = new Image[2];

    }

    images[1] = ((Texture) matParam.getValue()).getImage();

    }



    if(images == null)

    {

    images = new Image[1];

    }

    matParam = terrain.getMaterial(null).getParam("AlphaMap");

    images[0] = ((Texture) matParam.getValue()).getImage();



    return images;

    }



    /**
  • Goes through each pixel in the image. At each pixel it looks to see if the UV mouse coordinate is within the
  • of the brush. If it is in the brush radius, it gets the existing color from that pixel so it can add/subtract to/from it.
  • Essentially it does a radius check and adds in a fade value. It does this to the color value returned by the
  • first pixel color query.
  • Next it sets the color of that pixel. If it was within the radius, the color will change. If it was outside
  • the radius, then nothing will change, the color will be the same; but it will set it nonetheless. Not efficient.

    *
  • If the mouse is being dragged with the button down, then the dragged value should be set to true. This will reduce
  • the intensity of the brush to 10% of what it should be per spray. Otherwise it goes to 100% opacity within a few pixels.
  • This makes it work a little more realistically.

    *
  • @param image to manipulate
  • @param uv the world x,z coordinate
  • @param dragged true if the mouse button is down and it is being dragged, use to reduce brush intensity
  • @param radius in percentage so it can be translated to the image dimensions
  • @param erase true if the tool should remove the paint instead of add it
  • @param fadeFalloff the percentage of the radius when the paint begins to start fading

    /

    protected void doPaintAction(int texIndex, Terrain terrain, int alphaIdx,Vector2f uv, boolean dragged, float radius, float fadeFalloff){

    Vector2f texuv = new Vector2f();

    // ColorRGBA color = ColorRGBA.Black;



    Image[] images = getAlphaImages(terrain);





    undoLocs = new ArrayList<Vector2f>();

    undoColors = new ArrayList();

    // System.out.println(radius + " " + fadeFalloff);



    float width = images[0].getWidth();

    float height = images[0].getHeight();



    int minx = (int) Math.max(0, (uv.x
    width - radiuswidth)); // convert percents to pixels to limit how much we iterate

    int maxx = (int) Math.min(width,(uv.x
    width + radiuswidth));

    int miny = (int) Math.max(0,(uv.y
    height - radiusheight));

    int maxy = (int) Math.min(height,(uv.y
    height + radiusheight));



    float radiusSquared = radius
    radius;

    float radiusFalloff = radiusfadeFalloff;

    // go through each pixel, in the radius of the tool, in the image

    for (int y = miny; y < maxy; y++){

    for (int x = minx; x < maxx; x++){



    texuv.set((float)x / width, (float)y / height);// gets the position in percentage so it can compare with the mouse UV coordinate



    float dist = texuv.distanceSquared(uv);

    if (dist < radiusSquared ) { // if the pixel is within the distance of the radius, set a color (distance times intensity)



    ColorRGBA[] colors = new ColorRGBA[3];

    ColorRGBA[] oldcolors = new ColorRGBA[3];

    for(int i=0;i<images.length;i++)

    {

    colors = ColorRGBA.Black;

    manipulatePixel(images, x, y, colors, false); // gets the color at that location (false means don’t write to the buffer)

    oldcolors = colors.clone();

    // colors

    }



    // System.out.println(radiusSquared + " " + dist + " " + radiusFalloff);

    // calculate the fade falloff intensity

    float intensity = 0.1f;

    if (dist > radiusFalloff) {

    float dr = radius - radiusFalloff; // falloff to radius length

    float d2 = dist - radiusFalloff; // dist minus falloff

    float d3 = d2/dr; // dist percentage of falloff length

    // intensity = 1-d2; // fade out more the farther away it is

    intensity = 0.0002f/d3;



    if(intensity > 0.1)

    {

    intensity=0.1f;

    }



    }



    //if (dragged)

    // intensity = intensity
    0.1f; // magical divide it by 10 to reduce its intensity when mouse is dragged



    if(alphaIdx<images.length) //alphamap exists

    {

    switch (texIndex) {

    case 0:

    colors[alphaIdx].r += intensity;

    break;

    case 1:

    colors[alphaIdx].g += intensity;

    break;

    case 2:

    colors[alphaIdx].b += intensity;

    break;

    case 3:

    colors[alphaIdx].a += intensity;

    break;

    }

    colors[alphaIdx].clamp();

    }





    for(int i=0;i<images.length;i++)

    {

    if(alphaIdx!=i || texIndex!=0)

    colors.r *= (1-intensity);

    if(alphaIdx!=i || texIndex!=1)

    colors.g *= (1-intensity);

    if(alphaIdx!=i || texIndex!=2)

    colors.b *= (1-intensity);

    if(alphaIdx!=i || texIndex!:3)

    colors.a *= (1-intensity);

    }









    undoLocs.add(new Vector2f(x,y));

    undoColors.add(oldcolors);



    for(int i=0;i<images.length;i++)

    {

    if(oldcolors.equals(colors) == false)

    {

    manipulatePixel(images, x, y, colors, true);

    }

    }// set the new color

    }



    }

    }

    for(int i=0;i<images.length;i++)

    {

    images.getData(0).rewind();

    images.setUpdateNeeded();

    }

    }





    private void resetColor(Terrain terrain,List<Vector2f> undoLocs, List<ColorRGBA[]> undoColors, int selectedTextureIndex) {

    // List<Float> neg = new ArrayList<Float>();

    // int alphaIdx = selectedTextureIndex/4; // 4 = rgba = 4 textures

    Image[] images = getAlphaImages(terrain);



    int texIndex = selectedTextureIndex - ((selectedTextureIndex/4)*4); // selectedTextureIndex/4 is an int floor, do not simplify the equation



    for (int i=0;i<undoLocs.size();i++)

    {

    Vector2f loc = undoLocs.get(i);

    for(int a=0;a<images.length;a++)

    {

    ColorRGBA color = undoColors.get(i)[a];

    manipulatePixel(images[a], (int)loc.getX(), (int)loc.getY(), color, true);

    }

    //neg.add( intensity );

    }



    for(int a=0;a<images.length;a++)

    {

    images[a].getData(0).rewind();

    images[a].setUpdateNeeded();

    }

    // undoColor = neg;

    }





    /**
  • We are only using RGBA8 images for alpha textures right now.
  • @param image to get/set the color on
  • @param x location
  • @param y location
  • @param color color to get/set
  • @param write to write the color or not

    */

    protected void manipulatePixel(Image image, int x, int y, ColorRGBA color, boolean write){

    ByteBuffer buf = image.getData(0);

    int width = image.getWidth();



    int position = (y * width + x) * 4;



    if ( position> buf.capacity()-1 || position<0 )

    return;



    if (write) {

    switch (image.getFormat()){

    case RGBA8:

    buf.position( position );

    buf.put(float2byte(color.r))

    .put(float2byte(color.g))

    .put(float2byte(color.b))

    .put(float2byte(color.a));

    return;

    case ABGR8:

    buf.position( position );

    buf.put(float2byte(color.a))

    .put(float2byte(color.b))

    .put(float2byte(color.g))

    .put(float2byte(color.r));

    return;

    default:

    throw new UnsupportedOperationException("Image format: "+image.getFormat());

    }

    } else {

    switch (image.getFormat()){

    case RGBA8:

    buf.position( position );

    color.set(byte2float(buf.get()), byte2float(buf.get()), byte2float(buf.get()), byte2float(buf.get()));

    return;

    case ABGR8:

    buf.position( position );

    float a = byte2float(buf.get());

    float b = byte2float(buf.get());

    float g = byte2float(buf.get());

    float r = byte2float(buf.get());

    color.set(r,g,b,a);

    return;

    default:

    throw new UnsupportedOperationException("Image format: "+image.getFormat());

    }

    }



    }



    private float byte2float(byte b){

    return ((float)(b & 0xFF)) / 255f;

    }



    private byte float2byte(float f){

    return (byte) (f * 255f);

    }

    }

    [/java]
1 Like

oh excellent @ogerlord, I will look at adding these fixes in soon.

Thanks for the patch!

@lwsquad

Hi. I have been looking at this myself. There will probably be an editor plugin available at some point (there has even been some progress towards that already), but it has low priority now. When tree-impostors and saving/loading binary files are finished, it might become prioritized again. Using terrain alpha maps to define planting-zones will have to do until then.

2 Likes

@androlo

sounds great on the editor plugin! Would love to see one in the future!