Skip to content

Instantly share code, notes, and snippets.

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

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

Select an option

Save Vavassor/3fee77253e7c6c31d3ec7d31350ab6eb to your computer and use it in GitHub Desktop.
Unlit Billboard Geometry Shaders
// Billboards are a flat image or sprite that always face the camera. This shader creates a billboard for each point
// in a point cloud. (a mesh using MeshTopology.Points)
//
// 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 Points 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
[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 geometry geom
#pragma multi_compile_fog
#pragma multi_compile_instancing
#pragma shader_feature_local _ _COLORMODE_MULTIPLY _COLORMODE_OVERLAY
#pragma shader_feature_local USE_GAMMA_COLORSPACE
#pragma shader_feature_local USE_FLIPBOOK
#pragma shader_feature_local USE_FLIPBOOK_SMOOTHING
#pragma shader_feature_local USE_ALPHA_TEST
#pragma shader_feature_local _BILLBOARD_MODE_NONE _BILLBOARD_MODE_SPHERICAL _BILLBOARD_MODE_CYLINDRICAL_WORLD _BILLBOARD_MODE_CYLINDRICAL_LOCAL
#pragma shader_feature_local USE_NON_UNIFORM_SCALE
#pragma shader_feature_local KEEP_CONSTANT_SCALING
#pragma shader_feature_local USE_OUTLINE
#pragma shader_feature_local USE_DISTANCE_FADE
#pragma shader_feature_local USE_PIXEL_SHARPEN
#pragma shader_feature_local FLIP_FACING_HORIZONTAL
#pragma shader_feature_local 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.xyz) + 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 Attributes
{
float4 vertex : POSITION;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct GeomAttributes
{
float4 vertex : POSITION;
UNITY_VERTEX_OUTPUT_STEREO
};
struct Varyings
{
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....................................................................
Varyings GetVaryings(GeomAttributes v, float3 cornerOffset, float2 uv)
{
Varyings o = (Varyings) 0;
UNITY_TRANSFER_VERTEX_OUTPUT_STEREO(v, o);
float3 billboardCenterOs = v.vertex;
float3 positionOs = cornerOffset;
float3 billboardCenterWs = mul(unity_ObjectToWorld, float4(billboardCenterOs, 1.0)).xyz;
#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(uv, _MainTex);
#ifdef USE_FLIPBOOK
float2 transformedTexcoord = TRANSFORM_TEX(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;
}
GeomAttributes vert(Attributes v)
{
GeomAttributes o;
UNITY_SETUP_INSTANCE_ID(v);
UNITY_INITIALIZE_OUTPUT(GeomAttributes, o);
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
o.vertex = v.vertex;
return o;
}
[maxvertexcount(4)]
void geom(point GeomAttributes input[1], inout TriangleStream<Varyings> triStream)
{
triStream.Append(GetVaryings(input[0], float3(-0.5, -0.5, 0.0), float2(0.0, 0.0)));
triStream.Append(GetVaryings(input[0], float3(-0.5, 0.5, 0.0), float2(0.0, 1.0)));
triStream.Append(GetVaryings(input[0], float3(0.5, -0.5, 0.0), float2(1.0, 0.0)));
triStream.Append(GetVaryings(input[0], float3(0.5, 0.5, 0.0), float2(1.0, 1.0)));
triStream.RestartStrip();
}
fixed4 frag(Varyings 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
}
}
}
// Billboards are a flat image or sprite that always face the camera. This shader creates a billboard for every corner
// of every triangle in a mesh. Triangles that share a corner will produce multiple all in one spot!
//
// 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 Triangle Corners 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
[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 geometry geom
#pragma multi_compile_fog
#pragma multi_compile_instancing
#pragma shader_feature_local _ _COLORMODE_MULTIPLY _COLORMODE_OVERLAY
#pragma shader_feature_local USE_GAMMA_COLORSPACE
#pragma shader_feature_local USE_FLIPBOOK
#pragma shader_feature_local USE_FLIPBOOK_SMOOTHING
#pragma shader_feature_local USE_ALPHA_TEST
#pragma shader_feature_local _BILLBOARD_MODE_NONE _BILLBOARD_MODE_SPHERICAL _BILLBOARD_MODE_CYLINDRICAL_WORLD _BILLBOARD_MODE_CYLINDRICAL_LOCAL
#pragma shader_feature_local USE_NON_UNIFORM_SCALE
#pragma shader_feature_local KEEP_CONSTANT_SCALING
#pragma shader_feature_local USE_OUTLINE
#pragma shader_feature_local USE_DISTANCE_FADE
#pragma shader_feature_local USE_PIXEL_SHARPEN
#pragma shader_feature_local FLIP_FACING_HORIZONTAL
#pragma shader_feature_local 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.xyz) + 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 Attributes
{
float4 vertex : POSITION;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct GeomAttributes
{
float4 vertex : POSITION;
UNITY_VERTEX_OUTPUT_STEREO
};
struct Varyings
{
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....................................................................
Varyings GetVaryings(GeomAttributes v, float3 cornerOffset, float2 uv)
{
Varyings o = (Varyings) 0;
UNITY_TRANSFER_VERTEX_OUTPUT_STEREO(v, o);
float3 billboardCenterOs = v.vertex;
float3 positionOs = cornerOffset;
float3 billboardCenterWs = mul(unity_ObjectToWorld, float4(billboardCenterOs, 1.0)).xyz;
#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;
uv = (uv + sign(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(uv, _MainTex);
#ifdef USE_FLIPBOOK
float2 transformedTexcoord = TRANSFORM_TEX(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;
}
GeomAttributes vert(Attributes v)
{
GeomAttributes o;
UNITY_SETUP_INSTANCE_ID(v);
UNITY_INITIALIZE_OUTPUT(GeomAttributes, o);
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
o.vertex = v.vertex;
return o;
}
[maxvertexcount(12)]
void geom(triangle GeomAttributes input[3], inout TriangleStream<Varyings> triStream)
{
triStream.Append(GetVaryings(input[0], float3(-0.5, -0.5, 0.0), float2(0.0, 0.0)));
triStream.Append(GetVaryings(input[0], float3(-0.5, 0.5, 0.0), float2(0.0, 1.0)));
triStream.Append(GetVaryings(input[0], float3(0.5, -0.5, 0.0), float2(1.0, 0.0)));
triStream.Append(GetVaryings(input[0], float3(0.5, 0.5, 0.0), float2(1.0, 1.0)));
triStream.RestartStrip();
triStream.Append(GetVaryings(input[1], float3(-0.5, -0.5, 0.0), float2(0.0, 0.0)));
triStream.Append(GetVaryings(input[1], float3(-0.5, 0.5, 0.0), float2(0.0, 1.0)));
triStream.Append(GetVaryings(input[1], float3(0.5, -0.5, 0.0), float2(1.0, 0.0)));
triStream.Append(GetVaryings(input[1], float3(0.5, 0.5, 0.0), float2(1.0, 1.0)));
triStream.RestartStrip();
triStream.Append(GetVaryings(input[2], float3(-0.5, -0.5, 0.0), float2(0.0, 0.0)));
triStream.Append(GetVaryings(input[2], float3(-0.5, 0.5, 0.0), float2(0.0, 1.0)));
triStream.Append(GetVaryings(input[2], float3(0.5, -0.5, 0.0), float2(1.0, 0.0)));
triStream.Append(GetVaryings(input[2], float3(0.5, 0.5, 0.0), float2(1.0, 1.0)));
triStream.RestartStrip();
}
fixed4 frag(Varyings 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

Vavassor commented Oct 1, 2026 •

Copy link
Copy Markdown
Author

These are versions of the Unlit Billboard shader that use geometry shaders to produce many billboards per mesh.

Geometry shaders aren't supported on some platforms like iOS or Meta Quest. Instead you can use the Unlit Billboard shader in the other gist and use the Pivot Around setting. See the comment at the top of the shader file for more details.

Unlit Billboard Triangle Corners shader

Produces billboards per corner of every triangle in the mesh. Beware that triangles that share a corner produce multiple billboards all in one spot. For example in the following screenshot, the bubble in the center of the hexagon in the bottom-left is brighter because it's actually several billboards stacked up.
Unity_p5t6JIxDJ0

Unlit Billboard Points shader

Produces billboards for each point in a point cloud mesh. To use this shader, you either need a tool to import point clouds like Pcx or create the mesh using MeshTopology.Points in a C# script.

@Vavassor

Vavassor commented Oct 3, 2026

Copy link
Copy Markdown
Author

Fixed a compile error invalid subscript "stereoEyeTargetIndex" with both of these shaders.

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