1000 Forms of Bunnies victor's tech art blog.

Porting Shadertoy to Unreal Engine Using Custom Node (Case 1)

Default decal compares with fixed decal using biplanar projection. Code ported from shadertoy.

Recently I’ve been helping my friends implementing a better decal projection in Unreal Engine as the default decal projection is fairly limited. It is causing stretching pattern on the parallel surface as projection intended to be.

A potential solution was presented by my teammate about using triplanar/biplanar projection on the decal to wrap the pattern all around the object that affected by the decal. Biplanar projection is particularly tempting as it is requiring only 2 texture sampling comparing with 3 in triplanar.

…is based on the same three projections but uses only two texture fetches at the cost of some extra arithmetic, which is probably a good trade-off between computation and bandwidth

The main reference material we found is once again iq’s blog: biplanar mapping.

I won’t cover the algorithm here but how to port the code over to Unreal Engine as a documentation of the workflow.

This post I will take this task as a case to document the process but porting Shadertoy can be more complicated when involving volume textures, multipasses etc. Will cover in future porting cases.


Config custom node

Refer to Unreal Engine Custom Node for details.

I usually wrap my hlsl code in a function and just call the function the code block. This way I can leave the shader path at Include File Paths instead of using #include statement in the code block.

Code converting

Shadertoy code is GLSL and custom node is accepting HLSL. Simplified code to port is:

vec4 biplanar( sampler2D sam, in vec3 p, in vec3 n, in float k )
{
    // grab coord derivatives for texturing
    vec3 dpdx = dFdx(p);
    vec3 dpdy = dFdy(p);
    n = abs(n);

    // major axis (in x; yz are following axis)
    ivec3 ma = (n.x>n.y && n.x>n.z) ? ivec3(0,1,2) :
               (n.y>n.z)            ? ivec3(1,2,0) :
                                      ivec3(2,0,1) ;
    // minor axis (in x; yz are following axis)
    ivec3 mi = (n.x<n.y && n.x<n.z) ? ivec3(0,1,2) :
               (n.y<n.z)            ? ivec3(1,2,0) :
                                      ivec3(2,0,1) ;
        
    // median axis (in x;  yz are following axis)
    ivec3 me = ivec3(3) - mi - ma;
    
    // project+fetch
    vec4 x = textureGrad( sam, vec2(   p[ma.y],   p[ma.z]), 
                               vec2(dpdx[ma.y],dpdx[ma.z]), 
                               vec2(dpdy[ma.y],dpdy[ma.z]) );
    vec4 y = textureGrad( sam, vec2(   p[me.y],   p[me.z]), 
                               vec2(dpdx[me.y],dpdx[me.z]),
                               vec2(dpdy[me.y],dpdy[me.z]) );
    
    // blend and return
    vec2 m = vec2(n[ma.x],n[me.x]);
    // optional - add local support (prevents discontinuty)
    m = clamp( (m-0.5773)/(1.0-0.5773), 0.0, 1.0 );
    // transition control
    m = pow( m, vec2(k/8.0) );
	return (x*m.x + y*m.y) / (m.x + m.y);
}

Data type

First we need to translate the data type syntax:

  • vec3 to float3
  • ivec3 to int3

Functions

Certain functions are also named differently:

  • dFdx() to ddx()
  • dFdy() to ddy()

An introduction to shader derivative functions

  • mix() to lerp()
  • fract() to frac()
  • mod() to fmod()
  • clamp(x, 0., 1.) to saturate() // seems clamp works in UE

Textures

Texture data:

  • sampler2D to Texture2D

Texture2D type is called a Texture Object in Unreal. Don’t confused against a Texture Sampler node:

Texture sampling:

  • texture2D() to
    • Texture2DSample(Tex, TexSampler, UV)
    • Tex.Sample(TexSampler, UV) // recommended

Direct3D 9 uses intrinsic texture functions to perform texture operations. This example is from the BasicHLSL Sample and uses tex2D(s, t) (DirectX HLSL) to perform texture sampling. Output.RGBColor = tex2D(MeshTextureSampler, In.TextureUV) * In.Diffuse; Direct3D 10 uses templated texture objects instead. Here is an example of the equivalent texture operation. Output.RGBColor = g_MeshTexture.Sample(MeshTextureSampler, In.TextureUV) * In.Diffuse;

Note that in Unreal each texture object Tex will automatically define a correponding sampler called TexSampler, just add “Sampler” behind the texture obj name. And it has to be explicitly passed into the function call as in Tex.Sample(TexSampler, UV).

Here textureGrad() need to be replaced by Tex.SampleGrad(TexSampler, UVs, ddx(UVs), ddy(UVs)); (alternative tex2Dgrad)

Syntax

Note that in GLSL you can initialize vecter with all elements in same value like vec2(1.) ( aka vec2(1., 1.)) but in HLSL you cannot. quick hack is to do (1.).xx for initializing float2(1., 1.).

Final code (Biplanar Projection)

// https://www.shadertoy.com/view/ws3Bzf

// "p" point being textured
// "n" surface normal at "p"
// "k" controls the sharpness of the blending in the transitions areas
// "s" texture sampler // tex_inSampler

float4 Biplanar(Texture2D Tex,  SamplerState TexSampler, float3 p,  float3 n,  float k )
{
    // grab coord derivatives for texturing
    float3 dpdx = ddx(p);
    float3 dpdy = ddy(p);
    n = abs(n);

    // major axis (in x; yz are following axis)
    int3 ma = (n.x>n.y && n.x>n.z) ? int3(0,1,2) :
               (n.y>n.z)            ? int3(1,2,0) :
                                      int3(2,0,1) ;
    // minor axis (in x; yz are following axis)
    int3 mi = (n.x<n.y && n.x<n.z) ? int3(0,1,2) :
               (n.y<n.z)            ? int3(1,2,0) :
                                      int3(2,0,1) ;
        
    // median axis (in x;  yz are following axis)
    int3 me = int3(3,3,3) - mi - ma;

    float4 x = Tex.SampleGrad( TexSampler, float2(p[ma.y],p[ma.z]), float2(dpdx[ma.y],dpdx[ma.z]), float2(dpdy[ma.y],dpdy[ma.z]) );
    float4 y = Tex.SampleGrad( TexSampler, float2(p[me.y],p[me.z]), float2(dpdx[me.y],dpdx[me.z]), float2(dpdy[me.y],dpdy[me.z]) );
     
    // blend and return
    float2 m = float2(n[ma.x],n[me.x]);

    // optional - add local support (prevents discontinuty)
    m = clamp( (m-0.5773)/(1.0-0.5773), 0.0, 1.0 );

    // transition control
    m = pow( m, (k/8.0).xx);
    m = float2(pow(m.x, (k/8.0)),pow( m.y, (k/8.0)));

    return (x*m.x + y*m.y) / (m.x + m.y);
}


Triplanar Projection

// https://www.shadertoy.com/view/MtsGWH

float4 boxmap( Texture2D Tex,  SamplerState TexSampler, float3 p,  float3 n,  float k )
{
    // project+fetch
	float4 x = Tex.Sample(TexSampler,p.yz);
	float4 y = Tex.Sample(TexSampler,p.zx );
	float4 z = Tex.Sample(TexSampler,p.xy );
    
    // and blend
    n = abs(n);
    float3 m = float3(pow( n.x,k ),pow( n.y,k ),pow( n.z,k ));
	return (x*m.x + y*m.y + z*m.z) / (m.x + m.y + m.z);
}

Comparison

Default uv mapping; biplanar projection; triplanar projectio..

As expected the biplanar projection requires more math tricks hence more instructions.

Apply to decal!

More pitfalls applying the shader to decal actors in Unreal Engine.

To be continued…

comments powered by Disqus