Procedural Terrain Rendering How-To

Can I have the mushroom planet?

Sure.

2 Likes

lol!!

@zameran very glad to have you join ā€˜the team’! It looks like you bring some depth of experience with the intricacies of the noise funcitons - something I very much lack. My primary focus has been LOD algorithms / asynchronous data transfer mechanisms, culling routines, etc. Haven’t allowed myself to focus on much of the ā€˜artistry’ side of the house. This thread has been wonderfully inspiring for me though!

Anyway, I figured I’d give a quick update. My Unity project is coming back online, basically started from scratch now that I (somewhat) know what I’m doing with it. But in the meantime, I’ve been working in JOGL/Processing.org, which is my go-to language of choice for prototyping.

Based upon some PMs with @JoergZdarsky and some of @cybercritic’s comments, I decided to take a whack at normal mapping. Here are a few screenshots of my very preliminary results

The 2nd shot shows my terrain normals. They’re calculated analytically using the derivative of simplex noise. Unfortunately there’s a bug right now, they diverge from numerically computed normals at >3 octaves. Still trying to figure out what correction factor I’m missing, but we’re getting there. Analytic normals are way faster than the numerical ones, although you can’t use them if you have discontinuities in the noise function, such as abs().

Anyway, those normals are at each vertex point… so all of the detail in between the vertices is coming from a normal map. Currently I’m just generating one and tiling it, but all the code is in there to allow for completely ā€˜unique’ normal maps for each location on the terrain. I’m not sure if that will be feasible (it takes a relatively while to generate the normal map… many times longer than a terrain patch… but then again, the height data used to generate that normal can be re-used for finer LOD terrain patch verticies… hmm…), but will be trying it in the coming week.

Nothing special with the noise functions, just basic ridged and non-ridged fbm.

1 Like

I’m curious @NavyFish, if you want to experiment a little, don’t generate a normal map if it takes so long, make a bump map instead and calculate the normals in the frag shader. I kind of feel that you are offloading the shader a bit too much. I think of it as a pipe, a decently wide water pipe, feeding drops through it or feeding a steady stream yields the same performance.

Nice, @NavyFish! Realy cool. My progress in normal/bump mapping bad. Atm i calculate only normlamap for Quad.

Can some one explain me one thing…

If i in my noise alg. using tex2D(…,) to get data from gradient table texture for example.
… Lol. While typing this post problem solved.

So. Only one bad thing that i discovered until start using Compute shaders, is that included .cginc files in CS compiles via strange method… This cause a lot of errors, strange errors.

Eg. First error in line:

// 3D Perlin noise
float Noise(float3 p)
{
…
// Find unit cube that contains point
// Find relative x,y,z of point in cube
float3 P = fmod(floor§, 256.0) * one;
…
}

Edit: Yap, the problem in tex2D and tex2Dlod…

If i using tex2Dlod - all is good. But if some tex2D in .cginc file - fatality…

Edit: Faaa. I solved dat ā€œcubic rubicā€ space engine cell noise.

htt p://puu.sh/mitCy/71aab101ee.png

Yaaahhoooo! :sunny:

I will give that a shot. But calculating them real time - are you talking about using the hardware dx and dy function (at least that’s what OpenGL has, not sure about DX11)? Or are you talking about numerically calculating them each frame? If so… that’s the exact same calculation I’m doing, but instead of doing it once and saving the results, it’d be doing it each frame, so that doesn’t make sense. Would love to know what method you’re describing. Open to experiment.

[quote=ā€œzameran, post:190, topic:765ā€]
Yaaahhoooo! :sunny:
[/quote] Glad you fixed it… I was clueless !

I don’t really like posting raw code, but Google is a bit messy with the topic…

//get adjacent tex-coords
float2 tn = texCoord+float2(0,-_TexelSize);
float2 ts = texCoord+float2(0,_TexelSize);
float2 te = texCoord+float2(_TexelSize,0);
float2 tw = texCoord+float2(-_TexelSize,0);

float2 cc = texCoord;

//get heights				
float n = tex2D(heightSampler,tn.y < 0 ? cc : tn).r * 0.25; 
float s = tex2D(heightSampler,ts.y > 1 ? cc : ts).r * 0.25; 
float e = tex2D(heightSampler,te.x > 1 ? cc : te).r * 0.25; 
float w = tex2D(heightSampler,tw.x < 0 ? cc : tw).r * 0.25; 

//get cross				
float3 ew = normalize(float3(2*_TexelSize,e-w,0)); 
float3 ns = normalize(float3(0,s-n,2*_TexelSize)); 
float3 result = normalize(cross(ew,ns));

/edit
Stole it from here and modified it a bit.

I will release raw version of my noise engine later. Raw version will contain all branches of development, theory code, and some examples too.

About using tex2D and tex2Dlod in .cginc files… I don’t know anything about it :smile: If u can - explain some ā€œunderwater stonesā€ā€¦

About normalmapping per Quad…
I generate by (Method is pretty simple)

float left = patchPreOutput[(id.x - 1) + id.y * nVerticesPerSide].noise * 2;
float right = patchPreOutput[(id.x + 1) + id.y * nVerticesPerSide].noise * 2;
float up = patchPreOutput[id.x + (id.y - 1) * nVerticesPerSide].noise * 2;
float down = patchPreOutput[id.x + (id.y + 1) * nVerticesPerSide].noise * 2;

float xdelta = ((left - right) + 1) * 0.5;
float ydelta = ((up - down) + 1) * 0.5;
Normal[id.xy] = float4(xdelta, ydelta, 1, 0);

and result is:

128x128 RenderTexture

As you can see 1 pixel on borders is… failed… I know the reason, so i wanna to recode my prototype to output N Verts per side Quad, but all calculations, like normal mapping, erosion etc, i will do on N + 2 Verts per side (One extra vert per side, like a border, you know)

P.S Please tell me, if my English is unreadable :pensive:

Just make sure you dont get in trouble with Vladimir as (depending on what you intent to release) parts of your project might include code from Space Engine, I wouldnt put that in. And we should respect each others work.

Most of that code reworked, remastered, and can be founded on internet.
Sure i can do smth to don’t get in trouble. Sure.

Edit: Only output alg. are from se. Eg. Asteroid, selena, etc.
Edit: Anyway my own noise engine under dev. now.

@cybercritic So that’s basically the same thing I’m doing to generate my normals, except for two reasons: first, since my normals are evenly spaced along a regular grid, I don’t have to take a cross product in order to calculate the normal. Small savings, but every bit counts (particularly in the fragment shader!!). You could do the same as well, as long as your textures are being stretched homogenously. If the texture is applied non-homogenously, then adjacent tex coords will be unevenly spaced, so the approach fails. But if they’re evenly spaced, you don’t need a cross product. Try this:

//get heights				
float n = tex2D(heightSampler,tn.y < 0 ? cc : tn).r; 
float s = tex2D(heightSampler,ts.y > 1 ? cc : ts).r;
float e = tex2D(heightSampler,te.x > 1 ? cc : te).r;
float w = tex2D(heightSampler,tw.x < 0 ? cc : tw).r;

float ew = e-w;
float ns = n-s;
float3 result = normalize(ew, ns, 2*_TexelSize)

This should create a normal which, on ā€œflatā€ terrain equals (0,0,1).

Beyond that relatively small difference, we use the same approach… except that I calculate these normals just once, whereas you calculate them every frame. I don’t see the advantage to that. Basically, at the time I create the equivalent heightSampler texture, I also calculate the normals for each texel. Those are then stored in a ā€œnormalMapā€ texture using the R,G,and B components as the X,Y,Z components of the normal vector. Do this calculation once. Then, the normals calculation section of your fragment shader becomes:

float3 result = tex2D(normalMap, cc);

Perhaps I’m missing something in your code, but it looks like you could save your FS a ton of work by storing the calculated normals as a normal map.

tex2D is simply a texture lookup function. You pass it the texture (sampler2d variable) and the texture coordinates, and it returns a float4 that represents the color stored at that location in the texture. Does that help?

[quote=ā€œzameran, post:193, topic:765ā€]
About normalmapping per Quad…I generate by (Method is pretty simple)
[/quote] Looks like we use the same approach. If producing a 128x128 normal map, I actually first create a 130x130 heightfield, so that the border texels in my normal map can sample from 4 neighbors. Joerg also does the same.

Good enough! What’s your native language?

I could save 0.001 fps by modifying that code, you are really obsessively micromanaging your code @NavyFish, wait untill you get to atmospheric scattering, if you continue optimizing to that level, a hard shock is inbound.

I’m running at over 60fps and Unity is capped at 60fps, there is no point in optimizing until there is a need to optimize IMO.

/edit
Sure, it’s a good suggestion, is it needed to even think about such optimization at this time, not for me.

Hey there, I’m yet another guy working on a procedural planet, in WebGL. It’s such an addicting, yet complex hobby.

I have one question on how the normal map is computed. For me I simply create a position map, which stores the texel’s position on the sphere in the output’s R, G and B channels and then I store the height in the A channel. Afterwards I create a normal map which looks up the texel positions from the position map.

This method works, but I will probably get better precision if I just generate the normal map from a flat height map, perhaps using Jacobian matrix to derive the tangent, I saw that method in Acko’s blog.

What method do you guys use?

Thanks, I’m looking forward to having fun discussions on procedural planet rendering :slight_smile: I will share a link to my WebGL demo when I’m a little bit more than satisfied with the result.

Hey, welcome! WebGL is a fun technology. I was originally doing something quite similar, but wanted to switch over to using transform feedback to generate my data. WebGL was based upon the GLES2.0 spec at that time, which doesn’t support TF. But it looks like GLES3.0 supports TF, (and GLES3.1 supports a full Compute Shader apparently!), and WebGL2 is built upon GLES3.0. Curious how widely WebGL2 is supported… but either way, awesome… I love browser-based tech! Can’t wait to see what you’re working on.

You probably don’t need to have a ā€˜position’ map. Just figure out an equation to convert between world coordinates and the texture coordinates. For example, if the upper-left vertex of the patch has texture coordinates (0,0) and the bottom right (1,1), then your patch -> texture coordinates function is simply float2 texCoords = modulus(patchCoords.xy * textureScale, float2(1,1)); where textureScale is the ratio between texture : patch size (i.e. a textureScale value of 4 would repeat the texture 4 times across the patch). You’re likely looking for a 1:1 ratio, though, if you’re not wanting to wrap/repeat your textures.

The ā€˜resolutions’ of the patch and texture do not need to match, however. The texture could be 512x512 texels, with the patch only 32x32, for example, but as long as the patch’s texCoords range from (0,0) to (1,1), your texture should map to it correctly.

If you do it this way, you can replace the position data in your RGB channels with the normal vector data, and retain the A channel for height values (that’s actually exactly how I do it :smile: ). And by packing the height data into the A channel of the normal vector, you end up saving 2x the VRAM. This is because in GLES2.0, unpacked floats and float3s will still consume an entire float4’s worth of memory in order to remain byte-aligned.

With regard to tangent vectors (ie tangent and bi-tangent) - if you’re working on a regular grid (i.e. spacing between your x and y vertices is equal and constant across the grid), then you can use a couple of nice optimizations in calculating these vectors (to include the normal). Forget about the Jacobian! Here’s the algo:

hR = height value of the texel immediately to the right of this one.
hL = height value of the texel immediately to the left of this one. 
hU = height value  of the texel immediately above this one.
hD = height value of the texel immediately below this one. 

float dx = hR - hL;
float dy = hU - hD;

float3 tangent = normalize(float3(texelSpacing, 0, dx * 0.5));
float3 biTangent = normalize(float3(0, texelSpacing, dy * 0.5));
float3 normal = normalize(float3(dx, dy, 2*texelSpacing));

You’ll note that:

cross(tangent, biTangent) == normal;
cross(biTangent, normal) == tangent;
cross(normal, tangent) == biTangent;

As expected for orthonormalized coordinate basis. Hooray for loss-less optimizations!

Note that this produces the tan/bi-tan/normal vectors in what I like to call ā€œPatch Spaceā€ā€¦ i.e. a space whose origin is at the center of the patch (although that’s not neccessary), and which is oriented such that the z-axis (in this case, although it could be any of the cardinal axes) is normal to the patch (if it were completely flat). You’ll need to transform this patch-space into another coordinate system - such as eyeSpace - if you want to do lighting calculations, etc, but that’s a fairly typical practice. I find that having things defined in patchSpace (think of that as a type of modelSpace) as opposed to ā€˜planetSpace’ (i.e. origin at planet center, Y axis aligned with North Pole, for example) greatly simplifies multiple algorithms along the way.

Hope that helps! Feel free to ask more questions or share whatever you’d like!

edit: Another nice thing about computing and storing stuff in ā€˜patchSpace’ is that it allows you to make lots of assumptions. Here’s a useful one:

Many people will store the tan and biTan vectors for each vertex in order to speed up lighting calculations. You may wish to do so, but if memory is the bottleneck and you have spare ALU capabilities, you can cheaply back-solve for the T and B vectors, having just the N (assuming you’re in patch space). Check it out:

biTangent = cross(normal, float3(1,0,0));
tangent = cross(-normal, biTangent);

I bet you could do this without the cross products too… hmm… I’ll get back to you on that!

edit2: sure enough! no cross-products required:

float normScaler = (2*texelSpacing) / normal.x;
vec3 reconstitutedBasis = vec3(normal.x, normal.y, normal.z) * normScaler;
float dx = reconstitutedBasis.x;
float dy = reconstitutedBasis.y;
vec3 tangent = normalize(vec3(texelSpacing, 0, dx*.5);
vec3 biTangent = normalize(vec3(0, texelSpacing, dy*.5);

No idea if that’s actually cheaper than computing the cross product twice, but I’d bet it is. Also, disclaimer, this is completely untested, :wink:

edit3: perhaps just to spite cybercritic, you can remove 3 multiplications from the above code… just remove the 2* and each *.5. Course, it’s a bit less readable that way, but I aint judgin :slight_smile:

edit4: oh yes… and that tangent space basis is orthonormalized, so:

is equal to

No dealing with expensive matrix inversions! just transpose your basis!

(note you’d have to pre-multiply the Vobj position by the planet/worldSpace --> patchSpace transformation matrix first, to get Vobj into patch space. Then the above matrix will put you into tangent space (for each vertex). Do that conversion from patchSpace to tangent space in the vertex shader, and use the interpolated TBN vectors it gives you in the fragment shader)

Good enough! What’s your native language?

Russian. I’am from Crimea.

Looks like we use the same approach. If producing a 128x128 normal map, I actually first create a 130x130 heightfield, so that the border texels in my normal map can sample from 4 neighbors. Joerg also does the same.

Ya, but my method is 1:1.
I will recode all to calculate VertsPerSide + 2, but output will be VertsPerSide.
Very helpful if you gonna use normalmapping, erosion, etc.

@NavyFish Can you explain how your LOD system work, if u still using same prototype, that was explained before by you…? I have some progress, but i can’t figure out how i need change PatchCubeCenter after LODLevel : 1.

I will give a better summary tonight when I get home just leaving work now. Writing from my phone, heh. But the 4 patchCubeCenters for LOD 1 (if LOD 0 is the undivided cube face) for the zPositive face are:

-.5planetRadius, -.5planetRadius, planetRadius
-.5planetRadius, +.5planetRadius, planetRadius
+.5planetRadius, +.5planetRadius, planetRadius
+.5planetRadius, -.5planetRadius, planetRadius

Hm. I have the same, but… Ok, i will test it out, need to review my code. Thanks!

Edit:

My small addition to bank of crazy planet types. (Biome colors)

1 Like

Pretty simple. In your quadtree node class (which’s instances should be the baseline for all your planet’s plane coordinates at least in [-1|+1] space!) you just add to the constructur (which receives the four outer edge coordinates (outerVector) as parameter)

this.centerVector = Vector3.Lerp(this.outerVector1, this.outerVector4,0.5f);

Note that I use the following order, so the above code has to use different outerVectors depending on your code.

    // QuadtreeTerrain Vector coordinates (cubespace)
    public Vector3 outerVector1;  // 1 --- 2 top left vector                            [-1,1] range in cube space / not normalized  
    public Vector3 outerVector2;  // |     | top right vector                           [-1,1] range in cube space / not normalized  
    public Vector3 outerVector3;  // |     | bottom left vector                         [-1,1] range in cube space / not normalized 
    public Vector3 outerVector4;  // 3 --- 4 bottom right vector                        [-1,1] range in cube space / not normalized 
    public Vector3 centerVector;  // Central vector of the plane of this specific node. [-1,1] range in cube space / not normalized 

Thats it.

Ya, i checked.
On last attempt i used the same method, but… look at this. (For zPositive (Front quad))

ID0 - Bottom Right.
ID1 - Top Right.
ID2 - Top Left.
ID3 - Bottom Left.

LOD0 (Main Quad, ID0)
CubeFaceEastDirection (1, 0, 0)
CubeFaceNorthDirection (0, -1, 0)
PatchCubeCenter (0, 0, 1)

LOD1 (Subquad of ID0 LOD0, ID0)
CubeFaceEastDirection (0.5, 0, 0)
CubeFaceNorthDirection (0, -0.5, 0)
PatchCubeCenter (-0.5, -0.5, 1)
(All 4 quads are ok)

LOD2 (Subquad of ID0 LOD1, ID0)
CubeFaceEastDirection (0.25, 0, 0)
CubeFaceNorthDirection (0, -0.25, 0)
PatchCubeCenter (-0.75, -0.75, 1)

LOD2 (Subquad of ID0 LOD1, ID1)
CubeFaceEastDirection (0.25, 0, 0)
CubeFaceNorthDirection (0, -0.25, 0)
PatchCubeCenter (-0.75, -0.25, 1)

LOD2 (Subquad of ID0 LOD1, ID2)
CubeFaceEastDirection (0.25, 0, 0)
CubeFaceNorthDirection (0, -0.25, 0)
PatchCubeCenter (-0.25, -0.25, 1)

LOD2 (Subquad of ID0 LOD1, ID3)
CubeFaceEastDirection (0.25, 0, 0)
CubeFaceNorthDirection (0, -0.25, 0)
PatchCubeCenter (-0.25, -0.75, 1)

So. CubeFaceEastDirection and CubeFaceNorthDirection are ok, cuz calculation is pretty simple:

Vector3 cfed = Parent.quadGenerationConstants.cubeFaceEastDirection / 2;
Vector3 cfnd = Parent.quadGenerationConstants.cubeFaceNorthDirection / 2;

But as LOD level increases i need to shift positions depend on how far from QuadCenter(of LOD0)
these quad is. You can see that on my positions, described in this post before. How?

I think screenshot will be helpful.