@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,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* World-space meshes plus their graph, as parts each expressed about its own node.
|
|
3
|
+
*
|
|
4
|
+
* **Why this exists at all.** `MESH` holds world-space positions, deliberately: `docs/FORMAT.md`
|
|
5
|
+
* §4.4 rule 4 forbids a minor version from changing what an existing byte means, so geometry
|
|
6
|
+
* could not move into node-local space without breaking every reader that predates the
|
|
7
|
+
* hierarchy. The consequence is that a consumer holding a vehicle and its graph still cannot
|
|
8
|
+
* turn a wheel, because the wheel's vertices are expressed around the model's origin and not
|
|
9
|
+
* around the hub's. Turning one into the other is exact and mechanical, so the engine does it
|
|
10
|
+
* once here instead of every consumer deriving it slightly differently.
|
|
11
|
+
*
|
|
12
|
+
* **`DrftLoader` deliberately does not call this.** Its own note says placing parts under a graph
|
|
13
|
+
* is a scene decision, and that is still true — this is the arithmetic that decision needs,
|
|
14
|
+
* offered to whoever makes it.
|
|
15
|
+
*
|
|
16
|
+
* **What it costs.** One 4x4 inversion per node and one transform per vertex: 221k vertices for
|
|
17
|
+
* the largest asset this was measured against, once, at load. Not per frame. A caller loading a
|
|
18
|
+
* large model should run it behind a frame budget instead of in one block.
|
|
19
|
+
*/
|
|
20
|
+
import type { DrftNode, MeshData } from '@driftengine/drft';
|
|
21
|
+
export interface Localised {
|
|
22
|
+
/** Parallel to the meshes given, each about the origin of the node that placed it. */
|
|
23
|
+
parts: MeshData[];
|
|
24
|
+
/** Parallel to the nodes given: the accumulated world matrix, column-major. */
|
|
25
|
+
world: Float32Array[];
|
|
26
|
+
/** Nodes whose geometry could not be localised, named so a person can look at them. */
|
|
27
|
+
warnings: string[];
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Move each mesh into the frame of the node that placed it, and report every node's world matrix.
|
|
31
|
+
*
|
|
32
|
+
* A mesh no node claims is passed through untouched, which is what a format carrying no hierarchy
|
|
33
|
+
* yields and is the correct no-op.
|
|
34
|
+
*/
|
|
35
|
+
export declare function localiseNodes(meshes: readonly MeshData[], nodes: readonly DrftNode[]): Localised;
|
package/dist/localise.js
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* World-space meshes plus their graph, as parts each expressed about its own node.
|
|
3
|
+
*
|
|
4
|
+
* **Why this exists at all.** `MESH` holds world-space positions, deliberately: `docs/FORMAT.md`
|
|
5
|
+
* §4.4 rule 4 forbids a minor version from changing what an existing byte means, so geometry
|
|
6
|
+
* could not move into node-local space without breaking every reader that predates the
|
|
7
|
+
* hierarchy. The consequence is that a consumer holding a vehicle and its graph still cannot
|
|
8
|
+
* turn a wheel, because the wheel's vertices are expressed around the model's origin and not
|
|
9
|
+
* around the hub's. Turning one into the other is exact and mechanical, so the engine does it
|
|
10
|
+
* once here instead of every consumer deriving it slightly differently.
|
|
11
|
+
*
|
|
12
|
+
* **`DrftLoader` deliberately does not call this.** Its own note says placing parts under a graph
|
|
13
|
+
* is a scene decision, and that is still true — this is the arithmetic that decision needs,
|
|
14
|
+
* offered to whoever makes it.
|
|
15
|
+
*
|
|
16
|
+
* **What it costs.** One 4x4 inversion per node and one transform per vertex: 221k vertices for
|
|
17
|
+
* the largest asset this was measured against, once, at load. Not per frame. A caller loading a
|
|
18
|
+
* large model should run it behind a frame budget instead of in one block.
|
|
19
|
+
*/
|
|
20
|
+
/** Column-major 4x4 from a node's TRS, matching what a caller hands a renderer. */
|
|
21
|
+
function compose(node) {
|
|
22
|
+
const [x, y, z, w] = node.rotation;
|
|
23
|
+
const [sx, sy, sz] = node.scale;
|
|
24
|
+
const out = new Float32Array(16);
|
|
25
|
+
out[0] = (1 - 2 * (y * y + z * z)) * sx;
|
|
26
|
+
out[1] = 2 * (x * y + z * w) * sx;
|
|
27
|
+
out[2] = 2 * (x * z - y * w) * sx;
|
|
28
|
+
out[4] = 2 * (x * y - z * w) * sy;
|
|
29
|
+
out[5] = (1 - 2 * (x * x + z * z)) * sy;
|
|
30
|
+
out[6] = 2 * (y * z + x * w) * sy;
|
|
31
|
+
out[8] = 2 * (x * z + y * w) * sz;
|
|
32
|
+
out[9] = 2 * (y * z - x * w) * sz;
|
|
33
|
+
out[10] = (1 - 2 * (x * x + y * y)) * sz;
|
|
34
|
+
out[12] = node.translation[0];
|
|
35
|
+
out[13] = node.translation[1];
|
|
36
|
+
out[14] = node.translation[2];
|
|
37
|
+
out[15] = 1;
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
/** `out = a * b`, column-major. */
|
|
41
|
+
function multiply(a, b) {
|
|
42
|
+
const out = new Float32Array(16);
|
|
43
|
+
for (let column = 0; column < 4; column++) {
|
|
44
|
+
for (let row = 0; row < 4; row++) {
|
|
45
|
+
let sum = 0;
|
|
46
|
+
for (let k = 0; k < 4; k++)
|
|
47
|
+
sum += a[k * 4 + row] * b[column * 4 + k];
|
|
48
|
+
out[column * 4 + row] = sum;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
/** A full 4x4 inverse, or null where the matrix is singular. */
|
|
54
|
+
function invert(m) {
|
|
55
|
+
const a = (i) => m[i];
|
|
56
|
+
const inv = new Float32Array(16);
|
|
57
|
+
inv[0] =
|
|
58
|
+
a(5) * a(10) * a(15) -
|
|
59
|
+
a(5) * a(11) * a(14) -
|
|
60
|
+
a(9) * a(6) * a(15) +
|
|
61
|
+
a(9) * a(7) * a(14) +
|
|
62
|
+
a(13) * a(6) * a(11) -
|
|
63
|
+
a(13) * a(7) * a(10);
|
|
64
|
+
inv[4] =
|
|
65
|
+
-a(4) * a(10) * a(15) +
|
|
66
|
+
a(4) * a(11) * a(14) +
|
|
67
|
+
a(8) * a(6) * a(15) -
|
|
68
|
+
a(8) * a(7) * a(14) -
|
|
69
|
+
a(12) * a(6) * a(11) +
|
|
70
|
+
a(12) * a(7) * a(10);
|
|
71
|
+
inv[8] =
|
|
72
|
+
a(4) * a(9) * a(15) -
|
|
73
|
+
a(4) * a(11) * a(13) -
|
|
74
|
+
a(8) * a(5) * a(15) +
|
|
75
|
+
a(8) * a(7) * a(13) +
|
|
76
|
+
a(12) * a(5) * a(11) -
|
|
77
|
+
a(12) * a(7) * a(9);
|
|
78
|
+
inv[12] =
|
|
79
|
+
-a(4) * a(9) * a(14) +
|
|
80
|
+
a(4) * a(10) * a(13) +
|
|
81
|
+
a(8) * a(5) * a(14) -
|
|
82
|
+
a(8) * a(6) * a(13) -
|
|
83
|
+
a(12) * a(5) * a(10) +
|
|
84
|
+
a(12) * a(6) * a(9);
|
|
85
|
+
inv[1] =
|
|
86
|
+
-a(1) * a(10) * a(15) +
|
|
87
|
+
a(1) * a(11) * a(14) +
|
|
88
|
+
a(9) * a(2) * a(15) -
|
|
89
|
+
a(9) * a(3) * a(14) -
|
|
90
|
+
a(13) * a(2) * a(11) +
|
|
91
|
+
a(13) * a(3) * a(10);
|
|
92
|
+
inv[5] =
|
|
93
|
+
a(0) * a(10) * a(15) -
|
|
94
|
+
a(0) * a(11) * a(14) -
|
|
95
|
+
a(8) * a(2) * a(15) +
|
|
96
|
+
a(8) * a(3) * a(14) +
|
|
97
|
+
a(12) * a(2) * a(11) -
|
|
98
|
+
a(12) * a(3) * a(10);
|
|
99
|
+
inv[9] =
|
|
100
|
+
-a(0) * a(9) * a(15) +
|
|
101
|
+
a(0) * a(11) * a(13) +
|
|
102
|
+
a(8) * a(1) * a(15) -
|
|
103
|
+
a(8) * a(3) * a(13) -
|
|
104
|
+
a(12) * a(1) * a(11) +
|
|
105
|
+
a(12) * a(3) * a(9);
|
|
106
|
+
inv[13] =
|
|
107
|
+
a(0) * a(9) * a(14) -
|
|
108
|
+
a(0) * a(10) * a(13) -
|
|
109
|
+
a(8) * a(1) * a(14) +
|
|
110
|
+
a(8) * a(2) * a(13) +
|
|
111
|
+
a(12) * a(1) * a(10) -
|
|
112
|
+
a(12) * a(2) * a(9);
|
|
113
|
+
inv[2] =
|
|
114
|
+
a(1) * a(6) * a(15) -
|
|
115
|
+
a(1) * a(7) * a(14) -
|
|
116
|
+
a(5) * a(2) * a(15) +
|
|
117
|
+
a(5) * a(3) * a(14) +
|
|
118
|
+
a(13) * a(2) * a(7) -
|
|
119
|
+
a(13) * a(3) * a(6);
|
|
120
|
+
inv[6] =
|
|
121
|
+
-a(0) * a(6) * a(15) +
|
|
122
|
+
a(0) * a(7) * a(14) +
|
|
123
|
+
a(4) * a(2) * a(15) -
|
|
124
|
+
a(4) * a(3) * a(14) -
|
|
125
|
+
a(12) * a(2) * a(7) +
|
|
126
|
+
a(12) * a(3) * a(6);
|
|
127
|
+
inv[10] =
|
|
128
|
+
a(0) * a(5) * a(15) -
|
|
129
|
+
a(0) * a(7) * a(13) -
|
|
130
|
+
a(4) * a(1) * a(15) +
|
|
131
|
+
a(4) * a(3) * a(13) +
|
|
132
|
+
a(12) * a(1) * a(7) -
|
|
133
|
+
a(12) * a(3) * a(5);
|
|
134
|
+
inv[14] =
|
|
135
|
+
-a(0) * a(5) * a(14) +
|
|
136
|
+
a(0) * a(6) * a(13) +
|
|
137
|
+
a(4) * a(1) * a(14) -
|
|
138
|
+
a(4) * a(2) * a(13) -
|
|
139
|
+
a(12) * a(1) * a(6) +
|
|
140
|
+
a(12) * a(2) * a(5);
|
|
141
|
+
inv[3] =
|
|
142
|
+
-a(1) * a(6) * a(11) +
|
|
143
|
+
a(1) * a(7) * a(10) +
|
|
144
|
+
a(5) * a(2) * a(11) -
|
|
145
|
+
a(5) * a(3) * a(10) -
|
|
146
|
+
a(9) * a(2) * a(7) +
|
|
147
|
+
a(9) * a(3) * a(6);
|
|
148
|
+
inv[7] =
|
|
149
|
+
a(0) * a(6) * a(11) -
|
|
150
|
+
a(0) * a(7) * a(10) -
|
|
151
|
+
a(4) * a(2) * a(11) +
|
|
152
|
+
a(4) * a(3) * a(10) +
|
|
153
|
+
a(8) * a(2) * a(7) -
|
|
154
|
+
a(8) * a(3) * a(6);
|
|
155
|
+
inv[11] =
|
|
156
|
+
-a(0) * a(5) * a(11) +
|
|
157
|
+
a(0) * a(7) * a(9) +
|
|
158
|
+
a(4) * a(1) * a(11) -
|
|
159
|
+
a(4) * a(3) * a(9) -
|
|
160
|
+
a(8) * a(1) * a(7) +
|
|
161
|
+
a(8) * a(3) * a(5);
|
|
162
|
+
inv[15] =
|
|
163
|
+
a(0) * a(5) * a(10) -
|
|
164
|
+
a(0) * a(6) * a(9) -
|
|
165
|
+
a(4) * a(1) * a(10) +
|
|
166
|
+
a(4) * a(2) * a(9) +
|
|
167
|
+
a(8) * a(1) * a(6) -
|
|
168
|
+
a(8) * a(2) * a(5);
|
|
169
|
+
const determinant = a(0) * inv[0] +
|
|
170
|
+
a(1) * inv[4] +
|
|
171
|
+
a(2) * inv[8] +
|
|
172
|
+
a(3) * inv[12];
|
|
173
|
+
if (determinant === 0 || !Number.isFinite(determinant))
|
|
174
|
+
return null;
|
|
175
|
+
for (let i = 0; i < 16; i++)
|
|
176
|
+
inv[i] = inv[i] / determinant;
|
|
177
|
+
return inv;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Move each mesh into the frame of the node that placed it, and report every node's world matrix.
|
|
181
|
+
*
|
|
182
|
+
* A mesh no node claims is passed through untouched, which is what a format carrying no hierarchy
|
|
183
|
+
* yields and is the correct no-op.
|
|
184
|
+
*/
|
|
185
|
+
export function localiseNodes(meshes, nodes) {
|
|
186
|
+
const world = [];
|
|
187
|
+
for (const node of nodes) {
|
|
188
|
+
const local = compose(node);
|
|
189
|
+
world.push(node.parent < 0 ? local : multiply(world[node.parent], local));
|
|
190
|
+
}
|
|
191
|
+
const warnings = [];
|
|
192
|
+
const parts = meshes.map((mesh) => mesh);
|
|
193
|
+
for (let index = 0; index < nodes.length; index++) {
|
|
194
|
+
const node = nodes[index];
|
|
195
|
+
if (node.mesh < 0)
|
|
196
|
+
continue;
|
|
197
|
+
const mesh = meshes[node.mesh];
|
|
198
|
+
if (mesh === undefined)
|
|
199
|
+
continue;
|
|
200
|
+
const inverse = invert(world[index]);
|
|
201
|
+
if (inverse === null) {
|
|
202
|
+
/*
|
|
203
|
+
* A singular basis has no inverse and no rotation worth recovering, so the mesh stays where
|
|
204
|
+
* it is. Named, because a part that cannot be animated is something a person needs told
|
|
205
|
+
* rather than left to discover when it will not turn.
|
|
206
|
+
*/
|
|
207
|
+
warnings.push(`node "${node.name}" has a singular transform, so its mesh is left in world space and ` +
|
|
208
|
+
'cannot be animated about its own origin.');
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
const positions = new Float32Array(mesh.positions.length);
|
|
212
|
+
for (let at = 0; at < mesh.positions.length; at += 3) {
|
|
213
|
+
const x = mesh.positions[at];
|
|
214
|
+
const y = mesh.positions[at + 1];
|
|
215
|
+
const z = mesh.positions[at + 2];
|
|
216
|
+
positions[at] =
|
|
217
|
+
x * inverse[0] +
|
|
218
|
+
y * inverse[4] +
|
|
219
|
+
z * inverse[8] +
|
|
220
|
+
inverse[12];
|
|
221
|
+
positions[at + 1] =
|
|
222
|
+
x * inverse[1] +
|
|
223
|
+
y * inverse[5] +
|
|
224
|
+
z * inverse[9] +
|
|
225
|
+
inverse[13];
|
|
226
|
+
positions[at + 2] =
|
|
227
|
+
x * inverse[2] +
|
|
228
|
+
y * inverse[6] +
|
|
229
|
+
z * inverse[10] +
|
|
230
|
+
inverse[14];
|
|
231
|
+
}
|
|
232
|
+
/*
|
|
233
|
+
* Normals take the inverse transpose, which for the inverse already in hand is the transpose
|
|
234
|
+
* of that inverse — the same correction `gltf.ts` applies when it flattens, in the opposite
|
|
235
|
+
* direction. Renormalised, because a node carrying a scale would otherwise leave them short.
|
|
236
|
+
*/
|
|
237
|
+
const normals = new Float32Array(mesh.normals.length);
|
|
238
|
+
for (let at = 0; at < mesh.normals.length; at += 3) {
|
|
239
|
+
const x = mesh.normals[at];
|
|
240
|
+
const y = mesh.normals[at + 1];
|
|
241
|
+
const z = mesh.normals[at + 2];
|
|
242
|
+
const nx = x * inverse[0] + y * inverse[1] + z * inverse[2];
|
|
243
|
+
const ny = x * inverse[4] + y * inverse[5] + z * inverse[6];
|
|
244
|
+
const nz = x * inverse[8] + y * inverse[9] + z * inverse[10];
|
|
245
|
+
const length = Math.hypot(nx, ny, nz) || 1;
|
|
246
|
+
normals[at] = nx / length;
|
|
247
|
+
normals[at + 1] = ny / length;
|
|
248
|
+
normals[at + 2] = nz / length;
|
|
249
|
+
}
|
|
250
|
+
parts[node.mesh] = { ...mesh, positions, normals };
|
|
251
|
+
}
|
|
252
|
+
return { parts, world, warnings };
|
|
253
|
+
}
|
package/dist/obj.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wavefront OBJ, with its `.mtl` companion.
|
|
3
|
+
*
|
|
4
|
+
* Tier 1, and the cheapest thing on the list to put there: the format was specified in the
|
|
5
|
+
* eighties, has not changed since, and is plain text. There is no version to track and no
|
|
6
|
+
* vendor to follow, so a complete reader written once stays correct — which is worth more
|
|
7
|
+
* than a partial reader for a format that moves.
|
|
8
|
+
*
|
|
9
|
+
* One `MeshData` per material group, matching the glTF path: a group is the unit carrying
|
|
10
|
+
* a single material, and `MeshData` holds one material's worth of vertex attributes.
|
|
11
|
+
*/
|
|
12
|
+
import type { MeshData } from '@driftengine/drft';
|
|
13
|
+
import type { DrftMaterial } from '@driftengine/drft';
|
|
14
|
+
import type { AssetReference } from './assetPath.ts';
|
|
15
|
+
interface ObjMaterial {
|
|
16
|
+
diffuse: [number, number, number];
|
|
17
|
+
emissive: [number, number, number];
|
|
18
|
+
/** Highlight strength, from `Ks`/`Ns` or the PBR `Pm` extension. */
|
|
19
|
+
specular: number;
|
|
20
|
+
/** Highlight width, from the PBR `Pr` extension, or derived from `Ns`. */
|
|
21
|
+
roughness: number;
|
|
22
|
+
/** The colour map this material names, as declared, or null. */
|
|
23
|
+
albedo: string | null;
|
|
24
|
+
/** 1 is opaque. `d` states it directly and `Tr` states its inverse. */
|
|
25
|
+
opacity: number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Parse a `.mtl`.
|
|
29
|
+
*
|
|
30
|
+
* `Pr` and `Pm` are the widely used PBR extension rather than the original specification,
|
|
31
|
+
* and they are preferred where present because they say directly what this engine shades
|
|
32
|
+
* with. Where they are absent, `Ns` — a Phong exponent — is converted, since an exponent
|
|
33
|
+
* and a roughness describe the same thing from opposite ends.
|
|
34
|
+
*
|
|
35
|
+
* `d` and `Tr` are the same quantity written in opposite directions: `d` is how much of the
|
|
36
|
+
* surface is *there* and `Tr` is how much of it is not. Reading either as the other inverts
|
|
37
|
+
* every transparent material in a file, turning solid geometry invisible and glass solid,
|
|
38
|
+
* so both are read explicitly rather than one being assumed.
|
|
39
|
+
*/
|
|
40
|
+
export declare function parseMtl(text: string): Map<string, ObjMaterial>;
|
|
41
|
+
/**
|
|
42
|
+
* Parse an OBJ. `materials` is the parsed `.mtl`, if the caller resolved one.
|
|
43
|
+
*
|
|
44
|
+
* Faces with more than three vertices are triangulated as a fan. That is correct for the
|
|
45
|
+
* convex polygons OBJ files actually contain, and the format offers nothing that would let
|
|
46
|
+
* a reader do better for a concave one.
|
|
47
|
+
*/
|
|
48
|
+
export declare function parseObj(text: string, materials?: Map<string, ObjMaterial>): {
|
|
49
|
+
meshes: MeshData[];
|
|
50
|
+
warnings: string[];
|
|
51
|
+
materials: DrftMaterial[];
|
|
52
|
+
textures: AssetReference[];
|
|
53
|
+
};
|
|
54
|
+
export {};
|
package/dist/obj.js
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wavefront OBJ, with its `.mtl` companion.
|
|
3
|
+
*
|
|
4
|
+
* Tier 1, and the cheapest thing on the list to put there: the format was specified in the
|
|
5
|
+
* eighties, has not changed since, and is plain text. There is no version to track and no
|
|
6
|
+
* vendor to follow, so a complete reader written once stays correct — which is worth more
|
|
7
|
+
* than a partial reader for a format that moves.
|
|
8
|
+
*
|
|
9
|
+
* One `MeshData` per material group, matching the glTF path: a group is the unit carrying
|
|
10
|
+
* a single material, and `MeshData` holds one material's worth of vertex attributes.
|
|
11
|
+
*/
|
|
12
|
+
import { DrftError } from '@driftengine/drft';
|
|
13
|
+
const DEFAULT_MATERIAL = {
|
|
14
|
+
diffuse: [0.8, 0.8, 0.8],
|
|
15
|
+
emissive: [0, 0, 0],
|
|
16
|
+
specular: 0,
|
|
17
|
+
roughness: 0.5,
|
|
18
|
+
albedo: null,
|
|
19
|
+
opacity: 1,
|
|
20
|
+
};
|
|
21
|
+
const clamp01 = (value) => Number.isFinite(value) ? Math.min(1, Math.max(0, value)) : 1;
|
|
22
|
+
/**
|
|
23
|
+
* The filename out of a `map_*` line, past whatever options precede it.
|
|
24
|
+
*
|
|
25
|
+
* A map line is not just a path: `map_Kd -s 1 1 1 -bm 0.2 brick.png` is legal and common,
|
|
26
|
+
* and taking `parts[1]` gets `-s`. The options are skipped by shape rather than by a table
|
|
27
|
+
* of every flag, since the flags differ per exporter and their arity is not consistent:
|
|
28
|
+
* anything that begins with a dash, parses as a number, or is `on`/`off` is an option or an
|
|
29
|
+
* option's argument, and the first token that is none of those begins the name.
|
|
30
|
+
*
|
|
31
|
+
* The rest is joined rather than taken as one token, because a texture path may contain
|
|
32
|
+
* spaces and the line has already been split on them.
|
|
33
|
+
*/
|
|
34
|
+
function mapPath(parts) {
|
|
35
|
+
let at = 1;
|
|
36
|
+
while (at < parts.length) {
|
|
37
|
+
const token = parts[at];
|
|
38
|
+
if (token.startsWith('-') ||
|
|
39
|
+
token === 'on' ||
|
|
40
|
+
token === 'off' ||
|
|
41
|
+
!Number.isNaN(Number(token))) {
|
|
42
|
+
at++;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
const name = parts.slice(at).join(' ').trim();
|
|
48
|
+
return name;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Parse a `.mtl`.
|
|
52
|
+
*
|
|
53
|
+
* `Pr` and `Pm` are the widely used PBR extension rather than the original specification,
|
|
54
|
+
* and they are preferred where present because they say directly what this engine shades
|
|
55
|
+
* with. Where they are absent, `Ns` — a Phong exponent — is converted, since an exponent
|
|
56
|
+
* and a roughness describe the same thing from opposite ends.
|
|
57
|
+
*
|
|
58
|
+
* `d` and `Tr` are the same quantity written in opposite directions: `d` is how much of the
|
|
59
|
+
* surface is *there* and `Tr` is how much of it is not. Reading either as the other inverts
|
|
60
|
+
* every transparent material in a file, turning solid geometry invisible and glass solid,
|
|
61
|
+
* so both are read explicitly rather than one being assumed.
|
|
62
|
+
*/
|
|
63
|
+
export function parseMtl(text) {
|
|
64
|
+
const materials = new Map();
|
|
65
|
+
let current = null;
|
|
66
|
+
for (const rawLine of text.split('\n')) {
|
|
67
|
+
const line = rawLine.trim();
|
|
68
|
+
if (line.length === 0 || line.startsWith('#'))
|
|
69
|
+
continue;
|
|
70
|
+
const parts = line.split(/\s+/);
|
|
71
|
+
const keyword = parts[0];
|
|
72
|
+
const numbers = parts.slice(1).map(Number);
|
|
73
|
+
if (keyword === 'newmtl') {
|
|
74
|
+
current = { ...DEFAULT_MATERIAL, diffuse: [0.8, 0.8, 0.8], emissive: [0, 0, 0] };
|
|
75
|
+
materials.set(parts.slice(1).join(' '), current);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (current === null)
|
|
79
|
+
continue;
|
|
80
|
+
if (keyword === 'Kd')
|
|
81
|
+
current.diffuse = [numbers[0] ?? 0, numbers[1] ?? 0, numbers[2] ?? 0];
|
|
82
|
+
else if (keyword === 'Ke')
|
|
83
|
+
current.emissive = [numbers[0] ?? 0, numbers[1] ?? 0, numbers[2] ?? 0];
|
|
84
|
+
else if (keyword === 'Pr')
|
|
85
|
+
current.roughness = numbers[0] ?? 0.5;
|
|
86
|
+
else if (keyword === 'Pm')
|
|
87
|
+
current.specular = numbers[0] ?? 0;
|
|
88
|
+
else if (keyword === 'Ns') {
|
|
89
|
+
/*
|
|
90
|
+
* A Phong exponent is a width expressed backwards: 0 is a mirror-flat surface with a
|
|
91
|
+
* broad lobe and 1000 is a pinpoint. Mapped rather than ignored, so a file with no
|
|
92
|
+
* PBR extension still arrives with its highlights roughly the right size.
|
|
93
|
+
*/
|
|
94
|
+
const exponent = Math.max(numbers[0] ?? 0, 0);
|
|
95
|
+
current.roughness = Math.min(1, Math.max(0.02, Math.sqrt(2 / (exponent + 2))));
|
|
96
|
+
}
|
|
97
|
+
else if (keyword === 'map_Kd')
|
|
98
|
+
current.albedo = mapPath(parts);
|
|
99
|
+
else if (keyword === 'd')
|
|
100
|
+
current.opacity = clamp01(numbers[0] ?? 1);
|
|
101
|
+
else if (keyword === 'Tr')
|
|
102
|
+
current.opacity = clamp01(1 - (numbers[0] ?? 0));
|
|
103
|
+
else if (keyword === 'Ks' && current.specular === 0) {
|
|
104
|
+
current.specular = Math.max(numbers[0] ?? 0, numbers[1] ?? 0, numbers[2] ?? 0);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return materials;
|
|
108
|
+
}
|
|
109
|
+
function resolveIndex(raw, poolLength) {
|
|
110
|
+
if (raw === '')
|
|
111
|
+
return -1;
|
|
112
|
+
const value = Number.parseInt(raw, 10);
|
|
113
|
+
if (Number.isNaN(value))
|
|
114
|
+
return -1;
|
|
115
|
+
/* Negative indices count back from the end, which the format allows and exporters use. */
|
|
116
|
+
return value < 0 ? poolLength + value : value - 1;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Parse an OBJ. `materials` is the parsed `.mtl`, if the caller resolved one.
|
|
120
|
+
*
|
|
121
|
+
* Faces with more than three vertices are triangulated as a fan. That is correct for the
|
|
122
|
+
* convex polygons OBJ files actually contain, and the format offers nothing that would let
|
|
123
|
+
* a reader do better for a concave one.
|
|
124
|
+
*/
|
|
125
|
+
export function parseObj(text, materials = new Map()) {
|
|
126
|
+
const positions = [];
|
|
127
|
+
const normals = [];
|
|
128
|
+
const uvs = [];
|
|
129
|
+
const warnings = [];
|
|
130
|
+
/* Faces accumulate per material, so each group becomes one mesh. */
|
|
131
|
+
const groups = new Map();
|
|
132
|
+
let currentMaterial = '';
|
|
133
|
+
const faceFor = (name) => {
|
|
134
|
+
let list = groups.get(name);
|
|
135
|
+
if (list === undefined) {
|
|
136
|
+
list = [];
|
|
137
|
+
groups.set(name, list);
|
|
138
|
+
}
|
|
139
|
+
return list;
|
|
140
|
+
};
|
|
141
|
+
for (const rawLine of text.split('\n')) {
|
|
142
|
+
const line = rawLine.trim();
|
|
143
|
+
if (line.length === 0 || line.startsWith('#'))
|
|
144
|
+
continue;
|
|
145
|
+
const parts = line.split(/\s+/);
|
|
146
|
+
const keyword = parts[0];
|
|
147
|
+
if (keyword === 'v') {
|
|
148
|
+
positions.push(Number(parts[1]), Number(parts[2]), Number(parts[3]));
|
|
149
|
+
}
|
|
150
|
+
else if (keyword === 'vn') {
|
|
151
|
+
normals.push(Number(parts[1]), Number(parts[2]), Number(parts[3]));
|
|
152
|
+
}
|
|
153
|
+
else if (keyword === 'vt') {
|
|
154
|
+
uvs.push(Number(parts[1]), Number(parts[2] ?? 0));
|
|
155
|
+
}
|
|
156
|
+
else if (keyword === 'usemtl') {
|
|
157
|
+
currentMaterial = parts.slice(1).join(' ');
|
|
158
|
+
}
|
|
159
|
+
else if (keyword === 'f') {
|
|
160
|
+
const corners = [];
|
|
161
|
+
for (let i = 1; i < parts.length; i++) {
|
|
162
|
+
const [p = '', t = '', n = ''] = parts[i].split('/');
|
|
163
|
+
corners.push({
|
|
164
|
+
position: resolveIndex(p, positions.length / 3),
|
|
165
|
+
uv: resolveIndex(t, uvs.length / 2),
|
|
166
|
+
normal: resolveIndex(n, normals.length / 3),
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
if (corners.length < 3) {
|
|
170
|
+
warnings.push(`skipped a face with ${corners.length} vertices`);
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
const list = faceFor(currentMaterial);
|
|
174
|
+
for (let i = 1; i + 1 < corners.length; i++) {
|
|
175
|
+
list.push(corners[0], corners[i], corners[i + 1]);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (positions.length === 0)
|
|
180
|
+
throw new DrftError('obj: no vertices');
|
|
181
|
+
const meshes = [];
|
|
182
|
+
const perMesh = [];
|
|
183
|
+
/* Deduplicated: several materials commonly share one map, and embedding it twice would
|
|
184
|
+
put the same megabytes in the file twice and give a consumer two names for one image. */
|
|
185
|
+
const textures = [];
|
|
186
|
+
const textureIndex = new Map();
|
|
187
|
+
for (const [name, corners] of groups) {
|
|
188
|
+
if (corners.length === 0)
|
|
189
|
+
continue;
|
|
190
|
+
const material = materials.get(name) ?? DEFAULT_MATERIAL;
|
|
191
|
+
let albedo = -1;
|
|
192
|
+
if (material.albedo !== null && material.albedo !== '') {
|
|
193
|
+
const existing = textureIndex.get(material.albedo);
|
|
194
|
+
if (existing === undefined) {
|
|
195
|
+
albedo = textures.length;
|
|
196
|
+
textureIndex.set(material.albedo, albedo);
|
|
197
|
+
textures.push({ name: material.albedo });
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
albedo = existing;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
meshes.push(buildGroup(corners, positions, normals, uvs, material, warnings, name));
|
|
204
|
+
perMesh.push({
|
|
205
|
+
name,
|
|
206
|
+
color: material.diffuse,
|
|
207
|
+
specular: material.specular,
|
|
208
|
+
roughness: material.roughness,
|
|
209
|
+
emissive: Math.min(1, Math.max(material.emissive[0], material.emissive[1], material.emissive[2])),
|
|
210
|
+
emissiveColor: Math.max(material.emissive[0], material.emissive[1], material.emissive[2]) > 0
|
|
211
|
+
? material.emissive
|
|
212
|
+
: [-1, -1, -1],
|
|
213
|
+
opacity: material.opacity,
|
|
214
|
+
/* `Pm` is the PBR extension's metalness and the only thing an mtl says about
|
|
215
|
+
reflection, so it feeds this as well as the highlight. */
|
|
216
|
+
reflectivity: Math.min(1, Math.max(0, material.specular)),
|
|
217
|
+
normalMap: -1,
|
|
218
|
+
ormMap: -1,
|
|
219
|
+
emissiveMap: -1,
|
|
220
|
+
roughnessScale: 1,
|
|
221
|
+
metallicScale: 1,
|
|
222
|
+
occlusionStrength: 0,
|
|
223
|
+
cutout: 0,
|
|
224
|
+
albedo,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
if (meshes.length === 0)
|
|
228
|
+
throw new DrftError('obj: no faces');
|
|
229
|
+
return { meshes, warnings, materials: perMesh, textures };
|
|
230
|
+
}
|
|
231
|
+
function buildGroup(corners, positions, normals, uvs, material, warnings, name) {
|
|
232
|
+
const count = corners.length;
|
|
233
|
+
const outPositions = new Float32Array(count * 3);
|
|
234
|
+
const outNormals = new Float32Array(count * 3);
|
|
235
|
+
const outColors = new Float32Array(count * 3);
|
|
236
|
+
const outUvs = new Float32Array(count * 2);
|
|
237
|
+
const indices = new Uint32Array(count);
|
|
238
|
+
let hasUvs = false;
|
|
239
|
+
let missingNormals = false;
|
|
240
|
+
for (let i = 0; i < count; i++) {
|
|
241
|
+
const corner = corners[i];
|
|
242
|
+
const p = corner.position * 3;
|
|
243
|
+
outPositions[i * 3] = positions[p] ?? 0;
|
|
244
|
+
outPositions[i * 3 + 1] = positions[p + 1] ?? 0;
|
|
245
|
+
outPositions[i * 3 + 2] = positions[p + 2] ?? 0;
|
|
246
|
+
if (corner.normal >= 0) {
|
|
247
|
+
const n = corner.normal * 3;
|
|
248
|
+
outNormals[i * 3] = normals[n] ?? 0;
|
|
249
|
+
outNormals[i * 3 + 1] = normals[n + 1] ?? 0;
|
|
250
|
+
outNormals[i * 3 + 2] = normals[n + 2] ?? 0;
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
missingNormals = true;
|
|
254
|
+
}
|
|
255
|
+
if (corner.uv >= 0) {
|
|
256
|
+
hasUvs = true;
|
|
257
|
+
const t = corner.uv * 2;
|
|
258
|
+
outUvs[i * 2] = uvs[t] ?? 0;
|
|
259
|
+
/*
|
|
260
|
+
* V is flipped, once, here.
|
|
261
|
+
*
|
|
262
|
+
* **OBJ puts the texture origin at the bottom left by specification** and this engine
|
|
263
|
+
* uploads with it at the top left (`surfaceTexture.ts` sets `UNPACK_FLIP_Y_WEBGL`), which
|
|
264
|
+
* is the same single correction `fbx.ts` and `usd.ts` both make and document. This reader
|
|
265
|
+
* was the one of the four that did not, so every textured `.obj` imported upside down on
|
|
266
|
+
* its maps while the same asset's `.fbx` sibling was correct, which is exactly what made
|
|
267
|
+
* it read as a model fault rather than a reader one.
|
|
268
|
+
*
|
|
269
|
+
* Found from outside on a bought Formula 1 bundle, whose airbox lettering was inverted.
|
|
270
|
+
* glTF needs no flip and has none: its origin is top left already.
|
|
271
|
+
*/
|
|
272
|
+
outUvs[i * 2 + 1] = 1 - (uvs[t + 1] ?? 0);
|
|
273
|
+
}
|
|
274
|
+
outColors[i * 3] = material.diffuse[0];
|
|
275
|
+
outColors[i * 3 + 1] = material.diffuse[1];
|
|
276
|
+
outColors[i * 3 + 2] = material.diffuse[2];
|
|
277
|
+
indices[i] = i;
|
|
278
|
+
}
|
|
279
|
+
/*
|
|
280
|
+
* A file with no `vn` gets face normals computed here rather than a buffer of zeroes.
|
|
281
|
+
* A zero normal is not a neutral value — every lighting term multiplies by it, so the
|
|
282
|
+
* geometry would arrive black and look like a shading bug rather than a missing
|
|
283
|
+
* attribute.
|
|
284
|
+
*/
|
|
285
|
+
if (missingNormals) {
|
|
286
|
+
warnings.push(`${name === '' ? 'obj' : name}: no vertex normals, so faces are shaded flat`);
|
|
287
|
+
for (let i = 0; i + 2 < count; i += 3) {
|
|
288
|
+
const ax = outPositions[i * 3];
|
|
289
|
+
const ay = outPositions[i * 3 + 1];
|
|
290
|
+
const az = outPositions[i * 3 + 2];
|
|
291
|
+
const ux = outPositions[(i + 1) * 3] - ax;
|
|
292
|
+
const uy = outPositions[(i + 1) * 3 + 1] - ay;
|
|
293
|
+
const uz = outPositions[(i + 1) * 3 + 2] - az;
|
|
294
|
+
const vx = outPositions[(i + 2) * 3] - ax;
|
|
295
|
+
const vy = outPositions[(i + 2) * 3 + 1] - ay;
|
|
296
|
+
const vz = outPositions[(i + 2) * 3 + 2] - az;
|
|
297
|
+
let nx = uy * vz - uz * vy;
|
|
298
|
+
let ny = uz * vx - ux * vz;
|
|
299
|
+
let nz = ux * vy - uy * vx;
|
|
300
|
+
const length = Math.hypot(nx, ny, nz) || 1;
|
|
301
|
+
nx /= length;
|
|
302
|
+
ny /= length;
|
|
303
|
+
nz /= length;
|
|
304
|
+
for (let c = 0; c < 3; c++) {
|
|
305
|
+
if (outNormals[(i + c) * 3] === 0 &&
|
|
306
|
+
outNormals[(i + c) * 3 + 1] === 0 &&
|
|
307
|
+
outNormals[(i + c) * 3 + 2] === 0) {
|
|
308
|
+
outNormals[(i + c) * 3] = nx;
|
|
309
|
+
outNormals[(i + c) * 3 + 1] = ny;
|
|
310
|
+
outNormals[(i + c) * 3 + 2] = nz;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
const emissiveStrength = Math.max(...material.emissive);
|
|
316
|
+
const emissive = new Float32Array(count).fill(emissiveStrength);
|
|
317
|
+
const emissiveColor = new Float32Array(count * 3);
|
|
318
|
+
for (let i = 0; i < count; i++) {
|
|
319
|
+
const named = emissiveStrength > 0;
|
|
320
|
+
emissiveColor[i * 3] = named ? material.emissive[0] : -1;
|
|
321
|
+
emissiveColor[i * 3 + 1] = named ? material.emissive[1] : -1;
|
|
322
|
+
emissiveColor[i * 3 + 2] = named ? material.emissive[2] : -1;
|
|
323
|
+
}
|
|
324
|
+
return {
|
|
325
|
+
positions: outPositions,
|
|
326
|
+
normals: outNormals,
|
|
327
|
+
colors: outColors,
|
|
328
|
+
emissive,
|
|
329
|
+
specular: new Float32Array(count).fill(material.specular),
|
|
330
|
+
roughness: new Float32Array(count).fill(material.roughness),
|
|
331
|
+
emissiveColor,
|
|
332
|
+
indices,
|
|
333
|
+
...(hasUvs ? { uvs: outUvs } : {}),
|
|
334
|
+
};
|
|
335
|
+
}
|