@driftengine/core 4.3.0 → 4.4.1

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 (218) hide show
  1. package/CHANGELOG.json +282 -0
  2. package/README.md +1 -1
  3. package/dist/cinematic/cameraPath.d.ts +53 -0
  4. package/dist/cinematic/cameraPath.js +118 -0
  5. package/dist/environment/daylight.d.ts +77 -0
  6. package/dist/environment/daylight.js +77 -0
  7. package/dist/geometry/meshConcat.d.ts +9 -0
  8. package/dist/geometry/meshConcat.js +101 -0
  9. package/dist/index.d.ts +12 -2
  10. package/dist/index.js +14 -1
  11. package/dist/render/ambientOcclusionPass.js +5 -1
  12. package/dist/render/backend/createRenderer.d.ts +13 -1
  13. package/dist/render/backend/createRenderer.js +6 -5
  14. package/dist/render/backend/webgl2/renderer.d.ts +147 -1
  15. package/dist/render/backend/webgl2/renderer.js +402 -23
  16. package/dist/render/backend/webgpu/buffers.js +46 -8
  17. package/dist/render/backend/webgpu/depthPass.d.ts +81 -3
  18. package/dist/render/backend/webgpu/depthPass.js +75 -14
  19. package/dist/render/backend/webgpu/exposurePass.d.ts +39 -0
  20. package/dist/render/backend/webgpu/exposurePass.js +190 -0
  21. package/dist/render/backend/webgpu/fieldCompose.d.ts +11 -0
  22. package/dist/render/backend/webgpu/fieldCompose.js +13 -0
  23. package/dist/render/backend/webgpu/flatPass.d.ts +3 -1
  24. package/dist/render/backend/webgpu/flatPass.js +12 -5
  25. package/dist/render/backend/webgpu/particlePass.d.ts +8 -0
  26. package/dist/render/backend/webgpu/particlePass.js +1 -0
  27. package/dist/render/backend/webgpu/postPass.d.ts +13 -3
  28. package/dist/render/backend/webgpu/probeBake.d.ts +59 -3
  29. package/dist/render/backend/webgpu/probeBake.js +97 -7
  30. package/dist/render/backend/webgpu/probeTargets.d.ts +65 -0
  31. package/dist/render/backend/webgpu/probeTargets.js +180 -0
  32. package/dist/render/backend/webgpu/renderer.d.ts +174 -11
  33. package/dist/render/backend/webgpu/renderer.js +682 -252
  34. package/dist/render/backend/webgpu/scatterPass.js +7 -7
  35. package/dist/render/backend/webgpu/shaders/clusterBin.wgsl.d.ts +1 -1
  36. package/dist/render/backend/webgpu/skinPaletteRing.d.ts +4 -0
  37. package/dist/render/backend/webgpu/skinPaletteRing.js +9 -0
  38. package/dist/render/backend/webgpu/skyPass.js +5 -1
  39. package/dist/render/backend/webgpu/uniformRing.d.ts +31 -1
  40. package/dist/render/backend/webgpu/uniformRing.js +53 -3
  41. package/dist/render/bloomChain.d.ts +6 -0
  42. package/dist/render/bloomChain.js +8 -0
  43. package/dist/render/clusteredLights.d.ts +19 -11
  44. package/dist/render/clusteredLights.js +96 -45
  45. package/dist/render/cutoutCaster.d.ts +23 -0
  46. package/dist/render/cutoutCaster.js +20 -0
  47. package/dist/render/driftLight/bake.d.ts +34 -0
  48. package/dist/render/driftLight/bake.js +95 -0
  49. package/dist/render/driftLight/falloff.d.ts +16 -0
  50. package/dist/render/driftLight/falloff.js +23 -0
  51. package/dist/render/driftLight/layout.d.ts +59 -0
  52. package/dist/render/driftLight/layout.js +138 -0
  53. package/dist/render/driftLight/lightField.d.ts +108 -0
  54. package/dist/render/driftLight/lightField.js +179 -0
  55. package/dist/render/driftLight/uniforms.d.ts +28 -0
  56. package/dist/render/driftLight/uniforms.js +41 -0
  57. package/dist/render/driftLight/visibility.d.ts +18 -0
  58. package/dist/render/driftLight/visibility.js +47 -0
  59. package/dist/render/emptyTexture.d.ts +6 -0
  60. package/dist/render/emptyTexture.js +19 -0
  61. package/dist/render/exposurePass.d.ts +48 -0
  62. package/dist/render/exposurePass.js +172 -0
  63. package/dist/render/flameLight.d.ts +71 -0
  64. package/dist/render/flameLight.js +63 -0
  65. package/dist/render/gi/bounceLights.d.ts +34 -0
  66. package/dist/render/gi/bounceLights.js +66 -0
  67. package/dist/render/gi/fieldReach.d.ts +12 -0
  68. package/dist/render/gi/fieldReach.js +62 -0
  69. package/dist/render/gi/globalField.d.ts +8 -0
  70. package/dist/render/gi/globalField.js +31 -0
  71. package/dist/render/gi/probeTrace.d.ts +3 -1
  72. package/dist/render/gi/probeTrace.js +3 -1
  73. package/dist/render/gpuCapability.js +19 -4
  74. package/dist/render/lightBudget.d.ts +11 -0
  75. package/dist/render/lightBudget.js +11 -0
  76. package/dist/render/pointLightSelection.d.ts +16 -0
  77. package/dist/render/pointLightSelection.js +178 -83
  78. package/dist/render/pointShadowBudget.d.ts +12 -2
  79. package/dist/render/pointShadowBudget.js +16 -10
  80. package/dist/render/probeGrid.d.ts +11 -1
  81. package/dist/render/probeGrid.js +5 -2
  82. package/dist/render/probeSweeps.d.ts +45 -0
  83. package/dist/render/probeSweeps.js +77 -0
  84. package/dist/render/reflectionProbe.d.ts +43 -1
  85. package/dist/render/reflectionProbe.js +17 -3
  86. package/dist/render/renderQuality.d.ts +3 -1
  87. package/dist/render/rendererHarness.d.ts +9 -0
  88. package/dist/render/rendererHarness.js +18 -3
  89. package/dist/render/sceneTarget.d.ts +20 -0
  90. package/dist/render/sceneTarget.js +62 -1
  91. package/dist/render/shaders/ambientOcclusion.d.ts +63 -6
  92. package/dist/render/shaders/ambientOcclusion.js +188 -71
  93. package/dist/render/shaders/cutoutCoverage.d.ts +31 -0
  94. package/dist/render/shaders/cutoutCoverage.js +40 -0
  95. package/dist/render/shaders/depth.d.ts +25 -0
  96. package/dist/render/shaders/depth.js +69 -3
  97. package/dist/render/shaders/exposure.d.ts +52 -0
  98. package/dist/render/shaders/exposure.js +124 -0
  99. package/dist/render/shaders/filmLook.d.ts +47 -0
  100. package/dist/render/shaders/filmLook.js +98 -0
  101. package/dist/render/shaders/flat/directionalShadow.js +35 -4
  102. package/dist/render/shaders/flat/driftLight.d.ts +29 -0
  103. package/dist/render/shaders/flat/driftLight.js +75 -0
  104. package/dist/render/shaders/flat/index.js +5 -0
  105. package/dist/render/shaders/flat/lobes.js +13 -0
  106. package/dist/render/shaders/flat/main.d.ts +1 -1
  107. package/dist/render/shaders/flat/main.js +68 -9
  108. package/dist/render/shaders/flat/preamble.js +23 -5
  109. package/dist/render/shaders/flat/probeGrid.js +46 -3
  110. package/dist/render/shaders/generated/ambientOcclusion.wgsl.d.ts +8 -3
  111. package/dist/render/shaders/generated/ambientOcclusion.wgsl.js +8 -3
  112. package/dist/render/shaders/generated/depth.wgsl.d.ts +100 -0
  113. package/dist/render/shaders/generated/depth.wgsl.js +100 -0
  114. package/dist/render/shaders/generated/exposure.wgsl.d.ts +42 -0
  115. package/dist/render/shaders/generated/exposure.wgsl.js +48 -0
  116. package/dist/render/shaders/generated/flat.wgsl.d.ts +1800 -1344
  117. package/dist/render/shaders/generated/flat.wgsl.js +1816 -1360
  118. package/dist/render/shaders/generated/localExposure.wgsl.d.ts +18 -0
  119. package/dist/render/shaders/generated/localExposure.wgsl.js +24 -0
  120. package/dist/render/shaders/generated/rush.wgsl.d.ts +40 -10
  121. package/dist/render/shaders/generated/rush.wgsl.js +40 -10
  122. package/dist/render/shaders/generated/temporalResolve.wgsl.d.ts +1 -1
  123. package/dist/render/shaders/generated/temporalResolve.wgsl.js +1 -1
  124. package/dist/render/shaders/gi/driftLight.wgsl.d.ts +16 -0
  125. package/dist/render/shaders/gi/driftLight.wgsl.js +49 -0
  126. package/dist/render/shaders/gi/probeBake.wgsl.d.ts +29 -4
  127. package/dist/render/shaders/gi/probeBake.wgsl.js +190 -20
  128. package/dist/render/shaders/globalMedium.js +4 -0
  129. package/dist/render/shaders/localExposure.d.ts +35 -0
  130. package/dist/render/shaders/localExposure.js +138 -0
  131. package/dist/render/shaders/prefilter.d.ts +1 -1
  132. package/dist/render/shaders/prefilter.js +6 -0
  133. package/dist/render/shaders/rush.js +33 -5
  134. package/dist/render/shaders/tangentFrame.d.ts +1 -1
  135. package/dist/render/shaders/tangentFrame.js +15 -3
  136. package/dist/render/shaders/temporalResolve.js +39 -1
  137. package/dist/render/shadowCasters.d.ts +5 -3
  138. package/dist/render/shadowMap.js +13 -0
  139. package/dist/render/surfaceTexture.d.ts +5 -0
  140. package/package.json +6 -6
  141. package/scripts/gpuCompute.mjs +24 -3
  142. package/scripts/png.mjs +23 -12
  143. package/src/cinematic/cameraPath.ts +178 -0
  144. package/src/environment/daylight.ts +152 -0
  145. package/src/geometry/meshConcat.ts +100 -0
  146. package/src/index.ts +32 -0
  147. package/src/render/ambientOcclusionPass.ts +5 -1
  148. package/src/render/backend/createRenderer.ts +22 -6
  149. package/src/render/backend/webgl2/renderer.ts +439 -20
  150. package/src/render/backend/webgpu/buffers.ts +51 -8
  151. package/src/render/backend/webgpu/depthPass.ts +78 -10
  152. package/src/render/backend/webgpu/exposurePass.ts +234 -0
  153. package/src/render/backend/webgpu/fieldCompose.ts +15 -0
  154. package/src/render/backend/webgpu/flatPass.ts +10 -2
  155. package/src/render/backend/webgpu/particlePass.ts +9 -0
  156. package/src/render/backend/webgpu/probeBake.ts +143 -9
  157. package/src/render/backend/webgpu/probeTargets.ts +211 -0
  158. package/src/render/backend/webgpu/renderer.ts +808 -273
  159. package/src/render/backend/webgpu/scatterPass.ts +7 -7
  160. package/src/render/backend/webgpu/skinPaletteRing.ts +10 -0
  161. package/src/render/backend/webgpu/skyPass.ts +5 -1
  162. package/src/render/backend/webgpu/uniformRing.ts +49 -3
  163. package/src/render/bloomChain.ts +9 -0
  164. package/src/render/clusteredLights.ts +108 -63
  165. package/src/render/cutoutCaster.ts +44 -0
  166. package/src/render/driftLight/bake.ts +125 -0
  167. package/src/render/driftLight/falloff.ts +26 -0
  168. package/src/render/driftLight/layout.ts +177 -0
  169. package/src/render/driftLight/lightField.ts +227 -0
  170. package/src/render/driftLight/uniforms.ts +61 -0
  171. package/src/render/driftLight/visibility.ts +58 -0
  172. package/src/render/emptyTexture.ts +41 -0
  173. package/src/render/exposurePass.ts +186 -0
  174. package/src/render/flameLight.ts +135 -0
  175. package/src/render/gi/bounceLights.ts +68 -0
  176. package/src/render/gi/fieldReach.ts +74 -0
  177. package/src/render/gi/globalField.ts +39 -0
  178. package/src/render/gi/probeTrace.ts +3 -1
  179. package/src/render/gpuCapability.ts +18 -5
  180. package/src/render/lightBudget.ts +12 -0
  181. package/src/render/pointLightSelection.ts +191 -79
  182. package/src/render/pointShadowBudget.ts +16 -8
  183. package/src/render/probeGrid.ts +13 -2
  184. package/src/render/probeSweeps.ts +80 -0
  185. package/src/render/reflectionProbe.ts +54 -2
  186. package/src/render/renderQuality.ts +3 -1
  187. package/src/render/rendererHarness.ts +25 -3
  188. package/src/render/sceneTarget.ts +75 -1
  189. package/src/render/shaders/ambientOcclusion.ts +205 -71
  190. package/src/render/shaders/cutoutCoverage.ts +43 -0
  191. package/src/render/shaders/depth.ts +80 -3
  192. package/src/render/shaders/exposure.ts +128 -0
  193. package/src/render/shaders/filmLook.ts +111 -0
  194. package/src/render/shaders/flat/directionalShadow.ts +35 -4
  195. package/src/render/shaders/flat/driftLight.ts +75 -0
  196. package/src/render/shaders/flat/index.ts +5 -0
  197. package/src/render/shaders/flat/lobes.ts +13 -0
  198. package/src/render/shaders/flat/main.ts +68 -9
  199. package/src/render/shaders/flat/preamble.ts +23 -5
  200. package/src/render/shaders/flat/probeGrid.ts +46 -3
  201. package/src/render/shaders/generated/ambientOcclusion.wgsl.ts +8 -3
  202. package/src/render/shaders/generated/depth.wgsl.ts +103 -0
  203. package/src/render/shaders/generated/exposure.wgsl.ts +51 -0
  204. package/src/render/shaders/generated/flat.wgsl.ts +1816 -1360
  205. package/src/render/shaders/generated/localExposure.wgsl.ts +26 -0
  206. package/src/render/shaders/generated/rush.wgsl.ts +40 -10
  207. package/src/render/shaders/generated/temporalResolve.wgsl.ts +1 -1
  208. package/src/render/shaders/gi/driftLight.wgsl.ts +49 -0
  209. package/src/render/shaders/gi/probeBake.wgsl.ts +191 -20
  210. package/src/render/shaders/globalMedium.ts +4 -0
  211. package/src/render/shaders/localExposure.ts +154 -0
  212. package/src/render/shaders/prefilter.ts +6 -0
  213. package/src/render/shaders/rush.ts +33 -5
  214. package/src/render/shaders/tangentFrame.ts +15 -3
  215. package/src/render/shaders/temporalResolve.ts +39 -1
  216. package/src/render/shadowCasters.ts +5 -3
  217. package/src/render/shadowMap.ts +14 -0
  218. package/src/render/surfaceTexture.ts +5 -0
@@ -0,0 +1,34 @@
1
+ /**
2
+ * One brick of DriftLight's volume: at every sample, the light that arrives from every light that
3
+ * reaches it, and the direction it mostly arrives from.
4
+ *
5
+ * **Two texels a sample.** The first is the summed colour, and in its fourth channel whether the
6
+ * sample is a sample at all: one standing inside solid geometry is not, and its zero lets the
7
+ * shader's filtered lookup leave it out rather than blend darkness into the wall beside it. The
8
+ * second is the arrival direction, weighted by each light's brightness, so it points at a lone light
9
+ * with a length of one and shrinks towards zero as lights surround the sample from all sides. The
10
+ * shader turns the pair back into the light on a surface of any facing: exactly Lambert for a single
11
+ * light, a quarter of the sum, the mean of a clamped cosine, for light from everywhere.
12
+ *
13
+ * **What it gives up is the specular.** Two numbers of direction cannot say where a highlight
14
+ * falls, which is why the exact lights near the camera are shaded exactly and this stands in only
15
+ * past them, where a highlight from a candle is a fraction of a pixel.
16
+ */
17
+ import type { PointLightFalloff } from './falloff.ts';
18
+ import type { LightFieldLayout, LightReach } from './layout.ts';
19
+ import type { DistanceAt } from './visibility.ts';
20
+ /** Samples in a brick, and so texels in each of its two blocks. */
21
+ export declare const BRICK_TEXELS: number;
22
+ /** What the bake reads of a light, which a `PointLightSource` already carries. */
23
+ export interface FieldLight extends LightReach {
24
+ readonly r: number;
25
+ readonly g: number;
26
+ readonly b: number;
27
+ /** The emitter's physical radius, which widens its penumbra. */
28
+ readonly sourceRadius: number;
29
+ }
30
+ /**
31
+ * Fill `light` and `direction`, `BRICK_TEXELS * 4` floats each, x fastest, for brick `brick`.
32
+ * `distance` is the scene's distance field, or null for no occlusion.
33
+ */
34
+ export declare function bakeBrick(layout: LightFieldLayout, brick: number, lights: readonly FieldLight[], falloff: PointLightFalloff, distance: DistanceAt | null, light: Float32Array, direction: Float32Array): void;
@@ -0,0 +1,95 @@
1
+ /**
2
+ * One brick of DriftLight's volume: at every sample, the light that arrives from every light that
3
+ * reaches it, and the direction it mostly arrives from.
4
+ *
5
+ * **Two texels a sample.** The first is the summed colour, and in its fourth channel whether the
6
+ * sample is a sample at all: one standing inside solid geometry is not, and its zero lets the
7
+ * shader's filtered lookup leave it out rather than blend darkness into the wall beside it. The
8
+ * second is the arrival direction, weighted by each light's brightness, so it points at a lone light
9
+ * with a length of one and shrinks towards zero as lights surround the sample from all sides. The
10
+ * shader turns the pair back into the light on a surface of any facing: exactly Lambert for a single
11
+ * light, a quarter of the sum, the mean of a clamped cosine, for light from everywhere.
12
+ *
13
+ * **What it gives up is the specular.** Two numbers of direction cannot say where a highlight
14
+ * falls, which is why the exact lights near the camera are shaded exactly and this stands in only
15
+ * past them, where a highlight from a candle is a fraction of a pixel.
16
+ */
17
+ import { pointLightShape } from './falloff.js';
18
+ import { BRICK_SAMPLES } from './layout.js';
19
+ import { softVisibility } from './visibility.js';
20
+ /** Samples in a brick, and so texels in each of its two blocks. */
21
+ export const BRICK_TEXELS = BRICK_SAMPLES * BRICK_SAMPLES * BRICK_SAMPLES;
22
+ /**
23
+ * Fill `light` and `direction`, `BRICK_TEXELS * 4` floats each, x fastest, for brick `brick`.
24
+ * `distance` is the scene's distance field, or null for no occlusion.
25
+ */
26
+ export function bakeBrick(layout, brick, lights, falloff, distance, light, direction) {
27
+ const { spacing, span, origin } = layout;
28
+ const bx = origin[0] + layout.brickCoords[brick * 3] * span;
29
+ const by = origin[1] + layout.brickCoords[brick * 3 + 1] * span;
30
+ const bz = origin[2] + layout.brickCoords[brick * 3 + 2] * span;
31
+ const first = layout.lightStart[brick];
32
+ const last = layout.lightStart[brick + 1];
33
+ for (let k = 0; k < BRICK_SAMPLES; k++) {
34
+ for (let j = 0; j < BRICK_SAMPLES; j++) {
35
+ for (let i = 0; i < BRICK_SAMPLES; i++) {
36
+ const texel = (i + BRICK_SAMPLES * (j + BRICK_SAMPLES * k)) * 4;
37
+ const px = bx + i * spacing;
38
+ const py = by + j * spacing;
39
+ const pz = bz + k * spacing;
40
+ light.fill(0, texel, texel + 4);
41
+ direction.fill(0, texel, texel + 4);
42
+ if (distance !== null && distance(px, py, pz) < 0)
43
+ continue;
44
+ let r = 0;
45
+ let g = 0;
46
+ let b = 0;
47
+ let towardX = 0;
48
+ let towardY = 0;
49
+ let towardZ = 0;
50
+ let weight = 0;
51
+ for (let listed = first; listed < last; listed++) {
52
+ const source = lights[layout.lights[listed]];
53
+ if (source === undefined)
54
+ continue;
55
+ const dx = source.x - px;
56
+ const dy = source.y - py;
57
+ const dz = source.z - pz;
58
+ const dist = Math.hypot(dx, dy, dz);
59
+ if (dist >= source.radius)
60
+ continue;
61
+ const shape = pointLightShape(dist, source.radius, falloff);
62
+ if (shape <= 0)
63
+ continue;
64
+ const seen = distance === null
65
+ ? 1
66
+ : softVisibility(distance, px, py, pz, source.x, source.y, source.z, source.sourceRadius);
67
+ if (seen <= 0)
68
+ continue;
69
+ const lr = source.r * shape * seen;
70
+ const lg = source.g * shape * seen;
71
+ const lb = source.b * shape * seen;
72
+ r += lr;
73
+ g += lg;
74
+ b += lb;
75
+ const bright = 0.2126 * lr + 0.7152 * lg + 0.0722 * lb;
76
+ if (dist > 1e-6 && bright > 0) {
77
+ towardX += (bright * dx) / dist;
78
+ towardY += (bright * dy) / dist;
79
+ towardZ += (bright * dz) / dist;
80
+ weight += bright;
81
+ }
82
+ }
83
+ light[texel] = r;
84
+ light[texel + 1] = g;
85
+ light[texel + 2] = b;
86
+ light[texel + 3] = 1;
87
+ if (weight > 0) {
88
+ direction[texel] = towardX / weight;
89
+ direction[texel + 1] = towardY / weight;
90
+ direction[texel + 2] = towardZ / weight;
91
+ }
92
+ }
93
+ }
94
+ }
95
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * How a point light's brightness falls with distance, as the lit shader draws it, for the light
3
+ * DriftLight sums into its volume.
4
+ *
5
+ * **One formula in two languages, tied by a test.** The frame shades a light in GLSL; the volume is
6
+ * summed in TypeScript. If they disagree the summed light and the exact light differ where they
7
+ * crossfade, and a seam follows the camera across every surface. `falloff.test.ts` reads the
8
+ * shader's lines and asserts these numbers against them.
9
+ *
10
+ * Both of the renderer's falloffs: `smooth`, the default, is a linear ramp squared, reaching zero at
11
+ * the radius; `inverseSquare` is physical, windowed so it still reaches zero at the radius, and
12
+ * floored at ten centimetres so a surface touching a light is not infinitely bright.
13
+ */
14
+ export type PointLightFalloff = 'smooth' | 'inverseSquare';
15
+ /** The shader's `shape` for an unshaped point light: no cone, no photometric profile. */
16
+ export declare function pointLightShape(dist: number, radius: number, falloff: PointLightFalloff): number;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * How a point light's brightness falls with distance, as the lit shader draws it, for the light
3
+ * DriftLight sums into its volume.
4
+ *
5
+ * **One formula in two languages, tied by a test.** The frame shades a light in GLSL; the volume is
6
+ * summed in TypeScript. If they disagree the summed light and the exact light differ where they
7
+ * crossfade, and a seam follows the camera across every surface. `falloff.test.ts` reads the
8
+ * shader's lines and asserts these numbers against them.
9
+ *
10
+ * Both of the renderer's falloffs: `smooth`, the default, is a linear ramp squared, reaching zero at
11
+ * the radius; `inverseSquare` is physical, windowed so it still reaches zero at the radius, and
12
+ * floored at ten centimetres so a surface touching a light is not infinitely bright.
13
+ */
14
+ /** The shader's `shape` for an unshaped point light: no cone, no photometric profile. */
15
+ export function pointLightShape(dist, radius, falloff) {
16
+ if (falloff === 'inverseSquare') {
17
+ const ratio = dist / Math.max(radius, 1e-4);
18
+ const window = Math.min(1, Math.max(0, 1 - ratio ** 4));
19
+ return (window * window) / Math.max(dist * dist, 0.01);
20
+ }
21
+ const linear = Math.min(1, Math.max(0, 1 - dist / radius));
22
+ return linear * linear;
23
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Where DriftLight's volume exists: the bricks some light reaches, the grid that finds them, and
3
+ * which lights reach each one.
4
+ *
5
+ * **Sparse, because light is.** A candle reaches a metre and a courtyard is forty, so a dense grid
6
+ * would be almost entirely zeros: a brick is kept only where a light's reach touches it, by the
7
+ * exact sphere-against-box test rather than the box-against-box one, which takes the corner bricks a
8
+ * sphere's box reaches and the sphere does not.
9
+ *
10
+ * **A brick is four samples a side and shares its faces with its neighbours**, so it spans three
11
+ * spacings and two bricks that meet agree on the samples between them. That is what lets the shader
12
+ * filter inside one brick with the hardware's trilinear lookup and never see a seam where it crosses
13
+ * into the next.
14
+ *
15
+ * **Numbered in the index's own order**, x fastest, so a layout depends on where the lights are and
16
+ * not on the order a scene listed them in: two loads of one scene produce one atlas.
17
+ *
18
+ * What it gives up: the index is dense over the lights' bounding box, so lights spread thinly over a
19
+ * kilometre would make a large, mostly empty grid. It refuses one past `MAX_INDEX_AXIS` bricks on
20
+ * any axis and says to make one field per region instead; what would change that is a hashed
21
+ * index, which costs the shader a probe loop.
22
+ */
23
+ /** Samples on each axis of a brick. The first and last are shared with the neighbour. */
24
+ export declare const BRICK_SAMPLES = 4;
25
+ /**
26
+ * Bricks on each axis of the index, which is a 3D texture: 256 is the least `MAX_3D_TEXTURE_SIZE`
27
+ * WebGL2 promises, and a limit on the cell count alone would pass a grid 300 bricks long and one
28
+ * wide that a device is free to refuse. At a metre a brick, a field 256 m on a side.
29
+ */
30
+ export declare const MAX_INDEX_AXIS = 256;
31
+ /** What the layout reads of a light: where it is and how far it reaches. */
32
+ export interface LightReach {
33
+ readonly x: number;
34
+ readonly y: number;
35
+ readonly z: number;
36
+ readonly radius: number;
37
+ }
38
+ export interface LightFieldLayout {
39
+ /** Metres between two samples. */
40
+ readonly spacing: number;
41
+ /** Metres a brick spans: three spacings. */
42
+ readonly span: number;
43
+ /** The world position of brick (0, 0, 0)'s first sample. */
44
+ readonly origin: readonly [number, number, number];
45
+ /** Bricks on each axis of the index. */
46
+ readonly dims: readonly [number, number, number];
47
+ /** Per index cell, x fastest: the brick's number plus one, or 0 where there is no brick. */
48
+ readonly index: Uint32Array;
49
+ /** How many bricks there are. */
50
+ readonly count: number;
51
+ /** Each brick's cell in the index, three integers a brick. */
52
+ readonly brickCoords: Int32Array;
53
+ /** Where brick `b`'s lights start in `lights`; `lightStart[b + 1]` is where they end. */
54
+ readonly lightStart: Uint32Array;
55
+ /** The lights reaching each brick, in the order the scene listed them. */
56
+ readonly lights: Uint32Array;
57
+ }
58
+ /** The bricks `lights` reach at `spacing` metres between samples. Built once; not a frame path. */
59
+ export declare function layoutLightField(lights: readonly LightReach[], spacing: number): LightFieldLayout;
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Where DriftLight's volume exists: the bricks some light reaches, the grid that finds them, and
3
+ * which lights reach each one.
4
+ *
5
+ * **Sparse, because light is.** A candle reaches a metre and a courtyard is forty, so a dense grid
6
+ * would be almost entirely zeros: a brick is kept only where a light's reach touches it, by the
7
+ * exact sphere-against-box test rather than the box-against-box one, which takes the corner bricks a
8
+ * sphere's box reaches and the sphere does not.
9
+ *
10
+ * **A brick is four samples a side and shares its faces with its neighbours**, so it spans three
11
+ * spacings and two bricks that meet agree on the samples between them. That is what lets the shader
12
+ * filter inside one brick with the hardware's trilinear lookup and never see a seam where it crosses
13
+ * into the next.
14
+ *
15
+ * **Numbered in the index's own order**, x fastest, so a layout depends on where the lights are and
16
+ * not on the order a scene listed them in: two loads of one scene produce one atlas.
17
+ *
18
+ * What it gives up: the index is dense over the lights' bounding box, so lights spread thinly over a
19
+ * kilometre would make a large, mostly empty grid. It refuses one past `MAX_INDEX_AXIS` bricks on
20
+ * any axis and says to make one field per region instead; what would change that is a hashed
21
+ * index, which costs the shader a probe loop.
22
+ */
23
+ /** Samples on each axis of a brick. The first and last are shared with the neighbour. */
24
+ export const BRICK_SAMPLES = 4;
25
+ /**
26
+ * Bricks on each axis of the index, which is a 3D texture: 256 is the least `MAX_3D_TEXTURE_SIZE`
27
+ * WebGL2 promises, and a limit on the cell count alone would pass a grid 300 bricks long and one
28
+ * wide that a device is free to refuse. At a metre a brick, a field 256 m on a side.
29
+ */
30
+ export const MAX_INDEX_AXIS = 256;
31
+ const EMPTY = {
32
+ spacing: 0,
33
+ span: 0,
34
+ origin: [0, 0, 0],
35
+ dims: [0, 0, 0],
36
+ index: new Uint32Array(0),
37
+ count: 0,
38
+ brickCoords: new Int32Array(0),
39
+ lightStart: new Uint32Array(1),
40
+ lights: new Uint32Array(0),
41
+ };
42
+ /** The bricks `lights` reach at `spacing` metres between samples. Built once; not a frame path. */
43
+ export function layoutLightField(lights, spacing) {
44
+ if (!(spacing > 0)) {
45
+ throw new Error(`layoutLightField: spacing is ${String(spacing)} and must be positive.`);
46
+ }
47
+ const span = spacing * (BRICK_SAMPLES - 1);
48
+ const min = [Infinity, Infinity, Infinity];
49
+ const max = [-Infinity, -Infinity, -Infinity];
50
+ let reaching = 0;
51
+ for (const light of lights) {
52
+ if (!(light.radius > 0))
53
+ continue;
54
+ reaching += 1;
55
+ const at = [light.x, light.y, light.z];
56
+ for (let axis = 0; axis < 3; axis++) {
57
+ min[axis] = Math.min(min[axis], at[axis] - light.radius);
58
+ max[axis] = Math.max(max[axis], at[axis] + light.radius);
59
+ }
60
+ }
61
+ if (reaching === 0)
62
+ return EMPTY;
63
+ const origin = min.map((value) => Math.floor(value / span) * span);
64
+ const dims = max.map((value, axis) => Math.floor((value - origin[axis]) / span) + 1);
65
+ if (dims.some((across) => across > MAX_INDEX_AXIS)) {
66
+ throw new Error(`layoutLightField: these lights span ${dims.join(' x ')} bricks against a limit of ` +
67
+ `${MAX_INDEX_AXIS} on each axis. Give each region of the world a field of its own, or a ` +
68
+ 'coarser spacing.');
69
+ }
70
+ const cells = dims[0] * dims[1] * dims[2];
71
+ /* Which cells a light reaches, visited twice: once to mark and count, once to list. */
72
+ const visit = (each) => {
73
+ lights.forEach((light, number) => {
74
+ if (!(light.radius > 0))
75
+ return;
76
+ const at = [light.x, light.y, light.z];
77
+ const low = [0, 0, 0];
78
+ const high = [0, 0, 0];
79
+ for (let axis = 0; axis < 3; axis++) {
80
+ const o = origin[axis];
81
+ const p = at[axis];
82
+ low[axis] = Math.max(0, Math.floor((p - light.radius - o) / span));
83
+ high[axis] = Math.min(dims[axis] - 1, Math.floor((p + light.radius - o) / span));
84
+ }
85
+ const reach2 = light.radius * light.radius;
86
+ for (let k = low[2]; k <= high[2]; k++) {
87
+ for (let j = low[1]; j <= high[1]; j++) {
88
+ for (let i = low[0]; i <= high[0]; i++) {
89
+ const cell = [i, j, k];
90
+ let d2 = 0;
91
+ for (let axis = 0; axis < 3; axis++) {
92
+ const lo = origin[axis] + cell[axis] * span;
93
+ const p = at[axis];
94
+ const out = Math.max(lo - p, 0, p - (lo + span));
95
+ d2 += out * out;
96
+ }
97
+ if (d2 <= reach2)
98
+ each(number, i + dims[0] * (j + dims[1] * k));
99
+ }
100
+ }
101
+ }
102
+ });
103
+ };
104
+ const index = new Uint32Array(cells);
105
+ let pairs = 0;
106
+ visit((_, cell) => {
107
+ index[cell] = 1;
108
+ pairs += 1;
109
+ });
110
+ let count = 0;
111
+ for (let cell = 0; cell < cells; cell++)
112
+ if (index[cell] !== 0)
113
+ index[cell] = ++count;
114
+ const brickCoords = new Int32Array(count * 3);
115
+ for (let cell = 0; cell < cells; cell++) {
116
+ const brick = index[cell] - 1;
117
+ if (brick < 0)
118
+ continue;
119
+ brickCoords[brick * 3] = cell % dims[0];
120
+ brickCoords[brick * 3 + 1] = Math.floor(cell / dims[0]) % dims[1];
121
+ brickCoords[brick * 3 + 2] = Math.floor(cell / (dims[0] * dims[1]));
122
+ }
123
+ const lightStart = new Uint32Array(count + 1);
124
+ visit((_, cell) => {
125
+ lightStart[index[cell]] = lightStart[index[cell]] + 1;
126
+ });
127
+ for (let brick = 0; brick < count; brick++) {
128
+ lightStart[brick + 1] = lightStart[brick + 1] + lightStart[brick];
129
+ }
130
+ const filled = lightStart.slice(0, count);
131
+ const listed = new Uint32Array(pairs);
132
+ visit((light, cell) => {
133
+ const brick = index[cell] - 1;
134
+ listed[filled[brick]] = light;
135
+ filled[brick] = filled[brick] + 1;
136
+ });
137
+ return { spacing, span, origin, dims, index, count, brickCoords, lightStart, lights: listed };
138
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * DriftLight: a scene's many fixed lights summed into one volume, which stands in for them wherever
3
+ * they are not shaded one by one.
4
+ *
5
+ * **The problem it answers.** A frame shades a few hundred lights at most, chosen nearest the
6
+ * camera, and a scene of thousands of candles left every one past that choice dark: a wall with a
7
+ * dozen candles beside it lit nothing until the camera walked up to it. This sums every fixed light
8
+ * once, occluded by the scene's distance field, into sparse bricks of light and direction; the lit
9
+ * shader shades exactly where the frame's choice is complete (`PointLightBuffer.complete`) and reads
10
+ * the volume past it, crossfading across a band so nothing pops. A light is never counted twice:
11
+ * the split is per pixel, and the field's own lights carry a mark (`inLightField`) the selection
12
+ * turns into the sign of their weight.
13
+ *
14
+ * **Baked a few bricks a step, by the scene.** `bake(bricks)` does the number asked, so a scene paces
15
+ * it as it paces anything else it builds behind a loading screen, and the build is deterministic:
16
+ * the engine reads no clock. The volume is not summed into the frame until every brick has landed,
17
+ * then fades in over `fadeSec`, so a field completing in view does not pop; a field baked behind a
18
+ * loading screen asks for no fade.
19
+ *
20
+ * **The lights are summed as they stood when the field was made**, from a copy: a scene dimming its
21
+ * lamps at dawn rewrites the very objects it gave the field, and a bake paced into the morning
22
+ * would otherwise sum lamps already out. `scale` dims what was summed.
23
+ *
24
+ * What it gives up is stated where each part does it: specular from summed lights (`bake.ts`),
25
+ * flicker past the choice (the volume holds each light's steady colour), and occlusion by anything
26
+ * that moves (only the fields given at construction shadow it).
27
+ */
28
+ import type { GlobalFieldInstance } from '../gi/globalField.ts';
29
+ import type { FieldLight } from './bake.ts';
30
+ import type { PointLightFalloff } from './falloff.ts';
31
+ import type { LightFieldLayout } from './layout.ts';
32
+ /** Seconds the summed light takes to fade in once the field is whole. */
33
+ export declare const FIELD_FADE_SEC = 0.5;
34
+ /** A light a field sums: a `PointLightSource`, whose `inLightField` the field sets. */
35
+ export interface LightFieldSource extends FieldLight {
36
+ inLightField?: boolean;
37
+ }
38
+ export interface LightFieldOptions {
39
+ /** How the renderer shades a point light, which the sum has to match. */
40
+ readonly falloff: PointLightFalloff;
41
+ /** Metres between samples; a third of a metre by default, so a brick spans a metre. */
42
+ readonly spacing?: number;
43
+ /** The scene's placed distance fields, which occlude the light. None leaves it unoccluded. */
44
+ readonly fields?: readonly GlobalFieldInstance[];
45
+ /** Metres over which exact and summed light crossfade at the shading radius. Two by default. */
46
+ readonly band?: number;
47
+ /**
48
+ * Seconds the summed light fades in over once the field is whole, `FIELD_FADE_SEC` by default.
49
+ * Zero for a field baked behind a loading screen, which has nothing on screen to pop and may be
50
+ * stepped by a clock that has stopped.
51
+ */
52
+ readonly fadeSec?: number;
53
+ }
54
+ export declare class LightField {
55
+ readonly layout: LightFieldLayout;
56
+ /** Texels on each axis of the atlas. */
57
+ readonly atlasSize: readonly [number, number, number];
58
+ /** Bricks on each axis of the atlas, which the shader needs to find a brick's corner. */
59
+ readonly atlasBricks: readonly [number, number, number];
60
+ /** The atlas as half floats, four channels a texel, x fastest. */
61
+ readonly atlas: Uint16Array;
62
+ /** Metres over which exact and summed light crossfade. */
63
+ readonly band: number;
64
+ /** Whether a distance field occludes this field's light. */
65
+ readonly occluded: boolean;
66
+ /** Seconds the summed light fades in over once whole; zero is at once. */
67
+ readonly fadeSec: number;
68
+ /** The summed light's brightness, for a scene that dims all its lamps at once: dawn, a switch. */
69
+ scale: number;
70
+ /** Where the frame's choice was centred, which the shader measures the radius from. */
71
+ readonly centre: Float32Array<ArrayBuffer>;
72
+ private readonly lights;
73
+ private readonly falloff;
74
+ private readonly distance;
75
+ private readonly lightBlock;
76
+ private readonly directionBlock;
77
+ private baked;
78
+ private taken;
79
+ private eased;
80
+ private shown;
81
+ constructor(lights: readonly LightFieldSource[], options: LightFieldOptions);
82
+ /** Whether every brick has been baked. */
83
+ get ready(): boolean;
84
+ /** How much of the bake is done, 0 to 1, for a loading screen. */
85
+ get progress(): number;
86
+ /** The radius, in metres from `centre`, inside which lights are shaded exactly. */
87
+ get radius(): number;
88
+ /** How much of the summed light is in the frame, 0 until the field is whole, then up to 1. */
89
+ get presence(): number;
90
+ /** The texel at which brick `brick`'s light block starts; its direction block is 4 along x. */
91
+ atlasCorner(brick: number): [number, number, number];
92
+ /** Bake up to `bricks` more bricks into the atlas, and answer how many were baked. */
93
+ bake(bricks: number): number;
94
+ /** The bricks baked since the last call, for a backend to upload. */
95
+ takeBaked(): {
96
+ from: number;
97
+ to: number;
98
+ };
99
+ /**
100
+ * Follow the frame's choice: `complete` from the selection, centred at (x, y, z). The radius
101
+ * shrinks at once, because a pixel inside it that a left-out light reaches would be shaded
102
+ * short, and grows back slowly, because a pixel it has not reached yet takes the summed light,
103
+ * which is only less exact. While nothing is summed the radius simply follows.
104
+ */
105
+ follow(complete: number, x: number, y: number, z: number, dtSec: number): void;
106
+ /** Write the brick just baked into its place in the atlas, as half floats. */
107
+ private place;
108
+ }
@@ -0,0 +1,179 @@
1
+ /**
2
+ * DriftLight: a scene's many fixed lights summed into one volume, which stands in for them wherever
3
+ * they are not shaded one by one.
4
+ *
5
+ * **The problem it answers.** A frame shades a few hundred lights at most, chosen nearest the
6
+ * camera, and a scene of thousands of candles left every one past that choice dark: a wall with a
7
+ * dozen candles beside it lit nothing until the camera walked up to it. This sums every fixed light
8
+ * once, occluded by the scene's distance field, into sparse bricks of light and direction; the lit
9
+ * shader shades exactly where the frame's choice is complete (`PointLightBuffer.complete`) and reads
10
+ * the volume past it, crossfading across a band so nothing pops. A light is never counted twice:
11
+ * the split is per pixel, and the field's own lights carry a mark (`inLightField`) the selection
12
+ * turns into the sign of their weight.
13
+ *
14
+ * **Baked a few bricks a step, by the scene.** `bake(bricks)` does the number asked, so a scene paces
15
+ * it as it paces anything else it builds behind a loading screen, and the build is deterministic:
16
+ * the engine reads no clock. The volume is not summed into the frame until every brick has landed,
17
+ * then fades in over `fadeSec`, so a field completing in view does not pop; a field baked behind a
18
+ * loading screen asks for no fade.
19
+ *
20
+ * **The lights are summed as they stood when the field was made**, from a copy: a scene dimming its
21
+ * lamps at dawn rewrites the very objects it gave the field, and a bake paced into the morning
22
+ * would otherwise sum lamps already out. `scale` dims what was summed.
23
+ *
24
+ * What it gives up is stated where each part does it: specular from summed lights (`bake.ts`),
25
+ * flicker past the choice (the volume holds each light's steady colour), and occlusion by anything
26
+ * that moves (only the fields given at construction shadow it).
27
+ */
28
+ import { distanceToInstances } from '../gi/globalField.js';
29
+ import { halfBits } from '../halfFloat.js';
30
+ import { BRICK_TEXELS, bakeBrick } from './bake.js';
31
+ import { BRICK_SAMPLES, layoutLightField } from './layout.js';
32
+ /** Seconds the summed light takes to fade in once the field is whole. */
33
+ export const FIELD_FADE_SEC = 0.5;
34
+ /** How fast the shading radius grows back, per second, when the choice reaches further again. */
35
+ const RADIUS_GROWTH_PER_SEC = 2;
36
+ /** Bricks along each axis of the atlas: 256 texels a side, the least 3D texture WebGL2 promises. */
37
+ const ATLAS_BRICKS = [32, 64, 64];
38
+ /** A brick's footprint in the atlas: its light block and its direction block side by side. */
39
+ const BRICK_WIDTH = BRICK_SAMPLES * 2;
40
+ export class LightField {
41
+ layout;
42
+ /** Texels on each axis of the atlas. */
43
+ atlasSize;
44
+ /** Bricks on each axis of the atlas, which the shader needs to find a brick's corner. */
45
+ atlasBricks;
46
+ /** The atlas as half floats, four channels a texel, x fastest. */
47
+ atlas;
48
+ /** Metres over which exact and summed light crossfade. */
49
+ band;
50
+ /** Whether a distance field occludes this field's light. */
51
+ occluded;
52
+ /** Seconds the summed light fades in over once whole; zero is at once. */
53
+ fadeSec;
54
+ /** The summed light's brightness, for a scene that dims all its lamps at once: dawn, a switch. */
55
+ scale = 1;
56
+ /** Where the frame's choice was centred, which the shader measures the radius from. */
57
+ centre = new Float32Array(3);
58
+ lights;
59
+ falloff;
60
+ distance;
61
+ lightBlock = new Float32Array(BRICK_TEXELS * 4);
62
+ directionBlock = new Float32Array(BRICK_TEXELS * 4);
63
+ baked = 0;
64
+ taken = 0;
65
+ eased = 0;
66
+ shown = 0;
67
+ constructor(lights, options) {
68
+ for (const light of lights)
69
+ light.inLightField = true;
70
+ this.lights = lights.map(({ x, y, z, radius, r, g, b, sourceRadius }) => ({
71
+ x,
72
+ y,
73
+ z,
74
+ radius,
75
+ r,
76
+ g,
77
+ b,
78
+ sourceRadius,
79
+ }));
80
+ this.falloff = options.falloff;
81
+ this.band = options.band ?? 2;
82
+ this.fadeSec = Math.max(0, options.fadeSec ?? FIELD_FADE_SEC);
83
+ const fields = options.fields ?? [];
84
+ this.occluded = fields.length > 0;
85
+ this.distance = this.occluded ? distanceToInstances(fields) : null;
86
+ this.layout = layoutLightField(lights, options.spacing ?? 1 / 3);
87
+ const count = this.layout.count;
88
+ const capacity = ATLAS_BRICKS[0] * ATLAS_BRICKS[1] * ATLAS_BRICKS[2];
89
+ if (count > capacity) {
90
+ throw new Error(`LightField: these lights reach ${count} bricks and one field holds ${capacity}. ` +
91
+ 'Give each region of the world a field of its own, or a coarser spacing.');
92
+ }
93
+ const ax = Math.max(1, Math.min(count, ATLAS_BRICKS[0]));
94
+ const ay = Math.max(1, Math.min(Math.ceil(count / ax), ATLAS_BRICKS[1]));
95
+ const az = Math.max(1, Math.ceil(count / (ax * ay)));
96
+ this.atlasBricks = [ax, ay, az];
97
+ this.atlasSize = [ax * BRICK_WIDTH, ay * BRICK_SAMPLES, az * BRICK_SAMPLES];
98
+ this.atlas = new Uint16Array(this.atlasSize[0] * this.atlasSize[1] * this.atlasSize[2] * 4);
99
+ }
100
+ /** Whether every brick has been baked. */
101
+ get ready() {
102
+ return this.baked >= this.layout.count;
103
+ }
104
+ /** How much of the bake is done, 0 to 1, for a loading screen. */
105
+ get progress() {
106
+ return this.layout.count === 0 ? 1 : this.baked / this.layout.count;
107
+ }
108
+ /** The radius, in metres from `centre`, inside which lights are shaded exactly. */
109
+ get radius() {
110
+ return this.eased;
111
+ }
112
+ /** How much of the summed light is in the frame, 0 until the field is whole, then up to 1. */
113
+ get presence() {
114
+ return this.shown;
115
+ }
116
+ /** The texel at which brick `brick`'s light block starts; its direction block is 4 along x. */
117
+ atlasCorner(brick) {
118
+ const [ax, ay] = this.atlasBricks;
119
+ return [
120
+ (brick % ax) * BRICK_WIDTH,
121
+ (Math.floor(brick / ax) % ay) * BRICK_SAMPLES,
122
+ Math.floor(brick / (ax * ay)) * BRICK_SAMPLES,
123
+ ];
124
+ }
125
+ /** Bake up to `bricks` more bricks into the atlas, and answer how many were baked. */
126
+ bake(bricks) {
127
+ const end = Math.min(this.layout.count, this.baked + Math.max(0, Math.floor(bricks)));
128
+ const done = end - this.baked;
129
+ for (let brick = this.baked; brick < end; brick++) {
130
+ bakeBrick(this.layout, brick, this.lights, this.falloff, this.distance, this.lightBlock, this.directionBlock);
131
+ this.place(brick);
132
+ }
133
+ this.baked = end;
134
+ return done;
135
+ }
136
+ /** The bricks baked since the last call, for a backend to upload. */
137
+ takeBaked() {
138
+ const range = { from: this.taken, to: this.baked };
139
+ this.taken = this.baked;
140
+ return range;
141
+ }
142
+ /**
143
+ * Follow the frame's choice: `complete` from the selection, centred at (x, y, z). The radius
144
+ * shrinks at once, because a pixel inside it that a left-out light reaches would be shaded
145
+ * short, and grows back slowly, because a pixel it has not reached yet takes the summed light,
146
+ * which is only less exact. While nothing is summed the radius simply follows.
147
+ */
148
+ follow(complete, x, y, z, dtSec) {
149
+ this.centre[0] = x;
150
+ this.centre[1] = y;
151
+ this.centre[2] = z;
152
+ const showing = this.shown > 0;
153
+ if (!showing || complete < this.eased)
154
+ this.eased = complete;
155
+ else
156
+ this.eased += (complete - this.eased) * (1 - Math.exp(-RADIUS_GROWTH_PER_SEC * dtSec));
157
+ if (this.ready) {
158
+ this.shown = this.fadeSec > 0 ? Math.min(1, this.shown + dtSec / this.fadeSec) : 1;
159
+ }
160
+ }
161
+ /** Write the brick just baked into its place in the atlas, as half floats. */
162
+ place(brick) {
163
+ const [x0, y0, z0] = this.atlasCorner(brick);
164
+ const [w, h] = this.atlasSize;
165
+ for (let k = 0; k < BRICK_SAMPLES; k++) {
166
+ for (let j = 0; j < BRICK_SAMPLES; j++) {
167
+ for (let i = 0; i < BRICK_SAMPLES; i++) {
168
+ const from = (i + BRICK_SAMPLES * (j + BRICK_SAMPLES * k)) * 4;
169
+ const light = (((z0 + k) * h + (y0 + j)) * w + (x0 + i)) * 4;
170
+ const direction = light + BRICK_SAMPLES * 4;
171
+ for (let c = 0; c < 4; c++) {
172
+ this.atlas[light + c] = halfBits(this.lightBlock[from + c]);
173
+ this.atlas[direction + c] = halfBits(this.directionBlock[from + c]);
174
+ }
175
+ }
176
+ }
177
+ }
178
+ }
179
+ }