@driftengine/assets 3.61.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 +9 -0
- package/README.md +9 -0
- package/dist/assetPath.d.ts +51 -0
- package/dist/assetPath.js +59 -0
- package/dist/coarseLevel.d.ts +94 -0
- package/dist/coarseLevel.js +606 -0
- package/dist/dds.d.ts +41 -0
- package/dist/dds.js +425 -0
- package/dist/drawKey.d.ts +64 -0
- package/dist/drawKey.js +42 -0
- package/dist/drftLoader.d.ts +420 -0
- package/dist/drftLoader.js +996 -0
- package/dist/drftTextures.d.ts +87 -0
- package/dist/drftTextures.js +138 -0
- package/dist/fbx.d.ts +86 -0
- package/dist/fbx.js +792 -0
- package/dist/fbxHarness.d.ts +68 -0
- package/dist/fbxHarness.js +185 -0
- package/dist/fbxInflate.d.ts +60 -0
- package/dist/fbxInflate.js +204 -0
- package/dist/fbxMaterial.d.ts +87 -0
- package/dist/fbxMaterial.js +345 -0
- package/dist/fbxRig.d.ts +130 -0
- package/dist/fbxRig.js +601 -0
- package/dist/gltf.d.ts +242 -0
- package/dist/gltf.js +904 -0
- package/dist/gltfAccessor.d.ts +2 -0
- package/dist/gltfAccessor.js +106 -0
- package/dist/gltfSkin.d.ts +32 -0
- package/dist/gltfSkin.js +144 -0
- package/dist/iesPhotometry.d.ts +44 -0
- package/dist/iesPhotometry.js +94 -0
- package/dist/imageInfo.d.ts +31 -0
- package/dist/imageInfo.js +119 -0
- package/dist/index.d.ts +58 -0
- package/dist/index.js +132 -0
- package/dist/kn5.d.ts +158 -0
- package/dist/kn5.js +557 -0
- package/dist/loadProgress.d.ts +118 -0
- package/dist/loadProgress.js +1 -0
- package/dist/localise.d.ts +35 -0
- package/dist/localise.js +253 -0
- package/dist/obj.d.ts +54 -0
- package/dist/obj.js +335 -0
- package/dist/orient.d.ts +104 -0
- package/dist/orient.js +241 -0
- package/dist/radianceHdr.d.ts +36 -0
- package/dist/radianceHdr.js +172 -0
- package/dist/readModel.d.ts +153 -0
- package/dist/readModel.js +386 -0
- package/dist/recognise.d.ts +38 -0
- package/dist/recognise.js +97 -0
- package/dist/stl.d.ts +22 -0
- package/dist/stl.js +131 -0
- package/dist/tangentOrder.d.ts +24 -0
- package/dist/tangentOrder.js +31 -0
- package/dist/threemf.d.ts +46 -0
- package/dist/threemf.js +254 -0
- package/dist/uploadBudget.d.ts +82 -0
- package/dist/uploadBudget.js +86 -0
- package/dist/usd.d.ts +49 -0
- package/dist/usd.js +542 -0
- package/dist/weld.d.ts +44 -0
- package/dist/weld.js +237 -0
- package/dist/zip.d.ts +35 -0
- package/dist/zip.js +110 -0
- package/package.json +61 -0
- package/src/assetPath.ts +79 -0
- package/src/coarseLevel.ts +720 -0
- package/src/dds.ts +507 -0
- package/src/drawKey.ts +91 -0
- package/src/drftLoader.ts +1197 -0
- package/src/drftTextures.ts +153 -0
- package/src/fbx.ts +934 -0
- package/src/fbxHarness.ts +215 -0
- package/src/fbxInflate.ts +258 -0
- package/src/fbxMaterial.ts +410 -0
- package/src/fbxRig.ts +652 -0
- package/src/gltf.ts +1161 -0
- package/src/gltfAccessor.ts +127 -0
- package/src/gltfSkin.ts +210 -0
- package/src/iesPhotometry.ts +136 -0
- package/src/imageInfo.ts +130 -0
- package/src/index.ts +162 -0
- package/src/kn5.ts +705 -0
- package/src/loadProgress.ts +131 -0
- package/src/localise.ts +275 -0
- package/src/obj.ts +386 -0
- package/src/orient.ts +262 -0
- package/src/radianceHdr.ts +195 -0
- package/src/readModel.ts +515 -0
- package/src/recognise.ts +124 -0
- package/src/stl.ts +147 -0
- package/src/tangentOrder.ts +32 -0
- package/src/threemf.ts +303 -0
- package/src/uploadBudget.ts +90 -0
- package/src/usd.ts +642 -0
- package/src/weld.ts +264 -0
- package/src/zip.ts +130 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { DrftError } from '@driftengine/drft';
|
|
2
|
+
/**
|
|
3
|
+
* Reading one glTF accessor into floats, and the two tables that describe one.
|
|
4
|
+
*
|
|
5
|
+
* **Its own module so `gltfSkin.ts` and `gltf.ts` can both use it without importing each other.**
|
|
6
|
+
* The skin reader needs the graph the mesh reader deliberately discards, and the mesh reader needs
|
|
7
|
+
* the skin reader's remap — which would be a runtime import cycle if the accessor reader stayed in
|
|
8
|
+
* either of them. The `GltfDocument` type still comes from `gltf.ts` and costs nothing: a type
|
|
9
|
+
* import is erased, so no cycle survives to run.
|
|
10
|
+
*
|
|
11
|
+
* Every component type widens to `Float32Array`, which is what `MeshData` carries throughout —
|
|
12
|
+
* including `JOINTS_0`, whose unsigned bytes are exact in float32 well past the 512-joint cap.
|
|
13
|
+
*/
|
|
14
|
+
const COMPONENT_BYTES = {
|
|
15
|
+
5120: 1, // BYTE
|
|
16
|
+
5121: 1, // UNSIGNED_BYTE
|
|
17
|
+
5122: 2, // SHORT
|
|
18
|
+
5123: 2, // UNSIGNED_SHORT
|
|
19
|
+
5125: 4, // UNSIGNED_INT
|
|
20
|
+
5126: 4, // FLOAT
|
|
21
|
+
};
|
|
22
|
+
const TYPE_COMPONENTS = {
|
|
23
|
+
SCALAR: 1,
|
|
24
|
+
VEC2: 2,
|
|
25
|
+
VEC3: 3,
|
|
26
|
+
VEC4: 4,
|
|
27
|
+
MAT4: 16,
|
|
28
|
+
};
|
|
29
|
+
export function accessorFloats(doc, buffers, index, what) {
|
|
30
|
+
const accessor = doc.accessors?.[index];
|
|
31
|
+
if (accessor === undefined)
|
|
32
|
+
throw new DrftError(`gltf: ${what} names accessor ${index}, which is absent`);
|
|
33
|
+
if (accessor.sparse !== undefined) {
|
|
34
|
+
throw new DrftError(`gltf: ${what} uses a sparse accessor, which this reader does not implement. ` +
|
|
35
|
+
`Re-export without sparse accessors, or convert with a tool that expands them.`);
|
|
36
|
+
}
|
|
37
|
+
const components = TYPE_COMPONENTS[accessor.type];
|
|
38
|
+
const componentBytes = COMPONENT_BYTES[accessor.componentType];
|
|
39
|
+
if (components === undefined || componentBytes === undefined) {
|
|
40
|
+
throw new DrftError(`gltf: ${what} has an unsupported type ${accessor.type}/${accessor.componentType}`);
|
|
41
|
+
}
|
|
42
|
+
const out = new Float32Array(accessor.count * components);
|
|
43
|
+
if (accessor.bufferView === undefined)
|
|
44
|
+
return out; // Defined as zeroes.
|
|
45
|
+
const bufferView = doc.bufferViews?.[accessor.bufferView];
|
|
46
|
+
if (bufferView === undefined)
|
|
47
|
+
throw new DrftError(`gltf: ${what} names a bufferView that is absent`);
|
|
48
|
+
const source = buffers[bufferView.buffer];
|
|
49
|
+
if (source === undefined)
|
|
50
|
+
throw new DrftError(`gltf: ${what} names buffer ${bufferView.buffer}, absent`);
|
|
51
|
+
const stride = bufferView.byteStride ?? components * componentBytes;
|
|
52
|
+
const base = (bufferView.byteOffset ?? 0) + (accessor.byteOffset ?? 0);
|
|
53
|
+
const view = new DataView(source.buffer, source.byteOffset, source.byteLength);
|
|
54
|
+
/* Normalised integers carry their range in the type, which is why this is not a cast. */
|
|
55
|
+
const scale = accessor.normalized === true ? normalisedScale(accessor.componentType) : 1;
|
|
56
|
+
for (let i = 0; i < accessor.count; i++) {
|
|
57
|
+
const at = base + i * stride;
|
|
58
|
+
for (let c = 0; c < components; c++) {
|
|
59
|
+
const offset = at + c * componentBytes;
|
|
60
|
+
if (offset + componentBytes > source.byteLength) {
|
|
61
|
+
throw new DrftError(`gltf: ${what} reads past the end of its buffer at ${offset}`);
|
|
62
|
+
}
|
|
63
|
+
out[i * components + c] = readComponent(view, offset, accessor.componentType) * scale;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
function readComponent(view, at, componentType) {
|
|
69
|
+
switch (componentType) {
|
|
70
|
+
case 5120:
|
|
71
|
+
return view.getInt8(at);
|
|
72
|
+
case 5121:
|
|
73
|
+
return view.getUint8(at);
|
|
74
|
+
case 5122:
|
|
75
|
+
return view.getInt16(at, true);
|
|
76
|
+
case 5123:
|
|
77
|
+
return view.getUint16(at, true);
|
|
78
|
+
case 5125:
|
|
79
|
+
return view.getUint32(at, true);
|
|
80
|
+
default:
|
|
81
|
+
return view.getFloat32(at, true);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function normalisedScale(componentType) {
|
|
85
|
+
switch (componentType) {
|
|
86
|
+
case 5120:
|
|
87
|
+
return 1 / 127;
|
|
88
|
+
case 5121:
|
|
89
|
+
return 1 / 255;
|
|
90
|
+
case 5122:
|
|
91
|
+
return 1 / 32767;
|
|
92
|
+
case 5123:
|
|
93
|
+
return 1 / 65535;
|
|
94
|
+
default:
|
|
95
|
+
return 1;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* The matrix a skinned primitive is built with: none.
|
|
100
|
+
*
|
|
101
|
+
* A palette entry already carries its joint's inverse bind, so it takes a vertex from model space
|
|
102
|
+
* to where the joint moved it — and `uModel` then places the character. Baking the node's world
|
|
103
|
+
* matrix in as well applies that placement twice and throws the rig across the scene. See
|
|
104
|
+
* `gltfSkin.ts`.
|
|
105
|
+
*/
|
|
106
|
+
const IDENTITY = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { AnimationClip, Joint } from '@driftengine/drft';
|
|
2
|
+
import type { GltfDocument } from './gltf.ts';
|
|
3
|
+
/**
|
|
4
|
+
* Skins and animations out of a glTF document, sorted into the order a palette can be resolved in.
|
|
5
|
+
*
|
|
6
|
+
* **Its own module rather than more of `gltf.ts`**, because the two answer different questions: that
|
|
7
|
+
* file turns primitives into geometry in world space and says in its header that the node graph
|
|
8
|
+
* does not survive it. A skin is the one thing that needs the graph, and keeping the reader for it
|
|
9
|
+
* separate is what lets the flattening stay exactly as it was for every document without one.
|
|
10
|
+
*
|
|
11
|
+
* **Nothing here depends on `@driftengine/animation`.** The shapes it produces live in the format
|
|
12
|
+
* package, beside the chunks that carry them, so the package that writes a rig and the package
|
|
13
|
+
* that plays one meet over data rather than over each other.
|
|
14
|
+
*/
|
|
15
|
+
export interface GltfSkin {
|
|
16
|
+
readonly joints: readonly Joint[];
|
|
17
|
+
/** Sixteen floats a joint, column-major, in the same order as `joints`. */
|
|
18
|
+
readonly inverseBind: Float32Array;
|
|
19
|
+
/**
|
|
20
|
+
* glTF joint index to sorted joint index.
|
|
21
|
+
*
|
|
22
|
+
* The importer sorts parents-first because a palette is resolved in index order, and it is also
|
|
23
|
+
* the only layer that can: every index naming a joint has to move with it — a mesh's `JOINTS_0`
|
|
24
|
+
* and every animation channel's target. A caller re-indexing a mesh needs this.
|
|
25
|
+
*/
|
|
26
|
+
readonly remap: Uint16Array;
|
|
27
|
+
}
|
|
28
|
+
export interface GltfAnimated {
|
|
29
|
+
readonly skins: readonly GltfSkin[];
|
|
30
|
+
readonly clips: readonly AnimationClip[];
|
|
31
|
+
}
|
|
32
|
+
export declare function readGltfSkins(doc: GltfDocument, buffers: readonly Uint8Array[]): GltfAnimated;
|
package/dist/gltfSkin.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { DrftError } from '@driftengine/drft';
|
|
2
|
+
import { accessorFloats } from './gltfAccessor.js';
|
|
3
|
+
export function readGltfSkins(doc, buffers) {
|
|
4
|
+
const definitions = doc.skins ?? [];
|
|
5
|
+
if (definitions.length === 0)
|
|
6
|
+
return { skins: [], clips: [] };
|
|
7
|
+
/* Which node is whose child, which glTF states only in the other direction. */
|
|
8
|
+
const parentOf = new Map();
|
|
9
|
+
(doc.nodes ?? []).forEach((node, at) => {
|
|
10
|
+
for (const child of node.children ?? [])
|
|
11
|
+
parentOf.set(child, at);
|
|
12
|
+
});
|
|
13
|
+
const skins = [];
|
|
14
|
+
/** Node index to sorted joint index, for every skin, so a channel can find its track. */
|
|
15
|
+
const jointOfNode = new Map();
|
|
16
|
+
for (const definition of definitions) {
|
|
17
|
+
const order = sortParentsFirst(definition.joints, parentOf, doc);
|
|
18
|
+
const joints = order.map((node) => {
|
|
19
|
+
const parent = parentOf.get(node);
|
|
20
|
+
/*
|
|
21
|
+
* A parent outside this skin makes the joint a root *of this skin*. glTF allows it — a rig
|
|
22
|
+
* hung under a scene node — and reading it as missing would refuse valid files.
|
|
23
|
+
*/
|
|
24
|
+
const within = parent === undefined ? -1 : order.indexOf(parent);
|
|
25
|
+
return { parent: within, name: doc.nodes?.[node]?.name ?? `joint ${node}` };
|
|
26
|
+
});
|
|
27
|
+
const remap = new Uint16Array(definition.joints.length);
|
|
28
|
+
definition.joints.forEach((node, old) => {
|
|
29
|
+
remap[old] = order.indexOf(node);
|
|
30
|
+
});
|
|
31
|
+
skins.push({ joints, inverseBind: readInverseBind(doc, buffers, definition, order), remap });
|
|
32
|
+
order.forEach((node, at) => jointOfNode.set(node, at));
|
|
33
|
+
}
|
|
34
|
+
return { skins, clips: readClips(doc, buffers, jointOfNode) };
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The skin's joints, parents before children.
|
|
38
|
+
*
|
|
39
|
+
* A depth-first walk from each root rather than a sort with a comparator, because "before its
|
|
40
|
+
* parent" is not a total order over nodes and a comparator would have to invent one. Visiting a
|
|
41
|
+
* node only after its parent is what the walk gives for free.
|
|
42
|
+
*/
|
|
43
|
+
function sortParentsFirst(joints, parentOf, doc) {
|
|
44
|
+
const within = new Set(joints);
|
|
45
|
+
for (const node of joints) {
|
|
46
|
+
if (doc.nodes?.[node] === undefined) {
|
|
47
|
+
throw new DrftError(`gltf: a skin names node ${node}, which is absent`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const out = [];
|
|
51
|
+
const placed = new Set();
|
|
52
|
+
const place = (node, depth) => {
|
|
53
|
+
if (placed.has(node))
|
|
54
|
+
return;
|
|
55
|
+
/*
|
|
56
|
+
* A cycle would recurse forever. glTF forbids one, and `gltf.ts` refuses a graph with one on
|
|
57
|
+
* the mesh path — but this reader walks the graph the other way round, from children up, so it
|
|
58
|
+
* checks for itself rather than relying on a check that may not have run.
|
|
59
|
+
*/
|
|
60
|
+
if (depth > joints.length) {
|
|
61
|
+
throw new DrftError(`gltf: the joint hierarchy around node ${node} has a cycle`);
|
|
62
|
+
}
|
|
63
|
+
const parent = parentOf.get(node);
|
|
64
|
+
if (parent !== undefined && within.has(parent))
|
|
65
|
+
place(parent, depth + 1);
|
|
66
|
+
if (placed.has(node))
|
|
67
|
+
return;
|
|
68
|
+
placed.add(node);
|
|
69
|
+
out.push(node);
|
|
70
|
+
};
|
|
71
|
+
for (const node of joints)
|
|
72
|
+
place(node, 0);
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The inverse bind matrices, reordered to match the sorted joints.
|
|
77
|
+
*
|
|
78
|
+
* Absent means identity, which glTF defines: a rig whose bind pose is the node graph's own rest
|
|
79
|
+
* transforms needs no matrices, and reading absence as zeroes would collapse every vertex.
|
|
80
|
+
*/
|
|
81
|
+
function readInverseBind(doc, buffers, definition, order) {
|
|
82
|
+
const out = new Float32Array(order.length * 16);
|
|
83
|
+
if (definition.inverseBindMatrices === undefined) {
|
|
84
|
+
for (let j = 0; j < order.length; j++) {
|
|
85
|
+
out[j * 16] = 1;
|
|
86
|
+
out[j * 16 + 5] = 1;
|
|
87
|
+
out[j * 16 + 10] = 1;
|
|
88
|
+
out[j * 16 + 15] = 1;
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
const source = accessorFloats(doc, buffers, definition.inverseBindMatrices, 'skin inverseBindMatrices');
|
|
93
|
+
if (source.length < definition.joints.length * 16) {
|
|
94
|
+
throw new DrftError(`gltf: a skin has ${definition.joints.length} joints but ${source.length / 16} inverse ` +
|
|
95
|
+
`bind matrices. Every joint needs one, or the ones past the end read as zero and ` +
|
|
96
|
+
`collapse the vertices they weight.`);
|
|
97
|
+
}
|
|
98
|
+
order.forEach((node, at) => {
|
|
99
|
+
const old = definition.joints.indexOf(node);
|
|
100
|
+
out.set(source.subarray(old * 16, old * 16 + 16), at * 16);
|
|
101
|
+
});
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
/** glTF's path names are this engine's, but only three of them drive a joint. */
|
|
105
|
+
function trackPath(path) {
|
|
106
|
+
if (path === 'translation' || path === 'rotation' || path === 'scale')
|
|
107
|
+
return path;
|
|
108
|
+
/*
|
|
109
|
+
* `weights` drives morph targets rather than a joint, and is dropped here rather than refused:
|
|
110
|
+
* a file carrying both should still animate its skeleton. Track A's morph phase is what reads it.
|
|
111
|
+
*/
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
function readClips(doc, buffers, jointOfNode) {
|
|
115
|
+
const clips = [];
|
|
116
|
+
(doc.animations ?? []).forEach((animation, index) => {
|
|
117
|
+
const tracks = [];
|
|
118
|
+
let duration = 0;
|
|
119
|
+
for (const channel of animation.channels) {
|
|
120
|
+
const path = trackPath(channel.target.path);
|
|
121
|
+
const node = channel.target.node;
|
|
122
|
+
/*
|
|
123
|
+
* A channel targeting a node no skin contains is skipped rather than refused. A scene's
|
|
124
|
+
* camera or a prop is animated in the same block as its characters, and a file that
|
|
125
|
+
* animates one is not a broken file.
|
|
126
|
+
*/
|
|
127
|
+
if (path === null || node === undefined)
|
|
128
|
+
continue;
|
|
129
|
+
const joint = jointOfNode.get(node);
|
|
130
|
+
if (joint === undefined)
|
|
131
|
+
continue;
|
|
132
|
+
const sampler = animation.samplers[channel.sampler];
|
|
133
|
+
if (sampler === undefined) {
|
|
134
|
+
throw new DrftError(`gltf: animation ${index} has a channel naming sampler ${channel.sampler}, which is absent`);
|
|
135
|
+
}
|
|
136
|
+
const times = accessorFloats(doc, buffers, sampler.input, `animation ${index} input`);
|
|
137
|
+
const values = accessorFloats(doc, buffers, sampler.output, `animation ${index} output`);
|
|
138
|
+
duration = Math.max(duration, times[times.length - 1] ?? 0);
|
|
139
|
+
tracks.push({ joint, path, times, values });
|
|
140
|
+
}
|
|
141
|
+
clips.push({ name: animation.name ?? `animation ${index}`, durationSec: duration, tracks });
|
|
142
|
+
});
|
|
143
|
+
return clips;
|
|
144
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** The IESNA LM-63 photometric file: what a real fixture actually throws, and in which direction. */
|
|
2
|
+
/**
|
|
3
|
+
* A fixture's measured intensity, as a grid over vertical and horizontal angles.
|
|
4
|
+
*
|
|
5
|
+
* **Candela rather than a normalised curve**, because the two answer different questions and only
|
|
6
|
+
* one of them is in the file: the shape of the distribution is what a renderer samples, and the
|
|
7
|
+
* absolute output is what tells a consumer whether this fixture is a reading lamp or a floodlight.
|
|
8
|
+
* Normalising here would throw the second away, and a consumer that wants the shape can divide by
|
|
9
|
+
* `maxCandela` — which is why it is carried rather than left to be recomputed.
|
|
10
|
+
*/
|
|
11
|
+
export interface IesProfile {
|
|
12
|
+
/**
|
|
13
|
+
* Vertical angles in degrees, increasing. 0 is straight down for the common type-C fixture.
|
|
14
|
+
*
|
|
15
|
+
* Not resampled to a regular grid here. A real file is dense where the distribution changes and
|
|
16
|
+
* sparse where it does not, and flattening that is a decision about a texture rather than about
|
|
17
|
+
* a file — `iesProfile.ts` in the renderer makes it.
|
|
18
|
+
*/
|
|
19
|
+
readonly verticalAngles: Float32Array;
|
|
20
|
+
/** Horizontal angles in degrees, increasing. A single 0 means the fixture is axially symmetric. */
|
|
21
|
+
readonly horizontalAngles: Float32Array;
|
|
22
|
+
/** Candela, horizontal-major: all vertical angles of the first plane, then the second. */
|
|
23
|
+
readonly candela: Float32Array;
|
|
24
|
+
/** The brightest value in the grid, so a consumer can normalise without walking it again. */
|
|
25
|
+
readonly maxCandela: number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Read an LM-63 photometric file.
|
|
29
|
+
*
|
|
30
|
+
* **Whitespace-delimited across line breaks, because that is what the format is.** LM-63 attaches
|
|
31
|
+
* no meaning to a newline inside its numeric lists, and every file a manufacturer publishes wraps
|
|
32
|
+
* them — so a reader that parses line by line passes a hand-built fixture and fails on everything
|
|
33
|
+
* real. The header is the only part that is line-oriented, and it ends at `TILT=`.
|
|
34
|
+
*
|
|
35
|
+
* **Pure, and it takes text rather than a URL**, for the reason `readRadianceHdr` gives: the
|
|
36
|
+
* fetching decision belongs to the consumer who has to make it, and a pure function is one that can
|
|
37
|
+
* be tested exactly.
|
|
38
|
+
*
|
|
39
|
+
* **What it costs** is holding the whole grid, which for a dense fixture is a few thousand floats.
|
|
40
|
+
* **What would make it wrong** is a `TILT` other than `NONE`: that describes a fixture mounted at
|
|
41
|
+
* an angle with its own correction table, and applying the distribution without it would be
|
|
42
|
+
* confidently wrong rather than obviously wrong — so it is refused by name.
|
|
43
|
+
*/
|
|
44
|
+
export declare function readIesProfile(text: string): IesProfile;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/** The IESNA LM-63 photometric file: what a real fixture actually throws, and in which direction. */
|
|
2
|
+
/**
|
|
3
|
+
* Read an LM-63 photometric file.
|
|
4
|
+
*
|
|
5
|
+
* **Whitespace-delimited across line breaks, because that is what the format is.** LM-63 attaches
|
|
6
|
+
* no meaning to a newline inside its numeric lists, and every file a manufacturer publishes wraps
|
|
7
|
+
* them — so a reader that parses line by line passes a hand-built fixture and fails on everything
|
|
8
|
+
* real. The header is the only part that is line-oriented, and it ends at `TILT=`.
|
|
9
|
+
*
|
|
10
|
+
* **Pure, and it takes text rather than a URL**, for the reason `readRadianceHdr` gives: the
|
|
11
|
+
* fetching decision belongs to the consumer who has to make it, and a pure function is one that can
|
|
12
|
+
* be tested exactly.
|
|
13
|
+
*
|
|
14
|
+
* **What it costs** is holding the whole grid, which for a dense fixture is a few thousand floats.
|
|
15
|
+
* **What would make it wrong** is a `TILT` other than `NONE`: that describes a fixture mounted at
|
|
16
|
+
* an angle with its own correction table, and applying the distribution without it would be
|
|
17
|
+
* confidently wrong rather than obviously wrong — so it is refused by name.
|
|
18
|
+
*/
|
|
19
|
+
export function readIesProfile(text) {
|
|
20
|
+
if (!/IESNA/i.test(text.slice(0, 200))) {
|
|
21
|
+
throw new Error(`readIesProfile: not an IESNA photometric file — no IESNA signature in the first line, ` +
|
|
22
|
+
`found "${text.slice(0, 40).replace(/\n[\s\S]*/, '')}"`);
|
|
23
|
+
}
|
|
24
|
+
const tilt = /^TILT=(.*)$/m.exec(text);
|
|
25
|
+
if (tilt === null)
|
|
26
|
+
throw new Error('readIesProfile: no TILT= line, so the header never ended');
|
|
27
|
+
const tiltValue = (tilt[1] ?? '').trim().toUpperCase();
|
|
28
|
+
if (tiltValue !== 'NONE') {
|
|
29
|
+
throw new Error(`readIesProfile: TILT=${tiltValue} is not handled. That fixture is mounted at an angle and ` +
|
|
30
|
+
'carries its own correction table; applying its distribution without one would be ' +
|
|
31
|
+
'confidently wrong rather than obviously wrong.');
|
|
32
|
+
}
|
|
33
|
+
/* Everything after the TILT line is numbers, and the line breaks in it mean nothing. */
|
|
34
|
+
const body = text.slice(tilt.index + tilt[0].length);
|
|
35
|
+
const numbers = body
|
|
36
|
+
.split(/[\s,]+/)
|
|
37
|
+
.filter((token) => token.length > 0)
|
|
38
|
+
.map(Number);
|
|
39
|
+
let at = 0;
|
|
40
|
+
const next = (what) => {
|
|
41
|
+
const value = numbers[at];
|
|
42
|
+
at += 1;
|
|
43
|
+
if (value === undefined || !Number.isFinite(value)) {
|
|
44
|
+
throw new Error(`readIesProfile: expected ${what} at value ${at} and the file ended or held "${value}"`);
|
|
45
|
+
}
|
|
46
|
+
return value;
|
|
47
|
+
};
|
|
48
|
+
/* The ten-number line: lamps, lumens, multiplier, counts, type, units, and three dimensions. */
|
|
49
|
+
next('the lamp count');
|
|
50
|
+
next('lumens per lamp');
|
|
51
|
+
const multiplier = next('the candela multiplier');
|
|
52
|
+
const verticalCount = Math.round(next('the vertical angle count'));
|
|
53
|
+
const horizontalCount = Math.round(next('the horizontal angle count'));
|
|
54
|
+
next('the photometric type');
|
|
55
|
+
next('the units type');
|
|
56
|
+
next('the fixture width');
|
|
57
|
+
next('the fixture length');
|
|
58
|
+
next('the fixture height');
|
|
59
|
+
/* The seven-number line, of which only the ballast factor reaches the output. */
|
|
60
|
+
const ballast = next('the ballast factor');
|
|
61
|
+
next('the future-use field');
|
|
62
|
+
next('the input watts');
|
|
63
|
+
if (verticalCount <= 0 || horizontalCount <= 0) {
|
|
64
|
+
throw new Error(`readIesProfile: a ${verticalCount} by ${horizontalCount} grid has no distribution in it`);
|
|
65
|
+
}
|
|
66
|
+
const verticalAngles = new Float32Array(verticalCount);
|
|
67
|
+
for (let i = 0; i < verticalCount; i++)
|
|
68
|
+
verticalAngles[i] = next(`vertical angle ${i}`);
|
|
69
|
+
const horizontalAngles = new Float32Array(horizontalCount);
|
|
70
|
+
for (let i = 0; i < horizontalCount; i++)
|
|
71
|
+
horizontalAngles[i] = next(`horizontal angle ${i}`);
|
|
72
|
+
const wanted = verticalCount * horizontalCount;
|
|
73
|
+
const remaining = numbers.length - at;
|
|
74
|
+
if (remaining !== wanted) {
|
|
75
|
+
throw new Error(`readIesProfile: ${verticalCount} vertical by ${horizontalCount} horizontal needs ${wanted} ` +
|
|
76
|
+
`candela values and the file holds ${remaining}`);
|
|
77
|
+
}
|
|
78
|
+
/*
|
|
79
|
+
* **Both factors, and the file means their product.** The multiplier is on the ten-number line
|
|
80
|
+
* and the ballast factor on the seven-number line, and a fixture's real output is candela times
|
|
81
|
+
* both. Dropping either gives a profile uniformly too dim, which reads as the light having been
|
|
82
|
+
* authored wrong rather than as the file being misread.
|
|
83
|
+
*/
|
|
84
|
+
const scale = multiplier * ballast;
|
|
85
|
+
const candela = new Float32Array(wanted);
|
|
86
|
+
let maxCandela = 0;
|
|
87
|
+
for (let i = 0; i < wanted; i++) {
|
|
88
|
+
const value = next(`candela ${i}`) * scale;
|
|
89
|
+
candela[i] = value;
|
|
90
|
+
if (value > maxCandela)
|
|
91
|
+
maxCandela = value;
|
|
92
|
+
}
|
|
93
|
+
return { verticalAngles, horizontalAngles, candela, maxCandela };
|
|
94
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What an image is, from its own first bytes. One responsibility: identify, never decode.
|
|
3
|
+
*
|
|
4
|
+
* The baker embeds compressed images untouched, so it never decodes one — but the format
|
|
5
|
+
* stores a codec and a size beside each payload, and both have to come from somewhere. They
|
|
6
|
+
* come from here, by reading the few bytes every container puts at its front for exactly
|
|
7
|
+
* this purpose.
|
|
8
|
+
*
|
|
9
|
+
* **Why the size is stored at all, when the decoder will report it anyway.** A loader that
|
|
10
|
+
* has to decode before it can size anything cannot budget: it learns that a level carries
|
|
11
|
+
* 400 MB of texture only after it has allocated it. Reading it here costs a dozen bytes per
|
|
12
|
+
* texture and lets a consumer refuse, downscale or stream before committing.
|
|
13
|
+
*
|
|
14
|
+
* **The extension is not consulted.** A `.jpg` that is really a PNG is common enough to be
|
|
15
|
+
* unremarkable, and every one of these formats identifies itself unambiguously in its first
|
|
16
|
+
* bytes. Trusting the name over the content is how a file that would have loaded fine gets
|
|
17
|
+
* refused, and how one that cannot be decoded gets embedded anyway.
|
|
18
|
+
*/
|
|
19
|
+
export interface ImageInfo {
|
|
20
|
+
readonly codec: number;
|
|
21
|
+
readonly width: number;
|
|
22
|
+
readonly height: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Identify an image and read its dimensions, or refuse by name.
|
|
26
|
+
*
|
|
27
|
+
* Refusing is the designed outcome for anything else. A tier 2 import that quietly dropped
|
|
28
|
+
* an unrecognised texture would produce a model that is merely the wrong colour, which is
|
|
29
|
+
* discovered late and blamed on the shading.
|
|
30
|
+
*/
|
|
31
|
+
export declare function describeImage(bytes: Uint8Array): ImageInfo;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What an image is, from its own first bytes. One responsibility: identify, never decode.
|
|
3
|
+
*
|
|
4
|
+
* The baker embeds compressed images untouched, so it never decodes one — but the format
|
|
5
|
+
* stores a codec and a size beside each payload, and both have to come from somewhere. They
|
|
6
|
+
* come from here, by reading the few bytes every container puts at its front for exactly
|
|
7
|
+
* this purpose.
|
|
8
|
+
*
|
|
9
|
+
* **Why the size is stored at all, when the decoder will report it anyway.** A loader that
|
|
10
|
+
* has to decode before it can size anything cannot budget: it learns that a level carries
|
|
11
|
+
* 400 MB of texture only after it has allocated it. Reading it here costs a dozen bytes per
|
|
12
|
+
* texture and lets a consumer refuse, downscale or stream before committing.
|
|
13
|
+
*
|
|
14
|
+
* **The extension is not consulted.** A `.jpg` that is really a PNG is common enough to be
|
|
15
|
+
* unremarkable, and every one of these formats identifies itself unambiguously in its first
|
|
16
|
+
* bytes. Trusting the name over the content is how a file that would have loaded fine gets
|
|
17
|
+
* refused, and how one that cannot be decoded gets embedded anyway.
|
|
18
|
+
*/
|
|
19
|
+
import { CODEC_JPEG, CODEC_PNG, CODEC_WEBP, DrftError } from '@driftengine/drft';
|
|
20
|
+
const PNG_SIGNATURE = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
|
|
21
|
+
function startsWith(bytes, signature, at = 0) {
|
|
22
|
+
if (bytes.length < at + signature.length)
|
|
23
|
+
return false;
|
|
24
|
+
for (let i = 0; i < signature.length; i++) {
|
|
25
|
+
if (bytes[at + i] !== signature[i])
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
function ascii(bytes, at, length) {
|
|
31
|
+
let out = '';
|
|
32
|
+
for (let i = 0; i < length; i++)
|
|
33
|
+
out += String.fromCharCode(bytes[at + i] ?? 0);
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* JPEG's size lives in a start-of-frame marker, and finding it means walking the segments.
|
|
38
|
+
*
|
|
39
|
+
* There is no fixed offset: a file carries any number of leading segments (EXIF, colour
|
|
40
|
+
* profiles, comments), each length-prefixed, and the frame header sits after them. The
|
|
41
|
+
* fifteen SOF markers differ in *compression*, not in the four bytes wanted here, so all of
|
|
42
|
+
* them are accepted apart from the four that are not frames at all.
|
|
43
|
+
*/
|
|
44
|
+
function jpegSize(bytes) {
|
|
45
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
46
|
+
let at = 2;
|
|
47
|
+
while (at + 4 <= bytes.length) {
|
|
48
|
+
if (bytes[at] !== 0xff) {
|
|
49
|
+
at++;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const marker = bytes[at + 1];
|
|
53
|
+
/* Padding and the standalone markers carry no length to skip. */
|
|
54
|
+
if (marker === 0xff || marker === 0xd8 || (marker >= 0xd0 && marker <= 0xd9)) {
|
|
55
|
+
at += 2;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const length = view.getUint16(at + 2, false);
|
|
59
|
+
const isFrame = marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc;
|
|
60
|
+
if (isFrame) {
|
|
61
|
+
if (at + 9 > bytes.length)
|
|
62
|
+
break;
|
|
63
|
+
return { height: view.getUint16(at + 5, false), width: view.getUint16(at + 7, false) };
|
|
64
|
+
}
|
|
65
|
+
/* A start-of-scan is followed by entropy-coded data, so the frame is behind us. */
|
|
66
|
+
if (marker === 0xda)
|
|
67
|
+
break;
|
|
68
|
+
if (length < 2)
|
|
69
|
+
break;
|
|
70
|
+
at += 2 + length;
|
|
71
|
+
}
|
|
72
|
+
throw new DrftError('a JPEG carries no start-of-frame marker, so its size cannot be read');
|
|
73
|
+
}
|
|
74
|
+
/** WEBP is a RIFF container with three payload shapes, each storing its size differently. */
|
|
75
|
+
function webpSize(bytes) {
|
|
76
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
77
|
+
const kind = ascii(bytes, 12, 4);
|
|
78
|
+
if (kind === 'VP8X' && bytes.length >= 30) {
|
|
79
|
+
/* Canvas size, stored minus one across three little-endian bytes each. */
|
|
80
|
+
const width = (bytes[24] | (bytes[25] << 8) | (bytes[26] << 16)) + 1;
|
|
81
|
+
const height = (bytes[27] | (bytes[28] << 8) | (bytes[29] << 16)) + 1;
|
|
82
|
+
return { width, height };
|
|
83
|
+
}
|
|
84
|
+
if (kind === 'VP8 ' && bytes.length >= 30) {
|
|
85
|
+
return { width: view.getUint16(26, true) & 0x3fff, height: view.getUint16(28, true) & 0x3fff };
|
|
86
|
+
}
|
|
87
|
+
if (kind === 'VP8L' && bytes.length >= 25) {
|
|
88
|
+
const bits = view.getUint32(21, true);
|
|
89
|
+
return { width: (bits & 0x3fff) + 1, height: ((bits >> 14) & 0x3fff) + 1 };
|
|
90
|
+
}
|
|
91
|
+
throw new DrftError(`a WEBP with a "${kind}" payload is not one this reader can size`);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Identify an image and read its dimensions, or refuse by name.
|
|
95
|
+
*
|
|
96
|
+
* Refusing is the designed outcome for anything else. A tier 2 import that quietly dropped
|
|
97
|
+
* an unrecognised texture would produce a model that is merely the wrong colour, which is
|
|
98
|
+
* discovered late and blamed on the shading.
|
|
99
|
+
*/
|
|
100
|
+
export function describeImage(bytes) {
|
|
101
|
+
if (startsWith(bytes, PNG_SIGNATURE)) {
|
|
102
|
+
if (bytes.length < 24)
|
|
103
|
+
throw new DrftError('a PNG ends before its header');
|
|
104
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
105
|
+
return {
|
|
106
|
+
codec: CODEC_PNG,
|
|
107
|
+
width: view.getUint32(16, false),
|
|
108
|
+
height: view.getUint32(20, false),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
if (bytes.length > 3 && bytes[0] === 0xff && bytes[1] === 0xd8) {
|
|
112
|
+
return { codec: CODEC_JPEG, ...jpegSize(bytes) };
|
|
113
|
+
}
|
|
114
|
+
if (ascii(bytes, 0, 4) === 'RIFF' && ascii(bytes, 8, 4) === 'WEBP') {
|
|
115
|
+
return { codec: CODEC_WEBP, ...webpSize(bytes) };
|
|
116
|
+
}
|
|
117
|
+
const head = [...bytes.subarray(0, 4)].map((b) => b.toString(16).padStart(2, '0')).join(' ');
|
|
118
|
+
throw new DrftError(`an image starting ${head} is not PNG, JPEG or WEBP. Convert it to one of those and bake again.`);
|
|
119
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/*! DriftEngine | Copyright 2026 Drift Technologies | Apache-2.0 | https://github.com/drftrun/driftengine */
|
|
2
|
+
/**
|
|
3
|
+
* Reading a model, and getting it onto the GPU.
|
|
4
|
+
*
|
|
5
|
+
* Separate from `@driftengine/drft` because these need a renderer: the loader uploads, and
|
|
6
|
+
* the readers exist to produce something the loader can upload. A procedurally generated
|
|
7
|
+
* world imports none of this and pays for none of it.
|
|
8
|
+
*/
|
|
9
|
+
export { DrftLoader } from './drftLoader.ts';
|
|
10
|
+
export type { DrftFit, DrftLoaderOptions } from './drftLoader.ts';
|
|
11
|
+
export { drawKeyOf, resolveDrawGrouping } from './drawKey.ts';
|
|
12
|
+
export type { DrawGrouping, DrawSurfaceOverride } from './drawKey.ts';
|
|
13
|
+
export type { DrftLoadPhase, DrftLoadProgress, DrftPart } from './loadProgress.ts';
|
|
14
|
+
export { TextureSet, textureColorSpaces } from './drftTextures.ts';
|
|
15
|
+
export { assetCandidates, basenameOf } from './assetPath.ts';
|
|
16
|
+
export { describeImage } from './imageInfo.ts';
|
|
17
|
+
export type { ImageInfo } from './imageInfo.ts';
|
|
18
|
+
export type { AssetReference } from './assetPath.ts';
|
|
19
|
+
export { readGlb, gltfToMeshes } from './gltf.ts';
|
|
20
|
+
export type { GltfDocument } from './gltf.ts';
|
|
21
|
+
export { parseObj, parseMtl } from './obj.ts';
|
|
22
|
+
export { parseStl } from './stl.ts';
|
|
23
|
+
export { readRadianceHdr } from './radianceHdr.ts';
|
|
24
|
+
export { readIesProfile } from './iesPhotometry.ts';
|
|
25
|
+
export type { IesProfile } from './iesPhotometry.ts';
|
|
26
|
+
export type { RadianceImage } from './radianceHdr.ts';
|
|
27
|
+
export type { StlAppearance } from './stl.ts';
|
|
28
|
+
export { parseUsda, usdzToMeshes } from './usd.ts';
|
|
29
|
+
export type { UsdResult } from './usd.ts';
|
|
30
|
+
export { parseThreeMfModel, threeMfToMeshes } from './threemf.ts';
|
|
31
|
+
export type { ThreeMfResult } from './threemf.ts';
|
|
32
|
+
export { readZip, findEntry } from './zip.ts';
|
|
33
|
+
export type { ZipEntry } from './zip.ts';
|
|
34
|
+
export { recognise, describeRecognised } from './recognise.ts';
|
|
35
|
+
export type { Recognised } from './recognise.ts';
|
|
36
|
+
export { fbxToMeshes } from './fbx.ts';
|
|
37
|
+
export type { Inflate } from './fbx.ts';
|
|
38
|
+
export { readModel, readerFor, extensionOf, MODEL_FORMATS } from './readModel.ts';
|
|
39
|
+
export type { ModelSource, ModelImport } from './readModel.ts';
|
|
40
|
+
export { browserInflate, browserInflateRaw, prepareFbxInflate, prepareZipInflate, } from './fbxInflate.ts';
|
|
41
|
+
export type { AsyncInflate } from './fbxInflate.ts';
|
|
42
|
+
export { weldMesh, dropDefaultAttributes } from './weld.ts';
|
|
43
|
+
export { deriveTangentsFor } from './tangentOrder.ts';
|
|
44
|
+
export { buildCoarseLevel, isOutlineWorthWriting, DEFAULT_COARSE_CELLS } from './coarseLevel.ts';
|
|
45
|
+
export type { CoarseLevelOptions } from './coarseLevel.ts';
|
|
46
|
+
export { orientMeshes, parseUpAxis, describeUpAxis } from './orient.ts';
|
|
47
|
+
export type { UpAxis } from './orient.ts';
|
|
48
|
+
export { readGltfSkins } from './gltfSkin.ts';
|
|
49
|
+
export type { GltfAnimated, GltfSkin } from './gltfSkin.ts';
|
|
50
|
+
export { convertHandedness } from './orient.ts';
|
|
51
|
+
export type { Handedness } from './orient.ts';
|
|
52
|
+
export { orientNodes } from './orient.ts';
|
|
53
|
+
export { mirrorNodes } from './orient.ts';
|
|
54
|
+
export { localiseNodes } from './localise.ts';
|
|
55
|
+
export type { Localised } from './localise.ts';
|
|
56
|
+
export { colliderBeside, levelsBeside } from './readModel.ts';
|
|
57
|
+
export { ddsToRgba, isDds } from './dds.ts';
|
|
58
|
+
export type { DdsImage } from './dds.ts';
|