Cubemap direction based on view direction.

Hello,

I am attempting to create a shader that results in the cubemap’s front always facing the camera, similar to the metal mario effect in SM64. I have broken the process down to 3 steps but am not getting the desired effect:

  1. Calculate the view direction of the camera
  2. Create the reflection via the “reflect” function call
  3. Apply the reflection via the “texCUBElod” function

My assumption is I am doing step 1 wrong but my lack of shader skills could result in much more being incorrect. Below is the shader code:

Shader "Custom/cubemap"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _CubeMap("CubeMap", Cube) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
                float3 normal : NORMAL;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
                float3 worldNormal : TEXCOORD1;
                float3 worldVertex : TEXCOORD2;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;

            samplerCUBE _CubeMap;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                o.worldNormal = normalize(mul(unity_ObjectToWorld, float4(v.normal, 0))).xyz;
                o.worldVertex = mul(unity_ObjectToWorld, v.vertex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                // sample the texture
                //fixed4 col = tex2D(_MainTex, i.uv);
                half3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldVertex));
                //half3 viewDir = normalize(i.worldVertex.xyz - _WorldSpaceCameraPos.xyz);
                fixed3 reflection = reflect(-viewDir, i.worldNormal);
                fixed4 col = texCUBElod(_CubeMap, float4(reflection,1));
                return col;
            }
            ENDCG
        }
    }
}

If you’re trying to reproduce the metal effect from Mario 64, then you don’t want a Cubemap at all. You want a Matcap.

You can download a free asset from the Unity Asset store that implements a bunch of shaders setup to use them here:

If you really want to write this yourself, here’s an example shader I posted about some alternative techniques to the usual default implementation, which is to use the view space normals.

(With this tweet showing the difference between the implementations.)

However if you want to stick with Cubemaps, then the key bit of code is:
float3 viewSpaceReflection = mul((float3x3)UNITY_MATRIX_V, reflection);
Note you may also need to add this line for it to look right, as view space Z is flipped from world space:
viewSpaceNormal.z = -viewSpaceReflection.z;

Thank you bgolus, as usual you are a lighthouse in a sea of shader black magic.

It was my understanding they used cubespheres in mario 64 but of course we dont really use those now as they are much more expensive and less accurate than a cubemap.
I’ll need to mess around with matcaps more to fully understand them but on the cubemap code could you give a bit more context on how I should be using that float3?
Also while you are here do you know if the “shimmer” effect used in OOT for things like the gemstones is a similar technique?

8384973--1105935--original.gif

I’ve not heard the term “cubespheres” before. They use spherical environment maps, or “sphere maps”, which makes sense seeing as the N64 hardware is derived from SGI hardware and sphere maps were (and kind of still are) the defacto way of capturing a real world environment map quickly. The texture for it is effectively what you’d get from a photo of a chrome sphere.

Rigs like this have been standard equipment in the film industry for decades now. Toss them in front of the camera before a take and you’ve got easy reference for lighting and image grading to get your CG image to match what was filmed.

The funny thing is you say sphere maps are “expensive”. They didn’t use cubemaps back then because they were (and technically still are) much more expensive. The difference is today’s GPUs are way faster and have built in support for cubemaps and way more memory, so it’s a non issue today.

Matcaps are a cheap method that’s very similar to sphere maps, which is why I recommended them.

That’s the most likely way they were done, yes. Probably using a texture with a bunch of random lines across it. You can probably even find some websites with those textures already extracted from OoT if you’re curious what exactly they look like.

Yeah no clue where I got cubespheres from, my brain just mashed together cubemap and spheremap I guess. Knowing cubemaps efficiency is interesting as most people online make a blanket statement that they are better than spheres without explaining why like you did here. They just say they run faster and leave it at that.

But anyways thanks for all the help I’ll dive in to your suggestions!

So I dug into N64 spheremaps to see exactly how they work.

As best I can tell they’re not “like” matcaps, they literally are matcaps. The N64 documentation is extra techncial, but the SGI documentation is pretty straightforward.

uv.x = viewNormal.x / 2 + 0.5
uv.y = viewNormal.y / 2 + 0.5

Which is a matcap.

Amazing, where did you find that exact piece of code in the documentation? SGI workstation documentation brings up a very large list of things on google.

Sadly I can’t find the page I found this on now. I also think I was wrong… or more specifically I think that SGI documentation was wrong.

Digging around some more I found Spheremaps use a slightly more complex math than this, though in an orthographic view it simplifies down to be the same!

However the full math looks something like this:

float3 viewDir = normalize(i.worldPos - _WorldSpaceCameraPos.xyz);
float3 normalWS = normalize(i.worldNormal);
float3 reflectionWS = reflect(viewDir, normalWS);
float3 reflectionVS = mul((float3x3)UNITY_MATRIX_V, reflectionWS);
float m = 2 * sqrt(reflectionVS.x*reflectionVS.x + reflectionVS.y*reflectionVS.y + pow(reflectionVS.z+1,2));
float2 spheremapUV = reflectionVS.xy / m + 0.5;
half4 col = tex2D(_SphereMap, spheremapUV);

With a mat-cap like reflection texture it almost 1:1 matches a cube map!

1 Like

Thats the good stuff, thank you bgolus!