@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
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@driftengine/capture",
|
|
3
|
+
"version": "4.0.0",
|
|
4
|
+
"description": "A video to a playable scene, on the player's device: the models it runs and the stages that use them",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"drift-source": "./src/index.ts",
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./package.json": "./package.json",
|
|
16
|
+
"./*": "./*"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"src",
|
|
21
|
+
"!src/**/*.test.ts",
|
|
22
|
+
"!src/**/*.test.mjs",
|
|
23
|
+
"!src/**/__snapshots__",
|
|
24
|
+
"README.md",
|
|
25
|
+
"LICENSE",
|
|
26
|
+
"NOTICE"
|
|
27
|
+
],
|
|
28
|
+
"sideEffects": false,
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"@driftengine/core": "4.0.0",
|
|
31
|
+
"@driftengine/drft": "4.0.0",
|
|
32
|
+
"@driftengine/nav": "4.0.0",
|
|
33
|
+
"@driftengine/physics": "4.0.0",
|
|
34
|
+
"@driftengine/splats": "4.0.0",
|
|
35
|
+
"@driftengine/texture": "4.0.0"
|
|
36
|
+
},
|
|
37
|
+
"author": "Drift Technologies",
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "git+https://github.com/drftrun/driftengine.git",
|
|
41
|
+
"directory": "packages/capture"
|
|
42
|
+
},
|
|
43
|
+
"homepage": "https://github.com/drftrun/driftengine#readme",
|
|
44
|
+
"bugs": "https://github.com/drftrun/driftengine/issues",
|
|
45
|
+
"keywords": [
|
|
46
|
+
"driftengine",
|
|
47
|
+
"3d",
|
|
48
|
+
"typescript",
|
|
49
|
+
"photogrammetry",
|
|
50
|
+
"depth-estimation",
|
|
51
|
+
"gaussian-splatting",
|
|
52
|
+
"reconstruction"
|
|
53
|
+
],
|
|
54
|
+
"engines": {
|
|
55
|
+
"node": ">=22.12.0"
|
|
56
|
+
},
|
|
57
|
+
"publishConfig": {
|
|
58
|
+
"access": "public"
|
|
59
|
+
},
|
|
60
|
+
"scripts": {
|
|
61
|
+
"prepack": "node ../../scripts/build.mjs @driftengine/capture"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A clip in a browser as a `FrameSource`: a video element decoded into a canvas, a frame at a time.
|
|
3
|
+
*
|
|
4
|
+
* **A declared browser module** — a native host writes its own source over its own decoder, which is
|
|
5
|
+
* the whole point of the seam. Nothing else in this package touches a DOM.
|
|
6
|
+
*
|
|
7
|
+
* **Why a video element rather than WebCodecs.** `VideoDecoder` takes encoded chunks and a browser
|
|
8
|
+
* ships no demuxer to make them from an MP4, so a WebCodecs source means a demuxing dependency in a
|
|
9
|
+
* package whose bar for one is high. A video element demuxes, decodes and seeks with nothing added,
|
|
10
|
+
* and `drawImage` into a 2D canvas is how its frame becomes bytes. **What it gives up**: a frame is
|
|
11
|
+
* addressed by time rather than by its place in the stream — the middle of frame `index` at the
|
|
12
|
+
* clip's rate — so a clip of variable frame rate answers the frame covering that instant rather
|
|
13
|
+
* than the `index`-th one. What would make that wrong is a capture that needs a particular frame
|
|
14
|
+
* rather than a moment; nothing here does, since frames are chosen by motion.
|
|
15
|
+
*
|
|
16
|
+
* **The rate is measured, not assumed**, from the first frames the decoder presents, and falls back
|
|
17
|
+
* to the caller's `frameRate` where `requestVideoFrameCallback` is absent.
|
|
18
|
+
*/
|
|
19
|
+
import type { FrameSize, FrameSource } from './frames.ts';
|
|
20
|
+
|
|
21
|
+
/** A browser's clip, and how to let it go. */
|
|
22
|
+
export interface BrowserClip extends FrameSource {
|
|
23
|
+
/** Releases the element and the object URL; the source answers nothing afterwards. */
|
|
24
|
+
close(): void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface FrameCallbackMetadata {
|
|
28
|
+
readonly mediaTime: number;
|
|
29
|
+
readonly presentedFrames: number;
|
|
30
|
+
}
|
|
31
|
+
type VideoWithCallback = HTMLVideoElement & {
|
|
32
|
+
requestVideoFrameCallback?: (
|
|
33
|
+
callback: (now: number, metadata: FrameCallbackMetadata) => void,
|
|
34
|
+
) => number;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const once = (video: HTMLVideoElement, event: string): Promise<void> =>
|
|
38
|
+
new Promise((resolve, reject) => {
|
|
39
|
+
const done = (): void => {
|
|
40
|
+
video.removeEventListener(event, done);
|
|
41
|
+
video.removeEventListener('error', failed);
|
|
42
|
+
resolve();
|
|
43
|
+
};
|
|
44
|
+
const failed = (): void => {
|
|
45
|
+
video.removeEventListener(event, done);
|
|
46
|
+
video.removeEventListener('error', failed);
|
|
47
|
+
reject(new Error(`the clip could not be read: ${video.error?.message ?? 'no reason given'}`));
|
|
48
|
+
};
|
|
49
|
+
video.addEventListener(event, done);
|
|
50
|
+
video.addEventListener('error', failed);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
/** The rate the decoder presents frames at, from two it presents, or null where it cannot say. */
|
|
54
|
+
async function measuredRate(video: VideoWithCallback): Promise<number | null> {
|
|
55
|
+
if (typeof video.requestVideoFrameCallback !== 'function') return null;
|
|
56
|
+
const times: number[] = [];
|
|
57
|
+
await new Promise<void>((resolve) => {
|
|
58
|
+
const take = (_now: number, metadata: FrameCallbackMetadata): void => {
|
|
59
|
+
times.push(metadata.mediaTime);
|
|
60
|
+
if (times.length >= 3) resolve();
|
|
61
|
+
else video.requestVideoFrameCallback?.(take);
|
|
62
|
+
};
|
|
63
|
+
video.requestVideoFrameCallback?.(take);
|
|
64
|
+
void video.play().catch(() => resolve());
|
|
65
|
+
setTimeout(resolve, 2000);
|
|
66
|
+
});
|
|
67
|
+
video.pause();
|
|
68
|
+
const steps: number[] = [];
|
|
69
|
+
for (let i = 1; i < times.length; i += 1)
|
|
70
|
+
steps.push((times[i] as number) - (times[i - 1] as number));
|
|
71
|
+
const shortest = Math.min(...steps.filter((step) => step > 0));
|
|
72
|
+
return Number.isFinite(shortest) && shortest > 0 ? 1 / shortest : null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The clip's frames, addressed at the clip's rate. `frameRate` is the fallback where the browser
|
|
77
|
+
* cannot say what the rate is.
|
|
78
|
+
*/
|
|
79
|
+
export async function browserFrameSource(
|
|
80
|
+
clip: Blob,
|
|
81
|
+
options: { readonly frameRate?: number } = {},
|
|
82
|
+
): Promise<BrowserClip> {
|
|
83
|
+
// platform: browser default — this module is the browser's own `FrameSource`.
|
|
84
|
+
const url = URL.createObjectURL(clip);
|
|
85
|
+
const video = document.createElement('video') as VideoWithCallback;
|
|
86
|
+
video.preload = 'auto';
|
|
87
|
+
video.muted = true;
|
|
88
|
+
video.playsInline = true;
|
|
89
|
+
video.src = url;
|
|
90
|
+
await once(video, 'loadedmetadata');
|
|
91
|
+
/*
|
|
92
|
+
* **A container that parses is not a track that decodes**, and the difference is silent.
|
|
93
|
+
*
|
|
94
|
+
* Chrome on Linux plays no HEVC, which is what every recent phone records by default. The file
|
|
95
|
+
* loads, `loadedmetadata` fires, `duration` comes back correct to the millisecond — and
|
|
96
|
+
* `videoWidth` is zero, because nothing decoded. Left alone, the first frame draws a zero-wide
|
|
97
|
+
* image into a zero-wide canvas and dies inside `getImageData` with "the source width is 0",
|
|
98
|
+
* which names neither the clip nor the codec and sends a reader into the canvas code.
|
|
99
|
+
*
|
|
100
|
+
* So it is refused here, by name, with the duration as evidence that the file itself is fine.
|
|
101
|
+
* **What this cannot do is say which codec it was**: the element reports no track it could not
|
|
102
|
+
* decode, so the message names the one that always works rather than guessing at the one that
|
|
103
|
+
* did not.
|
|
104
|
+
*/
|
|
105
|
+
if (video.videoWidth === 0 || video.videoHeight === 0) {
|
|
106
|
+
const seconds = Number.isFinite(video.duration) ? video.duration.toFixed(2) : 'an unknown';
|
|
107
|
+
throw new Error(
|
|
108
|
+
`this browser cannot decode the clip's video track: it read ${seconds} s of container and ` +
|
|
109
|
+
'no picture. A codec this browser does not have is the usual cause — HEVC on a build ' +
|
|
110
|
+
'without it, which is what most phones record. Convert the clip to H.264 and try again.',
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
const rate = (await measuredRate(video)) ?? options.frameRate ?? 30;
|
|
114
|
+
const width = video.videoWidth;
|
|
115
|
+
const height = video.videoHeight;
|
|
116
|
+
const count = Math.max(1, Math.floor(video.duration * rate));
|
|
117
|
+
const canvas = new OffscreenCanvas(width, height);
|
|
118
|
+
const context = canvas.getContext('2d', { willReadFrequently: true });
|
|
119
|
+
if (context === null) throw new Error('a clip needs a 2D canvas to become pixels');
|
|
120
|
+
let open = true;
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
frameCount: () => (open ? count : 0),
|
|
124
|
+
async frameAt(index: number, out: Uint8Array): Promise<FrameSize> {
|
|
125
|
+
if (!open) throw new Error('the clip is closed');
|
|
126
|
+
if (out.length >= width * height * 4) {
|
|
127
|
+
/* The middle of the frame, so rounding cannot land on the boundary either side. */
|
|
128
|
+
const at = Math.min((index + 0.5) / rate, Math.max(0, video.duration - 1 / rate));
|
|
129
|
+
if (Math.abs(video.currentTime - at) > 1e-6) {
|
|
130
|
+
video.currentTime = at;
|
|
131
|
+
await once(video, 'seeked');
|
|
132
|
+
}
|
|
133
|
+
context.drawImage(video, 0, 0, width, height);
|
|
134
|
+
out.set(context.getImageData(0, 0, width, height).data);
|
|
135
|
+
}
|
|
136
|
+
return { width, height };
|
|
137
|
+
},
|
|
138
|
+
close() {
|
|
139
|
+
open = false;
|
|
140
|
+
video.removeAttribute('src');
|
|
141
|
+
video.load();
|
|
142
|
+
URL.revokeObjectURL(url);
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
package/src/collide.ts
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A captured mesh as something to stand on and to put in a file.
|
|
3
|
+
*
|
|
4
|
+
* **The mesh a capture draws and the mesh it collides against are the same triangles and not the
|
|
5
|
+
* same array.** What comes out of `marchVolume` and `decimate` is built for a picture: it carries
|
|
6
|
+
* normals and colours a collision test never reads, and it carries the occasional triangle with no
|
|
7
|
+
* area, which is harmless in a render and is not harmless in a contact — a plane needs a normal,
|
|
8
|
+
* and a triangle with no area has none. So the collision mesh is the drawn one with the arealess
|
|
9
|
+
* triangles taken out and the vertices nothing uses any more taken with them.
|
|
10
|
+
*
|
|
11
|
+
* **A room is a static mesh body and a prop is a set of hulls, and the difference is not size.** A
|
|
12
|
+
* mesh body is exact and cannot move: every triangle is tested as it stands, which is what a floor
|
|
13
|
+
* and a wall want. A convex decomposition is approximate and can be thrown: a hull has a centre of
|
|
14
|
+
* mass, an inertia and a cheap support function, which is what a crate wants. Giving a room hulls
|
|
15
|
+
* would round off its corners; giving a crate a mesh body would leave it unable to be picked up.
|
|
16
|
+
*
|
|
17
|
+
* **The container's caps are the decomposition's brief.** `COLL` holds at most thirty-two hulls of
|
|
18
|
+
* at most sixty-four points, so a decomposition that ignored them would produce a prop that cannot
|
|
19
|
+
* be written — found at the writer, after the expensive part. It is asked for inside them instead.
|
|
20
|
+
*/
|
|
21
|
+
import { MAX_COLLIDER_HULLS, type MeshData } from '@driftengine/drft';
|
|
22
|
+
import { decomposeConvex, type DecomposeOptions } from '@driftengine/physics';
|
|
23
|
+
|
|
24
|
+
/** A mesh as `meshShape` takes it, and what had to go for it to be one. */
|
|
25
|
+
export interface CollisionSource {
|
|
26
|
+
readonly positions: Float32Array;
|
|
27
|
+
readonly indices: Uint32Array;
|
|
28
|
+
/** Triangles dropped for having no area to have a plane from. */
|
|
29
|
+
readonly dropped: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface CollisionOptions {
|
|
33
|
+
/**
|
|
34
|
+
* The smallest triangle kept, in square metres.
|
|
35
|
+
*
|
|
36
|
+
* **Not zero**, because a triangle of a square micrometre has a plane in the arithmetic and none
|
|
37
|
+
* in any useful sense: its normal is the direction of its own rounding error, and a contact
|
|
38
|
+
* against it points somewhere arbitrary. A square millimetre is far below anything a capture
|
|
39
|
+
* resolves and far above where a cross product stops meaning anything.
|
|
40
|
+
*/
|
|
41
|
+
readonly minimumArea?: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const DEFAULT_MINIMUM_AREA = 1e-6;
|
|
45
|
+
|
|
46
|
+
/** `mesh`'s triangles, less the ones with no area, renumbered onto the vertices that remain. */
|
|
47
|
+
export function collisionMesh(mesh: MeshData, options: CollisionOptions = {}): CollisionSource {
|
|
48
|
+
const minimumArea = options.minimumArea ?? DEFAULT_MINIMUM_AREA;
|
|
49
|
+
const triangles = mesh.indices.length / 3;
|
|
50
|
+
const renumbered = new Map<number, number>();
|
|
51
|
+
const positions: number[] = [];
|
|
52
|
+
const indices: number[] = [];
|
|
53
|
+
let dropped = 0;
|
|
54
|
+
|
|
55
|
+
for (let face = 0; face < triangles; face += 1) {
|
|
56
|
+
const a = mesh.indices[face * 3] as number;
|
|
57
|
+
const b = mesh.indices[face * 3 + 1] as number;
|
|
58
|
+
const c = mesh.indices[face * 3 + 2] as number;
|
|
59
|
+
if (areaOf(mesh.positions, a, b, c) < minimumArea) {
|
|
60
|
+
dropped += 1;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
for (const vertex of [a, b, c]) {
|
|
64
|
+
let at = renumbered.get(vertex);
|
|
65
|
+
if (at === undefined) {
|
|
66
|
+
at = positions.length / 3;
|
|
67
|
+
renumbered.set(vertex, at);
|
|
68
|
+
for (let k = 0; k < 3; k += 1) positions.push(mesh.positions[vertex * 3 + k] as number);
|
|
69
|
+
}
|
|
70
|
+
indices.push(at);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
positions: Float32Array.from(positions),
|
|
76
|
+
indices: Uint32Array.from(indices),
|
|
77
|
+
dropped,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Twice the area of a triangle, halved: the length of the cross product of two of its edges. */
|
|
82
|
+
function areaOf(positions: Float32Array, a: number, b: number, c: number): number {
|
|
83
|
+
const ux = (positions[b * 3] as number) - (positions[a * 3] as number);
|
|
84
|
+
const uy = (positions[b * 3 + 1] as number) - (positions[a * 3 + 1] as number);
|
|
85
|
+
const uz = (positions[b * 3 + 2] as number) - (positions[a * 3 + 2] as number);
|
|
86
|
+
const vx = (positions[c * 3] as number) - (positions[a * 3] as number);
|
|
87
|
+
const vy = (positions[c * 3 + 1] as number) - (positions[a * 3 + 1] as number);
|
|
88
|
+
const vz = (positions[c * 3 + 2] as number) - (positions[a * 3 + 2] as number);
|
|
89
|
+
const nx = uy * vz - uz * vy;
|
|
90
|
+
const ny = uz * vx - ux * vz;
|
|
91
|
+
const nz = ux * vy - uy * vx;
|
|
92
|
+
return Math.sqrt(nx * nx + ny * ny + nz * nz) / 2;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** A prop's hulls, and how much empty space they added. */
|
|
96
|
+
export interface PropHulls {
|
|
97
|
+
/** xyz-packed points per hull, as `COLL` writes them and `hullShape` takes them. */
|
|
98
|
+
readonly hulls: readonly Float32Array[];
|
|
99
|
+
/** `(hullVolume − sourceVolume) / sourceVolume`, from the decomposition itself. */
|
|
100
|
+
readonly bloat: number;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* `mesh` decomposed into convex hulls the container can hold.
|
|
105
|
+
*
|
|
106
|
+
* **Asked for inside the caps rather than checked against them afterwards.** A decomposition is the
|
|
107
|
+
* expensive part of baking a prop, and one that comes back with thirty-three hulls is one that has
|
|
108
|
+
* to be thrown away and asked for again — so the cap is what it is asked for. A hull that still
|
|
109
|
+
* arrives with more points than a record can hold is refused by name, because silently dropping its
|
|
110
|
+
* points changes the shape a game collides against without telling anybody.
|
|
111
|
+
*/
|
|
112
|
+
export function propHulls(
|
|
113
|
+
mesh: MeshData | CollisionSource,
|
|
114
|
+
options: DecomposeOptions = {},
|
|
115
|
+
): PropHulls {
|
|
116
|
+
const decomposition = decomposeConvex(mesh.positions, mesh.indices, {
|
|
117
|
+
...options,
|
|
118
|
+
maxHulls: Math.min(options.maxHulls ?? MAX_COLLIDER_HULLS, MAX_COLLIDER_HULLS),
|
|
119
|
+
});
|
|
120
|
+
/*
|
|
121
|
+
* **The points need no cap of their own**, and it is worth saying why rather than checking for
|
|
122
|
+
* something that cannot happen. A part carries at most one point per support direction, which is
|
|
123
|
+
* fifty, and a collider record holds sixty-four — so the decomposition is already inside the
|
|
124
|
+
* container by construction. `collide.test.ts` asserts that relationship rather than this file
|
|
125
|
+
* testing each hull against a limit it can never reach.
|
|
126
|
+
*/
|
|
127
|
+
return { hulls: decomposition.parts.map((part) => part.points), bloat: decomposition.bloat };
|
|
128
|
+
}
|