- Home /
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?
The problem there would be having two objects next to each other that have different textures.
Answer by aldonaletto · Jan 16, 2013 at 04:40 AM
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:
Logged in just to upvote this answer, 28. September 2017. , Unity 2017.1.1 this still works perfectly!
Thanks a lot!
Also just logged in to say - you're a hero, great answer! (and you seem to answer pretty much every question I google for Unity!)
@aldonaletto This is really cool, I've got a version working as above, but I'm trying to expand upon the idea...
Could I ask 2 questions please? - how would I change the size of the area that is revealed? - I tried to implement multiple lights - I changed the RevealingLight code to loop through an array of objects but only 1 of the objects locations is revealed by the shader, do you know what I'm doing wrong?
void Update () {
foreach (Transform t in RevealPseudoLights) {
Rend.material.SetVector ("_LightPos", t.position);
Rend.material.SetVector ("_LightDir", t.forward);
}
}
Any help at all would be fantastic! Thank you!
@JohnEvelyn This shader calculates the area that the revealing spot light illu$$anonymous$$ates and makes the material visible in such area. A spot light generates a cone of light (as shown in this legacy doc), and you can control its size with the shader parameter _SpotAngle
$$anonymous$$y script is intended to find a single revealing light and keep track of it. If you have several revealing lights in a list, each foreach iteration will overwrite the revealing light position and direction in the shader, thus only the last one in the list will take effect. A simple (but not 100% correct) solution would be to discard the revealing lights that were too far or pointing in the wrong direction - for instance:
public float range = 5f;
public float angle = 30f;
void Update(){
foreach (Transform t in RevealPseudoLights){
Vector3 dir = transform.position - t.position; // object direction relative to light
float dist = dir.magnitude; // object distance from light position
// only update shader parameters if object inside light range and angle
if (dist<=range && Vector3.Angle(dir, t.forward)<=angle/2){
Rend.material.SetVector("_LightPos", t.position);
Rend.material.SetVector("_LightDir", t.forward);
}
}
}
@aldonaletto Fantastic! thank you so much, that's a really clear explanation
This is awesome, but I'm very new to Unity and I'm having trouble getting the script you mentioned to work. What am I doing wrong?
using UnityEngine;
using System.Collections;
public class LightReveal2 : $$anonymous$$onoBehaviour {
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);
}
}
}
I'm getting an error on line 8 that says the ':' and ';' are "unexpected"
Haha, I'm kind of slow and I figured it out. It's a javascript that you posted, not a C# script! Now it works perfectly. Thanks!
Answer by insominx · Jan 15, 2013 at 10:50 PM
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.
Answer by robertbu · Jan 15, 2013 at 11:35 PM
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.
You could also make a very simple shader that just checks the intensity of the light and does something like: if (lightIntensity > someValue) { color = mySecondTextureColor; } else { color = myFirstTextureColor; }