I am working on clipmap terrain with streaming chunks loaded into a texture array. There are currently 2 texture arrays, one for the heightmap chunks and one for the normal map chunks.

Chunks load in and out depending on whether they're in a specified range, and it's all working nicely for the heightmap because I'm using texelFetch and have a 1:1 ratio of vertices to pixels in each chunk, so have avoided issues with blending near edges. Now I'm trying to get normal mapping working for calculating the light intensity on the terrain, and at this point I have to use texture instead to avoid blocky shadows.

Initially (see the original question before I edited it) I had an issue with texture coordinates. I am not creating any new geometry on-the-fly, my clipmap meshes follow the camera around and both Y axis position/height and normal map texture coordinate calculation is done in the vertex shader (less costly than it would be to remap every vertex texture coordinate before shading). The problem with doing this where every "chunk" maps across the texture coordinate range (0, 0) - (1, 1) is that along the edges and in the corners along the chunk boundary there are triangles where the vertex texture coord needs to be both 0 or 1, depending on which face they're rendering.

I thought of a neat little solution to this that I'm quite proud of. The problem was caused by jumps in texture coordinates, so just flip every normal map in the X or Y direction if the respective X/Y chunk coordinate is odd, and then have the texture coordinates output by the vertex shader change direction rather than jumping, like this: Chunk texture coordinates

That works pretty well. There are no jumps visible that I can see when I colour the terrain by texture coordinates (r, g) == (s, t), just smooth gradients: Chunks coloured by texture coordinate

After regenerating the normal maps with appropriate flipping, the problem has been eliminated, but only from the X axis. In theory the Y axis should do the same thing. It's the same solution, and the normal maps seem to be oriented correctly but there's still this awful weirdness at the chunk boundary: Terrain boundary normal map issue

Terrain boundary normap map issue 2

On flat terrain in the background, all the normals sampled are the same so it's not visible, but you can see how as the clipmap resolution lowers, the bottom Y edge of each chunk is still affected (not the top edge, specifically the bottom one).

I've tried fiddling with the pixel offsets but nothing has seemed to work. I am also fairly certain the sunlight intensity calculation is correct as that's not something I changed when the X-axis chunk boundaries started working. It just seems to be sampling some really weird other location in the texture.

Here's my shader code, maybe it will illustrate what I mean more (IVS stands for intra-vertex spacing and is equal to 0.5 world units in this case):

//Vertex shader for terrain.
#include <attributes>

uniform mat4 ModelViewMatrix;
uniform mat3 NormalMatrix;
uniform mat4 MVP;

uniform float Scale;
uniform vec2 Offset;
uniform mat4 RotMatrix;
uniform float IVS;
uniform float MinHeight;
uniform float MaxHeight;
uniform int ChunkSize;
uniform int MapWidth;
uniform int MapHeight;
uniform sampler2DArray Heightfield;
uniform sampler2D LayerIndexMap;

out float Height;
out vec3 NormalMapCoords;

void main() {
    // Get the XZ world coordinates of the vertex.
    vec2 vertXZ = Offset + (RotMatrix * vec4(VertexPosition, 1.0)).xz * Scale;
    float chunkWorldSize = ChunkSize * IVS;

    // Get the chunk xy and the layer index to sample from.
    int chunkX = int(floor(vertXZ.x / chunkWorldSize));
    int chunkY = int(floor(vertXZ.y / chunkWorldSize));
    float layerIdx = texelFetch(LayerIndexMap, ivec2(chunkX, chunkY), 0).r * 256.0;

    // Scale the vertex coords to the chunk image size to get pixel XY coordinates to sample height from.
    vec2 pixelXY = vec2(
        mod(vertXZ.x, chunkWorldSize) / IVS,
        mod(vertXZ.y, chunkWorldSize) / IVS
    ); // if IVS = 0.5 and vertXZ = 511.5, then pixelXY = 1023
    float sampledHeight = texelFetch(Heightfield, ivec3(pixelXY.x, pixelXY.y, layerIdx), 0).r;

    // Multiply by 0 if we're out of range.
    sampledHeight = sampledHeight * int(chunkX >= 0 && chunkX < MapWidth && chunkY >= 0 && chunkY < MapHeight);

    // Scale sampled value by the height range.
    Height = ((MaxHeight - MinHeight) * sampledHeight) + MinHeight;
    vec3 finalPos = vec3(VertexPosition.x, Height, VertexPosition.z);

    // Set the vertex position.
    gl_Position = MVP * mat4(1.0) * vec4(finalPos, 1.0);

    // Set coordinates to sample normal map at.
    // These are slightly different from height sampling coordinates as we need to add half a pixel (in texture units)
    // to make sure we sample the centre of the pixel.
    float p = ChunkSize * 2.0;
    float tX = mod((vertXZ.x / IVS) + 0.5, p);
    float tY = mod((vertXZ.y / IVS) + 0.5, p);
    NormalMapCoords = vec3(
        2.0 * abs((tX / p) - floor((tX / p) + 0.5)), // For tX == 0, 1024, 2048... then p == 0, 1, 0...
        2.0 * abs(((tY + (p/2)) / p) - floor(((tY + (p/2)) / p) + 0.5)), // For tY == 0, 1024, 2048... then p == 1, 0, 1...

And the fragment shader:

//Fragment shader for terrain.
uniform vec3 SunVec;
uniform sampler2DArray NormalMap;
uniform int MeshType; //TODO debug
uniform int RotIndex; //TODO debug

in float Height;
in vec3 NormalMapCoords;

out vec4 fragColor;

// All components are in the range [0…1], including hue.
vec3 hsv2rgb(vec3 c)
    vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
    vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
    return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);

void main() {
    vec3 baseCol = vec3(hsv2rgb(vec3(106.0 / 255.0, 1, 1))); // Flat colour.
    //vec3 baseCol = vec3(hsv2rgb(vec3(Height / 512.0, 1, 1))); // Colour by height.
    //vec3 baseCol = vec3(hsv2rgb(vec3(NormalMapCoords.y, 1, 1))); // Colour by normal map coords.

    vec3 ambient = vec3(0.1, 0.1, 0.1);

    // Phong diffuse.
    vec2 normalXZ = texture(NormalMap, NormalMapCoords, 0).rg * 2.0 - 1.0;
    float normalY = sqrt(1.0 - (normalXZ.x * normalXZ.x) - (normalXZ.y * normalXZ.y));
    float sunlight = max(0.0, dot(normalize(vec3(normalXZ.x, normalY, normalXZ.y)), -SunVec));

    vec3 finalCol = (ambient + sunlight) * baseCol;
    fragColor = vec4(finalCol, 1.0);

You must log in to answer this question.

Browse other questions tagged .