I’m enjoying playing through my old Nintendo Wii titles with my six-year old son – he’s into Super Mario Galaxy, but I have to help him with the tricky levels (naturally, I’m happy to oblige!). One such level is in the “Haunty Halls” galaxy, in which the level is mostly invisible, and the floor in the immediate vicinity of the player only becomes visible after Yoshi eats a bulb berry. The player must then quickly navigate through the level to find the next berry before the visible area shrinks and the player falls into darkness.

I thought I’d have a go at a similar effect in Unity, and came up with the following CG shader:
Shader "Custom/Proximity" {
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {} // Regular object texture
_PlayerPosition ("Player Position", vector) = (0,0,0,0) // The location of the player - will be set by script
_VisibleDistance ("Visibility Distance", float) = 10.0 // How close does the player have to be to make object visible
_OutlineWidth ("Outline Width", float) = 3.0 // Used to add an outline around visible area a la Mario Galaxy
_OutlineColour ("Outline Colour", color) = (1.0,1.0,0.0,1.0) // Colour of the outline
}
SubShader {
Tags { "RenderType"="Transparent" "Queue"="Transparent" }
Pass {
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// Access the shaderlab properties
uniform sampler2D _MainTex;
uniform float4 _PlayerPosition;
uniform float _VisibleDistance;
uniform float _OutlineWidth;
uniform fixed4 _OutlineColour;
// Input to vertex shader
struct vertexInput {
float4 vertex : POSITION;
float4 texcoord : TEXCOORD0;
};
// Input to fragment shader
struct vertexOutput {
float4 pos : SV_POSITION;
float4 position_in_world_space : TEXCOORD0;
float4 tex : TEXCOORD1;
};
// VERTEX SHADER
vertexOutput vert(vertexInput input)
{
vertexOutput output;
output.pos = mul(UNITY_MATRIX_MVP, input.vertex);
output.position_in_world_space = mul(_Object2World, input.vertex);
output.tex = input.texcoord;
return output;
}
// FRAGMENT SHADER
float4 frag(vertexOutput input) : COLOR
{
// Calculate distance to player position
float dist = distance(input.position_in_world_space, _PlayerPosition);
// Return appropriate colour
if (dist < _VisibleDistance) {
return tex2D(_MainTex, float2(input.tex)); // Visible
}
else if (dist < _VisibleDistance + _OutlineWidth) {
return _OutlineColour; // Edge of visible range
}
else {
float4 tex = tex2D(_MainTex, float2(input.tex)); // Outside visible range
tex.a = 0;
return tex;
}
}
ENDCG
} // End Pass
} // End Subshader
FallBack "Diffuse"
} // End Shader
It’s fairly self-explanatory – the key feature is that, in the vertex shader, the input vertex coordinates input.vertex (which are supplied in model coordinates) are multiplied by the Unity _Object2World matrix to turn them into world coordinates, output.position_in_world_space. Then, in the fragment shader, the CG distance function is used to calculate the distance from the current fragment to the player’s position, _PlayerPosition, and return one of three colours accordingly.
If the fragments outside visible range are set to semitransparent grey and the border colour set to yellow, it looks as follows:
In order to make the shader dynamic, the _PlayerPosition property must be updated programmatically through script, by attaching the following (Unity)script onto the object to which the shader is placed, and dragging the player object from the inspector onto the “player” property slot:
#pragma strict
// Take effect even in edit mode
@script ExecuteInEditMode()
// Get a reference to the player
public var player : Transform;
function Update ()
{
if (player != null) {
// Pass the player location to the shader
renderer.sharedMaterial.SetVector("_PlayerPosition", player.position);
}
}
And here’s a video of it in action:



Pingback: Animal Crossing Curved World Shader | Alastair Aitchison
Amazing shader! Didn’t know about the
Will be modifying it to obtain a slightly different effect.