@driftengine/splats 3.61.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 (71) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +9 -0
  3. package/README.md +56 -0
  4. package/dist/half.d.ts +32 -0
  5. package/dist/half.js +88 -0
  6. package/dist/index.d.ts +32 -0
  7. package/dist/index.js +38 -0
  8. package/dist/shaders/generated/splat.wgsl.d.ts +89 -0
  9. package/dist/shaders/generated/splat.wgsl.js +95 -0
  10. package/dist/shaders/splat.d.ts +25 -0
  11. package/dist/shaders/splat.js +337 -0
  12. package/dist/splat.d.ts +26 -0
  13. package/dist/splat.js +63 -0
  14. package/dist/splatBudget.d.ts +40 -0
  15. package/dist/splatBudget.js +45 -0
  16. package/dist/splatCapture.d.ts +76 -0
  17. package/dist/splatCapture.js +108 -0
  18. package/dist/splatCull.d.ts +25 -0
  19. package/dist/splatCull.js +80 -0
  20. package/dist/splatData.d.ts +177 -0
  21. package/dist/splatData.js +223 -0
  22. package/dist/splatGl.d.ts +49 -0
  23. package/dist/splatGl.js +176 -0
  24. package/dist/splatGpu.d.ts +50 -0
  25. package/dist/splatGpu.js +180 -0
  26. package/dist/splatLayout.d.ts +52 -0
  27. package/dist/splatLayout.js +75 -0
  28. package/dist/splatMatrix.d.ts +29 -0
  29. package/dist/splatMatrix.js +68 -0
  30. package/dist/splatPass.d.ts +83 -0
  31. package/dist/splatPass.js +206 -0
  32. package/dist/splatPly.d.ts +14 -0
  33. package/dist/splatPly.js +242 -0
  34. package/dist/splatSog.d.ts +110 -0
  35. package/dist/splatSog.js +285 -0
  36. package/dist/splatSogDecoder.d.ts +26 -0
  37. package/dist/splatSogDecoder.js +29 -0
  38. package/dist/splatSort.d.ts +137 -0
  39. package/dist/splatSort.js +199 -0
  40. package/dist/splatSortWorker.d.ts +14 -0
  41. package/dist/splatSortWorker.js +137 -0
  42. package/dist/splatSorter.d.ts +112 -0
  43. package/dist/splatSorter.js +231 -0
  44. package/dist/splatView.d.ts +52 -0
  45. package/dist/splatView.js +115 -0
  46. package/package.json +56 -0
  47. package/src/fixtures/README.md +36 -0
  48. package/src/fixtures/cloud.sog +0 -0
  49. package/src/fixtures/cloud.texels.json +27 -0
  50. package/src/fixtures/cloud.truth.json +582 -0
  51. package/src/half.ts +92 -0
  52. package/src/index.ts +55 -0
  53. package/src/shaders/generated/splat.wgsl.ts +98 -0
  54. package/src/shaders/splat.ts +344 -0
  55. package/src/splat.ts +75 -0
  56. package/src/splatBudget.ts +48 -0
  57. package/src/splatCapture.ts +154 -0
  58. package/src/splatCull.ts +91 -0
  59. package/src/splatData.ts +398 -0
  60. package/src/splatGl.ts +262 -0
  61. package/src/splatGpu.ts +259 -0
  62. package/src/splatLayout.ts +86 -0
  63. package/src/splatMatrix.ts +81 -0
  64. package/src/splatPass.ts +324 -0
  65. package/src/splatPly.ts +283 -0
  66. package/src/splatSog.ts +375 -0
  67. package/src/splatSogDecoder.ts +33 -0
  68. package/src/splatSort.ts +296 -0
  69. package/src/splatSortWorker.ts +155 -0
  70. package/src/splatSorter.ts +285 -0
  71. package/src/splatView.ts +147 -0
@@ -0,0 +1,231 @@
1
+ /** The scheduler: one sort in flight, newest view wins, and the worker is a default not a mechanism. */
2
+ import { createSplatSortScratch, sortSplatsByDepth } from './splatSort.js';
3
+ import { createSplatSortWorker } from './splatSortWorker.js';
4
+ /**
5
+ * How far the view may turn before the order is worth recomputing.
6
+ *
7
+ * A dot product, so 0.999 is about 2.6 degrees. Below that the change in ordering is a handful of
8
+ * adjacent swaps among splats that overlap anyway, and re-sorting for it would mean a sort every
9
+ * frame at any camera speed — which is the cost this whole arrangement exists to avoid. **A still
10
+ * camera sorts zero times a second**, which is the property to preserve.
11
+ */
12
+ const RESORT_DOT = 0.999;
13
+ /**
14
+ * How far the camera may move, as a fraction of the capture's own radius, before a re-sort.
15
+ *
16
+ * **A fraction rather than a distance, because a distance means different things for a room and
17
+ * for a landscape.** Only consulted when there is a budget: see `frame`.
18
+ */
19
+ const RESORT_MOVE_FRACTION = 0.1;
20
+ /**
21
+ * The sort, on the main thread, right now.
22
+ *
23
+ * The fallback when a worker cannot be built, and the implementation a test injects. Synchronous
24
+ * inside a promise: there is nothing to await, and pretending otherwise would hide that this
25
+ * blocks.
26
+ *
27
+ * **The scratch is cached across calls and grown when it has to be.** It is 384 KB of histogram
28
+ * plus four bytes a splat, and allocating that per sort would make the fallback path generate more
29
+ * garbage than the work it is doing. Sharing one scratch between two captures is safe because this
30
+ * is synchronous: no second sort can start while one is running on this thread.
31
+ */
32
+ let mainThreadScratch = null;
33
+ export function sortOnMainThread(request) {
34
+ if (mainThreadScratch === null || mainThreadScratch.keys.length < request.count) {
35
+ mainThreadScratch = createSplatSortScratch(request.count);
36
+ }
37
+ const count = sortSplatsByDepth(request, mainThreadScratch);
38
+ return Promise.resolve({ order: request.out, count });
39
+ }
40
+ /**
41
+ * The shipped capability: a worker over the same sort this module runs on the main thread.
42
+ *
43
+ * `sortSplatsByDepth.toString()` is what goes into the worker, so there is one implementation of
44
+ * the decision and not two — the 2026-08-17 rule, which is about exactly this. It works because
45
+ * that function closes over nothing: its scratch and its two bucket counts all arrive as
46
+ * arguments.
47
+ */
48
+ export function createDefaultSplatSort() {
49
+ return createSplatSortWorker(sortSplatsByDepth.toString());
50
+ }
51
+ /**
52
+ * Hold the newest order, ask for a new one when the view has changed enough, and never queue.
53
+ *
54
+ * **At most one sort in flight, and a request while one is running is dropped rather than
55
+ * queued.** The newest view is the only one worth sorting for: a queue would sort for a camera
56
+ * position the player has already left, and then sort again, so it converts a busy moment into a
57
+ * backlog that never catches up.
58
+ *
59
+ * **Two buffers, ping-ponged.** One is being filled by the sorter and the other is what the pass
60
+ * draws from, so a sort landing mid-frame never rewrites the order under a draw call.
61
+ */
62
+ export class SplatSorter {
63
+ splats;
64
+ sort;
65
+ budget;
66
+ moveFraction;
67
+ ready;
68
+ /** How many splats the standing order was computed over, so an arrival is a reason to re-sort. */
69
+ sortedReady = 0;
70
+ /** Half the diagonal of the capture's own bounds. The scale the move gate is a fraction of. */
71
+ radius;
72
+ buffers;
73
+ /** Which buffer the sorter owns. The other is the one a caller may read. */
74
+ filling = 0;
75
+ inFlight = false;
76
+ /** The view the standing order was computed for. Meaningless until `hasSorted`. */
77
+ sortedDirX = 0;
78
+ sortedDirY = 0;
79
+ sortedDirZ = 0;
80
+ sortedOriginX = 0;
81
+ sortedOriginY = 0;
82
+ sortedOriginZ = 0;
83
+ hasSorted = false;
84
+ /** How many indices the standing order actually holds, which a budget makes smaller. */
85
+ kept = 0;
86
+ /** Bumped by every sort that lands, so a caller can tell a new order from the one it has. */
87
+ generation = 0;
88
+ constructor(options) {
89
+ this.splats = options.splats;
90
+ /*
91
+ * The worker is the default and the main thread is the fallback, and neither is the mechanism:
92
+ * a caller that supplies its own never reaches either. Built once per sorter rather than once
93
+ * per module, so two captures do not share one worker and serialise behind each other.
94
+ */
95
+ this.sort = options.sort ?? createDefaultSplatSort();
96
+ this.budget = options.budget ?? 0;
97
+ this.moveFraction = options.moveFraction ?? RESORT_MOVE_FRACTION;
98
+ this.ready = options.ready ?? (() => options.splats.count);
99
+ const { boundsMin, boundsMax } = options.splats;
100
+ this.radius =
101
+ 0.5 *
102
+ Math.hypot((boundsMax[0] ?? 0) - (boundsMin[0] ?? 0), (boundsMax[1] ?? 0) - (boundsMin[1] ?? 0), (boundsMax[2] ?? 0) - (boundsMin[2] ?? 0));
103
+ const size = Math.max(1, options.splats.count);
104
+ this.buffers = [new Uint32Array(size), new Uint32Array(size)];
105
+ }
106
+ /** The standing order, or null until the first sort lands. */
107
+ get order() {
108
+ return this.hasSorted ? this.buffers[1 - this.filling] : null;
109
+ }
110
+ /** How many entries of `order` are real. Below the capture's count when a budget bit. */
111
+ get drawCount() {
112
+ return this.kept;
113
+ }
114
+ /** How many orders have landed. A caller uploads when this changes and not otherwise. */
115
+ get version() {
116
+ return this.generation;
117
+ }
118
+ /**
119
+ * Whether a sort is in flight.
120
+ *
121
+ * **What a measuring page waits on, and it has to wait on more than `version`.** With a budget
122
+ * the sort chooses *which* splats are drawn and not merely their order, so a capture
123
+ * photographed at the first landed sort is a capture photographed at whichever sort the worker
124
+ * happened to finish — and two backends that reach the held frame at different wall-clock times
125
+ * then draw different splats. A held camera is settled when this is false and the standing
126
+ * `version` has been uploaded: at a fixed direction `frame` asks for nothing further, so that
127
+ * state is reached and then keeps.
128
+ */
129
+ get sorting() {
130
+ return this.inFlight;
131
+ }
132
+ /**
133
+ * Offer the current view, in the capture's own space. Starts a sort if one is wanted.
134
+ *
135
+ * `resolveSplatView` is what turns a camera and a model matrix into this. `force` is for the
136
+ * first frame and for a capture whose transform moved, where the view in its own space may be
137
+ * unchanged and the order is stale anyway.
138
+ *
139
+ * **Turning always matters; moving only matters when there is a budget.** Depth is measured
140
+ * along the view axis, so translating the camera shifts every splat's depth by the same amount
141
+ * and leaves the ordering exactly as it was — which is why this gate did not exist until a
142
+ * budget did. A budget keeps the splats largest on screen, which is the extent over the
143
+ * *distance*, so a camera that walks across a capture without turning changes which splats are
144
+ * drawn. What that costs is a sort every tenth of a capture-radius while a viewer moves; what
145
+ * would make it wrong is a budget so generous that nothing is ever dropped, where the gate spends
146
+ * sorts to reach the same answer.
147
+ */
148
+ frame(local, force = false) {
149
+ const count = Math.max(0, Math.min(this.ready(), this.splats.count));
150
+ if (count <= 0)
151
+ return;
152
+ if (this.inFlight)
153
+ return;
154
+ /*
155
+ * **An arrival is a reason to sort even from a camera that has not moved.** The order this
156
+ * sorter holds is an order over fewer splats than the capture now has, so it is not the order
157
+ * it wants — and a viewer watching a capture load is exactly the viewer who is standing
158
+ * still. Without this a stream stops densifying the moment nobody moves.
159
+ */
160
+ let wanted = force || !this.hasSorted || count !== this.sortedReady;
161
+ if (!wanted) {
162
+ const turned = local.dirX * this.sortedDirX + local.dirY * this.sortedDirY + local.dirZ * this.sortedDirZ <
163
+ RESORT_DOT;
164
+ wanted = turned;
165
+ }
166
+ if (!wanted && this.budget > 0 && this.budget < count && this.radius > 0) {
167
+ const moved = Math.hypot(local.originX - this.sortedOriginX, local.originY - this.sortedOriginY, local.originZ - this.sortedOriginZ);
168
+ wanted = moved > this.moveFraction * this.radius;
169
+ }
170
+ if (!wanted)
171
+ return;
172
+ this.inFlight = true;
173
+ const target = this.buffers[this.filling];
174
+ /*
175
+ * Snapshotted before the call, because `local` belongs to the caller and is rewritten in place
176
+ * every frame — so reading it again when the promise settles would record the view the camera
177
+ * has *now* as the one this order was computed for, and the re-sort gates would then compare
178
+ * against a lie. Six numbers on the stack rather than a copy of the object, so nothing is
179
+ * allocated for it.
180
+ */
181
+ const dirX = local.dirX;
182
+ const dirY = local.dirY;
183
+ const dirZ = local.dirZ;
184
+ const originX = local.originX;
185
+ const originY = local.originY;
186
+ const originZ = local.originZ;
187
+ void this.sort({
188
+ positions: this.splats.positions,
189
+ extents: this.splats.extents,
190
+ count,
191
+ dirX,
192
+ dirY,
193
+ dirZ,
194
+ originX,
195
+ originY,
196
+ originZ,
197
+ budget: this.budget,
198
+ out: target,
199
+ })
200
+ .then((result) => {
201
+ /*
202
+ * The buffer that comes back is the one that was handed out — a worker transfers it away
203
+ * and transfers it back, so the array identity may differ even though the storage is the
204
+ * same. Store what arrived rather than what was sent.
205
+ */
206
+ this.buffers[this.filling] = result.order;
207
+ this.filling = 1 - this.filling;
208
+ this.sortedDirX = dirX;
209
+ this.sortedDirY = dirY;
210
+ this.sortedDirZ = dirZ;
211
+ this.sortedOriginX = originX;
212
+ this.sortedOriginY = originY;
213
+ this.sortedOriginZ = originZ;
214
+ this.kept = result.count;
215
+ this.sortedReady = count;
216
+ this.hasSorted = true;
217
+ this.generation++;
218
+ })
219
+ .catch(() => {
220
+ /*
221
+ * **A rejected sort leaves the previous order drawing**, which is the honest degradation:
222
+ * a capture one camera step out of order is very slightly wrong at some silhouettes, and a
223
+ * capture with no order at all is not drawn. Swallowed rather than rethrown because this
224
+ * is reached from a frame and the loop may not throw.
225
+ */
226
+ })
227
+ .finally(() => {
228
+ this.inFlight = false;
229
+ });
230
+ }
231
+ }
@@ -0,0 +1,52 @@
1
+ /** Where the camera is, and which way it faces, expressed in one capture's own space. */
2
+ /**
3
+ * The camera as the sorter needs it: in the capture's coordinates rather than the world's.
4
+ *
5
+ * Mutable and filled in place, because this is resolved once a frame per batch and the engine's
6
+ * rule about per-frame allocation binds a package exactly as it binds the renderer.
7
+ */
8
+ export interface SplatViewLocal {
9
+ /** The camera's forward in the capture's own space, unit length. */
10
+ dirX: number;
11
+ dirY: number;
12
+ dirZ: number;
13
+ /** The camera's position in the capture's own space. */
14
+ originX: number;
15
+ originY: number;
16
+ originZ: number;
17
+ }
18
+ export declare function createSplatViewLocal(): SplatViewLocal;
19
+ /**
20
+ * Fill `out` with the camera, in the space the capture's own positions are written in.
21
+ *
22
+ * **One sorter serves one batch, and this is what makes that affordable.** A batch has a model
23
+ * matrix so that two captures can compose in one scene; its splat positions are in its own frame
24
+ * and the sort reads them there, so asking the sort for a *world* direction would mean
25
+ * transforming a million positions every time the view turned. Transforming the camera instead is
26
+ * six numbers.
27
+ *
28
+ * **The direction is the model's transpose and not its inverse, and the two disagree exactly where
29
+ * it matters.** What the sort needs is an ordering that matches the depth those splats really have
30
+ * once the model has moved them, and the world depth of a capture-space point `p` is
31
+ * `dot(M p + t - c, d)`, which rearranges to `dot(p, Mᵀd)` plus a constant. So `Mᵀd` is the
32
+ * direction that orders correctly for *any* invertible model, including one with a non-uniform
33
+ * scale; `M⁻¹d` is the direction that would be right if the transform were a rotation, and a
34
+ * plausible-looking answer everywhere else. `splatView.test.ts` asserts the ordering rather than
35
+ * the arithmetic, which is why the model it uses is stretched.
36
+ *
37
+ * The origin is the inverse, because that genuinely is a point: `M⁻¹(c − t)` is where the camera
38
+ * sits in the capture's frame, and it is what the budget's distances are measured from. It also
39
+ * happens to be exactly the point whose projection along `Mᵀd` cancels the constant above, so the
40
+ * two halves agree by construction rather than by arrangement.
41
+ *
42
+ * **What this gives up**: the ordering is along the view *axis* rather than by distance to the
43
+ * camera point, so two splats at equal depth and far apart across the frame are ordered by a plane
44
+ * rather than by a sphere. That is the ordering every splat renderer uses and it costs nothing
45
+ * until a capture wraps around the viewer. What would make it wrong is exactly that case —
46
+ * standing inside a capture at a wide field of view, where the error shows at the frame's corners.
47
+ *
48
+ * **A singular model falls back to the world rather than to `NaN`.** This is reached from a frame
49
+ * and the loop may not throw; a `NaN` direction is a sort in which every comparison is false,
50
+ * which is silently input order rather than a visible fault.
51
+ */
52
+ export declare function resolveSplatView(view: ArrayLike<number>, model: ArrayLike<number>, out: SplatViewLocal): void;
@@ -0,0 +1,115 @@
1
+ /** Where the camera is, and which way it faces, expressed in one capture's own space. */
2
+ export function createSplatViewLocal() {
3
+ return { dirX: 0, dirY: 0, dirZ: -1, originX: 0, originY: 0, originZ: 0 };
4
+ }
5
+ /**
6
+ * Fill `out` with the camera, in the space the capture's own positions are written in.
7
+ *
8
+ * **One sorter serves one batch, and this is what makes that affordable.** A batch has a model
9
+ * matrix so that two captures can compose in one scene; its splat positions are in its own frame
10
+ * and the sort reads them there, so asking the sort for a *world* direction would mean
11
+ * transforming a million positions every time the view turned. Transforming the camera instead is
12
+ * six numbers.
13
+ *
14
+ * **The direction is the model's transpose and not its inverse, and the two disagree exactly where
15
+ * it matters.** What the sort needs is an ordering that matches the depth those splats really have
16
+ * once the model has moved them, and the world depth of a capture-space point `p` is
17
+ * `dot(M p + t - c, d)`, which rearranges to `dot(p, Mᵀd)` plus a constant. So `Mᵀd` is the
18
+ * direction that orders correctly for *any* invertible model, including one with a non-uniform
19
+ * scale; `M⁻¹d` is the direction that would be right if the transform were a rotation, and a
20
+ * plausible-looking answer everywhere else. `splatView.test.ts` asserts the ordering rather than
21
+ * the arithmetic, which is why the model it uses is stretched.
22
+ *
23
+ * The origin is the inverse, because that genuinely is a point: `M⁻¹(c − t)` is where the camera
24
+ * sits in the capture's frame, and it is what the budget's distances are measured from. It also
25
+ * happens to be exactly the point whose projection along `Mᵀd` cancels the constant above, so the
26
+ * two halves agree by construction rather than by arrangement.
27
+ *
28
+ * **What this gives up**: the ordering is along the view *axis* rather than by distance to the
29
+ * camera point, so two splats at equal depth and far apart across the frame are ordered by a plane
30
+ * rather than by a sphere. That is the ordering every splat renderer uses and it costs nothing
31
+ * until a capture wraps around the viewer. What would make it wrong is exactly that case —
32
+ * standing inside a capture at a wide field of view, where the error shows at the frame's corners.
33
+ *
34
+ * **A singular model falls back to the world rather than to `NaN`.** This is reached from a frame
35
+ * and the loop may not throw; a `NaN` direction is a sort in which every comparison is false,
36
+ * which is silently input order rather than a visible fault.
37
+ */
38
+ export function resolveSplatView(view, model, out) {
39
+ /*
40
+ * The camera's forward in the world is the third row of the view rotation, negated: the view
41
+ * matrix takes world to camera and a camera looks down its own -z.
42
+ */
43
+ const forwardX = -(view[2] ?? 0);
44
+ const forwardY = -(view[6] ?? 0);
45
+ const forwardZ = -(view[10] ?? 0);
46
+ /* The camera's position is -Rᵀt, which for a rigid view matrix is an exact inverse and costs
47
+ nine multiplies rather than a general inversion. */
48
+ const tx = view[12] ?? 0;
49
+ const ty = view[13] ?? 0;
50
+ const tz = view[14] ?? 0;
51
+ const cameraX = -((view[0] ?? 0) * tx + (view[1] ?? 0) * ty + (view[2] ?? 0) * tz);
52
+ const cameraY = -((view[4] ?? 0) * tx + (view[5] ?? 0) * ty + (view[6] ?? 0) * tz);
53
+ const cameraZ = -((view[8] ?? 0) * tx + (view[9] ?? 0) * ty + (view[10] ?? 0) * tz);
54
+ /*
55
+ * **Named by row then column, because binding these the other way is a transposed inverse and a
56
+ * plausible picture.** `model` is column-major, so the element at row `r` and column `c` is
57
+ * `model[c * 4 + r]` — while the cofactor formulas below are written the way every reference
58
+ * writes them, in rows. Reading three consecutive array entries as a row looks like a matching
59
+ * pattern and quietly inverts the transpose; it did here, and the ordering test caught it by a
60
+ * sign.
61
+ */
62
+ const a00 = model[0] ?? 0;
63
+ const a10 = model[1] ?? 0;
64
+ const a20 = model[2] ?? 0;
65
+ const a01 = model[4] ?? 0;
66
+ const a11 = model[5] ?? 0;
67
+ const a21 = model[6] ?? 0;
68
+ const a02 = model[8] ?? 0;
69
+ const a12 = model[9] ?? 0;
70
+ const a22 = model[10] ?? 0;
71
+ /*
72
+ * Mᵀd. Transposing turns the matrix's columns into rows, and a column of a column-major array is
73
+ * three adjacent entries — so this reads as three plain dot products against the array as
74
+ * stored, which is the one place the layout helps rather than hinders.
75
+ */
76
+ const rawX = a00 * forwardX + a10 * forwardY + a20 * forwardZ;
77
+ const rawY = a01 * forwardX + a11 * forwardY + a21 * forwardZ;
78
+ const rawZ = a02 * forwardX + a12 * forwardY + a22 * forwardZ;
79
+ const length = Math.hypot(rawX, rawY, rawZ);
80
+ /* Cofactors of the first row, which give the determinant and a third of the inverse at once. */
81
+ const c00 = a11 * a22 - a12 * a21;
82
+ const c01 = a12 * a20 - a10 * a22;
83
+ const c02 = a10 * a21 - a11 * a20;
84
+ const determinant = a00 * c00 + a01 * c01 + a02 * c02;
85
+ if (length === 0 ||
86
+ determinant === 0 ||
87
+ !Number.isFinite(length) ||
88
+ !Number.isFinite(determinant)) {
89
+ out.dirX = forwardX;
90
+ out.dirY = forwardY;
91
+ out.dirZ = forwardZ;
92
+ out.originX = cameraX;
93
+ out.originY = cameraY;
94
+ out.originZ = cameraZ;
95
+ return;
96
+ }
97
+ out.dirX = rawX / length;
98
+ out.dirY = rawY / length;
99
+ out.dirZ = rawZ / length;
100
+ const relativeX = cameraX - (model[12] ?? 0);
101
+ const relativeY = cameraY - (model[13] ?? 0);
102
+ const relativeZ = cameraZ - (model[14] ?? 0);
103
+ const inverse = 1 / determinant;
104
+ /* M⁻¹ = adj(M)/det, and the adjugate is the *transpose* of the cofactor matrix — which is why
105
+ c01 and c02 open the second and third rows here rather than the first. */
106
+ out.originX =
107
+ (c00 * relativeX + (a02 * a21 - a01 * a22) * relativeY + (a01 * a12 - a02 * a11) * relativeZ) *
108
+ inverse;
109
+ out.originY =
110
+ (c01 * relativeX + (a00 * a22 - a02 * a20) * relativeY + (a02 * a10 - a00 * a12) * relativeZ) *
111
+ inverse;
112
+ out.originZ =
113
+ (c02 * relativeX + (a01 * a20 - a00 * a21) * relativeY + (a00 * a11 - a01 * a10) * relativeZ) *
114
+ inverse;
115
+ }
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@driftengine/splats",
3
+ "version": "3.61.0",
4
+ "description": "Gaussian splat captures: readers, an off-frame sort, and a pass that composes into the scene",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "drift-source": "./src/index.ts",
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "./package.json": "./package.json",
16
+ "./*": "./*"
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "src",
21
+ "!src/**/*.test.ts",
22
+ "!src/**/*.test.mjs",
23
+ "!src/**/__snapshots__",
24
+ "README.md",
25
+ "LICENSE",
26
+ "NOTICE"
27
+ ],
28
+ "sideEffects": false,
29
+ "peerDependencies": {
30
+ "@driftengine/core": "3.61.0"
31
+ },
32
+ "author": "Drift Technologies",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/drftrun/driftengine.git",
36
+ "directory": "packages/splats"
37
+ },
38
+ "homepage": "https://github.com/drftrun/driftengine#readme",
39
+ "bugs": "https://github.com/drftrun/driftengine/issues",
40
+ "keywords": [
41
+ "driftengine",
42
+ "3d",
43
+ "webgl",
44
+ "webgpu",
45
+ "typescript",
46
+ "gaussian-splatting",
47
+ "radiance-field",
48
+ "point-cloud"
49
+ ],
50
+ "engines": {
51
+ "node": ">=22.12.0"
52
+ },
53
+ "publishConfig": {
54
+ "access": "public"
55
+ }
56
+ }
@@ -0,0 +1,36 @@
1
+ # The `.sog` fixture, and where each of its three files came from
2
+
3
+ **Nothing here was written by hand, and the chain matters more than the files.** Track O withdrew
4
+ its mock capability providers on the argument that a mock is a second implementation of a contract
5
+ with no first implementation to check it against, so anything it agrees with is itself. A `.sog`
6
+ fixture invented in this repository would have exactly that shape: the reader would agree with it
7
+ by construction, and the encoding it agreed on could be anybody's.
8
+
9
+ So the chain has an independent encoder and an independent decoder in it, and neither is ours:
10
+
11
+ 1. **`cloud.truth.json`** — sixty-four Gaussians with values chosen to be distinguishable: positions
12
+ on a curve, scales spread over an order of magnitude, a rotation that turns through three
13
+ radians, opacities from 0.15 to 0.95. These were written as a `.ply`, in the encodings a training
14
+ run writes — scale as its logarithm, opacity as its logit, rotation as `wxyz`.
15
+ 2. **`cloud.sog`** — that `.ply` through **`@playcanvas/splat-transform` 3.3.3**, which is the tool
16
+ a consumer with a capture actually runs and the reference implementation of the container. It is
17
+ a real bundle: a ZIP written streaming, so every local header carries a size of zero and the real
18
+ sizes are in the central directory, which is the detail a reader gets wrong first.
19
+ 3. **`cloud.texels.json`** — the five WebP images decoded to RGBA by **Pillow 10.2**, base64'd.
20
+ Committed because Node has no WebP decoder and this engine will not vendor one: `readSplatSog`
21
+ takes the decoder as a parameter, so the test supplies these and the browser supplies its own.
22
+
23
+ The test therefore compares values this repository chose against values this repository decoded out
24
+ of a file somebody else's encoder wrote and somebody else's decoder read. Nothing in that loop
25
+ checks itself.
26
+
27
+ `scripts/sog-check.mjs` closes the last gap by decoding the same bundle with the browser's own WebP
28
+ decoder, which is what `browserWebpDecoder` actually calls.
29
+
30
+ ## Reproducing it
31
+
32
+ ```sh
33
+ node scripts/sogFixture.mjs cloud.ply packages/splats/src/fixtures/cloud.truth.json
34
+ npx @playcanvas/splat-transform@3.3.3 cloud.ply packages/splats/src/fixtures/cloud.sog
35
+ python3 scripts/sogTexels.py packages/splats/src/fixtures/cloud.sog packages/splats/src/fixtures/cloud.texels.json
36
+ ```
Binary file
@@ -0,0 +1,27 @@
1
+ {
2
+ "means_l.webp": {
3
+ "width": 8,
4
+ "height": 8,
5
+ "rgba": "00Lt/wmN3v8fpCD/3k+i/4P25f8RjLb/X9bX//2Ogv9HJ3z/f79O/w4AAP+BAwH/3MpT/x5W9f9Kqen/YMgv/2K5yP9QhLf/LDT+//bWnv+wepz/WjX7/0C1l//k4sH/R4hd/2Dab/8pIQL/mLwg//Ugv/+AXO7//xGO/1u9LP9mHDv/1cwX/54D3P+36qv/GqGz/75BMv+jaNP/t40z/wFVnv97ROn/n+Bx/+2wsP8gYDn/3zll//X9bf8AAP7/fqPW/wnxXv+kSpX/TvV6/wgnDP9/PbD/fWnd//C3BP8iUEH/0glK/668Mf+cWsD/4GQ4/7Sfx/+e9fP/K//W/w=="
6
+ },
7
+ "means_u.webp": {
8
+ "width": 8,
9
+ "height": 8,
10
+ "rgba": "zwIA/8wEAf/IBwP/vxAG/7sVCP/ECwT/rSwR/7YcC/+yJA7/0wAA/9cAAP/aAAD/3QAA/+ECAP/kBAH/5wcD/+oLBP/tEAb/8BYI//IcC//1JA7/+C0R/6g2Ff+iQRn/nU4e/5dcI/+RbSn/in8v//o3Ff/9Qhn//08e/3yiPf91sUX/br1O/2jJV/9i0mL/Xdtu/1fjfP+DkjX/Te+X/0n0ov9E+Kz/UumK/zv9vv9A+7b/N//G/zP/zf8AgP//ApL//wWi//8Hsf7/Cr39/w3J/P8s/9n/Jfvj/yj93/8i+Oj/D9P6/xLb+P8V4/X/HvTs/xvv7/8Y6fL/MP/T/w=="
11
+ },
12
+ "scales.webp": {
13
+ "width": 8,
14
+ "height": 8,
15
+ "rgba": "moVj/5iEYv+Xg2H/koBe/49/Xf+UgmD/iHpa/419XP+Le1v/nIZk/52HZv+eiWf/n4po/6CMaf+hjWr/oo5s/6OQbf+kkW7/pZNv/6aVcP+nlnL/qJdz/4Z4WP+FdVf/g3FW/4FuVf9+a1T/fGhS/6mYdf+qmXb//5t3/3ViUP9vX0//alxO/2ZZTP9hVkv/XFNK/1dQSf95ZVH/TkpG/0lHRf9ERET/Uk1I/zo+Qv8/QUP/NjtA/zE4P/8AFir/BRcs/woZLf8OGi7/EBww/xIeMf8oMj3/Iyw6/yUvPP8gKTn/FB8y/xUhM/8WIjT/Hic4/xsmN/8YJDb/LDU+/w=="
16
+ },
17
+ "quats.webp": {
18
+ "width": 8,
19
+ "height": 8,
20
+ "rgba": "zuDN/9LfzP/V3sv/3dvJ/9rI4PzZ3cr/1sTb/NnH3/zXxt38yuHO/8biz//C48//vuTQ/7rl0f+25tH/sufS/67o0/+p6NP/penU/6Hp1P+d6tT/merV/9TD2vzSwtj80cDW/M+/1PzNvtP8zLzR/JTq1f+Q69X/jOvV/8i5zfzGuMv8xLbI/MK1xvzAs8T8vrHC/LywwPzKu8/8t6y7/LWqufyzqLb8uq69/K6lsfyxp7T8rKOv/KqhrPx/f3/8goGC/ISDhPyHhYf8iYeK/IyJjfylnaf8oJmi/KKbpfydl6D8jouP/JGNkvyTj5X8m5Wd/JmTmvyWkZj8p5+q/A=="
21
+ },
22
+ "sh0.webp": {
23
+ "width": 8,
24
+ "height": 8,
25
+ "rgba": "iCJavoYkWruEJlq3fihasXwrWq6BJ1q0dDFapHotWqt3L1qnix9awY4cWsSQG1rIkhpay5UYWs6YFFrRmRJa1ZoQWtidD1rbng1a3p8KWuKhCFrlowZa6HIyWqFwNFqebjZammw4WpdqOlqUaDxakaUEWuunAlrv/wBa8mRbWopiXVqHYF9ahF5hWoBcYlp9PmNaejxlWndmPVqNN2lacDVrWm0zbFpqOWdacy5vWmMxbVpnLHFaYCpzWl0Al1omA5RaKQWRWiwHkFovCY9aMw2NWjYmd1pWIXtaUCN5WlMdfVpND4paORGHWjwThVpAG39aSRqDWkYWhFpDJ3VaWg=="
26
+ }
27
+ }