Saturday, 18 October 2014

Parallax Oclusion Mapping

Yesterday I dropped myself on the Unreal Engine forums and while digging on the rendering section, found this post related to attempting to reproduce Parallax Occlusion Mapping (POM for short) in UE4.

In UDK, we had access to Light vector in our materials and we could reproduce the effect following a guide we had in UDK Gems but right now, Light vector is a node that can only be used with light function materials, and the node tree described in that guide is pretty much a huge mess in my opinion.
Fortunately, we can reproduce the effect more easily in UE4 making use of the "Custom" node which allow us to put some HLSL code in our material.

But today I wanted also, not only share Ehamloptiran findings, but also make a little comparative on materials bump effects. So let's take a look!

Plain diffuse 1Plain diffuse 2

As shown in the image, the first one is a simple flat material. No normal map added, just plain diffuse shading. Simple but very cheap. Very commonly used on mobile games.

Normal map 1Normal map 2

Then we got the same material with a normal map. This is pretty much your average material.
Iterative parallax 1Iterative parallax 2

Now we have our normal material but this time it does include an iterative parallax effect on it's UV. This is a very cheap and simple kind of parallax effect and I described how to do it some days ago

POM

This is POM, as you may see, the effect it's quite impressive at first glance, but I has some inconvenients. First off, this material is very expensive and can heavily hit your performance.

POM 1POM 2POM 3

POM Seams

There is also issues when approaching UV seams and not to mention the texture shimmering effect when looking the surface from an oblique angle which can only be solved increasing the samples.
Tessellation 1Tessellation 2

And now we have tessellation which in fact it does bump the detail of your heightmap using real geometry. This method also has it's downside and it is very dependant on the topology of the mesh it is applied. In theses sample cubes, we only have 12 triangles which is not enough to achieve a good tessellation. Also, there are some issues related to cracks on the seams of the mesh (even when using the crack free option in the material), but that can be solved using color per vertex to adjust the strenght of tessellation in critical areas.

Regarding to materials, here are the networks to achieve theses effects:

Iterative parallax

IP material network

You can make more iterations if you want but I think 4 is more than enough to achieve the effect.

Tessellated material

Tesselation material network

With this one I'm using also controlling tessellation over distance. We don't need over 20k triangles for a mesh that's is gonna be seen from far away so with this technique we can lerp the tessellation intensity over distance. I did also a cheap contrast of my heightmap.

Parallax Occlusion Mapping

Here we must do some HLSL coding using the"Custom" node.

POM material network

As said before, the "POM" node it's in fact a Custom HLSL node its output must be set as CMOT Float 2

This is the code:
float CurrRayHeight = 1.0;
float2 CurrOffset = float2( 0, 0 );
float2 LastOffset = float2( 0, 0 );

float LastSampledHeight = 1;
float CurrSampledHeight = 1;

int CurrSample = 0;

while ( CurrSample < (int) InNumSamples )
{
float4 Temp = Material.Texture2D_0.SampleGrad( Material.Texture2D_0Sampler, InTexCoord + CurrOffset, InDX, InDY );
CurrSampledHeight = ( ( Temp.r * InChannelMask.r ) + ( Temp.g * InChannelMask.g ) + ( Temp.b * InChannelMask.b ) + ( Temp.a * InChannelMaskAlpha ) );

if ( CurrSampledHeight > CurrRayHeight )
{
float Delta1 = CurrSampledHeight - CurrRayHeight;
float Delta2 = ( CurrRayHeight + InStepSize ) - LastSampledHeight;

float Ratio = Delta1/( Delta1 + Delta2 );

CurrOffset = ( Ratio ) * LastOffset + ( 1.0 - Ratio ) * CurrOffset;

CurrSample = InNumSamples + 1;
}
else
{
CurrSample++;

CurrRayHeight -= InStepSize;

LastOffset = CurrOffset;
CurrOffset += InStepSize * InMaxOffset;

LastSampledHeight = CurrSampledHeight;
}
}
return CurrOffset;
And theses are the input variables:
1. InNumSamples
2. InStepSize
3. InTexCoord
4. InDX
5. InDY
6. NormalHeightMap (Not actually used, just there to ensure texture doesn't get optimized out)
7. InMaxOffset
8. InChannelMask
9. InChannelMaskAlpha (added because the custom node converts vector parameter to float3 instead of float4)

Also we do have a small but not less important not for the Silhouette clipping. Output is CMOT Float 2 too.
Code:
clip( InFinalCoords );
clip( 1.0f - InFinalCoords );

return InFinalCoords;
Input:
1. InFinalCoords

As well as Iterative parallax, this is an UV trick, so you have to the output to the UV from the different texture you will be using.

NOTE: Make sure that the height map is the first texture you drop into the material graph as the above custom code block accesses that texture directly via Material.Texture2D_0 and Material.Texture2D_0Sampler If you don't drop it in first, it will be referencing the wrong texture. To fix that you will need to change those two values to whichever one is assigned to the heightmap.

And this is it. Hope you found this article useful!

0 comments:

Post a Comment