Camera-facing Billboards
Make a flat picture always turn to face the camera, as used for trees, enemy sprites, name tags and health bars in many 3D games.
Picture someone at a stadium holding up a cardboard sign. As you walk around them, they keep turning the sign so you can always read it. A billboard does the same with a flat image in your game.
Three kinds of "facing"
| Style | How it turns | Good for |
|---|---|---|
| Copy camera | Matches the camera's rotation exactly | Name tags, icons, health bars |
| Upright | Only spins left/right, never tilts | Trees, Doom-style enemies, characters |
| Look at camera | Points straight at the camera, tilting too | Glowing orbs, simple effects |
1. The easy way: a script
Drag a sprite (or a Quad) into the scene and attach this. It works with any pipeline.
using UnityEngine;
public class Billboard : MonoBehaviour
{
[SerializeField] private bool stayUpright = true;
private Transform cam;
void Start() => cam = Camera.main.transform;
// LateUpdate runs after the camera has moved this frame, so there's no lag
void LateUpdate()
{
if (!stayUpright)
{
transform.rotation = cam.rotation; // copy the camera
return;
}
Vector3 away = transform.position - cam.position; // arrow from camera to me
away.y = 0; // ignore height, stay upright
if (away.sqrMagnitude > 0.001f)
transform.rotation = Quaternion.LookRotation(away);
}
}
Why "away" and not "toward"? A sprite's visible front faces its local -Z. Pointing its +Z away from the camera means the front faces the camera. If your image appears mirrored or invisible, rotate the child model 180° on Y.
2. The fast way: a shader
With thousands of billboards (a forest, a particle-like crowd), running a script on each one gets slow. A shader can do the turning on the GPU instead. The object's Transform never changes; only the drawn image turns.
Use this with a Quad (not a SpriteRenderer) in a URP project. Read Your First HLSL Shader first. Only Vert is new.
Shader "Simple/Billboard"
{
Properties
{
_BaseMap ("Texture", 2D) = "white" {}
_BaseColor ("Tint", Color) = (1, 1, 1, 1)
_Cutoff ("Alpha Cutoff", Range(0, 1)) = 0.5
}
SubShader
{
Tags { "RenderPipeline"="UniversalPipeline" "Queue"="AlphaTest"
"RenderType"="TransparentCutout" "DisableBatching"="True" }
Pass
{
Cull Off
HLSLPROGRAM
#pragma vertex Vert
#pragma fragment Frag
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
struct Attributes { float4 positionOS : POSITION; float2 uv : TEXCOORD0; };
struct Varyings { float4 positionCS : SV_POSITION; float2 uv : TEXCOORD0; };
TEXTURE2D(_BaseMap); SAMPLER(sampler_BaseMap);
CBUFFER_START(UnityPerMaterial)
float4 _BaseMap_ST; half4 _BaseColor; half _Cutoff;
CBUFFER_END
Varyings Vert(Attributes input)
{
Varyings output;
// Where the object's centre is in the world
float3 center = TransformObjectToWorld(float3(0, 0, 0));
// The camera's right and up directions
float3 camRight = UNITY_MATRIX_I_V._m00_m10_m20;
float3 camUp = UNITY_MATRIX_I_V._m01_m11_m21;
// Rebuild each corner using the camera's directions
float3 world = center + camRight * input.positionOS.x
+ camUp * input.positionOS.y;
output.positionCS = TransformWorldToHClip(world);
output.uv = TRANSFORM_TEX(input.uv, _BaseMap);
return output;
}
half4 Frag(Varyings input) : SV_Target
{
half4 c = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv) * _BaseColor;
clip(c.a - _Cutoff); // throw away see-through pixels
return half4(c.rgb, 1);
}
ENDHLSL
}
}
}
This version ignores the object's scale and rotation to stay short. Scale the image by multiplying positionOS.x and .y by a size property.
Script or shader?
| Script | Shader | |
|---|---|---|
| Easy to set up | Yes | Needs shader knowledge |
| Works with SpriteRenderer | Yes | No (use a Quad) |
| Colliders turn too | Yes | No, only the picture turns |
| Thousands of objects | Gets slow | Fast |
Start with the script. Switch to the shader only if the Profiler shows the script is a problem.
Common Mistakes
- Using
transform.LookAt(camera). It points the back of a sprite at you, so it looks mirrored or invisible. - Rotating in
Updateinstead ofLateUpdate, which causes a one-frame wobble when the camera moves. - Shader version vanishing at screen edges: Unity decides visibility using the original flat shape. Make the Quad's bounds bigger with
meshRenderer.localBoundsif this happens. - Leaving Static ticked on shader billboards. Static batching merges them and breaks the effect.
See also
- Your First HLSL Shader
- Working with Particle Systems (particles are billboards too)
- Quaternion
This page is a simplified, AI-assisted interpretation. For the complete and authoritative reference, see the official documentation.