Hi, fellow Unity devs ![]()
I’ve been trying to implement the “billboard reflection” technique that you can see in the Unreal Samaritan Demo and now known as “Image-based reflection” in UDK.
It’s basically a textured quad that represents an object and is only rendered in reflections. It is less accurate but cheaper than the MirrorReflection shader since, unlike the latter, it doesn’t require a RenderTexture and is directly rendered from a texture like the built-in decal shader (it also means that you have to place a quad for everything that you want to reflect).
It is used in conjunction with the Box Projected Cubemap shader (I’ve written the shader on top of it).
Since I’m not too good with maths and I’m pretty much a newbie in shader language, I’ve been struggling with this but I’ve finally managed to implement the core of this feature. Here’s the result so far:
And here’s the code for the shader:
Shader "Custom/BPCEM with billboard reflection" {
Properties {
_Color ("Main Color", Color) = (1,1,1,1)
_ReflectColor ("Reflection Color", Color) = (1,1,1,0.5)
_MainTex ("Base (RGB) RefStrength (A)", 2D) = "white" {}
_Cube ("Reflection Cubemap", Cube) = "_Skybox" { TexGen CubeReflect }
_BumpMap ("Normalmap", 2D) = "bump" {}
_BoxPosition ("Bounding Box Position", Vector) = (0, 0, 0)
_BoxSize ("Bounding Box Size", Vector) = (10, 10, 10)
}
SubShader {
Tags { "RenderType"="Opaque" }
LOD 300
CGPROGRAM
#pragma target 3.0
#pragma surface surf Lambert
sampler2D _MainTex;
sampler2D _BumpMap;
sampler2D _Billboard;
samplerCUBE _Cube;
fixed4 _Color;
fixed4 _ReflectColor;
float3 _BoxSize;
float3 _BoxPosition;
float3 _QuadLLPos;
fixed3 _QuadX;
fixed3 _QuadY;
float2 _QuadScale;
fixed _Culling;
struct Input {
float2 uv_MainTex;
float2 uv_BumpMap;
fixed3 worldPos;
float3 worldNormal;
INTERNAL_DATA
};
void surf (Input IN, inout SurfaceOutput o) {
// Base diffuse texture
fixed4 tex = tex2D(_MainTex, IN.uv_MainTex);
fixed4 c = tex * _Color;
o.Albedo = c.rgb;
fixed3 n = UnpackNormal(tex2D(_BumpMap, IN.uv_BumpMap));
// Reflection-ray
float3 viewDir = IN.worldPos - _WorldSpaceCameraPos;
float3 worldNorm = IN.worldNormal;
worldNorm.xy -= n;
float3 reflectDir = reflect (viewDir, worldNorm);
fixed3 nReflDirection = normalize(reflectDir);
// Parallax correction
float3 boxStart = _BoxPosition - _BoxSize / 2.0;
float3 firstPlaneIntersect = (boxStart + _BoxSize - IN.worldPos) / nReflDirection;
float3 secondPlaneIntersect = (boxStart - IN.worldPos) / nReflDirection;
float3 furthestPlane = (nReflDirection > 0.0) ? firstPlaneIntersect : secondPlaneIntersect;
float3 intersectDistance = min(min(furthestPlane.x, furthestPlane.y), furthestPlane.z);
float3 intersectPosition = IN.worldPos + nReflDirection * intersectDistance;
fixed4 reflcol = texCUBE(_Cube, intersectPosition - _BoxPosition);
// Ray-Plane intersection
fixed3 quadNormal = cross(_QuadX, _QuadY);
float planeIntersectDistance = (dot(IN.worldPos - _QuadLLPos, quadNormal) / dot(nReflDirection, quadNormal));
intersectPosition = IN.worldPos - nReflDirection * planeIntersectDistance;
float3 localPlaneIntersectPosition = intersectPosition - _QuadLLPos;
float2 billboardUV = float2(dot(_QuadX, localPlaneIntersectPosition) / _QuadScale.x, dot(_QuadY, localPlaneIntersectPosition) / _QuadScale.y);
fixed4 billboardCol = tex2D(_Billboard, billboardUV);
float reflectDot = dot(nReflDirection, quadNormal);
fixed w = billboardCol.a;
w = billboardUV.x > 1.0 ? 0.0 : w;
w = billboardUV.y > 1.0 ? 0.0 : w;
w = billboardUV.x < 0.0 ? 0.0 : w;
w = billboardUV.y < 0.0 ? 0.0 : w;
w = reflectDot <= 0.0 _Culling == 1.0 ? 0.0 : w;
reflcol.rgb = lerp(reflcol.rgb, billboardCol.rgb, w);
// Reflection display
reflcol *= tex.a;
o.Emission = reflcol.rgb * _ReflectColor.rgb;
o.Alpha = reflcol.a * _ReflectColor.a;
}
ENDCG
}
FallBack "Reflective/VertexLit"
}
You need to provide the shader (with SetVector, SetFloat, etc.) the following parameters:
- “_QuadLLPos” the quad’s lower-left vertex position (in World Coordinates)
- “_QuadX” the quad’s “transform.right”
- “_QuadY” the quad’s “transform.up”
- “_QuadScale” the quad’s localScale (x and y) in Vector2
- “_Culling” should the reflection quad’s backface be culled ? (1.0 for cull, any other value for 2-sided)
Please refer to the Box Projected Cubemapping shader topic (referenced at the end of this post) for parameters related to it (although I just changed one of them, from “EnvBoxStart” to “BoxPosition” which is the bounding box’s center pos in world space).
The shader currently supports translation, rotation scaling, movietextures, backface culling and alpha testing.
However, it works only with one billboard for now. So I have to make it work with multiple quads, as well as taking into account the occlusion between them.
There’s still a lot to do and the code could use some optimization but I wanted to share this since I found nothing about how to implement it in Unity or concrete cg code when I started working on it.
References:
http://udn.epicgames.com/Three/rsrc/Three/DirectX11Rendering/MartinM_GDC11_DX11_presentation.pdf
