@nakednous/host 0.0.5 → 0.0.6
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/README.md +12 -2
- package/dist/index.js +338 -20
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -82,7 +82,8 @@ dependency first and an options object last, and registers with the context so
|
|
|
82
82
|
| `host.poseTrack(opts)` · `host.cameraTrack(cam, opts)` | core tracks ticked while they play; `{ handles }` builds `TrackHandles`, one handle per keyframe |
|
|
83
83
|
| `host.hid(opts)` · `host.gamepad(opts)` | rate streams — `lin[3]` / `ang[3]` in raw device units, `bind(helm)` feeds it each tick; `available`, `connected` |
|
|
84
84
|
| `host.image(url)` · `host.video(opts)` · `host.raster(draw, w, h)` | texture sources — an `ImageBitmap`, a hidden `<video>` from a file or the camera with a `ready` promise, a Canvas2D drawing |
|
|
85
|
-
| `host.
|
|
85
|
+
| `host.mesh(url, opts)` | one mesh — the arrays shape `{ position, indices, normal?, tangent?, texcoord?, color?, joints?, weights? }` with its `bounds` `{ min, max, center, diag }` beside them — from an OBJ file, a one-part glTF 2.0 file (`.glb`, `.gltf`), or any format through `{ parse: (arrayBuffer) => arrays }`; what a bridge's buffer upload takes as it is, and `size / mesh.bounds.diag` fits any file to `size`. A file of several parts rejects, naming `model`. `{ format }` when the URL does not say. **`normals`**: left out, the file's normals are kept and a mesh without any gets smooth ones; `'smooth'` always recomputes them, summed per position, so a faceted file or a texture seam smooths too; `'flat'` always recomputes them on the flattened mesh (every triangle owning its vertices: more vertices, indices counting up); `false` computes nothing. The procedure, its limits and sources are tree's `mesh` module's; glTF viewers compute flat normals for a file without any — here that is `'flat'` |
|
|
86
|
+
| `host.model(url, opts)` | a model with its structure — `{ parts: [{ name, node, skin, mesh, targets, color }], nodes: { names, parents, rest }, skins: [{ name, joints, inverseBind }], clips: [{ name, duration, channels }] }` — what a rig, morph targets or animation need. A part's `mesh` is as `host.mesh` returns it, in the part's own space; `targets` are morph deltas; nodes come parents first with a rest pose, and with `skins` and `clips` feed tree's `clipSample` · `poseBlend` · `poseWorld` · `jointPalette` as they are. Same options; `'flat'` flattens each part's morph targets with its mesh. An OBJ file is one white part under one identity node. host's own parsers: no textures, cameras, sparse accessors or compressed geometry |
|
|
86
87
|
| `host.labels` · `host.hasLabels` | the label layer, created on first access — `set(id, text, x, y, z, opts)` at a world anchor, `setScreen(id, text, sx, sy, opts)` in canvas px, `{ frame: true }` for a label that lives one frame, `tick()`; `hasLabels` probes without creating |
|
|
87
88
|
| `host.orbit(cam, opts)` | the fall-through gesture — one pointer orbits, two pan and dolly, the wheel dollies; `damping` in seconds (default 0.2; 0 exact) makes the camera trail the gesture and complete it after release; `update(dt)` reports whether the camera moved; the vertical sense follows the bag's projection (y-up GL, y-flipped p5) |
|
|
88
89
|
| `host.tick(dt)` | external-loop mode: tick the players; the caller flushes the pointer once its consumers ran |
|
|
@@ -91,7 +92,7 @@ dependency first and an options object last, and registers with the context so
|
|
|
91
92
|
The module-level functions behind the methods are exported too (`createView`, `createPointer`,
|
|
92
93
|
`createPlayers`, `createLoop`, `observeCanvas`, `Handle`, `PointerRouter`, `cameraHelm`,
|
|
93
94
|
`poseHelm`, `helmBasis`, `poseTrack`, `cameraTrack`, `TrackHandles`, `createHid`,
|
|
94
|
-
`createGamepad`, `decodeSpaceNavigator`, `loadImage`, `createVideo`, `raster`, `loadModel`, `createLabels`,
|
|
95
|
+
`createGamepad`, `decodeSpaceNavigator`, `loadImage`, `createVideo`, `raster`, `loadMesh`, `loadModel`, `createLabels`,
|
|
95
96
|
`createOrbit`), so an adapter can compose them on its own context.
|
|
96
97
|
|
|
97
98
|
---
|
|
@@ -154,6 +155,15 @@ npm run build # rollup → dist/index.js
|
|
|
154
155
|
|
|
155
156
|
---
|
|
156
157
|
|
|
158
|
+
## Acknowledgements
|
|
159
|
+
|
|
160
|
+
- [glTF 2.0](https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html) (Khronos Group) — the format `host/gltf` reads: the GLB container, accessors and buffer views, skins, morph targets, animations.
|
|
161
|
+
- Wavefront OBJ — the format `host/obj` reads.
|
|
162
|
+
- [three.js](https://threejs.org/) — the orbit's damping follows the shape of its `OrbitControls`, made time-based.
|
|
163
|
+
- [twgl](https://twgljs.org/) — the arrays shape a model's meshes arrive in.
|
|
164
|
+
|
|
165
|
+
Both parsers are host's own; no third-party code ships in the package.
|
|
166
|
+
|
|
157
167
|
## License
|
|
158
168
|
|
|
159
169
|
AGPL-3.0-only
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { WEBGL, cameraView, cameraProj, mat4Mul, mat4Invert, PLANE, createConstraint, WORLD, AXIS, DIAL, POINT, mapDirection, pixelRatio, rayHitSphere, unproject, SPHERE, dirFromAzEl, DIRECTION, mapLocation, MATRIX, PoseHelm, cameraToPose, EYE, SELF, qToMat4, cameraEye, cameraFromPose, CameraTrack, PoseTrack, qFromAxisAngle, SCREEN, cameraCopy, createCamera, coastAlive, coastStep, cameraOrbit, cameraPan, cameraDolly } from '@nakednous/tree';
|
|
1
|
+
import { WEBGL, cameraView, cameraProj, mat4Mul, mat4Invert, PLANE, createConstraint, WORLD, AXIS, DIAL, POINT, mapDirection, pixelRatio, rayHitSphere, unproject, SPHERE, dirFromAzEl, DIRECTION, mapLocation, MATRIX, PoseHelm, cameraToPose, EYE, SELF, qToMat4, cameraEye, cameraFromPose, CameraTrack, PoseTrack, qFromAxisAngle, mat4ToTransform, meshBounds, meshFlatten, meshNormals, meshGroups, SCREEN, cameraCopy, createCamera, coastAlive, coastStep, cameraOrbit, cameraPan, cameraDolly } from '@nakednous/tree';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* @file The view bag — the matrices every pick, solve, label and orbit reads.
|
|
@@ -1923,12 +1923,203 @@ function parseObj(text) {
|
|
|
1923
1923
|
}
|
|
1924
1924
|
|
|
1925
1925
|
/**
|
|
1926
|
-
* @file
|
|
1926
|
+
* @file glTF 2.0 → meshes in the arrays shape, the node hierarchy, skins and animation clips.
|
|
1927
|
+
* @module host/gltf
|
|
1928
|
+
* @license AGPL-3.0-only
|
|
1929
|
+
*
|
|
1930
|
+
* The format is glTF 2.0 (Khronos Group): a JSON document plus binary buffers,
|
|
1931
|
+
* or both in one GLB container. parseGlb splits a container; parseGltf reads
|
|
1932
|
+
* the document over its buffers into what a bridge uploads and what tree's
|
|
1933
|
+
* skin functions (clipSample, poseWorld, jointPalette) take as they are.
|
|
1934
|
+
*
|
|
1935
|
+
* Read: triangle primitives (POSITION, NORMAL, TANGENT, TEXCOORD_0, JOINTS_0,
|
|
1936
|
+
* WEIGHTS_0, indices), morph targets (POSITION and NORMAL deltas), the
|
|
1937
|
+
* material's base colour factor, nodes, skins, animations. Normalised integer
|
|
1938
|
+
* accessors arrive as floats; interleaved views are unpacked. Not read:
|
|
1939
|
+
* textures, cameras, other primitive modes, sparse accessors and compressed
|
|
1940
|
+
* geometry (Draco, meshopt) — the last two throw.
|
|
1941
|
+
*
|
|
1942
|
+
* Nodes are re-indexed parents first, and every node index in the result
|
|
1943
|
+
* (a mesh's, a skin's joints, a channel's) is the new one.
|
|
1944
|
+
*/
|
|
1945
|
+
|
|
1946
|
+
|
|
1947
|
+
const GLB_MAGIC = 0x46546C67, CHUNK_JSON = 0x4E4F534A, CHUNK_BIN = 0x004E4942;
|
|
1948
|
+
|
|
1949
|
+
const COMPONENTS = { SCALAR: 1, VEC2: 2, VEC3: 3, VEC4: 4, MAT2: 4, MAT3: 9, MAT4: 16 };
|
|
1950
|
+
const TYPES = {
|
|
1951
|
+
5120: Int8Array, 5121: Uint8Array, 5122: Int16Array, 5123: Uint16Array, 5125: Uint32Array, 5126: Float32Array,
|
|
1952
|
+
};
|
|
1953
|
+
const NORMALISED = { 5120: 127, 5121: 255, 5122: 32767, 5123: 65535 };
|
|
1954
|
+
const ATTRIBUTES = {
|
|
1955
|
+
POSITION: 'position', NORMAL: 'normal', TANGENT: 'tangent', TEXCOORD_0: 'texcoord',
|
|
1956
|
+
JOINTS_0: 'joints', WEIGHTS_0: 'weights',
|
|
1957
|
+
};
|
|
1958
|
+
const IDENTITY = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
|
|
1959
|
+
|
|
1960
|
+
/**
|
|
1961
|
+
* Split a GLB container into its JSON document and binary chunk.
|
|
1962
|
+
* @param {ArrayBuffer} buffer
|
|
1963
|
+
* @returns {{ json:object, bin:ArrayBuffer|null }}
|
|
1964
|
+
*/
|
|
1965
|
+
function parseGlb(buffer) {
|
|
1966
|
+
const view = new DataView(buffer);
|
|
1967
|
+
if (view.getUint32(0, true) !== GLB_MAGIC) throw new Error('[host] gltf: not a GLB container.');
|
|
1968
|
+
if (view.getUint32(4, true) !== 2) throw new Error('[host] gltf: GLB version ' + view.getUint32(4, true) + ', 2 expected.');
|
|
1969
|
+
const end = Math.min(view.getUint32(8, true), buffer.byteLength);
|
|
1970
|
+
let json = null, bin = null;
|
|
1971
|
+
for (let at = 12; at + 8 <= end;) {
|
|
1972
|
+
const length = view.getUint32(at, true), type = view.getUint32(at + 4, true);
|
|
1973
|
+
const start = at + 8;
|
|
1974
|
+
if (type === CHUNK_JSON) json = JSON.parse(new TextDecoder().decode(new Uint8Array(buffer, start, length)));
|
|
1975
|
+
else if (type === CHUNK_BIN && !bin) bin = buffer.slice(start, start + length);
|
|
1976
|
+
at = start + length + (-length & 3);
|
|
1977
|
+
}
|
|
1978
|
+
if (!json) throw new Error('[host] gltf: GLB without a JSON chunk.');
|
|
1979
|
+
return { json, bin };
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
/**
|
|
1983
|
+
* Read a glTF document over its buffers.
|
|
1984
|
+
*
|
|
1985
|
+
* A part is one triangle primitive under one node: `mesh` in the
|
|
1986
|
+
* arrays shape (position, normal?, tangent?, texcoord?, joints?, weights?,
|
|
1987
|
+
* indices), `targets` its morph targets as delta arrays, `color` the
|
|
1988
|
+
* material's base colour factor, `node` the node it hangs from and `skin` its
|
|
1989
|
+
* skin's index or −1. A rigid mesh draws under its node's world matrix; a
|
|
1990
|
+
* skinned one under its skin's joint palette.
|
|
1991
|
+
*
|
|
1992
|
+
* @param {object} json The glTF document.
|
|
1993
|
+
* @param {ArrayBuffer[]} buffers One per `json.buffers` entry.
|
|
1994
|
+
* @returns {{ parts:{ name:string, node:number, skin:number, mesh:object,
|
|
1995
|
+
* targets:{ name:string, position?:object, normal?:object }[], color:number[] }[],
|
|
1996
|
+
* nodes:{ names:string[], parents:Int32Array, rest:Float32Array },
|
|
1997
|
+
* skins:{ name:string, joints:Uint16Array, inverseBind:Float32Array }[],
|
|
1998
|
+
* clips:{ name:string, duration:number, channels:object[] }[] }}
|
|
1999
|
+
* nodes.rest: the rest pose, ten numbers per node (translation, rotation
|
|
2000
|
+
* [x,y,z,w], scale). A clip channel: { node, path, interp, times, values }.
|
|
2001
|
+
*/
|
|
2002
|
+
function parseGltf(json, buffers) {
|
|
2003
|
+
for (const ext of json.extensionsRequired || []) {
|
|
2004
|
+
if (/draco|meshopt/i.test(ext)) throw new Error('[host] gltf: ' + ext + ' is not supported.');
|
|
2005
|
+
}
|
|
2006
|
+
const cache = new Map();
|
|
2007
|
+
|
|
2008
|
+
const accessor = (index) => {
|
|
2009
|
+
if (cache.has(index)) return cache.get(index);
|
|
2010
|
+
const a = json.accessors[index];
|
|
2011
|
+
if (a.sparse) throw new Error('[host] gltf: sparse accessors are not supported.');
|
|
2012
|
+
const n = COMPONENTS[a.type], Type = TYPES[a.componentType], scale = a.normalized && NORMALISED[a.componentType];
|
|
2013
|
+
const data = new (scale ? Float32Array : Type)(a.count * n);
|
|
2014
|
+
if (a.bufferView !== undefined) {
|
|
2015
|
+
const bv = json.bufferViews[a.bufferView];
|
|
2016
|
+
const base = (bv.byteOffset || 0) + (a.byteOffset || 0), size = Type.BYTES_PER_ELEMENT;
|
|
2017
|
+
const stride = bv.byteStride || n * size;
|
|
2018
|
+
const view = new DataView(buffers[bv.buffer]);
|
|
2019
|
+
const get = {
|
|
2020
|
+
5120: 'getInt8', 5121: 'getUint8', 5122: 'getInt16', 5123: 'getUint16', 5125: 'getUint32', 5126: 'getFloat32',
|
|
2021
|
+
}[a.componentType];
|
|
2022
|
+
for (let i = 0; i < a.count; i++) {
|
|
2023
|
+
for (let c = 0; c < n; c++) {
|
|
2024
|
+
const v = view[get](base + i * stride + c * size, true);
|
|
2025
|
+
data[i * n + c] = scale ? Math.max(v / scale, -1) : v;
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
const out = { numComponents: n, data };
|
|
2030
|
+
cache.set(index, out);
|
|
2031
|
+
return out;
|
|
2032
|
+
};
|
|
2033
|
+
|
|
2034
|
+
// nodes, parents first
|
|
2035
|
+
const src = json.nodes || [], parentOf = new Array(src.length).fill(-1);
|
|
2036
|
+
src.forEach((node, i) => { for (const c of node.children || []) parentOf[c] = i; });
|
|
2037
|
+
const order = [], remap = new Array(src.length).fill(-1);
|
|
2038
|
+
const visit = (i) => { remap[i] = order.length; order.push(i); for (const c of src[i].children || []) visit(c); };
|
|
2039
|
+
src.forEach((_, i) => { if (parentOf[i] < 0) visit(i); });
|
|
2040
|
+
|
|
2041
|
+
const names = [], parents = new Int32Array(order.length), rest = new Float32Array(order.length * 10);
|
|
2042
|
+
const xf = { pos: [0, 0, 0], rot: [0, 0, 0, 1], scl: [1, 1, 1] };
|
|
2043
|
+
order.forEach((old, i) => {
|
|
2044
|
+
const node = src[old];
|
|
2045
|
+
names.push(node.name || '');
|
|
2046
|
+
parents[i] = parentOf[old] < 0 ? -1 : remap[parentOf[old]];
|
|
2047
|
+
let t = node.translation || [0, 0, 0], r = node.rotation || [0, 0, 0, 1], s = node.scale || [1, 1, 1];
|
|
2048
|
+
if (node.matrix) { mat4ToTransform(xf, node.matrix); t = xf.pos; r = xf.rot; s = xf.scl; }
|
|
2049
|
+
rest.set(t, i * 10); rest.set(r, i * 10 + 3); rest.set(s, i * 10 + 7);
|
|
2050
|
+
});
|
|
2051
|
+
|
|
2052
|
+
// parts: one per triangle primitive per node
|
|
2053
|
+
const primitives = (json.meshes || []).map((mesh) => {
|
|
2054
|
+
const out = [];
|
|
2055
|
+
for (const prim of mesh.primitives) {
|
|
2056
|
+
if ((prim.mode ?? 4) !== 4 || prim.attributes.POSITION === undefined) continue;
|
|
2057
|
+
const arrays = {};
|
|
2058
|
+
for (const [semantic, key] of Object.entries(ATTRIBUTES)) {
|
|
2059
|
+
if (prim.attributes[semantic] !== undefined) arrays[key] = accessor(prim.attributes[semantic]);
|
|
2060
|
+
}
|
|
2061
|
+
const count = arrays.position.data.length / 3;
|
|
2062
|
+
const data = prim.indices !== undefined
|
|
2063
|
+
? Uint32Array.from(accessor(prim.indices).data)
|
|
2064
|
+
: Uint32Array.from({ length: count }, (_, i) => i);
|
|
2065
|
+
arrays.indices = { numComponents: 3, data };
|
|
2066
|
+
const targetNames = (mesh.extras && mesh.extras.targetNames) || [];
|
|
2067
|
+
const targets = (prim.targets || []).map((target, i) => {
|
|
2068
|
+
const t = { name: targetNames[i] || '' };
|
|
2069
|
+
if (target.POSITION !== undefined) t.position = accessor(target.POSITION);
|
|
2070
|
+
if (target.NORMAL !== undefined) t.normal = accessor(target.NORMAL);
|
|
2071
|
+
return t;
|
|
2072
|
+
});
|
|
2073
|
+
const material = prim.material !== undefined ? json.materials[prim.material] : null;
|
|
2074
|
+
const pbr = material && material.pbrMetallicRoughness;
|
|
2075
|
+
out.push({ arrays, targets, color: (pbr && pbr.baseColorFactor ? pbr.baseColorFactor : [1, 1, 1, 1]).slice() });
|
|
2076
|
+
}
|
|
2077
|
+
return { name: mesh.name || '', primitives: out };
|
|
2078
|
+
});
|
|
2079
|
+
|
|
2080
|
+
const parts = [];
|
|
2081
|
+
order.forEach((old, i) => {
|
|
2082
|
+
const node = src[old];
|
|
2083
|
+
if (node.mesh === undefined) return;
|
|
2084
|
+
const mesh = primitives[node.mesh];
|
|
2085
|
+
for (const p of mesh.primitives) {
|
|
2086
|
+
parts.push({ name: mesh.name || names[i], node: i, skin: node.skin ?? -1, mesh: p.arrays, targets: p.targets, color: p.color });
|
|
2087
|
+
}
|
|
2088
|
+
});
|
|
2089
|
+
|
|
2090
|
+
const skins = (json.skins || []).map((skin) => {
|
|
2091
|
+
const joints = Uint16Array.from(skin.joints, j => remap[j]);
|
|
2092
|
+
const inverseBind = skin.inverseBindMatrices !== undefined
|
|
2093
|
+
? Float32Array.from(accessor(skin.inverseBindMatrices).data)
|
|
2094
|
+
: Float32Array.from({ length: joints.length * 16 }, (_, k) => IDENTITY[k % 16]);
|
|
2095
|
+
return { name: skin.name || '', joints, inverseBind };
|
|
2096
|
+
});
|
|
2097
|
+
|
|
2098
|
+
const clips = (json.animations || []).map((anim) => {
|
|
2099
|
+
let duration = 0;
|
|
2100
|
+
const channels = [];
|
|
2101
|
+
for (const ch of anim.channels) {
|
|
2102
|
+
if (ch.target.node === undefined) continue;
|
|
2103
|
+
const sampler = anim.samplers[ch.sampler];
|
|
2104
|
+
const times = accessor(sampler.input).data, values = accessor(sampler.output).data;
|
|
2105
|
+
if (times.length) duration = Math.max(duration, times[times.length - 1]);
|
|
2106
|
+
channels.push({ node: remap[ch.target.node], path: ch.target.path, interp: sampler.interpolation || 'LINEAR', times, values });
|
|
2107
|
+
}
|
|
2108
|
+
return { name: anim.name || '', duration, channels };
|
|
2109
|
+
});
|
|
2110
|
+
|
|
2111
|
+
return { parts, nodes: { names, parents, rest }, skins, clips };
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
/**
|
|
2115
|
+
* @file Media sources — images, video and Canvas2D rasters for a bridge's textures, models for its buffers.
|
|
1927
2116
|
* @module host/media
|
|
1928
2117
|
* @license AGPL-3.0-only
|
|
1929
2118
|
*
|
|
1930
|
-
* A
|
|
1931
|
-
*
|
|
2119
|
+
* A mesh arrives as arrays for a bridge's buffer upload, its bounds beside
|
|
2120
|
+
* them: loadMesh. A model keeps a file's structure — parts, hierarchy, skins,
|
|
2121
|
+
* clips: loadModel. Both read OBJ (host/obj) and glTF 2.0 (host/gltf), or
|
|
2122
|
+
* another format through a parser the caller passes.
|
|
1932
2123
|
*
|
|
1933
2124
|
* What a texture upload reads from: an ImageBitmap fetched from a URL, a
|
|
1934
2125
|
* hidden <video> element from a file or the user's camera, a bitmap drawn
|
|
@@ -1955,26 +2146,151 @@ async function loadImage(url, opts) {
|
|
|
1955
2146
|
return o.bitmap ? createImageBitmap(blob, o.bitmap) : createImageBitmap(blob);
|
|
1956
2147
|
}
|
|
1957
2148
|
|
|
2149
|
+
const _ONE_NODE = () => ({ names: [''], parents: Int32Array.of(-1), rest: Float32Array.of(0, 0, 0, 0, 0, 0, 1, 1, 1, 1) });
|
|
2150
|
+
const _part = (mesh) => ({ name: '', node: 0, skin: -1, mesh, targets: [], color: [1, 1, 1, 1] });
|
|
2151
|
+
|
|
2152
|
+
// A morph target's deltas expanded through its mesh's indices; a target carries no indices of its own.
|
|
2153
|
+
function _flatTarget(target, indices) {
|
|
2154
|
+
const flat = meshFlatten(target, indices);
|
|
2155
|
+
delete flat.indices;
|
|
2156
|
+
return flat;
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2159
|
+
// The normals option applied to every part, then each mesh's bounds; parts sharing a mesh share the result.
|
|
2160
|
+
// undefined / true the file's normals; smooth ones where a mesh has none
|
|
2161
|
+
// 'smooth' always recomputed, summed per position (tree's meshGroups), so a faceted file smooths
|
|
2162
|
+
// 'flat' always recomputed on the flattened mesh (tree's meshFlatten), morph targets flattened with it
|
|
2163
|
+
// false nothing computed
|
|
2164
|
+
function _finish(model, o) {
|
|
2165
|
+
const mode = o.normals === undefined || o.normals === true ? 'fill' : o.normals;
|
|
2166
|
+
if (mode !== 'fill' && mode !== 'smooth' && mode !== 'flat' && mode !== false) {
|
|
2167
|
+
throw new Error("[host] model: normals is one of 'smooth', 'flat', false, or left out.");
|
|
2168
|
+
}
|
|
2169
|
+
const done = new Map();
|
|
2170
|
+
for (const part of model.parts) {
|
|
2171
|
+
const source = part.mesh;
|
|
2172
|
+
if (done.has(source)) {
|
|
2173
|
+
part.mesh = done.get(source);
|
|
2174
|
+
if (mode === 'flat') part.targets = part.targets.map(t => _flatTarget(t, source.indices.data));
|
|
2175
|
+
continue;
|
|
2176
|
+
}
|
|
2177
|
+
let mesh = source;
|
|
2178
|
+
const bounds = meshBounds({ min: [0, 0, 0], max: [0, 0, 0], center: [0, 0, 0], diag: 0 }, mesh.position.data);
|
|
2179
|
+
if (mode === 'flat') {
|
|
2180
|
+
mesh = meshFlatten(source);
|
|
2181
|
+
const p = mesh.position.data;
|
|
2182
|
+
mesh.normal = { numComponents: 3, data: meshNormals(new Float32Array(p.length), p, mesh.indices.data) };
|
|
2183
|
+
part.targets = part.targets.map(t => _flatTarget(t, source.indices.data));
|
|
2184
|
+
} else if (mode === 'smooth' || (mode === 'fill' && !mesh.normal)) {
|
|
2185
|
+
const p = mesh.position.data;
|
|
2186
|
+
const groups = meshGroups(new Int32Array(p.length / 3), p, 1e-6 * bounds.diag);
|
|
2187
|
+
mesh.normal = { numComponents: 3, data: meshNormals(new Float32Array(p.length), p, mesh.indices.data, { groups }) };
|
|
2188
|
+
}
|
|
2189
|
+
mesh.bounds = bounds;
|
|
2190
|
+
done.set(source, mesh);
|
|
2191
|
+
part.mesh = mesh;
|
|
2192
|
+
}
|
|
2193
|
+
return model;
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
// Fetch and parse into the model shape, before the normals and bounds.
|
|
2197
|
+
async function _load(url, o, what) {
|
|
2198
|
+
const get = async (at, init) => {
|
|
2199
|
+
const res = await fetch(at, init);
|
|
2200
|
+
if (!res.ok) throw new Error('[host] ' + what + ': ' + at + ' → ' + res.status);
|
|
2201
|
+
return res;
|
|
2202
|
+
};
|
|
2203
|
+
if (typeof o.parse === 'function') {
|
|
2204
|
+
const mesh = await o.parse(await (await get(url, o.fetch)).arrayBuffer());
|
|
2205
|
+
return { parts: [_part(mesh)], nodes: _ONE_NODE(), skins: [], clips: [] };
|
|
2206
|
+
}
|
|
2207
|
+
const format = (o.format || (/\.(\w+)(?:[?#].*)?$/.exec(url) || [])[1] || '').toLowerCase();
|
|
2208
|
+
if (format !== 'obj' && format !== 'glb' && format !== 'gltf') {
|
|
2209
|
+
throw new Error('[host] ' + what + ': ' + url + ' → unknown format; opts.format is one of obj, glb, gltf, or pass opts.parse.');
|
|
2210
|
+
}
|
|
2211
|
+
const res = await get(url, o.fetch);
|
|
2212
|
+
if (format === 'obj') return { parts: [_part(parseObj(await res.text()))], nodes: _ONE_NODE(), skins: [], clips: [] };
|
|
2213
|
+
if (format === 'glb') {
|
|
2214
|
+
const { json, bin } = parseGlb(await res.arrayBuffer());
|
|
2215
|
+
return parseGltf(json, [bin]);
|
|
2216
|
+
}
|
|
2217
|
+
const json = await res.json();
|
|
2218
|
+
const base = typeof location !== 'undefined' ? new URL(url, location.href) : url;
|
|
2219
|
+
const resolve = (uri) => {
|
|
2220
|
+
if (/^data:/.test(uri)) return uri;
|
|
2221
|
+
try { return String(new URL(uri, base)); } catch (e) { return url.replace(/[^/]*$/, '') + uri; }
|
|
2222
|
+
};
|
|
2223
|
+
const buffers = await Promise.all((json.buffers || []).map(async b => (await get(resolve(b.uri), o.fetch)).arrayBuffer()));
|
|
2224
|
+
return parseGltf(json, buffers);
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2227
|
+
/**
|
|
2228
|
+
* Fetch a mesh: one object in the arrays shape — position, indices, and
|
|
2229
|
+
* normal, tangent, texcoord, color, joints, weights when the file carries them
|
|
2230
|
+
* — with `bounds`, its extent `{ min, max, center, diag }`, beside them. A
|
|
2231
|
+
* bridge's buffer upload takes it as it is; `size / mesh.bounds.diag` is the
|
|
2232
|
+
* scale that fits any file to `size`.
|
|
2233
|
+
*
|
|
2234
|
+
* Formats: OBJ (host/obj), and glTF 2.0 as `.glb` or `.gltf` (host/gltf) when
|
|
2235
|
+
* the file holds one part. A file of several parts rejects, naming loadModel:
|
|
2236
|
+
* a mesh is never silently the first of many. `opts.parse` reads any other
|
|
2237
|
+
* format: it receives the fetched ArrayBuffer and returns the arrays shape.
|
|
2238
|
+
*
|
|
2239
|
+
* Normals. Left out, the option keeps the file's normals and gives smooth
|
|
2240
|
+
* ones to a mesh that has none. 'smooth' always recomputes them, summed per
|
|
2241
|
+
* position, so a file that arrives faceted or cut along a texture seam
|
|
2242
|
+
* smooths too. 'flat' always recomputes them on the flattened mesh — every
|
|
2243
|
+
* triangle owning its vertices, so the vertex count grows and the indices
|
|
2244
|
+
* count up. false computes nothing. The procedure, its limits (no crease
|
|
2245
|
+
* angle; grouping by grid cell, at 1e-6 of the bounds' diagonal) and its
|
|
2246
|
+
* sources are tree/mesh's. glTF viewers compute flat normals for a file
|
|
2247
|
+
* without any; here that is `normals: 'flat'`.
|
|
2248
|
+
*
|
|
2249
|
+
* @param {string} url
|
|
2250
|
+
* @param {{ fetch?:object, format?:string, parse?:function(ArrayBuffer):object,
|
|
2251
|
+
* normals?:'smooth'|'flat'|false }} [opts] fetch: the fetch init. format: 'obj' |
|
|
2252
|
+
* 'glb' | 'gltf' when the URL's extension does not say. parse: a parser for another
|
|
2253
|
+
* format. normals: as above.
|
|
2254
|
+
* @returns {Promise<object>} The mesh.
|
|
2255
|
+
*/
|
|
2256
|
+
async function loadMesh(url, opts) {
|
|
2257
|
+
const o = opts || {};
|
|
2258
|
+
const model = await _load(url, o, 'mesh');
|
|
2259
|
+
if (model.parts.length !== 1) {
|
|
2260
|
+
throw new Error('[host] mesh: ' + url + ' → ' + model.parts.length + ' parts; loadModel keeps them apart.');
|
|
2261
|
+
}
|
|
2262
|
+
return _finish(model, o).parts[0].mesh;
|
|
2263
|
+
}
|
|
2264
|
+
|
|
1958
2265
|
/**
|
|
1959
|
-
* Fetch
|
|
1960
|
-
*
|
|
1961
|
-
*
|
|
2266
|
+
* Fetch a model with its structure — what a rig, morph targets or animation
|
|
2267
|
+
* need; a single mesh is loadMesh's:
|
|
2268
|
+
*
|
|
2269
|
+
* { parts: [{ name, node, skin, mesh, targets, color }], nodes, skins, clips }
|
|
1962
2270
|
*
|
|
1963
|
-
*
|
|
1964
|
-
*
|
|
2271
|
+
* A part is one triangle primitive under one node: `mesh` as loadMesh returns
|
|
2272
|
+
* it (the arrays shape with `bounds`, in the part's own space), `targets` its
|
|
2273
|
+
* morph targets as delta arrays, `color` its base colour [r, g, b, a], `node`
|
|
2274
|
+
* the node it hangs from and `skin` its skin's index or −1. `nodes` is the
|
|
2275
|
+
* hierarchy `{ names, parents, rest }`, parents first, `rest` a pose of ten
|
|
2276
|
+
* numbers per node; `skins` are `{ name, joints, inverseBind }` and `clips`
|
|
2277
|
+
* `{ name, duration, channels }` — what tree's clipSample, poseWorld and
|
|
2278
|
+
* jointPalette take. A rigid part draws under its node's world matrix, a
|
|
2279
|
+
* skinned one under its skin's joint palette. An OBJ file is one white part
|
|
2280
|
+
* under one identity node.
|
|
2281
|
+
*
|
|
2282
|
+
* The options are loadMesh's; `normals` applies to every part, 'flat'
|
|
2283
|
+
* flattening each part's morph targets with its mesh.
|
|
1965
2284
|
*
|
|
1966
2285
|
* @param {string} url
|
|
1967
|
-
* @param {{ fetch?:object
|
|
1968
|
-
*
|
|
1969
|
-
*
|
|
1970
|
-
*
|
|
1971
|
-
* indices:{numComponents:number,data:Uint32Array} }>}
|
|
2286
|
+
* @param {{ fetch?:object, format?:string, parse?:function(ArrayBuffer):object,
|
|
2287
|
+
* normals?:'smooth'|'flat'|false }} [opts]
|
|
2288
|
+
* @returns {Promise<{ parts:object[], nodes:{ names:string[], parents:Int32Array, rest:Float32Array },
|
|
2289
|
+
* skins:object[], clips:object[] }>}
|
|
1972
2290
|
*/
|
|
1973
2291
|
async function loadModel(url, opts) {
|
|
1974
2292
|
const o = opts || {};
|
|
1975
|
-
|
|
1976
|
-
if (!res.ok) throw new Error('[host] model: ' + url + ' → ' + res.status);
|
|
1977
|
-
return parseObj(await res.text());
|
|
2293
|
+
return _finish(await _load(url, o, 'model'), o);
|
|
1978
2294
|
}
|
|
1979
2295
|
|
|
1980
2296
|
/**
|
|
@@ -2478,7 +2794,7 @@ function createOrbit(host, cam, opts) {
|
|
|
2478
2794
|
* host.cameraHelm(cam, opts) · host.poseHelm(opts)
|
|
2479
2795
|
* host.poseTrack(opts) · host.cameraTrack(cam, opts)
|
|
2480
2796
|
* host.hid(opts) · host.gamepad(opts)
|
|
2481
|
-
* host.image(url) · host.video(opts) · host.raster(draw, w, h) · host.model(url)
|
|
2797
|
+
* host.image(url) · host.video(opts) · host.raster(draw, w, h) · host.mesh(url) · host.model(url)
|
|
2482
2798
|
* host.labels // the DOM label layer, created on first access
|
|
2483
2799
|
* host.orbit(cam, opts)
|
|
2484
2800
|
* host.tick(dt) // external-loop mode
|
|
@@ -2573,7 +2889,9 @@ function createHost(canvas, opts) {
|
|
|
2573
2889
|
video(opts) { return host.register(createVideo(opts)); },
|
|
2574
2890
|
/** Draw through a 2D context into a texture source — see host/media. */
|
|
2575
2891
|
raster(draw, w, h, opts) { return raster(draw, w, h, opts); },
|
|
2576
|
-
/** Fetch
|
|
2892
|
+
/** Fetch one mesh — the arrays shape with its bounds — from an OBJ or glTF file, or any format through opts.parse — see host/media. */
|
|
2893
|
+
mesh(url, opts) { return loadMesh(url, opts); },
|
|
2894
|
+
/** Fetch a model with its structure: parts, nodes, skins, clips — see host/media. */
|
|
2577
2895
|
model(url, opts) { return loadModel(url, opts); },
|
|
2578
2896
|
/**
|
|
2579
2897
|
* The label layer — see host/labels. Created on first access, so a
|
|
@@ -2630,5 +2948,5 @@ function createHost(canvas, opts) {
|
|
|
2630
2948
|
return host;
|
|
2631
2949
|
}
|
|
2632
2950
|
|
|
2633
|
-
export { GAMEPAD_MAP, HID_FILTERS, Handle, PointerRouter, TrackHandles, VIEW, cameraHelm, cameraTrack, createGamepad, createHid, createHost, createLabels, createLoop, createOrbit, createPlayers, createPointer, createVideo, createView, decodeSpaceNavigator, helmBasis, isConstraint, loadImage, loadModel, measureCanvas, observeCanvas, poseHelm, poseTrack, raster, validConstraint };
|
|
2951
|
+
export { GAMEPAD_MAP, HID_FILTERS, Handle, PointerRouter, TrackHandles, VIEW, cameraHelm, cameraTrack, createGamepad, createHid, createHost, createLabels, createLoop, createOrbit, createPlayers, createPointer, createVideo, createView, decodeSpaceNavigator, helmBasis, isConstraint, loadImage, loadMesh, loadModel, measureCanvas, observeCanvas, poseHelm, poseTrack, raster, validConstraint };
|
|
2634
2952
|
//# sourceMappingURL=index.js.map
|