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:
From Left to Right: Flat Surface, Parallax Mapping, +Occlusion Mapping
Here is the material function wrapper:
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:
PDO On:
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):
Setting Min Steps to 4 but leaving Max Steps at 32 gives complexity:
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.
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.