@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/dist/scene.d.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every stage of a capture assembled into one `.drft`, and the proposals read back out of it.
|
|
3
|
+
*
|
|
4
|
+
* **One file, because a capture that arrives as six is a capture somebody has to reassemble.** The
|
|
5
|
+
* cloud draws, the mesh is what a body collides with, the decode program is the surface's material,
|
|
6
|
+
* the polygon mesh is the way across it, and the proposals are what a scene might make of the
|
|
7
|
+
* regions. They were produced by five stages that know nothing about each other; what they have in
|
|
8
|
+
* common is the capture, and that is what a container is for.
|
|
9
|
+
*
|
|
10
|
+
* **The proposals are written as a scene, and they are still proposals.** `ENTS` carries them under
|
|
11
|
+
* a component this package declares — not under a consumer's `transform`, whose field ids belong to
|
|
12
|
+
* that consumer and which this package has no way to know. So a capture proposes in its own words,
|
|
13
|
+
* an editor accepts or rejects, and what a scene ends up holding is the consumer's decision. The
|
|
14
|
+
* schema is written out here rather than built, because `@driftengine/capture` does not depend on
|
|
15
|
+
* the entity model and must not: the file is data, and the data is what the two sides share.
|
|
16
|
+
*
|
|
17
|
+
* **What is not written is the collision mesh**, and that is a payload decision with a reason: it
|
|
18
|
+
* is the drawn mesh with its degenerate triangles removed, which `collisionMesh` does in a
|
|
19
|
+
* millisecond at load. A second copy of the same triangles in the file is a second copy to
|
|
20
|
+
* download. Hulls *are* written, because a convex decomposition is not something to redo on a
|
|
21
|
+
* player's machine.
|
|
22
|
+
*/
|
|
23
|
+
import { type DrftMaterial, type DrftSplats, type DtexEntry, type EntsScene, type MeshData, type NavPolyMesh } from '@driftengine/drft';
|
|
24
|
+
import type { EntityProposal } from './propose.ts';
|
|
25
|
+
export interface CaptureScene {
|
|
26
|
+
/** The surface a capture produced, as it is drawn. */
|
|
27
|
+
readonly mesh: MeshData;
|
|
28
|
+
/** The materials the mesh refers to. A decode program names one of them by index. */
|
|
29
|
+
readonly materials?: readonly DrftMaterial[];
|
|
30
|
+
/** A material as a decode program over a latent, from the texture stage. */
|
|
31
|
+
readonly material?: DtexEntry;
|
|
32
|
+
/** The cloud, for what a mesh cannot show. */
|
|
33
|
+
readonly splats?: DrftSplats;
|
|
34
|
+
/** A prop's convex hulls, from `propHulls`. Three floats a point. */
|
|
35
|
+
readonly hulls?: readonly Float32Array[];
|
|
36
|
+
/** The way across the scene, with the placement it was built at. */
|
|
37
|
+
readonly navigation?: NavPolyMesh;
|
|
38
|
+
/** What a scene might make of each region. */
|
|
39
|
+
readonly proposals?: readonly EntityProposal[];
|
|
40
|
+
}
|
|
41
|
+
/** The component a capture proposes in. Named for what it is: a proposal, not a thing. */
|
|
42
|
+
export declare const PROPOSAL_COMPONENT = "captureProposal";
|
|
43
|
+
/**
|
|
44
|
+
* The fields a proposal carries, by stable id.
|
|
45
|
+
*
|
|
46
|
+
* **Six numbers rather than a bounds array**, because the field types an entity model declares are
|
|
47
|
+
* scalars and a strings — there is no list among them, and inventing one here would be inventing a
|
|
48
|
+
* type system for a single consumer. `label` is empty where nobody named the region, and
|
|
49
|
+
* `components` is the suggested set joined by spaces, which is what an editor offers and what it is
|
|
50
|
+
* free to ignore.
|
|
51
|
+
*/
|
|
52
|
+
export declare const PROPOSAL_SCHEMA: {
|
|
53
|
+
readonly name: "captureProposal";
|
|
54
|
+
readonly fields: readonly [{
|
|
55
|
+
readonly id: 'capture::proposal::label';
|
|
56
|
+
readonly name: 'label';
|
|
57
|
+
readonly type: 'String';
|
|
58
|
+
}, {
|
|
59
|
+
readonly id: 'capture::proposal::components';
|
|
60
|
+
readonly name: 'components';
|
|
61
|
+
readonly type: 'String';
|
|
62
|
+
}, {
|
|
63
|
+
readonly id: 'capture::proposal::walkable';
|
|
64
|
+
readonly name: 'walkable';
|
|
65
|
+
readonly type: 'bool';
|
|
66
|
+
}, {
|
|
67
|
+
readonly id: 'capture::proposal::minX';
|
|
68
|
+
readonly name: 'minX';
|
|
69
|
+
readonly type: 'f32';
|
|
70
|
+
}, {
|
|
71
|
+
readonly id: 'capture::proposal::minY';
|
|
72
|
+
readonly name: 'minY';
|
|
73
|
+
readonly type: 'f32';
|
|
74
|
+
}, {
|
|
75
|
+
readonly id: 'capture::proposal::minZ';
|
|
76
|
+
readonly name: 'minZ';
|
|
77
|
+
readonly type: 'f32';
|
|
78
|
+
}, {
|
|
79
|
+
readonly id: 'capture::proposal::maxX';
|
|
80
|
+
readonly name: 'maxX';
|
|
81
|
+
readonly type: 'f32';
|
|
82
|
+
}, {
|
|
83
|
+
readonly id: 'capture::proposal::maxY';
|
|
84
|
+
readonly name: 'maxY';
|
|
85
|
+
readonly type: 'f32';
|
|
86
|
+
}, {
|
|
87
|
+
readonly id: 'capture::proposal::maxZ';
|
|
88
|
+
readonly name: 'maxZ';
|
|
89
|
+
readonly type: 'f32';
|
|
90
|
+
}];
|
|
91
|
+
};
|
|
92
|
+
/** The proposals as a scene, keyed by field id — the form `serializeWorld` writes. */
|
|
93
|
+
export declare function proposalScene(proposals: readonly EntityProposal[]): EntsScene;
|
|
94
|
+
/**
|
|
95
|
+
* The proposals a scene carries, in the order they were written.
|
|
96
|
+
*
|
|
97
|
+
* **The way back, and it is here rather than in an editor** because the two halves have to agree
|
|
98
|
+
* about the field ids and one module holding both is how they stay that way. An entity that carries
|
|
99
|
+
* no proposal is skipped: a consumer may have added its own things to the same scene, and a capture
|
|
100
|
+
* reading a file is a guest in it.
|
|
101
|
+
*/
|
|
102
|
+
export declare function readProposals(scene: EntsScene): EntityProposal[];
|
|
103
|
+
/** Every stage of `scene` as one `.drft`. */
|
|
104
|
+
export declare function captureFile(scene: CaptureScene): ArrayBuffer;
|
package/dist/scene.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every stage of a capture assembled into one `.drft`, and the proposals read back out of it.
|
|
3
|
+
*
|
|
4
|
+
* **One file, because a capture that arrives as six is a capture somebody has to reassemble.** The
|
|
5
|
+
* cloud draws, the mesh is what a body collides with, the decode program is the surface's material,
|
|
6
|
+
* the polygon mesh is the way across it, and the proposals are what a scene might make of the
|
|
7
|
+
* regions. They were produced by five stages that know nothing about each other; what they have in
|
|
8
|
+
* common is the capture, and that is what a container is for.
|
|
9
|
+
*
|
|
10
|
+
* **The proposals are written as a scene, and they are still proposals.** `ENTS` carries them under
|
|
11
|
+
* a component this package declares — not under a consumer's `transform`, whose field ids belong to
|
|
12
|
+
* that consumer and which this package has no way to know. So a capture proposes in its own words,
|
|
13
|
+
* an editor accepts or rejects, and what a scene ends up holding is the consumer's decision. The
|
|
14
|
+
* schema is written out here rather than built, because `@driftengine/capture` does not depend on
|
|
15
|
+
* the entity model and must not: the file is data, and the data is what the two sides share.
|
|
16
|
+
*
|
|
17
|
+
* **What is not written is the collision mesh**, and that is a payload decision with a reason: it
|
|
18
|
+
* is the drawn mesh with its degenerate triangles removed, which `collisionMesh` does in a
|
|
19
|
+
* millisecond at load. A second copy of the same triangles in the file is a second copy to
|
|
20
|
+
* download. Hulls *are* written, because a convex decomposition is not something to redo on a
|
|
21
|
+
* player's machine.
|
|
22
|
+
*/
|
|
23
|
+
import { writeDrft, } from '@driftengine/drft';
|
|
24
|
+
/** The component a capture proposes in. Named for what it is: a proposal, not a thing. */
|
|
25
|
+
export const PROPOSAL_COMPONENT = 'captureProposal';
|
|
26
|
+
/**
|
|
27
|
+
* The fields a proposal carries, by stable id.
|
|
28
|
+
*
|
|
29
|
+
* **Six numbers rather than a bounds array**, because the field types an entity model declares are
|
|
30
|
+
* scalars and a strings — there is no list among them, and inventing one here would be inventing a
|
|
31
|
+
* type system for a single consumer. `label` is empty where nobody named the region, and
|
|
32
|
+
* `components` is the suggested set joined by spaces, which is what an editor offers and what it is
|
|
33
|
+
* free to ignore.
|
|
34
|
+
*/
|
|
35
|
+
export const PROPOSAL_SCHEMA = {
|
|
36
|
+
name: PROPOSAL_COMPONENT,
|
|
37
|
+
fields: [
|
|
38
|
+
{ id: 'capture::proposal::label', name: 'label', type: 'String' },
|
|
39
|
+
{ id: 'capture::proposal::components', name: 'components', type: 'String' },
|
|
40
|
+
{ id: 'capture::proposal::walkable', name: 'walkable', type: 'bool' },
|
|
41
|
+
{ id: 'capture::proposal::minX', name: 'minX', type: 'f32' },
|
|
42
|
+
{ id: 'capture::proposal::minY', name: 'minY', type: 'f32' },
|
|
43
|
+
{ id: 'capture::proposal::minZ', name: 'minZ', type: 'f32' },
|
|
44
|
+
{ id: 'capture::proposal::maxX', name: 'maxX', type: 'f32' },
|
|
45
|
+
{ id: 'capture::proposal::maxY', name: 'maxY', type: 'f32' },
|
|
46
|
+
{ id: 'capture::proposal::maxZ', name: 'maxZ', type: 'f32' },
|
|
47
|
+
],
|
|
48
|
+
};
|
|
49
|
+
const BOUNDS = ['minX', 'minY', 'minZ', 'maxX', 'maxY', 'maxZ'];
|
|
50
|
+
/** The proposals as a scene, keyed by field id — the form `serializeWorld` writes. */
|
|
51
|
+
export function proposalScene(proposals) {
|
|
52
|
+
return {
|
|
53
|
+
version: 1,
|
|
54
|
+
schemas: { [PROPOSAL_COMPONENT]: PROPOSAL_SCHEMA },
|
|
55
|
+
entities: proposals.map((proposal) => {
|
|
56
|
+
const fields = {
|
|
57
|
+
'capture::proposal::label': proposal.label ?? '',
|
|
58
|
+
'capture::proposal::components': proposal.components.join(' '),
|
|
59
|
+
'capture::proposal::walkable': proposal.walkable,
|
|
60
|
+
};
|
|
61
|
+
BOUNDS.forEach((name, at) => {
|
|
62
|
+
fields[`capture::proposal::${name}`] = proposal.bounds[at];
|
|
63
|
+
});
|
|
64
|
+
return { components: { [PROPOSAL_COMPONENT]: fields } };
|
|
65
|
+
}),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The proposals a scene carries, in the order they were written.
|
|
70
|
+
*
|
|
71
|
+
* **The way back, and it is here rather than in an editor** because the two halves have to agree
|
|
72
|
+
* about the field ids and one module holding both is how they stay that way. An entity that carries
|
|
73
|
+
* no proposal is skipped: a consumer may have added its own things to the same scene, and a capture
|
|
74
|
+
* reading a file is a guest in it.
|
|
75
|
+
*/
|
|
76
|
+
export function readProposals(scene) {
|
|
77
|
+
const out = [];
|
|
78
|
+
scene.entities.forEach((entity, at) => {
|
|
79
|
+
const fields = entity.components[PROPOSAL_COMPONENT];
|
|
80
|
+
if (fields === undefined)
|
|
81
|
+
return;
|
|
82
|
+
const bounds = new Float64Array(6);
|
|
83
|
+
BOUNDS.forEach((name, slot) => {
|
|
84
|
+
bounds[slot] = Number(fields[`capture::proposal::${name}`] ?? 0);
|
|
85
|
+
});
|
|
86
|
+
const label = String(fields['capture::proposal::label'] ?? '');
|
|
87
|
+
const components = String(fields['capture::proposal::components'] ?? '');
|
|
88
|
+
out.push({
|
|
89
|
+
region: at,
|
|
90
|
+
bounds,
|
|
91
|
+
label: label === '' ? null : label,
|
|
92
|
+
components: components === '' ? [] : components.split(' '),
|
|
93
|
+
walkable: fields['capture::proposal::walkable'] === true,
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
/** Every stage of `scene` as one `.drft`. */
|
|
99
|
+
export function captureFile(scene) {
|
|
100
|
+
return writeDrft({
|
|
101
|
+
meshes: [scene.mesh],
|
|
102
|
+
materials: scene.materials,
|
|
103
|
+
dtex: scene.material === undefined ? undefined : [scene.material],
|
|
104
|
+
splats: scene.splats,
|
|
105
|
+
colliders: scene.hulls,
|
|
106
|
+
navigation: scene.navigation,
|
|
107
|
+
entities: scene.proposals === undefined ? undefined : proposalScene(scene.proposals),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { MeshData } from '@driftengine/drft';
|
|
2
|
+
/** What a region is, once something has decided where it is. */
|
|
3
|
+
export interface Region {
|
|
4
|
+
/** Triangles belonging to it. */
|
|
5
|
+
readonly triangles: readonly number[];
|
|
6
|
+
/** `minX, minY, minZ, maxX, maxY, maxZ`, in the capture's world. */
|
|
7
|
+
readonly bounds: Float64Array;
|
|
8
|
+
/** Its area-weighted mean normal, which is what makes a floor a floor. */
|
|
9
|
+
readonly normal: Float64Array;
|
|
10
|
+
readonly area: number;
|
|
11
|
+
/** A label somebody attached, or null — which is the ordinary case. */
|
|
12
|
+
readonly label: string | null;
|
|
13
|
+
}
|
|
14
|
+
export interface SegmentOptions {
|
|
15
|
+
/**
|
|
16
|
+
* How far two triangles' normals may turn and still be one surface, in degrees.
|
|
17
|
+
*
|
|
18
|
+
* **The one number that decides what a region is.** Too small and a gently curved wall becomes a
|
|
19
|
+
* hundred slivers; too large and a floor swallows the skirting board.
|
|
20
|
+
*/
|
|
21
|
+
readonly creaseDegrees?: number;
|
|
22
|
+
/** Below this area a run of triangles is not a region. */
|
|
23
|
+
readonly minimumArea?: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Regions from the geometry alone: runs of triangles that share a plane.
|
|
27
|
+
*
|
|
28
|
+
* Two triangles join when they share an edge and their normals agree within the crease angle. Two
|
|
29
|
+
* surfaces that are parallel but apart — a table top and the floor under it — stay apart because
|
|
30
|
+
* they share no edge, not because anything measured the gap.
|
|
31
|
+
*
|
|
32
|
+
* **A plane offset is deliberately not compared, and the reason is worth keeping.** It was, and the
|
|
33
|
+
* check could never say what it claimed: two triangles either side of a shared edge both contain
|
|
34
|
+
* that edge, so equal normals force equal offsets, and unequal normals make the difference grow
|
|
35
|
+
* with distance from *the origin* — the same crease would join near the origin and split far from
|
|
36
|
+
* it, on a capture whose origin is wherever the first camera happened to stand.
|
|
37
|
+
*/
|
|
38
|
+
export declare function segmentGeometry(mesh: MeshData, options?: SegmentOptions): Region[];
|
|
39
|
+
export declare function planeOf(mesh: MeshData, face: number, planes: Float64Array, areas: Float64Array): void;
|
|
40
|
+
export declare function regionOf(mesh: MeshData, faces: readonly number[], planes: Float64Array, areas: Float64Array): Region;
|
package/dist/segment.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A captured mesh cut into surfaces, by its geometry alone.
|
|
3
|
+
*
|
|
4
|
+
* **This is what a consumer with no weights gets, and it is not a degraded version of anything.**
|
|
5
|
+
* Connected runs of triangles that share a plane find a room's floor, its walls and the top of a
|
|
6
|
+
* table without knowing what any of them *is* — which is most of what a scene needs to be useful,
|
|
7
|
+
* and is available to every build where a licence does not permit shipping a model. `masks.ts` is
|
|
8
|
+
* the other source of regions, not the real one.
|
|
9
|
+
*
|
|
10
|
+
* **Deterministic.** A mesh segmented twice gives the same regions in the same order, because a
|
|
11
|
+
* region's number ends up in a file and a consumer's scene refers to it.
|
|
12
|
+
*/
|
|
13
|
+
import { exactCos } from '@driftengine/core';
|
|
14
|
+
const DEFAULT_CREASE = 20;
|
|
15
|
+
const DEFAULT_MINIMUM_AREA = 0.02;
|
|
16
|
+
/**
|
|
17
|
+
* Regions from the geometry alone: runs of triangles that share a plane.
|
|
18
|
+
*
|
|
19
|
+
* Two triangles join when they share an edge and their normals agree within the crease angle. Two
|
|
20
|
+
* surfaces that are parallel but apart — a table top and the floor under it — stay apart because
|
|
21
|
+
* they share no edge, not because anything measured the gap.
|
|
22
|
+
*
|
|
23
|
+
* **A plane offset is deliberately not compared, and the reason is worth keeping.** It was, and the
|
|
24
|
+
* check could never say what it claimed: two triangles either side of a shared edge both contain
|
|
25
|
+
* that edge, so equal normals force equal offsets, and unequal normals make the difference grow
|
|
26
|
+
* with distance from *the origin* — the same crease would join near the origin and split far from
|
|
27
|
+
* it, on a capture whose origin is wherever the first camera happened to stand.
|
|
28
|
+
*/
|
|
29
|
+
export function segmentGeometry(mesh, options = {}) {
|
|
30
|
+
const crease = exactCos(((options.creaseDegrees ?? DEFAULT_CREASE) * Math.PI) / 180);
|
|
31
|
+
const minimumArea = options.minimumArea ?? DEFAULT_MINIMUM_AREA;
|
|
32
|
+
const triangles = mesh.indices.length / 3;
|
|
33
|
+
const planes = new Float64Array(triangles * 4);
|
|
34
|
+
const areas = new Float64Array(triangles);
|
|
35
|
+
for (let face = 0; face < triangles; face += 1)
|
|
36
|
+
planeOf(mesh, face, planes, areas);
|
|
37
|
+
/* Which triangles share an edge: the pair on each undirected edge of the mesh. */
|
|
38
|
+
const parent = new Int32Array(triangles);
|
|
39
|
+
for (let face = 0; face < triangles; face += 1)
|
|
40
|
+
parent[face] = face;
|
|
41
|
+
const owner = new Map();
|
|
42
|
+
const vertices = mesh.positions.length / 3;
|
|
43
|
+
const welded = weldPositions(mesh);
|
|
44
|
+
for (let face = 0; face < triangles; face += 1) {
|
|
45
|
+
for (let slot = 0; slot < 3; slot += 1) {
|
|
46
|
+
const a = welded[mesh.indices[face * 3 + slot]];
|
|
47
|
+
const b = welded[mesh.indices[face * 3 + ((slot + 1) % 3)]];
|
|
48
|
+
const key = a < b ? a * vertices + b : b * vertices + a;
|
|
49
|
+
const held = owner.get(key);
|
|
50
|
+
if (held === undefined) {
|
|
51
|
+
owner.set(key, face);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (joins(planes, held, face, crease))
|
|
55
|
+
union(parent, held, face);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/* Gathered in the order their lowest triangle appears, so the numbering is the mesh's own. */
|
|
59
|
+
const members = new Map();
|
|
60
|
+
for (let face = 0; face < triangles; face += 1) {
|
|
61
|
+
const root = find(parent, face);
|
|
62
|
+
const held = members.get(root);
|
|
63
|
+
if (held === undefined)
|
|
64
|
+
members.set(root, [face]);
|
|
65
|
+
else
|
|
66
|
+
held.push(face);
|
|
67
|
+
}
|
|
68
|
+
const out = [];
|
|
69
|
+
for (const [, faces] of members) {
|
|
70
|
+
const region = regionOf(mesh, faces, planes, areas);
|
|
71
|
+
if (region.area < minimumArea)
|
|
72
|
+
continue;
|
|
73
|
+
out.push(region);
|
|
74
|
+
}
|
|
75
|
+
/* Largest first, so a consumer reading a prefix reads the parts of the scene that matter. */
|
|
76
|
+
out.sort((a, b) => b.area - a.area);
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* One vertex per distinct position, so a mesh whose faces do not share vertices still has edges.
|
|
81
|
+
*
|
|
82
|
+
* **Flat shading splits every vertex**, and so does any export that keeps a hard crease: each face
|
|
83
|
+
* arrives with its own copy of each corner. Joining by vertex index alone then finds no shared edge
|
|
84
|
+
* anywhere on such a mesh and answers *every triangle is its own region* — a segmentation that
|
|
85
|
+
* found nothing, returned with a straight face. What it gives up: two surfaces that touch without
|
|
86
|
+
* being joined in the file are joined here, which is what the crease and offset tests are for.
|
|
87
|
+
*
|
|
88
|
+
* Welded on the exact bits of the position rather than within a tolerance, because that is what a
|
|
89
|
+
* duplicate is — a copy — and a tolerance would make the result depend on which vertex was met
|
|
90
|
+
* first. This engine's own marching welds already, so on a capture's own mesh it is a no-op.
|
|
91
|
+
*/
|
|
92
|
+
function weldPositions(mesh) {
|
|
93
|
+
const vertices = mesh.positions.length / 3;
|
|
94
|
+
const out = new Int32Array(vertices);
|
|
95
|
+
const seen = new Map();
|
|
96
|
+
for (let v = 0; v < vertices; v += 1) {
|
|
97
|
+
const key = `${mesh.positions[v * 3]},${mesh.positions[v * 3 + 1]},${mesh.positions[v * 3 + 2]}`;
|
|
98
|
+
const held = seen.get(key);
|
|
99
|
+
if (held === undefined) {
|
|
100
|
+
seen.set(key, v);
|
|
101
|
+
out[v] = v;
|
|
102
|
+
}
|
|
103
|
+
else
|
|
104
|
+
out[v] = held;
|
|
105
|
+
}
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
108
|
+
export function planeOf(mesh, face, planes, areas) {
|
|
109
|
+
const a = mesh.indices[face * 3];
|
|
110
|
+
const b = mesh.indices[face * 3 + 1];
|
|
111
|
+
const c = mesh.indices[face * 3 + 2];
|
|
112
|
+
const ux = mesh.positions[b * 3] - mesh.positions[a * 3];
|
|
113
|
+
const uy = mesh.positions[b * 3 + 1] - mesh.positions[a * 3 + 1];
|
|
114
|
+
const uz = mesh.positions[b * 3 + 2] - mesh.positions[a * 3 + 2];
|
|
115
|
+
const vx = mesh.positions[c * 3] - mesh.positions[a * 3];
|
|
116
|
+
const vy = mesh.positions[c * 3 + 1] - mesh.positions[a * 3 + 1];
|
|
117
|
+
const vz = mesh.positions[c * 3 + 2] - mesh.positions[a * 3 + 2];
|
|
118
|
+
const nx = uy * vz - uz * vy;
|
|
119
|
+
const ny = uz * vx - ux * vz;
|
|
120
|
+
const nz = ux * vy - uy * vx;
|
|
121
|
+
const length = Math.sqrt(nx * nx + ny * ny + nz * nz);
|
|
122
|
+
areas[face] = length / 2;
|
|
123
|
+
if (!(length > 0))
|
|
124
|
+
return;
|
|
125
|
+
planes[face * 4] = nx / length;
|
|
126
|
+
planes[face * 4 + 1] = ny / length;
|
|
127
|
+
planes[face * 4 + 2] = nz / length;
|
|
128
|
+
planes[face * 4 + 3] =
|
|
129
|
+
(nx / length) * mesh.positions[a * 3] +
|
|
130
|
+
(ny / length) * mesh.positions[a * 3 + 1] +
|
|
131
|
+
(nz / length) * mesh.positions[a * 3 + 2];
|
|
132
|
+
}
|
|
133
|
+
function joins(planes, first, second, crease) {
|
|
134
|
+
let facing = 0;
|
|
135
|
+
for (let k = 0; k < 3; k += 1) {
|
|
136
|
+
facing += planes[first * 4 + k] * planes[second * 4 + k];
|
|
137
|
+
}
|
|
138
|
+
return facing >= crease;
|
|
139
|
+
}
|
|
140
|
+
export function regionOf(mesh, faces, planes, areas) {
|
|
141
|
+
const bounds = Float64Array.from([Infinity, Infinity, Infinity, -Infinity, -Infinity, -Infinity]);
|
|
142
|
+
const normal = new Float64Array(3);
|
|
143
|
+
let area = 0;
|
|
144
|
+
for (const face of faces) {
|
|
145
|
+
const weight = areas[face];
|
|
146
|
+
area += weight;
|
|
147
|
+
for (let k = 0; k < 3; k += 1) {
|
|
148
|
+
normal[k] = normal[k] + planes[face * 4 + k] * weight;
|
|
149
|
+
}
|
|
150
|
+
for (let slot = 0; slot < 3; slot += 1) {
|
|
151
|
+
const vertex = mesh.indices[face * 3 + slot];
|
|
152
|
+
for (let k = 0; k < 3; k += 1) {
|
|
153
|
+
const value = mesh.positions[vertex * 3 + k];
|
|
154
|
+
bounds[k] = Math.min(bounds[k], value);
|
|
155
|
+
bounds[k + 3] = Math.max(bounds[k + 3], value);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const length = Math.sqrt(normal[0] * normal[0] +
|
|
160
|
+
normal[1] * normal[1] +
|
|
161
|
+
normal[2] * normal[2]);
|
|
162
|
+
if (length > 0)
|
|
163
|
+
for (let k = 0; k < 3; k += 1)
|
|
164
|
+
normal[k] = normal[k] / length;
|
|
165
|
+
return { triangles: [...faces].sort((a, b) => a - b), bounds, normal, area, label: null };
|
|
166
|
+
}
|
|
167
|
+
function find(parent, at) {
|
|
168
|
+
let root = at;
|
|
169
|
+
while (parent[root] !== root)
|
|
170
|
+
root = parent[root];
|
|
171
|
+
let walk = at;
|
|
172
|
+
while (parent[walk] !== root) {
|
|
173
|
+
const next = parent[walk];
|
|
174
|
+
parent[walk] = root;
|
|
175
|
+
walk = next;
|
|
176
|
+
}
|
|
177
|
+
return root;
|
|
178
|
+
}
|
|
179
|
+
function union(parent, a, b) {
|
|
180
|
+
const first = find(parent, a);
|
|
181
|
+
const second = find(parent, b);
|
|
182
|
+
if (first !== second)
|
|
183
|
+
parent[second] = first;
|
|
184
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analytic scenes rendered on the CPU, so a capture's stages can be held to geometry that is known
|
|
3
|
+
* before anything runs.
|
|
4
|
+
*
|
|
5
|
+
* **It ships rather than sitting in a test**, as `@driftengine/nav`'s test field does: a consumer
|
|
6
|
+
* porting this, or checking a device against the reference, needs the same scenes the engine's own
|
|
7
|
+
* tests use, and a fixture that exists only under `*.test.ts` cannot be imported.
|
|
8
|
+
*
|
|
9
|
+
* **A ray cast rather than a rasteriser.** Every pixel's answer is the distance along its own ray,
|
|
10
|
+
* so the depth written beside the colour is the truth rather than an interpolation — which is what
|
|
11
|
+
* makes it a fixture for depth, for triangulation and for the pose that comes out of them. The
|
|
12
|
+
* shapes are boxes and planes, which intersect in closed form, and the surfaces are a procedural
|
|
13
|
+
* checker with a seeded value noise on top: **a corner detector needs something to find**, and a
|
|
14
|
+
* flat colour would make every later test pass for the wrong reason.
|
|
15
|
+
*
|
|
16
|
+
* **What it gives up**: no shadows, no reflections, one directional light. Nothing in a capture
|
|
17
|
+
* cares — what is measured is where things are, not how they look.
|
|
18
|
+
*/
|
|
19
|
+
export interface TestCamera {
|
|
20
|
+
readonly width: number;
|
|
21
|
+
readonly height: number;
|
|
22
|
+
/** `fx`, `fy`, `cx`, `cy`. */
|
|
23
|
+
readonly intrinsics: readonly [number, number, number, number];
|
|
24
|
+
/** 3 × 4 row-major, world to camera. */
|
|
25
|
+
readonly worldToCamera: Float64Array;
|
|
26
|
+
}
|
|
27
|
+
export interface TestBox {
|
|
28
|
+
readonly min: readonly [number, number, number];
|
|
29
|
+
readonly max: readonly [number, number, number];
|
|
30
|
+
readonly seed: number;
|
|
31
|
+
}
|
|
32
|
+
export interface TestPlane {
|
|
33
|
+
readonly normal: readonly [number, number, number];
|
|
34
|
+
/** The plane is `normal · x = offset`. */
|
|
35
|
+
readonly offset: number;
|
|
36
|
+
readonly seed: number;
|
|
37
|
+
}
|
|
38
|
+
export interface TestScene {
|
|
39
|
+
readonly boxes: readonly TestBox[];
|
|
40
|
+
readonly planes: readonly TestPlane[];
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* A world-to-camera for an eye looking at a target, with the world's up.
|
|
44
|
+
*
|
|
45
|
+
* **The camera's axes are the ones an image has**: x to the right, **y downward** and z forward, so
|
|
46
|
+
* row zero of a frame is the top of it. Taking y upward instead renders every scene upside down,
|
|
47
|
+
* which a test of a corner's position finds and a test of a colour does not.
|
|
48
|
+
*/
|
|
49
|
+
export declare function lookAt(eye: readonly [number, number, number], target: readonly [number, number, number], up?: readonly [number, number, number]): Float64Array;
|
|
50
|
+
/**
|
|
51
|
+
* `scene` through `camera` into `out`, RGBA, and the distance along the camera's own axis into
|
|
52
|
+
* `depth` where one
|
|
53
|
+
* is given — zero where the ray met nothing.
|
|
54
|
+
*/
|
|
55
|
+
export declare function renderTestScene(scene: TestScene, camera: TestCamera, out: Uint8Array, depth?: Float32Array): void;
|