@driftengine/capture 4.0.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 (199) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +29 -0
  3. package/README.md +68 -0
  4. package/dist/browserFrames.d.ts +31 -0
  5. package/dist/browserFrames.js +105 -0
  6. package/dist/collide.d.ts +59 -0
  7. package/dist/collide.js +92 -0
  8. package/dist/decimate.d.ts +42 -0
  9. package/dist/decimate.js +439 -0
  10. package/dist/delight.d.ts +42 -0
  11. package/dist/delight.js +351 -0
  12. package/dist/delightScene.d.ts +37 -0
  13. package/dist/delightScene.js +183 -0
  14. package/dist/depth.d.ts +44 -0
  15. package/dist/depth.js +53 -0
  16. package/dist/features.d.ts +33 -0
  17. package/dist/features.js +260 -0
  18. package/dist/frames.d.ts +47 -0
  19. package/dist/frames.js +87 -0
  20. package/dist/fusion.d.ts +77 -0
  21. package/dist/fusion.js +152 -0
  22. package/dist/gaussians/adam.d.ts +57 -0
  23. package/dist/gaussians/adam.js +88 -0
  24. package/dist/gaussians/depth.d.ts +8 -0
  25. package/dist/gaussians/depth.js +73 -0
  26. package/dist/gaussians/gradients.d.ts +36 -0
  27. package/dist/gaussians/gradients.js +228 -0
  28. package/dist/gaussians/harmonics.d.ts +46 -0
  29. package/dist/gaussians/harmonics.js +99 -0
  30. package/dist/gaussians/optimise.d.ts +50 -0
  31. package/dist/gaussians/optimise.js +125 -0
  32. package/dist/gaussians/parameters.d.ts +35 -0
  33. package/dist/gaussians/parameters.js +165 -0
  34. package/dist/gaussians/project.d.ts +72 -0
  35. package/dist/gaussians/project.js +137 -0
  36. package/dist/gaussians/projectGradient.d.ts +15 -0
  37. package/dist/gaussians/projectGradient.js +213 -0
  38. package/dist/gaussians/rasterise.d.ts +13 -0
  39. package/dist/gaussians/rasterise.js +65 -0
  40. package/dist/gaussians/refine.d.ts +36 -0
  41. package/dist/gaussians/refine.js +136 -0
  42. package/dist/gaussians/similarity.d.ts +25 -0
  43. package/dist/gaussians/similarity.js +80 -0
  44. package/dist/index.d.ts +101 -0
  45. package/dist/index.js +96 -0
  46. package/dist/marching.d.ts +38 -0
  47. package/dist/marching.js +258 -0
  48. package/dist/masks.d.ts +59 -0
  49. package/dist/masks.js +165 -0
  50. package/dist/math/dense.d.ts +68 -0
  51. package/dist/math/dense.js +294 -0
  52. package/dist/math/schur.d.ts +23 -0
  53. package/dist/math/schur.js +140 -0
  54. package/dist/models/batchNorm.d.ts +17 -0
  55. package/dist/models/batchNorm.js +24 -0
  56. package/dist/models/clipTokenizer.d.ts +40 -0
  57. package/dist/models/clipTokenizer.js +122 -0
  58. package/dist/models/clipTower.d.ts +27 -0
  59. package/dist/models/clipTower.js +45 -0
  60. package/dist/models/depthAnything2.d.ts +38 -0
  61. package/dist/models/depthAnything2.js +163 -0
  62. package/dist/models/depthAnything3.d.ts +26 -0
  63. package/dist/models/depthAnything3.js +171 -0
  64. package/dist/models/dualDpt.d.ts +19 -0
  65. package/dist/models/dualDpt.js +138 -0
  66. package/dist/models/hiera.d.ts +49 -0
  67. package/dist/models/hiera.js +120 -0
  68. package/dist/models/hieraAttention.d.ts +16 -0
  69. package/dist/models/hieraAttention.js +68 -0
  70. package/dist/models/miniature.d.ts +36 -0
  71. package/dist/models/miniature.js +216 -0
  72. package/dist/models/mobileSam.d.ts +41 -0
  73. package/dist/models/mobileSam.js +88 -0
  74. package/dist/models/owlv2.d.ts +49 -0
  75. package/dist/models/owlv2.js +131 -0
  76. package/dist/models/owlv2Detect.d.ts +25 -0
  77. package/dist/models/owlv2Detect.js +80 -0
  78. package/dist/models/owlv2Miniature.d.ts +27 -0
  79. package/dist/models/owlv2Miniature.js +87 -0
  80. package/dist/models/rope.d.ts +23 -0
  81. package/dist/models/rope.js +84 -0
  82. package/dist/models/sam21.d.ts +45 -0
  83. package/dist/models/sam21.js +126 -0
  84. package/dist/models/sam21Miniature.d.ts +37 -0
  85. package/dist/models/sam21Miniature.js +194 -0
  86. package/dist/models/sam21Video.d.ts +30 -0
  87. package/dist/models/sam21Video.js +101 -0
  88. package/dist/models/sam2Choice.d.ts +19 -0
  89. package/dist/models/sam2Choice.js +55 -0
  90. package/dist/models/sam2Decoder.d.ts +36 -0
  91. package/dist/models/sam2Decoder.js +108 -0
  92. package/dist/models/sam2Memories.d.ts +54 -0
  93. package/dist/models/sam2Memories.js +93 -0
  94. package/dist/models/sam2Memory.d.ts +30 -0
  95. package/dist/models/sam2Memory.js +136 -0
  96. package/dist/models/sam2Positions.d.ts +24 -0
  97. package/dist/models/sam2Positions.js +94 -0
  98. package/dist/models/sam2Tracker.d.ts +69 -0
  99. package/dist/models/sam2Tracker.js +172 -0
  100. package/dist/models/samDecoder.d.ts +45 -0
  101. package/dist/models/samDecoder.js +96 -0
  102. package/dist/models/samMiniature.d.ts +43 -0
  103. package/dist/models/samMiniature.js +161 -0
  104. package/dist/models/samPrompt.d.ts +57 -0
  105. package/dist/models/samPrompt.js +160 -0
  106. package/dist/models/seeded.d.ts +16 -0
  107. package/dist/models/seeded.js +48 -0
  108. package/dist/models/tinyVit.d.ts +38 -0
  109. package/dist/models/tinyVit.js +82 -0
  110. package/dist/models/twoWay.d.ts +46 -0
  111. package/dist/models/twoWay.js +67 -0
  112. package/dist/models/vit.d.ts +45 -0
  113. package/dist/models/vit.js +157 -0
  114. package/dist/models/windowAttention.d.ts +25 -0
  115. package/dist/models/windowAttention.js +104 -0
  116. package/dist/poses.d.ts +31 -0
  117. package/dist/poses.js +750 -0
  118. package/dist/prepare.d.ts +30 -0
  119. package/dist/prepare.js +238 -0
  120. package/dist/propose.d.ts +36 -0
  121. package/dist/propose.js +63 -0
  122. package/dist/resample.d.ts +39 -0
  123. package/dist/resample.js +165 -0
  124. package/dist/run.d.ts +3 -0
  125. package/dist/run.js +1 -0
  126. package/dist/scene.d.ts +104 -0
  127. package/dist/scene.js +109 -0
  128. package/dist/segment.d.ts +40 -0
  129. package/dist/segment.js +184 -0
  130. package/dist/testScene.d.ts +55 -0
  131. package/dist/testScene.js +214 -0
  132. package/dist/twoView.d.ts +22 -0
  133. package/dist/twoView.js +435 -0
  134. package/package.json +63 -0
  135. package/src/browserFrames.ts +145 -0
  136. package/src/collide.ts +128 -0
  137. package/src/decimate.ts +510 -0
  138. package/src/delight.ts +427 -0
  139. package/src/delightScene.ts +249 -0
  140. package/src/depth.ts +100 -0
  141. package/src/features.ts +302 -0
  142. package/src/frames.ts +111 -0
  143. package/src/fusion.ts +229 -0
  144. package/src/gaussians/adam.ts +131 -0
  145. package/src/gaussians/depth.ts +87 -0
  146. package/src/gaussians/gradients.ts +285 -0
  147. package/src/gaussians/harmonics.ts +122 -0
  148. package/src/gaussians/optimise.ts +191 -0
  149. package/src/gaussians/parameters.ts +186 -0
  150. package/src/gaussians/project.ts +207 -0
  151. package/src/gaussians/projectGradient.ts +234 -0
  152. package/src/gaussians/rasterise.ts +81 -0
  153. package/src/gaussians/refine.ts +176 -0
  154. package/src/gaussians/similarity.ts +87 -0
  155. package/src/index.ts +225 -0
  156. package/src/marching.ts +358 -0
  157. package/src/masks.ts +201 -0
  158. package/src/math/dense.ts +317 -0
  159. package/src/math/schur.ts +167 -0
  160. package/src/models/batchNorm.ts +57 -0
  161. package/src/models/clipTokenizer.ts +129 -0
  162. package/src/models/clipTower.ts +87 -0
  163. package/src/models/depthAnything2.ts +276 -0
  164. package/src/models/depthAnything3.ts +229 -0
  165. package/src/models/dualDpt.ts +233 -0
  166. package/src/models/hiera.ts +231 -0
  167. package/src/models/hieraAttention.ts +121 -0
  168. package/src/models/miniature.ts +241 -0
  169. package/src/models/mobileSam.ts +135 -0
  170. package/src/models/owlv2.ts +190 -0
  171. package/src/models/owlv2Detect.ts +116 -0
  172. package/src/models/owlv2Miniature.ts +108 -0
  173. package/src/models/rope.ts +111 -0
  174. package/src/models/sam21.ts +182 -0
  175. package/src/models/sam21Miniature.ts +225 -0
  176. package/src/models/sam21Video.ts +154 -0
  177. package/src/models/sam2Choice.ts +62 -0
  178. package/src/models/sam2Decoder.ts +218 -0
  179. package/src/models/sam2Memories.ts +165 -0
  180. package/src/models/sam2Memory.ts +247 -0
  181. package/src/models/sam2Positions.ts +106 -0
  182. package/src/models/sam2Tracker.ts +300 -0
  183. package/src/models/samDecoder.ts +225 -0
  184. package/src/models/samMiniature.ts +190 -0
  185. package/src/models/samPrompt.ts +227 -0
  186. package/src/models/seeded.ts +50 -0
  187. package/src/models/tinyVit.ts +178 -0
  188. package/src/models/twoWay.ts +122 -0
  189. package/src/models/vit.ts +265 -0
  190. package/src/models/windowAttention.ts +155 -0
  191. package/src/poses.ts +894 -0
  192. package/src/prepare.ts +306 -0
  193. package/src/propose.ts +96 -0
  194. package/src/resample.ts +197 -0
  195. package/src/run.ts +7 -0
  196. package/src/scene.ts +141 -0
  197. package/src/segment.ts +220 -0
  198. package/src/testScene.ts +283 -0
  199. package/src/twoView.ts +536 -0
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Adam over a cloud's parameters, one moment pair per number.
3
+ *
4
+ * **Why Adam and not a plain descent.** The six families being fitted are in wildly different
5
+ * units — metres, logarithms of metres, a quaternion, a colour on nought to one — and a single
6
+ * step size that suits one of them moves another by nothing or by everything. Adam divides each
7
+ * number's step by the size of its own recent gradients, so a rate here is a *fraction of the
8
+ * parameter's own scale* and the families become comparable. It is also what makes the loss's
9
+ * absolute size irrelevant: a frame of a million pixels and one of a thousand descend alike.
10
+ *
11
+ * **The bias correction is a running product rather than a power.** `β₁ⁿ` would want `**`, which
12
+ * `packages/capture` may not use — the determinism gate refuses it, because an exponent is one of
13
+ * the operations a platform may round its own way. Multiplying by β each step is the same number
14
+ * by the same arithmetic the rest of the fit uses.
15
+ */
16
+ /**
17
+ * One optimised family: the values, their gradient, and Adam's two moments.
18
+ *
19
+ * Every array is allocated at the cloud's budget and used to `count * stride`, because the budget
20
+ * is the one thing the cloud may not exceed — so densification never reallocates and a plan can
21
+ * gather into the same arrays it reads.
22
+ */
23
+ export interface Family {
24
+ /** Numbers per Gaussian. */
25
+ readonly stride: number;
26
+ /** The step size, as a fraction of what this family's own gradients have been. */
27
+ readonly rate: number;
28
+ readonly values: Float64Array;
29
+ readonly gradient: Float64Array;
30
+ readonly moment: Float64Array;
31
+ readonly second: Float64Array;
32
+ }
33
+ export declare function createFamily(stride: number, rate: number, budget: number): Family;
34
+ /** How far into the decay the fit is: β₁ⁿ and β₂ⁿ, carried rather than raised. */
35
+ export interface AdamState {
36
+ unbias1: number;
37
+ unbias2: number;
38
+ }
39
+ export declare function createAdamState(): AdamState;
40
+ /**
41
+ * One step over every family, on the gradients standing in them.
42
+ *
43
+ * **The gradient is read and not owned**: the caller writes every number of it before every step,
44
+ * so there is nothing here to clear. A caller that accumulated into it instead would need that
45
+ * clear back, and would be the one to add it.
46
+ */
47
+ export declare function adamStep(families: readonly Family[], count: number, state: AdamState): void;
48
+ /**
49
+ * Rewrite every family by a plan: slot `i` takes its values from `sources[i]`.
50
+ *
51
+ * **A slot marked fresh starts Adam over.** A Gaussian that was just split off its parent has none
52
+ * of that parent's history — its gradients are about to be different ones — and carrying the
53
+ * moments across would take a step sized for a shape that no longer exists. The reference
54
+ * implementation zeroes them for the same reason, and it is also what separates two halves of a
55
+ * split: identical values with identical moments receive identical steps forever.
56
+ */
57
+ export declare function gather(families: readonly Family[], sources: Int32Array, fresh: Uint8Array, count: number, scratch: Float64Array): void;
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Adam over a cloud's parameters, one moment pair per number.
3
+ *
4
+ * **Why Adam and not a plain descent.** The six families being fitted are in wildly different
5
+ * units — metres, logarithms of metres, a quaternion, a colour on nought to one — and a single
6
+ * step size that suits one of them moves another by nothing or by everything. Adam divides each
7
+ * number's step by the size of its own recent gradients, so a rate here is a *fraction of the
8
+ * parameter's own scale* and the families become comparable. It is also what makes the loss's
9
+ * absolute size irrelevant: a frame of a million pixels and one of a thousand descend alike.
10
+ *
11
+ * **The bias correction is a running product rather than a power.** `β₁ⁿ` would want `**`, which
12
+ * `packages/capture` may not use — the determinism gate refuses it, because an exponent is one of
13
+ * the operations a platform may round its own way. Multiplying by β each step is the same number
14
+ * by the same arithmetic the rest of the fit uses.
15
+ */
16
+ const BETA1 = 0.9;
17
+ const BETA2 = 0.999;
18
+ const EPSILON = 1e-8;
19
+ export function createFamily(stride, rate, budget) {
20
+ return {
21
+ stride,
22
+ rate,
23
+ values: new Float64Array(budget * stride),
24
+ gradient: new Float64Array(budget * stride),
25
+ moment: new Float64Array(budget * stride),
26
+ second: new Float64Array(budget * stride),
27
+ };
28
+ }
29
+ export function createAdamState() {
30
+ return { unbias1: 1, unbias2: 1 };
31
+ }
32
+ /**
33
+ * One step over every family, on the gradients standing in them.
34
+ *
35
+ * **The gradient is read and not owned**: the caller writes every number of it before every step,
36
+ * so there is nothing here to clear. A caller that accumulated into it instead would need that
37
+ * clear back, and would be the one to add it.
38
+ */
39
+ export function adamStep(families, count, state) {
40
+ state.unbias1 *= BETA1;
41
+ state.unbias2 *= BETA2;
42
+ const correction1 = 1 - state.unbias1;
43
+ const correction2 = 1 - state.unbias2;
44
+ for (const family of families) {
45
+ const { values, gradient, moment, second, rate, stride } = family;
46
+ const end = count * stride;
47
+ for (let at = 0; at < end; at += 1) {
48
+ const g = gradient[at];
49
+ const m = BETA1 * moment[at] + (1 - BETA1) * g;
50
+ const v = BETA2 * second[at] + (1 - BETA2) * g * g;
51
+ moment[at] = m;
52
+ second[at] = v;
53
+ values[at] =
54
+ values[at] -
55
+ (rate * (m / correction1)) / (Math.sqrt(v / correction2) + EPSILON);
56
+ }
57
+ }
58
+ }
59
+ /**
60
+ * Rewrite every family by a plan: slot `i` takes its values from `sources[i]`.
61
+ *
62
+ * **A slot marked fresh starts Adam over.** A Gaussian that was just split off its parent has none
63
+ * of that parent's history — its gradients are about to be different ones — and carrying the
64
+ * moments across would take a step sized for a shape that no longer exists. The reference
65
+ * implementation zeroes them for the same reason, and it is also what separates two halves of a
66
+ * split: identical values with identical moments receive identical steps forever.
67
+ */
68
+ export function gather(families, sources, fresh, count, scratch) {
69
+ for (const family of families) {
70
+ gatherArray(family.values, family.stride, sources, fresh, count, scratch, false);
71
+ gatherArray(family.moment, family.stride, sources, fresh, count, scratch, true);
72
+ gatherArray(family.second, family.stride, sources, fresh, count, scratch, true);
73
+ }
74
+ }
75
+ /** One array gathered through `scratch`, because a slot may take from a slot below itself. */
76
+ function gatherArray(array, stride, sources, fresh, count, scratch, blankWhenFresh) {
77
+ for (let slot = 0; slot < count; slot += 1) {
78
+ const from = sources[slot] * stride;
79
+ for (let c = 0; c < stride; c += 1)
80
+ scratch[slot * stride + c] = array[from + c];
81
+ }
82
+ for (let slot = 0; slot < count; slot += 1) {
83
+ const blank = blankWhenFresh && fresh[slot] !== 0;
84
+ for (let c = 0; c < stride; c += 1) {
85
+ array[slot * stride + c] = blank ? 0 : scratch[slot * stride + c];
86
+ }
87
+ }
88
+ }
@@ -0,0 +1,8 @@
1
+ import { type GaussianSet, type RasterCamera } from './project.ts';
2
+ /**
3
+ * The cloud's depth and coverage, one of each per pixel.
4
+ *
5
+ * A pixel no light reached is left at zero in both, which is a coverage of nothing rather than a
6
+ * surface at the camera — a caller reading the depth alone would find a wall in front of its face.
7
+ */
8
+ export declare function renderDepth(set: GaussianSet, camera: RasterCamera, depth: Float32Array, coverage: Float32Array): void;
@@ -0,0 +1,73 @@
1
+ /**
2
+ * How far away a cloud of Gaussians is, a pixel at a time.
3
+ *
4
+ * **A splat cloud has no surface**, so there is no depth to read off it the way a triangle
5
+ * rasteriser reads one. What it has is light arriving from a sequence of overlapping blobs, and the
6
+ * honest answer is where that light came from: each splat's own depth, weighted by how much of the
7
+ * pixel it actually contributed. A nearly opaque splat in front takes almost all of the weight, so
8
+ * the answer is its depth; a thin haze in front of a wall averages towards the wall.
9
+ *
10
+ * **The coverage is answered beside it, and callers need both.** At a silhouette a cloud thins out
11
+ * and a pixel may be a tenth covered — a depth averaged out of a tenth of a pixel's light is not a
12
+ * measurement of anything, and a fusion that treated it as one would carve the silhouette into the
13
+ * volume. So this answers how much light arrived as well, and the fusion weights by it rather than
14
+ * choosing a cutoff here on the caller's behalf.
15
+ *
16
+ * **Depth along the camera's own axis, not along the ray.** That is what a projection's `z` is and
17
+ * what `fuseDepth` compares a voxel against; a radial distance would be a different quantity with
18
+ * the same name, which is the kind of mistake that reads as a scene slightly the wrong shape.
19
+ */
20
+ import { exactExp } from '@driftengine/core';
21
+ import { visibleGaussians, } from './project.js';
22
+ /** Below this a Gaussian contributes less than a quarter of a value of an eight-bit channel. */
23
+ const FAINT = 1 / 255 / 4;
24
+ /**
25
+ * The cloud's depth and coverage, one of each per pixel.
26
+ *
27
+ * A pixel no light reached is left at zero in both, which is a coverage of nothing rather than a
28
+ * surface at the camera — a caller reading the depth alone would find a wall in front of its face.
29
+ */
30
+ export function renderDepth(set, camera, depth, coverage) {
31
+ const { width, height } = camera;
32
+ depth.fill(0);
33
+ coverage.fill(0);
34
+ const pixels = width * height;
35
+ const transmittance = new Float64Array(pixels).fill(1);
36
+ const weighted = new Float64Array(pixels);
37
+ const { order, projected } = visibleGaussians(set, camera);
38
+ for (const at of order) {
39
+ const one = projected[at];
40
+ const determinant = one.a * one.d - one.b * one.b;
41
+ const inverse = [one.d / determinant, -one.b / determinant, one.a / determinant];
42
+ const left = Math.max(0, Math.floor(one.x - one.radius));
43
+ const right = Math.min(width - 1, Math.ceil(one.x + one.radius));
44
+ const top = Math.max(0, Math.floor(one.y - one.radius));
45
+ const bottom = Math.min(height - 1, Math.ceil(one.y + one.radius));
46
+ const opacity = set.opacities[at];
47
+ for (let y = top; y <= bottom; y += 1) {
48
+ for (let x = left; x <= right; x += 1) {
49
+ const dx = x + 0.5 - one.x;
50
+ const dy = y + 0.5 - one.y;
51
+ const power = -0.5 *
52
+ (inverse[0] * dx * dx +
53
+ 2 * inverse[1] * dx * dy +
54
+ inverse[2] * dy * dy);
55
+ const alpha = opacity * exactExp(power);
56
+ if (alpha < FAINT)
57
+ continue;
58
+ const pixel = y * width + x;
59
+ const share = alpha * transmittance[pixel];
60
+ weighted[pixel] = weighted[pixel] + share * one.depth;
61
+ coverage[pixel] = coverage[pixel] + share;
62
+ transmittance[pixel] = transmittance[pixel] * (1 - alpha);
63
+ }
64
+ }
65
+ }
66
+ for (let pixel = 0; pixel < pixels; pixel += 1) {
67
+ const covered = coverage[pixel];
68
+ /* Divided by the light that arrived rather than by one: a half-covered pixel is still at the
69
+ depth of what covered it, and the caller is told how much that was. */
70
+ if (covered > 0)
71
+ depth[pixel] = weighted[pixel] / covered;
72
+ }
73
+ }
@@ -0,0 +1,36 @@
1
+ import { type GaussianSet, type RasterCamera } from './project.ts';
2
+ import type { Frame } from './rasterise.ts';
3
+ /** Where the gradients go: one entry per parameter of every Gaussian. */
4
+ export interface GaussianGradients {
5
+ /** Three per Gaussian. */
6
+ readonly positions: Float64Array;
7
+ readonly scales: Float64Array;
8
+ /** Four per Gaussian, xyzw. */
9
+ readonly rotations: Float64Array;
10
+ readonly colors: Float64Array;
11
+ /** Nine per Gaussian, and left at zero for a cloud with no band to fit. */
12
+ readonly sh1: Float64Array;
13
+ /**
14
+ * The gradients **as the screen sees them**, nine per Gaussian: the two of its centre in pixels,
15
+ * the three of its screen covariance, its opacity, and its resolved colour.
16
+ *
17
+ * Everything else here is a chain rule away from these. They are kept because the tiled device
18
+ * rasteriser computes exactly this much and nothing further — the projection and the band are
19
+ * per-splat work that is not worth a dispatch — so this is the surface
20
+ * `scripts/gpu-parity.mjs` holds the two rasterisers to.
21
+ */
22
+ readonly screen: Float64Array;
23
+ /** One per Gaussian. */
24
+ readonly opacities: Float64Array;
25
+ }
26
+ export declare function createGradients(count: number): GaussianGradients;
27
+ /** Numbers a Gaussian's screen-space gradient occupies: mean, covariance, opacity, colour. */
28
+ export declare const SCREEN_GRADIENTS = 9;
29
+ /** The squared difference between a render and a frame, and the render's own gradient. */
30
+ export declare function imageLoss(rendered: Frame, target: Frame, gradient: Float64Array): number;
31
+ /**
32
+ * The gradients of a render against `dPixels` — the loss's derivative by each rendered channel,
33
+ * four a pixel — added into `out`. The render is repeated here rather than taken as an argument,
34
+ * because the backward walk needs what each Gaussian contributed on the way.
35
+ */
36
+ export declare function accumulateGradients(set: GaussianSet, camera: RasterCamera, dPixels: Float64Array, out: GaussianGradients): void;
@@ -0,0 +1,228 @@
1
+ /**
2
+ * What a Gaussian would have to change to make the picture right: the derivatives of the rendered
3
+ * frame with respect to every parameter of every Gaussian.
4
+ *
5
+ * **Analytic, and checked against a central difference.** Every term below is a chain rule through
6
+ * the same projection `rasterise.ts` draws with — the view transform, the perspective Jacobian, the
7
+ * screen covariance, its inverse, the exponent, the alpha, and the compositing — and
8
+ * `gradients.test.ts` compares each against the rasteriser it differentiates, nudged. A gradient
9
+ * that is subtly wrong does not fail: it trains to a slightly wrong cloud, which is why the
10
+ * comparison is the test rather than a measure of how well it fits.
11
+ *
12
+ * **The compositing is walked twice.** Forward, nearest first, to know what each Gaussian
13
+ * contributed and what light was left after it; then backward, furthest first, carrying the colour
14
+ * accumulated behind — because what a Gaussian's alpha is worth depends on everything in front of
15
+ * it and everything behind it at once.
16
+ */
17
+ import { exactExp } from '@driftengine/core';
18
+ import { cameraCentre, sh1Basis, sh1DirectionGradient, splatColour, SH1_COEFFICIENTS, viewDirection, } from './harmonics.js';
19
+ import { visibleGaussians, } from './project.js';
20
+ import { screenToParameters } from './projectGradient.js';
21
+ export function createGradients(count) {
22
+ return {
23
+ positions: new Float64Array(count * 3),
24
+ scales: new Float64Array(count * 3),
25
+ rotations: new Float64Array(count * 4),
26
+ colors: new Float64Array(count * 3),
27
+ sh1: new Float64Array(count * SH1_COEFFICIENTS),
28
+ screen: new Float64Array(count * SCREEN_GRADIENTS),
29
+ opacities: new Float64Array(count),
30
+ };
31
+ }
32
+ const FAINT = 1 / 255 / 4;
33
+ /** Numbers a Gaussian's screen-space gradient occupies: mean, covariance, opacity, colour. */
34
+ export const SCREEN_GRADIENTS = 9;
35
+ /** The squared difference between a render and a frame, and the render's own gradient. */
36
+ export function imageLoss(rendered, target, gradient) {
37
+ let sum = 0;
38
+ for (let at = 0; at < gradient.length; at += 1) {
39
+ const off = rendered[at] - target[at];
40
+ sum += off * off;
41
+ gradient[at] = 2 * off;
42
+ }
43
+ return sum;
44
+ }
45
+ /**
46
+ * The gradients of a render against `dPixels` — the loss's derivative by each rendered channel,
47
+ * four a pixel — added into `out`. The render is repeated here rather than taken as an argument,
48
+ * because the backward walk needs what each Gaussian contributed on the way.
49
+ */
50
+ export function accumulateGradients(set, camera, dPixels, out) {
51
+ const { width, height } = camera;
52
+ const { order, projected } = visibleGaussians(set, camera);
53
+ const pixels = width * height;
54
+ /* Forward: the light left at each pixel after every Gaussian in front of it. */
55
+ const transmittance = new Float64Array(pixels).fill(1);
56
+ for (const at of order) {
57
+ const one = projected[at];
58
+ const determinant = one.a * one.d - one.b * one.b;
59
+ const inverse = [one.d / determinant, -one.b / determinant, one.a / determinant];
60
+ const box = boxOf(one, width, height);
61
+ for (let y = box.top; y <= box.bottom; y += 1) {
62
+ for (let x = box.left; x <= box.right; x += 1) {
63
+ const dx = x + 0.5 - one.x;
64
+ const dy = y + 0.5 - one.y;
65
+ const power = -0.5 *
66
+ (inverse[0] * dx * dx +
67
+ 2 * inverse[1] * dx * dy +
68
+ inverse[2] * dy * dy);
69
+ const alpha = set.opacities[at] * exactExp(power);
70
+ if (alpha < FAINT)
71
+ continue;
72
+ const pixel = y * width + x;
73
+ transmittance[pixel] = transmittance[pixel] * (1 - alpha);
74
+ }
75
+ }
76
+ }
77
+ /* Backward: furthest first, carrying what stands behind each Gaussian. */
78
+ const behind = new Float64Array(pixels * 3);
79
+ const behindAlpha = new Float64Array(pixels);
80
+ const light = Float64Array.from(transmittance);
81
+ const eye = new Float64Array(3);
82
+ cameraCentre(camera.worldToCamera, eye);
83
+ const direction = new Float64Array(3);
84
+ const basis = new Float64Array(3);
85
+ const colour = new Float64Array(3);
86
+ const dColour = new Float64Array(3);
87
+ const dDirection = new Float64Array(3);
88
+ for (let index = order.length - 1; index >= 0; index -= 1) {
89
+ const at = order[index];
90
+ const one = projected[at];
91
+ const determinant = one.a * one.d - one.b * one.b;
92
+ const inverse = [one.d / determinant, -one.b / determinant, one.a / determinant];
93
+ const box = boxOf(one, width, height);
94
+ const opacity = set.opacities[at];
95
+ const distance = viewDirection(set.positions, at, eye, direction);
96
+ sh1Basis(direction, basis);
97
+ splatColour(set.colors, set.sh1, at, basis, colour);
98
+ dColour.fill(0);
99
+ /* The screen-space gradients this Gaussian gathers over its own pixels. */
100
+ let dMeanX = 0;
101
+ let dMeanY = 0;
102
+ let dA = 0;
103
+ let dB = 0;
104
+ let dD = 0;
105
+ let dOpacity = 0;
106
+ for (let y = box.top; y <= box.bottom; y += 1) {
107
+ for (let x = box.left; x <= box.right; x += 1) {
108
+ const dx = x + 0.5 - one.x;
109
+ const dy = y + 0.5 - one.y;
110
+ const power = -0.5 *
111
+ (inverse[0] * dx * dx +
112
+ 2 * inverse[1] * dx * dy +
113
+ inverse[2] * dy * dy);
114
+ const weight = exactExp(power);
115
+ const alpha = opacity * weight;
116
+ if (alpha < FAINT)
117
+ continue;
118
+ const pixel = y * width + x;
119
+ /* Undo this Gaussian's own share to recover the light that reached it. */
120
+ const after = light[pixel];
121
+ const before = alpha === 1 ? after : after / (1 - alpha);
122
+ let dAlpha = 0;
123
+ for (let c = 0; c < 3; c += 1) {
124
+ const grad = dPixels[pixel * 4 + c];
125
+ /* What this Gaussian puts on the pixel, and what its alpha takes from behind it. */
126
+ dColour[c] = dColour[c] + grad * alpha * before;
127
+ dAlpha += grad * before * (colour[c] - behind[pixel * 3 + c]);
128
+ }
129
+ const gradAlpha = dPixels[pixel * 4 + 3];
130
+ dAlpha += gradAlpha * before * (1 - behindAlpha[pixel]);
131
+ dOpacity += dAlpha * weight;
132
+ /* Through the exponent to the mean and the covariance. */
133
+ const dPower = dAlpha * opacity * weight;
134
+ const gx = inverse[0] * dx + inverse[1] * dy;
135
+ const gy = inverse[1] * dx + inverse[2] * dy;
136
+ dMeanX += dPower * gx;
137
+ dMeanY += dPower * gy;
138
+ /*
139
+ * The exponent by the inverse covariance, **entry by entry rather than by three numbers**:
140
+ * the off-diagonal is one of a symmetric pair, so it takes half of what a single shared
141
+ * parameter would, and `dB` below is what puts the two halves back together. Written the
142
+ * other way it is a factor of two on every cross term, which is a wrong gradient that
143
+ * still points roughly the right way — so it fits, slightly wrongly, rather than failing.
144
+ */
145
+ const dInverseA = -0.5 * dPower * dx * dx;
146
+ const dInverseB = -0.5 * dPower * dx * dy;
147
+ const dInverseD = -0.5 * dPower * dy * dy;
148
+ /* d(Σ⁻¹)/dΣ for a symmetric 2 × 2, written out. */
149
+ const ia = inverse[0];
150
+ const ib = inverse[1];
151
+ const id = inverse[2];
152
+ dA += -(ia * dInverseA * ia +
153
+ ib * dInverseB * ia +
154
+ ia * dInverseB * ib +
155
+ ib * dInverseD * ib);
156
+ dB +=
157
+ -(ia * dInverseA * ib + ib * dInverseB * ib + ia * dInverseB * id + ib * dInverseD * id) *
158
+ 2;
159
+ dD += -(ib * dInverseA * ib +
160
+ id * dInverseB * ib +
161
+ ib * dInverseB * id +
162
+ id * dInverseD * id);
163
+ /* And carry this Gaussian into what stands behind the next one. */
164
+ for (let c = 0; c < 3; c += 1) {
165
+ behind[pixel * 3 + c] =
166
+ alpha * colour[c] + (1 - alpha) * behind[pixel * 3 + c];
167
+ }
168
+ behindAlpha[pixel] = alpha + (1 - alpha) * behindAlpha[pixel];
169
+ light[pixel] = before;
170
+ }
171
+ }
172
+ out.opacities[at] = out.opacities[at] + dOpacity;
173
+ /* What a device's tiled pass answers, before any of the chain rules below it. */
174
+ const screen = at * SCREEN_GRADIENTS;
175
+ out.screen[screen] = out.screen[screen] + dMeanX;
176
+ out.screen[screen + 1] = out.screen[screen + 1] + dMeanY;
177
+ out.screen[screen + 2] = out.screen[screen + 2] + dA;
178
+ out.screen[screen + 3] = out.screen[screen + 3] + dB;
179
+ out.screen[screen + 4] = out.screen[screen + 4] + dD;
180
+ out.screen[screen + 5] = out.screen[screen + 5] + dOpacity;
181
+ for (let c = 0; c < 3; c += 1) {
182
+ out.screen[screen + 6 + c] = out.screen[screen + 6 + c] + dColour[c];
183
+ }
184
+ /*
185
+ * The colour this Gaussian showed, back to the terms it was made of. **A channel the shader
186
+ * clamped at zero took no gradient**: it is flat there, so a constant term already below zero
187
+ * is not pushed further down by a frame that wanted it darker still.
188
+ */
189
+ for (let c = 0; c < 3; c += 1) {
190
+ if (!(colour[c] > 0))
191
+ continue;
192
+ out.colors[at * 3 + c] = out.colors[at * 3 + c] + dColour[c];
193
+ if (set.sh1 === undefined)
194
+ continue;
195
+ for (let band = 0; band < 3; band += 1) {
196
+ const slot = at * SH1_COEFFICIENTS + band * 3 + c;
197
+ out.sh1[slot] =
198
+ out.sh1[slot] + dColour[c] * basis[band];
199
+ }
200
+ }
201
+ /*
202
+ * And the band is a **second way a position changes the picture**: moving a Gaussian turns the
203
+ * direction it is seen from, which reshades it. Small next to the term that moves it across the
204
+ * frame, and the difference finds it all the same.
205
+ */
206
+ if (set.sh1 !== undefined && distance > 0) {
207
+ sh1DirectionGradient(set.sh1, at, colour, dColour, dDirection);
208
+ let along = 0;
209
+ for (let axis = 0; axis < 3; axis += 1)
210
+ along += dDirection[axis] * direction[axis];
211
+ for (let axis = 0; axis < 3; axis += 1) {
212
+ out.positions[at * 3 + axis] =
213
+ out.positions[at * 3 + axis] +
214
+ (dDirection[axis] - along * direction[axis]) / distance;
215
+ }
216
+ }
217
+ /* Through the projection to the Gaussian's own parameters. */
218
+ screenToParameters(set, at, camera, dMeanX, dMeanY, dA, dB, dD, out);
219
+ }
220
+ }
221
+ function boxOf(one, width, height) {
222
+ return {
223
+ left: Math.max(0, Math.floor(one.x - one.radius)),
224
+ right: Math.min(width - 1, Math.ceil(one.x + one.radius)),
225
+ top: Math.max(0, Math.floor(one.y - one.radius)),
226
+ bottom: Math.min(height - 1, Math.ceil(one.y + one.radius)),
227
+ };
228
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * The colour a Gaussian shows from a direction: its constant term plus the degree-1 band.
3
+ *
4
+ * **This is the splat shader's line, in JavaScript.** A capture fitted here is drawn by
5
+ * `@driftengine/splats`, so the two evaluations have to agree down to the sign of each basis —
6
+ * `SH_C1 * (-direction.y * c0 + direction.z * c1 - direction.x * c2)`, clamped at zero, shaded by
7
+ * the direction from the camera to the Gaussian in the capture's own world. A band evaluated with
8
+ * a different order here fits coefficients that look right in the fitter and wrong in the game,
9
+ * where it reads as a shader fault rather than as a capture that was never right.
10
+ *
11
+ * **Degree 1 and no further**, because that is what a record carries: nine coefficients and a
12
+ * scale in one more texel, and `SPLAT_WORDS_SH1` is what decided it.
13
+ */
14
+ /** The l=1 basis constant, as the shader's `SH_C1`. */
15
+ export declare const SH_C1 = 0.4886025119029199;
16
+ /** Nine coefficients a Gaussian, interleaved by basis and then by channel. */
17
+ export declare const SH1_COEFFICIENTS = 9;
18
+ /** The three degree-1 basis values for a direction, the band constant folded in, into `out`. */
19
+ export declare function sh1Basis(direction: ArrayLike<number>, out: Float64Array): void;
20
+ /** Where a camera stands in the world, from a 3 × 4 world-to-camera: −Rᵀ · t. */
21
+ export declare function cameraCentre(worldToCamera: ArrayLike<number>, out: Float64Array): void;
22
+ /**
23
+ * The direction the band is shaded by: from `centre` to the Gaussian at `at`, normalised.
24
+ *
25
+ * A Gaussian standing exactly where the camera does has no direction to give. It answers with the
26
+ * camera's own forward axis rather than a division by zero — which costs nothing, because a
27
+ * Gaussian at the camera is behind the near plane and is not drawn.
28
+ *
29
+ * Answers the distance as well, because the gradient below divides by it: a direction is a
30
+ * normalisation, and how much a step moves it depends on how far away the thing is.
31
+ */
32
+ export declare function viewDirection(positions: ArrayLike<number>, at: number, centre: ArrayLike<number>, out: Float64Array): number;
33
+ /** A Gaussian's colour from a direction whose basis is already resolved, clamped at zero. */
34
+ export declare function splatColour(colors: ArrayLike<number>, sh1: ArrayLike<number> | undefined, at: number, basis: ArrayLike<number>, out: Float64Array): void;
35
+ /**
36
+ * The band's own gradient by the **direction** it was shaded from, into `out`.
37
+ *
38
+ * The colour is `−C·u_y·c₀ + C·u_z·c₁ − C·u_x·c₂` a channel, so each axis of the direction gathers
39
+ * one basis's coefficients. A channel the clamp flattened contributes nothing, exactly as it
40
+ * contributes nothing to the constant term.
41
+ *
42
+ * What the caller still owes is the step from a direction to a position: a direction is a
43
+ * normalisation, so only the part of a move across the line of sight turns it, and the amount it
44
+ * turns by falls off with distance.
45
+ */
46
+ export declare function sh1DirectionGradient(sh1: ArrayLike<number>, at: number, colour: ArrayLike<number>, dColour: ArrayLike<number>, out: Float64Array): void;
@@ -0,0 +1,99 @@
1
+ /**
2
+ * The colour a Gaussian shows from a direction: its constant term plus the degree-1 band.
3
+ *
4
+ * **This is the splat shader's line, in JavaScript.** A capture fitted here is drawn by
5
+ * `@driftengine/splats`, so the two evaluations have to agree down to the sign of each basis —
6
+ * `SH_C1 * (-direction.y * c0 + direction.z * c1 - direction.x * c2)`, clamped at zero, shaded by
7
+ * the direction from the camera to the Gaussian in the capture's own world. A band evaluated with
8
+ * a different order here fits coefficients that look right in the fitter and wrong in the game,
9
+ * where it reads as a shader fault rather than as a capture that was never right.
10
+ *
11
+ * **Degree 1 and no further**, because that is what a record carries: nine coefficients and a
12
+ * scale in one more texel, and `SPLAT_WORDS_SH1` is what decided it.
13
+ */
14
+ /** The l=1 basis constant, as the shader's `SH_C1`. */
15
+ export const SH_C1 = 0.4886025119029199;
16
+ /** Nine coefficients a Gaussian, interleaved by basis and then by channel. */
17
+ export const SH1_COEFFICIENTS = 9;
18
+ /** The three degree-1 basis values for a direction, the band constant folded in, into `out`. */
19
+ export function sh1Basis(direction, out) {
20
+ out[0] = -SH_C1 * direction[1];
21
+ out[1] = SH_C1 * direction[2];
22
+ out[2] = -SH_C1 * direction[0];
23
+ }
24
+ /** Where a camera stands in the world, from a 3 × 4 world-to-camera: −Rᵀ · t. */
25
+ export function cameraCentre(worldToCamera, out) {
26
+ out[0] = 0;
27
+ out[1] = 0;
28
+ out[2] = 0;
29
+ for (let r = 0; r < 3; r += 1) {
30
+ for (let c = 0; c < 3; c += 1) {
31
+ out[c] =
32
+ out[c] -
33
+ worldToCamera[r * 4 + c] * worldToCamera[r * 4 + 3];
34
+ }
35
+ }
36
+ }
37
+ /**
38
+ * The direction the band is shaded by: from `centre` to the Gaussian at `at`, normalised.
39
+ *
40
+ * A Gaussian standing exactly where the camera does has no direction to give. It answers with the
41
+ * camera's own forward axis rather than a division by zero — which costs nothing, because a
42
+ * Gaussian at the camera is behind the near plane and is not drawn.
43
+ *
44
+ * Answers the distance as well, because the gradient below divides by it: a direction is a
45
+ * normalisation, and how much a step moves it depends on how far away the thing is.
46
+ */
47
+ export function viewDirection(positions, at, centre, out) {
48
+ const x = positions[at * 3] - centre[0];
49
+ const y = positions[at * 3 + 1] - centre[1];
50
+ const z = positions[at * 3 + 2] - centre[2];
51
+ const length = Math.sqrt(x * x + y * y + z * z);
52
+ if (!(length > 0)) {
53
+ out[0] = 0;
54
+ out[1] = 0;
55
+ out[2] = 1;
56
+ return 0;
57
+ }
58
+ out[0] = x / length;
59
+ out[1] = y / length;
60
+ out[2] = z / length;
61
+ return length;
62
+ }
63
+ /** A Gaussian's colour from a direction whose basis is already resolved, clamped at zero. */
64
+ export function splatColour(colors, sh1, at, basis, out) {
65
+ for (let c = 0; c < 3; c += 1) {
66
+ let value = colors[at * 3 + c];
67
+ if (sh1 !== undefined) {
68
+ for (let band = 0; band < 3; band += 1) {
69
+ value += basis[band] * sh1[at * SH1_COEFFICIENTS + band * 3 + c];
70
+ }
71
+ }
72
+ out[c] = value > 0 ? value : 0;
73
+ }
74
+ }
75
+ /**
76
+ * The band's own gradient by the **direction** it was shaded from, into `out`.
77
+ *
78
+ * The colour is `−C·u_y·c₀ + C·u_z·c₁ − C·u_x·c₂` a channel, so each axis of the direction gathers
79
+ * one basis's coefficients. A channel the clamp flattened contributes nothing, exactly as it
80
+ * contributes nothing to the constant term.
81
+ *
82
+ * What the caller still owes is the step from a direction to a position: a direction is a
83
+ * normalisation, so only the part of a move across the line of sight turns it, and the amount it
84
+ * turns by falls off with distance.
85
+ */
86
+ export function sh1DirectionGradient(sh1, at, colour, dColour, out) {
87
+ out[0] = 0;
88
+ out[1] = 0;
89
+ out[2] = 0;
90
+ for (let c = 0; c < 3; c += 1) {
91
+ if (!(colour[c] > 0))
92
+ continue;
93
+ const weight = dColour[c] * SH_C1;
94
+ const base = at * SH1_COEFFICIENTS;
95
+ out[0] = out[0] - weight * sh1[base + 6 + c];
96
+ out[1] = out[1] - weight * sh1[base + c];
97
+ out[2] = out[2] + weight * sh1[base + 3 + c];
98
+ }
99
+ }