@tinypoly/gpu-renderer 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/environment.d.ts.map +1 -1
- package/dist/environment.js +16 -3
- package/dist/environment.js.map +1 -1
- package/dist/frameLayout.d.ts.map +1 -1
- package/dist/frameLayout.js +2 -0
- package/dist/frameLayout.js.map +1 -1
- package/dist/grass.d.ts +106 -0
- package/dist/grass.d.ts.map +1 -0
- package/dist/grass.js +350 -0
- package/dist/grass.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/materialLayout.d.ts +6 -0
- package/dist/materialLayout.d.ts.map +1 -1
- package/dist/materialLayout.js +2 -0
- package/dist/materialLayout.js.map +1 -1
- package/dist/pathTracePass.d.ts +2 -2
- package/dist/pathTracePass.d.ts.map +1 -1
- package/dist/pathTracePass.js +11 -9
- package/dist/pathTracePass.js.map +1 -1
- package/dist/renderer.d.ts.map +1 -1
- package/dist/renderer.js +14 -3
- package/dist/renderer.js.map +1 -1
- package/dist/shaderProgram.d.ts.map +1 -1
- package/dist/shaderProgram.js +10 -1
- package/dist/shaderProgram.js.map +1 -1
- package/dist/subsurface.d.ts +2 -0
- package/dist/subsurface.d.ts.map +1 -1
- package/dist/subsurface.js +3 -2
- package/dist/subsurface.js.map +1 -1
- package/dist/wgsl/bsdf.wgsl.d.ts +1 -1
- package/dist/wgsl/bsdf.wgsl.d.ts.map +1 -1
- package/dist/wgsl/bsdf.wgsl.js +39 -3
- package/dist/wgsl/bsdf.wgsl.js.map +1 -1
- package/dist/wgsl/grass.wgsl.d.ts +14 -0
- package/dist/wgsl/grass.wgsl.d.ts.map +1 -0
- package/dist/wgsl/grass.wgsl.js +456 -0
- package/dist/wgsl/grass.wgsl.js.map +1 -0
- package/dist/wgsl/lights.wgsl.d.ts +1 -1
- package/dist/wgsl/lights.wgsl.d.ts.map +1 -1
- package/dist/wgsl/lights.wgsl.js +5 -0
- package/dist/wgsl/lights.wgsl.js.map +1 -1
- package/dist/wgsl/pathTrace.wgsl.d.ts +1 -1
- package/dist/wgsl/pathTrace.wgsl.d.ts.map +1 -1
- package/dist/wgsl/pathTrace.wgsl.js +34 -5
- package/dist/wgsl/pathTrace.wgsl.js.map +1 -1
- package/dist/wgsl/surface.wgsl.d.ts +1 -1
- package/dist/wgsl/surface.wgsl.d.ts.map +1 -1
- package/dist/wgsl/surface.wgsl.js +2 -0
- package/dist/wgsl/surface.wgsl.js.map +1 -1
- package/package.json +2 -2
- package/src/environment.ts +19 -2
- package/src/frameLayout.ts +2 -0
- package/src/grass.ts +523 -0
- package/src/index.ts +2 -0
- package/src/materialLayout.ts +7 -0
- package/src/pathTracePass.ts +11 -9
- package/src/renderer.ts +21 -6
- package/src/shaderProgram.ts +11 -1
- package/src/subsurface.ts +3 -2
- package/src/wgsl/bsdf.wgsl.ts +39 -3
- package/src/wgsl/grass.wgsl.ts +456 -0
- package/src/wgsl/lights.wgsl.ts +5 -0
- package/src/wgsl/pathTrace.wgsl.ts +34 -5
- package/src/wgsl/surface.wgsl.ts +2 -0
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grass grown on a material's surface: see grass.ts for the layout of what the texture buffer holds.
|
|
3
|
+
*
|
|
4
|
+
* A triangle that grows grass is a shell, its footprint on the ground times the height of the lawn above its plane, and
|
|
5
|
+
* the shells have a hierarchy of their own. Inside a shell the ray walks the world's tiles front to back, and in each
|
|
6
|
+
* one it is taken into the space of that tile's patch, where the plane is the ground and blades stand on it: a shear
|
|
7
|
+
* that keeps the ray's distances as they are. The search in a tile is held to the stretch of the ray over that tile and
|
|
8
|
+
* that triangle, so a blade that leans over a border is seen once, from the patch it is drawn in, and the first tile
|
|
9
|
+
* with a hit holds the closest one in the shell.
|
|
10
|
+
*/
|
|
11
|
+
export const GRASS_WGSL = /* wgsl */ `
|
|
12
|
+
const GRASS_NONE: u32 = 0xffffffffu;
|
|
13
|
+
const GRASS_MAX_TILES: u32 = 48u;
|
|
14
|
+
// What a hit on a blade carries as its triangle, which no scene triangle can be.
|
|
15
|
+
const GRASS_TRIANGLE: u32 = 0xfffffffeu;
|
|
16
|
+
|
|
17
|
+
struct GrassHit {
|
|
18
|
+
found: bool,
|
|
19
|
+
distance: f32,
|
|
20
|
+
// The lawn's record and the patch triangle hit, both as offsets into the texture buffer.
|
|
21
|
+
record: u32,
|
|
22
|
+
triangle: u32,
|
|
23
|
+
u: f32,
|
|
24
|
+
v: f32,
|
|
25
|
+
// The ground plane's slope and where the tile starts, which take the patch back to the world.
|
|
26
|
+
slope: vec2<f32>,
|
|
27
|
+
tile: vec2<f32>,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
fn noGrass(maxDistance: f32) -> GrassHit {
|
|
31
|
+
var hit: GrassHit;
|
|
32
|
+
hit.found = false;
|
|
33
|
+
hit.distance = maxDistance;
|
|
34
|
+
|
|
35
|
+
return hit;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
fn grassNode(at: u32) -> BvhNode {
|
|
39
|
+
var node: BvhNode;
|
|
40
|
+
node.leftLow = vec3<f32>(extraFloat(at), extraFloat(at + 1u), extraFloat(at + 2u));
|
|
41
|
+
node.leftLink = bitcast<i32>(textures[at + 3u]);
|
|
42
|
+
node.leftHigh = vec3<f32>(extraFloat(at + 4u), extraFloat(at + 5u), extraFloat(at + 6u));
|
|
43
|
+
node.leftCount = bitcast<i32>(textures[at + 7u]);
|
|
44
|
+
node.rightLow = vec3<f32>(extraFloat(at + 8u), extraFloat(at + 9u), extraFloat(at + 10u));
|
|
45
|
+
node.rightLink = bitcast<i32>(textures[at + 11u]);
|
|
46
|
+
node.rightHigh = vec3<f32>(extraFloat(at + 12u), extraFloat(at + 13u), extraFloat(at + 14u));
|
|
47
|
+
node.rightCount = bitcast<i32>(textures[at + 15u]);
|
|
48
|
+
|
|
49
|
+
return node;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
fn grassVec3(at: u32) -> vec3<f32> {
|
|
53
|
+
return vec3<f32>(extraFloat(at), extraFloat(at + 1u), extraFloat(at + 2u));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
fn grassHash(cell: vec2<i32>) -> u32 {
|
|
57
|
+
return pcgHash((bitcast<u32>(cell.x) * 1597334677u) ^ pcgHash(bitcast<u32>(cell.y) * 3812015801u));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Smooth value noise in zero to one, for what changes across the lawn rather than from blade to blade. */
|
|
61
|
+
fn grassNoise(p: vec2<f32>) -> f32 {
|
|
62
|
+
let cell = floor(p);
|
|
63
|
+
let f = p - cell;
|
|
64
|
+
let s = f * f * (3.0 - 2.0 * f);
|
|
65
|
+
let c = vec2<i32>(cell);
|
|
66
|
+
let corner = vec4<f32>(
|
|
67
|
+
f32(grassHash(c) >> 8u), f32(grassHash(c + vec2<i32>(1, 0)) >> 8u),
|
|
68
|
+
f32(grassHash(c + vec2<i32>(0, 1)) >> 8u), f32(grassHash(c + vec2<i32>(1, 1)) >> 8u)) / 16777216.0;
|
|
69
|
+
|
|
70
|
+
return mix(mix(corner.x, corner.y, s.x), mix(corner.z, corner.w, s.x), s.y);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Whether a blade grows where its root stands: the lawn thins out in patches, and each blade has its own number. */
|
|
74
|
+
fn grassKeeps(record: u32, root: vec2<f32>, rand: f32) -> bool {
|
|
75
|
+
let patchiness = extraFloat(record + 22u);
|
|
76
|
+
|
|
77
|
+
if (patchiness <= 0.0) {
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let thin = smoothstep(0.3, 0.75, grassNoise(root * extraFloat(record + 23u) + vec2<f32>(31.7, 11.3)));
|
|
82
|
+
|
|
83
|
+
return rand >= patchiness * thin;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The closest blade of one patch along the ray, between near and the hit so far: origin and direction already in the
|
|
88
|
+
* patch's space. Records the triangle in best and returns whether it found one.
|
|
89
|
+
*/
|
|
90
|
+
fn tracePatch(record: u32, root: u32, tile: vec2<f32>, origin: vec3<f32>, direction: vec3<f32>, near: f32,
|
|
91
|
+
far: f32, best: ptr<function, GrassHit>) -> bool {
|
|
92
|
+
let triangles = textures[record + 3u];
|
|
93
|
+
let blades = textures[record + 4u];
|
|
94
|
+
let signs = select(vec3<f32>(-1.0), vec3<f32>(1.0), direction >= vec3<f32>(0.0));
|
|
95
|
+
let inverse = signs / max(abs(direction), vec3<f32>(1e-20));
|
|
96
|
+
var closest = far;
|
|
97
|
+
var found = false;
|
|
98
|
+
var stack: array<u32, 32>;
|
|
99
|
+
var depth = 0u;
|
|
100
|
+
var current = 0u;
|
|
101
|
+
|
|
102
|
+
loop {
|
|
103
|
+
let node = grassNode(root + current * 16u);
|
|
104
|
+
let leftEnter = enterBox(node.leftLow, node.leftHigh, origin, inverse, closest);
|
|
105
|
+
let rightEnter = enterBox(node.rightLow, node.rightHigh, origin, inverse, closest);
|
|
106
|
+
var next = NO_NODE;
|
|
107
|
+
|
|
108
|
+
for (var side = 0u; side < 2u; side = side + 1u) {
|
|
109
|
+
let enter = select(leftEnter, rightEnter, side == 1u);
|
|
110
|
+
let link = select(node.leftLink, node.rightLink, side == 1u);
|
|
111
|
+
let count = select(node.leftCount, node.rightCount, side == 1u);
|
|
112
|
+
|
|
113
|
+
if (enter >= 1e30) {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (link >= 0) {
|
|
118
|
+
if (next == NO_NODE) {
|
|
119
|
+
next = u32(link);
|
|
120
|
+
} else if (depth < 32u) {
|
|
121
|
+
stack[depth] = u32(link);
|
|
122
|
+
depth = depth + 1u;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
let start = u32(-1 - link);
|
|
129
|
+
|
|
130
|
+
for (var i = 0u; i < u32(count); i = i + 1u) {
|
|
131
|
+
let at = triangles + (start + i) * 16u;
|
|
132
|
+
let a = grassVec3(at);
|
|
133
|
+
let edge1 = grassVec3(at + 3u) - a;
|
|
134
|
+
let edge2 = grassVec3(at + 6u) - a;
|
|
135
|
+
let p = cross(direction, edge2);
|
|
136
|
+
let determinant = dot(edge1, p);
|
|
137
|
+
|
|
138
|
+
if (abs(determinant) < 1e-14) {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let invDet = 1.0 / determinant;
|
|
143
|
+
let offset = origin - a;
|
|
144
|
+
let u = dot(offset, p) * invDet;
|
|
145
|
+
let q = cross(offset, edge1);
|
|
146
|
+
let v = dot(direction, q) * invDet;
|
|
147
|
+
let t = dot(edge2, q) * invDet;
|
|
148
|
+
|
|
149
|
+
if (u < 0.0 || v < 0.0 || u + v > 1.0 || t <= near || t >= closest) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
let blade = blades + textures[at + 15u] * 4u;
|
|
154
|
+
let rootAt = tile + vec2<f32>(extraFloat(blade), extraFloat(blade + 1u));
|
|
155
|
+
|
|
156
|
+
if (!grassKeeps(record, rootAt, extraFloat(blade + 2u))) {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
closest = t;
|
|
161
|
+
found = true;
|
|
162
|
+
(*best).triangle = at;
|
|
163
|
+
(*best).u = u;
|
|
164
|
+
(*best).v = v;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (next == NO_NODE) {
|
|
169
|
+
if (depth == 0u) {
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
depth = depth - 1u;
|
|
174
|
+
next = stack[depth];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
current = next;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (found) {
|
|
181
|
+
(*best).distance = closest;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return found;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Keeps the part of [range.x, range.y] where offset + rate t stays at or above zero. */
|
|
188
|
+
fn grassClip(range: vec2<f32>, offset: f32, rate: f32) -> vec2<f32> {
|
|
189
|
+
if (abs(rate) < 1e-20) {
|
|
190
|
+
return select(range, vec2<f32>(1.0, 0.0), offset < 0.0);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
let at = -offset / rate;
|
|
194
|
+
|
|
195
|
+
return select(vec2<f32>(range.x, min(range.y, at)), vec2<f32>(max(range.x, at), range.y), rate > 0.0);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** The edge from p to q of an anticlockwise footprint, as the ray crosses it on the ground: inside is on the left. */
|
|
199
|
+
fn grassEdge(range: vec2<f32>, p: vec2<f32>, q: vec2<f32>, origin: vec2<f32>, direction: vec2<f32>) -> vec2<f32> {
|
|
200
|
+
let e = q - p;
|
|
201
|
+
|
|
202
|
+
return grassClip(range, e.x * (origin.y - p.y) - e.y * (origin.x - p.x), e.x * direction.y - e.y * direction.x);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** The grass over one triangle, against the closest hit so far. */
|
|
206
|
+
fn grassInShell(entry: u32, origin: vec3<f32>, direction: vec3<f32>, best: ptr<function, GrassHit>) {
|
|
207
|
+
let record = textures[entry];
|
|
208
|
+
let a = vec2<f32>(extraFloat(entry + 1u), extraFloat(entry + 2u));
|
|
209
|
+
let b = vec2<f32>(extraFloat(entry + 3u), extraFloat(entry + 4u));
|
|
210
|
+
let c = vec2<f32>(extraFloat(entry + 5u), extraFloat(entry + 6u));
|
|
211
|
+
let p0 = extraFloat(entry + 7u);
|
|
212
|
+
let slope = vec2<f32>(extraFloat(entry + 8u), extraFloat(entry + 9u));
|
|
213
|
+
let tile = extraFloat(record);
|
|
214
|
+
let top = extraFloat(record + 1u);
|
|
215
|
+
// The ray's height over the plane, at its start and per unit of distance.
|
|
216
|
+
let h0 = origin.y - p0 - slope.x * origin.x - slope.y * origin.z;
|
|
217
|
+
let dh = direction.y - slope.x * direction.x - slope.y * direction.z;
|
|
218
|
+
let ground = origin.xz;
|
|
219
|
+
let along = direction.xz;
|
|
220
|
+
var range = vec2<f32>(RAY_EPSILON, (*best).distance);
|
|
221
|
+
|
|
222
|
+
range = grassClip(range, h0, dh);
|
|
223
|
+
range = grassClip(range, top - h0, -dh);
|
|
224
|
+
range = grassEdge(range, a, b, ground, along);
|
|
225
|
+
range = grassEdge(range, b, c, ground, along);
|
|
226
|
+
range = grassEdge(range, c, a, ground, along);
|
|
227
|
+
|
|
228
|
+
if (range.x >= range.y) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Amanatides and Woo over the tiles of the ground.
|
|
233
|
+
var cell = floor((ground + along * range.x) / tile);
|
|
234
|
+
let stepping = select(vec2<f32>(-1.0), vec2<f32>(1.0), along >= vec2<f32>(0.0));
|
|
235
|
+
let span = abs(vec2<f32>(tile) / select(along, vec2<f32>(1e-20), abs(along) < vec2<f32>(1e-20)));
|
|
236
|
+
var crossing = vec2<f32>(1e30);
|
|
237
|
+
|
|
238
|
+
if (abs(along.x) >= 1e-20) {
|
|
239
|
+
crossing.x = ((cell.x + select(0.0, 1.0, along.x > 0.0)) * tile - ground.x) / along.x;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (abs(along.y) >= 1e-20) {
|
|
243
|
+
crossing.y = ((cell.y + select(0.0, 1.0, along.y > 0.0)) * tile - ground.y) / along.y;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
var start = range.x;
|
|
247
|
+
let variants = textures[record + 2u];
|
|
248
|
+
|
|
249
|
+
for (var i = 0u; i < GRASS_MAX_TILES; i = i + 1u) {
|
|
250
|
+
let end = min(min(crossing.x, crossing.y), range.y);
|
|
251
|
+
let corner = cell * tile;
|
|
252
|
+
let variant = grassHash(vec2<i32>(cell)) % max(variants, 1u);
|
|
253
|
+
let patchOrigin = vec3<f32>(origin.x - corner.x, h0, origin.z - corner.y);
|
|
254
|
+
let patchDirection = vec3<f32>(direction.x, dh, direction.z);
|
|
255
|
+
|
|
256
|
+
if (end > start
|
|
257
|
+
&& tracePatch(record, textures[record + 5u + variant], corner, patchOrigin, patchDirection, start, end, best)) {
|
|
258
|
+
(*best).found = true;
|
|
259
|
+
(*best).record = record;
|
|
260
|
+
(*best).slope = slope;
|
|
261
|
+
(*best).tile = corner;
|
|
262
|
+
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (end >= range.y) {
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (crossing.x < crossing.y) {
|
|
271
|
+
cell.x = cell.x + stepping.x;
|
|
272
|
+
start = crossing.x;
|
|
273
|
+
crossing.x = crossing.x + span.x;
|
|
274
|
+
} else {
|
|
275
|
+
cell.y = cell.y + stepping.y;
|
|
276
|
+
start = crossing.y;
|
|
277
|
+
crossing.y = crossing.y + span.y;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** The closest blade along a ray before maxDistance, in any lawn of the scene. */
|
|
283
|
+
fn traceGrass(origin: vec3<f32>, direction: vec3<f32>, maxDistance: f32) -> GrassHit {
|
|
284
|
+
var best = noGrass(maxDistance);
|
|
285
|
+
|
|
286
|
+
if (frame.grassShells == GRASS_NONE) {
|
|
287
|
+
return best;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
let signs = select(vec3<f32>(-1.0), vec3<f32>(1.0), direction >= vec3<f32>(0.0));
|
|
291
|
+
let inverse = signs / max(abs(direction), vec3<f32>(1e-20));
|
|
292
|
+
var stack: array<u32, 32>;
|
|
293
|
+
var enters: array<f32, 32>;
|
|
294
|
+
var depth = 0u;
|
|
295
|
+
var current = 0u;
|
|
296
|
+
|
|
297
|
+
loop {
|
|
298
|
+
if (current == NO_NODE) {
|
|
299
|
+
if (depth == 0u) {
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
depth = depth - 1u;
|
|
304
|
+
|
|
305
|
+
if (enters[depth] > best.distance) {
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
current = stack[depth];
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
let node = grassNode(frame.grassShells + current * 16u);
|
|
313
|
+
let leftEnter = enterBox(node.leftLow, node.leftHigh, origin, inverse, best.distance);
|
|
314
|
+
let rightEnter = enterBox(node.rightLow, node.rightHigh, origin, inverse, best.distance);
|
|
315
|
+
let leftFirst = leftEnter <= rightEnter;
|
|
316
|
+
|
|
317
|
+
current = NO_NODE;
|
|
318
|
+
|
|
319
|
+
for (var k = 0u; k < 2u; k = k + 1u) {
|
|
320
|
+
// The nearer child first; a leaf is searched at once, a node is visited next or waits.
|
|
321
|
+
let left = (k == 0u) == leftFirst;
|
|
322
|
+
let enter = select(rightEnter, leftEnter, left);
|
|
323
|
+
let link = select(node.rightLink, node.leftLink, left);
|
|
324
|
+
let count = select(node.rightCount, node.leftCount, left);
|
|
325
|
+
|
|
326
|
+
if (enter >= best.distance) {
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (link < 0) {
|
|
331
|
+
let first = u32(-1 - link);
|
|
332
|
+
|
|
333
|
+
for (var i = 0u; i < u32(count); i = i + 1u) {
|
|
334
|
+
grassInShell(frame.grassEntries + (first + i) * 12u, origin, direction, &best);
|
|
335
|
+
}
|
|
336
|
+
} else if (current == NO_NODE) {
|
|
337
|
+
current = u32(link);
|
|
338
|
+
} else if (depth < 32u) {
|
|
339
|
+
stack[depth] = u32(link);
|
|
340
|
+
enters[depth] = enter;
|
|
341
|
+
depth = depth + 1u;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return best;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* A blade as a surface: thin, waxy and translucent, a quarter of its light let through to the far side. The colour runs
|
|
351
|
+
* from the root to the tip, changes from blade to blade and across the lawn, and some blades are dry.
|
|
352
|
+
*/
|
|
353
|
+
fn grassSurface(hit: GrassHit, direction: vec3<f32>) -> Surface {
|
|
354
|
+
let at = hit.triangle;
|
|
355
|
+
let record = hit.record;
|
|
356
|
+
let w = 1.0 - hit.u - hit.v;
|
|
357
|
+
let height = w * extraFloat(at + 9u) + hit.u * extraFloat(at + 10u) + hit.v * extraFloat(at + 11u);
|
|
358
|
+
let side = w * extraFloat(at + 12u) + hit.u * extraFloat(at + 13u) + hit.v * extraFloat(at + 14u);
|
|
359
|
+
let blade = textures[record + 4u] + textures[at + 15u] * 4u;
|
|
360
|
+
let root = hit.tile + vec2<f32>(extraFloat(blade), extraFloat(blade + 1u));
|
|
361
|
+
let rand = extraFloat(blade + 2u);
|
|
362
|
+
let yaw = extraFloat(blade + 3u);
|
|
363
|
+
let a = grassVec3(at);
|
|
364
|
+
let local = cross(grassVec3(at + 3u) - a, grassVec3(at + 6u) - a);
|
|
365
|
+
// Normals go back through the inverse transpose of the shear.
|
|
366
|
+
var normal = normalize(vec3<f32>(local.x - hit.slope.x * local.y, local.y, local.z - hit.slope.y * local.y));
|
|
367
|
+
|
|
368
|
+
if (dot(normal, direction) > 0.0) {
|
|
369
|
+
normal = -normal;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
let across = normalize(vec3<f32>(cos(yaw), hit.slope.x * cos(yaw) + hit.slope.y * sin(yaw), sin(yaw)));
|
|
373
|
+
// A blade is rounded across: its normal turns towards each edge.
|
|
374
|
+
var shading = normalize(normal + across * (side * 0.55));
|
|
375
|
+
|
|
376
|
+
if (dot(shading, direction) >= 0.0) {
|
|
377
|
+
shading = normal;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
let variation = extraFloat(record + 21u);
|
|
381
|
+
let scale = extraFloat(record + 23u);
|
|
382
|
+
let wide = grassNoise(root * scale);
|
|
383
|
+
let fine = grassNoise(root * scale * 4.3 + vec2<f32>(5.2, 1.7));
|
|
384
|
+
var albedo = mix(grassVec3(record + 9u), grassVec3(record + 12u), smoothstep(0.0, 1.0, pow(height, 0.75)));
|
|
385
|
+
|
|
386
|
+
// Across the lawn, greener and yellower stretches; from blade to blade, lighter and darker.
|
|
387
|
+
albedo = albedo * mix(vec3<f32>(1.0), vec3<f32>(1.12, 1.0, 0.7), variation * (wide - 0.4));
|
|
388
|
+
albedo = albedo * (1.0 + variation * 0.6 * (fract(rand * 13.37) - 0.5));
|
|
389
|
+
|
|
390
|
+
let dryness = extraFloat(record + 18u);
|
|
391
|
+
let dry = grassVec3(record + 15u) * (0.85 + 0.3 * fract(rand * 7.77));
|
|
392
|
+
|
|
393
|
+
if (fract(rand * 31.1) < dryness * (0.4 + 1.2 * fine)) {
|
|
394
|
+
albedo = mix(dry * 0.8, dry, height);
|
|
395
|
+
} else {
|
|
396
|
+
// Tips dry first.
|
|
397
|
+
albedo = mix(albedo, dry, smoothstep(0.82, 1.0, height) * dryness * 1.5);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// The sheath at the root is paler and duller.
|
|
401
|
+
albedo = mix(albedo * 0.7 + vec3<f32>(0.03, 0.03, 0.01), albedo, smoothstep(0.0, 0.2, height));
|
|
402
|
+
albedo = clamp(albedo, vec3<f32>(0.0), vec3<f32>(0.9));
|
|
403
|
+
|
|
404
|
+
let translucency = extraFloat(record + 19u);
|
|
405
|
+
var surface: Surface;
|
|
406
|
+
|
|
407
|
+
surface.material = 0u;
|
|
408
|
+
surface.flags = MATERIAL_LIT | MATERIAL_PBR;
|
|
409
|
+
surface.kind = KIND_STANDARD;
|
|
410
|
+
surface.blending = BLEND_NORMAL;
|
|
411
|
+
surface.facing = normal;
|
|
412
|
+
surface.front = true;
|
|
413
|
+
surface.normal = shading;
|
|
414
|
+
surface.albedo = albedo * (1.0 - 0.5 * translucency);
|
|
415
|
+
// Light through a leaf is more saturated and yellower than off it.
|
|
416
|
+
surface.translucent = min(albedo * translucency * vec3<f32>(1.05, 1.2, 0.6), vec3<f32>(0.9));
|
|
417
|
+
surface.opacity = 1.0;
|
|
418
|
+
surface.alpha = 1.0;
|
|
419
|
+
surface.roughness = clamp(extraFloat(record + 20u), 0.0525, 1.0);
|
|
420
|
+
surface.metalness = 0.0;
|
|
421
|
+
surface.ior = 1.5;
|
|
422
|
+
surface.specularColor = vec3<f32>(0.04);
|
|
423
|
+
surface.specularF90 = 1.0;
|
|
424
|
+
surface.specularStrength = 1.0;
|
|
425
|
+
surface.envMap = NO_TEXTURE;
|
|
426
|
+
surface.envMapIntensity = 1.0;
|
|
427
|
+
surface.gradientMap = NO_TEXTURE;
|
|
428
|
+
|
|
429
|
+
return surface;
|
|
430
|
+
}
|
|
431
|
+
`;
|
|
432
|
+
/** What the kernel calls when no material grows grass: the same names, doing nothing. */
|
|
433
|
+
export const GRASS_STUB_WGSL = /* wgsl */ `
|
|
434
|
+
const GRASS_NONE: u32 = 0xffffffffu;
|
|
435
|
+
const GRASS_TRIANGLE: u32 = 0xfffffffeu;
|
|
436
|
+
|
|
437
|
+
struct GrassHit {
|
|
438
|
+
found: bool,
|
|
439
|
+
distance: f32,
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
fn traceGrass(origin: vec3<f32>, direction: vec3<f32>, maxDistance: f32) -> GrassHit {
|
|
443
|
+
var hit: GrassHit;
|
|
444
|
+
hit.found = false;
|
|
445
|
+
hit.distance = maxDistance;
|
|
446
|
+
|
|
447
|
+
return hit;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
fn grassSurface(hit: GrassHit, direction: vec3<f32>) -> Surface {
|
|
451
|
+
var surface: Surface;
|
|
452
|
+
|
|
453
|
+
return surface;
|
|
454
|
+
}
|
|
455
|
+
`;
|
|
456
|
+
//# sourceMappingURL=grass.wgsl.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"grass.wgsl.js","sourceRoot":"","sources":["../../src/wgsl/grass.wgsl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,UAAU,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoapC,CAAC;AAEF,yFAAyF;AACzF,MAAM,CAAC,MAAM,eAAe,GAAG,UAAU,CAAC;;;;;;;;;;;;;;;;;;;;;;CAsBzC,CAAC"}
|
|
@@ -6,5 +6,5 @@
|
|
|
6
6
|
* The falloff, the spot cone and the rectangle's projected area follow `material.js` in the CPU renderer, so direct
|
|
7
7
|
* light from an analytic light agrees with it.
|
|
8
8
|
*/
|
|
9
|
-
export declare const LIGHTS_WGSL = "\nstruct LightSample {\n // Unit vector from the shaded point towards the light.\n direction: vec3<f32>,\n // How far an occlusion ray may travel before it is past the light.\n distance: f32,\n // What the light delivers, already divided by the density it was sampled with.\n value: vec3<f32>,\n // Solid angle density of this direction, for the power heuristic. Zero when no bounce ray could find this light,\n // which is every light that occupies no geometry, and then the light strategy keeps the whole contribution.\n misPdf: f32,\n valid: bool,\n occlude: bool,\n kind: u32,\n};\n\nfn noLight() -> LightSample {\n var sample: LightSample;\n sample.valid = false;\n\n return sample;\n}\n\n/** The light whose cumulative probability range contains u. */\nfn lightByProbability(u: f32) -> Light {\n var low = 0u;\n var high = frame.lightCount - 1u;\n\n loop {\n if (low >= high) {\n break;\n }\n\n let middle = (low + high) / 2u;\n\n // The probabilities packed tight in the texture buffer: the same floats as lights[].cdf, a word apart.\n if (extraFloat(frame.lightCdf + middle) > u) {\n high = middle;\n } else {\n low = middle + 1u;\n }\n }\n\n return lights[low];\n}\n\nconst TREE_NODE_WORDS: u32 = 16u;\nconst TREE_LEAF: u32 = 0x80000000u;\nconst TREE_ROOT_PARENT: u32 = 0xffffffffu;\nconst TREE_ENTRY_WORDS: u32 = 4u;\nconst NO_LIGHT: u32 = 0xffffffffu;\n\n/**\n * How much the lights under a node of the light tree can deliver to a point: their power over the square of the\n * distance, times how well their cone of directions and the receiving surface face each other, each widened by the\n * angle the node's box takes from the point (Conty Estevez and Kulla's importance). A point inside the box rules\n * nothing out. A zero normal is a receiver that takes light from everywhere, such as a point in a volume.\n */\nfn lightNodeImportance(tree: u32, node: u32, position: vec3<f32>, normal: vec3<f32>) -> f32 {\n let at = tree + node * TREE_NODE_WORDS;\n let low = extraVec4(at);\n let high = extraVec4(at + 4u);\n\n if (low.w <= 0.0) {\n return 0.0;\n }\n\n let centre = (low.xyz + high.xyz) * 0.5;\n let radius2 = dot(high.xyz - low.xyz, high.xyz - low.xyz) * 0.25;\n let offset = position - centre;\n let distance2 = dot(offset, offset);\n\n if (distance2 <= radius2) {\n return low.w / max(radius2, 1e-12);\n }\n\n let away = offset * inverseSqrt(distance2);\n // The angle the box takes from the point, as its sine and cosine: the angles below are widened by it without a\n // single inverse trigonometric function, by the sum and difference formulas.\n let sinU2 = radius2 / distance2;\n let cosU = sqrt(1.0 - sinU2);\n let sinU = sqrt(sinU2);\n var orientation = 1.0;\n\n // A cone of minus one is the whole sphere: such lights shine towards every point alike.\n if (high.w > -1.0) {\n let cone = extraVec4(at + 8u);\n let cosO = high.w;\n let sinO = sqrt(max(0.0, 1.0 - cosO * cosO));\n // The cone widened by the box: cos and sin of theta_o + theta_u, the whole sphere once that passes pi.\n let cosWide = cosO * cosU - sinO * sinU;\n let sinWide = sinO * cosU + cosO * sinU;\n let cosTheta = dot(cone.xyz, away);\n\n // cos(max(0, theta - theta_o - theta_u)), which is one while the point lies within the widened cone.\n if (sinWide >= 0.0 && cosTheta < cosWide) {\n let sinTheta = sqrt(max(0.0, 1.0 - cosTheta * cosTheta));\n let cosPast = cosTheta * cosWide + sinTheta * sinWide;\n\n orientation = select(0.0, cosPast, cosPast >= cone.w);\n }\n }\n\n var facing = 1.0;\n\n if (any(normal != vec3<f32>(0.0))) {\n let cosN = -dot(normal, away);\n\n // cos(max(0, theta_n - theta_u)), and nothing once that reaches a right angle.\n if (cosN < cosU) {\n let sinN = sqrt(max(0.0, 1.0 - cosN * cosN));\n\n facing = max(0.0, cosN * cosU + sinN * sinU);\n }\n }\n\n return low.w * orientation * facing / distance2;\n}\n\n/** The chance of stepping from a node of a tree (the lights' or the windows') to each of its children, from here. */\nfn lightChildOdds(tree: u32, left: u32, right: u32, position: vec3<f32>, normal: vec3<f32>) -> vec2<f32> {\n var importance = vec2<f32>(0.0);\n\n // One loop over the two children, so the importance is written into the kernel once per walk.\n for (var k = 0u; k < 2u; k = k + 1u) {\n importance[k] = lightNodeImportance(tree, select(right, left, k == 0u), position, normal);\n }\n\n let total = importance.x + importance.y;\n\n return select(importance / total, vec2<f32>(0.0), total <= 0.0);\n}\n\n/**\n * An entry picked down a tree by importance from this point, with the chance of picking it; NO_LIGHT for none. The\n * tree is the lights' or the windows', given by where its nodes and its leaves' list start.\n */\nfn sampleLightTree(tree: u32, list: u32, position: vec3<f32>, normal: vec3<f32>, u: f32, pdf: ptr<function, f32>,\n density: ptr<function, f32>) -> u32 {\n var node = 0u;\n var chance = 1.0;\n var x = u;\n\n for (var depth = 0u; depth < 64u; depth = depth + 1u) {\n let at = tree + node * TREE_NODE_WORDS;\n let first = textures[at + 12u];\n\n // A leaf lists its lights, too close together to tell apart from here: one is picked by its share of the power.\n if ((first & TREE_LEAF) != 0u) {\n let start = list + (first & ~TREE_LEAF) * TREE_ENTRY_WORDS;\n let count = textures[at + 13u];\n var slot = start + (count - 1u) * TREE_ENTRY_WORDS;\n\n for (var j = 0u; j + 1u < count; j = j + 1u) {\n if (x < extraFloat(start + j * TREE_ENTRY_WORDS + 3u)) {\n slot = start + j * TREE_ENTRY_WORDS;\n break;\n }\n }\n\n *pdf = chance * extraFloat(slot + 1u);\n *density = extraFloat(slot + 2u);\n\n return textures[slot];\n }\n\n let second = textures[at + 13u];\n let odds = lightChildOdds(tree, first, second, position, normal);\n\n if (odds.x + odds.y <= 0.0) {\n return NO_LIGHT;\n }\n\n if (x < odds.x) {\n node = first;\n chance = chance * odds.x;\n x = x / odds.x;\n } else {\n node = second;\n chance = chance * odds.y;\n x = (x - odds.x) / max(odds.y, 1e-20);\n }\n\n x = min(x, 0.99999994);\n }\n\n return NO_LIGHT;\n}\n\n/** The chance the tree would have picked the local light at this index from this point, walked up from its leaf. */\nfn lightTreePdf(index: u32, position: vec3<f32>, normal: vec3<f32>) -> f32 {\n var node = textures[frame.lightLeaves + index * 2u];\n let slot = frame.lightList + textures[frame.lightLeaves + index * 2u + 1u] * TREE_ENTRY_WORDS;\n // Its share within its leaf, and the area density of a point on it.\n var chance = extraFloat(slot + 1u) * extraFloat(slot + 2u);\n\n for (var depth = 0u; depth < 64u; depth = depth + 1u) {\n let parent = textures[frame.lightTree + node * TREE_NODE_WORDS + 14u];\n\n if (parent == TREE_ROOT_PARENT) {\n return chance;\n }\n\n let at = frame.lightTree + parent * TREE_NODE_WORDS;\n let first = textures[at + 12u];\n let odds = lightChildOdds(frame.lightTree, first, textures[at + 13u], position, normal);\n\n chance = chance * select(odds.y, odds.x, node == first);\n node = parent;\n }\n\n return 0.0;\n}\n\n/**\n * The area density with which direct sampling from this point would have picked a point on this triangle, or zero if\n * it never would. Only the leading entries are emissive triangles, and they are in triangle order, so one binary search\n * finds the entry; the tree, when there is one, says how likely it was from here.\n */\nfn lightPdfOfTriangle(triangle: u32, position: vec3<f32>, normal: vec3<f32>) -> f32 {\n var low = 0u;\n var high = frame.areaLightCount;\n\n loop {\n if (low >= high) {\n break;\n }\n\n let middle = (low + high) / 2u;\n let candidate = textures[frame.lightTriangles + middle];\n\n if (candidate == triangle) {\n if (frame.lightTree != 0u) {\n return frame.localLightPick * lightTreePdf(middle, position, normal);\n }\n\n return lights[middle].pdf;\n }\n\n if (candidate < triangle) {\n low = middle + 1u;\n } else {\n high = middle;\n }\n }\n\n return 0.0;\n}\n\n/** Share of the sky's samples aimed through the windows, from a point inside a room. */\nconst PORTAL_SHARE: f32 = 0.5;\n\n/**\n * How often the sky is aimed at through the windows from this point: from inside a room, where something stands above\n * it, half the time; outside never, where the open sky is better found by its brightness. A function of the point alone,\n * so the heuristic can ask it again when a bounce ray escapes.\n */\nfn portalChance(position: vec3<f32>) -> f32 {\n if (!PORTALS || frame.portalCount == 0u) {\n return 0.0;\n }\n\n return select(0.0, PORTAL_SHARE, traceAny(position, vec3<f32>(0.0, 1.0, 0.0), 1e30) != NO_HIT);\n}\n\n/** Window subtrees a ray's walk through the windows' tree keeps waiting. */\nconst PORTAL_STACK: u32 = 32u;\n\n/**\n * The solid angle density with which aiming through the windows produces this direction: over every window triangle\n * it crosses, the chance the tree picks that window from here times the area density of a point on it, over the area\n * it shows from here. The ray walks the tree as it would a hierarchy, by the nodes' boxes, carrying the chance of\n * reaching each node, so the windows it misses cost a box test and the ones it crosses no walk back up.\n */\nfn portalPdf(position: vec3<f32>, direction: vec3<f32>) -> f32 {\n var nodes: array<u32, PORTAL_STACK>;\n var chances: array<f32, PORTAL_STACK>;\n var top = 1u;\n let inverse = 1.0 / direction;\n var pdf = 0.0;\n\n nodes[0] = 0u;\n chances[0] = 1.0;\n\n loop {\n if (top == 0u) {\n break;\n }\n\n top = top - 1u;\n\n let node = nodes[top];\n let chance = chances[top];\n let at = frame.portals + node * TREE_NODE_WORDS;\n\n if (enterBox(extraVec4(at).xyz, extraVec4(at + 4u).xyz, position, inverse, 1e30) >= 1e30) {\n continue;\n }\n\n let first = textures[at + 12u];\n let second = textures[at + 13u];\n\n if ((first & TREE_LEAF) != 0u) {\n let start = frame.portalList + (first & ~TREE_LEAF) * TREE_ENTRY_WORDS;\n\n for (var j = 0u; j < second; j = j + 1u) {\n let slot = start + j * TREE_ENTRY_WORDS;\n let triangle = textures[slot];\n let t = intersectTriangle(leafOf(triangle), position, direction).x;\n\n if (t > 0.0) {\n let cosine = abs(dot(triangleNormal(triangle), direction));\n\n pdf = pdf + chance * extraFloat(slot + 1u) * extraFloat(slot + 2u) * t * t / max(cosine, 1e-6);\n }\n }\n\n continue;\n }\n\n let odds = lightChildOdds(frame.portals, first, second, position, vec3<f32>(0.0));\n\n for (var k = 0u; k < 2u; k = k + 1u) {\n if (odds[k] > 0.0 && top < PORTAL_STACK) {\n nodes[top] = select(second, first, k == 0u);\n chances[top] = chance * odds[k];\n top = top + 1u;\n }\n }\n }\n\n return pdf;\n}\n\n/** A direction through a point on a window the tree picks from here, or none from a point on the window itself. */\nfn samplePortal(position: vec3<f32>, u: vec2<f32>) -> vec3<f32> {\n var chance = 0.0;\n var density = 0.0;\n let triangle = sampleLightTree(frame.portals, frame.portalList, position, vec3<f32>(0.0), u.x, &chance, &density);\n\n if (triangle == NO_LIGHT) {\n return vec3<f32>(0.0);\n }\n\n // The tree spent the sample walking down; a fresh one places the point, which the pick does not depend on.\n let point = trianglePointAt(triangle, triangleBarycentric(vec2<f32>(fract(u.x * 4096.0), u.y)));\n let offset = point - position;\n\n return select(vec3<f32>(0.0), normalize(offset), dot(offset, offset) > 1e-12);\n}\n\n/** The density of the sky's sampling from this point for this direction: by brightness, through windows, or both. */\nfn skyPdf(position: vec3<f32>, direction: vec3<f32>, share: f32) -> f32 {\n var pdf = (1.0 - share) * environmentPdf(direction);\n\n if (share > 0.0) {\n pdf = pdf + share * portalPdf(position, direction);\n }\n\n return pdf;\n}\n\n/** The power heuristic: how much of a contribution the strategy that produced it keeps, against the other one. */\nfn misWeight(pdf: f32, other: f32) -> f32 {\n let a = pdf * pdf;\n let b = other * other;\n\n return select(a / (a + b), 0.0, a + b <= 0.0);\n}\n\n/**\n * Spreads a direction over a cone, which is how a light of some apparent size softens its own shadow. The settings give\n * the angular diameter; a rasterizer would blur the shadow map by the same amount.\n */\nfn spreadDirection(direction: vec3<f32>, u: vec2<f32>) -> vec3<f32> {\n if (frame.shadowSpread <= 0.0) {\n return direction;\n }\n\n let basis = orthonormalBasis(direction);\n let radius = sqrt(u.x) * frame.shadowSpread;\n let angle = 2.0 * PI * u.y;\n\n return normalize(direction + basis[0] * (radius * cos(angle)) + basis[1] * (radius * sin(angle)));\n}\n\n/** Three's getDistanceAttenuation: the inverse power law, cut off smoothly at the light's range. */\nfn distanceFalloff(light: Light, distance: f32) -> f32 {\n var falloff = 1.0 / max(pow(distance, light.decay), 0.01);\n\n if (light.cutoff > 0.0) {\n let edge = clamp(1.0 - pow(distance / light.cutoff, 4.0), 0.0, 1.0);\n\n falloff = falloff * edge * edge;\n }\n\n return falloff;\n}\n\n/** A point uniformly distributed over a rectangular light, and the axis it emits along. */\nfn rectanglePoint(light: Light, u: vec2<f32>) -> vec3<f32> {\n return light.position + light.tangent * (u.x * 2.0 - 1.0) + light.bitangent * (u.y * 2.0 - 1.0);\n}\n\n/**\n * Where the light that pick selects arrives from, and how much of it. u places the point on a surface light or spreads a\n * point light's shadow, so the same two dimensions serve every kind.\n */\nfn sampleLight(position: vec3<f32>, normal: vec3<f32>, pick: f32, u: vec2<f32>) -> LightSample {\n if (frame.lightCount == 0u) {\n return noLight();\n }\n\n // The lights in the scene go through the tree, by what they can deliver here; what lies outside it by its weight.\n if (frame.lightTree != 0u && pick < frame.localLightPick) {\n var chance = 0.0;\n var density = 1.0;\n let index = sampleLightTree(frame.lightTree, frame.lightList, position, normal, pick / frame.localLightPick, &chance, &density);\n\n if (index == NO_LIGHT || chance <= 0.0) {\n return noLight();\n }\n\n return sampleLightEntry(lights[index], frame.localLightPick * chance * density, position, u);\n }\n\n let light = lightByProbability(pick);\n\n return sampleLightEntry(light, light.pdf, position, u);\n}\n\n/** Three's spotLightMap: inside the projection's frustum the texture tints the light; outside it leaves it alone. */\nfn spotLightTint(light: Light, position: vec3<f32>) -> vec3<f32> {\n if ((light.flags & LIGHT_HAS_MAP) == 0u) {\n return vec3<f32>(1.0);\n }\n\n let at = light.triangle;\n let map = textures[at];\n let matrix = mat4x4<f32>(extraVec4(at + 1u), extraVec4(at + 5u), extraVec4(at + 9u), extraVec4(at + 13u));\n let coordinate = matrix * vec4<f32>(position, 1.0);\n\n if (coordinate.w <= 0.0) {\n return vec3<f32>(1.0);\n }\n\n let projected = coordinate.xyz / coordinate.w;\n\n if (any(projected <= vec3<f32>(0.0)) || any(projected >= vec3<f32>(1.0))) {\n return vec3<f32>(1.0);\n }\n\n return sampleTextureRaw(map, projected.xy).rgb;\n}\n\n/** pdf is the chance of picking this light, times the area density of a point on it where it has an area. */\nfn sampleLightEntry(light: Light, pdf: f32, position: vec3<f32>, u: vec2<f32>) -> LightSample {\n var sample: LightSample;\n sample.valid = true;\n sample.kind = light.kind;\n // Emissive geometry is always tested: a panel that lit the room through its own wall would not be a path tracer.\n sample.occlude = light.kind == LIGHT_AREA\n || ((light.flags & LIGHT_CASTS_SHADOW) != 0u && frame.shadowsEnabled != 0u);\n sample.misPdf = 0.0;\n\n if (light.kind == LIGHT_ENVIRONMENT) {\n // From inside a room, part of the time through a window: daylight comes in there or not at all.\n let share = portalChance(position);\n var direction = vec3<f32>(0.0);\n\n if (u.x < share) {\n direction = samplePortal(position, vec2<f32>(u.x / share, u.y));\n } else {\n let drawn = sampleEnvironmentDirection(vec2<f32>(min((u.x - share) / (1.0 - share), 0.99999994), u.y));\n\n if (drawn.pdf > 0.0) {\n direction = drawn.direction;\n }\n }\n\n let mixed = skyPdf(position, direction, share);\n\n if (all(direction == vec3<f32>(0.0)) || mixed <= 0.0) {\n return noLight();\n }\n\n let density = pdf * mixed;\n\n sample.direction = direction;\n sample.distance = 1e30;\n sample.value = environmentRadiance(direction) / density;\n // A bounce ray that escapes finds the same light, so the two strategies share it.\n sample.misPdf = density;\n\n return sample;\n }\n\n if (light.kind == LIGHT_DIRECTIONAL) {\n sample.direction = spreadDirection(light.direction, u);\n sample.distance = 1e30;\n // The colour already carries the intensity, which for a directional light is the irradiance it delivers.\n sample.value = light.color / pdf;\n\n return sample;\n }\n\n if (light.kind == LIGHT_AREA || light.kind == LIGHT_RECT) {\n let area = light.kind == LIGHT_AREA;\n let bary = triangleBarycentric(u);\n let point = select(rectanglePoint(light, u), trianglePointAt(light.triangle, bary), area);\n let axis = select(normalize(cross(light.tangent, light.bitangent)), triangleNormal(light.triangle), area);\n let offset = point - position;\n let distanceSquared = dot(offset, offset);\n let distance = sqrt(distanceSquared);\n\n if (distance <= RAY_EPSILON) {\n return noLight();\n }\n\n sample.direction = offset / distance;\n // An emissive triangle radiates from both sides in this milestone; a rectangular light only from the side it faces.\n let cosine = dot(axis, sample.direction);\n let cosLight = select(max(cosine, 0.0), abs(cosine), area);\n\n if (cosLight <= 1e-6) {\n return noLight();\n }\n\n // Stop just short of the light so its own surface is not reported as the blocker.\n sample.distance = distance * (1.0 - 1e-3);\n let density = pdf * distanceSquared / cosLight;\n // The emissive map is read at the very point that was sampled, so this agrees with what a bounce ray would find.\n let emission = select(light.color, emissionAt(light.triangle, bary), area);\n\n sample.value = emission / density;\n // Only a triangle of the scene's geometry can also be found by a bounce ray, so only it needs the heuristic.\n sample.misPdf = select(0.0, density, area);\n\n return sample;\n }\n\n // A point or a spot light: both fall off with distance and are sampled at their centre.\n let offset = light.position - position;\n let distance = length(offset);\n\n if (distance <= RAY_EPSILON) {\n return noLight();\n }\n\n let towards = offset / distance;\n var power = distanceFalloff(light, distance);\n\n if (light.kind == LIGHT_SPOT) {\n let cosine = dot(light.direction, towards);\n\n if (light.innerCos <= light.outerCos) {\n power = power * select(0.0, 1.0, cosine >= light.outerCos);\n } else {\n let t = clamp((cosine - light.outerCos) / (light.innerCos - light.outerCos), 0.0, 1.0);\n\n power = power * t * t * (3.0 - 2.0 * t);\n }\n\n if (power <= 0.0) {\n return noLight();\n }\n }\n\n sample.direction = spreadDirection(towards, u);\n sample.distance = distance;\n sample.value = light.color * (power / pdf);\n\n if (light.kind == LIGHT_SPOT) {\n sample.value = sample.value * spotLightTint(light, position);\n }\n\n return sample;\n}\n\n/**\n * Whether a material stops every shadow ray that meets it, wherever: opaque, no cutout, no transmission, no clipping\n * or wireframe, no shader that might discard, and normal blending. Only then can the first triangle found decide.\n */\nfn blocksAll(m: u32) -> bool {\n let flags = materials[m].flags;\n let blending = materials[m].blending;\n\n return (flags & (MATERIAL_TRANSPARENT | MATERIAL_ALPHA_HASH | MATERIAL_WIREFRAME | MATERIAL_VOLUME)) == 0u\n && materials[m].alphaTest <= 0.0\n && materials[m].transmission <= 0.0\n && materials[m].clipCount == 0u\n && materials[m].program == NO_PROGRAM\n && materials[m].kind != KIND_SHADOW\n && (blending == BLEND_NORMAL || blending == BLEND_NONE);\n}\n\n/** How many surfaces a shadow ray may pass through before it gives up and calls the light blocked. */\nconst MAX_SHADOW_SKIPS: u32 = 8u;\n\n/** No material to skip: every blocker counts. */\nconst NO_MATERIAL: u32 = 0xffffffffu;\n\n/**\n * What a shadow ray keeps of a thin transmissive wall it meets: the share Three lets through, tinted by the base colour,\n * less what the interface reflects. It is the expectation of the straight crossing in sampleDielectric, so a light\n * seen through a window is found the same by both estimators. A volume bends the ray it lets through, so it blocks.\n */\nfn thinCrossing(triangle: u32, bary: vec2<f32>, direction: vec3<f32>) -> vec3<f32> {\n let m = materialIndexOf(triangle);\n\n if (materials[m].transmission <= 0.0) {\n return vec3<f32>(0.0);\n }\n\n let metalness = materials[m].metalness * sampleMap(materials[m].metalnessMap, triangle, bary).b;\n let transmission = transmissionAt(m, triangle, bary, metalness);\n let thickness = materials[m].thickness * sampleMap(materials[m].thicknessMap, triangle, bary).g;\n\n if (transmission <= 0.0 || thickness > 0.0) {\n return vec3<f32>(0.0);\n }\n\n let albedo = materials[m].color.rgb * sampleMap(materials[m].map, triangle, bary).rgb;\n let reflectance = fresnelDielectric(abs(dot(authoredNormal(triangle, bary), direction)), 1.0 / max(materials[m].ior, 1.0));\n\n return albedo * (transmission * (1.0 - reflectance));\n}\n\n/** The colour a blended surface filters what is behind it by: its base colour, with the map and vertex colours. */\nfn blendColor(triangle: u32, bary: vec2<f32>) -> vec3<f32> {\n let m = materialIndexOf(triangle);\n var color = materials[m].color.rgb * sampleMap(materials[m].map, triangle, bary).rgb;\n\n if ((materials[m].flags & MATERIAL_VERTEX_COLORS) != 0u) {\n color = color * triangleColor(triangle, bary).rgb;\n }\n\n return color;\n}\n\n/**\n * How much of the light this sample came from reaches the shaded point. A surface that only partly covers its triangle\n * blocks the ray only that often, which is what makes a cutout leaf cast a cutout shadow; a thin transmissive wall\n * lets its own share through, tinted. A volume's boundary lets the ray in or out, and the medium inside takes its\n * share over the distance between; medium is the density at the shaded point itself, zero outside any volume.\n */\nfn lightTransmittance(position: vec3<f32>, chosen: LightSample, rng: ptr<function, PathSampler>, skipMaterial: u32,\n medium: f32) -> vec3<f32> {\n // A light that casts no shadow is blocked by nothing, but a medium on the way still takes its share: then the walk\n // below only looks for volumes and passes every other surface.\n if (!chosen.occlude && !VOLUMES) {\n return vec3<f32>(1.0);\n }\n\n if (chosen.occlude) {\n // Most blockers are plainly opaque, and for those any triangle on the way settles it: the search stops at the\n // first one found instead of looking for the closest. Anything that lets light through goes the long way below.\n let first = traceAny(position, chosen.direction, chosen.distance);\n\n if (first == NO_HIT) {\n return vec3<f32>(exp(-medium * chosen.distance));\n }\n\n let firstMaterial = materialIndexOf(first);\n\n if (firstMaterial != skipMaterial && blocksAll(firstMaterial)) {\n return vec3<f32>(0.0);\n }\n }\n\n var origin = position;\n var remaining = chosen.distance;\n var through = vec3<f32>(1.0);\n // The medium the ray is in, and how far along the ray it entered it.\n var density = medium;\n var travelled = 0.0;\n var enteredAt = 0.0;\n\n for (var skip = 0u; skip < MAX_SHADOW_SKIPS; skip = skip + 1u) {\n let blocker = traceScene(origin, chosen.direction, remaining);\n\n if (blocker.triangle == NO_HIT) {\n return through * exp(-density * (travelled + remaining - enteredAt));\n }\n\n let bary = vec2<f32>(blocker.u, blocker.v);\n let m = materialIndexOf(blocker.triangle);\n let blending = materials[m].blending;\n\n if (VOLUMES && (materials[m].flags & MATERIAL_VOLUME) != 0u) {\n let at = travelled + blocker.distance;\n\n if (dot(authoredNormal(blocker.triangle, bary), chosen.direction) < 0.0) {\n density = select(0.0, materials[m].volume.w, frame.volumesEnabled != 0u);\n enteredAt = at;\n } else {\n through = through * exp(-density * (at - enteredAt));\n density = 0.0;\n }\n } else if (chosen.occlude && m != skipMaterial && materials[m].kind != KIND_SHADOW && blending != BLEND_ADDITIVE) {\n let coverage = coverageAt(blocker.triangle, bary, chosen.direction);\n\n if (blending == BLEND_MULTIPLY) {\n through = through * mix(vec3<f32>(1.0), blendColor(blocker.triangle, bary), coverage);\n } else if (blending == BLEND_SUBTRACTIVE) {\n through = through * (vec3<f32>(1.0) - blendColor(blocker.triangle, bary) * coverage);\n } else if (coverage >= 1.0 || sample2D(rng).x < coverage) {\n through = through * thinCrossing(blocker.triangle, bary, chosen.direction);\n }\n\n if (all(through <= vec3<f32>(0.0))) {\n return vec3<f32>(0.0);\n }\n }\n\n // The ray carries on from just past the surface it saw through.\n origin = origin + chosen.direction * (blocker.distance + RAY_EPSILON);\n remaining = remaining - blocker.distance - RAY_EPSILON;\n travelled = travelled + blocker.distance + RAY_EPSILON;\n\n if (remaining <= RAY_EPSILON) {\n return through * exp(-density * (travelled - enteredAt));\n }\n }\n\n // Too many surfaces to see through: a shadow ray gives up dark, a ray that only counts the media keeps what it has.\n return select(vec3<f32>(0.0), through, !chosen.occlude);\n}\n\n/**\n * How shadowed a point is, as Three's ShadowMaterial reads it: for every light that casts a shadow, whether something\n * stands between it and the point, the darkest of them winning as in the CPU renderer. A rectangular light casts no\n * shadow in Three, so it takes no part.\n */\nfn shadowMaskAt(position: vec3<f32>, rng: ptr<function, PathSampler>) -> f32 {\n if (frame.shadowsEnabled == 0u) {\n return 0.0;\n }\n\n var shadow = 0.0;\n\n for (var i = frame.areaLightCount; i < frame.lightCount; i = i + 1u) {\n let light = lights[i];\n\n if (light.kind == LIGHT_ENVIRONMENT || light.kind == LIGHT_RECT || (light.flags & LIGHT_CASTS_SHADOW) == 0u) {\n continue;\n }\n\n let chosen = sampleLightEntry(light, light.pdf, position, sample2D(rng));\n\n if (!chosen.valid) {\n continue;\n }\n\n let visible = luminanceOf(lightTransmittance(position, chosen, rng, NO_MATERIAL, 0.0));\n\n shadow = max(shadow, 1.0 - clamp(visible, 0.0, 1.0));\n }\n\n return shadow;\n}\n";
|
|
9
|
+
export declare const LIGHTS_WGSL = "\nstruct LightSample {\n // Unit vector from the shaded point towards the light.\n direction: vec3<f32>,\n // How far an occlusion ray may travel before it is past the light.\n distance: f32,\n // What the light delivers, already divided by the density it was sampled with.\n value: vec3<f32>,\n // Solid angle density of this direction, for the power heuristic. Zero when no bounce ray could find this light,\n // which is every light that occupies no geometry, and then the light strategy keeps the whole contribution.\n misPdf: f32,\n valid: bool,\n occlude: bool,\n kind: u32,\n};\n\nfn noLight() -> LightSample {\n var sample: LightSample;\n sample.valid = false;\n\n return sample;\n}\n\n/** The light whose cumulative probability range contains u. */\nfn lightByProbability(u: f32) -> Light {\n var low = 0u;\n var high = frame.lightCount - 1u;\n\n loop {\n if (low >= high) {\n break;\n }\n\n let middle = (low + high) / 2u;\n\n // The probabilities packed tight in the texture buffer: the same floats as lights[].cdf, a word apart.\n if (extraFloat(frame.lightCdf + middle) > u) {\n high = middle;\n } else {\n low = middle + 1u;\n }\n }\n\n return lights[low];\n}\n\nconst TREE_NODE_WORDS: u32 = 16u;\nconst TREE_LEAF: u32 = 0x80000000u;\nconst TREE_ROOT_PARENT: u32 = 0xffffffffu;\nconst TREE_ENTRY_WORDS: u32 = 4u;\nconst NO_LIGHT: u32 = 0xffffffffu;\n\n/**\n * How much the lights under a node of the light tree can deliver to a point: their power over the square of the\n * distance, times how well their cone of directions and the receiving surface face each other, each widened by the\n * angle the node's box takes from the point (Conty Estevez and Kulla's importance). A point inside the box rules\n * nothing out. A zero normal is a receiver that takes light from everywhere, such as a point in a volume.\n */\nfn lightNodeImportance(tree: u32, node: u32, position: vec3<f32>, normal: vec3<f32>) -> f32 {\n let at = tree + node * TREE_NODE_WORDS;\n let low = extraVec4(at);\n let high = extraVec4(at + 4u);\n\n if (low.w <= 0.0) {\n return 0.0;\n }\n\n let centre = (low.xyz + high.xyz) * 0.5;\n let radius2 = dot(high.xyz - low.xyz, high.xyz - low.xyz) * 0.25;\n let offset = position - centre;\n let distance2 = dot(offset, offset);\n\n if (distance2 <= radius2) {\n return low.w / max(radius2, 1e-12);\n }\n\n let away = offset * inverseSqrt(distance2);\n // The angle the box takes from the point, as its sine and cosine: the angles below are widened by it without a\n // single inverse trigonometric function, by the sum and difference formulas.\n let sinU2 = radius2 / distance2;\n let cosU = sqrt(1.0 - sinU2);\n let sinU = sqrt(sinU2);\n var orientation = 1.0;\n\n // A cone of minus one is the whole sphere: such lights shine towards every point alike.\n if (high.w > -1.0) {\n let cone = extraVec4(at + 8u);\n let cosO = high.w;\n let sinO = sqrt(max(0.0, 1.0 - cosO * cosO));\n // The cone widened by the box: cos and sin of theta_o + theta_u, the whole sphere once that passes pi.\n let cosWide = cosO * cosU - sinO * sinU;\n let sinWide = sinO * cosU + cosO * sinU;\n let cosTheta = dot(cone.xyz, away);\n\n // cos(max(0, theta - theta_o - theta_u)), which is one while the point lies within the widened cone.\n if (sinWide >= 0.0 && cosTheta < cosWide) {\n let sinTheta = sqrt(max(0.0, 1.0 - cosTheta * cosTheta));\n let cosPast = cosTheta * cosWide + sinTheta * sinWide;\n\n orientation = select(0.0, cosPast, cosPast >= cone.w);\n }\n }\n\n var facing = 1.0;\n\n if (any(normal != vec3<f32>(0.0))) {\n let cosN = -dot(normal, away);\n\n // cos(max(0, theta_n - theta_u)), and nothing once that reaches a right angle.\n if (cosN < cosU) {\n let sinN = sqrt(max(0.0, 1.0 - cosN * cosN));\n\n facing = max(0.0, cosN * cosU + sinN * sinU);\n }\n }\n\n return low.w * orientation * facing / distance2;\n}\n\n/** The chance of stepping from a node of a tree (the lights' or the windows') to each of its children, from here. */\nfn lightChildOdds(tree: u32, left: u32, right: u32, position: vec3<f32>, normal: vec3<f32>) -> vec2<f32> {\n var importance = vec2<f32>(0.0);\n\n // One loop over the two children, so the importance is written into the kernel once per walk.\n for (var k = 0u; k < 2u; k = k + 1u) {\n importance[k] = lightNodeImportance(tree, select(right, left, k == 0u), position, normal);\n }\n\n let total = importance.x + importance.y;\n\n return select(importance / total, vec2<f32>(0.0), total <= 0.0);\n}\n\n/**\n * An entry picked down a tree by importance from this point, with the chance of picking it; NO_LIGHT for none. The\n * tree is the lights' or the windows', given by where its nodes and its leaves' list start.\n */\nfn sampleLightTree(tree: u32, list: u32, position: vec3<f32>, normal: vec3<f32>, u: f32, pdf: ptr<function, f32>,\n density: ptr<function, f32>) -> u32 {\n var node = 0u;\n var chance = 1.0;\n var x = u;\n\n for (var depth = 0u; depth < 64u; depth = depth + 1u) {\n let at = tree + node * TREE_NODE_WORDS;\n let first = textures[at + 12u];\n\n // A leaf lists its lights, too close together to tell apart from here: one is picked by its share of the power.\n if ((first & TREE_LEAF) != 0u) {\n let start = list + (first & ~TREE_LEAF) * TREE_ENTRY_WORDS;\n let count = textures[at + 13u];\n var slot = start + (count - 1u) * TREE_ENTRY_WORDS;\n\n for (var j = 0u; j + 1u < count; j = j + 1u) {\n if (x < extraFloat(start + j * TREE_ENTRY_WORDS + 3u)) {\n slot = start + j * TREE_ENTRY_WORDS;\n break;\n }\n }\n\n *pdf = chance * extraFloat(slot + 1u);\n *density = extraFloat(slot + 2u);\n\n return textures[slot];\n }\n\n let second = textures[at + 13u];\n let odds = lightChildOdds(tree, first, second, position, normal);\n\n if (odds.x + odds.y <= 0.0) {\n return NO_LIGHT;\n }\n\n if (x < odds.x) {\n node = first;\n chance = chance * odds.x;\n x = x / odds.x;\n } else {\n node = second;\n chance = chance * odds.y;\n x = (x - odds.x) / max(odds.y, 1e-20);\n }\n\n x = min(x, 0.99999994);\n }\n\n return NO_LIGHT;\n}\n\n/** The chance the tree would have picked the local light at this index from this point, walked up from its leaf. */\nfn lightTreePdf(index: u32, position: vec3<f32>, normal: vec3<f32>) -> f32 {\n var node = textures[frame.lightLeaves + index * 2u];\n let slot = frame.lightList + textures[frame.lightLeaves + index * 2u + 1u] * TREE_ENTRY_WORDS;\n // Its share within its leaf, and the area density of a point on it.\n var chance = extraFloat(slot + 1u) * extraFloat(slot + 2u);\n\n for (var depth = 0u; depth < 64u; depth = depth + 1u) {\n let parent = textures[frame.lightTree + node * TREE_NODE_WORDS + 14u];\n\n if (parent == TREE_ROOT_PARENT) {\n return chance;\n }\n\n let at = frame.lightTree + parent * TREE_NODE_WORDS;\n let first = textures[at + 12u];\n let odds = lightChildOdds(frame.lightTree, first, textures[at + 13u], position, normal);\n\n chance = chance * select(odds.y, odds.x, node == first);\n node = parent;\n }\n\n return 0.0;\n}\n\n/**\n * The area density with which direct sampling from this point would have picked a point on this triangle, or zero if\n * it never would. Only the leading entries are emissive triangles, and they are in triangle order, so one binary search\n * finds the entry; the tree, when there is one, says how likely it was from here.\n */\nfn lightPdfOfTriangle(triangle: u32, position: vec3<f32>, normal: vec3<f32>) -> f32 {\n var low = 0u;\n var high = frame.areaLightCount;\n\n loop {\n if (low >= high) {\n break;\n }\n\n let middle = (low + high) / 2u;\n let candidate = textures[frame.lightTriangles + middle];\n\n if (candidate == triangle) {\n if (frame.lightTree != 0u) {\n return frame.localLightPick * lightTreePdf(middle, position, normal);\n }\n\n return lights[middle].pdf;\n }\n\n if (candidate < triangle) {\n low = middle + 1u;\n } else {\n high = middle;\n }\n }\n\n return 0.0;\n}\n\n/** Share of the sky's samples aimed through the windows, from a point inside a room. */\nconst PORTAL_SHARE: f32 = 0.5;\n\n/**\n * How often the sky is aimed at through the windows from this point: from inside a room, where something stands above\n * it, half the time; outside never, where the open sky is better found by its brightness. A function of the point alone,\n * so the heuristic can ask it again when a bounce ray escapes.\n */\nfn portalChance(position: vec3<f32>) -> f32 {\n if (!PORTALS || frame.portalCount == 0u) {\n return 0.0;\n }\n\n return select(0.0, PORTAL_SHARE, traceAny(position, vec3<f32>(0.0, 1.0, 0.0), 1e30) != NO_HIT);\n}\n\n/** Window subtrees a ray's walk through the windows' tree keeps waiting. */\nconst PORTAL_STACK: u32 = 32u;\n\n/**\n * The solid angle density with which aiming through the windows produces this direction: over every window triangle\n * it crosses, the chance the tree picks that window from here times the area density of a point on it, over the area\n * it shows from here. The ray walks the tree as it would a hierarchy, by the nodes' boxes, carrying the chance of\n * reaching each node, so the windows it misses cost a box test and the ones it crosses no walk back up.\n */\nfn portalPdf(position: vec3<f32>, direction: vec3<f32>) -> f32 {\n var nodes: array<u32, PORTAL_STACK>;\n var chances: array<f32, PORTAL_STACK>;\n var top = 1u;\n let inverse = 1.0 / direction;\n var pdf = 0.0;\n\n nodes[0] = 0u;\n chances[0] = 1.0;\n\n loop {\n if (top == 0u) {\n break;\n }\n\n top = top - 1u;\n\n let node = nodes[top];\n let chance = chances[top];\n let at = frame.portals + node * TREE_NODE_WORDS;\n\n if (enterBox(extraVec4(at).xyz, extraVec4(at + 4u).xyz, position, inverse, 1e30) >= 1e30) {\n continue;\n }\n\n let first = textures[at + 12u];\n let second = textures[at + 13u];\n\n if ((first & TREE_LEAF) != 0u) {\n let start = frame.portalList + (first & ~TREE_LEAF) * TREE_ENTRY_WORDS;\n\n for (var j = 0u; j < second; j = j + 1u) {\n let slot = start + j * TREE_ENTRY_WORDS;\n let triangle = textures[slot];\n let t = intersectTriangle(leafOf(triangle), position, direction).x;\n\n if (t > 0.0) {\n let cosine = abs(dot(triangleNormal(triangle), direction));\n\n pdf = pdf + chance * extraFloat(slot + 1u) * extraFloat(slot + 2u) * t * t / max(cosine, 1e-6);\n }\n }\n\n continue;\n }\n\n let odds = lightChildOdds(frame.portals, first, second, position, vec3<f32>(0.0));\n\n for (var k = 0u; k < 2u; k = k + 1u) {\n if (odds[k] > 0.0 && top < PORTAL_STACK) {\n nodes[top] = select(second, first, k == 0u);\n chances[top] = chance * odds[k];\n top = top + 1u;\n }\n }\n }\n\n return pdf;\n}\n\n/** A direction through a point on a window the tree picks from here, or none from a point on the window itself. */\nfn samplePortal(position: vec3<f32>, u: vec2<f32>) -> vec3<f32> {\n var chance = 0.0;\n var density = 0.0;\n let triangle = sampleLightTree(frame.portals, frame.portalList, position, vec3<f32>(0.0), u.x, &chance, &density);\n\n if (triangle == NO_LIGHT) {\n return vec3<f32>(0.0);\n }\n\n // The tree spent the sample walking down; a fresh one places the point, which the pick does not depend on.\n let point = trianglePointAt(triangle, triangleBarycentric(vec2<f32>(fract(u.x * 4096.0), u.y)));\n let offset = point - position;\n\n return select(vec3<f32>(0.0), normalize(offset), dot(offset, offset) > 1e-12);\n}\n\n/** The density of the sky's sampling from this point for this direction: by brightness, through windows, or both. */\nfn skyPdf(position: vec3<f32>, direction: vec3<f32>, share: f32) -> f32 {\n var pdf = (1.0 - share) * environmentPdf(direction);\n\n if (share > 0.0) {\n pdf = pdf + share * portalPdf(position, direction);\n }\n\n return pdf;\n}\n\n/** The power heuristic: how much of a contribution the strategy that produced it keeps, against the other one. */\nfn misWeight(pdf: f32, other: f32) -> f32 {\n let a = pdf * pdf;\n let b = other * other;\n\n return select(a / (a + b), 0.0, a + b <= 0.0);\n}\n\n/**\n * Spreads a direction over a cone, which is how a light of some apparent size softens its own shadow. The settings give\n * the angular diameter; a rasterizer would blur the shadow map by the same amount.\n */\nfn spreadDirection(direction: vec3<f32>, u: vec2<f32>) -> vec3<f32> {\n if (frame.shadowSpread <= 0.0) {\n return direction;\n }\n\n let basis = orthonormalBasis(direction);\n let radius = sqrt(u.x) * frame.shadowSpread;\n let angle = 2.0 * PI * u.y;\n\n return normalize(direction + basis[0] * (radius * cos(angle)) + basis[1] * (radius * sin(angle)));\n}\n\n/** Three's getDistanceAttenuation: the inverse power law, cut off smoothly at the light's range. */\nfn distanceFalloff(light: Light, distance: f32) -> f32 {\n var falloff = 1.0 / max(pow(distance, light.decay), 0.01);\n\n if (light.cutoff > 0.0) {\n let edge = clamp(1.0 - pow(distance / light.cutoff, 4.0), 0.0, 1.0);\n\n falloff = falloff * edge * edge;\n }\n\n return falloff;\n}\n\n/** A point uniformly distributed over a rectangular light, and the axis it emits along. */\nfn rectanglePoint(light: Light, u: vec2<f32>) -> vec3<f32> {\n return light.position + light.tangent * (u.x * 2.0 - 1.0) + light.bitangent * (u.y * 2.0 - 1.0);\n}\n\n/**\n * Where the light that pick selects arrives from, and how much of it. u places the point on a surface light or spreads a\n * point light's shadow, so the same two dimensions serve every kind.\n */\nfn sampleLight(position: vec3<f32>, normal: vec3<f32>, pick: f32, u: vec2<f32>) -> LightSample {\n if (frame.lightCount == 0u) {\n return noLight();\n }\n\n // The lights in the scene go through the tree, by what they can deliver here; what lies outside it by its weight.\n if (frame.lightTree != 0u && pick < frame.localLightPick) {\n var chance = 0.0;\n var density = 1.0;\n let index = sampleLightTree(frame.lightTree, frame.lightList, position, normal, pick / frame.localLightPick, &chance, &density);\n\n if (index == NO_LIGHT || chance <= 0.0) {\n return noLight();\n }\n\n return sampleLightEntry(lights[index], frame.localLightPick * chance * density, position, u);\n }\n\n let light = lightByProbability(pick);\n\n return sampleLightEntry(light, light.pdf, position, u);\n}\n\n/** Three's spotLightMap: inside the projection's frustum the texture tints the light; outside it leaves it alone. */\nfn spotLightTint(light: Light, position: vec3<f32>) -> vec3<f32> {\n if ((light.flags & LIGHT_HAS_MAP) == 0u) {\n return vec3<f32>(1.0);\n }\n\n let at = light.triangle;\n let map = textures[at];\n let matrix = mat4x4<f32>(extraVec4(at + 1u), extraVec4(at + 5u), extraVec4(at + 9u), extraVec4(at + 13u));\n let coordinate = matrix * vec4<f32>(position, 1.0);\n\n if (coordinate.w <= 0.0) {\n return vec3<f32>(1.0);\n }\n\n let projected = coordinate.xyz / coordinate.w;\n\n if (any(projected <= vec3<f32>(0.0)) || any(projected >= vec3<f32>(1.0))) {\n return vec3<f32>(1.0);\n }\n\n return sampleTextureRaw(map, projected.xy).rgb;\n}\n\n/** pdf is the chance of picking this light, times the area density of a point on it where it has an area. */\nfn sampleLightEntry(light: Light, pdf: f32, position: vec3<f32>, u: vec2<f32>) -> LightSample {\n var sample: LightSample;\n sample.valid = true;\n sample.kind = light.kind;\n // Emissive geometry is always tested: a panel that lit the room through its own wall would not be a path tracer.\n sample.occlude = light.kind == LIGHT_AREA\n || ((light.flags & LIGHT_CASTS_SHADOW) != 0u && frame.shadowsEnabled != 0u);\n sample.misPdf = 0.0;\n\n if (light.kind == LIGHT_ENVIRONMENT) {\n // From inside a room, part of the time through a window: daylight comes in there or not at all.\n let share = portalChance(position);\n var direction = vec3<f32>(0.0);\n\n if (u.x < share) {\n direction = samplePortal(position, vec2<f32>(u.x / share, u.y));\n } else {\n let drawn = sampleEnvironmentDirection(vec2<f32>(min((u.x - share) / (1.0 - share), 0.99999994), u.y));\n\n if (drawn.pdf > 0.0) {\n direction = drawn.direction;\n }\n }\n\n let mixed = skyPdf(position, direction, share);\n\n if (all(direction == vec3<f32>(0.0)) || mixed <= 0.0) {\n return noLight();\n }\n\n let density = pdf * mixed;\n\n sample.direction = direction;\n sample.distance = 1e30;\n sample.value = environmentRadiance(direction) / density;\n // A bounce ray that escapes finds the same light, so the two strategies share it.\n sample.misPdf = density;\n\n return sample;\n }\n\n if (light.kind == LIGHT_DIRECTIONAL) {\n sample.direction = spreadDirection(light.direction, u);\n sample.distance = 1e30;\n // The colour already carries the intensity, which for a directional light is the irradiance it delivers.\n sample.value = light.color / pdf;\n\n return sample;\n }\n\n if (light.kind == LIGHT_AREA || light.kind == LIGHT_RECT) {\n let area = light.kind == LIGHT_AREA;\n let bary = triangleBarycentric(u);\n let point = select(rectanglePoint(light, u), trianglePointAt(light.triangle, bary), area);\n let axis = select(normalize(cross(light.tangent, light.bitangent)), triangleNormal(light.triangle), area);\n let offset = point - position;\n let distanceSquared = dot(offset, offset);\n let distance = sqrt(distanceSquared);\n\n if (distance <= RAY_EPSILON) {\n return noLight();\n }\n\n sample.direction = offset / distance;\n // An emissive triangle radiates from both sides in this milestone; a rectangular light only from the side it faces.\n let cosine = dot(axis, sample.direction);\n let cosLight = select(max(cosine, 0.0), abs(cosine), area);\n\n if (cosLight <= 1e-6) {\n return noLight();\n }\n\n // Stop just short of the light so its own surface is not reported as the blocker.\n sample.distance = distance * (1.0 - 1e-3);\n let density = pdf * distanceSquared / cosLight;\n // The emissive map is read at the very point that was sampled, so this agrees with what a bounce ray would find.\n let emission = select(light.color, emissionAt(light.triangle, bary), area);\n\n sample.value = emission / density;\n // Only a triangle of the scene's geometry can also be found by a bounce ray, so only it needs the heuristic.\n sample.misPdf = select(0.0, density, area);\n\n return sample;\n }\n\n // A point or a spot light: both fall off with distance and are sampled at their centre.\n let offset = light.position - position;\n let distance = length(offset);\n\n if (distance <= RAY_EPSILON) {\n return noLight();\n }\n\n let towards = offset / distance;\n var power = distanceFalloff(light, distance);\n\n if (light.kind == LIGHT_SPOT) {\n let cosine = dot(light.direction, towards);\n\n if (light.innerCos <= light.outerCos) {\n power = power * select(0.0, 1.0, cosine >= light.outerCos);\n } else {\n let t = clamp((cosine - light.outerCos) / (light.innerCos - light.outerCos), 0.0, 1.0);\n\n power = power * t * t * (3.0 - 2.0 * t);\n }\n\n if (power <= 0.0) {\n return noLight();\n }\n }\n\n sample.direction = spreadDirection(towards, u);\n sample.distance = distance;\n sample.value = light.color * (power / pdf);\n\n if (light.kind == LIGHT_SPOT) {\n sample.value = sample.value * spotLightTint(light, position);\n }\n\n return sample;\n}\n\n/**\n * Whether a material stops every shadow ray that meets it, wherever: opaque, no cutout, no transmission, no clipping\n * or wireframe, no shader that might discard, and normal blending. Only then can the first triangle found decide.\n */\nfn blocksAll(m: u32) -> bool {\n let flags = materials[m].flags;\n let blending = materials[m].blending;\n\n return (flags & (MATERIAL_TRANSPARENT | MATERIAL_ALPHA_HASH | MATERIAL_WIREFRAME | MATERIAL_VOLUME)) == 0u\n && materials[m].alphaTest <= 0.0\n && materials[m].transmission <= 0.0\n && materials[m].clipCount == 0u\n && materials[m].program == NO_PROGRAM\n && materials[m].kind != KIND_SHADOW\n && (blending == BLEND_NORMAL || blending == BLEND_NONE);\n}\n\n/** How many surfaces a shadow ray may pass through before it gives up and calls the light blocked. */\nconst MAX_SHADOW_SKIPS: u32 = 8u;\n\n/** No material to skip: every blocker counts. */\nconst NO_MATERIAL: u32 = 0xffffffffu;\n\n/**\n * What a shadow ray keeps of a thin transmissive wall it meets: the share Three lets through, tinted by the base colour,\n * less what the interface reflects. It is the expectation of the straight crossing in sampleDielectric, so a light\n * seen through a window is found the same by both estimators. A volume bends the ray it lets through, so it blocks.\n */\nfn thinCrossing(triangle: u32, bary: vec2<f32>, direction: vec3<f32>) -> vec3<f32> {\n let m = materialIndexOf(triangle);\n\n if (materials[m].transmission <= 0.0) {\n return vec3<f32>(0.0);\n }\n\n let metalness = materials[m].metalness * sampleMap(materials[m].metalnessMap, triangle, bary).b;\n let transmission = transmissionAt(m, triangle, bary, metalness);\n let thickness = materials[m].thickness * sampleMap(materials[m].thicknessMap, triangle, bary).g;\n\n if (transmission <= 0.0 || thickness > 0.0) {\n return vec3<f32>(0.0);\n }\n\n let albedo = materials[m].color.rgb * sampleMap(materials[m].map, triangle, bary).rgb;\n let reflectance = fresnelDielectric(abs(dot(authoredNormal(triangle, bary), direction)), 1.0 / max(materials[m].ior, 1.0));\n\n return albedo * (transmission * (1.0 - reflectance));\n}\n\n/** The colour a blended surface filters what is behind it by: its base colour, with the map and vertex colours. */\nfn blendColor(triangle: u32, bary: vec2<f32>) -> vec3<f32> {\n let m = materialIndexOf(triangle);\n var color = materials[m].color.rgb * sampleMap(materials[m].map, triangle, bary).rgb;\n\n if ((materials[m].flags & MATERIAL_VERTEX_COLORS) != 0u) {\n color = color * triangleColor(triangle, bary).rgb;\n }\n\n return color;\n}\n\n/**\n * How much of the light this sample came from reaches the shaded point. A surface that only partly covers its triangle\n * blocks the ray only that often, which is what makes a cutout leaf cast a cutout shadow; a thin transmissive wall\n * lets its own share through, tinted. A volume's boundary lets the ray in or out, and the medium inside takes its\n * share over the distance between; medium is the density at the shaded point itself, zero outside any volume.\n */\nfn lightTransmittance(position: vec3<f32>, chosen: LightSample, rng: ptr<function, PathSampler>, skipMaterial: u32,\n medium: f32) -> vec3<f32> {\n // A light that casts no shadow is blocked by nothing, but a medium on the way still takes its share: then the walk\n // below only looks for volumes and passes every other surface.\n if (!chosen.occlude && !VOLUMES) {\n return vec3<f32>(1.0);\n }\n\n // A blade of grass on the way blocks the light.\n if (GRASS && chosen.occlude && traceGrass(position, chosen.direction, chosen.distance).found) {\n return vec3<f32>(0.0);\n }\n\n if (chosen.occlude) {\n // Most blockers are plainly opaque, and for those any triangle on the way settles it: the search stops at the\n // first one found instead of looking for the closest. Anything that lets light through goes the long way below.\n let first = traceAny(position, chosen.direction, chosen.distance);\n\n if (first == NO_HIT) {\n return vec3<f32>(exp(-medium * chosen.distance));\n }\n\n let firstMaterial = materialIndexOf(first);\n\n if (firstMaterial != skipMaterial && blocksAll(firstMaterial)) {\n return vec3<f32>(0.0);\n }\n }\n\n var origin = position;\n var remaining = chosen.distance;\n var through = vec3<f32>(1.0);\n // The medium the ray is in, and how far along the ray it entered it.\n var density = medium;\n var travelled = 0.0;\n var enteredAt = 0.0;\n\n for (var skip = 0u; skip < MAX_SHADOW_SKIPS; skip = skip + 1u) {\n let blocker = traceScene(origin, chosen.direction, remaining);\n\n if (blocker.triangle == NO_HIT) {\n return through * exp(-density * (travelled + remaining - enteredAt));\n }\n\n let bary = vec2<f32>(blocker.u, blocker.v);\n let m = materialIndexOf(blocker.triangle);\n let blending = materials[m].blending;\n\n if (VOLUMES && (materials[m].flags & MATERIAL_VOLUME) != 0u) {\n let at = travelled + blocker.distance;\n\n if (dot(authoredNormal(blocker.triangle, bary), chosen.direction) < 0.0) {\n density = select(0.0, materials[m].volume.w, frame.volumesEnabled != 0u);\n enteredAt = at;\n } else {\n through = through * exp(-density * (at - enteredAt));\n density = 0.0;\n }\n } else if (chosen.occlude && m != skipMaterial && materials[m].kind != KIND_SHADOW && blending != BLEND_ADDITIVE) {\n let coverage = coverageAt(blocker.triangle, bary, chosen.direction);\n\n if (blending == BLEND_MULTIPLY) {\n through = through * mix(vec3<f32>(1.0), blendColor(blocker.triangle, bary), coverage);\n } else if (blending == BLEND_SUBTRACTIVE) {\n through = through * (vec3<f32>(1.0) - blendColor(blocker.triangle, bary) * coverage);\n } else if (coverage >= 1.0 || sample2D(rng).x < coverage) {\n through = through * thinCrossing(blocker.triangle, bary, chosen.direction);\n }\n\n if (all(through <= vec3<f32>(0.0))) {\n return vec3<f32>(0.0);\n }\n }\n\n // The ray carries on from just past the surface it saw through.\n origin = origin + chosen.direction * (blocker.distance + RAY_EPSILON);\n remaining = remaining - blocker.distance - RAY_EPSILON;\n travelled = travelled + blocker.distance + RAY_EPSILON;\n\n if (remaining <= RAY_EPSILON) {\n return through * exp(-density * (travelled - enteredAt));\n }\n }\n\n // Too many surfaces to see through: a shadow ray gives up dark, a ray that only counts the media keeps what it has.\n return select(vec3<f32>(0.0), through, !chosen.occlude);\n}\n\n/**\n * How shadowed a point is, as Three's ShadowMaterial reads it: for every light that casts a shadow, whether something\n * stands between it and the point, the darkest of them winning as in the CPU renderer. A rectangular light casts no\n * shadow in Three, so it takes no part.\n */\nfn shadowMaskAt(position: vec3<f32>, rng: ptr<function, PathSampler>) -> f32 {\n if (frame.shadowsEnabled == 0u) {\n return 0.0;\n }\n\n var shadow = 0.0;\n\n for (var i = frame.areaLightCount; i < frame.lightCount; i = i + 1u) {\n let light = lights[i];\n\n if (light.kind == LIGHT_ENVIRONMENT || light.kind == LIGHT_RECT || (light.flags & LIGHT_CASTS_SHADOW) == 0u) {\n continue;\n }\n\n let chosen = sampleLightEntry(light, light.pdf, position, sample2D(rng));\n\n if (!chosen.valid) {\n continue;\n }\n\n let visible = luminanceOf(lightTransmittance(position, chosen, rng, NO_MATERIAL, 0.0));\n\n shadow = max(shadow, 1.0 - clamp(visible, 0.0, 1.0));\n }\n\n return shadow;\n}\n";
|
|
10
10
|
//# sourceMappingURL=lights.wgsl.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lights.wgsl.d.ts","sourceRoot":"","sources":["../../src/wgsl/lights.wgsl.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,eAAO,MAAM,WAAW,
|
|
1
|
+
{"version":3,"file":"lights.wgsl.d.ts","sourceRoot":"","sources":["../../src/wgsl/lights.wgsl.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,eAAO,MAAM,WAAW,u91BA8vBvB,CAAC"}
|
package/dist/wgsl/lights.wgsl.js
CHANGED
|
@@ -660,6 +660,11 @@ fn lightTransmittance(position: vec3<f32>, chosen: LightSample, rng: ptr<functio
|
|
|
660
660
|
return vec3<f32>(1.0);
|
|
661
661
|
}
|
|
662
662
|
|
|
663
|
+
// A blade of grass on the way blocks the light.
|
|
664
|
+
if (GRASS && chosen.occlude && traceGrass(position, chosen.direction, chosen.distance).found) {
|
|
665
|
+
return vec3<f32>(0.0);
|
|
666
|
+
}
|
|
667
|
+
|
|
663
668
|
if (chosen.occlude) {
|
|
664
669
|
// Most blockers are plainly opaque, and for those any triangle on the way settles it: the search stops at the
|
|
665
670
|
// first one found instead of looking for the closest. Anything that lets light through goes the long way below.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lights.wgsl.js","sourceRoot":"","sources":["../../src/wgsl/lights.wgsl.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,UAAU,CAAC
|
|
1
|
+
{"version":3,"file":"lights.wgsl.js","sourceRoot":"","sources":["../../src/wgsl/lights.wgsl.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,UAAU,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8vBrC,CAAC"}
|