Monday, August 27, 2018

Lucky Bioms: Lava

The lava biom is another procedural-ish background that goes on forever.  The main components are the tessellated displaced ground plane that makes up the rocky lava surface, and a second duplicate plane that uses a geometry shader for the lava bubbles.  On top of that there are some smoke particles that spawn in a large circle around the camera and some swirling ember particles.



The mesh is made up of triangulated quads that are 10x10 units square.

Terrain Mesh

These meshes and particles follow the camera on the X and Z snapping to every 10 units.  This prevents any vertex swimming that can occur when a displacement map moves along tessellated geometry.

Following Camera

The surface of the lava I started with a pretty simple terrain made in world machine.

World Machine Terrain

Simple Terrain Layout

This outputs the normal, height and sediment channels that I combines into a single texture and made tile using Materialize.  This texture was going to be viewed at a very low resolution so I added a bit of blur to the whole thing, just enough so that there's no single pixel deails.  The normals are also used for the flow direction of the lava streams and the sediment was used as a mask for the streams.

Terrain

The other textures used are a tiling rock that was made by taking some of the existing rock assets and arranging them in a tiling pattern and then rendering out the normals.  I generated an edge map, height map, and ambient occlusion map in Materialize and combined them all into a single texture.  The edge and ao are combined in the blue channel and used as an overlay for the rock color in the shader.

The tessellation is edge length based with an adjustable capping value.  This means that the mesh topology will change as the camera gets nearer and further from it, this can cause vertex swimming as the tessellating vertexes slide over the displacement map.  An easy way to hide this is to use the mip maps of the displacement textures.  This shader uses mip0 at 50 meters and lerps to mip8 at 400 meters.  There are better, mathier, ways to determine the best mip level to use but this was quick and easy and, along with some extra fog on the ground, hid most of the problems.

Rocks

The flowing lava and lava pool texture have normals in their red and green channels and value overlay in the blue, same as the rock; but have a glow mask in the alpha channel.  These 2 textures don't contribute to the displacement.  A moving displacement map would cause vertex swimming and not sampling these textures makes the vertex shader a bit cheaper.

Flowing Lava

Lava Pool

The height from the terrain is added to the height from the rocks masked by the inverse of the sediment map.  The flowing lava is masked by the sediment map, the flow direction is generated from the blurry terrain normal map, and it's intensity is scaled by the sediment map.  This makes the flow faster in the middle of a stream and slower at the edges.


Lava Flow

The lava pool is masked by a height cutoff and can be adjusted to make larger or smaller pools.


Lava Pool Height Adjust

The lava bubbles are made using a geometry shader to turn the triangles into particles.  I rendered out some flip book animations of fluid sims to create the splashes.  The alpha is changed to a distance field so the splashes will have a smoother cutoff when they get alpha clipped.


Bubble Particle 25%

Bubble Particle 100%

The displacement code needs to be copied in the bubble shader so that the bubbles will know what height they should be.  The also get masked out based on if there is lava where they spawned.  And finally, since they have access to the flow map, they get a little nudge in that direction so it looks like they are following the lava streams.


Lava Bubble Flow

An interesting, and very much undocumented at the time of this implementation, trick you can do with opaque shaders is adjust the depth that they draw.  This can help a lot when you have opaque particles that are clipping through opaque geometry.  By adjusting the depth of the particle ( screenDepth + (1.0 - alpha) * adjustAmount ) you can ensure that there will never be a hard line where the flat particle geo intersects with the world.  In Super Lucky's Tale custom surface shaders can adjust their depth by defining _DEPTH_ADJUST and passing the adjustment through the DepthAdjust parameter that is part of the custom lighting surface struct.
 
// depth adjust fragment struct
struct fragOut
{
 fixed4 color : SV_Target;
 float depth : SV_DEPTH;
};

// fragment shader
#ifdef _DEPTH_ADJUST
fragOut frag_surf (v2f_surf IN) {
#else
fixed4 frag_surf (v2f_surf IN) : SV_Target {
#endif

 #ifdef _DEPTH_ADJUST
  fragOut output;
  output.color = c; // color from the surface function
  output.depth = ( ( 1.0 / ( IN.screenPos.z + o.DepthAdjust ) ) - _ZBufferParams.w ) / _ZBufferParams.z;
  return output;
 #else
  return c;
 #endif
}
The smoke particles turned out to be far more work than they should have been.  At the time Super Lucky's Tale was going to be a vr game, and one really annoying thing that happens with billboard particles in vr games is that when you tilt your head to the side, the particles tilt with you.  I tried using vertical aligned particles but those were horizontally aligned with the camera view direction and would whip around and cut through the world unnaturally when you turned your head.  So I wrote this big awful custom particle shader just for this smoke that was vertically aligned and would point at the camera position.  These features are now standard in the particle editor so hopefully no one will ever have to deal with that.


Smoke Particle
Smoke Particle Distortion


As for the smoke pixel shader, it uses some fake lighting from the normal map and a distortion texture that gives is a swirly dissolve like the smoke from Breath of the Wild.  Drop some embers on there and it's done!

Flying Around


That's about it!  Phoenix FD was used for the fluid sims, World Machine for the terrain, and lots of little things were done with Materialize, which you can download for free here.

Monday, August 6, 2018

Lucky Bioms: Clouds

To try and make things easy for the design team, the backgrounds in Super Lucky's Tale are template scenes that go on infinitely.  The designers can then place down platforms and art without worrying too much about what is going on in the background.  These template scenes are usually a collection of assets that follow the camera around and use world space shaders and whatnot to make a procedural-ish, never ending background.  In the Lucky Bioms series I'll go over a few of these background templates and explain an bit about how they were made.


The first biom I made was the clouds.  This biom is in one of the early public demos for Super Lucky's Tale.  The main reference was the Mario Kart 8 Cloudtop Cruise race course, which looks really cool but is static.  Static in Mario Kart is not so bad since you are racing around the course and stuff is usually flying past you pretty quickly.  But for the slower pace of Lucky's Tale Things need to have a little motion all the time so a tessellated displacement shader was the way to go.



3D Tiling Worley Noise

To make puffy clouds you could use a couple of displacement maps and scroll them on top of each other but you would end up seeing the pattern and may end up looking a bit meh.  So I decided to try out 3D Worley noise.  It's expensive to calculate Worley noise in 2D in a shader so 3D would have been even worse, and with the performance budget I thought it best to store the noise in a 3D texture.  Using textures often produces softer results as opposed to calculating each pixel and since Lucky's Tale has a soft look it was even more reason to store the displacement in a texture.

Now I don't know much about Worley noise or making it tile but luckily I found this shader toy by David Hoskins which covered most of the work.  From there it was pretty simple to extend it to 3D.
 
float worley3d(float3 p) {
 float d = 100.0;
 for (int xo = -1; xo <= 1; ++xo){
  for (int yo = -1; yo <= 1; ++yo){
   for (int zo = -1; zo <= 1; ++zo){
    float3 tp = floor(p) + float3(xo, yo, zo);
    d = min( d, length( p - tp - noise3d(tp) ) );
   }
  }
 }
 return cos(d); // use cosine to get round gradient
 //return 1.0 - d;
}

The first implementation was in C# script and took about 15 seconds to generate a 32x32x32 noise texture, it was also a bit harsh on the edges where cells met.  I moved the noise generation to a compute shader and could generate a 128x128x128 noise texture with 5x5x5 super sampling instantly!  Below is a shader visualizing the noise texture in world space. As the cube is dragged around you can see how cells appear and disappear.



Cloud Shader

The noise texture is mapped to surfaces using world space coordinates and is scrolled "through" a surface rather than across it.  As the surface samples different layers of the noise, the cloud cells expand and shrink in a way that scrolling 2D textures just can't replicate.

One thing about displacing geo with a shader is that the normals aren't changed to reflect the displacement.  So to fix this, tangent normals are generated in the vertex shader by sampling the noise map a little bit in the world tangent and world bi-normal direction and using the difference to make tangent normals.  These new tangent normals are then passed through to the pixel shader where they are treated like a tangent space normal map.  The normals are not super accurate but they were nice and soft and fit well with the soft cartoony look of the game.



This is the displacement function from the cloud vertex shader.  This builds the tangent normals from the displacement map, adds the displacement values together and updates the world normals and occlusion.  The tangent normals and occlusion gets passed to the pixel shader in the vertex color.
 
void displaceStuff ( inout float3 worldPos, inout float3 worldNormal, float3 worldTangent, float3 worldBinormal, inout float3 localNormal, inout float occlusion, float tile ){

 // Main tex coords for cloud displacement
 float3 texCoords = worldPos.xyz * 0.01 * _Tiling1.xyz * tile + _Time.y * _Tiling2.xyz;
 float4 disp = tex3Dlod (_WorleyNoiseTex, float4( texCoords, 0) );

 // get the coords for the tangent and binormal displacement
 float3 texCoordsTan = texCoords + normalize( worldTangent.xyz ) * 0.05;
 float3 texCoordsBiNorm = texCoords + normalize( worldBinormal.xyz ) * 0.05;

 // sample the displacement for the tangent normal
 float4 dispTan = tex3Dlod (_WorleyNoiseTex, float4( texCoordsTan, 0) );
 float4 dispBiNorm = tex3Dlod (_WorleyNoiseTex, float4( texCoordsBiNorm, 0) );

 // get tangent normal offset from displacements
 float2 localNormOffset = float2( disp.x - dispTan.x, disp.x - dispBiNorm.x );

 // scale the normal by the one over the tiling value
 float oneOverTile = ( 1.0 / tile );
 localNormal.xy += localNormOffset * _BumpIntensity * oneOverTile;
 occlusion *= pow( disp.x, oneOverTile );

 // set up the tSpace thingy for converting tangent directions to world directions
 float3 tSpace0 = float3( worldTangent.x, worldBinormal.x, worldNormal.x );
 float3 tSpace1 = float3( worldTangent.y, worldBinormal.y, worldNormal.y );
 float3 tSpace2 = float3( worldTangent.z, worldBinormal.z, worldNormal.z );

 // update the world normal
 worldNormal.x = dot(tSpace0, localNormal);
 worldNormal.y = dot(tSpace1, localNormal);
 worldNormal.z = dot(tSpace2, localNormal);
 worldNormal = normalize( worldNormal );

 // push the world position in by the average value of the displacement map
 worldPos -= worldNormal * 0.7937 * _Displacement * oneOverTile;
 // push the world position out based on the new world normal
 worldPos += worldNormal * disp.x * _Displacement * oneOverTile;

}
The displacement happens along those new normals to make the clouds puff out more instead of displace straight up.



This unfortunately splits the mesh on texture seams so you have to hide the seems of anything in the world you want to put the shader on.



The Cloud Plane

The cloud ground plane is a round mesh made up of 1x1 meter squares.


This mesh follows the camera on the x and z with its position being snapped to the closest meter.  This ensures that while the mesh is following you you never see it pop abruptly into place because the topology doesn't change as it moves.


So obviously this mesh can't be tessellated and stretch off into the distance as that would be too expensive.  So instead the 3D Worley noise map is used in the global fog function and is sampled at the level that the ground plane is at.  Then the approximate height of the displaced cloud plane can be figured out and objects beyond the cloud plane mesh can be faded to the solid fog color where the cloud plane would have intersected them.  The Cloud plane itself fades to the solid fog color at a distance so you rarely see the edge of it.


There is another shader for background clouds that supports transmission from the sun light and has a simpler pixel shader.  This shader also has a bit of code that pushes it back towards the camera far plane to keep it from drawing in front of the world.