Hey everyone, I’ve been working on a billboarding shader with some special behaviour for a project of mine, and thanks to some helpful folks on these forums (namely @bgolus - thanks a million!) I’ve ended up with something that does more or less what I’d like.
This fragment shader supports transparent sprites that face the camera all the time, but calculate their z-depth as if they were standing completely upright, to avoid clipping into geometry behind them.
Because I’m apparently a glutton for difficult tasks, I’ve been trying to add support for some basic lighting and shadows, with limited success. Using a single pass and ForwardBase I’ve managed to make something properly responsive to directional lights, but none of the examples I’ve followed seem to allow this transparent shader to register point/spot lights in the environment, or receive shadows cast over it. Likewise, attempts at adding a second ForwardAdd pass haven’t worked out, likely because all of the examples I’ve seen are meant for opaque shaders.
I’ve considered using a surface shader for this, but I’m reasonably certain the billboarding behaviour I want needs to make adjustments in clip space to fake that the billboard is standing upright, which surface shaders don’t expose. If I’m wrong, feel free to correct me.
Ideally, what I’d like to achieve as far as lighting is:
- Receives light from nearby sources and directional lights, according to its current normals/facing.
- Receives shadows and draws darker when in areas of shadow, but pixel-perfect accuracy isn’t necessary. Simply tinting the whole sprite if its origin were in darkness would be fine.
- Does not need to cast shadows. In fact, due to the nature of the billboard in this 3D world, cast stenciled shadows might look very distorted.
- Specular reflections and shininess aren’t necessary - these are hand-drawn sprites with hand-drawn highlights.
Here’s a shot using placeholder sprites of what I have working now:
Note how directional vertex lighting is working (all the sprites are tinted blue), but shadows and point lights are ignored, despite my attempts in the shader to implement them.
The shader code is below. Am I missing something, or overlooking some workaround for transparent sprites that receive lighting/shadows? Thanks in advance!
Shader "Sprites/Billboard_VLit_ZDepth"
{
Properties
{
_Color("Color", Color) = (1,1,1,1)
_MainTex("Texture", 2D) = "white" {}
}
SubShader
{
Tags{ "Queue" = "Transparent" "IgnoreProjector" = "True" "RenderType" = "Transparent" "DisableBatching" = "True" } //"LightMode" = "ForwardBase" }
ZWrite Off
Cull Off
Blend SrcAlpha OneMinusSrcAlpha
Pass
{
Tags{ "LightMode" = "ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fog
#pragma multi_compile_fwdbase
#include "UnityCG.cginc"
#include "AutoLight.cginc"
#include "Lighting.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
fixed4 color : COLOR;
};
struct v2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
UNITY_FOG_COORDS(1)
LIGHTING_COORDS(2, 3)
SHADOW_COORDS(4)
fixed4 color : COLOR0;
};
sampler2D _MainTex;
float4 _MainTex_ST;
fixed4 _Color;
float rayPlaneIntersection(float3 rayDir, float3 rayPos, float3 planeNormal, float3 planePos)
{
float denom = dot(planeNormal, rayDir);
denom = max(denom, 0.000001); // avoid divide by zero
float3 diff = planePos - rayPos;
return dot(diff, planeNormal) / denom;
}
v2f vert(appdata v)
{
v2f o;
o.uv = v.uv.xy;
// billboard mesh towards camera
float3 vpos = mul((float3x3)unity_ObjectToWorld, v.vertex.xyz);
float4 worldCoord = float4(unity_ObjectToWorld._m03, unity_ObjectToWorld._m13, unity_ObjectToWorld._m23, 1);
float4 viewPos = mul(UNITY_MATRIX_V, worldCoord) + float4(vpos, 0);
o.pos = mul(UNITY_MATRIX_P, viewPos);
// calculate distance to vertical billboard plane seen at this vertex's screen position
float3 planeNormal = normalize(float3(UNITY_MATRIX_V._m20, 0.0, UNITY_MATRIX_V._m22));
float3 planePoint = unity_ObjectToWorld._m03_m13_m23;
float3 rayStart = _WorldSpaceCameraPos.xyz;
float3 rayDir = -normalize(mul(UNITY_MATRIX_I_V, float4(viewPos.xyz, 1.0)).xyz - rayStart); // convert view to world, minus camera pos
float dist = rayPlaneIntersection(rayDir, rayStart, planeNormal, planePoint);
// calculate the clip space z for vertical plane
float4 planeOutPos = mul(UNITY_MATRIX_VP, float4(rayStart + rayDir * dist, 1.0));
float newPosZ = planeOutPos.z / planeOutPos.w * o.pos.w;
// use the closest clip space z
#if defined(UNITY_REVERSED_Z)
o.pos.z = max(o.pos.z, newPosZ);
#else
o.pos.z = min(o.pos.z, newPosZ);
#endif
//Calculate Lighting
// get vertex normal in world space
half3 worldNormal = UnityObjectToWorldNormal(planeNormal); //v.normal
// dot product between normal and light direction for
// standard diffuse (Lambert) lighting
half nl = max(0, dot(worldNormal, _WorldSpaceLightPos0.xyz));
// factor in the light color
o.color.rgb = nl * _LightColor0;
o.color.rgb += ShadeSH9(half4(worldNormal, 1));
o.color.a = v.color.a;
UNITY_TRANSFER_FOG(o,o.vertex);
TRANSFER_VERTEX_TO_FRAGMENT(o);
TRANSFER_SHADOW(o);
return o;
}
fixed4 frag(v2f i) : SV_Target
{
float attenuation = LIGHT_ATTENUATION(i);
fixed shadow = SHADOW_ATTENUATION(i);
fixed4 col = tex2D(_MainTex, i.uv) * _Color * i.color * attenuation * shadow;
//clip(col.a - 0.2); //For if using cutout shader
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
Fallback "VertexLit"
}
