Bump offset/parallax mapping window shader

Hi, I’m trying to write a window shader that offsets an interior texture based on viewing angle.
Here is an example of how to do this in unreal:

The offset is done by a node called bump offset which is given the viewing direction and the texture coordinates. This is what is called parallax mapping, but with a static height instead of a hightmap.

How would I go about writing this function in cg?
I’ve tried this to translate an open gl function I found here: Learn OpenGL, extensive tutorial resource for learning Modern OpenGL

like this:

 float2 ParallaxMapping(float2 texCoords, float3 viewDir)
 { 
 float height = -1; 
 float2 p = viewDir.xy * (height * 0.1);
 return texCoords - p; 
 }

and then in the vertex program set the view dir to:

 mul(modelMatrix, input.vertex).xyz - _WorldSpaceCameraPos;

and then in the fragment program do this to the uv:

 uv = ParallaxMapping(input.tex.xy, input.viewDir);

and it kind of works, looks great when looking up and down but moves inverted to how I want it when looking from left to right, and it moves unpredictably when the window is rotated. I think I need to take the normal of the windowpane into account somehow.

If you have done this before I’d love to get some tips to point me in the right direction! :slight_smile:

This is an interesting technique! It kept me thinking the whole Sunday, I hate when that happens :slight_smile:

Now about 6 hours later, I put together a shader with three different approaches which I came up with. You can find a download link to the project at the bottom of this post.

Here is how the result looks:

Shader Code

//
// download from http://www.console-dev.de/bin/Unity_Shader_Fake_Interior.zip
//
// this shader is part of an answer to the following forum post:
// http://forum.unity3d.com/threads/bump-offset-parallax-mapping-window-shader.407091/
//
// I believe this approach is rather unconventional.
// My idea was that I could use the clip space coordinates to offset the mesh texture coordinates.
// clip space is in range -1 to +1
// if the vertex is at the left screen edge, x would be -1 in clip space
// if the vertex is at the right screen edge, x would be +1 in clip space
// the sample principle applies for the y coordinate.
//
// if the interior quad is also mapped in this fashion (top-left = 0,0 and bottom-right=1,1)
// it should be possible to simply add the clip-space coordinates to the texture coordinates
// and we have the parallax effect already already working.
//
// however, if we look to the left we actually want to reveal more of the left part of the texture.
// just adding the clip-space coordinates would simple move the texture to the left, revealing more of
// the right texture part. so I simply changed the sign (subtract rather than add) to scroll it into
// the opposite direction.
//
// since we just modify texture coordinates, we can also do just everything in the vertex shader \o/
//
// in this file you can find 3 different tests, showing my hackery.
//

Shader "Custom/Interior"
{
Properties {
    _Color ("Main Color", Color) = (1,1,1,1)
    _MainTex ("Base (RGB)", 2D) = "white" {}

    // The maximum parallax value should not be greater than
    // the uv border in the mesh. Usually you would uv map a quad from:
    // top-left=0,0 to bottom-right=1,1
    // however, the shader moves the uv's by the specified _Parallax value.
    // in order to never go outside the 0,0 .. 1,1 range, you "add a border to the uvs in the mesh".
    // To properly support a maximum parallax value of 0.25, you would uv map the quad as:
    // top-left=0.25,0.25 to bottom-right=0.75,0.75
    // if the shader adds the _Parallax value to the uvs, we have the full uv range again.
    _Parallax ("Parallax", Range (0, 1)) = 0.25
}

CGINCLUDE
sampler2D _MainTex;
float4 _MainTex_ST;
fixed4 _Color;
float _Parallax;

struct Input
{
    float2 st_MainTex;
};

// test 2 seems to work well and I understand why it is working
#define TEST_2

#if defined(TEST_1)
inline float2 InteriorUV(float4 vertex, float3 normal, float2 texcoord)
{
    float4 clipSpace = mul(UNITY_MATRIX_MVP, vertex);
    clipSpace.y = -clipSpace.y;
    clipSpace = normalize(clipSpace);

    // applys texture scaling or displacement using the screen center as origin.
    // this results in undesired scretching at the screen borders.
    // you can still compensate by adjusting the texture tiling and offset in the material.
    // to get a _Parallax value of 0.25 to apply the scaling from the quad center, you would need
    // to modify the material settings as followed:
    //   Tiling X = 1.5    Y = 1.5
    //   Offset X = -0.25  Y = -0.25
    float2 offset = clipSpace.xy * _Parallax;
    return texcoord - offset;
}
#endif

#if defined(TEST_2)
inline float2 InteriorUV(float4 vertex, float3 normal, float2 texcoord)
{
    float4 clipSpace = mul(UNITY_MATRIX_MVP, vertex);
    clipSpace.y = -clipSpace.y;
    clipSpace = normalize(clipSpace);

    // This is the same as TEST_1, but we modify the texcoord so that the
    // displacement occurs not using the screen center anymore, but the
    // quads center. this seems to look nice at all angles and positions.
    float2 offset = clipSpace.xy * _Parallax;
    return (texcoord + texcoord * _Parallax * 2) - _Parallax - offset;
}
#endif

#if defined(TEST_3)
// another approach that seems to work, figured out through trial&error.
// I don't understand entirely why it works though :)
// it seems to work really nice though.
inline float2 InteriorUV(float4 vertex, float3 normal, float2 texcoord)
{
    float3 distance = normalize(mul((float3x3)UNITY_MATRIX_MV, reflect(ObjSpaceViewDir(vertex), normal)));

    float2 offset = distance.xy * _Parallax;
    return texcoord - offset;
}
#endif


void vert(inout appdata_full v, out Input o)
{
    UNITY_INITIALIZE_OUTPUT(Input, o);

    float2 texcoord = TRANSFORM_TEX(v.texcoord, _MainTex);
    o.st_MainTex = InteriorUV(v.vertex, v.normal, texcoord);
}

void surf (Input IN, inout SurfaceOutput o)
{
    fixed4 c = tex2D(_MainTex, IN.st_MainTex) * _Color;
    o.Albedo = c.rgb;
    o.Alpha = c.a;
}
ENDCG


SubShader
{
    Tags { "RenderType"="Opaque" }
    LOD 500

    CGPROGRAM
    #pragma surface surf Lambert vertex:vert
    #pragma target 3.0
    ENDCG
}



FallBack "Legacy Shaders/Diffuse"
}

I believe my approach is rather unconventional, but I also know nothing about interior mapping except for the video you posted.

My idea was that I could use the clip space coordinates to offset the mesh texture coordinates.
Clip space is in range -1 to +1. If the vertex is at the left screen edge, x would be -1 in clip space. If the vertex is at the right screen edge, x would be +1 in clip space. The same principle applies for the y coordinate.

If the interior quad is also mapped in this fashion (top-left = 0,0 and bottom-right=1,1 (but you need a border, see shader comments)) it should be possible to simply add the clip-space coordinates to the texture coordinates and we have the parallax effect already working.

However, if we look to the left we actually want to reveal more of the left part of the texture. Just adding the clip-space coordinates would move the texture to the left, moving in more of the right texture part. So I simply changed the sign (subtract rather than add) to scroll it into the opposite direction.

Since we just modify texture coordinates, we can also do everything in the vertex shader \o/

You can find three different approaches to this problem in in “Assets/Interior.shader”, basically 3 different tests showing my hackery.

Here is the Unity 5.3 compatible project with all the assets shown in the video.
http://www.console-dev.de/bin/Unity_Shader_Fake_Interior.zip

2 Likes

It’s a nice idea, it’s not interior mapping as seen in bioshock infinite and friends, but it does seem much lighter on the gpu and not computationally expensive. I can see this being sort of ideal for mid and far lods.

Check some of the tricks out on this site, here’s one of them: Windows AC/Row/Infinite | Simonschreibt.

1 Like

Hello again!
Yours is an intresting solution, but it doesn’t really take the position of the camera into account which makes it break when you strafe.

I recently had time to start thinking about this again (got my degree in 3d art, officially unemployed!), and here is what I’ve come up with:

If you take the normal direction of the verticies of the window pane and their position and the camera position all in world space shouldn’t you be able to convert the camera position into a kind of vertex space that is relative to that vertex’s position and rotation? Then you could simply take the x and y values of this new point and use them as offset values of the uv after setting the min and max values of the offset and a scaling factor.

The problem is that my vector math is both rusty, and basic. I figured that the math needed would be close to when you calculate view space coordinates for verticies, so I tried to converting some code I found online:

This is the method I use to create the vertex space matrix

float4x4 viewFrom(float3 nor,float3 up, float3 vpos){
  
            //normal of the vertex, the way the matrix should "look"
            float3 n = normalize(nor);
            //up direction of the camera, I assume (0,1,0) when I send it
            float3 v = normalize(up);

            //third angle, looking to the side, cross product of the two
            float3 u = cross(nor,up);

            float4x4 m = float4x4(u.x, u.y, u.z, 0.0,
                                  v.x, v.y, v.z, 0.0,
                                  n.x, n.y, n.z, 0.0,
                                  0.0, 0.0, 0.0, 1.0);

            float4x4 mpos = float4x4(1.0, 0.0, 0.0, vpos.x,
                                       0.0, 1.0, 0.0, vpos.y,
                                      0.0, 0.0, 1.0, -vpos.z,
                                       0.0, 0.0, 0.0, 1.0);

               m = mul(m,mpos);
           
            return m;
        }

and then in my vertex shader I do this:

//get the matrix
float4x4 vertexMatrix = viewFrom(normalDirection.xyz,float3(0.0,1.0,0.0),vertexWorldPos.xyz);

//convert the position of the camera to this "vertex space"
             float3 relPos =    mul(vertexMatrix,_WorldSpaceCameraPos).xyz;

And finally I scale and clamp the value to a better scrolling offset that can be used to offset the uv:

float2 offsetUV = ((relPos).xy*_OffsetScale);
             offsetUV.x = clamp(offsetUV.x,-_MaxOffset,_MaxOffset);
             offsetUV.y = clamp(offsetUV.y,-_MaxOffset,_MaxOffset);


This works much better than my first try, but it still doesn’t work when rotated, and behaves differently when moved further away. Am I missing something obvious, or have I completely misjudged the problem?

1 Like

What you want for this kind of effect is the tangent space view direction.

If you’re using surface shaders I think you can just add viewDir to your input struct and you’ll get the tangent space view direction in the surf function. Then you just need something like this:

float2 interiorUV = IN.uv_MainTex + (IN.viewDir.xy / IN.viewDir.z) * _OffsetScale;

Thank you! That worked out perfectly! I knew I was overcomplicating things! :slight_smile:
Just one question, what is the point of the devision by the z-value? It seems to work without it but acts a little differently…

Oh and if somebody finds this and wonders how to do it in cg, here is how I got the tangent space view direction:

TANGENT_SPACE_ROTATION;
float3 viewDir  = mul(rotation,  ObjSpaceViewDir(v.vertex));

You just need to make sure you’ve included the tangent in your vertex input.

1 Like

If you use a normalized view direction vector, what you’re likely starting with, it is going to be essentially moving the offset around in a circle / hemisphere. Dividing by the z component projects the vector onto a unit plane. The normalized vector is fine too use as it gets you most of the parallax look, and gets you a built in “max” offset, but the other way is more accurate. You may want to fade out the effect at extreme angles.

Any chance we can see the final code? I love the effect, but I’m not a shader coder… I doubt I can get it to work by myself…