[SOLVED] Need help with TriPlanar texturing on a box

Hi

For my terrain, I am using a modified version of JME Box mesh (I removed the top and bottom faces) to use it as a block wall and added a Quad on the top as a block surface.

I am using terrain material with useTriPlanarMapping = true on both block surface and wall.

My issue is with the block wall texture mapping,

this is my dirt texture:

the texture is displayed correctly on the front and back side of the box

but incorrect at right and left side:

my custom box mesh code:

public class BlockWall extends AbstractBox{
    private static final short[] GEOMETRY_INDICES_DATA = {
         2,  1,  0,  3,  2,  0, // back
         6,  5,  4,  7,  6,  4, // right
        10,  9,  8, 11, 10,  8, // front
        14, 13, 12, 15, 14, 12, // left
        //18, 17, 16, 19, 18, 16, // top
        //22, 21, 20, 23, 22, 20  // bottom
    };

    private static final float[] GEOMETRY_NORMALS_DATA = {
        0,  0, -1,  0,  0, -1,  0,  0, -1,  0,  0, -1, // back
        1,  0,  0,  1,  0,  0,  1,  0,  0,  1,  0,  0, // right
        0,  0,  1,  0,  0,  1,  0,  0,  1,  0,  0,  1, // front
       -1,  0,  0, -1,  0,  0, -1,  0,  0, -1,  0,  0, // left
        //0,  1,  0,  0,  1,  0,  0,  1,  0,  0,  1,  0, // top
        //0, -1,  0,  0, -1,  0,  0, -1,  0,  0, -1,  0  // bottom
    };

    private static final float[] GEOMETRY_TEXTURE_DATA = {
        1, 0, 0, 0, 0, 1, 1, 1, // back
        1, 0, 0, 0, 0, 1, 1, 1, // right
        1, 0, 0, 0, 0, 1, 1, 1, // front
        1, 0, 0, 0, 0, 1, 1, 1, // left
        //1, 0, 0, 0, 0, 1, 1, 1, // top
        //1, 0, 0, 0, 0, 1, 1, 1  // bottom
    };

    /**
     * Creates a new box.
     * <p>
     * The box has a center of 0,0,0 and extends in the out from the center by
     * the given amount in <em>each</em> direction. So, for example, a box
     * with extent of 0.5 would be the unit cube.
     *
     * @param x the size of the box along the x axis, in both directions.
     * @param y the size of the box along the y axis, in both directions.
     * @param z the size of the box along the z axis, in both directions.
     */
    public BlockWall(float x, float y, float z) {
        super();
        updateGeometry(Vector3f.ZERO, x, y, z);
    }


    /**
     * Constructor instantiates a new <code>Box</code> object.
     * <p>
     * The minimum and maximum point are provided, these two points define the
     * shape and size of the box but not it's orientation or position. You should
     * use the {@link com.jme3.scene.Spatial#setLocalTranslation(com.jme3.math.Vector3f) }
     * and {@link com.jme3.scene.Spatial#setLocalRotation(com.jme3.math.Quaternion) }
     * methods to define those properties.
     * 
     * @param min the minimum point that defines the box.
     * @param max the maximum point that defines the box.
     */
    public BlockWall(Vector3f min, Vector3f max) {
        super();
        updateGeometry(min, max);
    }

    /**
     * Empty constructor for serialization only. Do not use.
     */
    public BlockWall(){
        super();
    }
    
    /**
     * Creates a clone of this box.
     * <p>
     * The cloned box will have '_clone' appended to its name, but all other
     * properties will be the same as this box.
     */
    @Override
    public BlockWall clone() {
        return new BlockWall(xExtent, yExtent, zExtent);
    }

    protected void doUpdateGeometryIndices() {
        if (getBuffer(Type.Index) == null){
            setBuffer(Type.Index, 3, BufferUtils.createShortBuffer(GEOMETRY_INDICES_DATA));
        }
    }

    protected void doUpdateGeometryNormals() {
        if (getBuffer(Type.Normal) == null){
            setBuffer(Type.Normal, 3, BufferUtils.createFloatBuffer(GEOMETRY_NORMALS_DATA));
        }
    }

    protected void doUpdateGeometryTextures() {
        if (getBuffer(Type.TexCoord) == null){
            setBuffer(Type.TexCoord, 2, BufferUtils.createFloatBuffer(GEOMETRY_TEXTURE_DATA));
        }
    }

    protected void doUpdateGeometryVertices() {
        FloatBuffer fpb = BufferUtils.createVector3Buffer(24);
        Vector3f[] v = computeVertices();
        fpb.put(new float[] {
                v[0].x, v[0].y, v[0].z, v[1].x, v[1].y, v[1].z, v[2].x, v[2].y, v[2].z, v[3].x, v[3].y, v[3].z, // back
                v[1].x, v[1].y, v[1].z, v[4].x, v[4].y, v[4].z, v[6].x, v[6].y, v[6].z, v[2].x, v[2].y, v[2].z, // right
                v[4].x, v[4].y, v[4].z, v[5].x, v[5].y, v[5].z, v[7].x, v[7].y, v[7].z, v[6].x, v[6].y, v[6].z, // front
                v[5].x, v[5].y, v[5].z, v[0].x, v[0].y, v[0].z, v[3].x, v[3].y, v[3].z, v[7].x, v[7].y, v[7].z, // left
                v[2].x, v[2].y, v[2].z, v[6].x, v[6].y, v[6].z, v[7].x, v[7].y, v[7].z, v[3].x, v[3].y, v[3].z, // top
                v[0].x, v[0].y, v[0].z, v[5].x, v[5].y, v[5].z, v[4].x, v[4].y, v[4].z, v[1].x, v[1].y, v[1].z  // bottom
        });
        setBuffer(Type.Position, 3, fpb);
        updateBound();
    }
}

it’s exactly the same as JME Box, I just commented top and bottom.

I guess it can be fixed with some reordering in indexes/position/uv, but I do not have enough knowledge to fix it myself.

Will appreciate if someone can help me to fix this?

Just in case I am using this material for my terrain
https://github.com/yaRnMcDonuts/AfflictedPbrTerrainConverter/blob/master/PBRTerrain/assets/MatDefs/shaders/AfflictedPBRTerrain_33.j3md

One more screen shot from both sides:

If you look at the wire-frame you will see that the front and back triangles are arranged one way, and the sides another. I’m sure if you alter the indexes of the sides it will appear as expected.

The indexes correspond to the vertex order to make up a triangle in anti-clockwise order. Instead of going left to right, they need to up down to up.

1 Like

So I need to modify right and left indices here?

private static final short[] GEOMETRY_INDICES_DATA = {
         2,  1,  0,  3,  2,  0, // back
         6,  5,  4,  7,  6,  4, // right
        10,  9,  8, 11, 10,  8, // front
        14, 13, 12, 15, 14, 12, // left
        //18, 17, 16, 19, 18, 16, // top
        //22, 21, 20, 23, 22, 20  // bottom
    };

Do not know which number actually corresponds to which vertex position in the box :blush:
going to toy around with different orders until getting the correct result. :slightly_smiling_face:

My gl-fu is almost 0 :wink:

Yes modify the left and right lines. They will be the numbers in that line. You probably only need to swap one number from the first set of three with the other. The middle one maybe. I’m probably just confusing you :stuck_out_tongue:

1 Like

Also, if you need a box with only some sides:

(it also subdivides but if you don’t want that just set slices to 0 I think)

new MBox(0.5, 0.5, 0.5, 0, 0, 0, MBox.FRONT_MASK | MBox.BACK_MASK | MBox.LEFT_MASK | MBox.RIGHT_MASK);

…for a box with no top/bottom.

I don’t know if I oriented the sides the same way or not but it’s possible.

Edit: the code indicates that I did.

Edit 2: meaning that MBox should work “out of the box” for you… so to speak.

Before i head to bed, if i see it in my head right, the first, second and third number needs to be first, third, second. On both triangles.

Just so you know.

Okay, I play around with different orders, I also tested with MBox mesh, no difference.

I noticed when I disable TriPlanarMapping it works fine.

For my use case, I need to enable TriPlanarMapping otherwise the texture will stretch all over the block if I resize it.

Might be I should modify the texture coordinates instead of modifying indices?

@yaRnMcDonuts I am using your PBR terrain shader, probably do you have any clue what is going wrong with TriPlanarMapping?

To get sure if it is not an issue with @yaRnMcDonuts PBR terrain material, I also tried it with the stock phong TerrainLighting.j3md in JME and I noticed issue yet happens when TriPlanarMapping is enabled.

Edit:
In case I am putting a link to material here:

So it’s the tripalanar mapping not being designed for this use-case I guess.

…unless you also change the texture coordinates.

Aren’t texture coords 3D with triplanar mapping? Set the the texture coords the same as the vertex positions + localTranslation.

Edit: I havent even looked at how the triplanar material works, but usually I just use the world vertex positions anyway…

But the way it interprets the world coordinate → texture coordinate depends on the direction of the face. It’s generally arbitrary because ground plane textures aren’t normally oriented.

…but it looks like along one axis the triplanar mapping may have it’s s, t flipped.

btw, I created a simple test case if someone willing to give it a try.

public class TestBlock extends SimpleApplication {

    public static void main(String[] args) {
        TestBlock main = new TestBlock();
        AppSettings s = new AppSettings(false);
        s.setRenderer(AppSettings.LWJGL_OPENGL3);
        main.setSettings(s);
        main.start();
    }

    public TestBlock() {
        super(new FlyCamAppState());
    }

    @Override
    public void simpleInitApp() {
        initLight();
        createSampleBlock();
    }

    private void initLight() {
        DirectionalLight light = new DirectionalLight();
        light.setDirection(new Vector3f(-0.2f, -1, -0.3f).normalizeLocal());
        rootNode.addLight(light);
    }

    private void createSampleBlock() {
        Node sampleBlock = new Node();
        
        BlockWall wallMesh = new BlockWall(1, 1, 1);
        //MBox wallMesh = new MBox(1, 1, 1, 0, 0, 0, MBox.FRONT_MASK | MBox.BACK_MASK | MBox.LEFT_MASK | MBox.RIGHT_MASK);
        Geometry wallGeo = new Geometry("wall", wallMesh);
        Material wallMat = createBlockMaterial();
        // wall material
        setBlockAlbedoMap(wallMat, "textures/block/dirt/image-7.png", 1f);
        wallGeo.setMaterial(wallMat);

        sampleBlock.attachChild(wallGeo);
        rootNode.attachChild(sampleBlock);
    }

    private Material createBlockMaterial() {
        Material mat = new Material(assetManager, "Common/MatDefs/Terrain/TerrainLighting.j3md");
        mat.setBoolean("useTriPlanarMapping", true);
        mat.setTexture("AlphaMap", assetManager.loadTexture("textures/terrain/terrain-block-alphamap.png"));
        return mat;
    }

    private void setBlockAlbedoMap(Material blockMat, String albedoMap, float scale) {
        TextureKey key = new TextureKey(albedoMap);
        Texture texture = assetManager.loadTexture(key);
        texture.setWrap(Texture.WrapMode.Repeat);
        blockMat.setTexture("DiffuseMap", texture);
        blockMat.setFloat("DiffuseMap_0_scale", scale);
    }

}

code for BlockWall:

/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package otm.moona.environment.view;

import com.jme3.math.Vector3f;
import com.jme3.scene.VertexBuffer.Type;
import com.jme3.scene.shape.AbstractBox;
import com.jme3.util.BufferUtils;
import java.nio.FloatBuffer;

/**
 *
 * @author Ali-RS
 */
public class BlockWall extends AbstractBox{
    private static final short[] GEOMETRY_INDICES_DATA = {
         2,  1,  0,  3,  2,  0, // back
         6,  5,  4,  7,  6,  4, // right
        10,  9,  8, 11, 10,  8, // front
        14, 13, 12, 15, 14, 12, // left
        //18, 17, 16, 19, 18, 16, // top
        //22, 21, 20, 23, 22, 20  // bottom
    };

    private static final float[] GEOMETRY_NORMALS_DATA = {
        0,  0, -1,  0,  0, -1,  0,  0, -1,  0,  0, -1, // back
        1,  0,  0,  1,  0,  0,  1,  0,  0,  1,  0,  0, // right
        0,  0,  1,  0,  0,  1,  0,  0,  1,  0,  0,  1, // front
       -1,  0,  0, -1,  0,  0, -1,  0,  0, -1,  0,  0, // left
        //0,  1,  0,  0,  1,  0,  0,  1,  0,  0,  1,  0, // top
        //0, -1,  0,  0, -1,  0,  0, -1,  0,  0, -1,  0  // bottom
    };

    private static final float[] GEOMETRY_TEXTURE_DATA = {
        1, 0, 0, 0, 0, 1, 1, 1, // back
        0, 0, 0, 1, 1, 1, 1, 0, // right
        1, 0, 0, 0, 0, 1, 1, 1, // front
        1, 0, 0, 0, 0, 1, 1, 1, // left
        //1, 0, 0, 0, 0, 1, 1, 1, // top
        //1, 0, 0, 0, 0, 1, 1, 1  // bottom
    };

    /**
     * Creates a new box.
     * <p>
     * The box has a center of 0,0,0 and extends in the out from the center by
     * the given amount in <em>each</em> direction. So, for example, a box
     * with extent of 0.5 would be the unit cube.
     *
     * @param x the size of the box along the x axis, in both directions.
     * @param y the size of the box along the y axis, in both directions.
     * @param z the size of the box along the z axis, in both directions.
     */
    public BlockWall(float x, float y, float z) {
        super();
        updateGeometry(Vector3f.ZERO, x, y, z);
    }


    /**
     * Constructor instantiates a new <code>Box</code> object.
     * <p>
     * The minimum and maximum point are provided, these two points define the
     * shape and size of the box but not it's orientation or position. You should
     * use the {@link com.jme3.scene.Spatial#setLocalTranslation(com.jme3.math.Vector3f) }
     * and {@link com.jme3.scene.Spatial#setLocalRotation(com.jme3.math.Quaternion) }
     * methods to define those properties.
     * 
     * @param min the minimum point that defines the box.
     * @param max the maximum point that defines the box.
     */
    public BlockWall(Vector3f min, Vector3f max) {
        super();
        updateGeometry(min, max);
    }

    /**
     * Empty constructor for serialization only. Do not use.
     */
    public BlockWall(){
        super();
    }
    
    /**
     * Creates a clone of this box.
     * <p>
     * The cloned box will have '_clone' appended to its name, but all other
     * properties will be the same as this box.
     */
    @Override
    public BlockWall clone() {
        return new BlockWall(xExtent, yExtent, zExtent);
    }

    protected void doUpdateGeometryIndices() {
        if (getBuffer(Type.Index) == null){
            setBuffer(Type.Index, 3, BufferUtils.createShortBuffer(GEOMETRY_INDICES_DATA));
        }
    }

    protected void doUpdateGeometryNormals() {
        if (getBuffer(Type.Normal) == null){
            setBuffer(Type.Normal, 3, BufferUtils.createFloatBuffer(GEOMETRY_NORMALS_DATA));
        }
    }

    protected void doUpdateGeometryTextures() {
        if (getBuffer(Type.TexCoord) == null){
            setBuffer(Type.TexCoord, 2, BufferUtils.createFloatBuffer(GEOMETRY_TEXTURE_DATA));
        }
    }

    protected void doUpdateGeometryVertices() {
        FloatBuffer fpb = BufferUtils.createVector3Buffer(24);
        Vector3f[] v = computeVertices();
        fpb.put(new float[] {
                v[0].x, v[0].y, v[0].z, v[1].x, v[1].y, v[1].z, v[2].x, v[2].y, v[2].z, v[3].x, v[3].y, v[3].z, // back
                v[1].x, v[1].y, v[1].z, v[4].x, v[4].y, v[4].z, v[6].x, v[6].y, v[6].z, v[2].x, v[2].y, v[2].z, // right
                v[4].x, v[4].y, v[4].z, v[5].x, v[5].y, v[5].z, v[7].x, v[7].y, v[7].z, v[6].x, v[6].y, v[6].z, // front
                v[5].x, v[5].y, v[5].z, v[0].x, v[0].y, v[0].z, v[3].x, v[3].y, v[3].z, v[7].x, v[7].y, v[7].z, // left
                v[2].x, v[2].y, v[2].z, v[6].x, v[6].y, v[6].z, v[7].x, v[7].y, v[7].z, v[3].x, v[3].y, v[3].z, // top
                v[0].x, v[0].y, v[0].z, v[5].x, v[5].y, v[5].z, v[4].x, v[4].y, v[4].z, v[1].x, v[1].y, v[1].z  // bottom
        });
        setBuffer(Type.Position, 3, fpb);
        updateBound();
    }
}

textures/terrain/terrain-block-alphamap.png :

“textures/block/dirt/image-7.png” :

Try to replace

      vec4 col1 = texture2D( map, coords.yz * scale);

with

      vec4 col1 = texture2D( map, coords.zy * scale);

(note YZ → ZY )
in the fragment shader.

1 Like

Wow, it solved the problem. :grinning:

Thank you so much @RiccardoBlb :heart:

btw, @pspeed I switched to use Lemur MBox but I needed to add these methods:

public Vector3f getExtents() {
    return extents;
}

public void resize(Vector3f extents) {
    this.extents.set(extents);
    refreshGeometry();
}

I can make a PR if you are OK with these changes.

Yep.

Done:

@pspeed should it also implement Savable?

@Override
public void read(JmeImporter e) throws IOException {
    super.read(e);
    InputCapsule ic = e.getCapsule(this);
    extents = (Vector3f) ic.readSavable("extents", null);
    slices = ic.readIntArray("slices", null);
    sideMask = ic.readInt("sideMask", 0);
}

@Override
public void write(JmeExporter e) throws IOException {
    super.write(e);
    OutputCapsule oc = e.getCapsule(this);
    oc.write(extents, "extents", null);
    oc.write(slices, "slices", null);
    oc.write(sideMask, "sideMask", 0);
}