Billboard Reflection

Hi, fellow Unity devs :slight_smile:
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

http://forum.unity3d.com/threads/113784-Has-any-one-experimented-with-Box-Projection-Correction-Environment-Mapping

Very promising and thank you for sharing code !

Would you mind sharing more info about how you send your parameters ? Maybe a scene setup ?

So far this is my attempt to make it work :

I’ve changed the way you did the ray to have a more physical approach.

I’m using a plane as a source for the billboard ( just the size,scale etc no image on it atm ) but to get the reflection facing me I need to rotate the plane in the wrong way also the size is wrong.

And here is my C# to send the data.

using UnityEngine;
using System.Collections;

[ExecuteInEditMode]
public class Billboard : MonoBehaviour 
{
	
	// Billboard
	
  	protected Vector3 m_right = new Vector3(1, 0, 0);
 	protected Vector3 m_up = new Vector3(0, 1, 0);
	
	//public Mesh plane;
	public GameObject vertice;
	public Vector3 quadLLPos;
	public Vector3 quadX;
	public Vector3 quadY;
	public Vector2 quadScale;
	public Texture2D billboard;

	void goVariable()
	{
		
		Shader.SetGlobalVector("_QuadLLPos",quadLLPos);
		Shader.SetGlobalVector("_QuadX",quadX);
		Shader.SetGlobalVector("_QuadY",quadY);
		Shader.SetGlobalVector("_QuadScale",quadScale);
		Shader.SetGlobalTexture("_Billboard",billboard);

	}

	// Use this for initialization
	void Start () 
	{
	
	}
	
	// Update is called once per frame
	void Update () 
	{
		quadLLPos = vertice.transform.position;
		
		quadX = this.transform.localToWorldMatrix.MultiplyVector(m_right);
    	quadX.Normalize();
		
		quadY = this.transform.localToWorldMatrix.MultiplyVector(m_up);
    	quadY.Normalize();
		
		quadScale = this.transform.localScale;
		goVariable();
	}
}

Thanks

Ok using a cube as shape works better :

But having a sample scene would help a lot :).

Sorry I didn’t respond earlier. I forgot about this thread and I kind of gave up on this effect for now after knowing that you can’t send arrays to shaders in Unity. I tried to used this for multiple billboards on my projects by stacking planes using this shader with transparency (a pretty dirty workaround I’ll give you that but for now, with 5 billboards, there is no noticable performance cost).

Thanks for your interst anyway :slight_smile: For the billboard source, I used Unity’s default “quad” and not the “plane”, and you should use that instead since I’ve based my “QuadX” and “QuadY” axis on their axis (the Quad’s normal is on -z while the plane’s is on +y) . Also it has less faces than the plane. I understand it’s quite an arbitrary decision so I’ll try to make it adapt to the normal of any planes…
Huh, forget about it actually, I now recall that I’ve changed my shader code so now you decide which Vector is your plane’s normal and you just give it the inverse matrix as well:

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 _QuadPosition;
fixed4 _QuadNormal;
float4x4 _QuadInverseMatrix;

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
    half3 boxStart = _BoxPosition - _BoxSize / 2.0;
    half3 firstPlaneIntersect = (boxStart + _BoxSize - IN.worldPos) / nReflDirection;
    half3 secondPlaneIntersect = (boxStart - IN.worldPos) / nReflDirection;
    half3 furthestPlane = (nReflDirection > 0.0) ? firstPlaneIntersect : secondPlaneIntersect;
    half3 intersectDistance = min(min(furthestPlane.x, furthestPlane.y), furthestPlane.z);
    half3 intersectPosition = IN.worldPos + nReflDirection * intersectDistance;
    fixed4 reflcol = texCUBE(_Cube, intersectPosition - _BoxPosition);
    
    // Ray-quad intersection
    half planeIntersectDistance = (dot(IN.worldPos - _QuadPosition, _QuadNormal.xyz) / dot(nReflDirection, _QuadNormal.xyz));
    intersectPosition = IN.worldPos - nReflDirection * planeIntersectDistance;
    half4 localPlaneIntersectPosition = mul(_QuadInverseMatrix, float4(intersectPosition, 1.0));
    half2 billboardUV = half2(localPlaneIntersectPosition.x, localPlaneIntersectPosition.y);
    fixed4 billboardCol = tex2D(_Billboard, billboardUV);
    
    half reflectDot = dot(nReflDirection, _QuadNormal.xyz);
    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  _QuadNormal.w == 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"
}

And this is the C# script that I attach to the source billboard:

using UnityEngine;
using System.Collections;

[ExecuteInEditMode]
public class ReflectionBillboardScript : MonoBehaviour {

	public Material[] materials;
	public bool backfaceCulling = true;

	private Mesh mesh;
	private Transform myTransform;
	private Matrix4x4 transformMatrix;
	private Vector3 quadNormal;

	void Update () {
		myTransform = transform;
		mesh = GetComponent<MeshFilter>().sharedMesh;

		quadNormal = -1.0F * myTransform.forward;

		transformMatrix.SetTRS(myTransform.TransformPoint(mesh.vertices[0]), myTransform.rotation, myTransform.localScale);

		for (int i = 0; i < materials.Length; i++) {
			materials[i].SetTexture("_Billboard", renderer.sharedMaterial.mainTexture);
		}

		for (int i = 0; i < materials.Length; i++) {
			materials[i].SetVector("_QuadPosition", myTransform.TransformPoint(mesh.vertices[0]));
			materials[i].SetVector("_QuadNormal", new Vector4(quadNormal.x, quadNormal.y, quadNormal.z, backfaceCulling ? 1.0F : 0.0F));
			materials[i].SetMatrix("_QuadInverseMatrix", transformMatrix.inverse);
		}
	}
}

“Material[ ] materials” are all the materials in which this billboard is reflected.
Also i’m still using Unity’s quad here so I’ve assigned my “quadNormal” value to the opposite of the quad’s forward (you can change it to “transform.up” if you’re using a Unity plane). The code should execute in edit mode but you might need to hit play or reload your scene for some updates.

I’ll try to upload a simple example scene.

Hey!

Thanks for the details !

I’ve myself switch to quad so it’s perfect.

For arrays it’s true but there’s work around : http://forum.unity3d.com/threads/223100-Image-Based-Reflection

I’ve tested Dolkar’s idea and it worked but how I did it was really tricky, with a better script I’m sur it can be done.

Yeah I’ve seen that you can use arrays that way but I think it is still too limited… Maybe for now we can make a fixed sized array which sets the limit to the number of billboards per reflective material and only assign through script what we need.

Also I’ve been wondering: is your reflection distored because of a normal map ?

PS : We should also add an “actual array length” property to the shader so that it doesn’t loop more than necessary.

I’ve changed my ray to match GGX D term so I can have different roughness :slight_smile: I also use the mip map of the image. I will share my code.

Yeah that would be nice to set array length ( 8 ? ). With my test I’ve tested two billboard and works really fine but still I got an issue with the masking ( only one mask working ), did you run into that issue ?

That’s really awesome!

Just wondering, can it be used to get less expensive reflections on a waterplane for example?

Would be nice also to setup a kind of capture gameobject that can save to disk the image reflection you want to capture ( with an Ortho camera ) in RGBM.

indiegamemodels : That would work yes and combined with a cubemap that will give you some nice effect.

Thanks for your quick answer! Definately something I will look into for my game (but I don’t use Unity)

Just thinking, but being able to use a static cubemap for static objects and this method for dynamic objects should give a great result for a very low performance cost!

Does this use unity pro?(didn’t see any use render texture in the script)

It should work with unity free.