summaryrefslogtreecommitdiffstats
path: root/impostor.cginc
blob: bb4c38f627c3ba294e0a01c1271c0b3892f8dbfa (plain)
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
#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