From 568f813fe37cd7beb1709fb1a92268fef2581d4e Mon Sep 17 00:00:00 2001 From: yum Date: Thu, 15 Jan 2026 16:52:25 -0800 Subject: Impostors: add parallax correction --- 3ner.shader | 1 + Scripts/Impostors.cs | 65 +++++++++++++++++++++++++-- globals.cginc | 1 + impostor.cginc | 121 +++++++++++++++++++++++++++++++++++++++++++-------- 4 files changed, 167 insertions(+), 21 deletions(-) diff --git a/3ner.shader b/3ner.shader index b210532..94a41e1 100644 --- a/3ner.shader +++ b/3ner.shader @@ -387,6 +387,7 @@ Shader "yum_food/3ner" _Impostors_Grid_Resolution("Grid Resolution", Int) = 5 _Impostors_Sphere_Radius("Sphere Radius", Float) = 1.0 _Impostors_Cutoff("Alpha Cutoff", Range(0, 1)) = 0.5 + _Impostors_Parallax("Parallax Strength", Range(0, 1)) = 1 [Toggle] _Impostors_Debug_Mode("Debug Mode", Float) = 0 [Toggle] _Impostors_Debug_Depth("Debug Depth", Float) = 0 diff --git a/Scripts/Impostors.cs b/Scripts/Impostors.cs index ffd32b4..46e6cd1 100644 --- a/Scripts/Impostors.cs +++ b/Scripts/Impostors.cs @@ -26,7 +26,7 @@ public class Impostors : MonoBehaviour private Material impostorMaterial; public bool HasImpostor => impostorObject != null; - private float Radius => sphere_radius_ * transform.lossyScale.x; + private float Radius => sphere_radius_ * Mathf.Max(transform.lossyScale.x, transform.lossyScale.y, transform.lossyScale.z); private const string OutputFolder = "Assets/yum_food/3ner/Impostor_Generated"; void OnEnable() => Camera.onPreRender += UpdateMainCameraPos; @@ -35,7 +35,7 @@ public class Impostors : MonoBehaviour static void UpdateMainCameraPos(Camera cam) { if (cam.cameraType == CameraType.Game || cam.cameraType == CameraType.SceneView) - Shader.SetGlobalVector("_ImpostorMainCameraPos", cam.transform.position); + Shader.SetGlobalVector("_Impostors_Main_Camera_Pos", cam.transform.position); } void OnDrawGizmos() @@ -105,9 +105,9 @@ public class Impostors : MonoBehaviour Camera cam = camObj.AddComponent(); cam.orthographic = true; - cam.orthographicSize = sphere_radius_; + cam.orthographicSize = Radius; cam.nearClipPlane = nearClippingDistance; - cam.farClipPlane = sphere_radius_ * 2f + nearClippingDistance; + cam.farClipPlane = Radius * 2f + nearClippingDistance; cam.cullingMask = cullingMask; cam.clearFlags = renderSkybox ? CameraClearFlags.Skybox : CameraClearFlags.SolidColor; cam.backgroundColor = Color.clear; @@ -337,6 +337,61 @@ public class Impostors : MonoBehaviour tex.Apply(); } + void DilateDepthTexture(Texture2D depthTex, Texture2D alphaSource, int iterations = 8) + { + int w = depthTex.width, h = depthTex.height; + Color[] depth = depthTex.GetPixels(); + Color[] mask = alphaSource.GetPixels(); + + bool[] filled = new bool[depth.Length]; + for (int i = 0; i < depth.Length; i++) + filled[i] = mask[i].a > 0.01f; + + Color[] buffer = (Color[])depth.Clone(); + bool[] filledNext = new bool[filled.Length]; + + int[] dx = { -1, 1, 0, 0 }; + int[] dy = { 0, 0, -1, 1 }; + + for (int iter = 0; iter < iterations; iter++) + { + System.Array.Copy(filled, filledNext, filled.Length); + + for (int y = 0; y < h; y++) + { + for (int x = 0; x < w; x++) + { + int i = y * w + x; + if (filled[i]) continue; + + float sum = 0f; + int count = 0; + for (int d = 0; d < 4; d++) + { + int nx = x + dx[d], ny = y + dy[d]; + if (nx < 0 || nx >= w || ny < 0 || ny >= h) continue; + int ni = ny * w + nx; + if (!filled[ni]) continue; + sum += depth[ni].r; + count++; + } + + if (count > 0) + { + buffer[i].r = sum / count; + filledNext[i] = true; + } + } + } + + var tmpFilled = filled; filled = filledNext; filledNext = tmpFilled; + var tmpDepth = depth; depth = buffer; buffer = tmpDepth; + } + + depthTex.SetPixels(depth); + depthTex.Apply(); + } + void SaveAndConfigureTexture(Texture2D atlas, TextureExportSettings settings, string baseName, out string path) { path = Path.Combine(OutputFolder, $"{baseName}_{settings.suffix}.{(settings.isEXR ? "exr" : "png")}"); @@ -386,6 +441,7 @@ public class Impostors : MonoBehaviour DilateTexture(albedoAtlas, albedoAtlas, preserveAlpha: true); DilateTexture(normalAtlas, albedoAtlas); DilateTexture(metallicGlossAtlas, albedoAtlas); + DilateDepthTexture(depthAtlas, albedoAtlas); Texture2D[] atlases = { albedoAtlas, normalAtlas, metallicGlossAtlas, depthAtlas }; string[] paths = new string[exportSettings.Length]; @@ -426,6 +482,7 @@ public class Impostors : MonoBehaviour impostorMaterial.SetInt("_Impostors_Grid_Resolution", gridResolution); impostorMaterial.SetFloat("_Impostors_Sphere_Radius", sphere_radius_); impostorMaterial.SetFloat("_Impostors_Enabled", 1); + impostorMaterial.SetFloat("_Impostors_Parallax", 1); impostorMaterial.SetFloat("_Cull", (float)UnityEngine.Rendering.CullMode.Front); AssetDatabase.CreateAsset(impostorMaterial, Path.Combine(OutputFolder, $"{baseName}_mat.mat")); diff --git a/globals.cginc b/globals.cginc index b1de3d8..6387c7e 100644 --- a/globals.cginc +++ b/globals.cginc @@ -190,6 +190,7 @@ Texture2D _Impostors_Depth_Atlas; int _Impostors_Grid_Resolution; float _Impostors_Sphere_Radius; float _Impostors_Cutoff; +float _Impostors_Parallax; float _Impostors_Debug_Mode; float _Impostors_Debug_Depth; float3 _Impostors_Main_Camera_Pos; diff --git a/impostor.cginc b/impostor.cginc index 1623681..5a923c5 100644 --- a/impostor.cginc +++ b/impostor.cginc @@ -16,6 +16,13 @@ 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 FrameBasis(float3 frameDir, out float3 planeX, out float3 planeY, out float3 planeN) { + planeN = normalize(frameDir); + float3 up = abs(planeN.y) > 0.999 ? float3(0,0,1) : float3(0,1,0); + planeX = normalize(cross(planeN, up)); + planeY = normalize(cross(planeX, planeN)); +} + 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); @@ -42,10 +49,8 @@ float4 GridCellBarycentric4(float2 p) { // 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)); + float3 planeX, planeY, planeN; + FrameBasis(frameDir, planeX, planeY, planeN); float projPivot = dot(planeN, pivotToCam); float projVertex = dot(planeN, vertexToCam); @@ -86,14 +91,21 @@ struct ImpostorResult { float smoothness; }; -ImpostorSample SampleImpostorCell(float2 cell, float3 frameDir, float3 pivotToCamOS, float3 vertexToCamOS, float gridRes) { - float2 uv = VirtualPlaneUV(frameDir, pivotToCamOS, vertexToCamOS); - uv = ClampUvInCell(uv); +float SampleImpostorDepthCell(float2 cell, float2 uvInCell, float gridRes) { + uvInCell = ClampUvInCell(uvInCell); + float invGridRes = rcp(gridRes); + float2 atlasUv = (cell + uvInCell) * invGridRes; + float2 gradX = ddx(uvInCell) * invGridRes; + float2 gradY = ddy(uvInCell) * invGridRes; + return _Impostors_Depth_Atlas.SampleGrad(bilinear_clamp_s, atlasUv, gradX, gradY).r; +} +ImpostorSample SampleImpostorCell(float2 cell, float2 uvInCell, float gridRes) { + uvInCell = ClampUvInCell(uvInCell); float invGridRes = rcp(gridRes); - float2 atlasUv = (cell + uv) * invGridRes; - float2 gradX = ddx(uv) * invGridRes; - float2 gradY = ddy(uv) * invGridRes; + float2 atlasUv = (cell + uvInCell) * invGridRes; + float2 gradX = ddx(uvInCell) * invGridRes; + float2 gradY = ddy(uvInCell) * invGridRes; ImpostorSample s; s.albedo = _Impostors_Atlas.SampleGrad(bilinear_clamp_s, atlasUv, gradX, gradY); @@ -102,9 +114,59 @@ ImpostorSample SampleImpostorCell(float2 cell, float3 frameDir, float3 pivotToCa return s; } +float2 ImpostorParallaxOffsetForFrame(float3 frameDir, float3 pivotToCamOS, float2 uvBase, float depth01) { + float3 planeX, planeY, planeN; + FrameBasis(frameDir, planeX, planeY, planeN); + + float2 camXY = float2(dot(pivotToCamOS, planeX), dot(pivotToCamOS, planeY)); + float camZ = dot(pivotToCamOS, planeN); + camZ = (abs(camZ) < 1e-4) ? (camZ < 0 ? -1e-4 : 1e-4) : camZ; + + // Bake depth is linear in ortho: 0=near surface, 1=far surface. + // Convert to signed "height" where nearer pixels shift more (matches typical bump-offset convention). + float zSurface = (depth01 - 0.5); + float2 planeCoord = 0.5 - uvBase; + + return (planeCoord - camXY) * (zSurface / camZ); +} + +ImpostorSample SampleImpostorCellParallaxSafe( + float2 cell, + float3 frameDir, + float3 pivotToCamOS, + float3 vertexToCamOS, + float gridRes) +{ + float2 uvBase = ClampUvInCell(VirtualPlaneUV(frameDir, pivotToCamOS, vertexToCamOS)); + ImpostorSample baseS = SampleImpostorCell(cell, uvBase, gridRes); + + float baseAlpha = baseS.albedo.a; + float parallaxStrength = _Impostors_Parallax * smoothstep(_Impostors_Cutoff, 1.0, baseAlpha); + if (parallaxStrength <= 0.001) return baseS; + + float depth01 = SampleImpostorDepthCell(cell, uvBase, gridRes); + float2 uvParallax = uvBase + ImpostorParallaxOffsetForFrame(frameDir, pivotToCamOS, uvBase, depth01) * parallaxStrength; + ImpostorSample parS = SampleImpostorCell(cell, uvParallax, gridRes); + + float denom = max(baseAlpha - _Impostors_Cutoff, 1e-4); + float t = saturate((parS.albedo.a - _Impostors_Cutoff) / denom); + baseS.albedo = lerp(baseS.albedo, parS.albedo, t); + baseS.normal = lerp(baseS.normal, parS.normal, t); + baseS.metallicGloss = lerp(baseS.metallicGloss, parS.metallicGloss, t); + return baseS; +} + ImpostorSample BlendImpostorSamples(ImpostorSample s00, ImpostorSample s01, ImpostorSample s10, ImpostorSample s11, float4 bw) { ImpostorSample result; - result.albedo = s00.albedo * bw.x + s01.albedo * bw.y + s10.albedo * bw.z + s11.albedo * bw.w; + float4 alpha = float4(s00.albedo.a, s01.albedo.a, s10.albedo.a, s11.albedo.a); + float alphaOut = dot(alpha, bw); + float3 premul = + s00.albedo.rgb * (alpha.x * bw.x) + + s01.albedo.rgb * (alpha.y * bw.y) + + s10.albedo.rgb * (alpha.z * bw.z) + + s11.albedo.rgb * (alpha.w * bw.w); + float3 rgbOut = premul / max(alphaOut, 1e-4); + result.albedo = float4(rgbOut, alphaOut); // Weight normal/metallicGloss by alpha to avoid blending with transparent (zero) pixels float4 alphaBw = float4(s00.albedo.a, s01.albedo.a, s10.albedo.a, s11.albedo.a) * bw; @@ -151,7 +213,14 @@ ImpostorResult impostor_frag(float3 worldPos) { float3 camPos = _WorldSpaceCameraPos; #endif float3 viewDir = normalize(worldPos - camPos); - bool didIntersect = RaySphereIntersect(camPos, viewDir, center, _Impostors_Sphere_Radius); + + float3 scale = float3( + length(unity_ObjectToWorld._m00_m10_m20), + length(unity_ObjectToWorld._m01_m11_m21), + length(unity_ObjectToWorld._m02_m12_m22)); + float radiusWS = _Impostors_Sphere_Radius * max(scale.x, max(scale.y, scale.z)); + + bool didIntersect = RaySphereIntersect(camPos, viewDir, center, radiusWS); clip(didIntersect - 0.5); // For lattice lookup, use the camera-to-impostor-center direction (matches billboard orientation). @@ -178,11 +247,29 @@ ImpostorResult impostor_frag(float3 worldPos) { float3 vertexPosOS = mul(worldToObject, worldPos - center); float3 vertexToCamOS = pivotToCamOS - vertexPosOS; - // Sample all atlases for each frame cell - ImpostorSample s00 = SampleImpostorCell(cell00, DirFromCell(cell00, gridRes), pivotToCamOS, vertexToCamOS, gridRes); - ImpostorSample s01 = SampleImpostorCell(cell01, DirFromCell(cell01, gridRes), pivotToCamOS, vertexToCamOS, gridRes); - ImpostorSample s10 = SampleImpostorCell(cell10, DirFromCell(cell10, gridRes), pivotToCamOS, vertexToCamOS, gridRes); - ImpostorSample s11 = SampleImpostorCell(cell11, DirFromCell(cell11, gridRes), pivotToCamOS, vertexToCamOS, gridRes); + float3 frameDir00 = DirFromCell(cell00, gridRes); + float3 frameDir01 = DirFromCell(cell01, gridRes); + float3 frameDir10 = DirFromCell(cell10, gridRes); + float3 frameDir11 = DirFromCell(cell11, gridRes); + + if (_Impostors_Debug_Depth > 0.5) { + float2 uvBase00 = ClampUvInCell(VirtualPlaneUV(frameDir00, pivotToCamOS, vertexToCamOS)); + float2 uvBase01 = ClampUvInCell(VirtualPlaneUV(frameDir01, pivotToCamOS, vertexToCamOS)); + float2 uvBase10 = ClampUvInCell(VirtualPlaneUV(frameDir10, pivotToCamOS, vertexToCamOS)); + float2 uvBase11 = ClampUvInCell(VirtualPlaneUV(frameDir11, pivotToCamOS, vertexToCamOS)); + float depth00 = SampleImpostorDepthCell(cell00, uvBase00, gridRes); + float depth01 = SampleImpostorDepthCell(cell01, uvBase01, gridRes); + float depth10 = SampleImpostorDepthCell(cell10, uvBase10, gridRes); + float depth11 = SampleImpostorDepthCell(cell11, uvBase11, gridRes); + float depthBlended = depth00 * bw.x + depth01 * bw.y + depth10 * bw.z + depth11 * bw.w; + result.albedo = float4(depthBlended.xxx, 1); + return result; + } + + ImpostorSample s00 = SampleImpostorCellParallaxSafe(cell00, frameDir00, pivotToCamOS, vertexToCamOS, gridRes); + ImpostorSample s01 = SampleImpostorCellParallaxSafe(cell01, frameDir01, pivotToCamOS, vertexToCamOS, gridRes); + ImpostorSample s10 = SampleImpostorCellParallaxSafe(cell10, frameDir10, pivotToCamOS, vertexToCamOS, gridRes); + ImpostorSample s11 = SampleImpostorCellParallaxSafe(cell11, frameDir11, pivotToCamOS, vertexToCamOS, gridRes); // Blend all samples ImpostorSample blended = BlendImpostorSamples(s00, s01, s10, s11, bw); -- cgit v1.2.3