@tinypoly/gpu-renderer 0.2.4 → 0.3.0

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.
Files changed (111) hide show
  1. package/ARCHITECTURE.md +48 -10
  2. package/README.md +2 -0
  3. package/dist/accumulator.d.ts.map +1 -1
  4. package/dist/accumulator.js +1 -0
  5. package/dist/accumulator.js.map +1 -1
  6. package/dist/denoise.js +2 -2
  7. package/dist/denoise.js.map +1 -1
  8. package/dist/device.d.ts.map +1 -1
  9. package/dist/device.js +9 -0
  10. package/dist/device.js.map +1 -1
  11. package/dist/frameLayout.d.ts.map +1 -1
  12. package/dist/frameLayout.js +5 -0
  13. package/dist/frameLayout.js.map +1 -1
  14. package/dist/geometry.d.ts +48 -8
  15. package/dist/geometry.d.ts.map +1 -1
  16. package/dist/geometry.js +409 -134
  17. package/dist/geometry.js.map +1 -1
  18. package/dist/grass.d.ts.map +1 -1
  19. package/dist/grass.js +2 -1
  20. package/dist/grass.js.map +1 -1
  21. package/dist/imageResolve.js +2 -2
  22. package/dist/imageResolve.js.map +1 -1
  23. package/dist/index.d.ts +1 -0
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +1 -0
  26. package/dist/index.js.map +1 -1
  27. package/dist/instancing.d.ts +100 -0
  28. package/dist/instancing.d.ts.map +1 -0
  29. package/dist/instancing.js +286 -0
  30. package/dist/instancing.js.map +1 -0
  31. package/dist/lensPass.d.ts +2 -0
  32. package/dist/lensPass.d.ts.map +1 -1
  33. package/dist/lensPass.js +7 -4
  34. package/dist/lensPass.js.map +1 -1
  35. package/dist/lights.d.ts.map +1 -1
  36. package/dist/lights.js +8 -5
  37. package/dist/lights.js.map +1 -1
  38. package/dist/memoryProbe.d.ts +76 -0
  39. package/dist/memoryProbe.d.ts.map +1 -0
  40. package/dist/memoryProbe.js +197 -0
  41. package/dist/memoryProbe.js.map +1 -0
  42. package/dist/oidnPass.d.ts +6 -0
  43. package/dist/oidnPass.d.ts.map +1 -1
  44. package/dist/oidnPass.js +18 -1
  45. package/dist/oidnPass.js.map +1 -1
  46. package/dist/pathTracePass.d.ts +15 -2
  47. package/dist/pathTracePass.d.ts.map +1 -1
  48. package/dist/pathTracePass.js +38 -16
  49. package/dist/pathTracePass.js.map +1 -1
  50. package/dist/renderer.d.ts +13 -0
  51. package/dist/renderer.d.ts.map +1 -1
  52. package/dist/renderer.js +248 -41
  53. package/dist/renderer.js.map +1 -1
  54. package/dist/sceneUpload.d.ts +26 -8
  55. package/dist/sceneUpload.d.ts.map +1 -1
  56. package/dist/sceneUpload.js +97 -27
  57. package/dist/sceneUpload.js.map +1 -1
  58. package/dist/shaderModule.d.ts.map +1 -1
  59. package/dist/shaderModule.js +2 -1
  60. package/dist/shaderModule.js.map +1 -1
  61. package/dist/skyBake.d.ts.map +1 -1
  62. package/dist/skyBake.js +5 -3
  63. package/dist/skyBake.js.map +1 -1
  64. package/dist/texturePack.d.ts +43 -3
  65. package/dist/texturePack.d.ts.map +1 -1
  66. package/dist/texturePack.js +209 -37
  67. package/dist/texturePack.js.map +1 -1
  68. package/dist/wgsl/bvh.wgsl.d.ts +5 -1
  69. package/dist/wgsl/bvh.wgsl.d.ts.map +1 -1
  70. package/dist/wgsl/bvh.wgsl.js +105 -15
  71. package/dist/wgsl/bvh.wgsl.js.map +1 -1
  72. package/dist/wgsl/common.wgsl.d.ts.map +1 -1
  73. package/dist/wgsl/common.wgsl.js +92 -11
  74. package/dist/wgsl/common.wgsl.js.map +1 -1
  75. package/dist/wgsl/lens.wgsl.d.ts +1 -1
  76. package/dist/wgsl/lens.wgsl.d.ts.map +1 -1
  77. package/dist/wgsl/lens.wgsl.js +14 -2
  78. package/dist/wgsl/lens.wgsl.js.map +1 -1
  79. package/dist/wgsl/surface.wgsl.d.ts +1 -1
  80. package/dist/wgsl/surface.wgsl.d.ts.map +1 -1
  81. package/dist/wgsl/surface.wgsl.js +24 -14
  82. package/dist/wgsl/surface.wgsl.js.map +1 -1
  83. package/dist/wgsl/texture.wgsl.d.ts +1 -1
  84. package/dist/wgsl/texture.wgsl.d.ts.map +1 -1
  85. package/dist/wgsl/texture.wgsl.js +11 -0
  86. package/dist/wgsl/texture.wgsl.js.map +1 -1
  87. package/package.json +1 -1
  88. package/src/accumulator.ts +1 -0
  89. package/src/denoise.ts +2 -2
  90. package/src/device.ts +12 -0
  91. package/src/frameLayout.ts +5 -0
  92. package/src/geometry.ts +527 -133
  93. package/src/grass.ts +2 -1
  94. package/src/imageResolve.ts +2 -2
  95. package/src/index.ts +1 -0
  96. package/src/instancing.ts +415 -0
  97. package/src/lensPass.ts +9 -4
  98. package/src/lights.ts +9 -5
  99. package/src/memoryProbe.ts +275 -0
  100. package/src/oidnPass.ts +21 -1
  101. package/src/pathTracePass.ts +42 -18
  102. package/src/renderer.ts +308 -47
  103. package/src/sceneUpload.ts +139 -36
  104. package/src/shaderModule.ts +2 -1
  105. package/src/skyBake.ts +5 -3
  106. package/src/texturePack.ts +274 -43
  107. package/src/wgsl/bvh.wgsl.ts +105 -15
  108. package/src/wgsl/common.wgsl.ts +92 -11
  109. package/src/wgsl/lens.wgsl.ts +14 -2
  110. package/src/wgsl/surface.wgsl.ts +24 -14
  111. package/src/wgsl/texture.wgsl.ts +11 -0
@@ -4,6 +4,10 @@
4
4
  * The positions are uploaded in the hierarchy's leaf order, so a leaf is a run of triangles read straight off the
5
5
  * buffer; the scene's own triangle number is looked up once, for the hit a trace returns. Every other reader goes the
6
6
  * other way, from a triangle number to where its corners sit.
7
+ *
8
+ * With INSTANCES compiled in, a leaf with a count of -1 is an instance: the ray is taken into its object space and
9
+ * the traversal goes on in its prototype's hierarchy, with a marker on the stack that brings it back to world space.
10
+ * The distance along the ray is the same in both spaces, since the direction is transformed and not normalized.
7
11
  */
8
12
  export const BVH_WGSL = /* wgsl */ `
9
13
  /** A corner of the triangle at this position of the hierarchy's leaf order. */
@@ -18,8 +22,37 @@ fn leafOf(triangle: u32) -> u32 {
18
22
  return indices[frame.leafOffset + triangle];
19
23
  }
20
24
 
25
+ /** A corner of a triangle a hit named, in world space. */
21
26
  fn triangleVertex(triangle: u32, corner: u32) -> vec3<f32> {
22
- return leafVertex(leafOf(triangle), corner);
27
+ let instance = instanceOf(triangle);
28
+ let p = leafVertex(leafOf(storedTriangle(triangle, instance)), corner);
29
+
30
+ if (instance != NO_INSTANCE) {
31
+ return instancePoint(instance, p);
32
+ }
33
+
34
+ return p;
35
+ }
36
+
37
+ /** The stack entry that leaves an instance: the traversal goes back to world space. */
38
+ const RETURN_ENTRY: u32 = 0xffffffffu;
39
+
40
+ /** A huge finite reciprocal instead of an infinite one: an axis aligned slab would otherwise produce 0 * inf. */
41
+ fn rayInverse(direction: vec3<f32>) -> vec3<f32> {
42
+ let signs = select(vec3<f32>(-1.0), vec3<f32>(1.0), direction >= vec3<f32>(0.0));
43
+
44
+ return signs / max(abs(direction), vec3<f32>(1e-20));
45
+ }
46
+
47
+ /** The number a hit goes by, from where it sits in the leaf order and the instance it was found in. */
48
+ fn namedTriangle(at: u32, instance: u32) -> u32 {
49
+ let stored = orderedTriangle(at);
50
+
51
+ if (INSTANCES && instance != NO_INSTANCE) {
52
+ return instanceWord(instance, 26u) + stored - instanceWord(instance, 25u);
53
+ }
54
+
55
+ return stored;
23
56
  }
24
57
 
25
58
  /** Moller-Trumbore. Returns the distance in x and the barycentric coordinates in yz, or a negative distance. */
@@ -127,7 +160,7 @@ fn waitingLeaf(entry: u32) -> vec2<i32> {
127
160
  * A leaf is tested at the top of the next step, the one place the loop tests triangles, which keeps the kernel small.
128
161
  * This visits what the binary tree's traversal visited, in the same order, so it finds the same hit.
129
162
  */
130
- fn traceScene(origin: vec3<f32>, direction: vec3<f32>, maxDistance: f32) -> Hit {
163
+ fn traceScene(worldOrigin: vec3<f32>, worldDirection: vec3<f32>, maxDistance: f32) -> Hit {
131
164
  var hit: Hit;
132
165
  hit.distance = maxDistance;
133
166
  hit.triangle = NO_HIT;
@@ -138,12 +171,15 @@ fn traceScene(origin: vec3<f32>, direction: vec3<f32>, maxDistance: f32) -> Hit
138
171
  return hit;
139
172
  }
140
173
 
141
- // A huge finite reciprocal instead of an infinite one: an axis aligned slab would otherwise produce 0 * inf.
142
- let signs = select(vec3<f32>(-1.0), vec3<f32>(1.0), direction >= vec3<f32>(0.0));
143
- let inverse = signs / max(abs(direction), vec3<f32>(1e-20));
174
+ var origin = worldOrigin;
175
+ var direction = worldDirection;
176
+ var inverse = rayInverse(direction);
177
+ // The instance the traversal is inside, and the one the closest hit so far was found in.
178
+ var instance = NO_INSTANCE;
179
+ var hitInstance = NO_INSTANCE;
144
180
 
145
- var stack: array<u32, 32>;
146
- var enters: array<f32, 32>;
181
+ var stack: array<u32, BVH_STACK>;
182
+ var enters: array<f32, BVH_STACK>;
147
183
  var depth = 0u;
148
184
  var current = 0u;
149
185
  // A leaf to test before anything else: its link, and its triangle count, zero when there is none.
@@ -151,10 +187,32 @@ fn traceScene(origin: vec3<f32>, direction: vec3<f32>, maxDistance: f32) -> Hit
151
187
 
152
188
  loop {
153
189
  if (leaf.y > 0) {
190
+ let before = hit.distance;
191
+
154
192
  intersectLeaf(leaf.x, leaf.y, origin, direction, &hit);
193
+
194
+ if (INSTANCES && hit.distance < before) {
195
+ hitInstance = instance;
196
+ }
197
+
155
198
  leaf.y = 0;
156
199
  }
157
200
 
201
+ if (INSTANCES && leaf.y < 0) {
202
+ leaf.y = 0;
203
+
204
+ if (depth < BVH_STACK) {
205
+ instance = u32(-1 - leaf.x);
206
+ stack[depth] = RETURN_ENTRY;
207
+ enters[depth] = -1.0;
208
+ depth = depth + 1u;
209
+ origin = instanceToObject(instance, worldOrigin, 1.0);
210
+ direction = instanceToObject(instance, worldDirection, 0.0);
211
+ inverse = rayInverse(direction);
212
+ current = instanceWord(instance, 24u);
213
+ }
214
+ }
215
+
158
216
  if (current == NO_NODE) {
159
217
  if (depth == 0u) {
160
218
  break;
@@ -169,6 +227,14 @@ fn traceScene(origin: vec3<f32>, direction: vec3<f32>, maxDistance: f32) -> Hit
169
227
 
170
228
  let entry = stack[depth];
171
229
 
230
+ if (INSTANCES && entry == RETURN_ENTRY) {
231
+ instance = NO_INSTANCE;
232
+ origin = worldOrigin;
233
+ direction = worldDirection;
234
+ inverse = rayInverse(direction);
235
+ continue;
236
+ }
237
+
172
238
  if ((entry & LEAF_ENTRY) != 0u) {
173
239
  leaf = waitingLeaf(entry);
174
240
  continue;
@@ -198,7 +264,7 @@ fn traceScene(origin: vec3<f32>, direction: vec3<f32>, maxDistance: f32) -> Hit
198
264
  }
199
265
 
200
266
  // The builder caps its depth so the far children always fit; the guard keeps a malformed tree inside the stack.
201
- if (farEnter < 1e30 && depth < 32u) {
267
+ if (farEnter < 1e30 && depth < BVH_STACK) {
202
268
  stack[depth] = select(u32(farLink), leafEntry(parent, leftFirst), farLink < 0);
203
269
  enters[depth] = farEnter;
204
270
  depth = depth + 1u;
@@ -206,7 +272,7 @@ fn traceScene(origin: vec3<f32>, direction: vec3<f32>, maxDistance: f32) -> Hit
206
272
  }
207
273
 
208
274
  if (hit.triangle != NO_HIT) {
209
- hit.triangle = orderedTriangle(hit.triangle);
275
+ hit.triangle = namedTriangle(hit.triangle, hitInstance);
210
276
  }
211
277
 
212
278
  return hit;
@@ -217,15 +283,17 @@ fn traceScene(origin: vec3<f32>, direction: vec3<f32>, maxDistance: f32) -> Hit
217
283
  * closest: what a shadow ray asks when every surface it could meet blocks it. Returns that triangle, or NO_HIT. The
218
284
  * right child goes first and the left one waits, the order the binary tree's traversal took.
219
285
  */
220
- fn traceAny(origin: vec3<f32>, direction: vec3<f32>, maxDistance: f32) -> u32 {
286
+ fn traceAny(worldOrigin: vec3<f32>, worldDirection: vec3<f32>, maxDistance: f32) -> u32 {
221
287
  if (frame.triangleCount == 0u) {
222
288
  return NO_HIT;
223
289
  }
224
290
 
225
- let signs = select(vec3<f32>(-1.0), vec3<f32>(1.0), direction >= vec3<f32>(0.0));
226
- let inverse = signs / max(abs(direction), vec3<f32>(1e-20));
291
+ var origin = worldOrigin;
292
+ var direction = worldDirection;
293
+ var inverse = rayInverse(direction);
294
+ var instance = NO_INSTANCE;
227
295
 
228
- var stack: array<u32, 32>;
296
+ var stack: array<u32, BVH_STACK>;
229
297
  var depth = 0u;
230
298
  var current = 0u;
231
299
  var leaf = vec2<i32>(0, 0);
@@ -235,12 +303,26 @@ fn traceAny(origin: vec3<f32>, direction: vec3<f32>, maxDistance: f32) -> u32 {
235
303
  let found = anyInLeaf(leaf.x, leaf.y, origin, direction, maxDistance);
236
304
 
237
305
  if (found != NO_HIT) {
238
- return orderedTriangle(found);
306
+ return namedTriangle(found, instance);
239
307
  }
240
308
 
241
309
  leaf.y = 0;
242
310
  }
243
311
 
312
+ if (INSTANCES && leaf.y < 0) {
313
+ leaf.y = 0;
314
+
315
+ if (depth < BVH_STACK) {
316
+ instance = u32(-1 - leaf.x);
317
+ stack[depth] = RETURN_ENTRY;
318
+ depth = depth + 1u;
319
+ origin = instanceToObject(instance, worldOrigin, 1.0);
320
+ direction = instanceToObject(instance, worldDirection, 0.0);
321
+ inverse = rayInverse(direction);
322
+ current = instanceWord(instance, 24u);
323
+ }
324
+ }
325
+
244
326
  if (current == NO_NODE) {
245
327
  if (depth == 0u) {
246
328
  break;
@@ -250,6 +332,14 @@ fn traceAny(origin: vec3<f32>, direction: vec3<f32>, maxDistance: f32) -> u32 {
250
332
 
251
333
  let entry = stack[depth];
252
334
 
335
+ if (INSTANCES && entry == RETURN_ENTRY) {
336
+ instance = NO_INSTANCE;
337
+ origin = worldOrigin;
338
+ direction = worldDirection;
339
+ inverse = rayInverse(direction);
340
+ continue;
341
+ }
342
+
253
343
  if ((entry & LEAF_ENTRY) != 0u) {
254
344
  leaf = waitingLeaf(entry);
255
345
  continue;
@@ -277,7 +367,7 @@ fn traceAny(origin: vec3<f32>, direction: vec3<f32>, maxDistance: f32) -> u32 {
277
367
  }
278
368
  }
279
369
 
280
- if (rightHit && leftHit && depth < 32u) {
370
+ if (rightHit && leftHit && depth < BVH_STACK) {
281
371
  stack[depth] = select(u32(node.leftLink), leafEntry(parent, false), node.leftLink < 0);
282
372
  depth = depth + 1u;
283
373
  }
@@ -1 +1 @@
1
- {"version":3,"file":"bvh.wgsl.js","sourceRoot":"","sources":["../../src/wgsl/bvh.wgsl.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,UAAU,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwRlC,CAAC"}
1
+ {"version":3,"file":"bvh.wgsl.js","sourceRoot":"","sources":["../../src/wgsl/bvh.wgsl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,UAAU,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8WlC,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"common.wgsl.d.ts","sourceRoot":"","sources":["../../src/wgsl/common.wgsl.ts"],"names":[],"mappings":"AAIA,6EAA6E;AAC7E,eAAO,MAAM,WAAW,QA4MvB,CAAC"}
1
+ {"version":3,"file":"common.wgsl.d.ts","sourceRoot":"","sources":["../../src/wgsl/common.wgsl.ts"],"names":[],"mappings":"AAIA,6EAA6E;AAC7E,eAAO,MAAM,WAAW,QA6RvB,CAAC"}
@@ -136,13 +136,16 @@ const TEXTURE_TABLE: u32 = ${TEXTURE_TABLE}u;
136
136
  // A WebGPU device only has to offer eight storage buffers per stage, so arrays of the same kind share one binding and
137
137
  // the uniform says where the second one starts. See sceneUpload.ts, which joins them.
138
138
  @group(0) @binding(0) var<uniform> frame: Frame;
139
- // Nine coordinates per triangle, in the hierarchy's leaf order: see leafOf in bvh.wgsl.ts.
139
+ // Nine coordinates per stored triangle, in the hierarchy's leaf order: see leafOf in bvh.wgsl.ts. A prototype's are
140
+ // in object space.
140
141
  @group(0) @binding(1) var<storage, read> positions: array<f32>;
141
- // Normals, then the UV sets from frame.uvOffset, vertex colours from frame.colorOffset and shader varyings from
142
- // frame.varyingOffset.
142
+ // Per vertex: the UV sets from frame.uvOffset and vertex colours from frame.colorOffset; then shader varyings, per
143
+ // corner, from frame.varyingOffset.
143
144
  @group(0) @binding(2) var<storage, read> attributes: array<f32>;
144
- // One material index per triangle, then the hierarchy's triangle order from frame.orderOffset on (the triangle at each
145
- // position of the leaf order), then from frame.leafOffset on the other way (the position of each triangle).
145
+ // One material index per triangle a hit can name, then the hierarchy's triangle order from frame.orderOffset on (the
146
+ // stored triangle at each position of the leaf order), then from frame.leafOffset on the other way (the position of
147
+ // each stored triangle), the instance of every instanced triangle from frame.instanceOffset, the three vertices of each
148
+ // stored triangle from frame.cornerOffset and every vertex's folded normal from frame.normalOffset.
146
149
  @group(0) @binding(3) var<storage, read> indices: array<u32>;
147
150
  @group(0) @binding(4) var<storage, read> materials: array<Material>;
148
151
  @group(0) @binding(5) var<storage, read> nodes: array<BvhNode>;
@@ -162,24 +165,102 @@ fn orderedTriangle(at: u32) -> u32 {
162
165
  return indices[frame.orderOffset + at];
163
166
  }
164
167
 
165
- /** One word of a UV set: three vertices of four components, two per set. */
166
- fn attributeUv(triangle: u32, corner: u32, channel: u32) -> vec2<f32> {
167
- let at = frame.uvOffset + triangle * 12u + corner * 4u + channel * 2u;
168
+ /** What a triangle of the scene's own belongs to: no instance. */
169
+ const NO_INSTANCE: u32 = 0xffffffffu;
170
+
171
+ /** The instance a triangle a hit named belongs to, or NO_INSTANCE for one of the scene's own. */
172
+ fn instanceOf(triangle: u32) -> u32 {
173
+ if (!INSTANCES || triangle < frame.sceneTriangles) {
174
+ return NO_INSTANCE;
175
+ }
176
+
177
+ return indices[frame.instanceOffset + triangle - frame.sceneTriangles];
178
+ }
179
+
180
+ /** One word of an instance's record: see INSTANCE_WORDS in instancing.ts. */
181
+ fn instanceWord(instance: u32, word: u32) -> u32 {
182
+ return textures[frame.instanceData + instance * 28u + word];
183
+ }
184
+
185
+ /** A row of an instance's transforms: 0 to 2 take object space to world, 3 to 5 back. */
186
+ fn instanceRow(instance: u32, row: u32) -> vec4<f32> {
187
+ let at = frame.instanceData + instance * 28u + row * 4u;
188
+
189
+ return vec4<f32>(bitcast<f32>(textures[at]), bitcast<f32>(textures[at + 1u]), bitcast<f32>(textures[at + 2u]),
190
+ bitcast<f32>(textures[at + 3u]));
191
+ }
192
+
193
+ /** Where a named triangle is stored: itself, or for an instance's, the prototype's triangle it repeats. */
194
+ fn storedTriangle(triangle: u32, instance: u32) -> u32 {
195
+ if (instance == NO_INSTANCE) {
196
+ return triangle;
197
+ }
198
+
199
+ return instanceWord(instance, 25u) + triangle - instanceWord(instance, 26u);
200
+ }
201
+
202
+ /** A point of an instance's prototype, in world space. */
203
+ fn instancePoint(instance: u32, p: vec3<f32>) -> vec3<f32> {
204
+ let q = vec4<f32>(p, 1.0);
205
+
206
+ return vec3<f32>(dot(instanceRow(instance, 0u), q), dot(instanceRow(instance, 1u), q), dot(instanceRow(instance, 2u), q));
207
+ }
208
+
209
+ /** A world space point (w 1) or direction (w 0), in an instance's object space. */
210
+ fn instanceToObject(instance: u32, v: vec3<f32>, w: f32) -> vec3<f32> {
211
+ let q = vec4<f32>(v, w);
212
+
213
+ return vec3<f32>(dot(instanceRow(instance, 3u), q), dot(instanceRow(instance, 4u), q), dot(instanceRow(instance, 5u), q));
214
+ }
215
+
216
+ /** An object space normal in world space, by the inverse transpose; not normalized. */
217
+ fn instanceNormal(instance: u32, n: vec3<f32>) -> vec3<f32> {
218
+ return n.x * instanceRow(instance, 3u).xyz + n.y * instanceRow(instance, 4u).xyz + n.z * instanceRow(instance, 5u).xyz;
219
+ }
220
+
221
+ /** The three vertices of a triangle a hit named. */
222
+ fn triangleCorners(triangle: u32) -> vec3<u32> {
223
+ let at = frame.cornerOffset + storedTriangle(triangle, instanceOf(triangle)) * 3u;
224
+
225
+ return vec3<u32>(indices[at], indices[at + 1u], indices[at + 2u]);
226
+ }
227
+
228
+ /** One UV set of a vertex: four components, two per set. */
229
+ fn vertexUv(vertex: u32, channel: u32) -> vec2<f32> {
230
+ let at = frame.uvOffset + vertex * 4u + channel * 2u;
168
231
 
169
232
  return vec2<f32>(attributes[at], attributes[at + 1u]);
170
233
  }
171
234
 
172
- /** A corner's vertex colour, or white when the scene has none. */
173
- fn attributeColor(triangle: u32, corner: u32) -> vec4<f32> {
235
+ /** A vertex's colour, or white when the scene has none. */
236
+ fn vertexColor(vertex: u32) -> vec4<f32> {
174
237
  if (frame.hasColors == 0u) {
175
238
  return vec4<f32>(1.0);
176
239
  }
177
240
 
178
- let at = frame.colorOffset + triangle * 12u + corner * 4u;
241
+ let at = frame.colorOffset + vertex * 4u;
179
242
 
180
243
  return vec4<f32>(attributes[at], attributes[at + 1u], attributes[at + 2u], attributes[at + 3u]);
181
244
  }
182
245
 
246
+ /** A normal folded onto an octahedron in two signed 16 bit halves, as sceneUpload.ts folds it; zero for NO_NORMAL. */
247
+ fn vertexNormal(vertex: u32) -> vec3<f32> {
248
+ let code = indices[frame.normalOffset + vertex];
249
+
250
+ if (code == 0x80008000u) {
251
+ return vec3<f32>(0.0);
252
+ }
253
+
254
+ let e = unpack2x16snorm(code);
255
+ var n = vec3<f32>(e.x, e.y, 1.0 - abs(e.x) - abs(e.y));
256
+ let t = max(-n.z, 0.0);
257
+
258
+ n.x = n.x + select(t, -t, n.x >= 0.0);
259
+ n.y = n.y + select(t, -t, n.y >= 0.0);
260
+
261
+ return normalize(n);
262
+ }
263
+
183
264
  fn extraFloat(at: u32) -> f32 {
184
265
  return bitcast<f32>(textures[at]);
185
266
  }
@@ -1 +1 @@
1
- {"version":3,"file":"common.wgsl.js","sourceRoot":"","sources":["../../src/wgsl/common.wgsl.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACtE,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAE3E,6EAA6E;AAC7E,MAAM,CAAC,MAAM,WAAW,GAAG,UAAU,CAAC;EACpC,YAAY,CAAC,IAAI;;;;;;;;;;;;;;;EAejB,eAAe,CAAC,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;6BAqFO,aAAa,CAAC,QAAQ;6BACtB,aAAa,CAAC,QAAQ;4BACvB,aAAa,CAAC,OAAO;0BACvB,aAAa,CAAC,KAAK;yBACpB,aAAa,CAAC,IAAI;0BACjB,aAAa,CAAC,KAAK;2BAClB,aAAa,CAAC,MAAM;0BACrB,aAAa,CAAC,KAAK;2BAClB,aAAa,CAAC,MAAM;2BACpB,aAAa,CAAC,MAAM;2BACpB,aAAa,CAAC,MAAM;;;;;;;;;;;;;;;;2BAgBpB,WAAW;0BACZ,UAAU;6BACP,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2EzC,CAAC"}
1
+ {"version":3,"file":"common.wgsl.js","sourceRoot":"","sources":["../../src/wgsl/common.wgsl.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACtE,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAE3E,6EAA6E;AAC7E,MAAM,CAAC,MAAM,WAAW,GAAG,UAAU,CAAC;EACpC,YAAY,CAAC,IAAI;;;;;;;;;;;;;;;EAejB,eAAe,CAAC,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;6BAqFO,aAAa,CAAC,QAAQ;6BACtB,aAAa,CAAC,QAAQ;4BACvB,aAAa,CAAC,OAAO;0BACvB,aAAa,CAAC,KAAK;yBACpB,aAAa,CAAC,IAAI;0BACjB,aAAa,CAAC,KAAK;2BAClB,aAAa,CAAC,MAAM;0BACrB,aAAa,CAAC,KAAK;2BAClB,aAAa,CAAC,MAAM;2BACpB,aAAa,CAAC,MAAM;2BACpB,aAAa,CAAC,MAAM;;;;;;;;;;;;;;;;2BAgBpB,WAAW;0BACZ,UAAU;6BACP,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4JzC,CAAC"}
@@ -13,5 +13,5 @@
13
13
  * texel takes the light of the texels behind it along every ray's direction, fading with the distance, with weights
14
14
  * that again sum to one.
15
15
  */
16
- export declare const LENS_WGSL = "\nstruct Lens {\n resolution: vec2<u32>,\n // Whether the colour comes from the denoiser's output rather than straight from the accumulation.\n denoised: u32,\n // Upsampling only: the level read from sits in the downsampled pyramid (the smallest one) instead of the folded one.\n fromDown: u32,\n sourceSize: vec2<u32>,\n // Where the level read from starts in its pyramid; a size of the full resolution reads the image itself.\n sourceOffset: u32,\n // The first downsample reads the image itself, and averages its taps by luminance against fireflies.\n fromImage: u32,\n targetSize: vec2<u32>,\n targetOffset: u32,\n // Where the largest level of the folded pyramid starts, which the composite reads the halo from.\n haloOffset: u32,\n haloSize: vec2<u32>,\n // How much of the light the halo takes, and how much each level takes from the one below it.\n intensity: f32,\n radius: f32,\n // How far red and blue move apart at the corners, as a share of the distance from the centre.\n aberration: f32,\n // How much of the light the aperture's rays take, how many there are, and how far they reach in streak texels.\n glare: f32,\n rays: u32,\n glareLength: f32,\n // The white balance, a linear sRGB matrix, row by row.\n white0: vec4<f32>,\n white1: vec4<f32>,\n white2: vec4<f32>,\n // The size of the level the rays are gathered at, and the angle of the first ray, in radians.\n streakSize: vec2<u32>,\n glareAngle: f32,\n // Whether the camera meters the image and exposes it to middle grey, which the meter pass works out.\n autoExposure: f32,\n // The share of a dark scene's darkness the meter keeps.\n darkAdaptation: f32,\n};\n\nconst PI: f32 = 3.14159265358979;\n\n/** Taps along each of the aperture's rays. */\nconst STREAK_TAPS: i32 = 24;\n\n@group(0) @binding(0) var<uniform> lens: Lens;\n@group(0) @binding(1) var<storage, read> accumulation: array<vec4<f32>>;\n@group(0) @binding(2) var<storage, read> denoised: array<vec4<f32>>;\n@group(0) @binding(3) var<storage, read_write> down: array<vec4<f32>>;\n@group(0) @binding(4) var<storage, read_write> up: array<vec4<f32>>;\n@group(0) @binding(5) var<storage, read_write> graded: array<vec4<f32>>;\n@group(0) @binding(6) var<storage, read_write> streaked: array<vec4<f32>>;\n// What the camera's meter read: the exposure that brings the image's average to middle grey, in x.\n@group(0) @binding(7) var<storage, read_write> metered: array<vec4<f32>>;\n\n/** What a light meter calibrates to: an average scene reflects about eighteen percent. */\nconst MIDDLE_GREY: f32 = 0.18;\nconst METER_GRID: vec2<u32> = vec2<u32>(64u, 36u);\nconst METER_THREADS: u32 = 256u;\n\n/**\n * How far above middle grey the meter lets the bright end of the image land: three stops, where AgX still shows\n * detail, three and a half under where it reaches white.\n */\nconst HIGHLIGHT_CEILING: f32 = 8.0;\n/** The share of the metered points that counts as the bright end: the light through a window, not a lamp's own pixels. */\nconst HIGHLIGHT_SHARE: f32 = 0.03;\n/**\n * The average below which a scene counts as dark: about three stops under middle grey, a room lit by a window on a\n * grey day. Above it the meter exposes fully; below it, only for the stops the dark adaptation lets go.\n */\nconst DIM_AVERAGE: f32 = 0.02;\n/** Half stops of luminance, from 2^-16 to 2^16, which the bright end is found in. */\nconst METER_BINS: u32 = 64u;\n\nvar<workgroup> meterSums: array<vec2<f32>, 256>;\nvar<workgroup> meterHistogram: array<atomic<u32>, 64>;\n\n/**\n * The camera's meter: the image's average luminance over a grid of points, taken as the mean of the logarithms so that\n * a bright window or a patch of sky cannot darken everything else, weighed towards the centre as a camera's\n * centre-weighted metering is. What no path has reached yet, or is black, does not count. One workgroup reads the\n * grid and sums it in shared memory.\n *\n * Then it guards the highlights, as a camera's matrix metering does: the average alone takes a dim attic with a shaft\n * of sun in it to middle grey and burns the shaft out, where a photographer keeps the room dim. The brightest few\n * percent of the points, found in a histogram of half stops, may land no more than {@link HIGHLIGHT_CEILING} times\n * brighter; an even scene, whose bright end is close to its average, is left as the average says.\n */\n@compute @workgroup_size(256, 1, 1)\nfn meter(@builtin(local_invocation_index) thread: u32) {\n var sum = vec2<f32>(0.0);\n let count = METER_GRID.x * METER_GRID.y;\n\n if (thread < METER_BINS) {\n atomicStore(&meterHistogram[thread], 0u);\n }\n\n workgroupBarrier();\n\n for (var k = thread; k < count; k = k + METER_THREADS) {\n let cell = vec2<f32>(f32(k % METER_GRID.x), f32(k / METER_GRID.x)) + 0.5;\n let uv = cell / vec2<f32>(METER_GRID);\n let luminance = dot(imageTexel(i32(uv.x * f32(lens.resolution.x)), i32(uv.y * f32(lens.resolution.y))),\n vec3<f32>(0.2126, 0.7152, 0.0722));\n let away = (uv - 0.5) * 2.0;\n let weight = select(0.0, 1.0 - 0.6 * min(dot(away, away) * 0.5, 1.0), luminance > 0.0);\n\n sum = sum + vec2<f32>(log(max(luminance, 1e-6)) * weight, weight);\n\n if (luminance > 0.0) {\n let bin = u32(clamp((log2(luminance) + 16.0) * 2.0, 0.0, f32(METER_BINS - 1u)));\n\n atomicAdd(&meterHistogram[bin], 1u);\n }\n }\n\n meterSums[thread] = sum;\n workgroupBarrier();\n\n for (var stride = METER_THREADS / 2u; stride > 0u; stride = stride / 2u) {\n if (thread < stride) {\n meterSums[thread] = meterSums[thread] + meterSums[thread + stride];\n }\n\n workgroupBarrier();\n }\n\n if (thread == 0u) {\n let total = meterSums[0];\n let average = select(MIDDLE_GREY, exp(total.x / max(total.y, 1e-6)), total.y > 0.0);\n var exposure = MIDDLE_GREY / average;\n\n // A dark scene keeps a share of its missing stops: a night stays a night instead of turning into dusk.\n if (average < DIM_AVERAGE) {\n exposure = exposure * pow(average / DIM_AVERAGE, lens.darkAdaptation);\n }\n\n // From the top down, the first half stop by which the bright share is reached; its upper edge is that end.\n var points = 0u;\n\n for (var bin = 0u; bin < METER_BINS; bin = bin + 1u) {\n points = points + atomicLoad(&meterHistogram[bin]);\n }\n\n var above = 0u;\n var bright = 0.0;\n\n for (var bin = i32(METER_BINS) - 1; bin >= 0 && points > 0u; bin = bin - 1) {\n above = above + atomicLoad(&meterHistogram[u32(bin)]);\n\n if (f32(above) >= HIGHLIGHT_SHARE * f32(points)) {\n bright = exp2(f32(bin + 1) * 0.5 - 16.0);\n break;\n }\n }\n\n if (bright > 0.0) {\n exposure = min(exposure, HIGHLIGHT_CEILING * MIDDLE_GREY / bright);\n }\n\n metered[0] = vec4<f32>(clamp(exposure, 1e-4, 1e4), average, bright, 0.0);\n }\n}\n\n/** The image's own texel: its mean so far, or nothing where no path has landed yet. */\nfn imageTexel(x: i32, y: i32) -> vec3<f32> {\n let size = vec2<i32>(lens.resolution);\n let index = u32(clamp(y, 0, size.y - 1) * size.x + clamp(x, 0, size.x - 1));\n\n if (lens.denoised != 0u) {\n return denoised[index].rgb;\n }\n\n let stored = accumulation[index];\n\n return select(stored.rgb / stored.w, vec3<f32>(0.0), stored.w <= 0.0);\n}\n\nfn downTexel(offset: u32, size: vec2<u32>, x: i32, y: i32) -> vec3<f32> {\n let s = vec2<i32>(size);\n\n return down[offset + u32(clamp(y, 0, s.y - 1) * s.x + clamp(x, 0, s.x - 1))].rgb;\n}\n\nfn upTexel(offset: u32, size: vec2<u32>, x: i32, y: i32) -> vec3<f32> {\n let s = vec2<i32>(size);\n\n return up[offset + u32(clamp(y, 0, s.y - 1) * s.x + clamp(x, 0, s.x - 1))].rgb;\n}\n\n/** Bilinear weights and the texel below and left of a point given in texels, edges clamped. */\nfn bilinearAt(position: vec2<f32>) -> vec4<f32> {\n let p = position - 0.5;\n let base = floor(p);\n\n return vec4<f32>(base, p - base);\n}\n\nfn imageBilinear(position: vec2<f32>) -> vec3<f32> {\n let b = bilinearAt(position);\n let x = i32(b.x);\n let y = i32(b.y);\n\n return mix(\n mix(imageTexel(x, y), imageTexel(x + 1, y), b.z),\n mix(imageTexel(x, y + 1), imageTexel(x + 1, y + 1), b.z), b.w);\n}\n\nfn downBilinear(offset: u32, size: vec2<u32>, position: vec2<f32>) -> vec3<f32> {\n let b = bilinearAt(position);\n let x = i32(b.x);\n let y = i32(b.y);\n\n return mix(\n mix(downTexel(offset, size, x, y), downTexel(offset, size, x + 1, y), b.z),\n mix(downTexel(offset, size, x, y + 1), downTexel(offset, size, x + 1, y + 1), b.z), b.w);\n}\n\nfn upBilinear(offset: u32, size: vec2<u32>, position: vec2<f32>) -> vec3<f32> {\n let b = bilinearAt(position);\n let x = i32(b.x);\n let y = i32(b.y);\n\n return mix(\n mix(upTexel(offset, size, x, y), upTexel(offset, size, x + 1, y), b.z),\n mix(upTexel(offset, size, x, y + 1), upTexel(offset, size, x + 1, y + 1), b.z), b.w);\n}\n\n/** The level being filtered, read bilinearly at a point given in its own texels. */\nfn sourceAt(position: vec2<f32>) -> vec3<f32> {\n if (lens.fromImage != 0u) {\n return imageBilinear(position);\n }\n\n return downBilinear(lens.sourceOffset, lens.sourceSize, position);\n}\n\nfn luminance(color: vec3<f32>) -> f32 {\n return dot(color, vec3<f32>(0.2126, 0.7152, 0.0722));\n}\n\n/** Karis' average: a group of taps weighed by one over one plus its luminance, so a lone firefly cannot flare. */\nfn karis(sum: vec3<f32>) -> vec4<f32> {\n let weight = 1.0 / (1.0 + luminance(sum));\n\n return vec4<f32>(sum * weight, weight);\n}\n\n/** One level of the pyramid from the one above it: Jimenez's 13 taps, four overlapping boxes and a centre one. */\n@compute @workgroup_size(8, 8, 1)\nfn downsample(@builtin(global_invocation_id) id: vec3<u32>) {\n if (id.x >= lens.targetSize.x || id.y >= lens.targetSize.y) {\n return;\n }\n\n let scale = vec2<f32>(lens.sourceSize) / vec2<f32>(lens.targetSize);\n let centre = (vec2<f32>(id.xy) + 0.5) * scale;\n let a = sourceAt(centre + vec2<f32>(-2.0, -2.0));\n let b = sourceAt(centre + vec2<f32>(0.0, -2.0));\n let c = sourceAt(centre + vec2<f32>(2.0, -2.0));\n let d = sourceAt(centre + vec2<f32>(-2.0, 0.0));\n let e = sourceAt(centre);\n let f = sourceAt(centre + vec2<f32>(2.0, 0.0));\n let g = sourceAt(centre + vec2<f32>(-2.0, 2.0));\n let h = sourceAt(centre + vec2<f32>(0.0, 2.0));\n let i = sourceAt(centre + vec2<f32>(2.0, 2.0));\n let j = sourceAt(centre + vec2<f32>(-1.0, -1.0));\n let k = sourceAt(centre + vec2<f32>(1.0, -1.0));\n let l = sourceAt(centre + vec2<f32>(-1.0, 1.0));\n let m = sourceAt(centre + vec2<f32>(1.0, 1.0));\n var result: vec3<f32>;\n\n if (lens.fromImage != 0u) {\n let groups = karis((j + k + l + m) * 0.25) * 0.5\n + karis((a + b + d + e) * 0.25) * 0.125\n + karis((b + c + e + f) * 0.25) * 0.125\n + karis((d + e + g + h) * 0.25) * 0.125\n + karis((e + f + h + i) * 0.25) * 0.125;\n\n result = groups.rgb / max(groups.w, 1e-6);\n } else {\n result = e * 0.125 + (a + c + g + i) * 0.03125 + (b + d + f + h) * 0.0625 + (j + k + l + m) * 0.125;\n }\n\n down[lens.targetOffset + id.y * lens.targetSize.x + id.x] = vec4<f32>(max(result, vec3<f32>(0.0)), 1.0);\n}\n\n/** A 3 x 3 tent over the smaller level, read at a point given in its texels. */\nfn tent(position: vec2<f32>) -> vec3<f32> {\n var sum = vec3<f32>(0.0);\n\n for (var dy = -1; dy <= 1; dy = dy + 1) {\n for (var dx = -1; dx <= 1; dx = dx + 1) {\n let weight = f32((2 - abs(dx)) * (2 - abs(dy))) / 16.0;\n let at = position + vec2<f32>(f32(dx), f32(dy));\n\n if (lens.fromDown != 0u) {\n sum = sum + downBilinear(lens.sourceOffset, lens.sourceSize, at) * weight;\n } else {\n sum = sum + upBilinear(lens.sourceOffset, lens.sourceSize, at) * weight;\n }\n }\n }\n\n return sum;\n}\n\n/** One level folded back: its own light, mixed by the radius with the wider spread of every level below it. */\n@compute @workgroup_size(8, 8, 1)\nfn upsample(@builtin(global_invocation_id) id: vec3<u32>) {\n if (id.x >= lens.targetSize.x || id.y >= lens.targetSize.y) {\n return;\n }\n\n let scale = vec2<f32>(lens.sourceSize) / vec2<f32>(lens.targetSize);\n let wider = tent((vec2<f32>(id.xy) + 0.5) * scale);\n let index = lens.targetOffset + id.y * lens.targetSize.x + id.x;\n\n up[index] = vec4<f32>(mix(down[index].rgb, wider, lens.radius), 1.0);\n}\n\n/** The aperture's rays at one texel of the streak level: the light behind it along each ray, fading with distance. */\n@compute @workgroup_size(8, 8, 1)\nfn streaks(@builtin(global_invocation_id) id: vec3<u32>) {\n if (id.x >= lens.targetSize.x || id.y >= lens.targetSize.y) {\n return;\n }\n\n let centre = vec2<f32>(id.xy) + 0.5;\n var sum = vec3<f32>(0.0);\n var total = 0.0;\n\n for (var r = 0u; r < lens.rays; r = r + 1u) {\n let angle = lens.glareAngle + f32(r) * 2.0 * PI / f32(lens.rays);\n let along = vec2<f32>(cos(angle), sin(angle));\n\n for (var j = 1; j <= STREAK_TAPS; j = j + 1) {\n let t = f32(j) / f32(STREAK_TAPS);\n let weight = exp(-4.0 * t);\n\n sum = sum + downBilinear(lens.sourceOffset, lens.sourceSize, centre - along * (t * lens.glareLength)) * weight;\n total = total + weight;\n }\n }\n\n streaked[id.y * lens.targetSize.x + id.x] = vec4<f32>(sum / max(total, 1e-6), 1.0);\n}\n\nfn streakTexel(x: i32, y: i32) -> vec3<f32> {\n let s = vec2<i32>(lens.streakSize);\n\n return streaked[u32(clamp(y, 0, s.y - 1) * s.x + clamp(x, 0, s.x - 1))].rgb;\n}\n\nfn streakBilinear(position: vec2<f32>) -> vec3<f32> {\n let b = bilinearAt(position);\n let x = i32(b.x);\n let y = i32(b.y);\n\n return mix(mix(streakTexel(x, y), streakTexel(x + 1, y), b.z), mix(streakTexel(x, y + 1), streakTexel(x + 1, y + 1), b.z), b.w);\n}\n\n/** The image as the lens delivers it: fringed towards the corners, with its halo, balanced for the camera's white. */\n@compute @workgroup_size(8, 8, 1)\nfn composite(@builtin(global_invocation_id) id: vec3<u32>) {\n if (id.x >= lens.resolution.x || id.y >= lens.resolution.y) {\n return;\n }\n\n let size = vec2<f32>(lens.resolution);\n let position = vec2<f32>(id.xy) + 0.5;\n var color = imageTexel(i32(id.x), i32(id.y));\n\n // Lateral chromatic aberration: each colour is magnified a little differently, red outwards and blue inwards,\n // in proportion to the distance from the centre, which is how a lens's dispersion moves them.\n if (lens.aberration > 0.0) {\n let centre = size * 0.5;\n let away = position - centre;\n let red = imageBilinear(centre + away * (1.0 - lens.aberration));\n let blue = imageBilinear(centre + away * (1.0 + lens.aberration));\n\n color = vec3<f32>(red.r, color.g, blue.b);\n }\n\n if (lens.intensity > 0.0) {\n // The largest folded level is already a tent over the levels below it, so one bilinear read of it is smooth.\n let halo = upBilinear(lens.haloOffset, lens.haloSize, position * vec2<f32>(lens.haloSize) / size);\n\n color = mix(color, halo, lens.intensity);\n }\n\n if (lens.glare > 0.0) {\n color = mix(color, streakBilinear(position * vec2<f32>(lens.streakSize) / size), lens.glare);\n }\n\n if (lens.autoExposure > 0.0) {\n color = color * metered[0].x;\n }\n\n let balanced = vec3<f32>(dot(lens.white0.xyz, color), dot(lens.white1.xyz, color), dot(lens.white2.xyz, color));\n\n graded[id.y * lens.resolution.x + id.x] = vec4<f32>(max(balanced, vec3<f32>(0.0)), 1.0);\n}\n";
16
+ export declare const LENS_WGSL = "\nstruct Lens {\n resolution: vec2<u32>,\n // Whether the colour comes from the denoiser's output rather than straight from the accumulation.\n denoised: u32,\n // Upsampling only: the level read from sits in the downsampled pyramid (the smallest one) instead of the folded one.\n fromDown: u32,\n sourceSize: vec2<u32>,\n // Where the level read from starts in its pyramid; a size of the full resolution reads the image itself.\n sourceOffset: u32,\n // The first downsample reads the image itself, and averages its taps by luminance against fireflies.\n fromImage: u32,\n targetSize: vec2<u32>,\n targetOffset: u32,\n // Where the largest level of the folded pyramid starts, which the composite reads the halo from.\n haloOffset: u32,\n haloSize: vec2<u32>,\n // How much of the light the halo takes, and how much each level takes from the one below it.\n intensity: f32,\n radius: f32,\n // How far red and blue move apart at the corners, as a share of the distance from the centre.\n aberration: f32,\n // How much of the light the aperture's rays take, how many there are, and how far they reach in streak texels.\n glare: f32,\n rays: u32,\n glareLength: f32,\n // The white balance, a linear sRGB matrix, row by row.\n white0: vec4<f32>,\n white1: vec4<f32>,\n white2: vec4<f32>,\n // The size of the level the rays are gathered at, and the angle of the first ray, in radians.\n streakSize: vec2<u32>,\n glareAngle: f32,\n // Whether the camera meters the image and exposes it to middle grey, which the meter pass works out.\n autoExposure: f32,\n // The share of a dark scene's darkness the meter keeps.\n darkAdaptation: f32,\n // The exposure applied after the lens, the meter's aside, which the rays are limited against.\n exposure: f32,\n};\n\nconst PI: f32 = 3.14159265358979;\n\n/** Taps along each of the aperture's rays. */\nconst STREAK_TAPS: i32 = 24;\n\n/**\n * The brightest a tap of the rays counts as, in multiples of the exposed white. A lamp is thousands of times white,\n * which would carry its rays across the frame at full length; past this they stay as short as a camera shows them.\n */\nconst GLARE_CEILING: f32 = 4.0;\n\n@group(0) @binding(0) var<uniform> lens: Lens;\n@group(0) @binding(1) var<storage, read> accumulation: array<vec4<f32>>;\n@group(0) @binding(2) var<storage, read> denoised: array<vec4<f32>>;\n@group(0) @binding(3) var<storage, read_write> down: array<vec4<f32>>;\n@group(0) @binding(4) var<storage, read_write> up: array<vec4<f32>>;\n@group(0) @binding(5) var<storage, read_write> graded: array<vec4<f32>>;\n@group(0) @binding(6) var<storage, read_write> streaked: array<vec4<f32>>;\n// What the camera's meter read: the exposure that brings the image's average to middle grey, in x.\n@group(0) @binding(7) var<storage, read_write> metered: array<vec4<f32>>;\n\n/** What a light meter calibrates to: an average scene reflects about eighteen percent. */\nconst MIDDLE_GREY: f32 = 0.18;\nconst METER_GRID: vec2<u32> = vec2<u32>(64u, 36u);\nconst METER_THREADS: u32 = 256u;\n\n/**\n * How far above middle grey the meter lets the bright end of the image land: three stops, where AgX still shows\n * detail, three and a half under where it reaches white.\n */\nconst HIGHLIGHT_CEILING: f32 = 8.0;\n/** The share of the metered points that counts as the bright end: the light through a window, not a lamp's own pixels. */\nconst HIGHLIGHT_SHARE: f32 = 0.03;\n/**\n * The average below which a scene counts as dark: about three stops under middle grey, a room lit by a window on a\n * grey day. Above it the meter exposes fully; below it, only for the stops the dark adaptation lets go.\n */\nconst DIM_AVERAGE: f32 = 0.02;\n/** Half stops of luminance, from 2^-16 to 2^16, which the bright end is found in. */\nconst METER_BINS: u32 = 64u;\n\nvar<workgroup> meterSums: array<vec2<f32>, 256>;\nvar<workgroup> meterHistogram: array<atomic<u32>, 64>;\n\n/**\n * The camera's meter: the image's average luminance over a grid of points, taken as the mean of the logarithms so that\n * a bright window or a patch of sky cannot darken everything else, weighed towards the centre as a camera's\n * centre-weighted metering is. What no path has reached yet, or is black, does not count. One workgroup reads the\n * grid and sums it in shared memory.\n *\n * Then it guards the highlights, as a camera's matrix metering does: the average alone takes a dim attic with a shaft\n * of sun in it to middle grey and burns the shaft out, where a photographer keeps the room dim. The brightest few\n * percent of the points, found in a histogram of half stops, may land no more than {@link HIGHLIGHT_CEILING} times\n * brighter; an even scene, whose bright end is close to its average, is left as the average says.\n */\n@compute @workgroup_size(256, 1, 1)\nfn meter(@builtin(local_invocation_index) thread: u32) {\n var sum = vec2<f32>(0.0);\n let count = METER_GRID.x * METER_GRID.y;\n\n if (thread < METER_BINS) {\n atomicStore(&meterHistogram[thread], 0u);\n }\n\n workgroupBarrier();\n\n for (var k = thread; k < count; k = k + METER_THREADS) {\n let cell = vec2<f32>(f32(k % METER_GRID.x), f32(k / METER_GRID.x)) + 0.5;\n let uv = cell / vec2<f32>(METER_GRID);\n let luminance = dot(imageTexel(i32(uv.x * f32(lens.resolution.x)), i32(uv.y * f32(lens.resolution.y))),\n vec3<f32>(0.2126, 0.7152, 0.0722));\n let away = (uv - 0.5) * 2.0;\n let weight = select(0.0, 1.0 - 0.6 * min(dot(away, away) * 0.5, 1.0), luminance > 0.0);\n\n sum = sum + vec2<f32>(log(max(luminance, 1e-6)) * weight, weight);\n\n if (luminance > 0.0) {\n let bin = u32(clamp((log2(luminance) + 16.0) * 2.0, 0.0, f32(METER_BINS - 1u)));\n\n atomicAdd(&meterHistogram[bin], 1u);\n }\n }\n\n meterSums[thread] = sum;\n workgroupBarrier();\n\n for (var stride = METER_THREADS / 2u; stride > 0u; stride = stride / 2u) {\n if (thread < stride) {\n meterSums[thread] = meterSums[thread] + meterSums[thread + stride];\n }\n\n workgroupBarrier();\n }\n\n if (thread == 0u) {\n let total = meterSums[0];\n let average = select(MIDDLE_GREY, exp(total.x / max(total.y, 1e-6)), total.y > 0.0);\n var exposure = MIDDLE_GREY / average;\n\n // A dark scene keeps a share of its missing stops: a night stays a night instead of turning into dusk.\n if (average < DIM_AVERAGE) {\n exposure = exposure * pow(average / DIM_AVERAGE, lens.darkAdaptation);\n }\n\n // From the top down, the first half stop by which the bright share is reached; its upper edge is that end.\n var points = 0u;\n\n for (var bin = 0u; bin < METER_BINS; bin = bin + 1u) {\n points = points + atomicLoad(&meterHistogram[bin]);\n }\n\n var above = 0u;\n var bright = 0.0;\n\n for (var bin = i32(METER_BINS) - 1; bin >= 0 && points > 0u; bin = bin - 1) {\n above = above + atomicLoad(&meterHistogram[u32(bin)]);\n\n if (f32(above) >= HIGHLIGHT_SHARE * f32(points)) {\n bright = exp2(f32(bin + 1) * 0.5 - 16.0);\n break;\n }\n }\n\n if (bright > 0.0) {\n exposure = min(exposure, HIGHLIGHT_CEILING * MIDDLE_GREY / bright);\n }\n\n metered[0] = vec4<f32>(clamp(exposure, 1e-4, 1e4), average, bright, 0.0);\n }\n}\n\n/** The image's own texel: its mean so far, or nothing where no path has landed yet. */\nfn imageTexel(x: i32, y: i32) -> vec3<f32> {\n let size = vec2<i32>(lens.resolution);\n let index = u32(clamp(y, 0, size.y - 1) * size.x + clamp(x, 0, size.x - 1));\n\n if (lens.denoised != 0u) {\n return denoised[index].rgb;\n }\n\n let stored = accumulation[index];\n\n return select(stored.rgb / stored.w, vec3<f32>(0.0), stored.w <= 0.0);\n}\n\nfn downTexel(offset: u32, size: vec2<u32>, x: i32, y: i32) -> vec3<f32> {\n let s = vec2<i32>(size);\n\n return down[offset + u32(clamp(y, 0, s.y - 1) * s.x + clamp(x, 0, s.x - 1))].rgb;\n}\n\nfn upTexel(offset: u32, size: vec2<u32>, x: i32, y: i32) -> vec3<f32> {\n let s = vec2<i32>(size);\n\n return up[offset + u32(clamp(y, 0, s.y - 1) * s.x + clamp(x, 0, s.x - 1))].rgb;\n}\n\n/** Bilinear weights and the texel below and left of a point given in texels, edges clamped. */\nfn bilinearAt(position: vec2<f32>) -> vec4<f32> {\n let p = position - 0.5;\n let base = floor(p);\n\n return vec4<f32>(base, p - base);\n}\n\nfn imageBilinear(position: vec2<f32>) -> vec3<f32> {\n let b = bilinearAt(position);\n let x = i32(b.x);\n let y = i32(b.y);\n\n return mix(\n mix(imageTexel(x, y), imageTexel(x + 1, y), b.z),\n mix(imageTexel(x, y + 1), imageTexel(x + 1, y + 1), b.z), b.w);\n}\n\nfn downBilinear(offset: u32, size: vec2<u32>, position: vec2<f32>) -> vec3<f32> {\n let b = bilinearAt(position);\n let x = i32(b.x);\n let y = i32(b.y);\n\n return mix(\n mix(downTexel(offset, size, x, y), downTexel(offset, size, x + 1, y), b.z),\n mix(downTexel(offset, size, x, y + 1), downTexel(offset, size, x + 1, y + 1), b.z), b.w);\n}\n\nfn upBilinear(offset: u32, size: vec2<u32>, position: vec2<f32>) -> vec3<f32> {\n let b = bilinearAt(position);\n let x = i32(b.x);\n let y = i32(b.y);\n\n return mix(\n mix(upTexel(offset, size, x, y), upTexel(offset, size, x + 1, y), b.z),\n mix(upTexel(offset, size, x, y + 1), upTexel(offset, size, x + 1, y + 1), b.z), b.w);\n}\n\n/** The level being filtered, read bilinearly at a point given in its own texels. */\nfn sourceAt(position: vec2<f32>) -> vec3<f32> {\n if (lens.fromImage != 0u) {\n return imageBilinear(position);\n }\n\n return downBilinear(lens.sourceOffset, lens.sourceSize, position);\n}\n\nfn luminance(color: vec3<f32>) -> f32 {\n return dot(color, vec3<f32>(0.2126, 0.7152, 0.0722));\n}\n\n/** Karis' average: a group of taps weighed by one over one plus its luminance, so a lone firefly cannot flare. */\nfn karis(sum: vec3<f32>) -> vec4<f32> {\n let weight = 1.0 / (1.0 + luminance(sum));\n\n return vec4<f32>(sum * weight, weight);\n}\n\n/** One level of the pyramid from the one above it: Jimenez's 13 taps, four overlapping boxes and a centre one. */\n@compute @workgroup_size(8, 8, 1)\nfn downsample(@builtin(global_invocation_id) id: vec3<u32>) {\n if (id.x >= lens.targetSize.x || id.y >= lens.targetSize.y) {\n return;\n }\n\n let scale = vec2<f32>(lens.sourceSize) / vec2<f32>(lens.targetSize);\n let centre = (vec2<f32>(id.xy) + 0.5) * scale;\n let a = sourceAt(centre + vec2<f32>(-2.0, -2.0));\n let b = sourceAt(centre + vec2<f32>(0.0, -2.0));\n let c = sourceAt(centre + vec2<f32>(2.0, -2.0));\n let d = sourceAt(centre + vec2<f32>(-2.0, 0.0));\n let e = sourceAt(centre);\n let f = sourceAt(centre + vec2<f32>(2.0, 0.0));\n let g = sourceAt(centre + vec2<f32>(-2.0, 2.0));\n let h = sourceAt(centre + vec2<f32>(0.0, 2.0));\n let i = sourceAt(centre + vec2<f32>(2.0, 2.0));\n let j = sourceAt(centre + vec2<f32>(-1.0, -1.0));\n let k = sourceAt(centre + vec2<f32>(1.0, -1.0));\n let l = sourceAt(centre + vec2<f32>(-1.0, 1.0));\n let m = sourceAt(centre + vec2<f32>(1.0, 1.0));\n var result: vec3<f32>;\n\n if (lens.fromImage != 0u) {\n let groups = karis((j + k + l + m) * 0.25) * 0.5\n + karis((a + b + d + e) * 0.25) * 0.125\n + karis((b + c + e + f) * 0.25) * 0.125\n + karis((d + e + g + h) * 0.25) * 0.125\n + karis((e + f + h + i) * 0.25) * 0.125;\n\n result = groups.rgb / max(groups.w, 1e-6);\n } else {\n result = e * 0.125 + (a + c + g + i) * 0.03125 + (b + d + f + h) * 0.0625 + (j + k + l + m) * 0.125;\n }\n\n down[lens.targetOffset + id.y * lens.targetSize.x + id.x] = vec4<f32>(max(result, vec3<f32>(0.0)), 1.0);\n}\n\n/** A 3 x 3 tent over the smaller level, read at a point given in its texels. */\nfn tent(position: vec2<f32>) -> vec3<f32> {\n var sum = vec3<f32>(0.0);\n\n for (var dy = -1; dy <= 1; dy = dy + 1) {\n for (var dx = -1; dx <= 1; dx = dx + 1) {\n let weight = f32((2 - abs(dx)) * (2 - abs(dy))) / 16.0;\n let at = position + vec2<f32>(f32(dx), f32(dy));\n\n if (lens.fromDown != 0u) {\n sum = sum + downBilinear(lens.sourceOffset, lens.sourceSize, at) * weight;\n } else {\n sum = sum + upBilinear(lens.sourceOffset, lens.sourceSize, at) * weight;\n }\n }\n }\n\n return sum;\n}\n\n/** One level folded back: its own light, mixed by the radius with the wider spread of every level below it. */\n@compute @workgroup_size(8, 8, 1)\nfn upsample(@builtin(global_invocation_id) id: vec3<u32>) {\n if (id.x >= lens.targetSize.x || id.y >= lens.targetSize.y) {\n return;\n }\n\n let scale = vec2<f32>(lens.sourceSize) / vec2<f32>(lens.targetSize);\n let wider = tent((vec2<f32>(id.xy) + 0.5) * scale);\n let index = lens.targetOffset + id.y * lens.targetSize.x + id.x;\n\n up[index] = vec4<f32>(mix(down[index].rgb, wider, lens.radius), 1.0);\n}\n\n/** The aperture's rays at one texel of the streak level: the light behind it along each ray, fading with distance. */\n@compute @workgroup_size(8, 8, 1)\nfn streaks(@builtin(global_invocation_id) id: vec3<u32>) {\n if (id.x >= lens.targetSize.x || id.y >= lens.targetSize.y) {\n return;\n }\n\n let centre = vec2<f32>(id.xy) + 0.5;\n // The exposure the image will have: the setting's, and the meter's when it meters.\n let exposure = lens.exposure * select(1.0, metered[0].x, lens.autoExposure > 0.0);\n var sum = vec3<f32>(0.0);\n var total = 0.0;\n\n for (var r = 0u; r < lens.rays; r = r + 1u) {\n let angle = lens.glareAngle + f32(r) * 2.0 * PI / f32(lens.rays);\n let along = vec2<f32>(cos(angle), sin(angle));\n\n for (var j = 1; j <= STREAK_TAPS; j = j + 1) {\n let t = f32(j) / f32(STREAK_TAPS);\n let weight = exp(-6.0 * t);\n\n let light = downBilinear(lens.sourceOffset, lens.sourceSize, centre - along * (t * lens.glareLength));\n\n sum = sum + light * (weight / max(1.0, luminance(light) * exposure / GLARE_CEILING));\n total = total + weight;\n }\n }\n\n streaked[id.y * lens.targetSize.x + id.x] = vec4<f32>(sum / max(total, 1e-6), 1.0);\n}\n\nfn streakTexel(x: i32, y: i32) -> vec3<f32> {\n let s = vec2<i32>(lens.streakSize);\n\n return streaked[u32(clamp(y, 0, s.y - 1) * s.x + clamp(x, 0, s.x - 1))].rgb;\n}\n\nfn streakBilinear(position: vec2<f32>) -> vec3<f32> {\n let b = bilinearAt(position);\n let x = i32(b.x);\n let y = i32(b.y);\n\n return mix(mix(streakTexel(x, y), streakTexel(x + 1, y), b.z), mix(streakTexel(x, y + 1), streakTexel(x + 1, y + 1), b.z), b.w);\n}\n\n/** The image as the lens delivers it: fringed towards the corners, with its halo, balanced for the camera's white. */\n@compute @workgroup_size(8, 8, 1)\nfn composite(@builtin(global_invocation_id) id: vec3<u32>) {\n if (id.x >= lens.resolution.x || id.y >= lens.resolution.y) {\n return;\n }\n\n let size = vec2<f32>(lens.resolution);\n let position = vec2<f32>(id.xy) + 0.5;\n var color = imageTexel(i32(id.x), i32(id.y));\n\n // Lateral chromatic aberration: each colour is magnified a little differently, red outwards and blue inwards,\n // in proportion to the distance from the centre, which is how a lens's dispersion moves them.\n if (lens.aberration > 0.0) {\n let centre = size * 0.5;\n let away = position - centre;\n let red = imageBilinear(centre + away * (1.0 - lens.aberration));\n let blue = imageBilinear(centre + away * (1.0 + lens.aberration));\n\n color = vec3<f32>(red.r, color.g, blue.b);\n }\n\n if (lens.intensity > 0.0) {\n // The largest folded level is already a tent over the levels below it, so one bilinear read of it is smooth.\n let halo = upBilinear(lens.haloOffset, lens.haloSize, position * vec2<f32>(lens.haloSize) / size);\n\n color = mix(color, halo, lens.intensity);\n }\n\n if (lens.glare > 0.0) {\n color = mix(color, streakBilinear(position * vec2<f32>(lens.streakSize) / size), lens.glare);\n }\n\n if (lens.autoExposure > 0.0) {\n color = color * metered[0].x;\n }\n\n let balanced = vec3<f32>(dot(lens.white0.xyz, color), dot(lens.white1.xyz, color), dot(lens.white2.xyz, color));\n\n graded[id.y * lens.resolution.x + id.x] = vec4<f32>(max(balanced, vec3<f32>(0.0)), 1.0);\n}\n";
17
17
  //# sourceMappingURL=lens.wgsl.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"lens.wgsl.d.ts","sourceRoot":"","sources":["../../src/wgsl/lens.wgsl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,SAAS,mpeAmZrB,CAAC"}
1
+ {"version":3,"file":"lens.wgsl.d.ts","sourceRoot":"","sources":["../../src/wgsl/lens.wgsl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,SAAS,kyfA+ZrB,CAAC"}
@@ -50,6 +50,8 @@ struct Lens {
50
50
  autoExposure: f32,
51
51
  // The share of a dark scene's darkness the meter keeps.
52
52
  darkAdaptation: f32,
53
+ // The exposure applied after the lens, the meter's aside, which the rays are limited against.
54
+ exposure: f32,
53
55
  };
54
56
 
55
57
  const PI: f32 = 3.14159265358979;
@@ -57,6 +59,12 @@ const PI: f32 = 3.14159265358979;
57
59
  /** Taps along each of the aperture's rays. */
58
60
  const STREAK_TAPS: i32 = 24;
59
61
 
62
+ /**
63
+ * The brightest a tap of the rays counts as, in multiples of the exposed white. A lamp is thousands of times white,
64
+ * which would carry its rays across the frame at full length; past this they stay as short as a camera shows them.
65
+ */
66
+ const GLARE_CEILING: f32 = 4.0;
67
+
60
68
  @group(0) @binding(0) var<uniform> lens: Lens;
61
69
  @group(0) @binding(1) var<storage, read> accumulation: array<vec4<f32>>;
62
70
  @group(0) @binding(2) var<storage, read> denoised: array<vec4<f32>>;
@@ -342,6 +350,8 @@ fn streaks(@builtin(global_invocation_id) id: vec3<u32>) {
342
350
  }
343
351
 
344
352
  let centre = vec2<f32>(id.xy) + 0.5;
353
+ // The exposure the image will have: the setting's, and the meter's when it meters.
354
+ let exposure = lens.exposure * select(1.0, metered[0].x, lens.autoExposure > 0.0);
345
355
  var sum = vec3<f32>(0.0);
346
356
  var total = 0.0;
347
357
 
@@ -351,9 +361,11 @@ fn streaks(@builtin(global_invocation_id) id: vec3<u32>) {
351
361
 
352
362
  for (var j = 1; j <= STREAK_TAPS; j = j + 1) {
353
363
  let t = f32(j) / f32(STREAK_TAPS);
354
- let weight = exp(-4.0 * t);
364
+ let weight = exp(-6.0 * t);
365
+
366
+ let light = downBilinear(lens.sourceOffset, lens.sourceSize, centre - along * (t * lens.glareLength));
355
367
 
356
- sum = sum + downBilinear(lens.sourceOffset, lens.sourceSize, centre - along * (t * lens.glareLength)) * weight;
368
+ sum = sum + light * (weight / max(1.0, luminance(light) * exposure / GLARE_CEILING));
357
369
  total = total + weight;
358
370
  }
359
371
  }
@@ -1 +1 @@
1
- {"version":3,"file":"lens.wgsl.js","sourceRoot":"","sources":["../../src/wgsl/lens.wgsl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,UAAU,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmZnC,CAAC"}
1
+ {"version":3,"file":"lens.wgsl.js","sourceRoot":"","sources":["../../src/wgsl/lens.wgsl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,UAAU,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+ZnC,CAAC"}