Per-Vertex Normals from perlin noise?
Asked Answered
C

3

11

I'm generating terrain in Opengl geometry shader and am having trouble calculating normals for lighting. I'm generating the terrain dynamically each frame with a perlin noise function implemented in the geometry shader. Because of this, I need an efficient way to calculate normals per-vertex based on the noise function (no texture or anything). I am able to take cross product of 2 side to get face normals, but they are generated dynamically with the geometry so I cannot then go back and smooth the face normals for vertex normals. How can I get vertex normals on the fly just using the noise function that generates the height of my terrain in the y plane (therefore height being between 1 and -1). I believe I have to sample the noise function 4 times for each vertex, but I tried something like the following and it didn't work...

vec3 xP1 = vertex + vec3(1.0, 0.0, 0.0);
vec3 xN1 = vertex + vec3(-1.0, 0.0, 0.0);
vec3 zP1 = vertex + vec3(0.0, 0.0, 1.0);
vec3 zN1 = vertex + vec3(0.0, 0.0, -1.0);

float sx = snoise(xP1) - snoise(xN1);
float sz = snoise(zP1) - snoise(zN1);

vec3 n = vec3(-sx, 1.0, sz);
normalize(n);

return n;

The above actually generated lighting that moved around like perlin noise! So any advice for how I can get the per-vertex normals correctly?

Crabstick answered 28/6, 2011 at 7:48 Comment(0)
G
8

You didn't say exactly how you were actually generating the positions. So I'm going to assume that you're using the Perlin noise to generate height values in a height map. So, for any position X, Y in the hieghtmap, you use a 2D noise function to generate the Z value.

So, let's assume that your position is computed as follows:

vec3 CalcPosition(in vec2 loc) {
    float height = MyNoiseFunc2D(loc);
    return vec3(loc, height);
}

This generates a 3D position. But in what space is this position in? That's the question.

Most noise functions expect loc to be two values on some particular floating-point range. How good your noise function is will determine what range you can pass values in. Now, if your model space 2D positions are not guaranteed to be within the noise function's range, then you need to transform them to that range, do the computations, and then transform it back to model space.

In so doing, you now have a 3D position. The transform for the X and Y values is simple (the reverse of the transform to the noise function's space), but what of the Z? Here, you have to apply some kind of scale to the height. The noise function will return a number on the range [0, 1), so you need to scale this range to the same model space that your X and Y values are going to. This is typically done by picking a maximum height and scaling the position appropriately. Therefore, our revised calc position looks something like this:

vec3 CalcPosition(in vec2 modelLoc, const in mat3 modelToNoise, const in mat4 noiseToModel)
{
    vec2 loc = modelToNoise * vec3(modelLoc, 1.0);
    float height = MyNoiseFunc2D(loc);
    vec4 modelPos = noiseToModel * vec4(loc, height, 1.0);
    return modelPos.xyz;
}

The two matrices transform to the noise function's space, and then transform back. Your actual code could use less complicated structures, depending on your use case, but a full affine transformation is simple to describe.

OK, now that we have established that, what you need to keep in mind is this: nothing makes sense unless you know what space it is in. Your normal, your positions, nothing matters until you establish what space it is in.

This function returns positions in model space. We need to calculate normals in model space. To do that, we need 3 positions: the current position of the vertex, and two positions that are slightly offset from the current position. The positions we get must be in model space, or our normal will not be.

Therefore, we need to have the following function:

void CalcDeltas(in vec2 modelLoc, const in mat3 modelToNoise, const in mat4 noiseToModel, out vec3 modelXOffset, out vec3 modelYOffset)
{
    vec2 loc = modelToNoise * vec3(modelLoc, 1.0);
    vec2 xOffsetLoc = loc + vec2(delta, 0.0);
    vec2 yOffsetLoc = loc + vec2(0.0, delta);
    float xOffsetHeight = MyNoiseFunc2D(xOffsetLoc);
    float yOffsetHeight = MyNoiseFunc2D(yOffsetLoc);
    modelXOffset = (noiseToModel * vec4(xOffsetLoc, xOffsetHeight, 1.0)).xyz;
    modelYOffset = (noiseToModel * vec4(yOffsetLoc, yOffsetHeight, 1.0)).xyz;
}

Obviously, you can merge these two functions into one.

The delta value is a small offset in the space of the noise texture's input. The size of this offset depends on your noise function; it needs to be big enough to return a height that is significantly different from the one returned by the actual current position. But it needs to be small enough that you aren't pulling from random parts of the noise distribution.

You should get to know your noise function.

Now that you have the three positions (the current position, the x-offset, and the y-offset) in model space, you can compute the vertex normal in model space:

vec3 modelXGrad = modelXOffset - modelPosition;
vec3 modelYGrad = modelYOffset - modelPosition;

vec3 modelNormal = normalize(cross(modelXGrad, modelYGrad));

From here, do the usual things. But never forget to keep track of the spaces of your various vectors.

Oh, and one more thing: this should be done in the vertex shader. There's no reason to do this in a geometry shader, since none of the computations affect other vertices. Let the GPU's parallelism work for you.

Grantee answered 28/6, 2011 at 8:29 Comment(4)
I thought about an approach like this, tried, and failed. After reading your answer I probably don't have everything in the correct space and it is causing errors. I'm working with vertex, geometry, and fragment shaders, so I realize I have to be extra careful about what space I keep things in. I'll try again tomorrow with a clear approach using this method and see what happens.Crabstick
I got it working with this approach. However, to get it to work I needed to make 4 offset vectors and use the gradients between the offsets (not the gradient of the offset with the modelPosition). And to get the lighting in proper direction I had to switch of the order of taking the cross product of modelXGrad and modelYGrad (just mentioning for others that come across this solution). Thanks for explaining it so well with the coordinate space...that helped. Also, I wanna mention that I was doing this in the geometry shader because I am tesselating the terrain in the geometry shader dynamicallyCrabstick
I'm too late, but I would love to understand how would you go about construyvting the modelToNoise and noiseToModel matrices for this? I mean, in glsl. I am aware of dabbling between local, world, and view spaces, but noise space? How you even create its matrix? ThanksSpiritualism
..constructing..*Spiritualism
P
13

The normal is the vector perpendicular to the tangent (also known as slope). The slope of a function is its derivative; for n dimensions its n partial derivatives. So you sample the noise around a center point P and at P ± (δx, 0) and P ± (0, δy), with δx, δy choosen to be as small as possible, but large enough for numerical stability. This yields you the tangents in each direction. Then you take the cross product of them, normalize the result and got the normal at P.

Preternatural answered 28/6, 2011 at 8:12 Comment(3)
+1: That's the right sort of approach, though I don't know how practical it is to do in a shader.Caesarea
This answer is saying the same thing as the other longer answer (basically), right? In any case I'm gonna try tomorrow and see if it works.Crabstick
@Nitrex88: @Nicol Bolas' answer is telling you the same as me, but in a more detailed and elaborate form. He's right of course that you've to keep track of which space you're in. However if we look at this from the perspective of functions, then it boils down to partial derivatives, i.e. gradients. You "wiggle" the noise sampling coordinate, and the resulting noise value wiggles accordingly. Then it's just a matter of relating the input wiggle space vectors to the output wiggle space.Preternatural
G
8

You didn't say exactly how you were actually generating the positions. So I'm going to assume that you're using the Perlin noise to generate height values in a height map. So, for any position X, Y in the hieghtmap, you use a 2D noise function to generate the Z value.

So, let's assume that your position is computed as follows:

vec3 CalcPosition(in vec2 loc) {
    float height = MyNoiseFunc2D(loc);
    return vec3(loc, height);
}

This generates a 3D position. But in what space is this position in? That's the question.

Most noise functions expect loc to be two values on some particular floating-point range. How good your noise function is will determine what range you can pass values in. Now, if your model space 2D positions are not guaranteed to be within the noise function's range, then you need to transform them to that range, do the computations, and then transform it back to model space.

In so doing, you now have a 3D position. The transform for the X and Y values is simple (the reverse of the transform to the noise function's space), but what of the Z? Here, you have to apply some kind of scale to the height. The noise function will return a number on the range [0, 1), so you need to scale this range to the same model space that your X and Y values are going to. This is typically done by picking a maximum height and scaling the position appropriately. Therefore, our revised calc position looks something like this:

vec3 CalcPosition(in vec2 modelLoc, const in mat3 modelToNoise, const in mat4 noiseToModel)
{
    vec2 loc = modelToNoise * vec3(modelLoc, 1.0);
    float height = MyNoiseFunc2D(loc);
    vec4 modelPos = noiseToModel * vec4(loc, height, 1.0);
    return modelPos.xyz;
}

The two matrices transform to the noise function's space, and then transform back. Your actual code could use less complicated structures, depending on your use case, but a full affine transformation is simple to describe.

OK, now that we have established that, what you need to keep in mind is this: nothing makes sense unless you know what space it is in. Your normal, your positions, nothing matters until you establish what space it is in.

This function returns positions in model space. We need to calculate normals in model space. To do that, we need 3 positions: the current position of the vertex, and two positions that are slightly offset from the current position. The positions we get must be in model space, or our normal will not be.

Therefore, we need to have the following function:

void CalcDeltas(in vec2 modelLoc, const in mat3 modelToNoise, const in mat4 noiseToModel, out vec3 modelXOffset, out vec3 modelYOffset)
{
    vec2 loc = modelToNoise * vec3(modelLoc, 1.0);
    vec2 xOffsetLoc = loc + vec2(delta, 0.0);
    vec2 yOffsetLoc = loc + vec2(0.0, delta);
    float xOffsetHeight = MyNoiseFunc2D(xOffsetLoc);
    float yOffsetHeight = MyNoiseFunc2D(yOffsetLoc);
    modelXOffset = (noiseToModel * vec4(xOffsetLoc, xOffsetHeight, 1.0)).xyz;
    modelYOffset = (noiseToModel * vec4(yOffsetLoc, yOffsetHeight, 1.0)).xyz;
}

Obviously, you can merge these two functions into one.

The delta value is a small offset in the space of the noise texture's input. The size of this offset depends on your noise function; it needs to be big enough to return a height that is significantly different from the one returned by the actual current position. But it needs to be small enough that you aren't pulling from random parts of the noise distribution.

You should get to know your noise function.

Now that you have the three positions (the current position, the x-offset, and the y-offset) in model space, you can compute the vertex normal in model space:

vec3 modelXGrad = modelXOffset - modelPosition;
vec3 modelYGrad = modelYOffset - modelPosition;

vec3 modelNormal = normalize(cross(modelXGrad, modelYGrad));

From here, do the usual things. But never forget to keep track of the spaces of your various vectors.

Oh, and one more thing: this should be done in the vertex shader. There's no reason to do this in a geometry shader, since none of the computations affect other vertices. Let the GPU's parallelism work for you.

Grantee answered 28/6, 2011 at 8:29 Comment(4)
I thought about an approach like this, tried, and failed. After reading your answer I probably don't have everything in the correct space and it is causing errors. I'm working with vertex, geometry, and fragment shaders, so I realize I have to be extra careful about what space I keep things in. I'll try again tomorrow with a clear approach using this method and see what happens.Crabstick
I got it working with this approach. However, to get it to work I needed to make 4 offset vectors and use the gradients between the offsets (not the gradient of the offset with the modelPosition). And to get the lighting in proper direction I had to switch of the order of taking the cross product of modelXGrad and modelYGrad (just mentioning for others that come across this solution). Thanks for explaining it so well with the coordinate space...that helped. Also, I wanna mention that I was doing this in the geometry shader because I am tesselating the terrain in the geometry shader dynamicallyCrabstick
I'm too late, but I would love to understand how would you go about construyvting the modelToNoise and noiseToModel matrices for this? I mean, in glsl. I am aware of dabbling between local, world, and view spaces, but noise space? How you even create its matrix? ThanksSpiritualism
..constructing..*Spiritualism
T
2

A link that helped me visualize and understand better this logic.

https://www.scratchapixel.com/lessons/procedural-generation-virtual-worlds/perlin-noise-part-2/perlin-noise-computing-derivatives.html

This was my implementation:

void main() {
    vec2 position = gl_FragCoord.xy;
    float point_value = perlin(position);

    // best offset for my aplication, tweek if necessary
    float delta = 0.032;

    // point a bit to the right of the original value
    vec2 position_offset_x = position + vec2(delta,0);
    // what is its perlin value
    float point_value_x = perlin(position_offset_x);
    // a vector from the point to the other one, using the perlin result
    // as the third dimension
    vec3 tangent_x = normalize(vec3(position,point_value) - vec3(position_offset_x,point_value_x));


    // same for Y
...

    // cross product of the two tangents of the point will create
    // the normal vector at that point
    vec3 norm = normalize(cross(tangent_x,tangent_y));

    // in this case, render the texture depicting the normals, in my 
    // case i used this norm to calculate how light affects this texture.
    gl_FragColor = vec4(norm,1.);
}

Results:

Texture with normals of perlin noise

Til answered 28/5, 2023 at 16:55 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.