#ifndef __IMPOSTOR_INC #define __IMPOSTOR_INC #include "UnityCG.cginc" #include "globals.cginc" #include "vertex_deformation.hlsl" // Utility functions for hemispherical octahedral mapping float2 HemiOctEncode(float3 N) { N.y = max(N.y, 1e-4); float3 p = hemi_octahedron_to_plane(normalize(N), 0, float3(1,0,0), float3(0,1,0), 1); return p.xz; } 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); } // Branchless barycentric weights in a unit square split into two triangles float4 GridCellBarycentric4(float2 p) { float w00 = 1.0 - max(p.x, p.y); float w11 = min(p.x, p.y); float w01 = max(p.y - p.x, 0.0); float w10 = max(p.x - p.y, 0.0); return float4(w00, w01, w10, w11); } // Compute UV on a virtual plane facing frameDir float2 VirtualPlaneUV(float3 frameDir, float3 pivotToCam, float3 vertexToCam) { 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)); return uv * -1.0 + 0.5; } // General purpose ray-sphere intersection bool RaySphereIntersect(float3 ro, float3 rayDir, float3 origin, float radius) { float3 originToRo = ro - origin; float b = dot(originToRo, rayDir); float c = dot(originToRo, originToRo) - radius * radius; return (b * b - c) >= 0.0; } #if defined(_IMPOSTORS) float2 ClampUvInCell(float2 uv) { uv = saturate(uv); float2 halfTexelInCell = 0.5 * _Impostors_Atlas_TexelSize.xy * (float)_Impostors_Grid_Resolution; return clamp(uv, halfTexelInCell, 1.0 - halfTexelInCell); } float4 SampleAtlasGrad(float2 uv, float2 cell, float2 uvGradX, float2 uvGradY) { uv = ClampUvInCell(uv); float invGridRes = rcp((float)_Impostors_Grid_Resolution); float2 atlasUv = (cell + uv) * invGridRes; // Important: gradients must be computed in-cell; do not let integer cell offsets affect mip selection. return _Impostors_Atlas.SampleGrad(bilinear_clamp_s, atlasUv, uvGradX * invGridRes, uvGradY * invGridRes); } // Billboard vertex transformation for impostors void impostor_vert(float4 vertexOS, inout float3 worldPos) { float3 center = mul(unity_ObjectToWorld, float4(0,0,0,1)).xyz; float3 scale = float3( length(unity_ObjectToWorld._m00_m10_m20), length(unity_ObjectToWorld._m01_m11_m21), length(unity_ObjectToWorld._m02_m12_m22)); #ifdef SHADOW_CASTER_PASS float3 camPos = _Impostors_Main_Camera_Pos; #else float3 camPos = _WorldSpaceCameraPos; #endif // Billboard facing the camera direction float3 viewWS = normalize(camPos - center); float3 right, up; BillboardBasis(viewWS, right, up); worldPos = center + vertexOS.x * right * scale.x + vertexOS.y * up * scale.y; } // Sample impostor atlas with view-dependent blending float4 impostor_frag(float3 worldPos) { // Calculate center in fragment shader to avoid extra interpolator float3 center = mul(unity_ObjectToWorld, float4(0,0,0,1)).xyz; // Sphere culling first #ifdef SHADOW_CASTER_PASS float3 camPos = _Impostors_Main_Camera_Pos; #else float3 camPos = _WorldSpaceCameraPos; #endif float3 viewDir = normalize(worldPos - camPos); bool didIntersect = RaySphereIntersect(camPos, viewDir, center, _Impostors_Sphere_Radius); clip(didIntersect - 0.5); // For lattice lookup, use the camera-to-impostor-center direction (matches billboard orientation). float3x3 worldToObject = (float3x3)unity_WorldToObject; float3 viewOS = normalize(mul(worldToObject, normalize(camPos - center))); // Get continuous grid position and find the 4 frames float gridRes = (float)_Impostors_Grid_Resolution; float2 grid = GridFromDir(viewOS, gridRes); float2 gridFloor = floor(grid); float2 gridFrac = frac(grid); // Branchless barycentric blend weights float4 bw = GridCellBarycentric4(gridFrac); // Frame cells (square corners) float2 cell00 = clamp(gridFloor, 0, gridRes - 1); float2 cell01 = clamp(gridFloor + float2(0,1), 0, gridRes - 1); float2 cell10 = clamp(gridFloor + float2(1,0), 0, gridRes - 1); float2 cell11 = clamp(gridFloor + float2(1,1), 0, gridRes - 1); // Get directions for each frame float3 dir00 = DirFromCell(cell00, gridRes); float3 dir01 = DirFromCell(cell01, gridRes); float3 dir10 = DirFromCell(cell10, gridRes); float3 dir11 = DirFromCell(cell11, gridRes); // Compute virtual plane UVs for all 4 frames float3 pivotToCamOS = mul(worldToObject, camPos - center); float3 vertexPosOS = mul(worldToObject, worldPos - center); float3 vertexToCamOS = pivotToCamOS - vertexPosOS; float2 uv00 = VirtualPlaneUV(dir00, pivotToCamOS, vertexToCamOS); float2 uv01 = VirtualPlaneUV(dir01, pivotToCamOS, vertexToCamOS); float2 uv10 = VirtualPlaneUV(dir10, pivotToCamOS, vertexToCamOS); float2 uv11 = VirtualPlaneUV(dir11, pivotToCamOS, vertexToCamOS); // Sample and blend frames float4 s00 = SampleAtlasGrad(uv00, cell00, ddx(uv00), ddy(uv00)); float4 s01 = SampleAtlasGrad(uv01, cell01, ddx(uv01), ddy(uv01)); float4 s10 = SampleAtlasGrad(uv10, cell10, ddx(uv10), ddy(uv10)); float4 s11 = SampleAtlasGrad(uv11, cell11, ddx(uv11), ddy(uv11)); float4 col = s00 * bw.x + s01 * bw.y + s10 * bw.z + s11 * bw.w; if (_Impostors_Debug_Mode > 0.5) return float4(bw.yz, 0, 1); clip(col.a - _Impostors_Cutoff); return col; } #endif // _IMPOSTORS #endif // __IMPOSTOR_INC