2017年10月20日 星期五

Tessellation Displacement & Tri Planar Mapping in Unity

Yo, I'm going to talk about tessellation displacement and tri planar mapping.


Fig 1. Terrain generated on GPU.

It's a kind of method which generates complex terrains on GPU (also known as Procedural Terrains).

Given a low-poly mesh and height map, we can do displacement mapping on original mesh on GPU.
With this, we can reduce CPU overhead on Input-Assembler Stage in render pipeline. But we can still get a decent terrain we want.

Displacement

Pretty simple, we use a height map which stores the intensity of displacement.
Apply it to our mesh and translate vertices along with its normal direction.

Fig 2. Displacement mapping with a height map.


// Displacement Code
// Since we are using height map on vertex-stage or domain stage(talk about this later)
// we can't use tex2D() or Sample().
// So we call tex2Dlod here (or you can use Load())
float3 Displacement(float3 vNormal, float2 uv, float heightScale)
{
float4 heightData = tex2Dlod(_HeightTex, float4(uv, 0, 0));
heightData.xyz = normalize(heightData) * vNormal * heightScale;

return heightData;
}
i.PosL += Displacement(i.NormalL, TRANSFORM_TEX(i.TexC, _HeightTex), _HeightMapScale);

We calculate displacement mapping on local space.
You can also calculate it on world space if you want.

But here comes a problem: Normal vector is wrong after displacement mapping.
We've changed position of vertices, but normal vectors are still the same as original.
To solve this problem, we copy height map and import it as normal map. (You can use other tools for converting height map to normal map of course.)
Unity will calculate normal vectors automatically. And we can put it into our shader.

Fig 3. Import height map as normal map.

With this normal map, we can adjust our vertex normal vectors and make it fits the vertices which are done with displacement mapping.

// since we displacement the geometry, we need to adjust normal
// sample new normal from height normal map
// calculate new tangent by using gram schmidt process
float3 normalHeight = UnpackNormal(tex2Dlod(_HeightBumpTex, float4(o.TexHeight, 0, 0))).xyz;
normalHeight = Vec3TsToWsNormalized(normalHeight, o.NormalW, o.TangentW, o.BinormalW);
o.TangentW.xyz = normalize(o.TangentW.xyz - (normalHeight* dot(o.TangentW.xyz, normalHeight)));
o.BinormalW = cross(normalHeight, o.TangentW) * o.TangentW.w;
o.NormalW = normalHeight;

Note that this sampling isn't for bump mapping. This is for vertex normal vectors adjustment.
In other words, we calculate normal mapping twice now. You need to do bump mapping with these new vectors to get correct lighting.

Tessellation

Terrain in Fig 2 is still not as good as Fig 1. Because we are doing displacement in vertex shader stage. The quality of displacement mapping depends on the quantity of vertices. We can get better results with more vertices. But it will increase CPU overhead on Input-Assembler Stage.

So here comes the Tessellation
Like the MSDN said, now we have two additional shaders: hull shader & domain shader.
With these feature, we can split our meshes to smaller pieces!
We can even adjust tessellation factor based on distance between object and camera for dynamic LOD.

Fig 4. Tessellation on meshes.

Hull Shader: To tell hardware how we tessellate our meshes.
Domain Shader: It's just like vertex shader after tessellation. Move all calculations to here from vertex shader.

Remember to add definition in Unity's shader
#pragma hull MainHs
#pragma domain MainDs

// ------------------- Main Hull Shader ------------------- //
// a struct which describes tessellation factor
// EdgeTess: Defines the tessellation amount on each edge of a patch.
// InsideTess: Defines the tessellation amount within a patch surface.
struct PatchTess
{
float EdgeTess[3]   : SV_TessFactor;
float InsideTess : SV_InsideTessFactor;
};

PatchTess ConstantHS(InputPatch<VertexOut, 3> patch, uint patchID : SV_PrimitiveID)
{
// tessellation factor calc
float4 centerW = mul(unity_ObjectToWorld, float4(0, 0, 0, 1.0f));
float distance = length(centerW.xyz - _WorldSpaceCameraPos.xyz);

PatchTess pt;
// multiply tessellation factor by distance between object position and camera
// this is for dynamic LOD.
        // _TessllationDistance: If distance between object and camera is larger than this value, it starts to "fade" meshes by decreasing tessellation factors.
pt.EdgeTess[0] = clamp(_TessellationFactor * (_TessllationDistance / distance), 1.0f, _TessellationFactor);
pt.EdgeTess[1] = clamp(_TessellationFactor * (_TessllationDistance / distance), 1.0f, _TessellationFactor);
pt.EdgeTess[2] = clamp(_TessellationFactor * (_TessllationDistance / distance), 1.0f, _TessellationFactor);
pt.InsideTess = clamp(_TessellationFactor * (_TessllationDistance / distance), 1.0f, _TessellationFactor);

        // Note that if you set any tessellation value to zero, this triangle will be culled.
        // Which means you can do some early-culling here!

return pt;
}

// hull shader
[domain("tri")]    // Unity only supports triangle patch, so set domain as tri
[partitioning("integer")]     // tells the tessellator how it is to interpret our tessellation factors,
integer should be enough
[outputtopology("triangle_cw")]    // output topology, we use clockwise triangles here
[outputcontrolpoints(3)]    // output control points, since Unity only supports triangle so we set this 3
[patchconstantfunc("ConstantHS")]    // a callback function which determines tessellation factors.
VertexOut MainHs(InputPatch<VertexOut, 3> p,
uint i : SV_OutputControlPointID)
{
return p[i]; // return input patch directly, tessellation will be finished when passed to domain shader
}

Domain shader comes after Hull shader:

// ------------------- Main Domain Shader ------------------- //
[domain("tri")]
DomainOut MainDs(PatchTess patchTess,
float3 domainLocation : SV_DomainLocation,
const OutputPatch<VertexOut, 3> tri)
{
// interpolation tessllation data
VertexOut i = (VertexOut)0;
i.PosL = tri[0].PosL * domainLocation.x + tri[1].PosL * domainLocation.y + tri[2].PosL * domainLocation.z;
i.TexC = tri[0].TexC * domainLocation.x + tri[1].TexC * domainLocation.y + tri[2].TexC * domainLocation.z;
i.NormalL = tri[0].NormalL * domainLocation.x + tri[1].NormalL * domainLocation.y + tri[2].NormalL * domainLocation.z;
i.TangentL = tri[0].TangentL * domainLocation.x + tri[1].TangentL * domainLocation.y + tri[2].TangentL * domainLocation.z;

// call displacement
i.PosL += Displacement(i.NormalL, TRANSFORM_TEX(i.TexC, _HeightTex), _HeightMapScale);

// data transfer
DomainOut o = (DomainOut)0;
o.PosH = UnityObjectToClipPos(i.PosL);
o.PosL = i.PosL;
o.TexAlbedo = TRANSFORM_TEX(i.TexC, _MainTex);
o.TexBump = TRANSFORM_TEX(i.TexC, _NormalTex);
o.TexHeight = TRANSFORM_TEX(i.TexC, _HeightTex);
o.NormalW = UnityObjectToWorldNormal(i.NormalL);
o.TangentW = float4(UnityObjectToWorldDir(i.TangentL.xyz), i.TangentL.w);
o.BinormalW = cross(o.NormalW, o.TangentW) * i.TangentL.w;

// since we displacement the geometry, we need to adjust normal
float3 normalHeight = UnpackNormal(tex2Dlod(_HeightBumpTex, float4(o.TexHeight, 0, 0))).xyz;
normalHeight = Vec3TsToWsNormalized(normalHeight, o.NormalW, o.TangentW, o.BinormalW);
o.TangentW.xyz = normalize(o.TangentW.xyz - (normalHeight* dot(o.TangentW.xyz, normalHeight)));
o.BinormalW = cross(normalHeight, o.TangentW) * o.TangentW.w;
o.NormalW = normalHeight;

return o;
}

With tessellation, we get a beautiful terrain!
But there is still a problem we need to deal with.

Tri Planar Mapping

If we apply displacement mapping with a high scale value, some meshes will be stretched too much.
And this makes texture mapping get stretched, too.

Fig 5. Texture is stretched too much!

To solve this, artists need to adjust texture coordinate and remake texture they used to fit new texture coordinate....It's too complicate!

Alternatively, we can solve this problem by using Tri Planar Mapping.
This method uses world space position of vertices for sampling (Of course we can use local space position.), and samples texture three times (along xyz axis). At last, we blend these three sampling as our final result.

float3 GetTriPlanarBlend(float3 _wNorm)
{
// in wNorm is the world-space normal
float3 blending = abs(_wNorm);
blending = (blending - 0.2) * 7;  // apply a tiling and offset on blending value for preventing                artifacts from averaging
blending = normalize(max(blending, 0.00001)); // Force weights to sum to 1.0
float b = (blending.x + blending.y + blending.z);
blending /= float3(b, b, b);
return blending;
}

float4 SampleTriplanarTexture(sampler2D tex, float3 worldPos, float3 blending)
{
        // sample texture three times along xyz axis
float4 xaxis = tex2D(tex, worldPos.yz).rgba;
float4 yaxis = tex2D(tex, worldPos.xz).rgba;
float4 zaxis = tex2D(tex, worldPos.xy).rgba;
float4 final = xaxis * blending.x + yaxis * blending.y + zaxis * blending.z;

return final;
}

// sample albedo
// note that you should do tri planar sampling on normal, specular, and other kind of maps.
float4 albedo = (float4)0;
#if(S_TRI_PLANAR)
albedo = SampleTriplanarTexture(_MainTex, i.PosL * _TriPlanarSize, triPlanarBlend);
#else
albedo = tex2D(_MainTex, i.TexAlbedo);
#endif

After tri planar mapping, texture mapping looks better!

Fig 6. Tri Planar Mapping

Note that this method may decrease performance on some GPU since it samples texture three times.
And it samples texture with position coordinates. The result won't be the same as texture coordinates.

Drawbacks

While proposed method gives a good results for us.
There has some drawbacks when using proposed method.

1. Bounding Box
    Since Unity calculates bounding box from original meshes we set. The bounding box will be wrong after displacement. (This is why many games only apply displacement mapping for a little details.) We can solve this by getting generated meshes from GPU by using Stream-Output Stage.
But geometry shader created by Unity doesn't support Stream-Output Stage. 

    For a precise result, we need to customize a geometry shader with native graphic plugin by calling ID3D11Device::CreateGeometryShaderWithStreamOutput() and render every objects which use this shader. (TOO COMPLICATE!)

   For an approximate result, we simply write a script to setup bounding box manually.

Fig 7. Set bounding box manually.

2. Collider 
    The problem is the same as bounding box. After displacement mapping, we need to adjust our colliders or calculate physics on GPU.

3. Static Lightmap
    Static lightmap may be wrong if we scale displacement mapping too much. It may not be a problem if we use directional light for baking lightmap (With directional light, only intensity matters.). If we use point light or spot light, some vertices may have incorrect lighting result due to the distance between a point and light is changed.

Summary

Tessellation Displacement & Tri Planar Mapping provides a way to build our terrains.
It can be used for reducing CPU overhead, and is suitable for adding some small details on our meshes.

If we want to build a large terrain, there are some drawbacks we need to consider.

Demo

You can download demo project here.
Remember to open it with Unity 5.5+ !


2017年10月15日 星期日

Thunderbolt Particle System in Unity.

Recently I want a thunderbolt effect in my game.
I hope my thunderbolt can emit randomly and interact with skybox (to lit the skybox).
Thus the proposed system is created.

Thunderbolt Calculation

The idea is simple, prepare a quad with a thunderbolt texture.
Duplicate the quads and connect them all together.
Fig.1 illustrates the idea.

Figure 1. Thunderbolt quads.

And the input thunberfolt texture should be like this:
Note that the branch of thunderbolt and the halo around thunderbolt aren't necessary.
Since my program will calculate it (I can't find a good texture for sample).
So prepare a texture with main thunderbolt and put it on the center of texture.
My system will connect the center point of the quad.

  
Figure2. A single thunderbolt quad.

Next, my system creates a set of random vertices for rendering thunderbolts.
By using the method Quaternion.FromToRotation(), we can calculate all random vertices and give them random directions.

Since this will cause CPU spike in a frame, I put this job on another thread.
After my system call Monitor.Pulse(obj), the worker thread will wake up and calculate these vertices. If worker thread is done, it will call Monitor.Wait(obj) to sleep.

For performance, my system put point primitives (a vertex is a point) to mesh filter only.
Instead, my system form a quad in Geometry Shader Stage.
This can reduce the CPU calculation to a quarter.

And this is how my thuderbolt emitted.

Halo Calculation

Figure3. Thunderbolt with halo around it.

For thunderbolt halo, my system render another pass for this.
It uses the same vertice as thunderbolt, but passed to another shader.
Basically the formula is like the point light attenuation, but my shader calculates a oval-ranged attenuation. So that it can fit my thunderbolt.


Figure4. Oval halo piled up together.

It doesn't look like oval, huh?
With this calculation, thunderbolt halo works properly.


Skybox Lighting

The last thing in my system, how to lit the skybox?
First, we have a skybox cubemap. And of course, we need to prepare a sky normal box.
With this sky normal box, we can use normal lighting model on it.

 Figure5. Sky normal cubemap.

But we know skybox is a huge cube and it can be scaled to 2000x (even 5000x).
We can't get a good calculation of lighting with 3D distance.

Instead, my system calculate the skybox lighting with screen space distance from thunderbolt root to the vertex on the sky for attenuation.

It's recommended to design sky normal map with a fixed directional light. 
We also need to specific thunderbolt lighting direction in script for a consistent result on the sky. (Fig 5. is a bad example since I get this from normal generator)

Thus the size of the skybox doesn't matter. 
We can get the lighting we want!

Demo



You can buy this asset on Unity's asset store: