POM material

Will do!
As far as I know, we have three ways of adding depth to our materials: Tessellation, BumpOffset and POM. The best of the three, in my opinion, is POM.

Great! Hopefully we can finally get an answer from Epic.
Agree, Tesselation Displacement works great for some stuff, but I think POM is more versatile as it works way better with small details, also it’s very cheap.

maybe I don’t fully understand the problem (I haven’t tried POM shader),
but can’t you sample a CustomTexture? i.e. make a TextureObjectParameter and feed to it your heightmap, and then make a Custom node where you sample it
I’ve been able to sample textures in HLSL like in the past, I’m sure it should be possible to do it here

Each has their uses and limitations. I wouldn’t necessarily say POM is the best. Tessellation has advantages over POM, but requires a well tessellated mesh to begin with to be able to obtain the detail, but you get shadows, proper decal placement, proper raycast hit (if you want per poly), etc. POM has issues with these, except maybe self shadowing. I never liked in cryengine for example where they used POM on the terrain, sure it looked nice from one perspective, but looked awful from others and when u shoot at it, u get floating decals.

That’s because Parallax mapping is one of those things that always looks like a good idea initially on paper, and then the more you try to fix the stretching and the bugs and the obvious artifacts like edge silhouettes and how it can’t properly receive shadows, the more you realize there’s a reason almost no triple A game has or will ever ship with it. Often you can just brute force the actual polys and end up with something faster anyway, without ever having to sit there and fiddle with any of .

It’s why so many people are so interested in a crack free realtime tesselation solution. If it weren’t for cracks/obvious popping it would be faster and much, much simpler to just throw displacement maps and actual polys at the problem.

Cryengine with all it’s weakness and limitation does one without problems. And almost any game developed by it uses POM for things like terrain, decals and roads.

POM can modify the depth buffer if done right, which makes receiving shadows work fine, and there are several ways to fix silhouette issues, albeit not perfectly IIRC, but it’s better than not having the effect at all.

Many games use POM, It’s not exactly a new technique.

I shouldn’t have cut down my comment…Well, what I actually wanted to say is that in many cases I would prefer POM and Tessellation. Both work better in different scenarios, but for example Tessellation requires a medium-poly mesh to begin with. So very simple meshes can’t use Tessellation. In many cases a combination of both would the best way I guess. Use Tessellation for that and in other cases POM for that.
I used CE3 and I used POM as well as Tessellation a lot. I haven’t had too many issues with decals, sometimes yes, but since most of the POM materials were subtle it wasn’t as bad as it could have been.

Greetings,
Dakraid

Um, I’ve performed many tests with tessellation, and in almost all the cases I know of, it sucked performance like no tomorrow. I even tried tessellating a mesh with 100,000 polygons and using a very low tessellation multiplier, but that barely output at 30 frames per second. POM is still king, until I get enough money for a GTX 970.

I tried tessellation for a landscape, anything bigger than 500m by 500m gives me unplayable frame rates with dynamic shadowing on the terrain. I don’t understand why, it barely adds any triangles and it’s just tessellated in a small radius around the player. Running an OC’d IB i5 and a GTX 760. I’m not unconvinced that there isn’t some bug with .

So I started working with POM, performance is better, but still not what I was expecting. Is there anything other than samples we can tweak in the code to increase performance with Eha’s code? I’m using 15 samples min and 25 max. I’m currently making the parallax calculation 3 times for 3 different heightmaps in my landscape material, and FPS drops from 120 (no POM) to 60-80 (3 calcs) with nothing else in the scene, but the base pass doesn’t have many instructions. I know nothing about HLSL, so I’ve kind of hit a brick wall.

That performance loss is way bigger than I experienced. I lost maybe 10 frames per second when i did a test earlier using 2k maps and 2 materials, max samples at 30 and min at maybe 20 or so. I used it on a vast landscape and tried it on a GTX 970 and a Radeon 7970 and had similar results.

I’m using 4K heightmaps, guess I’m just expecting too much. I’ll have to use lower resolution displacement textures since my material uses 16 heightmaps.

Also, is there any effective way to get rid of texture swimming? Is detecting oblique angles and then falling back to a non-POM texture going to look normal?

EDIT: Going from 4K to 2K increases FPS by one or two.

oooo is awesome :slight_smile: I am glad somebody got function working as a custom node as it was a pain working with those long material loop chains.

fwiw, the cobblestone alpha you used on page1 was made from an in-editor material function for making brick and tile textures. I was almost done with that as a content example project when I lost a ton of work due to a hard drive crash. Data recovery got most of the stuff back but the project containing all the materials is a mess since a bunch of random bits were not able to be r
here’s a video of what that looks like:
https://www.youtube.com/watch?v=HDqAwQQ1f1Q

1 Like

I decided to rewrite node from scratch without any reference. I have it working pretty well now.

After I got my version working I went back and tried out the version in thread. I noticed I can drop a few ms from the render time by replacing the following line:

CurrSample = InNumSamples + 1;

with “break;”

It looks like that is what it was trying to do but with the existing code it still requires the GPU to look at the loop and evaluate the expression an extra time.

Also you can get another speed boost by using >= on the very first compare, since as it stands right now, the first hit will always fail even when the heigthmap value is 1. By skipping that step in addition to the above break command should be a decent boost. Or just add a tiny fraction to the starting ray height.

Nice thanks for that , I am using shader quite extensively, so will help give me a little FPS boost

It’s almost ready. Got soft shadows working thanks to temporalAA shifting the light angle:

The shadows are a tad noisy due to the tempAA which I don’t like.

I almost have a version that doesn’t need TempAA using “proper” cone trace approximation working. It gives really nice smooth shadows on the ground, but introduces horrible stepping on the curved surfaces. I’m sure I will figure it out eventually:

edit, ok that was quick. still a bunch of cleanup so calling it for now but should have something to post tomorrow.

The tiny bit of artifacting that is left is from web compression since I copied the height image from here. Notice the blocks on the slopes.

Wow, that is awesome!

Looks great! Will work with multiple height maps in the same material, for landscape layers? That was always the biggest problem for me using the old method.

That looks fantastic, !

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