#ifndef __IMPOSTOR_INC #define __IMPOSTOR_INC #include "UnityCG.cginc" #include "vertex_deformation.hlsl" SamplerState bilinear_clamp_s; Texture2D _ImpostorAtlas; int _GridResolution; float _Cutoff; float _SphereRadius; float3 _ImpostorMainCameraPos; #ifndef IMPOSTOR_SHADOW_PASS float4 _Color; float _DebugMode; #endif float2 HemiOctEncode(float3 N) { N.y = max(N.y, 1e-4); N = normalize(N); float3 p = hemi_octahedron_to_plane(N, 0, float3(1,0,0), float3(0,1,0), 1); return float2(p.x, p.z); } float3 HemiOctDecode(float2 uv) { return normalize(plane_to_hemi_octahedron(float3(uv.x, 0, uv.y), 0, float3(1,0,0), float3(0,1,0), 1)); } void BillboardBasis(float3 fwd, out float3 right, out float3 up) { right = abs(fwd.y) > 0.999 ? float3(-1,0,0) : normalize(cross(float3(0,1,0), fwd)); up = cross(fwd, right); } float2 GridFromDir(float3 viewDir, float gridRes) { float2 uv = HemiOctEncode(viewDir) * 0.5 + 0.5; return clamp(uv * (gridRes - 1), 0, gridRes - 1); } float3 DirFromCell(float2 cell, float gridRes) { float2 uv = cell / max(1.0, gridRes - 1) * 2.0 - 1.0; return HemiOctDecode(uv); } float4 SampleAtlas(float2 uv, float2 cell) { return _ImpostorAtlas.Sample(bilinear_clamp_s, (cell + uv) / _GridResolution); } // Compute barycentric weights for a point within a grid cell. // The cell is a unit square split into two triangles along the (0,0)-(1,1) diagonal: // Triangle 0 (lower-left): vertices at (0,0), (0,1), (1,1) // Triangle 1 (upper-right): vertices at (0,0), (1,0), (1,1) // Returns: // .xyz = barycentric weights for the 3 triangle vertices // .w = which triangle (0 or 1) float4 GridCellBarycentric(float2 p) { float4 res; // Branchless barycentric weights (works for both triangles due to symmetry) res.x = min(1.0 - p.x, 1.0 - p.y); // weight for (0,0) vertex res.y = abs(p.x - p.y); // weight for (0,1) or (1,0) vertex res.z = min(p.x, p.y); // weight for (1,1) vertex res.w = ceil(p.x - p.y); // triangle select: 0 if p.y >= p.x, 1 otherwise return res; } // Compute UV on a virtual plane facing frameDir float2 VirtualPlaneUV(float3 frameDir, float3 pivotToCam, float3 vertexToCam, float size) { float3 planeN = normalize(frameDir); float3 up = abs(planeN.y) > 0.999 ? float3(0,0,1) : float3(0,1,0); float3 planeX = normalize(cross(planeN, up)); float3 planeY = normalize(cross(planeX, planeN)); float projPivot = dot(planeN, pivotToCam); float projVertex = dot(planeN, vertexToCam); float ratio = projPivot / projVertex; float3 offset = vertexToCam * ratio - pivotToCam; float2 uv = float2(dot(planeX, offset), dot(planeY, offset)); uv /= (2.0f * size); uv *= -1.0f; uv += 0.5; return uv; } struct appdata { float4 vertex : POSITION; UNITY_VERTEX_INPUT_INSTANCE_ID #ifdef IMPOSTOR_SHADOW_PASS float3 normal : NORMAL; #endif }; struct v2f { #ifdef IMPOSTOR_SHADOW_PASS V2F_SHADOW_CASTER; #else float4 pos : SV_POSITION; UNITY_FOG_COORDS(7) #endif float3 worldPos : TEXCOORD1; float3 centerPos : TEXCOORD2; UNITY_VERTEX_OUTPUT_STEREO }; v2f vert(appdata v) { v2f o; UNITY_SETUP_INSTANCE_ID(v); UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o); float3 center = mul(unity_ObjectToWorld, float4(0,0,0,1)).xyz; o.centerPos = center; float3 scale = float3( length(unity_ObjectToWorld._m00_m10_m20), length(unity_ObjectToWorld._m01_m11_m21), length(unity_ObjectToWorld._m02_m12_m22)); #ifdef IMPOSTOR_SHADOW_PASS float3 camPos = _ImpostorMainCameraPos; #else float3 camPos = _WorldSpaceCameraPos; #endif // Billboard facing the camera direction float3 viewWS = normalize(camPos - center); float3 right, up; BillboardBasis(viewWS, right, up); float3 worldPos = center + v.vertex.x * right * scale.x + v.vertex.y * up * scale.y; o.worldPos = worldPos; #ifdef IMPOSTOR_SHADOW_PASS v.vertex = mul(unity_WorldToObject, float4(worldPos, 1)); v.normal = -viewWS; TRANSFER_SHADOW_CASTER_NORMALOFFSET(o) #else o.pos = mul(UNITY_MATRIX_VP, float4(worldPos, 1)); UNITY_TRANSFER_FOG(o, o.pos); #endif return o; } // General purpose ray-sphere intersection. bool raySphereIntersect( float3 ro, float3 rayDir, float3 origin, float radius, out float3 posNear, out float3 posFar) { float3 originToRo = ro - origin; float b = dot(originToRo, rayDir); float c = dot(originToRo, originToRo) - radius * radius; float disc = b * b - c; if (disc < 0) { return false; } float t_near = -b - sqrt(disc); float t_far = -b + sqrt(disc); posNear = ro + rayDir * t_near; posFar = ro + rayDir * t_far; return true; } float4 frag(v2f i) : SV_Target { // Sphere culling first float3 eyeVec = i.worldPos - _WorldSpaceCameraPos; float3 viewDir = normalize(eyeVec); float3 posNear, posFar; bool didIntersect = raySphereIntersect( _WorldSpaceCameraPos, viewDir, i.centerPos, _SphereRadius, posNear, posFar); clip(didIntersect - 0.5); // Camera position for grid computation (matches billboard orientation) #ifdef IMPOSTOR_SHADOW_PASS float3 camPos = _ImpostorMainCameraPos; #else float3 camPos = _WorldSpaceCameraPos; #endif float3 center = i.centerPos; // View direction in object space float3 viewOS = normalize(mul((float3x3)unity_WorldToObject, normalize(camPos - center))); // Get continuous grid position and find the 3 frames float gridRes = (float)_GridResolution; float2 grid = GridFromDir(viewOS, gridRes); float2 gridFloor = floor(grid); float2 gridFrac = frac(grid); // Compute barycentric weights and determine which triangle float4 bary = GridCellBarycentric(gridFrac); float3 blendWeights = bary.xyz; // Frame cells float2 cell1 = gridFloor; float2 cell2 = clamp(gridFloor + lerp(float2(0,1), float2(1,0), bary.w), 0, gridRes - 1); float2 cell3 = clamp(gridFloor + float2(1,1), 0, gridRes - 1); // Get directions for each frame float3 dir1 = DirFromCell(cell1, gridRes); float3 dir2 = DirFromCell(cell2, gridRes); float3 dir3 = DirFromCell(cell3, gridRes); // Compute virtual plane UVs for all 3 frames float3 pivotToCamOS = mul((float3x3)unity_WorldToObject, camPos - center) * 100.0; float3 vertexPosOS = mul((float3x3)unity_WorldToObject, i.worldPos - center); float3 vertexToCamOS = pivotToCamOS - vertexPosOS; float2 uv1 = VirtualPlaneUV(dir1, pivotToCamOS, vertexToCamOS, 0.5); float2 uv2 = VirtualPlaneUV(dir2, pivotToCamOS, vertexToCamOS, 0.5); float2 uv3 = VirtualPlaneUV(dir3, pivotToCamOS, vertexToCamOS, 0.5); // Sample and blend frames float4 s1 = SampleAtlas(uv1, cell1); float4 s2 = SampleAtlas(uv2, cell2); float4 s3 = SampleAtlas(uv3, cell3); float4 col = s1 * blendWeights.x + s2 * blendWeights.y + s3 * blendWeights.z; #ifndef IMPOSTOR_SHADOW_PASS if (_DebugMode > 0.5) return float4(blendWeights.xy, 0, 1); #endif col *= _Color; clip(col.a - _Cutoff); #ifdef IMPOSTOR_SHADOW_PASS SHADOW_CASTER_FRAGMENT(i) #else UNITY_APPLY_FOG(i.fogCoord, col); return col; #endif } #endif