Skip to content

Instantly share code, notes, and snippets.

@lucasteles
Created June 5, 2026 19:12
Show Gist options
  • Select an option

  • Save lucasteles/074730548267988c23b41655aa8ee8c1 to your computer and use it in GitHub Desktop.

Select an option

Save lucasteles/074730548267988c23b41655aa8ee8c1 to your computer and use it in GitHub Desktop.
Godot Billboard "2.5D" Node
public enum FacingSide : sbyte
{
None = 0,
Left = -1,
Right = 1,
}
using System.Runtime.CompilerServices;
public partial class Node25D : Node3D
{
[Export]
public Camera3D Camera;
[Export]
protected bool mirror = true;
[Export]
public FacingSide FacingSide { get; private set; } = FacingSide.Right;
[ExportGroup("Billboard", "billboard")]
[Export]
bool billboardEnabled = true;
[Export(PropertyHint.Range, "0,1")]
float billboardWeight = 0.5f;
bool lookUseModelFront = true;
Vector3 lookModelUp = Vector3.Up;
public void SetFacingSide(FacingSide side)
{
if (FacingSide == side) return;
FacingSide = side;
ApplyFacingSide();
}
public override void _Ready()
{
if (!IsInstanceValid(Camera))
Camera = GetViewport().GetCamera3D();
ApplyFacingSide();
}
public override void _PhysicsProcess(double delta) => LookAtCamera();
void ApplyFacingSide()
{
var basis = GetBasis();
if (FacingSide is FacingSide.Left)
{
basis.X = Vector3.Left;
lookUseModelFront = false;
lookModelUp = Vector3.Down;
}
else
{
basis.X = Vector3.Right;
lookUseModelFront = true;
lookModelUp = Vector3.Up;
}
SetBasis(basis);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
void LookAtCamera()
{
if (!billboardEnabled) return;
var targetX = Mathf.Lerp(GlobalPosition.X, Camera.GlobalPosition.X, billboardWeight);
Vector3 target = new(targetX, GlobalPosition.Y, Camera.GlobalPosition.Z);
LookAt(target, lookModelUp, lookUseModelFront);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment