1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
|
#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 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);
}
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 for 3 points
float3 BarycentricWeights3(float2 gridFrac, bool isBottomRight) {
if (isBottomRight) {
// Bottom-Right Triangle: (0,0), (1,0), (1,1)
return float3(1.0 - gridFrac.x, gridFrac.x - gridFrac.y, gridFrac.y);
} else {
// Top-Left Triangle: (0,0), (0,1), (1,1)
return float3(1.0 - gridFrac.y, gridFrac.y - gridFrac.x, gridFrac.x);
}
}
// Compute UV on a virtual plane facing frameDir
float2 VirtualPlaneUV(float3 frameDir, float3 pivotToCam, float3 vertexToCam) {
float3 planeX, planeY, planeN;
FrameBasis(frameDir, planeX, planeY, planeN);
float projPivot = dot(planeN, pivotToCam);
float projVertex = dot(planeN, vertexToCam);
projVertex = (abs(projVertex) < 1e-4) ? (projVertex < 0 ? -1e-4 : 1e-4) : projVertex;
float ratio = projPivot / projVertex;
float3 offset = vertexToCam * ratio - pivotToCam;
float2 uv = float2(dot(planeX, offset), dot(planeY, offset));
return uv * -1.0 + 0.5;
}
#if defined(_IMPOSTORS)
// Decode linear 0-1 depth (0=near clip, 1=far clip) to world space distance from camera.
float DecodeImpostorDepth(float linearDepth) {
return lerp(_Impostors_Near_Clip, _Impostors_Far_Clip, linearDepth);
}
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);
}
struct ImpostorSample {
float4 albedo;
float4 normal;
float4 metallicGloss;
};
struct ImpostorResult {
float4 albedo;
float3 normal;
float metallic;
float smoothness;
float3 objPos;
// TODO rm
float debug;
};
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;
// 0 = near clip plane, 1 = far clip plane
return _Impostors_Depth_Atlas.SampleGrad(bilinear_clamp_s, atlasUv, gradX, gradY).r;
}
float SampleImpostorAlphaCell(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_Atlas.SampleGrad(bilinear_clamp_s, atlasUv, gradX, gradY).a;
}
ImpostorSample SampleImpostorCell(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;
ImpostorSample s;
s.albedo = _Impostors_Atlas.SampleGrad(bilinear_clamp_s, atlasUv, gradX, gradY);
s.normal = _Impostors_Normal_Atlas.SampleGrad(bilinear_clamp_s, atlasUv, gradX, gradY);
s.metallicGloss = _Impostors_Metallic_Gloss_Atlas.SampleGrad(bilinear_clamp_s, atlasUv, gradX, gradY).xzzy;
return s;
}
float2 ImpostorParallaxOffsetForFrame(float3 frameDir, float3 pivotToCamOS, float2 uvBase, float encodedDepth) {
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;
// Convert world-space depth to object space to match scaled impostor.
float objScale = max(2.0 * _Impostors_Sphere_Radius, 1e-4);
float objRadius = _Impostors_Sphere_Radius / objScale;
float objNear = _Impostors_Near_Clip / objScale;
float worldSpaceDepth = DecodeImpostorDepth(encodedDepth);
float objectSpaceDepth = worldSpaceDepth / objScale;
float depth01 = (objectSpaceDepth - objNear) / (2.0 * objRadius);
// 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);
}
// Reconstruct object-space offset from impostor center.
// frameDir: object-space direction from center to baking camera (same space as UV computation)
// UV encodes X,Y on the virtual plane (0.5 = center), depth is linear 0-1 (near to far clip).
float3 ReconstructObjectOffsetFromFrame(float3 frameDir, float2 uv, float encodedDepth) {
float3 planeX, planeY, planeN;
FrameBasis(frameDir, planeX, planeY, planeN);
float objScale = max(2.0f * _Impostors_Sphere_Radius, 1e-4);
float objRadius = _Impostors_Sphere_Radius / objScale;
float objNear = _Impostors_Near_Clip / objScale;
float objFar = _Impostors_Far_Clip / objScale;
float2 offsetXY = (0.5f - uv) * 2.0f * objRadius;
float objectSpaceDepth = lerp(objNear, objFar, encodedDepth);
float offsetZ = (objRadius + objNear) - objectSpaceDepth;
return offsetXY.x * planeX + offsetXY.y * planeY + offsetZ * planeN;
}
ImpostorSample BlendImpostorSamples(ImpostorSample s0, ImpostorSample s1, ImpostorSample s2, float3 bw) {
ImpostorSample result;
float3 alpha = float3(s0.albedo.a, s1.albedo.a, s2.albedo.a);
float alphaOut = dot(alpha, bw);
float3 premul =
s0.albedo.rgb * (alpha.x * bw.x) +
s1.albedo.rgb * (alpha.y * bw.y) +
s2.albedo.rgb * (alpha.z * bw.z);
float3 rgbOut = premul / max(alphaOut, 1e-4);
result.albedo = float4(rgbOut, alphaOut);
// Weight normal/metallicGloss by alpha to avoid blending with transparent (zero) pixels
float3 alphaBw = alpha * bw;
alphaBw /= max(alphaBw.x + alphaBw.y + alphaBw.z, 0.001);
result.normal = s0.normal * alphaBw.x + s1.normal * alphaBw.y + s2.normal * alphaBw.z;
result.metallicGloss = s0.metallicGloss * alphaBw.x + s1.metallicGloss * alphaBw.y + s2.metallicGloss * alphaBw.z;
return result;
}
// Billboard vertex transformation for impostors
void impostor_vert(inout float3 vertexOS) {
float3 center = mul(unity_ObjectToWorld, float4(0,0,0,1)).xyz;
#ifdef SHADOW_CASTER_PASS
float3 camPos = _Impostors_Main_Camera_Pos;
#else
float3 camPos = _WorldSpaceCameraPos;
#endif
// Billboard facing the camera direction (world space, then convert back to object space).
float3 viewWS = normalize(camPos - center);
float3 right, up;
BillboardBasis(viewWS, right, up);
float radiusScale = _Impostors_Sphere_Radius * 2.0;
float3 worldPos = center + vertexOS.x * right * radiusScale + vertexOS.y * up * radiusScale;
vertexOS = mul(unity_WorldToObject, float4(worldPos, 1)).xyz;
}
// Sample impostor atlas with view-dependent blending
ImpostorResult impostor_frag(float3 worldPos) {
ImpostorResult result = (ImpostorResult)0;
// 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);
float radiusWS = _Impostors_Sphere_Radius;
float3 originToRo = camPos - center;
float b = dot(originToRo, viewDir);
float c = dot(originToRo, originToRo) - radiusWS * radiusWS;
clip(b * b - c);
// 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 3 nearest frames
float gridRes = (float)_Impostors_Grid_Resolution;
float2 grid = GridFromDir(viewOS, gridRes);
float2 gridFloor = floor(grid);
float2 gridFrac = frac(grid);
// Determine triangle and weights
bool isBottomRight = gridFrac.x > gridFrac.y;
float3 bw = BarycentricWeights3(gridFrac, isBottomRight);
// Frame cells
float2 cell0 = clamp(gridFloor, 0, gridRes - 1);
float2 cell1 = clamp(gridFloor + (isBottomRight ? float2(1,0) : float2(0,1)), 0, gridRes - 1);
float2 cell2 = clamp(gridFloor + float2(1,1), 0, gridRes - 1);
// Compute shared vectors for virtual plane UV calculation
float3 pivotToCamOS = mul(worldToObject, camPos - center);
float3 vertexPosOS = mul(worldToObject, worldPos - center);
float3 vertexToCamOS = pivotToCamOS - vertexPosOS;
float3 frameDir0 = DirFromCell(cell0, gridRes);
float3 frameDir1 = DirFromCell(cell1, gridRes);
float3 frameDir2 = DirFromCell(cell2, gridRes);
float2 uvBase0 = ClampUvInCell(VirtualPlaneUV(frameDir0, pivotToCamOS, vertexToCamOS));
float2 uvBase1 = ClampUvInCell(VirtualPlaneUV(frameDir1, pivotToCamOS, vertexToCamOS));
float2 uvBase2 = ClampUvInCell(VirtualPlaneUV(frameDir2, pivotToCamOS, vertexToCamOS));
float baseAlpha0 = SampleImpostorAlphaCell(cell0, uvBase0, gridRes);
float baseAlpha1 = SampleImpostorAlphaCell(cell1, uvBase1, gridRes);
float baseAlpha2 = SampleImpostorAlphaCell(cell2, uvBase2, gridRes);
float baseAlphaBlended = baseAlpha0 * bw.x + baseAlpha1 * bw.y + baseAlpha2 * bw.z;
float parallaxStrength = _Impostors_Parallax * smoothstep(_Impostors_Cutoff, 1.0, baseAlphaBlended);
float depth0 = SampleImpostorDepthCell(cell0, uvBase0, gridRes);
float depth1 = SampleImpostorDepthCell(cell1, uvBase1, gridRes);
float depth2 = SampleImpostorDepthCell(cell2, uvBase2, gridRes);
float depthBlended = depth0 * bw.x + depth1 * bw.y + depth2 * bw.z;
if (_Impostors_Debug_Depth > 0.5) {
result.albedo = float4(depthBlended.xxx, 1);
return result;
}
float2 uv0 = uvBase0;
float2 uv1 = uvBase1;
float2 uv2 = uvBase2;
if (parallaxStrength > 0.001) {
uv0 = uvBase0 + ImpostorParallaxOffsetForFrame(frameDir0, pivotToCamOS, uvBase0, depthBlended) * parallaxStrength;
uv1 = uvBase1 + ImpostorParallaxOffsetForFrame(frameDir1, pivotToCamOS, uvBase1, depthBlended) * parallaxStrength;
uv2 = uvBase2 + ImpostorParallaxOffsetForFrame(frameDir2, pivotToCamOS, uvBase2, depthBlended) * parallaxStrength;
}
float2 uv0Final = uv0;
float2 uv1Final = uv1;
float2 uv2Final = uv2;
ImpostorSample s0 = SampleImpostorCell(cell0, uv0Final, gridRes);
ImpostorSample s1 = SampleImpostorCell(cell1, uv1Final, gridRes);
ImpostorSample s2 = SampleImpostorCell(cell2, uv2Final, gridRes);
// Parallax can push UVs into transparent pixels at silhouettes; fall back to unshifted UVs when that happens.
if (parallaxStrength > 0.001) {
if (s0.albedo.a < _Impostors_Cutoff && baseAlpha0 > _Impostors_Cutoff) {
uv0Final = uvBase0;
s0 = SampleImpostorCell(cell0, uv0Final, gridRes);
}
if (s1.albedo.a < _Impostors_Cutoff && baseAlpha1 > _Impostors_Cutoff) {
uv1Final = uvBase1;
s1 = SampleImpostorCell(cell1, uv1Final, gridRes);
}
if (s2.albedo.a < _Impostors_Cutoff && baseAlpha2 > _Impostors_Cutoff) {
uv2Final = uvBase2;
s2 = SampleImpostorCell(cell2, uv2Final, gridRes);
}
}
// Blend 3 samples
ImpostorSample blended = BlendImpostorSamples(s0, s1, s2, bw);
if (_Impostors_Debug_Mode > 0.5) {
// Debug view of weights
result.albedo = float4(bw, 1);
return result;
}
clip(blended.albedo.a - _Impostors_Cutoff);
result.albedo = blended.albedo;
result.normal = normalize(blended.normal.xyz * 2.0 - 1.0);
result.metallic = blended.metallicGloss.r;
result.smoothness = blended.metallicGloss.a;
// Reconstruct object-space position from each frame's UV and depth, then blend.
float depth0Pos = SampleImpostorDepthCell(cell0, uv0Final, gridRes);
float depth1Pos = SampleImpostorDepthCell(cell1, uv1Final, gridRes);
float depth2Pos = SampleImpostorDepthCell(cell2, uv2Final, gridRes);
// Use object-space frame directions (same space as UV computation)
float3 offset0 = ReconstructObjectOffsetFromFrame(frameDir0, uv0Final, depth0Pos);
float3 offset1 = ReconstructObjectOffsetFromFrame(frameDir1, uv1Final, depth1Pos);
float3 offset2 = ReconstructObjectOffsetFromFrame(frameDir2, uv2Final, depth2Pos);
float3 objOffset = offset0 * bw.x + offset1 * bw.y + offset2 * bw.z;
// Return object-space position (offset from origin)
result.objPos = objOffset;
return result;
}
#endif // _IMPOSTORS
#endif // __IMPOSTOR_INC
|