POM material

Hey guys,
It took me a bit to fix a bug with Pixel Depth Offset, but now node is officially checked into the main Engine. I will share my setup with you guys here though.
is based on an internal e-mail so some of info is already mentioned:

POM_01_example.png


From Left to Right: Flat Surface, Parallax Mapping, +Occlusion Mapping

Here is the material function wrapper:
251979-image-33664.png
Do not be alarmed, for the most basic case of Parallax-Only, you only need to specify the top 6 settings (I added the input for a Channel Mask right after taking image).

The middle group “Specify Manual Texture Size” is a way to save some instructions when using Pixel Depth Offset. Normally, the PDO needs to calculate the UV to World Size using some ddx/ddy math. If you have a simple material such as a tiling floor that will always be used at a certain scale you can save some instructions my manually specifying the number of world units that 1 quad of your texture uses.

All the shadowing options are at the bottom including the option to turn on shadows.

Note: All Shadow steps are performed for all pixels, there is no “Min/Max Shadow Steps” like there are for the Parallax steps. Also, for very gentle-slopes in the heightmap, the ‘soft shadow’ will show stair-stepping artifacts like on the ‘pyramid’ below, but notice the other shapes look OK:

Notes about Shadowing:
Soft shadows are approximated by using the minimum hit distance of any ray above the heightmap.

The shadow has to be applied directly in the material which has the obvious side effect of shadowing ALL lights not just the directional light.

Eventually, PixelDepthOffset alone will be enough to create real shadowing. These shadows will behave correctly with all lights. For to work the engine needs to replace CameraVector with LightVector during the shadow pass (or possibly a new ‘hybrid’ node that is communicated to have behavior).

Notes about Pixel Depth Offset:

PDO Off:
POM_01_PDO_off.png

PDO On:
POM_01_PDO_on.png

Currently using PDO will give you intersections of your heightfield with other geometry, accurate receiving of cast shadows and it will also enable SSAO to darken the crevices.

There is a catch though: currently PDO does not work well with dynamic shadows. Since the light will not receive the pixel depth offset, the ‘non-offset’ geometry will cast shadows onto the offset parts. is not an for static or stationary lights.

Notes about Cost:
It should be obvious is an expensive function. Furthermore, due to the way it uses loops, shader complexity or instruction count cannot be relied upon for accurate information. Instead performance is determined by the number of Min/Max steps.

Has separate Min/Max Steps which are blended using the viewing angle so that more Steps are performed at glancing angles. The “Min Steps” will be used when looking downward and the “Max Steps” will be used at glancing angles where more steps are typically required. Also the source texture matters since it takes more steps to trace to the bottom since you start at the top.

means that all white heightmaps will be much faster than all black heightmaps. The distribution of white to black matters since it will determine how many steps the average ray needs to hit bottom.

To help understand , the function includes a debug output called “Shader Complexity - Steps Debug”. The colors match the colors in the main shader complexity view and they show you how many steps were taken. By default, the color ‘white’ means 32 steps.

As a demonstration, if I set both Min and Max steps to 32, is the debug ‘Shader Complexity’ for example POM material (where I hooked up the node’s debug output to emissive):
POM_01_cost1.png

Setting Min Steps to 4 but leaving Max Steps at 32 gives complexity:
POM_01_cost2.png

Quality modes and Various Hardware:

Currently function is Self-Disabling based on Quality Mode and Level. means the only runs in High Quality mode with Shader Model 5. Anything else and it returns outputs that are either the original UVs or 0 for the offset outputs.
POM_01_quality.png

Code:

The function uses 2 separate versions of the Custom Node: one without shadows and one with. lets the no-shadows case take advantage of static switching.
No Shadow case:


float rayheight=1;
float oldray=1;
float2 offset=0;
float oldtex=1;
float texatray;
float yintersect;
int i;

while (i<MaxSteps+1)
{
	texatray=(HeightMapChannel, Tex.SampleGrad(TexSampler,UV+offset,InDDX,InDDY));

	if (rayheight < texatray)
	{
		float xintersect = (oldray-oldtex)+(texatray-rayheight);
		xintersect=(texatray-rayheight)/xintersect;
		yintersect=(oldray*(xintersect))+(rayheight*(1-xintersect));
		offset-=((xintersect)*UVDist);
		break;
	}

	oldray=rayheight;
	rayheight-=stepsize;
	offset+=UVDist;
	oldtex=texatray;

	
i++;
}

float3 output;
output.xy=offset;
output.z=yintersect;
return output;

Shadow case:




float rayheight=1;
float oldray=1;
float2 offset=0;
float oldtex=1;
float texatray;
float yintersect;

int i;

while(i<MaxSteps+2)
{
	
	float texatray=(HeightMapChannel, Tex.SampleGrad(TexSampler,UV+offset,InDDX, InDDY));
	
	if (rayheight < texatray)
	{
		float xintersect = (oldray-oldtex)+(texatray-rayheight);
		xintersect=(texatray-rayheight)/xintersect;
		yintersect=(oldray*(intersect))+(rayheight*(1-xintersect));
		offset-=(xintersect*UVDist);
		break;
	}

	oldray=rayheight;
	rayheight-=stepsize;
	offset+=UVDist;
	oldtex=texatray;

i++;
}


float2 saveoffset=offset;
float shadow=1;
float dist=0;


texatray=(HeightMapChannel, Tex.SampleGrad(TexSampler,UV+offset,InDDX, InDDY))+0.01;
float finalrayz=yintersect;

rayheight=texatray;
float lightstepsize=1/ShadowSteps;

int j=0;
while(j<ShadowSteps)
{
	if(rayheight < texatray)
	{
		shadow=0;
		break;
	}
	else
	{
		shadow=min(shadow,(rayheight-texatray)*k/dist);
	}
	
	oldray=rayheight;
	rayheight+=TangentLightVector.z*lightstepsize;

	offset+=TangentLightVector.xy*lightstepsize;
	oldtex=texatray;

	texatray=(HeightMapChannel, Tex.SampleGrad(TexSampler,UV+offset,InDDX, InDDY));
	dist+=lightstepsize;
	j++;	
}


float4 finalout;
finalout.xy=saveoffset;
finalout.z=finalrayz;
finalout.w=shadow;
return finalout;

Showing the inputs of the two nodes:

The inputs “TexHeight” are not required. I just left that in by accident from a previous version that used it.
The input “UVDist” is the same as the input named “InMaxOffset” earlier in thread, but also times “stepsize”.

For the shadow node version, the only new options inputs are:

TangentLightVector: is your lightvector transformed into tangent space, but also with some length adjustments:

ShadowSteps: number of steps for shadow

The only other custom material work is to solve the Pixel Depth Offset amount. To make it slightly cheaper when not using PDO, some additional calculation needs to be done on the returned B value before getting the lenght:

Notice that the B channel is separated, 1-x, then multiplied by the texture height before solving the distance. That returns the full 3d length of the offset accounting for the ray-intersection offset.

Also, the top DDX and DDY nodes are connected to the UV input of the custom nodes. basically solves how big in the world a UV value of 1 is.

k: The shadow penumbra

I know is quite a bit of things to get working but let me know if there are any questions.

1 Like

Woah, looking awesome. Can’t wait to try it out.

Finally an official POM integration, thanks so much !

hmm. Wishing I had waited 2 more weeks to see integration from . I just spent $30 on a POM function released by another user on the forums…

I was able to save 10 instructions from the main loop by removing the need for the ‘lastoffset’ variable, and also by Multiplying ‘stepsize’ by ‘UVDist’ outside of the custom node (which is plugged into UVDist, stepsize is also still needed for the z offset).

It may be possible to save a few more by simplifying how the ‘yintersect’ variable is passed. That variable is only needed for PixelDepthOffset (without it, the pixel depth offset has harsh step seams) but so far I cannot figure out another way to simplify that without adding an unwanted branch to the function. Any more inputs and it becomes dauntingly unsuable IMO.

Updated code for parallax only node:


float rayheight=1;
float oldray=1;
float2 offset=0;
float oldtex=1;
float texatray;
float yintersect;
int i;

while (i<MaxSteps+1)
{
	texatray=(HeightMapChannel, Tex.SampleGrad(TexSampler,UV+offset,InDDX,InDDY));

	if (rayheight < texatray)
	{
		float xintersect = (oldray-oldtex)+(texatray-rayheight);
		xintersect=(texatray-rayheight)/xintersect;
		yintersect=(oldray*(xintersect))+(rayheight*(1-xintersect));
		offset-=((xintersect)*UVDist);
		break;
	}

	oldray=rayheight;
	rayheight-=stepsize;
	offset+=UVDist;
	oldtex=texatray;

	
i++;
}

float3 output;
output.xy=offset;
output.z=yintersect;
return output;

Also it would be nice to get working for the rayheight<=texatray case since that will be a whole step faster for all white heightmap pixels.
So far when I try to get that working, its causing the first layer either to get invalid results (it attempts to divide by 0) or it doesn’t get the offset to match up with the next layer if I use an offset to keep it from dividing by 0. If anybody can help figure that out that would be awesome and make another tiny bit faster.
A separate if statement works but the branching overhead is probably not worth it unless there are a significant amount of white pixels.

If you don’t want another input branch, why don’t you just make 2 versions of POM, POM and POM+ or something?

Not a bad option to consider, but that itself has some downsides since it may not be possible to switch between functions and have all the pins stay connected.
As a test I commented out the yintersect line and returned 0 for that variable and it made no difference to the instruction count. Not that everything you do is properly counted but it may not be enough savings to warrant a separate node.

With Just parallax (no PDO or shadows), it’s showing as 174 instructions right now (with static lighting).

Using PDO the automated way kicks it up to 226 which is crazy (remember that requires 4 texture lookups: ddx-uv, ddy-uv, ddx-world, ddy-world). If you specify the UV-world size manually that brings the PDO cost down to 211. It still seems like a lot. Maybe it is not able to reuse much of the work for the pixel depth offset…

It’d be cool if UE4’s material editor had a option to swap nodes but keep inputs (wouldn’t it be nice to be able to swap multiply for add or subtract), might want to throw that on a wish list.

I believe there is already a request ticket for that.

I am trying to test out a blend between a POM material and a regular material. For example if you had bricks or stones and wanted them covered up by moss or mud.
The basic idea is working: use mask * height, and use the difference between that and the WPO.Z value to make a gradient which can be used to create a slope between the two surfaces:

POM_Blend_01.JPG

The problem is mapping the 2nd texture. Right now I am using “Virtual Plane Coordinates” with the gradient Z offset from WorldPosition.Z. looks right from the sides, but from opposing views it causes a mirroring effect since the pixels are actually trying to project out to where each would have mapped to their respective planes:
POM_Blend_02.JPG

I am guessing there is some kind of solution with line intersection once again but it doesn’t seem obvious how to solve it yet.

Remember line intersection problems can be solved by converting lines into y=mx+b (where m is slope and b is the y value when x=0) or x=y(t)+y(1-t) and making one of the variables equal for both lines. Just need to figure out how to inject the intersection into the virtual plane coordinates math (or probably better replace it with something simpler for ).

1 Like

Gorgeous work.
So is gonna be included in a future update as an official and permanent solution?

Its already in the branch and will be in 4.9. I just cherry picked the commit and am using it now in 4.8 preview 3 and it works well.

The function was actually saved with a fairly old engine version from around January. It’s still technically a 4.8 build but one of the earliest ones made so anybody on a 4.8 preview should be able to try it out.

Nice stuff ! Curious, would decals added over geometry take advantage of the pixel depth offset, or would they betray the illusion by rendering flat and intersecting the POM material’s depth?

Pixel depth offset works fine with decals.
Not the prettiest example (the default decal mat) but you can tell its projecting on the depth since I made the Z scale very shallow and rotated the decal:

That, is awesome.

float rayheight = 254/255;
Would solve full white first iteration problem?

awesome work

It does save one step for white pixels (You have to enter it as rayheight = (float)254/255; or alternatively 0.996)

but for some reason it also seems to cause the results from that first step to be offset 1 step backwards instead of receiving 0 offset. It does seem very close to working though, great suggestion.

Ok I figured it out. It does add 2 instructions to the material but it is hard to say if that cost is meaningful, or if the white-optimization will end up being very important in actual usage.

The top two lines need to say:

float rayheight=0.996;
float oldray=0.996+stepsize;

Adding stepsize to oldray allows the intersection to handle slope the same as the future steps (it simulates a step already having been taken).

Another option to optimize a POM that requires tons of steps would be to generate a distance field from the initial pixels and use that as the first step distance. The cost of the extra lookup and math would probably be a wash unless you were doing lots of steps.

, you are a God amongst men. A GOD!!!

As for optimizing the steps, I will reiterate that I tried every possible method to limit the step size from using pixel depth to diminish steps in the distance to a combination of distance and tangents, and nothing gave me a better overall framerate than just a basic tangent calculation: product of camera vector and vertex normal. Unless you need more than 50-60 steps and running a mesh with more polygons than pixels (in which case, holy cow, just model it out or tessellate), any other method to limit steps was rendered moot by the additional instructions.