@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.
- package/LICENSE +202 -0
- package/NOTICE +29 -0
- package/README.md +68 -0
- package/dist/browserFrames.d.ts +31 -0
- package/dist/browserFrames.js +105 -0
- package/dist/collide.d.ts +59 -0
- package/dist/collide.js +92 -0
- package/dist/decimate.d.ts +42 -0
- package/dist/decimate.js +439 -0
- package/dist/delight.d.ts +42 -0
- package/dist/delight.js +351 -0
- package/dist/delightScene.d.ts +37 -0
- package/dist/delightScene.js +183 -0
- package/dist/depth.d.ts +44 -0
- package/dist/depth.js +53 -0
- package/dist/features.d.ts +33 -0
- package/dist/features.js +260 -0
- package/dist/frames.d.ts +47 -0
- package/dist/frames.js +87 -0
- package/dist/fusion.d.ts +77 -0
- package/dist/fusion.js +152 -0
- package/dist/gaussians/adam.d.ts +57 -0
- package/dist/gaussians/adam.js +88 -0
- package/dist/gaussians/depth.d.ts +8 -0
- package/dist/gaussians/depth.js +73 -0
- package/dist/gaussians/gradients.d.ts +36 -0
- package/dist/gaussians/gradients.js +228 -0
- package/dist/gaussians/harmonics.d.ts +46 -0
- package/dist/gaussians/harmonics.js +99 -0
- package/dist/gaussians/optimise.d.ts +50 -0
- package/dist/gaussians/optimise.js +125 -0
- package/dist/gaussians/parameters.d.ts +35 -0
- package/dist/gaussians/parameters.js +165 -0
- package/dist/gaussians/project.d.ts +72 -0
- package/dist/gaussians/project.js +137 -0
- package/dist/gaussians/projectGradient.d.ts +15 -0
- package/dist/gaussians/projectGradient.js +213 -0
- package/dist/gaussians/rasterise.d.ts +13 -0
- package/dist/gaussians/rasterise.js +65 -0
- package/dist/gaussians/refine.d.ts +36 -0
- package/dist/gaussians/refine.js +136 -0
- package/dist/gaussians/similarity.d.ts +25 -0
- package/dist/gaussians/similarity.js +80 -0
- package/dist/index.d.ts +101 -0
- package/dist/index.js +96 -0
- package/dist/marching.d.ts +38 -0
- package/dist/marching.js +258 -0
- package/dist/masks.d.ts +59 -0
- package/dist/masks.js +165 -0
- package/dist/math/dense.d.ts +68 -0
- package/dist/math/dense.js +294 -0
- package/dist/math/schur.d.ts +23 -0
- package/dist/math/schur.js +140 -0
- package/dist/models/batchNorm.d.ts +17 -0
- package/dist/models/batchNorm.js +24 -0
- package/dist/models/clipTokenizer.d.ts +40 -0
- package/dist/models/clipTokenizer.js +122 -0
- package/dist/models/clipTower.d.ts +27 -0
- package/dist/models/clipTower.js +45 -0
- package/dist/models/depthAnything2.d.ts +38 -0
- package/dist/models/depthAnything2.js +163 -0
- package/dist/models/depthAnything3.d.ts +26 -0
- package/dist/models/depthAnything3.js +171 -0
- package/dist/models/dualDpt.d.ts +19 -0
- package/dist/models/dualDpt.js +138 -0
- package/dist/models/hiera.d.ts +49 -0
- package/dist/models/hiera.js +120 -0
- package/dist/models/hieraAttention.d.ts +16 -0
- package/dist/models/hieraAttention.js +68 -0
- package/dist/models/miniature.d.ts +36 -0
- package/dist/models/miniature.js +216 -0
- package/dist/models/mobileSam.d.ts +41 -0
- package/dist/models/mobileSam.js +88 -0
- package/dist/models/owlv2.d.ts +49 -0
- package/dist/models/owlv2.js +131 -0
- package/dist/models/owlv2Detect.d.ts +25 -0
- package/dist/models/owlv2Detect.js +80 -0
- package/dist/models/owlv2Miniature.d.ts +27 -0
- package/dist/models/owlv2Miniature.js +87 -0
- package/dist/models/rope.d.ts +23 -0
- package/dist/models/rope.js +84 -0
- package/dist/models/sam21.d.ts +45 -0
- package/dist/models/sam21.js +126 -0
- package/dist/models/sam21Miniature.d.ts +37 -0
- package/dist/models/sam21Miniature.js +194 -0
- package/dist/models/sam21Video.d.ts +30 -0
- package/dist/models/sam21Video.js +101 -0
- package/dist/models/sam2Choice.d.ts +19 -0
- package/dist/models/sam2Choice.js +55 -0
- package/dist/models/sam2Decoder.d.ts +36 -0
- package/dist/models/sam2Decoder.js +108 -0
- package/dist/models/sam2Memories.d.ts +54 -0
- package/dist/models/sam2Memories.js +93 -0
- package/dist/models/sam2Memory.d.ts +30 -0
- package/dist/models/sam2Memory.js +136 -0
- package/dist/models/sam2Positions.d.ts +24 -0
- package/dist/models/sam2Positions.js +94 -0
- package/dist/models/sam2Tracker.d.ts +69 -0
- package/dist/models/sam2Tracker.js +172 -0
- package/dist/models/samDecoder.d.ts +45 -0
- package/dist/models/samDecoder.js +96 -0
- package/dist/models/samMiniature.d.ts +43 -0
- package/dist/models/samMiniature.js +161 -0
- package/dist/models/samPrompt.d.ts +57 -0
- package/dist/models/samPrompt.js +160 -0
- package/dist/models/seeded.d.ts +16 -0
- package/dist/models/seeded.js +48 -0
- package/dist/models/tinyVit.d.ts +38 -0
- package/dist/models/tinyVit.js +82 -0
- package/dist/models/twoWay.d.ts +46 -0
- package/dist/models/twoWay.js +67 -0
- package/dist/models/vit.d.ts +45 -0
- package/dist/models/vit.js +157 -0
- package/dist/models/windowAttention.d.ts +25 -0
- package/dist/models/windowAttention.js +104 -0
- package/dist/poses.d.ts +31 -0
- package/dist/poses.js +750 -0
- package/dist/prepare.d.ts +30 -0
- package/dist/prepare.js +238 -0
- package/dist/propose.d.ts +36 -0
- package/dist/propose.js +63 -0
- package/dist/resample.d.ts +39 -0
- package/dist/resample.js +165 -0
- package/dist/run.d.ts +3 -0
- package/dist/run.js +1 -0
- package/dist/scene.d.ts +104 -0
- package/dist/scene.js +109 -0
- package/dist/segment.d.ts +40 -0
- package/dist/segment.js +184 -0
- package/dist/testScene.d.ts +55 -0
- package/dist/testScene.js +214 -0
- package/dist/twoView.d.ts +22 -0
- package/dist/twoView.js +435 -0
- package/package.json +63 -0
- package/src/browserFrames.ts +145 -0
- package/src/collide.ts +128 -0
- package/src/decimate.ts +510 -0
- package/src/delight.ts +427 -0
- package/src/delightScene.ts +249 -0
- package/src/depth.ts +100 -0
- package/src/features.ts +302 -0
- package/src/frames.ts +111 -0
- package/src/fusion.ts +229 -0
- package/src/gaussians/adam.ts +131 -0
- package/src/gaussians/depth.ts +87 -0
- package/src/gaussians/gradients.ts +285 -0
- package/src/gaussians/harmonics.ts +122 -0
- package/src/gaussians/optimise.ts +191 -0
- package/src/gaussians/parameters.ts +186 -0
- package/src/gaussians/project.ts +207 -0
- package/src/gaussians/projectGradient.ts +234 -0
- package/src/gaussians/rasterise.ts +81 -0
- package/src/gaussians/refine.ts +176 -0
- package/src/gaussians/similarity.ts +87 -0
- package/src/index.ts +225 -0
- package/src/marching.ts +358 -0
- package/src/masks.ts +201 -0
- package/src/math/dense.ts +317 -0
- package/src/math/schur.ts +167 -0
- package/src/models/batchNorm.ts +57 -0
- package/src/models/clipTokenizer.ts +129 -0
- package/src/models/clipTower.ts +87 -0
- package/src/models/depthAnything2.ts +276 -0
- package/src/models/depthAnything3.ts +229 -0
- package/src/models/dualDpt.ts +233 -0
- package/src/models/hiera.ts +231 -0
- package/src/models/hieraAttention.ts +121 -0
- package/src/models/miniature.ts +241 -0
- package/src/models/mobileSam.ts +135 -0
- package/src/models/owlv2.ts +190 -0
- package/src/models/owlv2Detect.ts +116 -0
- package/src/models/owlv2Miniature.ts +108 -0
- package/src/models/rope.ts +111 -0
- package/src/models/sam21.ts +182 -0
- package/src/models/sam21Miniature.ts +225 -0
- package/src/models/sam21Video.ts +154 -0
- package/src/models/sam2Choice.ts +62 -0
- package/src/models/sam2Decoder.ts +218 -0
- package/src/models/sam2Memories.ts +165 -0
- package/src/models/sam2Memory.ts +247 -0
- package/src/models/sam2Positions.ts +106 -0
- package/src/models/sam2Tracker.ts +300 -0
- package/src/models/samDecoder.ts +225 -0
- package/src/models/samMiniature.ts +190 -0
- package/src/models/samPrompt.ts +227 -0
- package/src/models/seeded.ts +50 -0
- package/src/models/tinyVit.ts +178 -0
- package/src/models/twoWay.ts +122 -0
- package/src/models/vit.ts +265 -0
- package/src/models/windowAttention.ts +155 -0
- package/src/poses.ts +894 -0
- package/src/prepare.ts +306 -0
- package/src/propose.ts +96 -0
- package/src/resample.ts +197 -0
- package/src/run.ts +7 -0
- package/src/scene.ts +141 -0
- package/src/segment.ts +220 -0
- package/src/testScene.ts +283 -0
- 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
|
+
}
|