@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,50 @@
1
+ /**
2
+ * A cloud of Gaussians fitted to a clip's frames, from a camera path and a sparse start.
3
+ *
4
+ * **The engine already knows how to draw the answer.** What comes out is `@driftengine/splats`'
5
+ * `SplatSource` — the same shape its `.ply` and `.sog` readers produce — so a fitted capture goes
6
+ * through `packSplats` and into a `.drft` beside anything else, and is drawn by the shader this
7
+ * package's rasteriser was written to match.
8
+ *
9
+ * **Two parameters are fitted through a transform, and it is what keeps the fit standing.** A
10
+ * scale is optimised as its logarithm and an opacity as its logit, so neither can be stepped past
11
+ * the end of its own range: a negative standard deviation is a covariance that is not a covariance,
12
+ * and an opacity above one is light arriving from a Gaussian that was never there. The gradients
13
+ * this receives are with respect to the real quantities, and the chain rule back to the transformed
14
+ * ones is one multiplication each.
15
+ *
16
+ * **The frames are linear and carry their own coverage.** A synthetic frame comes out of the
17
+ * rasteriser with both; a photograph has to be decoded from its display transform and given an
18
+ * alpha of one by whoever read it, because a photograph *is* fully covered and saying so is the
19
+ * caller's statement about their clip rather than this module's guess. A masked capture says
20
+ * something narrower with the same field, and the fit needs no further telling.
21
+ */
22
+ import type { SplatSource } from '@driftengine/splats';
23
+ import { type Frame } from './rasterise.ts';
24
+ export interface OptimiseOptions {
25
+ /** `fx`, `fy`, `cx`, `cy`, shared by every frame, as the path was solved with. */
26
+ readonly intrinsics: readonly [number, number, number, number];
27
+ readonly width: number;
28
+ readonly height: number;
29
+ /** Three per point: where the fit starts, usually the path's own sparse reconstruction. */
30
+ readonly seeds: Float64Array;
31
+ /** The most Gaussians the answer may carry. */
32
+ readonly budget: number;
33
+ readonly iterations: number;
34
+ /** The engine's seeded generator: the same one twice gives the same capture. */
35
+ readonly random: () => number;
36
+ /**
37
+ * The standard deviation every Gaussian starts at, in metres.
38
+ *
39
+ * Left out, it is the distance from each seed to its nearest neighbour — the spacing of the
40
+ * cloud, which is the size at which the seeds tile what they were sampled from. **A cloud of one
41
+ * seed has no spacing**, and is refused rather than given a number out of the air.
42
+ */
43
+ readonly initialScale?: number;
44
+ /** Fit the degree-1 band as well as the constant term. */
45
+ readonly viewDependent?: boolean;
46
+ /** How often the cloud is grown and pruned. */
47
+ readonly refineEvery?: number;
48
+ }
49
+ /** A clip fitted to Gaussians. Nothing here is the caller's array; everything is freshly cut. */
50
+ export declare function optimiseGaussians(targets: readonly Frame[], poses: readonly Float64Array[], options: OptimiseOptions): SplatSource;
@@ -0,0 +1,125 @@
1
+ import { adamStep, createAdamState } from './adam.js';
2
+ import { accumulateGradients, createGradients, imageLoss } from './gradients.js';
3
+ import { chain, clear, createCloud, harvest, materialise, start, viewOf, SPLIT_FRACTION, } from './parameters.js';
4
+ import { rasteriseGaussians } from './rasterise.js';
5
+ import { refineCloud } from './refine.js';
6
+ /** How often the cloud is grown and pruned, where the caller does not say. */
7
+ const DEFAULT_REFINE_EVERY = 50;
8
+ /** A clip fitted to Gaussians. Nothing here is the caller's array; everything is freshly cut. */
9
+ export function optimiseGaussians(targets, poses, options) {
10
+ const { intrinsics, width, height, seeds, budget, iterations, random } = options;
11
+ if (targets.length === 0 || targets.length !== poses.length) {
12
+ throw new RangeError(`capture: ${targets.length} frames against ${poses.length} poses, and a fit needs one of each`);
13
+ }
14
+ const seeded = Math.floor(seeds.length / 3);
15
+ if (seeded === 0)
16
+ throw new RangeError('capture: a fit needs at least one seed to start from');
17
+ if (seeded > budget) {
18
+ throw new RangeError(`capture: ${seeded} seeds do not fit a budget of ${budget} Gaussians`);
19
+ }
20
+ const cameras = poses.map((worldToCamera) => ({
21
+ width,
22
+ height,
23
+ intrinsics,
24
+ worldToCamera,
25
+ }));
26
+ const extent = sceneExtent(seeds, seeded, cameras);
27
+ const scale = options.initialScale ?? seedSpacing(seeds, seeded);
28
+ const cloud = createCloud(budget, extent, options.viewDependent === true);
29
+ start(cloud, seeds, seeded, scale);
30
+ const pixels = width * height * 4;
31
+ const rendered = new Float64Array(pixels);
32
+ const dPixels = new Float64Array(pixels);
33
+ const gradients = createGradients(budget);
34
+ const adam = createAdamState();
35
+ const refineEvery = options.refineEvery ?? DEFAULT_REFINE_EVERY;
36
+ const set = viewOf(cloud, budget);
37
+ for (let iteration = 0; iteration < iterations; iteration += 1) {
38
+ materialise(cloud);
39
+ set.count = cloud.count;
40
+ const view = iteration % targets.length;
41
+ rasteriseGaussians(set, cameras[view], rendered);
42
+ imageLoss(rendered, targets[view], dPixels);
43
+ clear(gradients, cloud.count);
44
+ accumulateGradients(set, cameras[view], dPixels, gradients);
45
+ chain(cloud, gradients);
46
+ adamStep(cloud.families, cloud.count, adam);
47
+ /*
48
+ * Grown and pruned between steps, never on the last one: a refinement leaves fresh Gaussians
49
+ * that have taken no step, and a cloud handed back in that state is one the caller is drawing
50
+ * before the fit has looked at it.
51
+ */
52
+ if (iteration + 1 < iterations && (iteration + 1) % refineEvery === 0) {
53
+ materialise(cloud);
54
+ refineCloud(cloud, { budget, splitAbove: SPLIT_FRACTION * extent, random });
55
+ }
56
+ }
57
+ materialise(cloud);
58
+ return harvest(cloud);
59
+ }
60
+ /**
61
+ * How big the capture is: the radius of the sphere holding the seeds and the cameras.
62
+ *
63
+ * **The cameras are in it deliberately.** A position's step is in metres, so it has to be a
64
+ * fraction of something real, and a seed cloud can be a single point — which has no size of its own
65
+ * and is still a scene, because the cameras looking at it are a distance away.
66
+ */
67
+ function sceneExtent(seeds, count, cameras) {
68
+ const points = [];
69
+ for (let at = 0; at < count; at += 1) {
70
+ points.push([
71
+ seeds[at * 3],
72
+ seeds[at * 3 + 1],
73
+ seeds[at * 3 + 2],
74
+ ]);
75
+ }
76
+ for (const camera of cameras) {
77
+ const m = camera.worldToCamera;
78
+ const centre = [0, 0, 0];
79
+ for (let r = 0; r < 3; r += 1) {
80
+ for (let c = 0; c < 3; c += 1) {
81
+ centre[c] = centre[c] - m[r * 4 + c] * m[r * 4 + 3];
82
+ }
83
+ }
84
+ points.push(centre);
85
+ }
86
+ const middle = [0, 0, 0];
87
+ for (const point of points) {
88
+ for (let c = 0; c < 3; c += 1)
89
+ middle[c] = middle[c] + point[c];
90
+ }
91
+ for (let c = 0; c < 3; c += 1)
92
+ middle[c] = middle[c] / points.length;
93
+ let furthest = 0;
94
+ for (const point of points) {
95
+ let square = 0;
96
+ for (let c = 0; c < 3; c += 1) {
97
+ const off = point[c] - middle[c];
98
+ square += off * off;
99
+ }
100
+ furthest = square > furthest ? square : furthest;
101
+ }
102
+ return Math.sqrt(furthest);
103
+ }
104
+ /** The mean distance from a seed to its nearest neighbour: the spacing the cloud was sampled at. */
105
+ function seedSpacing(seeds, count) {
106
+ if (count < 2)
107
+ return 0;
108
+ let total = 0;
109
+ for (let at = 0; at < count; at += 1) {
110
+ let nearest = Infinity;
111
+ for (let other = 0; other < count; other += 1) {
112
+ if (other === at)
113
+ continue;
114
+ let square = 0;
115
+ for (let c = 0; c < 3; c += 1) {
116
+ const off = seeds[at * 3 + c] - seeds[other * 3 + c];
117
+ square += off * off;
118
+ }
119
+ if (square < nearest)
120
+ nearest = square;
121
+ }
122
+ total += Math.sqrt(nearest);
123
+ }
124
+ return total / count;
125
+ }
@@ -0,0 +1,35 @@
1
+ import type { SplatSource } from '@driftengine/splats';
2
+ import { type Family } from './adam.ts';
3
+ import { type GaussianGradients } from './gradients.ts';
4
+ import type { GaussianSet } from './project.ts';
5
+ import type { RefineCloud } from './refine.ts';
6
+ /** A Gaussian wider than this fraction of the scene is split rather than cloned. */
7
+ export declare const SPLIT_FRACTION = 0.01;
8
+ /** The families, and the scratch the refinement rewrites them through. */
9
+ export interface Cloud extends RefineCloud {
10
+ readonly colors: Family;
11
+ readonly sh1: Family | null;
12
+ readonly logits: Family;
13
+ readonly scales: Float64Array;
14
+ readonly budget: number;
15
+ }
16
+ export declare function createCloud(budget: number, extent: number, viewDependent: boolean): Cloud;
17
+ /** Every seed as one grey, faint, round Gaussian pointing the way the world does. */
18
+ export declare function start(cloud: Cloud, seeds: Float64Array, count: number, scale: number): void;
19
+ /** The transformed parameters as the rasteriser wants them: real scales and real opacities. */
20
+ export declare function materialise(cloud: Cloud): void;
21
+ /** One `GaussianSet` over the cloud's own arrays, whose `count` moves as the cloud does. */
22
+ export declare function viewOf(cloud: Cloud, budget: number): GaussianSet;
23
+ export declare function clear(gradients: GaussianGradients, count: number): void;
24
+ /**
25
+ * The render's gradients onto the parameters that are actually stepped.
26
+ *
27
+ * Position, rotation, colour and the band are themselves; a scale arrives as a derivative by the
28
+ * metre and is wanted by the logarithm, which is one factor of the scale; an opacity arrives by the
29
+ * proportion and is wanted by the logit, which is the logistic's own derivative. **This is also
30
+ * where each Gaussian's pull is recorded** — how hard the frames are asking it to move, which is
31
+ * what the refinement grows the busiest of.
32
+ */
33
+ export declare function chain(cloud: Cloud, gradients: GaussianGradients): void;
34
+ /** The fitted cloud as a capture: single precision, cut to what it holds. */
35
+ export declare function harvest(cloud: Cloud): SplatSource;
@@ -0,0 +1,165 @@
1
+ /**
2
+ * What the fit actually steps, and how it becomes what the rasteriser draws.
3
+ *
4
+ * **Two of the six families are not the quantity they stand for.** A scale is held as its
5
+ * logarithm and an opacity as its logit, so that no step can carry either past the end of its own
6
+ * range — a negative standard deviation is not a covariance, and an opacity above one is light
7
+ * from a Gaussian that was never there. Everything downstream works in the real quantities, so
8
+ * this is where the two are made and where the gradients come back through them.
9
+ *
10
+ * **The chain back is one multiplication each and is nearly invisible to Adam**, which divides a
11
+ * step by the size of its own gradients and so cannot see a positive factor applied to every one
12
+ * of them. That is exactly why it is tested here by its own arithmetic rather than through a fit:
13
+ * a fit descends about as well with the factor missing, and the gradient is still wrong.
14
+ */
15
+ import { exactExp, exactLog } from '@driftengine/core';
16
+ import { createFamily } from './adam.js';
17
+ import { SCREEN_GRADIENTS } from './gradients.js';
18
+ import { SH1_COEFFICIENTS } from './harmonics.js';
19
+ /*
20
+ * The step sizes, as fractions of each family's own scale — which is what Adam makes them. They are
21
+ * the reference implementation's, and they are *decisions*: a fit that comes out soft wants the
22
+ * position rate lower and the opacity rate higher, and no test here asserts any of them.
23
+ *
24
+ * **The position rate is the one in real units**, so it is a fraction of the scene rather than a
25
+ * number: the same fit of a room and of a tabletop wants the same *relative* step.
26
+ */
27
+ const POSITION_RATE = 1.6e-4;
28
+ const SCALE_RATE = 5e-3;
29
+ const ROTATION_RATE = 1e-3;
30
+ const COLOUR_RATE = 2.5e-3;
31
+ /** The band moves at a twentieth of the constant term, as the reference has it. */
32
+ const SH1_RATE = COLOUR_RATE / 20;
33
+ const OPACITY_RATE = 5e-2;
34
+ /** Where a Gaussian starts: faint enough that the fit adds light rather than removing it. */
35
+ const INITIAL_OPACITY = 0.1;
36
+ /** A Gaussian wider than this fraction of the scene is split rather than cloned. */
37
+ export const SPLIT_FRACTION = 0.01;
38
+ export function createCloud(budget, extent, viewDependent) {
39
+ const positions = createFamily(3, POSITION_RATE * extent, budget);
40
+ const logScales = createFamily(3, SCALE_RATE, budget);
41
+ const rotations = createFamily(4, ROTATION_RATE, budget);
42
+ const colors = createFamily(3, COLOUR_RATE, budget);
43
+ const sh1 = viewDependent ? createFamily(SH1_COEFFICIENTS, SH1_RATE, budget) : null;
44
+ const logits = createFamily(1, OPACITY_RATE, budget);
45
+ const families = [positions, logScales, rotations, colors, logits];
46
+ if (sh1 !== null)
47
+ families.push(sh1);
48
+ return {
49
+ count: 0,
50
+ families,
51
+ positions,
52
+ logScales,
53
+ rotations,
54
+ colors,
55
+ sh1,
56
+ logits,
57
+ scales: new Float64Array(budget * 3),
58
+ opacities: new Float64Array(budget),
59
+ motion: new Float64Array(budget),
60
+ sources: new Int32Array(budget),
61
+ reverse: new Int32Array(budget),
62
+ fresh: new Uint8Array(budget),
63
+ scratch: new Float64Array(budget * SH1_COEFFICIENTS),
64
+ budget,
65
+ };
66
+ }
67
+ /** Every seed as one grey, faint, round Gaussian pointing the way the world does. */
68
+ export function start(cloud, seeds, count, scale) {
69
+ if (!(scale > 0)) {
70
+ throw new RangeError('capture: a fit needs a starting scale, and a single seed has no spacing');
71
+ }
72
+ const logScale = exactLog(scale);
73
+ const logit = exactLog(INITIAL_OPACITY / (1 - INITIAL_OPACITY));
74
+ for (let at = 0; at < count; at += 1) {
75
+ for (let c = 0; c < 3; c += 1) {
76
+ cloud.positions.values[at * 3 + c] = seeds[at * 3 + c];
77
+ cloud.logScales.values[at * 3 + c] = logScale;
78
+ cloud.colors.values[at * 3 + c] = 0.5;
79
+ }
80
+ cloud.rotations.values[at * 4 + 3] = 1;
81
+ cloud.logits.values[at] = logit;
82
+ }
83
+ cloud.count = count;
84
+ }
85
+ /** The transformed parameters as the rasteriser wants them: real scales and real opacities. */
86
+ export function materialise(cloud) {
87
+ for (let at = 0; at < cloud.count; at += 1) {
88
+ for (let c = 0; c < 3; c += 1) {
89
+ cloud.scales[at * 3 + c] = exactExp(cloud.logScales.values[at * 3 + c]);
90
+ }
91
+ cloud.opacities[at] = 1 / (1 + exactExp(-cloud.logits.values[at]));
92
+ }
93
+ }
94
+ /** One `GaussianSet` over the cloud's own arrays, whose `count` moves as the cloud does. */
95
+ export function viewOf(cloud, budget) {
96
+ const set = {
97
+ count: 0,
98
+ positions: cloud.positions.values,
99
+ scales: cloud.scales,
100
+ rotations: cloud.rotations.values,
101
+ colors: cloud.colors.values,
102
+ opacities: cloud.opacities,
103
+ };
104
+ return cloud.sh1 === null ? set : { ...set, sh1: cloud.sh1.values.subarray(0, budget * 9) };
105
+ }
106
+ export function clear(gradients, count) {
107
+ gradients.positions.fill(0, 0, count * 3);
108
+ gradients.scales.fill(0, 0, count * 3);
109
+ gradients.rotations.fill(0, 0, count * 4);
110
+ gradients.colors.fill(0, 0, count * 3);
111
+ gradients.sh1.fill(0, 0, count * SH1_COEFFICIENTS);
112
+ gradients.screen.fill(0, 0, count * SCREEN_GRADIENTS);
113
+ gradients.opacities.fill(0, 0, count);
114
+ }
115
+ /**
116
+ * The render's gradients onto the parameters that are actually stepped.
117
+ *
118
+ * Position, rotation, colour and the band are themselves; a scale arrives as a derivative by the
119
+ * metre and is wanted by the logarithm, which is one factor of the scale; an opacity arrives by the
120
+ * proportion and is wanted by the logit, which is the logistic's own derivative. **This is also
121
+ * where each Gaussian's pull is recorded** — how hard the frames are asking it to move, which is
122
+ * what the refinement grows the busiest of.
123
+ */
124
+ export function chain(cloud, gradients) {
125
+ for (let at = 0; at < cloud.count; at += 1) {
126
+ let pull = 0;
127
+ for (let c = 0; c < 3; c += 1) {
128
+ const move = gradients.positions[at * 3 + c];
129
+ cloud.positions.gradient[at * 3 + c] = move;
130
+ pull += move * move;
131
+ cloud.logScales.gradient[at * 3 + c] =
132
+ gradients.scales[at * 3 + c] * cloud.scales[at * 3 + c];
133
+ cloud.colors.gradient[at * 3 + c] = gradients.colors[at * 3 + c];
134
+ }
135
+ cloud.motion[at] = cloud.motion[at] + Math.sqrt(pull);
136
+ for (let c = 0; c < 4; c += 1) {
137
+ cloud.rotations.gradient[at * 4 + c] = gradients.rotations[at * 4 + c];
138
+ }
139
+ const opacity = cloud.opacities[at];
140
+ cloud.logits.gradient[at] = gradients.opacities[at] * opacity * (1 - opacity);
141
+ if (cloud.sh1 === null)
142
+ continue;
143
+ for (let c = 0; c < SH1_COEFFICIENTS; c += 1) {
144
+ cloud.sh1.gradient[at * SH1_COEFFICIENTS + c] = gradients.sh1[at * SH1_COEFFICIENTS + c];
145
+ }
146
+ }
147
+ }
148
+ /** The fitted cloud as a capture: single precision, cut to what it holds. */
149
+ export function harvest(cloud) {
150
+ const { count } = cloud;
151
+ const source = {
152
+ count,
153
+ positions: Float32Array.from(cloud.positions.values.subarray(0, count * 3)),
154
+ scales: Float32Array.from(cloud.scales.subarray(0, count * 3)),
155
+ rotations: Float32Array.from(cloud.rotations.values.subarray(0, count * 4)),
156
+ colors: Float32Array.from(cloud.colors.values.subarray(0, count * 3)),
157
+ opacities: Float32Array.from(cloud.opacities.subarray(0, count)),
158
+ };
159
+ if (cloud.sh1 === null)
160
+ return source;
161
+ return {
162
+ ...source,
163
+ sh1: Float32Array.from(cloud.sh1.values.subarray(0, count * SH1_COEFFICIENTS)),
164
+ };
165
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Where a Gaussian lands on screen: the projection `@driftengine/splats`' shader performs, in
3
+ * JavaScript, and the order a frame draws them in.
4
+ *
5
+ * **The same model, in the capture's own frame.** A perspective divide is not linear, so a
6
+ * three-dimensional Gaussian does not project to a two-dimensional one; the standard treatment,
7
+ * which that shader uses and this repeats, linearises about the splat's centre — `Σ' = J·R·Σ·Rᵀ·Jᵀ`
8
+ * — and adds a low-pass of 0.3 to the screen variances so that a splat smaller than a pixel still
9
+ * covers one. The one difference is handedness: the shader works in the renderer's view space,
10
+ * where z runs away from the camera negatively, and a capture's cameras look down positive z. The
11
+ * arithmetic is the same and the frame is the caller's.
12
+ *
13
+ * **It exists to be differentiated.** `projectGradient.ts` is this chain run backwards, and
14
+ * `gradients.test.ts` holds the two together by a central difference of one against the other.
15
+ */
16
+ export interface GaussianSet {
17
+ count: number;
18
+ /** Three per Gaussian, in the capture's world. */
19
+ readonly positions: Float64Array;
20
+ /** Three per Gaussian: a standard deviation along each of its own axes, in metres. */
21
+ readonly scales: Float64Array;
22
+ /** Four per Gaussian, xyzw, as the engine's quaternions are. */
23
+ readonly rotations: Float64Array;
24
+ /** Three per Gaussian, linear, which is a spherical harmonic's constant term. */
25
+ readonly colors: Float64Array;
26
+ /** Nine per Gaussian for the degree-1 band, or absent for a cloud with no view dependence. */
27
+ readonly sh1?: Float64Array;
28
+ /** One per Gaussian, 0 to 1. */
29
+ readonly opacities: Float64Array;
30
+ }
31
+ /**
32
+ * A frame this rasteriser writes: **single precision where it stands in for a render target, double
33
+ * where the caller is differentiating through it.** The fitting loop accumulates its loss in double
34
+ * and a frame quantised to single is a floor under every difference taken through one — measured at
35
+ * about a part in five hundred of the slope, against a part in ten million when the frame is double.
36
+ */
37
+ export type Frame = Float32Array | Float64Array;
38
+ export interface RasterCamera {
39
+ readonly width: number;
40
+ readonly height: number;
41
+ /** `fx`, `fy`, `cx`, `cy`. */
42
+ readonly intrinsics: readonly [number, number, number, number];
43
+ /** 3 × 4 row-major world-to-camera, as `estimatePoses` answers. */
44
+ readonly worldToCamera: Float64Array;
45
+ }
46
+ /** The low-pass the shader adds to the screen variances, in square pixels. */
47
+ export declare const LOW_PASS = 0.3;
48
+ /** How many standard deviations of a splat are drawn, and the largest radius in pixels. */
49
+ export declare const REACH = 3;
50
+ /** One Gaussian as the screen sees it. */
51
+ export interface Projected {
52
+ /** Where its centre lands, in pixels. */
53
+ readonly x: number;
54
+ readonly y: number;
55
+ /** Its screen covariance, `[a, b, d]` for `[[a, b], [b, d]]`, the low-pass included. */
56
+ readonly a: number;
57
+ readonly b: number;
58
+ readonly d: number;
59
+ /** Along the camera's own axis, which is what the painter's order is by. */
60
+ readonly depth: number;
61
+ /** The half-width of its axis-aligned box, in pixels. */
62
+ readonly radius: number;
63
+ }
64
+ /** The rotation a quaternion stands for, into `out` (3 × 3 row-major). */
65
+ export declare function rotationFromQuaternion(q: ArrayLike<number>, at: number, out: Float64Array): void;
66
+ /** One Gaussian projected, or null where it is behind the camera or too flat to draw. */
67
+ export declare function projectGaussian(set: GaussianSet, at: number, camera: RasterCamera): Projected | null;
68
+ /** The Gaussians a camera sees, nearest first, and where each lands. */
69
+ export declare function visibleGaussians(set: GaussianSet, camera: RasterCamera): {
70
+ readonly order: number[];
71
+ readonly projected: (Projected | null)[];
72
+ };
@@ -0,0 +1,137 @@
1
+ /** The low-pass the shader adds to the screen variances, in square pixels. */
2
+ export const LOW_PASS = 0.3;
3
+ /** How many standard deviations of a splat are drawn, and the largest radius in pixels. */
4
+ export const REACH = 3;
5
+ const MAX_RADIUS = 256;
6
+ /** The rotation a quaternion stands for, into `out` (3 × 3 row-major). */
7
+ export function rotationFromQuaternion(q, at, out) {
8
+ const x = q[at * 4];
9
+ const y = q[at * 4 + 1];
10
+ const z = q[at * 4 + 2];
11
+ const w = q[at * 4 + 3];
12
+ const length = Math.sqrt(x * x + y * y + z * z + w * w) || 1;
13
+ const nx = x / length;
14
+ const ny = y / length;
15
+ const nz = z / length;
16
+ const nw = w / length;
17
+ out[0] = 1 - 2 * (ny * ny + nz * nz);
18
+ out[1] = 2 * (nx * ny - nz * nw);
19
+ out[2] = 2 * (nx * nz + ny * nw);
20
+ out[3] = 2 * (nx * ny + nz * nw);
21
+ out[4] = 1 - 2 * (nx * nx + nz * nz);
22
+ out[5] = 2 * (ny * nz - nx * nw);
23
+ out[6] = 2 * (nx * nz - ny * nw);
24
+ out[7] = 2 * (ny * nz + nx * nw);
25
+ out[8] = 1 - 2 * (nx * nx + ny * ny);
26
+ }
27
+ /** One Gaussian projected, or null where it is behind the camera or too flat to draw. */
28
+ export function projectGaussian(set, at, camera) {
29
+ const [fx, fy, cx, cy] = camera.intrinsics;
30
+ const m = camera.worldToCamera;
31
+ const view = [0, 0, 0];
32
+ for (let r = 0; r < 3; r += 1) {
33
+ view[r] =
34
+ m[r * 4] * set.positions[at * 3] +
35
+ m[r * 4 + 1] * set.positions[at * 3 + 1] +
36
+ m[r * 4 + 2] * set.positions[at * 3 + 2] +
37
+ m[r * 4 + 3];
38
+ }
39
+ const z = view[2];
40
+ if (!(z > 1e-3))
41
+ return null;
42
+ /* The world covariance: R · diag(s²) · Rᵀ, turned into the camera's frame by the view rotation. */
43
+ const rotation = new Float64Array(9);
44
+ rotationFromQuaternion(set.rotations, at, rotation);
45
+ const scaled = new Float64Array(9);
46
+ for (let r = 0; r < 3; r += 1) {
47
+ for (let c = 0; c < 3; c += 1) {
48
+ const s = set.scales[at * 3 + c];
49
+ scaled[r * 3 + c] = rotation[r * 3 + c] * s;
50
+ }
51
+ }
52
+ const world = new Float64Array(9);
53
+ for (let r = 0; r < 3; r += 1) {
54
+ for (let c = 0; c < 3; c += 1) {
55
+ let sum = 0;
56
+ for (let k = 0; k < 3; k += 1)
57
+ sum += scaled[r * 3 + k] * scaled[c * 3 + k];
58
+ world[r * 3 + c] = sum;
59
+ }
60
+ }
61
+ const inCamera = new Float64Array(9);
62
+ for (let r = 0; r < 3; r += 1) {
63
+ for (let c = 0; c < 3; c += 1) {
64
+ let sum = 0;
65
+ for (let k = 0; k < 3; k += 1) {
66
+ for (let j = 0; j < 3; j += 1) {
67
+ sum += m[r * 4 + k] * world[k * 3 + j] * m[c * 4 + j];
68
+ }
69
+ }
70
+ inCamera[r * 3 + c] = sum;
71
+ }
72
+ }
73
+ /* The projection's Jacobian at this point, in pixels. */
74
+ const invZ = 1 / z;
75
+ const invZ2 = invZ * invZ;
76
+ const j = [
77
+ [fx * invZ, 0, -fx * view[0] * invZ2],
78
+ [0, fy * invZ, -fy * view[1] * invZ2],
79
+ ];
80
+ const screen = [0, 0, 0, 0];
81
+ for (let r = 0; r < 2; r += 1) {
82
+ for (let c = 0; c < 2; c += 1) {
83
+ let sum = 0;
84
+ for (let k = 0; k < 3; k += 1) {
85
+ for (let l = 0; l < 3; l += 1) {
86
+ sum +=
87
+ j[r][k] *
88
+ inCamera[k * 3 + l] *
89
+ j[c][l];
90
+ }
91
+ }
92
+ screen[r * 2 + c] = sum;
93
+ }
94
+ }
95
+ const a = screen[0] + LOW_PASS;
96
+ const b = screen[1];
97
+ const d = screen[3] + LOW_PASS;
98
+ const determinant = a * d - b * b;
99
+ if (!(determinant > 1e-12))
100
+ return null;
101
+ /* The ellipse's larger eigenvalue fixes how far it reaches. */
102
+ const mid = 0.5 * (a + d);
103
+ const spread = Math.sqrt(Math.max(0.1, mid * mid - determinant));
104
+ const radius = Math.min(MAX_RADIUS, REACH * Math.sqrt(Math.max(mid + spread, 0)));
105
+ return {
106
+ x: fx * view[0] * invZ + cx,
107
+ y: fy * view[1] * invZ + cy,
108
+ a,
109
+ b,
110
+ d,
111
+ depth: z,
112
+ radius,
113
+ };
114
+ }
115
+ /** The Gaussians a camera sees, nearest first, and where each lands. */
116
+ export function visibleGaussians(set, camera) {
117
+ const projected = [];
118
+ const order = [];
119
+ for (let at = 0; at < set.count; at += 1) {
120
+ const one = projectGaussian(set, at, camera);
121
+ projected.push(one);
122
+ if (one === null)
123
+ continue;
124
+ if (one.x + one.radius < 0 || one.x - one.radius >= camera.width)
125
+ continue;
126
+ if (one.y + one.radius < 0 || one.y - one.radius >= camera.height)
127
+ continue;
128
+ order.push(at);
129
+ }
130
+ /* Nearest first, ties by index so the answer does not depend on the sort's stability. */
131
+ order.sort((p, q) => {
132
+ const first = projected[p].depth;
133
+ const second = projected[q].depth;
134
+ return first === second ? p - q : first - second;
135
+ });
136
+ return { order, projected };
137
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The screen-space gradients of one Gaussian carried back to its own parameters.
3
+ *
4
+ * **`project.ts` run backwards**, term for term: the perspective Jacobian, the covariance it acts
5
+ * on, the rotation and scale that covariance is built from, and the quaternion behind the rotation.
6
+ * Nothing here knows about pixels or compositing — it takes what `gradients.ts` gathered over a
7
+ * Gaussian's own pixels and answers what that means for where it stands and how it is shaped.
8
+ */
9
+ import { type GaussianSet, type RasterCamera } from './project.ts';
10
+ import type { GaussianGradients } from './gradients.ts';
11
+ /**
12
+ * The screen-space gradients carried back to a Gaussian's position, scale and rotation, by the
13
+ * same chain the projection is built from.
14
+ */
15
+ export declare function screenToParameters(set: GaussianSet, at: number, camera: RasterCamera, dMeanX: number, dMeanY: number, dA: number, dB: number, dD: number, out: GaussianGradients): void;