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"

StyleHow it turnsGood for
Copy cameraMatches the camera's rotation exactlyName tags, icons, health bars
UprightOnly spins left/right, never tiltsTrees, Doom-style enemies, characters
Look at cameraPoints straight at the camera, tilting tooGlowing 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
        }
    }
}
Find object centreGet camera right & upRebuild cornersDraw
The quad's corners are rebuilt from the camera's directions every frame, on the GPU.

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?

ScriptShader
Easy to set upYesNeeds shader knowledge
Works with SpriteRendererYesNo (use a Quad)
Colliders turn tooYesNo, only the picture turns
Thousands of objectsGets slowFast

Start with the script. Switch to the shader only if the Profiler shows the script is a problem.

Common Mistakes

See also