Two textures on one surface (Revealing Light)

I’m trying to have a game where when the player shines a light on a surface it will refile some form of texture that was once not seen. Is there anyway to write a shader so when a object light shines on a certain object (like a cube) will swap out the texture but for only the bit the light is shining on?

That’s something that probably will solve your problem: it’s a special shader that makes visible a texture only inside the spot angle of a pseudo light. Explaining the “pseudo light”: I could not identify position and direction of a specific light at shader level, thus decided to fake it - this info is passed each frame to the shader by an auxiliary script.

The shader takes the hidden texture’s alpha channel into account, thus only its opaque pixels appear - this is useful for making hidden text messages (for instance) appear when illuminated by the “magic light”:

Shader "Custom/Hidden Texture" { 
Properties {
    _MainTex ("Base (RGB)", 2D) = "white" { }
    _SpotAngle ("Spot Angle", Float) = 30.0
    _Range ("Range", Float) = 5.0
    _Contrast ("Contrast", Range (20.0, 80.0)) = 50.0
} 

Subshader {
    Tags {"RenderType"="Transparent" "Queue"="Transparent"}
    Pass {
        Blend SrcAlpha OneMinusSrcAlpha
        ZTest LEqual
        
        CGPROGRAM
        #pragma vertex vert
        #pragma fragment frag
        #include "UnityCG.cginc"

        uniform sampler2D _MainTex;
        uniform float4 _LightPos; // light world position - set via script
        uniform float4 _LightDir; // light world direction - set via script
        uniform float _SpotAngle; // spotlight angle
        uniform float _Range; // spotlight range
        uniform float _Contrast; // adjusts contrast
        
        struct v2f_interpolated {
            float4 pos : SV_POSITION;
            float2 texCoord : TEXCOORD0;
            float3 lightDir : TEXCOORD1;     
        };    

        v2f_interpolated vert(appdata_full v){
            v2f_interpolated o;
            o.texCoord.xy = v.texcoord.xy;
            o.pos = mul(UNITY_MATRIX_MVP,  v.vertex);
            half3 worldSpaceVertex = mul(_Object2World, v.vertex).xyz;
            // calculate light direction to vertex    
            o.lightDir = worldSpaceVertex-_LightPos.xyz;
            return o;
        }

        half4 frag(v2f_interpolated i) : COLOR {
            half dist = saturate(1-(length(i.lightDir)/_Range)); // get distance factor
            half cosLightDir = dot(normalize(i.lightDir), normalize(_LightDir)); // get light angle
            half ang = cosLightDir-cos(radians(_SpotAngle/2)); // calculate angle factor
            half alpha = saturate(dist * ang * _Contrast); // combine distance, angle and contrast
            half4 c = tex2D(_MainTex, i.texCoord); // get texel
            c.a *= alpha; // combine texel and calculated alpha
            return c;    
        }
        ENDCG
    }    
}
}

Save this shader as “HiddenTexture.shader” (or other suitable name) in some Assets subfolder, select it in the Project view and click Create->Material - this creates the hidden texture material. Assign it to the object, then add a second material, which actually will behave as the main one: it appears all the time, and is covered by the hidden material only when the “light” is over it.

As I said, the shader actually doesn’t care about the actual lights: you must inform to it the position and direction of the light that reveals the hidden texture. A simple way to do this is to make the object find the light object at Start, and update its shader each frame in Update, like below (script attached to the object that has the hidden texture):

var tfLight: Transform;

function Start () {
    // find the revealing light named "RevealingLight":
    var goLight = GameObject.Find("RevealingLight");
    if (goLight) tfLight = goLight.transform;
}

function Update () {
    if (tfLight){
        renderer.material.SetVector("_LightPos", tfLight.position);
        renderer.material.SetVector("_LightDir", tfLight.forward);
    }
}

NOTES:

1- The “RevealingLight” object in the script above is a game object with this name, which has a spot light. As mentioned in the text, the light itself is just a “cosmetic” aid: the position and direction of the object is what really matters for the shader. All objects that have a “hidden texture” material must have this script attached, so that they can keep track of the light. If more than one revealing light exists, a different approach should be used instead: the light object should cast a ray in its forward direction, and pass the light info to the hit object case it has a hidden texture (some specific tag could mark these objects, for instance).

2- Two materials are used in each object: the first one is the hidden material, and the second is actually the main material - it appears all the time until the light aims at the object, revealing the hidden texture. This approach allows to use almost any shader in the main material.

3- The hidden texture alpha is preserved - if you want to make something like a symbol or message painted with invisible ink being revealed by the light, just make the texture background transparent in the image editor: the main texture will show through the transparent areas even under the “magic” light - like this:

6778-hiddenmessage.png

This is doable but non-trivial. You will need to make a custom frag shader that can test whether the light is shining on a particular texel and if it is then sample the 2nd texture for that texel.

To determine whether a spot light is shining on it you would have to know the position and direction of the spotlight and the position of the texel you’re currently operating on in the shader. From there you can calculate the angle between the light and your point on the wall. If it is less than the angle of your light, then you could consider it to be in the light (maybe use maximum distance away too).

As for getting the position of the texel, you could pass it from the vertex to the frag shader so it will get interpolated for every texel.

I have a very limited understanding of shaders. @Insomix’s solutions sounds like the “right” solution, but it occured to me that maybe this behavior could be faked. That is write a shader that has a diffuse texture combined with an overlay texture. Without light, just the overlay texture is displayed. With light the diffuse texture plays an increasing role:

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

    SubShader {
        Pass {
            Material {
                Diffuse [_Color]
            }
            Lighting On
            SetTexture [_MainTex] {
                constantColor [_Color]
                Combine texture * primary DOUBLE, texture * constant
            }
            SetTexture [_OverTex]
            {
            Combine previous + texture
            }
		}
    }
}

The overlay texture needs to be fairly dark. Changing the operator used in the final Combine chages the behavior in ways that might be useful. Maybe someone with more shader exerience can suggest changes to move it closer to your ideal.