Computing smooth normals for procedually generated terrain

Started by
18 comments, last by _WeirdCat_ 8 years, 3 months ago

Hey guys,

I recently implemented a way of procedurally generating my Terrain by following this Tutorial: https://rendermeapangolin.wordpress.com/2015/05/26/screen-space-grid/

This works quite well. If you have a look a the screenshot at the very end of this Tutorial, you can see, that the shading is some sort of flat shading. I would like to achieve a smoother shading. I tried to compute per-vertex normals within the vertex shader by averaging the normals of all faces that contain the vertex. This creates a smoother shading, however either I still have mistakes in my computation or it doesn't work so well with the Screen Space grid, because The normals have no.. "smooth transition" as I move my camera, but rather change their values drastically.

Currently I do the following:

//----------------------------------------------------------------

// Sample the heightmap(2048^2)

float height[5];
height[0] = texture( myTextureSampler, finalPos.xz + vec2(-1.0/2048.0, 0.0)).r;
height[1] = texture( myTextureSampler, finalPos.xz + vec2(0.0, -1.0/2048.0)).r;
height[2] = texture( myTextureSampler, finalPos.xz + vec2(0.0, 0.0)).r;
height[3] = texture( myTextureSampler, finalPos.xz + vec2(1.0/2048.0, 0.0)).r;
height[4] = texture( myTextureSampler, finalPos.xz + vec2(0.0, 1.0/2048.0)).r;

vec3 n[4];
vec3 l0;
vec3 l1;

// Bottom left Triangle
l0 = vec3(finalPos.x - 1.0/2048.0, height[0], finalPos.z) - vec3(finalPos.x, height[2], finalPos.z);
l1 = vec3(finalPos.x, height[1], finalPos.z - 1.0/2048.0) - vec3(finalPos.x, height[2], finalPos.z);
n[0] = normalize(cross(l0,l1));
if(n[0].y <0)
n[0] = -n[0];

// Upper right Triangle
l0 = vec3(finalPos.x + 1.0/2048.0, height[3], finalPos.z) - vec3(finalPos.x, height[2], finalPos.z);
l1 = vec3(finalPos.x, height[4], finalPos.z + 1.0/2048.0) - vec3(finalPos.x, height[2], finalPos.z);
n[1] = normalize(cross(l0,l1));
if(n[1].y <0)
n[1] = -n[1];

// Upper left Triangle
l0 = vec3(finalPos.x - 1.0/2048.0, height[0], finalPos.z) - vec3(finalPos.x, height[2], finalPos.z);
l1 = vec3(finalPos.x, height[4], finalPos.z + 1.0/2048.0) - vec3(finalPos.x, height[2], finalPos.z);
n[2] = normalize(cross(l0,l1));
if(n[2].y <0)
n[2] = -n[2];

// Bottom right Triangle
l0 = vec3(finalPos.x + 1.0/2048.0, height[3], finalPos.z) - vec3(finalPos.x, height[2], finalPos.z);
l1 = vec3(finalPos.x, height[1], finalPos.z - 1.0/2048.0) - vec3(finalPos.x, height[2], finalPos.z);
n[3] = normalize(cross(l0,l1));
if(n[3].y <0)
n[3] = -n[3];

// Average normal
oNormal = normalize((n[0] + n[1] + n[2] + n[3])/4);

//---------------------------------------------------------------------------------

The shader is certainly smooth now, but the normals have no smooth transition to one another. I suppose, that happens, because the vertices are more dense near the camera than further away and have not that uniform 1.0/2048.0 distance? Any Ideas, how I can realize a completely smooth shading, while procedurally generating Terrain as explained within the Tutorial?

Thank you very much!

Xardes

Advertisement

Screen shots would be informative, to illustrate the exact issue you are experiencing.

In the absence of visuals, my best guess would be that you are experiencing the issue explained towards the end of the tutorial, namely that you need to manually implement mipmapping using textureGrad().

Tristam MacDonald. Ex-BigTech Software Engineer. Future farmer. [https://trist.am]

Screen shots would be informative, to illustrate the exact issue you are experiencing.

In the absence of visuals, my best guess would be that you are experiencing the issue explained towards the end of the tutorial, namely that you need to manually implement mipmapping using textureGrad().

Tristam MacDonald. Ex-BigTech Software Engineer. Future farmer. [https://trist.am]

Screenshots don't add much information, as static images don't really demonstrate the problem. See this Video instead:

Please compare the lighting. The lighting of the leafs is smooth. The Terrain not. Its not flat shading, but the "transition of normals" isn't smooth. Still it looks better than before. I would like to achieve lighting that is as smooth as the lighting of the leafs.

Right now, I do the following (Changed since creating this thread):

1. Compute the world Space pos (ray tracing interception with xz plane) of the Screen Space vertex to proccess.

2. Compute the world space pos (ray tracing interception with xz plane) of the 4 Adjacent vertices (x/y +1/0, -1/0, 0/+1, 0/-1).

3. Compute the 4 world space triangle normals.

4. Compute the vertex average

Code:

vec3 finalPos[5];
vec3 camera_dir = normalize( ( inverse(projectionMatrix) * vec4(aPosition.x, aPosition.z, 1.0, 1.0) ).xyz );
vec3 world_dir = normalize( inverse(modelViewMatrix) * vec4(camera_dir, 0) ).xyz;
float t = camPos.y / -world_dir.y;
finalPos[0] = camPos + t*world_dir;

// 256x256 is the screen Space grid resolution
camera_dir = normalize( ( inverse(projectionMatrix) * vec4(aPosition.x + 1.0/256.0, aPosition.z, 1.0, 1.0) ).xyz );
world_dir = normalize( inverse(modelViewMatrix) * vec4(camera_dir, 0) ).xyz;
t = camPos.y / -world_dir.y;
finalPos[1] = camPos + t*world_dir;

camera_dir = normalize( ( inverse(projectionMatrix) * vec4(aPosition.x - 1.0/256.0, aPosition.z, 1.0, 1.0) ).xyz );
world_dir = normalize( inverse(modelViewMatrix) * vec4(camera_dir, 0) ).xyz;
t = camPos.y / -world_dir.y;
finalPos[2] = camPos + t*world_dir;

camera_dir = normalize( ( inverse(projectionMatrix) * vec4(aPosition.x, aPosition.z + 1.0/256.0, 1.0, 1.0) ).xyz );
world_dir = normalize( inverse(modelViewMatrix) * vec4(camera_dir, 0) ).xyz;
t = camPos.y / -world_dir.y;
finalPos[3] = camPos + t*world_dir;

camera_dir = normalize( ( inverse(projectionMatrix) * vec4(aPosition.x, aPosition.z - 1.0/256.0, 1.0, 1.0) ).xyz );
world_dir = normalize( inverse(modelViewMatrix) * vec4(camera_dir, 0) ).xyz;
t = camPos.y / -world_dir.y;
finalPos[4] = camPos + t*world_dir;

float height[5];
height[0] = 8.0 * texture( myTextureSampler, finalPos[0].xz / 128.0 ).r;
height[1] = 8.0 * texture( myTextureSampler, finalPos[1].xz / 128.0 ).r;
height[2] = 8.0 * texture( myTextureSampler, finalPos[2].xz / 128.0 ).r;
height[3] = 8.0 * texture( myTextureSampler, finalPos[3].xz / 128.0 ).r;
height[4] = 8.0 * texture( myTextureSampler, finalPos[4].xz / 128.0 ).r;

float sx = height[1] - height[2];
float sz = height[3] - height[4];

oPos = vec3(finalPos[0].x, height[0], finalPos[0].z);

// This is passed to the fragment shader!
oNormal = normalize(vec3(sx,2.0/255.0,sz));

Any Ideas, how I can get it more smooth/what I am doing wrong?

Looks like a sampling problem; try mipmapping that texture you're sampling.

RIP GameDev.net: launched 2 unusably-broken forum engines in as many years, and now has ceased operating as a forum at all, happy to remain naught but an advertising platform with an attached social media presense, headed by a staff who by their own admission have no idea what their userbase wants or expects.Here's to the good times; shame they exist in the past.
Its hard to tell because of the complex colours on your terrain (rendering it with just light, and no colour would be helpful). However, two questions spring to mind: are you using textureGrad() to sample the mimaps in your texture data? and are you snapping the sampling points to the nearest texel center? (this is usually done in shadow mapping to prevent the shadows from 'swimming')

Tristam MacDonald. Ex-BigTech Software Engineer. Future farmer. [https://trist.am]

Alright another video:

The color of the fragments in the video is as follows:

vec3 green = vec3(0.0, 0.75, 0.0);
vec3 grey = vec3(0.2, 0.2, 0.2);

vec3 col = mix(green, grey, oPos.y*0.1);

// Final fragment color

oFragColor = vec4(col, 1.0);

That means the color is more green in low areas and less green in higher ares. As you can see, the 3D world position generated by the heightmap is continuous (displayed to the limit of RGB space of course). Therefore the sampling of the heightmap is done correctly, is it not? I mean, if I just compute the normals correctly out of these world space coordinates, I should get smooth lighting, right?

If you want I will add another video with just diffuse lighting, I just wanted to make sure, everything is fine until here.

Thanks again.

That looks fine, heightmap makes sense. Let's see just diffuse lighting?

Tristam MacDonald. Ex-BigTech Software Engineer. Future farmer. [https://trist.am]

i had the same problem, calculation of normalmaps in shader didin't work at all, i had to move that calculation to cpu and then uploaded that to vertexbuffer or texture, then shader could interpolate properly

edit: i found more readable code i think



vec3 getvertNormal(int x, int y, float dome_sizeX, float dome_sizeZ)
{
int i = x;
int j = y;
int	  jMIN = j - 1;
int	  jMAX = j + 1;
int	  iMIN = i - 1;
int	  iMAX = i + 1;

if ( i-1 < 0 ) iMIN = TerraDim-1;
if ( j-1 < 0 ) jMIN = TerraDim-1;
if ( i > TerraDim-1 ) iMAX = 0;
if ( j > TerraDim-1 ) jMAX = 0;
float transX = dome_sizeX*(1.0/float(TerraDim));
float transZ = dome_sizeZ*(1.0/float(TerraDim));

vec3 vertex_position = vec3(float(x)*transX, 0.0, float(y)*transZ);
//terrain[y*TerraDim + x].v
//hpdata[y*TerraDim*4 + x*4 + 0] = Byte(tmp * 255.0);
float h2 = float(hpdata[ j*TerraDim * 4 + iMAX * 4 + 0 ])/255.0;
float h3 = float(hpdata[ j*TerraDim * 4 + iMIN * 4 + 0 ])/255.0;
float h4 = float(hpdata[ jMAX*TerraDim * 4 + i * 4 + 0 ])/255.0;
float h5 = float(hpdata[ jMIN*TerraDim * 4 + i * 4 + 0 ])/255.0;

float MIN_HEIGHT = vertex_position.y;
float dst = HEIGHT_SCALE;

vec3 minxy = vertex_position + vec3(-transX,MIN_HEIGHT+dst*h3, 0.0);
vec3 xmaxy = vertex_position + vec3(0.0, MIN_HEIGHT+dst*h4, transZ);
vec3 maxxy = vertex_position + vec3(transX, MIN_HEIGHT+dst*h2, 0.0);
vec3 xminy = vertex_position + vec3(0.0, MIN_HEIGHT+dst*h5, -transZ);


vec3 n1 = Normal(vertex_position, minxy, xmaxy, false);
vec3 n2 = Normal(vertex_position, xmaxy, maxxy, false);
vec3 n3 = Normal(vertex_position, maxxy, xminy, false);
vec3 n4 = Normal(vertex_position, xminy, minxy, false);
return Normalize( (n1+n2+n3+n4)/4.0 );

}

void GenerateNormals()
{
	GetAABB();

for (int y=0; y < TerraDim; y++)
for (int x=0; x < TerraDim; x++)
{
normals[y*TerraDim + x] = getvertNormal(x, y,absnf( max.x-min.x),absnf(max.z-min.z));
terrain[y*TerraDim + x].n = normals[y*TerraDim + x];
}
SendToGPU();
}

original code


OCEAN_GRID_SIZE is the terrain size
SIM_GRID_SIZE is the dimension of a grid
NormalMapVertexTranslation = float(OCEAN_GRID_SIZE)*(1.0/float(SIM_GRID_SIZE));
vec3 CalculateNormalMap(int x, int y)
{
int minx = x - 1;
int miny = y - 1;
int maxx = x + 1;
int maxy = y + 1;

if (minx < 0) minx = SIM_GRID_SIZE-1;
if (miny < 0) miny = SIM_GRID_SIZE-1;

if (maxx > SIM_GRID_SIZE-1) maxx = 0;
if (maxy > SIM_GRID_SIZE-1) maxy = 0;

vec3 p = AOS[ ArrI(x,y) ].v; //get the position of the vertex (x, z)
p.y = 0.0; //this is actual height i fetch that after calculating heights for 4 connected verts (up down left right - may wrote that in wrong order)

vec3 minxy = p + vec3(-NormalMapVertexTranslation,heightmap[ ArrI(minx, y) ] / NORMAL_SCALE, 0.0);
vec3 xmaxy = p + vec3(0.0, heightmap[ ArrI(x, maxy) ] / NORMAL_SCALE, NormalMapVertexTranslation);
vec3 maxxy = p + vec3(NormalMapVertexTranslation, heightmap[ ArrI(maxx, y) ] / NORMAL_SCALE, 0.0);
vec3 xminy = p + vec3(0.0, heightmap[ ArrI(x, miny) ] / NORMAL_SCALE, -NormalMapVertexTranslation);

p.y = heightmap[ ArrI(x, y) ] / NORMAL_SCALE; //now apply height to p

//calc all normals - false here means do not normalize.
vec3 n1 = Normal(p, minxy, xmaxy, false);
vec3 n2 = Normal(p, xmaxy, maxxy, false);
vec3 n3 = Normal(p, maxxy, xminy, false);
vec3 n4 = Normal(p, xminy, minxy, false);

//and get average of it
return Normalize( (n1+n2+n3+n4)/4.0 );
}

Alright. 2 Videos this time.

The first shows diffuse lighting, when I compute the normals in the fragment shader in exactly the same way it is done in the tutorial (but with 0.25 ambient light):

vec3 normal = normalize(cross( dFdx(oPos.xyz), dFdy(oPos.xyz)));

float diffuse = max(dot(normalize(normal),normalize(vec3(0.0,15.0,15.0))),0.25);

Video:

The second one compute the normals in vertex shader and only the lighting in fragment shader with the code posted earlier.

Video:

@WiredCat I don't get, why this wouldn't work in shader.. But I guess computing an appropriate normal map would be an elegant work around. Also saves all the computation in the shader. If I do it via CPU over multiple frames once the player reaches the end of the current area, that shouldn't be too difficult. Thank you, I will try that. o/

This topic is closed to new replies.

Advertisement