@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,285 @@
1
+ /** The scheduler: one sort in flight, newest view wins, and the worker is a default not a mechanism. */
2
+
3
+ import { createSplatSortScratch, sortSplatsByDepth } from './splatSort.ts';
4
+ import { createSplatSortWorker } from './splatSortWorker.ts';
5
+ import type {
6
+ SplatSortFn,
7
+ SplatSortRequest,
8
+ SplatSortResult,
9
+ SplatSortScratch,
10
+ } from './splatSort.ts';
11
+ import type { SplatData } from './splatData.ts';
12
+ import type { SplatViewLocal } from './splatView.ts';
13
+
14
+ /**
15
+ * How far the view may turn before the order is worth recomputing.
16
+ *
17
+ * A dot product, so 0.999 is about 2.6 degrees. Below that the change in ordering is a handful of
18
+ * adjacent swaps among splats that overlap anyway, and re-sorting for it would mean a sort every
19
+ * frame at any camera speed — which is the cost this whole arrangement exists to avoid. **A still
20
+ * camera sorts zero times a second**, which is the property to preserve.
21
+ */
22
+ const RESORT_DOT = 0.999;
23
+
24
+ /**
25
+ * How far the camera may move, as a fraction of the capture's own radius, before a re-sort.
26
+ *
27
+ * **A fraction rather than a distance, because a distance means different things for a room and
28
+ * for a landscape.** Only consulted when there is a budget: see `frame`.
29
+ */
30
+ const RESORT_MOVE_FRACTION = 0.1;
31
+
32
+ export interface SplatSorterOptions {
33
+ /** The capture. Its positions, extents and bounds are all read; its packed texels are not. */
34
+ readonly splats: SplatData;
35
+ /** Defaults to a `Blob` worker, falling back to the main thread. See `createDefaultSplatSort`. */
36
+ readonly sort?: SplatSortFn;
37
+ /**
38
+ * The most splats to draw. Zero, or anything at or above the capture's count, draws all of them.
39
+ *
40
+ * `defaultSplatBudget` is what a consumer that has no number of its own should pass.
41
+ */
42
+ readonly budget?: number;
43
+ /** Overrides `RESORT_MOVE_FRACTION`. Only consulted when there is a budget. */
44
+ readonly moveFraction?: number;
45
+ /**
46
+ * How many of the capture's splats have actually arrived. Defaults to all of them.
47
+ *
48
+ * **A capability rather than a number, because it changes while a capture streams.** A
49
+ * `SplatCapture` is allocated at its final count and filled block by block, so its unarrived
50
+ * tail is zeroed — every phantom splat sitting at the origin with no size. Sorting those would
51
+ * order a cloud of them into the middle of the capture and draw them, which reads as a corrupt
52
+ * file rather than as a load in progress. Pass `() => capture.ready` and this follows the load.
53
+ */
54
+ readonly ready?: () => number;
55
+ }
56
+
57
+ /**
58
+ * The sort, on the main thread, right now.
59
+ *
60
+ * The fallback when a worker cannot be built, and the implementation a test injects. Synchronous
61
+ * inside a promise: there is nothing to await, and pretending otherwise would hide that this
62
+ * blocks.
63
+ *
64
+ * **The scratch is cached across calls and grown when it has to be.** It is 384 KB of histogram
65
+ * plus four bytes a splat, and allocating that per sort would make the fallback path generate more
66
+ * garbage than the work it is doing. Sharing one scratch between two captures is safe because this
67
+ * is synchronous: no second sort can start while one is running on this thread.
68
+ */
69
+ let mainThreadScratch: SplatSortScratch | null = null;
70
+
71
+ export function sortOnMainThread(request: SplatSortRequest): Promise<SplatSortResult> {
72
+ if (mainThreadScratch === null || mainThreadScratch.keys.length < request.count) {
73
+ mainThreadScratch = createSplatSortScratch(request.count);
74
+ }
75
+ const count = sortSplatsByDepth(request, mainThreadScratch);
76
+ return Promise.resolve({ order: request.out, count });
77
+ }
78
+
79
+ /**
80
+ * The shipped capability: a worker over the same sort this module runs on the main thread.
81
+ *
82
+ * `sortSplatsByDepth.toString()` is what goes into the worker, so there is one implementation of
83
+ * the decision and not two — the 2026-08-17 rule, which is about exactly this. It works because
84
+ * that function closes over nothing: its scratch and its two bucket counts all arrive as
85
+ * arguments.
86
+ */
87
+ export function createDefaultSplatSort(): SplatSortFn {
88
+ return createSplatSortWorker(sortSplatsByDepth.toString());
89
+ }
90
+
91
+ /**
92
+ * Hold the newest order, ask for a new one when the view has changed enough, and never queue.
93
+ *
94
+ * **At most one sort in flight, and a request while one is running is dropped rather than
95
+ * queued.** The newest view is the only one worth sorting for: a queue would sort for a camera
96
+ * position the player has already left, and then sort again, so it converts a busy moment into a
97
+ * backlog that never catches up.
98
+ *
99
+ * **Two buffers, ping-ponged.** One is being filled by the sorter and the other is what the pass
100
+ * draws from, so a sort landing mid-frame never rewrites the order under a draw call.
101
+ */
102
+ export class SplatSorter {
103
+ private readonly splats: SplatData;
104
+ private readonly sort: SplatSortFn;
105
+ private readonly budget: number;
106
+ private readonly moveFraction: number;
107
+ private readonly ready: () => number;
108
+ /** How many splats the standing order was computed over, so an arrival is a reason to re-sort. */
109
+ private sortedReady = 0;
110
+ /** Half the diagonal of the capture's own bounds. The scale the move gate is a fraction of. */
111
+ private readonly radius: number;
112
+ private readonly buffers: [Uint32Array, Uint32Array];
113
+ /** Which buffer the sorter owns. The other is the one a caller may read. */
114
+ private filling = 0;
115
+ private inFlight = false;
116
+ /** The view the standing order was computed for. Meaningless until `hasSorted`. */
117
+ private sortedDirX = 0;
118
+ private sortedDirY = 0;
119
+ private sortedDirZ = 0;
120
+ private sortedOriginX = 0;
121
+ private sortedOriginY = 0;
122
+ private sortedOriginZ = 0;
123
+ private hasSorted = false;
124
+ /** How many indices the standing order actually holds, which a budget makes smaller. */
125
+ private kept = 0;
126
+ /** Bumped by every sort that lands, so a caller can tell a new order from the one it has. */
127
+ private generation = 0;
128
+
129
+ constructor(options: SplatSorterOptions) {
130
+ this.splats = options.splats;
131
+ /*
132
+ * The worker is the default and the main thread is the fallback, and neither is the mechanism:
133
+ * a caller that supplies its own never reaches either. Built once per sorter rather than once
134
+ * per module, so two captures do not share one worker and serialise behind each other.
135
+ */
136
+ this.sort = options.sort ?? createDefaultSplatSort();
137
+ this.budget = options.budget ?? 0;
138
+ this.moveFraction = options.moveFraction ?? RESORT_MOVE_FRACTION;
139
+ this.ready = options.ready ?? ((): number => options.splats.count);
140
+ const { boundsMin, boundsMax } = options.splats;
141
+ this.radius =
142
+ 0.5 *
143
+ Math.hypot(
144
+ (boundsMax[0] ?? 0) - (boundsMin[0] ?? 0),
145
+ (boundsMax[1] ?? 0) - (boundsMin[1] ?? 0),
146
+ (boundsMax[2] ?? 0) - (boundsMin[2] ?? 0),
147
+ );
148
+ const size = Math.max(1, options.splats.count);
149
+ this.buffers = [new Uint32Array(size), new Uint32Array(size)];
150
+ }
151
+
152
+ /** The standing order, or null until the first sort lands. */
153
+ get order(): Uint32Array | null {
154
+ return this.hasSorted ? (this.buffers[1 - this.filling] as Uint32Array) : null;
155
+ }
156
+
157
+ /** How many entries of `order` are real. Below the capture's count when a budget bit. */
158
+ get drawCount(): number {
159
+ return this.kept;
160
+ }
161
+
162
+ /** How many orders have landed. A caller uploads when this changes and not otherwise. */
163
+ get version(): number {
164
+ return this.generation;
165
+ }
166
+
167
+ /**
168
+ * Whether a sort is in flight.
169
+ *
170
+ * **What a measuring page waits on, and it has to wait on more than `version`.** With a budget
171
+ * the sort chooses *which* splats are drawn and not merely their order, so a capture
172
+ * photographed at the first landed sort is a capture photographed at whichever sort the worker
173
+ * happened to finish — and two backends that reach the held frame at different wall-clock times
174
+ * then draw different splats. A held camera is settled when this is false and the standing
175
+ * `version` has been uploaded: at a fixed direction `frame` asks for nothing further, so that
176
+ * state is reached and then keeps.
177
+ */
178
+ get sorting(): boolean {
179
+ return this.inFlight;
180
+ }
181
+
182
+ /**
183
+ * Offer the current view, in the capture's own space. Starts a sort if one is wanted.
184
+ *
185
+ * `resolveSplatView` is what turns a camera and a model matrix into this. `force` is for the
186
+ * first frame and for a capture whose transform moved, where the view in its own space may be
187
+ * unchanged and the order is stale anyway.
188
+ *
189
+ * **Turning always matters; moving only matters when there is a budget.** Depth is measured
190
+ * along the view axis, so translating the camera shifts every splat's depth by the same amount
191
+ * and leaves the ordering exactly as it was — which is why this gate did not exist until a
192
+ * budget did. A budget keeps the splats largest on screen, which is the extent over the
193
+ * *distance*, so a camera that walks across a capture without turning changes which splats are
194
+ * drawn. What that costs is a sort every tenth of a capture-radius while a viewer moves; what
195
+ * would make it wrong is a budget so generous that nothing is ever dropped, where the gate spends
196
+ * sorts to reach the same answer.
197
+ */
198
+ frame(local: SplatViewLocal, force = false): void {
199
+ const count = Math.max(0, Math.min(this.ready(), this.splats.count));
200
+ if (count <= 0) return;
201
+ if (this.inFlight) return;
202
+
203
+ /*
204
+ * **An arrival is a reason to sort even from a camera that has not moved.** The order this
205
+ * sorter holds is an order over fewer splats than the capture now has, so it is not the order
206
+ * it wants — and a viewer watching a capture load is exactly the viewer who is standing
207
+ * still. Without this a stream stops densifying the moment nobody moves.
208
+ */
209
+ let wanted = force || !this.hasSorted || count !== this.sortedReady;
210
+ if (!wanted) {
211
+ const turned =
212
+ local.dirX * this.sortedDirX + local.dirY * this.sortedDirY + local.dirZ * this.sortedDirZ <
213
+ RESORT_DOT;
214
+ wanted = turned;
215
+ }
216
+ if (!wanted && this.budget > 0 && this.budget < count && this.radius > 0) {
217
+ const moved = Math.hypot(
218
+ local.originX - this.sortedOriginX,
219
+ local.originY - this.sortedOriginY,
220
+ local.originZ - this.sortedOriginZ,
221
+ );
222
+ wanted = moved > this.moveFraction * this.radius;
223
+ }
224
+ if (!wanted) return;
225
+
226
+ this.inFlight = true;
227
+ const target = this.buffers[this.filling] as Uint32Array;
228
+ /*
229
+ * Snapshotted before the call, because `local` belongs to the caller and is rewritten in place
230
+ * every frame — so reading it again when the promise settles would record the view the camera
231
+ * has *now* as the one this order was computed for, and the re-sort gates would then compare
232
+ * against a lie. Six numbers on the stack rather than a copy of the object, so nothing is
233
+ * allocated for it.
234
+ */
235
+ const dirX = local.dirX;
236
+ const dirY = local.dirY;
237
+ const dirZ = local.dirZ;
238
+ const originX = local.originX;
239
+ const originY = local.originY;
240
+ const originZ = local.originZ;
241
+ void this.sort({
242
+ positions: this.splats.positions,
243
+ extents: this.splats.extents,
244
+ count,
245
+ dirX,
246
+ dirY,
247
+ dirZ,
248
+ originX,
249
+ originY,
250
+ originZ,
251
+ budget: this.budget,
252
+ out: target,
253
+ })
254
+ .then((result) => {
255
+ /*
256
+ * The buffer that comes back is the one that was handed out — a worker transfers it away
257
+ * and transfers it back, so the array identity may differ even though the storage is the
258
+ * same. Store what arrived rather than what was sent.
259
+ */
260
+ this.buffers[this.filling] = result.order;
261
+ this.filling = 1 - this.filling;
262
+ this.sortedDirX = dirX;
263
+ this.sortedDirY = dirY;
264
+ this.sortedDirZ = dirZ;
265
+ this.sortedOriginX = originX;
266
+ this.sortedOriginY = originY;
267
+ this.sortedOriginZ = originZ;
268
+ this.kept = result.count;
269
+ this.sortedReady = count;
270
+ this.hasSorted = true;
271
+ this.generation++;
272
+ })
273
+ .catch(() => {
274
+ /*
275
+ * **A rejected sort leaves the previous order drawing**, which is the honest degradation:
276
+ * a capture one camera step out of order is very slightly wrong at some silhouettes, and a
277
+ * capture with no order at all is not drawn. Swallowed rather than rethrown because this
278
+ * is reached from a frame and the loop may not throw.
279
+ */
280
+ })
281
+ .finally(() => {
282
+ this.inFlight = false;
283
+ });
284
+ }
285
+ }
@@ -0,0 +1,147 @@
1
+ /** Where the camera is, and which way it faces, expressed in one capture's own space. */
2
+
3
+ /**
4
+ * The camera as the sorter needs it: in the capture's coordinates rather than the world's.
5
+ *
6
+ * Mutable and filled in place, because this is resolved once a frame per batch and the engine's
7
+ * rule about per-frame allocation binds a package exactly as it binds the renderer.
8
+ */
9
+ export interface SplatViewLocal {
10
+ /** The camera's forward in the capture's own space, unit length. */
11
+ dirX: number;
12
+ dirY: number;
13
+ dirZ: number;
14
+ /** The camera's position in the capture's own space. */
15
+ originX: number;
16
+ originY: number;
17
+ originZ: number;
18
+ }
19
+
20
+ export function createSplatViewLocal(): SplatViewLocal {
21
+ return { dirX: 0, dirY: 0, dirZ: -1, originX: 0, originY: 0, originZ: 0 };
22
+ }
23
+
24
+ /**
25
+ * Fill `out` with the camera, in the space the capture's own positions are written in.
26
+ *
27
+ * **One sorter serves one batch, and this is what makes that affordable.** A batch has a model
28
+ * matrix so that two captures can compose in one scene; its splat positions are in its own frame
29
+ * and the sort reads them there, so asking the sort for a *world* direction would mean
30
+ * transforming a million positions every time the view turned. Transforming the camera instead is
31
+ * six numbers.
32
+ *
33
+ * **The direction is the model's transpose and not its inverse, and the two disagree exactly where
34
+ * it matters.** What the sort needs is an ordering that matches the depth those splats really have
35
+ * once the model has moved them, and the world depth of a capture-space point `p` is
36
+ * `dot(M p + t - c, d)`, which rearranges to `dot(p, Mᵀd)` plus a constant. So `Mᵀd` is the
37
+ * direction that orders correctly for *any* invertible model, including one with a non-uniform
38
+ * scale; `M⁻¹d` is the direction that would be right if the transform were a rotation, and a
39
+ * plausible-looking answer everywhere else. `splatView.test.ts` asserts the ordering rather than
40
+ * the arithmetic, which is why the model it uses is stretched.
41
+ *
42
+ * The origin is the inverse, because that genuinely is a point: `M⁻¹(c − t)` is where the camera
43
+ * sits in the capture's frame, and it is what the budget's distances are measured from. It also
44
+ * happens to be exactly the point whose projection along `Mᵀd` cancels the constant above, so the
45
+ * two halves agree by construction rather than by arrangement.
46
+ *
47
+ * **What this gives up**: the ordering is along the view *axis* rather than by distance to the
48
+ * camera point, so two splats at equal depth and far apart across the frame are ordered by a plane
49
+ * rather than by a sphere. That is the ordering every splat renderer uses and it costs nothing
50
+ * until a capture wraps around the viewer. What would make it wrong is exactly that case —
51
+ * standing inside a capture at a wide field of view, where the error shows at the frame's corners.
52
+ *
53
+ * **A singular model falls back to the world rather than to `NaN`.** This is reached from a frame
54
+ * and the loop may not throw; a `NaN` direction is a sort in which every comparison is false,
55
+ * which is silently input order rather than a visible fault.
56
+ */
57
+ export function resolveSplatView(
58
+ view: ArrayLike<number>,
59
+ model: ArrayLike<number>,
60
+ out: SplatViewLocal,
61
+ ): void {
62
+ /*
63
+ * The camera's forward in the world is the third row of the view rotation, negated: the view
64
+ * matrix takes world to camera and a camera looks down its own -z.
65
+ */
66
+ const forwardX = -(view[2] ?? 0);
67
+ const forwardY = -(view[6] ?? 0);
68
+ const forwardZ = -(view[10] ?? 0);
69
+
70
+ /* The camera's position is -Rᵀt, which for a rigid view matrix is an exact inverse and costs
71
+ nine multiplies rather than a general inversion. */
72
+ const tx = view[12] ?? 0;
73
+ const ty = view[13] ?? 0;
74
+ const tz = view[14] ?? 0;
75
+ const cameraX = -((view[0] ?? 0) * tx + (view[1] ?? 0) * ty + (view[2] ?? 0) * tz);
76
+ const cameraY = -((view[4] ?? 0) * tx + (view[5] ?? 0) * ty + (view[6] ?? 0) * tz);
77
+ const cameraZ = -((view[8] ?? 0) * tx + (view[9] ?? 0) * ty + (view[10] ?? 0) * tz);
78
+
79
+ /*
80
+ * **Named by row then column, because binding these the other way is a transposed inverse and a
81
+ * plausible picture.** `model` is column-major, so the element at row `r` and column `c` is
82
+ * `model[c * 4 + r]` — while the cofactor formulas below are written the way every reference
83
+ * writes them, in rows. Reading three consecutive array entries as a row looks like a matching
84
+ * pattern and quietly inverts the transpose; it did here, and the ordering test caught it by a
85
+ * sign.
86
+ */
87
+ const a00 = model[0] ?? 0;
88
+ const a10 = model[1] ?? 0;
89
+ const a20 = model[2] ?? 0;
90
+ const a01 = model[4] ?? 0;
91
+ const a11 = model[5] ?? 0;
92
+ const a21 = model[6] ?? 0;
93
+ const a02 = model[8] ?? 0;
94
+ const a12 = model[9] ?? 0;
95
+ const a22 = model[10] ?? 0;
96
+
97
+ /*
98
+ * Mᵀd. Transposing turns the matrix's columns into rows, and a column of a column-major array is
99
+ * three adjacent entries — so this reads as three plain dot products against the array as
100
+ * stored, which is the one place the layout helps rather than hinders.
101
+ */
102
+ const rawX = a00 * forwardX + a10 * forwardY + a20 * forwardZ;
103
+ const rawY = a01 * forwardX + a11 * forwardY + a21 * forwardZ;
104
+ const rawZ = a02 * forwardX + a12 * forwardY + a22 * forwardZ;
105
+ const length = Math.hypot(rawX, rawY, rawZ);
106
+
107
+ /* Cofactors of the first row, which give the determinant and a third of the inverse at once. */
108
+ const c00 = a11 * a22 - a12 * a21;
109
+ const c01 = a12 * a20 - a10 * a22;
110
+ const c02 = a10 * a21 - a11 * a20;
111
+ const determinant = a00 * c00 + a01 * c01 + a02 * c02;
112
+
113
+ if (
114
+ length === 0 ||
115
+ determinant === 0 ||
116
+ !Number.isFinite(length) ||
117
+ !Number.isFinite(determinant)
118
+ ) {
119
+ out.dirX = forwardX;
120
+ out.dirY = forwardY;
121
+ out.dirZ = forwardZ;
122
+ out.originX = cameraX;
123
+ out.originY = cameraY;
124
+ out.originZ = cameraZ;
125
+ return;
126
+ }
127
+
128
+ out.dirX = rawX / length;
129
+ out.dirY = rawY / length;
130
+ out.dirZ = rawZ / length;
131
+
132
+ const relativeX = cameraX - (model[12] ?? 0);
133
+ const relativeY = cameraY - (model[13] ?? 0);
134
+ const relativeZ = cameraZ - (model[14] ?? 0);
135
+ const inverse = 1 / determinant;
136
+ /* M⁻¹ = adj(M)/det, and the adjugate is the *transpose* of the cofactor matrix — which is why
137
+ c01 and c02 open the second and third rows here rather than the first. */
138
+ out.originX =
139
+ (c00 * relativeX + (a02 * a21 - a01 * a22) * relativeY + (a01 * a12 - a02 * a11) * relativeZ) *
140
+ inverse;
141
+ out.originY =
142
+ (c01 * relativeX + (a00 * a22 - a02 * a20) * relativeY + (a02 * a10 - a00 * a12) * relativeZ) *
143
+ inverse;
144
+ out.originZ =
145
+ (c02 * relativeX + (a01 * a20 - a00 * a21) * relativeY + (a00 * a11 - a01 * a10) * relativeZ) *
146
+ inverse;
147
+ }