Skip to content

Instantly share code, notes, and snippets.

@Vavassor
Last active October 6, 2026 13:35
Show Gist options
  • Select an option

  • Save Vavassor/aebb358730211791fbedff0bb6e57b30 to your computer and use it in GitHub Desktop.

Select an option

Save Vavassor/aebb358730211791fbedff0bb6e57b30 to your computer and use it in GitHub Desktop.
// Billboards are a flat image or sprite that always face the camera.
//
// For a single billboard, put the material on Unity's Quad mesh. You may want to try enabling GPU instancing on the
// material, because this shader disables batching! Billboarding uses the object pivot, but batching combines meshes
// so they all have one pivot.
//
// To use multiple billboards in one mesh choose Vertex as the Pivot Mode. For this you need a mesh that is a bunch of
// separate quads, each scaled down to a point. In Unity mesh import settings, make sure "Weld Vertices" is
// unchecked so they don't get welded together. Then this shader can display the billboards.
//
// This shader is for Unity's built in render pipeline.
//
// MIT License
// Copyright (c) 2025 Andrew Dawson
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
// documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
// Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
// WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Shader "Orchid Seal/OSP Billboard/Unlit Billboard Standalone"
{
Properties
{
[Header(Rendering)]
_MainTex("Texture", 2D) = "white" {}
_Color("Tint", Color) = (1, 1, 1, 1)
[KeywordEnum(Multiply, Overlay)] _ColorMode("Tint Color Mode", Float) = 0
[Toggle(USE_GAMMA_COLORSPACE)] _UseGammaSpace("Use Gamma Space Blending", Float) = 0
[Toggle(USE_PIXEL_SHARPEN)] _UsePixelSharpen("Sharp Pixels", Float) = 0
[Header(Billboarding)]
[KeywordEnum(None, Spherical, Cylindrical_World, Cylindrical_Local)] _Billboard_Mode("Billboard Mode", Float) = 1
_Position("Position (XY)", Vector) = (0, 0, 0, 0)
_RotationRoll("Rotation", Float) = 0
_Scale("Scale (XY)", Vector) = (1, 1, 0, 0)
[Toggle(USE_NON_UNIFORM_SCALE)] _UseNonUniformScale("Use Non-Uniform Object Scale", Float) = 1
[Toggle(KEEP_CONSTANT_SCALING)] _KeepConstantScaling("Keep Constant Scaling", Int) = 0
_ConstantScale("Constant Scale", Float) = 1
[Toggle(FLIP_FACING_HORIZONTAL)] _FlipFacingHorizontal("Flip Facing Horizontal", Float) = 0
[KeywordEnum(Object,Vertex)] Pivot_Mode("Pivot Around", Integer) = 0
[Header(Flipbook)]
[Toggle(USE_FLIPBOOK)] _UseFlipbook("Enable Flipbook", Float) = 0
_FlipbookTexArray("Texture Array", 2DArray) = "" {}
_FlipbookTint("Tint", Color) = (1,1,1,1)
_FlipbookScrollVelocity("Scroll Velocity (XY)", Vector) = (0, 0, 0, 0)
[Enum(Replace,0,Transparent,1,Add,2,Multiply,3)] _FlipbookBlendMode("Blend Mode", Float) = 0
_FlipbookFramesPerSecond("Frames Per Second", Float) = 30
[Toggle(USE_FLIPBOOK_SMOOTHING)] _UseFlipbookSmoothing("Smoothing", Float) = 0
[Toggle] _FlipbookUseManualFrame("Control Frame Manually", Float) = 0
_FlipbookManualFrame("Manual Frame", Float) = 0
[Header(Outline)]
[Toggle(USE_OUTLINE)] _UseOutline("Enable Outline", Float) = 0
_OutlineColor("Outline Color", Color) = (1, 1, 1, 1)
_OutlineWidth("Outline Width", Float) = 1
[Header(Distance Fade)]
[Toggle(USE_DISTANCE_FADE)] _UseDistanceFade("Enable Distance Fade", Float) = 0
_DistanceFadeMinAlpha("Min Alpha", Range(0, 1)) = 1
_DistanceFadeMaxAlpha("Max Alpha", Range(0, 1)) = 0
_DistanceFadeMin("Min Distance", Float) = 0
_DistanceFadeMax("Max Distance", Float) = 10
[Header(Vertex Animation)]
[Toggle(USE_VERTEX_ANIMATION)] _UseVertexAnimation("Enable Vertex Animation", Float) = 0
[Toggle] _RandomizeAnimWorldPosition("Randomize Animation by World Position", Float) = 0
[Space]
[Toggle] _FloatOn("Float Enabled", Float) = 0
_FloatAmplitude("Float Amplitude", Float) = 0.15
_FloatAxis("Float Axis", Vector) = (0, 1, 0, 0)
_FloatFrequency("Float Frequency", Float) = 3
_FloatPhase("Float Phase", Float) = 0
[Space]
[Toggle] _ThrobOn("Throb Enabled", Float) = 0
_ThrobAmplitude("Throb Amplitude", Float) = 0.15
_ThrobFrequency("Throb Frequency", Float) = 5
_ThrobPhase("Throb Phase", Float) = 0
[Header(Alpha Test)]
[Toggle(USE_ALPHA_TEST)] _UseAlphaTest("Enable Alpha Test", Float) = 0
_AlphaCutoff("Alpha Cutoff", Float) = 0.5
[Toggle] _AlphaToMask("Alpha To Mask", Float) = 0
[Header(Blend)]
[Enum(UnityEngine.Rendering.BlendMode)] _BlendSrc("Source Blend", Float) = 5 //"SrcAlpha"
[Enum(UnityEngine.Rendering.BlendMode)] _BlendDst("Destination Blend", Float) = 10 //"OneMinusSrcAlpha"
[Enum(Add,0,Sub,1,RevSub,2,Min,3,Max,4)] _BlendOp("Blend Operation", Float) = 0 // "Add"
[Header(Depth Test)]
[Enum(UnityEngine.Rendering.CompareFunction)] _ZTest("ZTest", Float) = 4 //"LessEqual"
[Enum(Off,0,On,1)] _ZWrite("ZWrite", Float) = 0.0 //"Off"
[Header(Stencil)]
_StencilRef("Reference", Int) = 0
_StencilReadMask("Read Mask", Int) = 255
_StencilWriteMask("Write Mask", Int) = 255
[Enum(UnityEngine.Rendering.CompareFunction)] _StencilComp("Comparison", Float) = 8
[Enum(UnityEngine.Rendering.StencilOp)] _StencilPass("Pass", Float) = 0
[Enum(UnityEngine.Rendering.StencilOp)] _StencilFail("Fail", Float) = 0
[Enum(UnityEngine.Rendering.StencilOp)] _StencilZFail("ZFail", Float) = 0
}
SubShader
{
Tags
{
"Queue" = "Transparent"
"IgnoreProjector" = "True"
"RenderType" = "Transparent"
"DisableBatching" = "True"
"PreviewType" = "Plane"
}
Blend [_BlendSrc] [_BlendDst]
BlendOp [_BlendOp]
ZTest [_ZTest]
ZWrite [_ZWrite]
Cull Off
AlphaToMask [_AlphaToMask]
Stencil
{
Ref [_StencilRef]
ReadMask [_StencilReadMask]
WriteMask [_StencilWriteMask]
Comp [_StencilComp]
Pass [_StencilPass]
Fail [_StencilFail]
ZFail [_StencilZFail]
}
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fog
#pragma multi_compile_instancing
#pragma shader_feature_local_fragment _ _COLORMODE_MULTIPLY _COLORMODE_OVERLAY
#pragma shader_feature_local_vertex _ PIVOT_MODE_VERTEX
#pragma shader_feature_local USE_GAMMA_COLORSPACE
#pragma shader_feature_local USE_FLIPBOOK
#pragma shader_feature_local USE_FLIPBOOK_SMOOTHING
#pragma shader_feature_local_fragment USE_ALPHA_TEST
#pragma shader_feature_local_vertex _BILLBOARD_MODE_NONE _BILLBOARD_MODE_SPHERICAL _BILLBOARD_MODE_CYLINDRICAL_WORLD _BILLBOARD_MODE_CYLINDRICAL_LOCAL
#pragma shader_feature_local_vertex USE_NON_UNIFORM_SCALE
#pragma shader_feature_local_vertex KEEP_CONSTANT_SCALING
#pragma shader_feature_local USE_OUTLINE
#pragma shader_feature_local USE_DISTANCE_FADE
#pragma shader_feature_local_fragment USE_PIXEL_SHARPEN
#pragma shader_feature_local_vertex FLIP_FACING_HORIZONTAL
#pragma shader_feature_local_vertex USE_VERTEX_ANIMATION
#include "UnityCG.cginc"
// Billboard...........................................................................
float3 GetCenterCameraPosition()
{
#if defined(USING_STEREO_MATRICES)
return (unity_StereoWorldSpaceCameraPos[0] + unity_StereoWorldSpaceCameraPos[1]) / 2.0;
#else
return _WorldSpaceCameraPos.xyz;
#endif
}
float3x3 LookAtVerticalMatrix(float3 forward, float3 up)
{
float3 xAxis = normalize(cross(up, forward));
float3 yAxis = up;
float3 zAxis = forward;
return float3x3(
xAxis.x, yAxis.x, zAxis.x,
xAxis.y, yAxis.y, zAxis.y,
xAxis.z, yAxis.z, zAxis.z
);
}
float3x3 LookAtMatrix(float3 forward, float3 up)
{
float3 zAxis = forward;
float3 xAxis = normalize(cross(up, zAxis));
float3 yAxis = normalize(cross(zAxis, xAxis));
return float3x3(
xAxis.x, yAxis.x, zAxis.x,
xAxis.y, yAxis.y, zAxis.y,
xAxis.z, yAxis.z, zAxis.z
);
}
bool IsInMirror()
{
return unity_CameraProjection[2][0] != 0.0f || unity_CameraProjection[2][1] != 0.0f;
}
float3x3 GetBillboardRotation(float3 objectCenterWs)
{
float3x3 rotation;
#if _BILLBOARD_MODE_CYLINDRICAL_LOCAL
float3 upAxis = normalize(unity_ObjectToWorld._m01_m11_m21);
float3 forwardWs = normalize(objectCenterWs - GetCenterCameraPosition());
if (IsInMirror())
{
forwardWs = mul((float3x3) unity_WorldToObject, unity_CameraWorldClipPlanes[5].xyz);
}
rotation = LookAtVerticalMatrix(forwardWs, upAxis);
#elif _BILLBOARD_MODE_CYLINDRICAL_WORLD
float3 forwardWs = normalize(objectCenterWs - GetCenterCameraPosition());
if (IsInMirror())
{
forwardWs = mul((float3x3) unity_WorldToObject, unity_CameraWorldClipPlanes[5].xyz);
}
rotation = LookAtVerticalMatrix(forwardWs, float3(0, 1, 0));
#else
#if defined(USING_STEREO_MATRICES)
float3 up = float3(0, 1, 0);
float3 forward = normalize(objectCenterWs - GetCenterCameraPosition());
#else
float3 up = UNITY_MATRIX_V._m10_m11_m12;
float3 forward = -UNITY_MATRIX_V._m20_m21_m22;
#endif
rotation = LookAtMatrix(forward, up);
#endif
return rotation;
}
float4 BillboardCs(float4 positionOs, float3 billboardCenterWs)
{
#if _BILLBOARD_MODE_NONE
return UnityObjectToClipPos(positionOs);
#else
float3x3 rotation = GetBillboardRotation(billboardCenterWs);
return mul(UNITY_MATRIX_VP, float4(mul(rotation, positionOs) + billboardCenterWs, 1));
#endif
}
// Flipbooks...........................................................................
// The index in the z component of the result is non-integral so that the
// fractional part can be used for smoothing, if that's enabled.
float4 GetFlipbookTexcoord(Texture2DArray flipbook, float2 texcoord, float framesPerSecond, float useManualFrame, int manualFrame)
{
float width, height;
uint elementCount;
flipbook.GetDimensions(width, height, elementCount);
float frame = _Time.y * framesPerSecond / elementCount;
uint index = frac(frame) * elementCount;
index = lerp(index, manualFrame, useManualFrame);
uint nextIndex = (index + 1) % elementCount;
float4 flipbookTexcoord = float4(texcoord, index + frac(frame), nextIndex);
return flipbookTexcoord;
}
// Fog.................................................................................
#if (defined(FOG_LINEAR) || defined(FOG_EXP) || defined(FOG_EXP2))
// This transfers fog using radial distance so that it works in mirrors.
fixed TransferFog(float4 clipPosition)
{
float3 viewPosition = UnityObjectToViewPos(clipPosition);
float fogCoord = length(viewPosition.xyz);
UNITY_CALC_FOG_FACTOR_RAW(fogCoord);
return saturate(unityFogFactor);
}
#define OSP_APPLY_FOG(col, input) col.rgb = lerp(unity_FogColor.rgb, col.rgb, input.fog);
#define OSP_FOG_COORDS(texcoordNumber) fixed fog : TEXCOORD##texcoordNumber;
#define OSP_TRANSFER_FOG(clipPosition, output) output.fog = TransferFog(clipPosition);
#else
#define OSP_APPLY_FOG(col, input)
#define OSP_FOG_COORDS(texcoordNumber)
#define OSP_TRANSFER_FOG(clipPosition, output)
#endif
// Pixel Sharpen.......................................................................
// https://github.com/cnlohr/shadertrixx/blob/main/README.md#lyuma-beautiful-retro-pixels-technique
float2 SharpenPixelUv(float2 uv, float4 texelSize)
{
float2 coord = uv.xy * texelSize.zw;
float2 fr = frac(coord + 0.5);
float2 fw = max(abs(ddx(coord)), abs(ddy(coord)));
return uv.xy + (saturate((fr-(1-fw)*0.5)/fw) - fr) * texelSize.xy;
}
// Transformation......................................................................
float2 Rotate2D(float2 v, float angle)
{
float s, c;
sincos(angle, s, c);
return float2(c * v.x - s * v.y, s * v.x + c * v.y);
}
float2 Transform2d(float2 position, float2 translation, float rotationDegrees, float2 scale)
{
return scale * Rotate2D(position.xy, radians(rotationDegrees)) + translation;
}
// Blending............................................................................
#define BLEND_MODE_REPLACE 0
#define BLEND_MODE_TRANSPARENT 1
#define BLEND_MODE_ADD 2
#define BLEND_MODE_MULTIPLY 3
float4 BlendColor(float4 s, float4 d, float blendMode)
{
switch (blendMode)
{
case BLEND_MODE_REPLACE: return s;
case BLEND_MODE_TRANSPARENT:
{
float resultAlpha = s.a + d.a * (1.0 - s.a);
float3 resultRgb = (s.a * s.rgb + (1.0 - s.a) * d.a * d.rgb) / resultAlpha;
return float4(resultRgb, resultAlpha);
}
case BLEND_MODE_ADD: return s + d;
case BLEND_MODE_MULTIPLY: return s * d;
default: return 0;
}
}
float BlendAlpha(float s, float d, float blendMode)
{
switch (blendMode)
{
case BLEND_MODE_REPLACE: return s;
case BLEND_MODE_TRANSPARENT: return s + d * (1.0 - s);
case BLEND_MODE_ADD: return s + d;
case BLEND_MODE_MULTIPLY: return s * d;
default: return 0;
}
}
// Color Spaces........................................................................
#if defined(UNITY_COLORSPACE_GAMMA) || !defined(USE_GAMMA_COLORSPACE)
#define TO_SHADING_COLOR_SPACE(col) col
#else
#define TO_SHADING_COLOR_SPACE(col) GammaToLinearSpace(col)
#endif
#if defined(UNITY_COLORSPACE_GAMMA) || !defined(USE_GAMMA_COLORSPACE)
#define FROM_SHADING_COLOR_SPACE(col) col
#else
#define FROM_SHADING_COLOR_SPACE(col) LinearToGammaSpace(col)
#endif
// Inputs..............................................................................
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct v2f
{
float2 uv0 : TEXCOORD0;
float4 uv1 : TEXCOORD1;
#ifdef USE_DISTANCE_FADE
float distanceFade :TEXCOORD2;
#endif
OSP_FOG_COORDS(4)
float4 pos : SV_POSITION;
UNITY_VERTEX_OUTPUT_STEREO
};
UNITY_DECLARE_TEX2D(_MainTex);
float4 _MainTex_ST;
float4 _MainTex_TexelSize;
float4 _Color;
// Transformation
float2 _Position;
float _RotationRoll;
float2 _Scale;
float _ConstantScale;
// Flipbook
UNITY_DECLARE_TEX2DARRAY(_FlipbookTexArray);
float4 _FlipbookTexArray_ST;
float4 _FlipbookTexArray_TexelSize;
float4 _FlipbookTint;
float2 _FlipbookScrollVelocity;
float _FlipbookBlendMode;
float _FlipbookFramesPerSecond;
float _FlipbookUseManualFrame;
float _FlipbookManualFrame;
// Outline
float4 _OutlineColor;
float _OutlineWidth;
// Distance Fade
float _DistanceFadeMinAlpha;
float _DistanceFadeMaxAlpha;
float _DistanceFadeMin;
float _DistanceFadeMax;
// Alpha Test
float _AlphaCutoff;
// Vertex Animation
half _RandomizeAnimWorldPosition;
// Float
half _FloatOn;
half _FloatAmplitude;
half3 _FloatAxis;
half _FloatFrequency;
half _FloatPhase;
// Throb
half _ThrobOn;
half _ThrobAmplitude;
half _ThrobFrequency;
half _ThrobPhase;
// Transformation......................................................................
// Decompose the scale from a transformation matrix.
float3 GetScale(float4x4 m)
{
float3 scale;
scale.x = length(float3(m[0][0], m[1][0], m[2][0]));
scale.y = length(float3(m[0][1], m[1][1], m[2][1]));
scale.z = length(float3(m[0][2], m[1][2], m[2][2]));
// It's not possible to determine which scale components were negative when decomposing. But we can at
// least support sprites flipping horizontally by assuming it was the X-axis.
float det = determinant(m);
if (det < 0)
{
scale.x = -scale.x;
}
return scale;
}
// Outline.............................................................................
float SampleAlpha(float2 uv0, float4 uv1)
{
float alpha = _Color.a * UNITY_SAMPLE_TEX2D(_MainTex, uv0).a;
#if defined(USE_FLIPBOOK)
float flipbookAlpha = UNITY_SAMPLE_TEX2DARRAY(_FlipbookTexArray, float3(uv1.xy, floor(uv1.z))).a;
#if defined(USE_FLIPBOOK_SMOOTHING)
float flipbookAlpha2 = UNITY_SAMPLE_TEX2DARRAY(_FlipbookTexArray, uv1.xyw).a;
flipbookAlpha = lerp(flipbookAlpha, flipbookAlpha2, frac(uv1.z));
#endif
alpha = BlendAlpha(_FlipbookTint.a * flipbookAlpha, alpha, _FlipbookBlendMode);
#endif
return alpha;
}
float GetOutline(float2 uv0, float4 uv1, float width, float baseAlpha)
{
float2 t0 = _MainTex_TexelSize.xy * width;
float2 t1 = _FlipbookTexArray_TexelSize.xy * width;
float4 offsets0 = float4(t0.x, t0.y, -t0.x, -t0.y);
float4 offsets1 = float4(t1.x, t1.y, -t1.x, -t1.y);
float a0 = SampleAlpha(uv0 + offsets0.xy, float4(uv1.xy + offsets1.xy, uv1.zw));
float a1 = SampleAlpha(uv0 + offsets0.xw, float4(uv1.xy + offsets1.xw, uv1.zw));
float a2 = SampleAlpha(uv0 + offsets0.zw, float4(uv1.xy + offsets1.zw, uv1.zw));
float a3 = SampleAlpha(uv0 + offsets0.zy, float4(uv1.xy + offsets1.zy, uv1.zw));
return saturate(a0 + a1 + a2 + a3) - baseAlpha;
}
// Shader Functions....................................................................
v2f vert(appdata v)
{
v2f o;
UNITY_SETUP_INSTANCE_ID(v);
UNITY_INITIALIZE_OUTPUT(v2f, o);
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
#if PIVOT_MODE_VERTEX
float3 positionOs = float3(0.5 * (v.uv.xy - 0.5), 0.0);
float3 billboardCenterWs = mul(unity_ObjectToWorld, v.vertex).xyz;
#else
float3 positionOs = v.vertex.xyz;
// object center
float3 billboardCenterWs = unity_ObjectToWorld._m03_m13_m23;
#endif
#if KEEP_CONSTANT_SCALING || USE_DISTANCE_FADE || FLIP_FACING_HORIZONTAL
float3 viewDirectionWs = billboardCenterWs - GetCenterCameraPosition();
#if KEEP_CONSTANT_SCALING || USE_DISTANCE_FADE
float distanceWs = length(viewDirectionWs);
#endif
#endif
#if KEEP_CONSTANT_SCALING
positionOs *= _ConstantScale / unity_CameraProjection._m11 * distanceWs;
#if defined(USING_STEREO_MATRICES)
positionOs *= 0.25; // Arbitrary scale factor. Just felt too large in VR? -Vavassor
#endif
#endif
#ifdef USE_NON_UNIFORM_SCALE
positionOs *= GetScale(unity_ObjectToWorld);
#else
positionOs *= length(mul(unity_ObjectToWorld, float4(0, 0, 1, 0)).xyz);
#endif
float2 scale = _Scale;
#ifdef FLIP_FACING_HORIZONTAL
if (dot(unity_WorldToObject._m20_m21_m22, viewDirectionWs) < 0)
{
scale.x *= -1;
}
#endif
#if USE_OUTLINE
#if defined(USE_FLIPBOOK)
float2 outlineTexelSize = min(_MainTex_TexelSize.xy, _FlipbookTexArray_TexelSize.xy);
#else
float2 outlineTexelSize = _MainTex_TexelSize.xy;
#endif
float2 paddingTexels = _OutlineWidth * outlineTexelSize;
scale += 2.0 * paddingTexels;
v.uv = (v.uv + sign(v.uv - float2(0.5, 0.5)) * paddingTexels);
#endif
positionOs.xy = Transform2d(positionOs.xy, _Position, _RotationRoll, scale);
#ifdef USE_VERTEX_ANIMATION
half randomizePhase = 0;
if (_RandomizeAnimWorldPosition)
{
randomizePhase = UNITY_TWO_PI * frac(127 * (billboardCenterWs.x + billboardCenterWs.y + billboardCenterWs.z));
}
if (_ThrobOn)
{
positionOs.xyz += _ThrobAmplitude * sin(_ThrobFrequency * _Time.y + _ThrobPhase + randomizePhase) * positionOs.xyz;
}
if (_FloatOn)
{
positionOs.xyz += _FloatAmplitude * sin(_FloatFrequency * _Time.y + _FloatPhase + randomizePhase) * _FloatAxis;
}
#endif
o.pos = BillboardCs(float4(positionOs, 1), billboardCenterWs);
o.uv0 = TRANSFORM_TEX(v.uv, _MainTex);
#ifdef USE_FLIPBOOK
float2 transformedTexcoord = TRANSFORM_TEX(v.uv, _FlipbookTexArray);
float2 scrolledTexcoord = transformedTexcoord + _Time.y * _FlipbookScrollVelocity;
o.uv1 = GetFlipbookTexcoord(_FlipbookTexArray, scrolledTexcoord, _FlipbookFramesPerSecond, _FlipbookUseManualFrame, _FlipbookManualFrame);
#endif
#ifdef USE_DISTANCE_FADE
o.distanceFade = lerp(_DistanceFadeMinAlpha, _DistanceFadeMaxAlpha, smoothstep(_DistanceFadeMin, _DistanceFadeMax, distanceWs));
#endif
OSP_TRANSFER_FOG(o,o.pos);
return o;
}
fixed4 frag(v2f i) : SV_Target
{
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(i);
#if defined(USE_PIXEL_SHARPEN)
i.uv0.xy = SharpenPixelUv(i.uv0.xy, _MainTex_TexelSize);
#ifdef USE_FLIPBOOK
i.uv1.xy = SharpenPixelUv(i.uv1.xy, _FlipbookTexArray_TexelSize);
#endif
#endif
fixed4 col = UNITY_SAMPLE_TEX2D(_MainTex, i.uv0);
col.rgb = TO_SHADING_COLOR_SPACE(col.rgb);
#if defined(_COLORMODE_MULTIPLY)
col *= _Color;
#elif defined(_COLORMODE_OVERLAY)
col.rgb = lerp(1 - 2 * (1 - col.rgb) * (1 - _Color.rgb), 2 * col.rgb * _Color.rgb, step(col.rgb, 0.5));
col.a *= _Color.a;
#endif
#if defined(USE_FLIPBOOK)
float4 flipbookColor = UNITY_SAMPLE_TEX2DARRAY(_FlipbookTexArray, float3(i.uv1.xy, floor(i.uv1.z)));
#if defined(USE_FLIPBOOK_SMOOTHING)
float4 flipbookColor2 = UNITY_SAMPLE_TEX2DARRAY(_FlipbookTexArray, i.uv1.xyw);
flipbookColor = lerp(flipbookColor, flipbookColor2, frac(i.uv1.z));
#endif
flipbookColor.rgb = TO_SHADING_COLOR_SPACE(flipbookColor.rgb);
col = BlendColor(_FlipbookTint * flipbookColor, col, _FlipbookBlendMode);
#endif
#ifdef USE_OUTLINE
col = lerp(col, _OutlineColor, GetOutline(i.uv0, i.uv1, _OutlineWidth, col.a));
#endif
#ifdef USE_DISTANCE_FADE
col.a *= i.distanceFade;
#endif
#ifdef USE_ALPHA_TEST
clip(col.a - _AlphaCutoff);
#endif
col.rgb = FROM_SHADING_COLOR_SPACE(col.rgb);
OSP_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
@Vavassor

Copy link
Copy Markdown
Author

The trees and coins below use this shader.

Billboard.Demo.1.mp4

@Vavassor

Copy link
Copy Markdown
Author

Made an OSP Billboard package that has more billboard shaders including UI Text!

@Vavassor

Copy link
Copy Markdown
Author

Updated to the same features as OSP billboard 0.5.0. Includes new billboarding modes, vertex animation, and outlines.

@Gyztor

Gyztor commented Sep 30, 2026

Copy link
Copy Markdown

did some poking at this but realised this is a bit more above what i know on shaders. Would it be possible to also have a version that prints a quad at every vertice on a mesh THAN applying the rotations on each one? i did some looking into it and i know this shader does it but it looks a LOT more complex than i thought when i tried to integrate a form of this into your shader. The main reason i ask is mainly for an idea to do stars as a point cluster mesh than have them be rendered with a billboard shader in gpu for better performance and more dynamic control rather than having a lot of quads or making a mesh manually.

@Vavassor

Vavassor commented Oct 1, 2026

Copy link
Copy Markdown
Author

@Gyztor Sure, I made two more billboards using geometry shaders. Check comments for more about them.

I haven't measured performance. But I have heard geometry shaders have a reputation of bad performance or at least inconsistent depending on GPU. Also they're not supported on some platforms.

So I have another idea I still need to try without a geometry shader. Which is to instead use a quad mesh where each quad is scaled down to a point. And then the shader "inflates" all of them and rotates them around each of the origin points.

@Gyztor

Gyztor commented Oct 1, 2026

Copy link
Copy Markdown

Yeah i figured quest would be problem. but you are honestly cracked at this i didn't expect a full implementation of this that is this detailed. i will (once i get some time) do some benchmark testing on this and see how it works with the idea for stars and see if it preforms well. Theoretically it should preform well, but i will definitely need to get my hands on a potato laptop and try it on that as well to see how well it reacts.

@Vavassor

Vavassor commented Oct 2, 2026

Copy link
Copy Markdown
Author

I added a "Pivot Around" option to this shader to support multiple billboards in one mesh. In "Vertex" mode, use a mesh which is quads (or any flat shape) each scaled down to a point. Then in Unity in the mesh import settings make sure Weld Vertices is unchecked. Then the shader inflates them again using the UVs as the shape of each billboard.

@Vavassor

Vavassor commented Oct 3, 2026

Copy link
Copy Markdown
Author

Also confirmed it working on a Quest 2.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment