Tiled terrain and normals

Hi!

I was just playing around with the default terrain sample from docs, loading a tiled (9x9) terrain I’ve made in WorldMachine (heightmaps only).

It works quite nicely, each chunk being generated in runtime, on height map load event (~150ms for the generation of each tile).

Vertices line up properly, only normals create a visible seam on border. This isn’t a bug I know, as the calculateNormals method knows only of the currently generated tile. But do you have any ideas on how to solve this?

My idea would be to either make sure that all generated tiles have equal normals at their borders OR you change the normals at the borders to be equal (say 0, 1, 0) OR for each adjacent tile you go and change the normals at the edges where the tiles meet to be the average of the two.

1 Like

Thanks @vaios for the tip.

What is the procedure to change/update normals after a mesh has been created and inserted into a model?

    var mesh = pc.createMesh(this.app.graphicsDevice, vertexData.positions, {
        normals: vertexData.normals,
        uvs: vertexData.uvs,
        indices: vertexData.indices
    });

Normals are added in the constructor, but I don’t see a way to access/update them later.

http://developer.playcanvas.com/en/api/pc.Mesh.html

You probably want to change normals before you create a mesh.

Best would be to take two matching vertices, and set average of their normal to both of them. This will give right results and then you create mesh and it will look good.

1 Like

Great this makes sense! Yes, vertices match perfectly on world space. Will do that.

@max @vaios the normals array returned is a single dimension array holding one value per index. How should I work with this? Finding each point on border and tweaking all three normal dimensions?

You need to figure out the order at which vertices are defined, and then formula of picking right indices, something like:

var vec = new pc.Vec3();
var width = 32;
for(var y = 0; y < height; y++) {
    var a = width * y; // figure out left vertex index
    var b = a + width - 1; // figure out right vertex index
    
    vec.set(normals[a * 3], normals[a * 3 + 1], normals[a * 3 + 2]);
    vec.add(normals[b * 3], normals[b * 3 + 1], normals[b * 3 + 2]);
    vec.normalize();

    normals[a * 3] = vec.x;
    normals[a * 3 + 1] = vec.y;
    normals[a * 3 + 2] = vec.z;

    normals[b * 3] = vec.x;
    normals[b * 3 + 1] = vec.y;
    normals[b * 3 + 2] = vec.z;
}

You would need to do it for horizontal sides, then for vertical, and then for 4 corners.

1 Like

Thanks @max, after figuring out the order at which vertices were defined, your code worked beautifully. Though there was no need from my visual tests to do this for the corners. Only for horiz/ver. sides.

Next, problem are texture seams between terrain tile borders (expected). Did a quick smoothstep in shader between 4 textures like this:


uniform sampler2D texture_heightMap;

uniform sampler2D texture_sandMap;
uniform sampler2D texture_grassMap;
uniform sampler2D texture_gravelMap;
uniform sampler2D texture_snowMap;

void getAlbedo() {
    
    vec4 bumpData = texture2D( texture_heightMap, $UV );
    float amount = bumpData.r;
    
    vec3 sand = (smoothstep(0.01, 0.30, amount) - smoothstep(0.28, 0.31, amount)) * texture2DSRGB( texture_sandMap, $UV * 25.0 ).$CH;
    vec3 grass = (smoothstep(0.28, 0.32, amount) - smoothstep(0.35, 0.40, amount)) * texture2DSRGB( texture_grassMap, $UV * 25.0).$CH;
    vec3 gravel = (smoothstep(0.30, 0.50, amount) - smoothstep(0.40, 0.70, amount)) * texture2DSRGB( texture_gravelMap, $UV * 25.0).$CH;
    vec3 snow = (smoothstep(0.50, 0.65, amount)) * texture2DSRGB( texture_snowMap, $UV * 25.0).$CH;
    
    dAlbedo = sand + grass + gravel + snow;
}

Thought that clamp to edge will fix that. Do we have a property like clamp to model border or similar? Or any ideas how to fix this?

Clamp to edge:

Thanks a lot!

@max @Mr_F any ideas? Sorry to bother but I can’t seem to find any solution. Most answers on the web are about setting openGL “special” clamp flags.

The only solution that seems to be working is to use a splatmap (RGBA colormap) to arrange the textures on the terrain, and set CLAMP TO EDGE to that specific splatmap.

That works, though the previous example, setting textures purely on fragment shader still creates seams.

So did you originally have 1 texture per 1 mesh? And the textures must seamlessly connect to each other?

The only way I know to fully remove any seams in such situation (done that) is to add border pixels. Suppose you have 256x256 connected tile textures. Imagine they’re all connected into a single big texture. For each tile, you crop it AND an additional border around it (say, 4 pixels) and get a 260x260 texture. Then you shrink it (with these borders) back to 256 to preserve the power of two. So in the end you should have a small overlap between adjacent tiles. Then you need to shrink the UVs correspondingly, so they don’t include the border. The border is only needed for bilinear texture filtering to take correct neighboring values, so you don’t have the seam, but instead the texture is interpolated towards adjacent tile’s pixels.
Now, all this will only work if your texture isn’t mipmapped and doesn’t use anisotropic filtering. If you want mipmapping to work, you can do the same, but use a larger border, not 4 pixels, but 8, 16, 32 etc, then it should work fine with auto-generated mipmaps to some extent (or you can waste less by generating mipmaps manually - maintaining the same border size on every level). Anisotropic filtering will also need a wider border.

1 Like

Thanks for the insight!

No, I don’t have any textures per mesh, only heightmaps from world machine. If I create a splatmap/colormap then the seams are gone. So yes, I’ve a partial solution right now.

I was aware also of this border solution, though I was trying to texture the terrain completely procedurally. Using a number of textures based on criteria like slope, angle, height. But when mixing in the shader I posted above I get these seams.

If you have 1 heightmap per mesh, same border stuff applies. If the heightmap is global and covers multiple meshes, then you shouldn’t have such seams.

Hmm, it is a global heightmap in world machine, that is tiled (using blending) on export to 24x24 heightmap images. The heightmaps are tiled perfectly (vertices line up) and only normals needed some care.

Anyhow, thanks for the help! I will go for the splatmap approach and maybe create some masks on world machine for slopes, erosions, peaks etc.