@driftengine/capture 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (199) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +29 -0
  3. package/README.md +68 -0
  4. package/dist/browserFrames.d.ts +31 -0
  5. package/dist/browserFrames.js +105 -0
  6. package/dist/collide.d.ts +59 -0
  7. package/dist/collide.js +92 -0
  8. package/dist/decimate.d.ts +42 -0
  9. package/dist/decimate.js +439 -0
  10. package/dist/delight.d.ts +42 -0
  11. package/dist/delight.js +351 -0
  12. package/dist/delightScene.d.ts +37 -0
  13. package/dist/delightScene.js +183 -0
  14. package/dist/depth.d.ts +44 -0
  15. package/dist/depth.js +53 -0
  16. package/dist/features.d.ts +33 -0
  17. package/dist/features.js +260 -0
  18. package/dist/frames.d.ts +47 -0
  19. package/dist/frames.js +87 -0
  20. package/dist/fusion.d.ts +77 -0
  21. package/dist/fusion.js +152 -0
  22. package/dist/gaussians/adam.d.ts +57 -0
  23. package/dist/gaussians/adam.js +88 -0
  24. package/dist/gaussians/depth.d.ts +8 -0
  25. package/dist/gaussians/depth.js +73 -0
  26. package/dist/gaussians/gradients.d.ts +36 -0
  27. package/dist/gaussians/gradients.js +228 -0
  28. package/dist/gaussians/harmonics.d.ts +46 -0
  29. package/dist/gaussians/harmonics.js +99 -0
  30. package/dist/gaussians/optimise.d.ts +50 -0
  31. package/dist/gaussians/optimise.js +125 -0
  32. package/dist/gaussians/parameters.d.ts +35 -0
  33. package/dist/gaussians/parameters.js +165 -0
  34. package/dist/gaussians/project.d.ts +72 -0
  35. package/dist/gaussians/project.js +137 -0
  36. package/dist/gaussians/projectGradient.d.ts +15 -0
  37. package/dist/gaussians/projectGradient.js +213 -0
  38. package/dist/gaussians/rasterise.d.ts +13 -0
  39. package/dist/gaussians/rasterise.js +65 -0
  40. package/dist/gaussians/refine.d.ts +36 -0
  41. package/dist/gaussians/refine.js +136 -0
  42. package/dist/gaussians/similarity.d.ts +25 -0
  43. package/dist/gaussians/similarity.js +80 -0
  44. package/dist/index.d.ts +101 -0
  45. package/dist/index.js +96 -0
  46. package/dist/marching.d.ts +38 -0
  47. package/dist/marching.js +258 -0
  48. package/dist/masks.d.ts +59 -0
  49. package/dist/masks.js +165 -0
  50. package/dist/math/dense.d.ts +68 -0
  51. package/dist/math/dense.js +294 -0
  52. package/dist/math/schur.d.ts +23 -0
  53. package/dist/math/schur.js +140 -0
  54. package/dist/models/batchNorm.d.ts +17 -0
  55. package/dist/models/batchNorm.js +24 -0
  56. package/dist/models/clipTokenizer.d.ts +40 -0
  57. package/dist/models/clipTokenizer.js +122 -0
  58. package/dist/models/clipTower.d.ts +27 -0
  59. package/dist/models/clipTower.js +45 -0
  60. package/dist/models/depthAnything2.d.ts +38 -0
  61. package/dist/models/depthAnything2.js +163 -0
  62. package/dist/models/depthAnything3.d.ts +26 -0
  63. package/dist/models/depthAnything3.js +171 -0
  64. package/dist/models/dualDpt.d.ts +19 -0
  65. package/dist/models/dualDpt.js +138 -0
  66. package/dist/models/hiera.d.ts +49 -0
  67. package/dist/models/hiera.js +120 -0
  68. package/dist/models/hieraAttention.d.ts +16 -0
  69. package/dist/models/hieraAttention.js +68 -0
  70. package/dist/models/miniature.d.ts +36 -0
  71. package/dist/models/miniature.js +216 -0
  72. package/dist/models/mobileSam.d.ts +41 -0
  73. package/dist/models/mobileSam.js +88 -0
  74. package/dist/models/owlv2.d.ts +49 -0
  75. package/dist/models/owlv2.js +131 -0
  76. package/dist/models/owlv2Detect.d.ts +25 -0
  77. package/dist/models/owlv2Detect.js +80 -0
  78. package/dist/models/owlv2Miniature.d.ts +27 -0
  79. package/dist/models/owlv2Miniature.js +87 -0
  80. package/dist/models/rope.d.ts +23 -0
  81. package/dist/models/rope.js +84 -0
  82. package/dist/models/sam21.d.ts +45 -0
  83. package/dist/models/sam21.js +126 -0
  84. package/dist/models/sam21Miniature.d.ts +37 -0
  85. package/dist/models/sam21Miniature.js +194 -0
  86. package/dist/models/sam21Video.d.ts +30 -0
  87. package/dist/models/sam21Video.js +101 -0
  88. package/dist/models/sam2Choice.d.ts +19 -0
  89. package/dist/models/sam2Choice.js +55 -0
  90. package/dist/models/sam2Decoder.d.ts +36 -0
  91. package/dist/models/sam2Decoder.js +108 -0
  92. package/dist/models/sam2Memories.d.ts +54 -0
  93. package/dist/models/sam2Memories.js +93 -0
  94. package/dist/models/sam2Memory.d.ts +30 -0
  95. package/dist/models/sam2Memory.js +136 -0
  96. package/dist/models/sam2Positions.d.ts +24 -0
  97. package/dist/models/sam2Positions.js +94 -0
  98. package/dist/models/sam2Tracker.d.ts +69 -0
  99. package/dist/models/sam2Tracker.js +172 -0
  100. package/dist/models/samDecoder.d.ts +45 -0
  101. package/dist/models/samDecoder.js +96 -0
  102. package/dist/models/samMiniature.d.ts +43 -0
  103. package/dist/models/samMiniature.js +161 -0
  104. package/dist/models/samPrompt.d.ts +57 -0
  105. package/dist/models/samPrompt.js +160 -0
  106. package/dist/models/seeded.d.ts +16 -0
  107. package/dist/models/seeded.js +48 -0
  108. package/dist/models/tinyVit.d.ts +38 -0
  109. package/dist/models/tinyVit.js +82 -0
  110. package/dist/models/twoWay.d.ts +46 -0
  111. package/dist/models/twoWay.js +67 -0
  112. package/dist/models/vit.d.ts +45 -0
  113. package/dist/models/vit.js +157 -0
  114. package/dist/models/windowAttention.d.ts +25 -0
  115. package/dist/models/windowAttention.js +104 -0
  116. package/dist/poses.d.ts +31 -0
  117. package/dist/poses.js +750 -0
  118. package/dist/prepare.d.ts +30 -0
  119. package/dist/prepare.js +238 -0
  120. package/dist/propose.d.ts +36 -0
  121. package/dist/propose.js +63 -0
  122. package/dist/resample.d.ts +39 -0
  123. package/dist/resample.js +165 -0
  124. package/dist/run.d.ts +3 -0
  125. package/dist/run.js +1 -0
  126. package/dist/scene.d.ts +104 -0
  127. package/dist/scene.js +109 -0
  128. package/dist/segment.d.ts +40 -0
  129. package/dist/segment.js +184 -0
  130. package/dist/testScene.d.ts +55 -0
  131. package/dist/testScene.js +214 -0
  132. package/dist/twoView.d.ts +22 -0
  133. package/dist/twoView.js +435 -0
  134. package/package.json +63 -0
  135. package/src/browserFrames.ts +145 -0
  136. package/src/collide.ts +128 -0
  137. package/src/decimate.ts +510 -0
  138. package/src/delight.ts +427 -0
  139. package/src/delightScene.ts +249 -0
  140. package/src/depth.ts +100 -0
  141. package/src/features.ts +302 -0
  142. package/src/frames.ts +111 -0
  143. package/src/fusion.ts +229 -0
  144. package/src/gaussians/adam.ts +131 -0
  145. package/src/gaussians/depth.ts +87 -0
  146. package/src/gaussians/gradients.ts +285 -0
  147. package/src/gaussians/harmonics.ts +122 -0
  148. package/src/gaussians/optimise.ts +191 -0
  149. package/src/gaussians/parameters.ts +186 -0
  150. package/src/gaussians/project.ts +207 -0
  151. package/src/gaussians/projectGradient.ts +234 -0
  152. package/src/gaussians/rasterise.ts +81 -0
  153. package/src/gaussians/refine.ts +176 -0
  154. package/src/gaussians/similarity.ts +87 -0
  155. package/src/index.ts +225 -0
  156. package/src/marching.ts +358 -0
  157. package/src/masks.ts +201 -0
  158. package/src/math/dense.ts +317 -0
  159. package/src/math/schur.ts +167 -0
  160. package/src/models/batchNorm.ts +57 -0
  161. package/src/models/clipTokenizer.ts +129 -0
  162. package/src/models/clipTower.ts +87 -0
  163. package/src/models/depthAnything2.ts +276 -0
  164. package/src/models/depthAnything3.ts +229 -0
  165. package/src/models/dualDpt.ts +233 -0
  166. package/src/models/hiera.ts +231 -0
  167. package/src/models/hieraAttention.ts +121 -0
  168. package/src/models/miniature.ts +241 -0
  169. package/src/models/mobileSam.ts +135 -0
  170. package/src/models/owlv2.ts +190 -0
  171. package/src/models/owlv2Detect.ts +116 -0
  172. package/src/models/owlv2Miniature.ts +108 -0
  173. package/src/models/rope.ts +111 -0
  174. package/src/models/sam21.ts +182 -0
  175. package/src/models/sam21Miniature.ts +225 -0
  176. package/src/models/sam21Video.ts +154 -0
  177. package/src/models/sam2Choice.ts +62 -0
  178. package/src/models/sam2Decoder.ts +218 -0
  179. package/src/models/sam2Memories.ts +165 -0
  180. package/src/models/sam2Memory.ts +247 -0
  181. package/src/models/sam2Positions.ts +106 -0
  182. package/src/models/sam2Tracker.ts +300 -0
  183. package/src/models/samDecoder.ts +225 -0
  184. package/src/models/samMiniature.ts +190 -0
  185. package/src/models/samPrompt.ts +227 -0
  186. package/src/models/seeded.ts +50 -0
  187. package/src/models/tinyVit.ts +178 -0
  188. package/src/models/twoWay.ts +122 -0
  189. package/src/models/vit.ts +265 -0
  190. package/src/models/windowAttention.ts +155 -0
  191. package/src/poses.ts +894 -0
  192. package/src/prepare.ts +306 -0
  193. package/src/propose.ts +96 -0
  194. package/src/resample.ts +197 -0
  195. package/src/run.ts +7 -0
  196. package/src/scene.ts +141 -0
  197. package/src/segment.ts +220 -0
  198. package/src/testScene.ts +283 -0
  199. package/src/twoView.ts +536 -0
@@ -0,0 +1,351 @@
1
+ /**
2
+ * A captured surface's material, separated from the light it was photographed under.
3
+ *
4
+ * ## What this is, said plainly
5
+ *
6
+ * **Inverse rendering is ill-posed and this does not pretend otherwise.** A photograph of a grey
7
+ * wall in shadow and a photograph of a dark wall in light are the same photograph. Nothing recovers
8
+ * the difference from pixels alone; what recovers it is an *assumption*, and the whole of the
9
+ * honesty in this file is in saying which assumptions and where they fail.
10
+ *
11
+ * **Shadow baked into albedo is the field's named failure.** A capture delit badly looks correct
12
+ * until it is relit — and then the old light is still painted on, a shadow follows an object that
13
+ * has moved, and the scene reads as flat and wrong in a way nobody can point at. Every measurement
14
+ * below is aimed at that one failure.
15
+ *
16
+ * **No learned model ships here, and the reason is licensing rather than quality.** The models that
17
+ * do this well are trained on datasets whose terms do not permit redistribution in a product, and
18
+ * a weight file a consumer cannot lawfully ship is worse than no weight file at all. So this is
19
+ * multi-view inverse rendering with a classical separation, and `tools/capture-prior/` is where a
20
+ * prior the project owns would be trained if one is wanted — on scenes DriftEngine itself renders,
21
+ * which is data nobody else has terms on.
22
+ *
23
+ * ## The two assumptions, and what each is worth
24
+ *
25
+ * **A Lambertian point looks the same from everywhere; a highlight does not.** That is what
26
+ * *multi-view* buys, and it buys exactly one thing: the view-dependent part. A point seen from six
27
+ * cameras gives six observations whose *median* rejects a highlight in any minority of them, and
28
+ * whose spread above that median is the specular energy — which is where the roughness estimate
29
+ * comes from. **What it cannot do is remove a cast shadow**, and that is worth stating because it
30
+ * is the assumption people expect it to carry: a shadowed point is shadowed from every camera, so
31
+ * every observation agrees, and agreement is not evidence of albedo.
32
+ *
33
+ * **A shadow changes intensity and not colour; a material change usually changes colour.** That is
34
+ * Retinex, and it is what separates a cast shadow from a dark patch of paint. Over the mesh's own
35
+ * edges, an edge where the intensity jumps and the chromaticity holds is called shading and the
36
+ * jump is given to the light; an edge where the chromaticity moves is called reflectance and the
37
+ * jump is given to the albedo. **Where it fails is a grey object on a grey floor** — no chromaticity
38
+ * to tell them apart — and that is what the confidence is for.
39
+ *
40
+ * ## What it achieves, measured
41
+ *
42
+ * On the synthetic scene in `delight.test.ts`: a cast shadow that makes one half of a floor **4.8
43
+ * times darker than the other in the photographs** comes back **2.2 times darker in the albedo** —
44
+ * half of it removed, in the logarithm. A matte floor's albedo lands within about a tenth of the
45
+ * truth, and the whole scene relit under a light it never saw is within a tenth of a channel of the
46
+ * renderer's own answer.
47
+ *
48
+ * **Half a shadow is a real result and not a finished one**, and the number is written here rather
49
+ * than rounded up because it is the number a learned prior would have to beat to be worth its
50
+ * weights. What is left is the part a chromaticity cannot see: where a shadow's edge falls inside a
51
+ * single material, the only thing separating shading from paint is a *prior about scenes*, and this
52
+ * has none.
53
+ *
54
+ * ## The confidence is the point, not a decoration
55
+ *
56
+ * Every surface here gets a number saying how much of this to believe, and the test that makes it
57
+ * mean something **correlates it against the actual error**. A channel that does not track the error
58
+ * is worse than none, because a consumer trusts it. Low confidence is the honest answer for glass,
59
+ * for a surface one camera saw, and for a grey object on a grey floor — and a pipeline that knows
60
+ * which surfaces those are can leave them alone rather than delighting them wrongly.
61
+ */
62
+ import { exactExp, exactLog } from '@driftengine/core';
63
+ import { cameraCentre, nearestHit } from './delightScene.js';
64
+ const DEFAULT_MINIMUM_VIEWS = 2;
65
+ const DEFAULT_CHROMATICITY_STEP = 0.05;
66
+ const DEFAULT_ITERATIONS = 120;
67
+ /** Where in the sorted shading the scene's "fully lit" is taken to be. */
68
+ const ANCHOR_PERCENTILE = 0.9;
69
+ /** Views past which another one adds little: four sample a hemisphere well enough to reject by. */
70
+ const ENOUGH_VIEWS = 4;
71
+ export function createDelightOut(vertices) {
72
+ return {
73
+ albedo: new Float32Array(vertices * 3),
74
+ roughness: new Float32Array(vertices),
75
+ normal: new Float32Array(vertices * 3),
76
+ confidence: new Float32Array(vertices),
77
+ };
78
+ }
79
+ /** `mesh`'s material, as seen in `views`. */
80
+ export function delight(views, mesh, options, out) {
81
+ const vertices = mesh.positions.length / 3;
82
+ const minimumViews = options.minimumViews ?? DEFAULT_MINIMUM_VIEWS;
83
+ const step = options.chromaticityStep ?? DEFAULT_CHROMATICITY_STEP;
84
+ const iterations = options.iterations ?? DEFAULT_ITERATIONS;
85
+ out.normal.set(mesh.normals.subarray(0, vertices * 3));
86
+ /* What each view saw of each vertex, where it saw it at all. */
87
+ const observed = new Float64Array(vertices * 3);
88
+ const seen = new Int32Array(vertices);
89
+ const spread = new Float64Array(vertices);
90
+ gather(views, mesh, observed, seen, spread);
91
+ /*
92
+ * The intensity a point returned and the colour it returned it in, kept apart. Retinex works on
93
+ * the first and leaves the second alone, because a shadow is the one and a material is the other.
94
+ */
95
+ const logObserved = new Float64Array(vertices * 3);
96
+ const chroma = new Float64Array(vertices * 3);
97
+ for (let vertex = 0; vertex < vertices; vertex += 1) {
98
+ let total = 0;
99
+ for (let c = 0; c < 3; c += 1)
100
+ total += observed[vertex * 3 + c];
101
+ for (let c = 0; c < 3; c += 1) {
102
+ logObserved[vertex * 3 + c] = exactLog(Math.max(observed[vertex * 3 + c], 1e-4));
103
+ chroma[vertex * 3 + c] = observed[vertex * 3 + c] / Math.max(total, 1e-6);
104
+ }
105
+ }
106
+ /*
107
+ * **One solve per channel, sharing one classification.** An intensity-only separation cannot tell
108
+ * a warm key light from a neutral ambient: a surface in shadow is lit by one and out of it by the
109
+ * other, so its recovered *colour* carries the difference even when its brightness does not.
110
+ * Measured on the synthetic scene, that alone was most of what was left — the shadowed and lit
111
+ * halves of one floor came back 35% apart. The decision about which edges are shading is still
112
+ * made once, on chromaticity, because that is what a shadow does not change.
113
+ */
114
+ const channelShading = [];
115
+ const channel = new Float64Array(vertices);
116
+ for (let c = 0; c < 3; c += 1) {
117
+ for (let vertex = 0; vertex < vertices; vertex += 1) {
118
+ channel[vertex] = logObserved[vertex * 3 + c];
119
+ }
120
+ channelShading.push(solveShading(mesh, channel, chroma, step, iterations, seen, minimumViews));
121
+ }
122
+ for (let vertex = 0; vertex < vertices; vertex += 1) {
123
+ /* What is left of each channel once its own light is taken out. */
124
+ for (let c = 0; c < 3; c += 1) {
125
+ const shading = channelShading[c][vertex];
126
+ const value = exactExp(logObserved[vertex * 3 + c] - shading);
127
+ out.albedo[vertex * 3 + c] = value > 1 ? 1 : value;
128
+ }
129
+ /*
130
+ * **The spread above the median is specular energy, and a wider lobe means a rougher surface.**
131
+ * A mirror puts all of it in one view and a chalk surface in none, so the roughness falls as
132
+ * the disagreement rises. Coarse by construction: six views sample a lobe badly, and a number
133
+ * that says "rough" or "polished" is what a capture can honestly carry.
134
+ */
135
+ const excess = spread[vertex];
136
+ out.roughness[vertex] = Math.min(1, Math.max(0.05, 1 - Math.min(1, excess * 4)));
137
+ out.confidence[vertex] = confidenceAt(seen[vertex], minimumViews, excess, views.length);
138
+ }
139
+ }
140
+ /**
141
+ * Every view's opinion of every vertex: the median colour, how many saw it, and their disagreement.
142
+ *
143
+ * **Visibility is a ray back to the camera against the mesh itself.** A vertex behind a wall is one
144
+ * the camera never saw, and reading the pixel it projects to reads the wall's colour as the
145
+ * surface's — which is not a subtle error, it is another object's paint.
146
+ */
147
+ function gather(views, mesh, observed, seen, spread) {
148
+ const vertices = mesh.positions.length / 3;
149
+ const samples = [];
150
+ for (let vertex = 0; vertex < vertices; vertex += 1)
151
+ samples.push([]);
152
+ for (const view of views) {
153
+ const { width, height, intrinsics } = view.camera;
154
+ const [fx, fy, cx, cy] = intrinsics;
155
+ const m = view.camera.worldToCamera;
156
+ const eye = cameraCentre(m);
157
+ for (let vertex = 0; vertex < vertices; vertex += 1) {
158
+ const point = [
159
+ mesh.positions[vertex * 3],
160
+ mesh.positions[vertex * 3 + 1],
161
+ mesh.positions[vertex * 3 + 2],
162
+ ];
163
+ /* Facing away from the camera is not seen, whatever the depth says. */
164
+ let facing = 0;
165
+ for (let k = 0; k < 3; k += 1) {
166
+ facing += mesh.normals[vertex * 3 + k] * (eye[k] - point[k]);
167
+ }
168
+ if (!(facing > 0))
169
+ continue;
170
+ let viewZ = m[11];
171
+ let viewX = m[3];
172
+ let viewY = m[7];
173
+ for (let k = 0; k < 3; k += 1) {
174
+ viewX += m[k] * point[k];
175
+ viewY += m[4 + k] * point[k];
176
+ viewZ += m[8 + k] * point[k];
177
+ }
178
+ if (!(viewZ > 0))
179
+ continue;
180
+ const px = Math.floor((fx * viewX) / viewZ + cx);
181
+ const py = Math.floor((fy * viewY) / viewZ + cy);
182
+ if (px < 0 || px >= width || py < 0 || py >= height)
183
+ continue;
184
+ /* Occluded by the surface itself: the ray to the camera meets something first. */
185
+ const toEye = [
186
+ eye[0] - point[0],
187
+ eye[1] - point[1],
188
+ eye[2] - point[2],
189
+ ];
190
+ const distance = Math.sqrt(toEye[0] * toEye[0] + toEye[1] * toEye[1] + toEye[2] * toEye[2]);
191
+ const direction = [
192
+ toEye[0] / distance,
193
+ toEye[1] / distance,
194
+ toEye[2] / distance,
195
+ ];
196
+ if (nearestHit(mesh, point, direction, 1e-3, distance - 1e-3) !== null)
197
+ continue;
198
+ const pixel = (py * width + px) * 4;
199
+ samples[vertex].push(view.frame[pixel], view.frame[pixel + 1], view.frame[pixel + 2]);
200
+ }
201
+ }
202
+ for (let vertex = 0; vertex < vertices; vertex += 1) {
203
+ const held = samples[vertex];
204
+ const count = held.length / 3;
205
+ seen[vertex] = count;
206
+ if (count === 0)
207
+ continue;
208
+ /* The median a channel at a time: a highlight in a minority of views is rejected by it. */
209
+ for (let c = 0; c < 3; c += 1) {
210
+ const column = [];
211
+ for (let at = 0; at < count; at += 1)
212
+ column.push(held[at * 3 + c]);
213
+ column.sort((a, b) => a - b);
214
+ observed[vertex * 3 + c] = column[Math.floor((count - 1) / 2)];
215
+ }
216
+ /*
217
+ * **How far the views stand from their own median, in either direction.** A view brighter than
218
+ * the rest is a highlight and one darker is an occlusion or an inter-reflection, and both say
219
+ * the same thing about the answer: the median is standing in for observations that did not
220
+ * agree. The mean of those distances and the worst of them were both measured here and are
221
+ * interchangeable on these scenes, so it is the mean — the plainer statistic, and the one that
222
+ * does not turn on a single camera.
223
+ */
224
+ let apart = 0;
225
+ for (let at = 0; at < count; at += 1) {
226
+ for (let c = 0; c < 3; c += 1) {
227
+ apart += Math.abs(held[at * 3 + c] - observed[vertex * 3 + c]);
228
+ }
229
+ }
230
+ spread[vertex] = apart / (count * 3);
231
+ }
232
+ }
233
+ /**
234
+ * The light, as a field over the mesh that explains every intensity jump a colour jump does not.
235
+ *
236
+ * A sweep at a time: each vertex takes the average of what its neighbours imply, where a shading
237
+ * edge implies the neighbour's shading plus the observed jump and a reflectance edge implies the
238
+ * neighbour's shading unchanged.
239
+ *
240
+ * **The constant is assumed rather than solved for, and which assumption matters.** Nothing in a
241
+ * photograph says whether the light was bright and the paint dark or the other way round, so one
242
+ * end has to be fixed. The textbook choice is *the brightest surface is white*, and it is wrong for
243
+ * a capture: a room of brown floor and blue wall has no white in it, and the recovery comes back
244
+ * about sixty per cent too bright — measured on the synthetic scene, which then failed its
245
+ * relighting gate by a third of a channel. What is assumed here instead is **the brightest
246
+ * illumination in the scene is full illumination**, which is a statement about the light rather
247
+ * than about the paint, and is true of any scene with a surface facing the light.
248
+ */
249
+ function solveShading(mesh, logIntensity, chroma, step, iterations, seen, minimumViews) {
250
+ const vertices = logIntensity.length;
251
+ const neighbours = adjacency(mesh, vertices);
252
+ const shading = new Float64Array(vertices);
253
+ const next = new Float64Array(vertices);
254
+ for (let sweep = 0; sweep < iterations; sweep += 1) {
255
+ for (let vertex = 0; vertex < vertices; vertex += 1) {
256
+ const around = neighbours[vertex];
257
+ if (around.length === 0) {
258
+ next[vertex] = shading[vertex];
259
+ continue;
260
+ }
261
+ let total = 0;
262
+ for (const other of around) {
263
+ let apart = 0;
264
+ for (let c = 0; c < 3; c += 1) {
265
+ apart += Math.abs(chroma[vertex * 3 + c] - chroma[other * 3 + c]);
266
+ }
267
+ const materialChange = apart > step;
268
+ const jump = materialChange
269
+ ? 0
270
+ : logIntensity[vertex] - logIntensity[other];
271
+ total += shading[other] + jump;
272
+ }
273
+ next[vertex] = total / around.length;
274
+ }
275
+ shading.set(next);
276
+ }
277
+ /*
278
+ * The most-lit surface anybody saw properly is taken as fully lit, which fixes the constant.
279
+ *
280
+ * **At a high percentile rather than at the maximum**, and that choice is the load-bearing part:
281
+ * one vertex is enough to set a maximum, and the one that sets it is whichever caught a highlight
282
+ * the views could not average away — the whole scene is then scaled against that vertex.
283
+ * Measured on the synthetic scene, a block's four-vertex top did exactly that and left a matte
284
+ * floor half again too bright.
285
+ *
286
+ * **The shift itself is usually small**, because the solve starts from zero everywhere and its
287
+ * iteration has no absolute term, so it settles around a shading of nought without being told to.
288
+ * That is an accident of the method rather than a decision, which is why the constant is set
289
+ * here explicitly instead of being left to it.
290
+ */
291
+ const levels = [];
292
+ for (let vertex = 0; vertex < vertices; vertex += 1) {
293
+ if (seen[vertex] < minimumViews)
294
+ continue;
295
+ levels.push(shading[vertex]);
296
+ }
297
+ if (levels.length > 0) {
298
+ levels.sort((a, b) => a - b);
299
+ const brightest = levels[Math.floor((levels.length - 1) * ANCHOR_PERCENTILE)];
300
+ for (let vertex = 0; vertex < vertices; vertex += 1) {
301
+ shading[vertex] = shading[vertex] - brightest;
302
+ }
303
+ }
304
+ return shading;
305
+ }
306
+ /** Which vertices share an edge with which, by position so a split seam is still one surface. */
307
+ function adjacency(mesh, vertices) {
308
+ const out = [];
309
+ for (let vertex = 0; vertex < vertices; vertex += 1)
310
+ out.push([]);
311
+ const triangles = mesh.indices.length / 3;
312
+ const seen = new Set();
313
+ for (let face = 0; face < triangles; face += 1) {
314
+ for (let slot = 0; slot < 3; slot += 1) {
315
+ const a = mesh.indices[face * 3 + slot];
316
+ const b = mesh.indices[face * 3 + ((slot + 1) % 3)];
317
+ if (a === b)
318
+ continue;
319
+ const key = a < b ? a * vertices + b : b * vertices + a;
320
+ if (seen.has(key))
321
+ continue;
322
+ seen.add(key);
323
+ out[a].push(b);
324
+ out[b].push(a);
325
+ }
326
+ }
327
+ return out;
328
+ }
329
+ /**
330
+ * How much of this vertex to believe.
331
+ *
332
+ * Three things make an answer weak and they are multiplied rather than averaged, because any one of
333
+ * them alone is enough to make the answer wrong: too few views to reject anything, a surface whose
334
+ * views disagree wildly — glass, a mirror, water — and a surface nobody saw at all.
335
+ */
336
+ function confidenceAt(seen, minimumViews, excess, views) {
337
+ if (seen === 0)
338
+ return 0;
339
+ if (seen < minimumViews)
340
+ return 0.1;
341
+ /*
342
+ * Every view that saw it is evidence, with diminishing returns past a handful — **counted
343
+ * absolutely, not against how many cameras there happened to be**. Dividing by the number
344
+ * supplied made two views of two look exactly as good as seven of seven, which is the opposite
345
+ * of what the number is for: a point two cameras saw is one where nothing could be rejected.
346
+ */
347
+ const coverage = Math.min(1, seen / ENOUGH_VIEWS);
348
+ /* And disagreement across views is the tell for a surface that is not diffuse at all. */
349
+ const agreement = 1 / (1 + excess * 12);
350
+ return Math.max(0, Math.min(1, coverage * agreement));
351
+ }
@@ -0,0 +1,37 @@
1
+ import type { MeshData } from '@driftengine/drft';
2
+ import type { RasterCamera } from './gaussians/project.ts';
3
+ export interface LitScene {
4
+ readonly mesh: MeshData;
5
+ /** Three per vertex, linear: what the recovery is measured against. */
6
+ readonly albedo: Float32Array;
7
+ /** One per vertex: the same. */
8
+ readonly roughness: Float32Array;
9
+ }
10
+ export interface Light {
11
+ /** Towards the light, unit length. */
12
+ readonly direction: readonly [number, number, number];
13
+ readonly colour: readonly [number, number, number];
14
+ /** What arrives from everywhere else, so a shadowed point is dark rather than black. */
15
+ readonly ambient: number;
16
+ }
17
+ /**
18
+ * `scene` seen from `camera`, into `out` as linear premultiplied RGBA.
19
+ *
20
+ * Each pixel is one ray: the nearest triangle, its interpolated normal and albedo, Lambert against
21
+ * the light, a shadow ray back to it, and a Blinn–Phong lobe whose width is the roughness. What a
22
+ * real capture would have instead of the shadow ray is a shadow in the photograph; what matters for
23
+ * the test is that the shadow is *there* and that the caller knows where.
24
+ */
25
+ export declare function renderLit(scene: LitScene, camera: RasterCamera, light: Light, out: Float32Array): void;
26
+ /** Where a camera stands, from its 3 × 4 world-to-camera. */
27
+ export declare function cameraCentre(worldToCamera: Float64Array): [number, number, number];
28
+ export interface MeshHit {
29
+ readonly triangle: number;
30
+ readonly distance: number;
31
+ /** Barycentric weights of the three corners, in order. */
32
+ readonly weights: readonly [number, number, number];
33
+ }
34
+ /** The nearest triangle a ray meets, by Möller–Trumbore over every one of them. */
35
+ export declare function nearestHit(mesh: MeshData, origin: readonly [number, number, number], direction: readonly [number, number, number], near: number, far: number): MeshHit | null;
36
+ /** A per-vertex attribute at a hit, by its barycentric weights. */
37
+ export declare function interpolate(mesh: MeshData, values: ArrayLike<number>, hit: MeshHit, stride: number): number[];
@@ -0,0 +1,183 @@
1
+ /**
2
+ * A lit scene with the answer known: what a delighting test needs and nothing else.
3
+ *
4
+ * **A fixture, not a renderer.** `testScene.ts` ray casts analytic boxes and planes for the
5
+ * geometry stages; this renders a *triangle mesh* with a light in it, because delighting is asked
6
+ * about a mesh and about shading — a cast shadow, a highlight, and an albedo that the caller knows
7
+ * because it put it there. It is deliberately the simplest thing that can pose the question: one
8
+ * directional light, Lambert plus a Blinn–Phong lobe, and a shadow by testing the ray to the light
9
+ * against the same triangles.
10
+ *
11
+ * **It lives beside the code rather than in the test file** because three tests need it and because
12
+ * the *relighting* gate needs to render the same scene under a second light — which is the one
13
+ * measurement that says whether a recovered material is a material or a picture of one.
14
+ */
15
+ import { exactExp, exactLog } from '@driftengine/core';
16
+ /**
17
+ * `scene` seen from `camera`, into `out` as linear premultiplied RGBA.
18
+ *
19
+ * Each pixel is one ray: the nearest triangle, its interpolated normal and albedo, Lambert against
20
+ * the light, a shadow ray back to it, and a Blinn–Phong lobe whose width is the roughness. What a
21
+ * real capture would have instead of the shadow ray is a shadow in the photograph; what matters for
22
+ * the test is that the shadow is *there* and that the caller knows where.
23
+ */
24
+ export function renderLit(scene, camera, light, out) {
25
+ const { width, height, intrinsics } = camera;
26
+ const [fx, fy, cx, cy] = intrinsics;
27
+ const m = camera.worldToCamera;
28
+ const eye = cameraCentre(m);
29
+ out.fill(0);
30
+ for (let y = 0; y < height; y += 1) {
31
+ for (let x = 0; x < width; x += 1) {
32
+ const ahead = toWorld(m, [(x + 0.5 - cx) / fx, (y + 0.5 - cy) / fy, 1]);
33
+ const direction = normalise([
34
+ ahead[0] - eye[0],
35
+ ahead[1] - eye[1],
36
+ ahead[2] - eye[2],
37
+ ]);
38
+ const hit = nearestHit(scene.mesh, eye, direction, 1e-4, Infinity);
39
+ const pixel = (y * width + x) * 4;
40
+ if (hit === null) {
41
+ out[pixel + 3] = 1;
42
+ continue;
43
+ }
44
+ const point = [0, 1, 2].map((k) => eye[k] + hit.distance * direction[k]);
45
+ const normal = interpolate(scene.mesh, scene.mesh.normals, hit, 3);
46
+ const albedo = interpolate(scene.mesh, scene.albedo, hit, 3);
47
+ const rough = interpolate(scene.mesh, scene.roughness, hit, 1)[0];
48
+ const facing = dot(normal, light.direction);
49
+ /* A ray back to the light, against the same triangles: a hard shadow with a hard edge. */
50
+ const lit = facing > 0 &&
51
+ nearestHit(scene.mesh, point, light.direction, 1e-3, 40) ===
52
+ null;
53
+ const toEye = normalise([
54
+ eye[0] - point[0],
55
+ eye[1] - point[1],
56
+ eye[2] - point[2],
57
+ ]);
58
+ const half = normalise([
59
+ toEye[0] + light.direction[0],
60
+ toEye[1] + light.direction[1],
61
+ toEye[2] + light.direction[2],
62
+ ]);
63
+ /*
64
+ * **A Blinn–Phong lobe that widens *and weakens* as the surface roughens.** The exponent
65
+ * alone is not enough: with a strength that does not fall, a rough surface comes out with an
66
+ * enormous broad highlight over the whole of it, which is not a surface anybody has seen. It
67
+ * also made this fixture's floor 1.77 times too bright through a delighting that was working
68
+ * correctly — the light really was mostly specular, so the recovery was right about a scene
69
+ * nobody would build.
70
+ */
71
+ const square = rough * rough;
72
+ const shininess = 2 / Math.max(1e-4, square * square) - 2;
73
+ /*
74
+ * **Normalised, which is the difference between a rough surface and a bright one.** A lobe
75
+ * that widens without losing energy puts a broad sheen over the whole of a rough surface,
76
+ * and it is not a small effect: measured here, the dark channels of a matte blue floor came
77
+ * back nearly twice as bright through a delighting that was working correctly. The peak of a
78
+ * Blinn–Phong lobe scales as (n + 2) / 8π, and 0.04 is a dielectric's reflectance head-on.
79
+ */
80
+ const strength = (0.04 * (shininess + 2)) / (8 * Math.PI);
81
+ /*
82
+ * The power by the engine's own reproducible pair rather than `Math.pow`, which the
83
+ * determinism gate refuses — two engines may differ by a unit in the last place, and a
84
+ * fixture that renders differently on two machines is a test nobody can compare.
85
+ */
86
+ const facingHalf = Math.max(0, dot(normal, half));
87
+ const lobe = facingHalf > 0 ? exactExp(shininess * exactLog(facingHalf)) : 0;
88
+ const specular = lit ? lobe * strength : 0;
89
+ for (let c = 0; c < 3; c += 1) {
90
+ const irradiance = (lit ? facing : 0) * light.colour[c] + light.ambient;
91
+ out[pixel + c] =
92
+ albedo[c] * irradiance + specular * light.colour[c];
93
+ }
94
+ out[pixel + 3] = 1;
95
+ }
96
+ }
97
+ }
98
+ /** Where a camera stands, from its 3 × 4 world-to-camera. */
99
+ export function cameraCentre(worldToCamera) {
100
+ const out = [0, 0, 0];
101
+ for (let r = 0; r < 3; r += 1) {
102
+ for (let c = 0; c < 3; c += 1) {
103
+ out[c] -= worldToCamera[r * 4 + c] * worldToCamera[r * 4 + 3];
104
+ }
105
+ }
106
+ return out;
107
+ }
108
+ /** A camera-space point in the world: the projection run backwards. */
109
+ function toWorld(m, view) {
110
+ const out = [0, 0, 0];
111
+ for (let r = 0; r < 3; r += 1) {
112
+ for (let c = 0; c < 3; c += 1) {
113
+ out[c] += m[r * 4 + c] * (view[r] - m[r * 4 + 3]);
114
+ }
115
+ }
116
+ return out;
117
+ }
118
+ /** The nearest triangle a ray meets, by Möller–Trumbore over every one of them. */
119
+ export function nearestHit(mesh, origin, direction, near, far) {
120
+ let best = null;
121
+ const triangles = mesh.indices.length / 3;
122
+ for (let face = 0; face < triangles; face += 1) {
123
+ const a = mesh.indices[face * 3];
124
+ const b = mesh.indices[face * 3 + 1];
125
+ const c = mesh.indices[face * 3 + 2];
126
+ const e1 = corner(mesh, b, a);
127
+ const e2 = corner(mesh, c, a);
128
+ const p = cross(direction, e2);
129
+ const determinant = dot(e1, p);
130
+ if (Math.abs(determinant) < 1e-12)
131
+ continue;
132
+ const inverse = 1 / determinant;
133
+ const t = [0, 1, 2].map((k) => origin[k] - mesh.positions[a * 3 + k]);
134
+ const u = dot(t, p) * inverse;
135
+ if (u < 0 || u > 1)
136
+ continue;
137
+ const q = cross(t, e1);
138
+ const v = dot(direction, q) * inverse;
139
+ if (v < 0 || u + v > 1)
140
+ continue;
141
+ const distance = dot(e2, q) * inverse;
142
+ if (distance < near || distance > far)
143
+ continue;
144
+ if (best !== null && distance >= best.distance)
145
+ continue;
146
+ best = { triangle: face, distance, weights: [1 - u - v, u, v] };
147
+ }
148
+ return best;
149
+ }
150
+ /** A per-vertex attribute at a hit, by its barycentric weights. */
151
+ export function interpolate(mesh, values, hit, stride) {
152
+ const out = new Array(stride).fill(0);
153
+ for (let corner = 0; corner < 3; corner += 1) {
154
+ const vertex = mesh.indices[hit.triangle * 3 + corner];
155
+ const weight = hit.weights[corner];
156
+ for (let k = 0; k < stride; k += 1) {
157
+ out[k] = out[k] + weight * values[vertex * stride + k];
158
+ }
159
+ }
160
+ return out;
161
+ }
162
+ function corner(mesh, to, from) {
163
+ return [0, 1, 2].map((k) => mesh.positions[to * 3 + k] - mesh.positions[from * 3 + k]);
164
+ }
165
+ function cross(a, b) {
166
+ return [
167
+ a[1] * b[2] - a[2] * b[1],
168
+ a[2] * b[0] - a[0] * b[2],
169
+ a[0] * b[1] - a[1] * b[0],
170
+ ];
171
+ }
172
+ function dot(a, b) {
173
+ return (a[0] * b[0] +
174
+ a[1] * b[1] +
175
+ a[2] * b[2]);
176
+ }
177
+ function normalise(v) {
178
+ const length = Math.sqrt(v[0] * v[0] +
179
+ v[1] * v[1] +
180
+ v[2] * v[2]);
181
+ const scale = length > 0 ? 1 / length : 0;
182
+ return [v[0] * scale, v[1] * scale, v[2] * scale];
183
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Depth, its confidence and a camera per view, from a clip's frames rather than from tensors.
3
+ *
4
+ * **The frames arrive as a host hands them over** — RGBA bytes at whatever size the clip is — and
5
+ * this prepares them as Depth Anything 3's own processor does, runs the views together so the
6
+ * model's across-view blocks see each other, and decodes what comes back. A graph bakes its shapes,
7
+ * so one is built per count of views and prepared size and kept: a clip's frames are all the same
8
+ * size, so the second estimate over the same clip builds nothing.
9
+ *
10
+ * **It answers with its own arrays rather than filling the caller's**, which the engine's hot paths
11
+ * never do: the prepared size is not known until the frames are prepared, so a caller cannot have
12
+ * sized anything, and this runs once a clip rather than once a frame.
13
+ */
14
+ import { type WeightSource } from '@driftengine/texture';
15
+ import { type DepthAnything3Config } from './models/depthAnything3.ts';
16
+ import type { GraphRun } from './run.ts';
17
+ /** A frame as a host hands it over: RGBA bytes, and the size they are. */
18
+ export interface RawFrame {
19
+ readonly pixels: Uint8Array;
20
+ readonly width: number;
21
+ readonly height: number;
22
+ }
23
+ export interface DepthView {
24
+ /** `[height, width]` metres, and one confidence each. */
25
+ readonly depth: Float32Array;
26
+ readonly confidence: Float32Array;
27
+ /** 3 × 4 row-major, world to camera. */
28
+ readonly worldToCamera: Float32Array;
29
+ /** 3 × 3 row-major. */
30
+ readonly intrinsics: Float32Array;
31
+ }
32
+ export interface DepthEstimate {
33
+ /** The size the frames were prepared at, which the maps and the cameras are in. */
34
+ readonly width: number;
35
+ readonly height: number;
36
+ readonly views: readonly DepthView[];
37
+ }
38
+ export interface DepthEstimator {
39
+ estimate(frames: readonly RawFrame[]): Promise<DepthEstimate>;
40
+ }
41
+ /** An estimator over one model's weights, running its graphs through `run`. */
42
+ export declare function createDepthEstimator(config: DepthAnything3Config, weights: WeightSource, run: GraphRun, options?: {
43
+ readonly size?: number;
44
+ }): DepthEstimator;
package/dist/depth.js ADDED
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Depth, its confidence and a camera per view, from a clip's frames rather than from tensors.
3
+ *
4
+ * **The frames arrive as a host hands them over** — RGBA bytes at whatever size the clip is — and
5
+ * this prepares them as Depth Anything 3's own processor does, runs the views together so the
6
+ * model's across-view blocks see each other, and decodes what comes back. A graph bakes its shapes,
7
+ * so one is built per count of views and prepared size and kept: a clip's frames are all the same
8
+ * size, so the second estimate over the same clip builds nothing.
9
+ *
10
+ * **It answers with its own arrays rather than filling the caller's**, which the engine's hot paths
11
+ * never do: the prepared size is not known until the frames are prepared, so a caller cannot have
12
+ * sized anything, and this runs once a clip rather than once a frame.
13
+ */
14
+ import { graphFromWeights } from '@driftengine/texture';
15
+ import { decodeCamera, decodeDepth, depthAnything3, } from './models/depthAnything3.js';
16
+ import { prepareDepthFrame } from './prepare.js';
17
+ /** An estimator over one model's weights, running its graphs through `run`. */
18
+ export function createDepthEstimator(config, weights, run, options = {}) {
19
+ const size = options.size ?? 504;
20
+ const graphs = new Map();
21
+ return {
22
+ async estimate(frames) {
23
+ if (frames.length === 0)
24
+ throw new RangeError('a depth estimate needs a frame');
25
+ const prepared = frames.map((frame) => prepareDepthFrame(frame.pixels, frame.width, frame.height, size));
26
+ const { width, height } = prepared[0];
27
+ for (const [at, one] of prepared.entries()) {
28
+ if (one.width !== width || one.height !== height) {
29
+ throw new RangeError(`frame ${at} prepares to ${one.width}×${one.height} and the first to ${width}×${height}` +
30
+ ': one estimate is one clip, at one size');
31
+ }
32
+ }
33
+ const key = `${frames.length}.${height}x${width}`;
34
+ let graph = graphs.get(key);
35
+ if (graph === undefined) {
36
+ graph = graphFromWeights(weights, depthAnything3(config, frames.length, height, width));
37
+ graphs.set(key, graph);
38
+ }
39
+ const out = await run(graph, new Map(prepared.map((one, view) => [`image${view}`, one.pixels])));
40
+ const pose = out.get('pose');
41
+ const views = prepared.map((_, view) => {
42
+ const depth = new Float32Array(width * height);
43
+ const confidence = new Float32Array(width * height);
44
+ decodeDepth(out.get(`logits${view}`), depth, confidence);
45
+ const worldToCamera = new Float32Array(12);
46
+ const intrinsics = new Float32Array(9);
47
+ decodeCamera(pose, view, height, width, worldToCamera, intrinsics);
48
+ return { depth, confidence, worldToCamera, intrinsics };
49
+ });
50
+ return { width, height, views };
51
+ },
52
+ };
53
+ }