@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
package/dist/orient.d.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turn an imported asset into the engine's frame. One responsibility: orientation.
|
|
3
|
+
*
|
|
4
|
+
* **This lives on the intermediate, not in a reader, and that placement is the point.** Every
|
|
5
|
+
* format arrives here eventually — glTF, OBJ, STL, FBX, and whatever is added next — and each
|
|
6
|
+
* one has its own idea of which way is up: glTF specifies Y-up and means it, STL specifies
|
|
7
|
+
* nothing at all, FBX records a convention per file and its exporters disagree about how to
|
|
8
|
+
* honour it. If each reader carried its own correction, the engine would slowly acquire one
|
|
9
|
+
* format's habits as its defaults, and the next reader would be written to match the last
|
|
10
|
+
* one's quirks rather than its own specification. So a reader's job is to report what its
|
|
11
|
+
* file says, faithfully and with no opinion, and the single opinionated step happens once,
|
|
12
|
+
* here, where it is written down and testable.
|
|
13
|
+
*
|
|
14
|
+
* **Why an override exists at all.** A file can be internally consistent and still disagree
|
|
15
|
+
* with itself: an asset whose `GlobalSettings` declare Y-up while every node in it was
|
|
16
|
+
* exported a half turn away is not malformed, and no amount of reading it more carefully
|
|
17
|
+
* reveals which of the two statements the author meant. That is not a defect a reader can
|
|
18
|
+
* repair, because the information is genuinely absent from the file — so it is supplied from
|
|
19
|
+
* outside, by somebody who has looked at the model, and the baker prints what it was told.
|
|
20
|
+
*
|
|
21
|
+
* Every turn here is a **rotation**, never a mirror. A reflection would flip the handedness
|
|
22
|
+
* of the geometry, which turns every triangle inside out and shades the model dark where it
|
|
23
|
+
* should be lit, and it is not obvious in a bounding box — the numbers look right and the
|
|
24
|
+
* render does not. Each entry in `TURNS` has determinant +1, so the winding a reader produced
|
|
25
|
+
* is still the winding the rasteriser wants, and the index buffers are passed through
|
|
26
|
+
* untouched.
|
|
27
|
+
*/
|
|
28
|
+
import type { DrftNode, MeshData } from '@driftengine/drft';
|
|
29
|
+
/** Which way is up in the asset, as a signed axis of the engine's own frame. */
|
|
30
|
+
export type UpAxis = '+x' | '-x' | '+y' | '-y' | '+z' | '-z';
|
|
31
|
+
export declare const UP_AXES: readonly UpAxis[];
|
|
32
|
+
/** Read an axis a person typed. `z` and `+z` mean the same thing; anything else is refused. */
|
|
33
|
+
export declare function parseUpAxis(text: string): UpAxis;
|
|
34
|
+
/** What the turn does, in words, for the line the baker prints. */
|
|
35
|
+
export declare function describeUpAxis(up: UpAxis): string;
|
|
36
|
+
/**
|
|
37
|
+
* Stand an asset up, given which of its axes points at the sky.
|
|
38
|
+
*
|
|
39
|
+
* Normals take the same matrix as positions rather than an inverse transpose, and that is
|
|
40
|
+
* exact rather than an approximation: for a rotation the two are the same matrix. There is
|
|
41
|
+
* no scale here by construction, which is the other half of why.
|
|
42
|
+
*/
|
|
43
|
+
export declare function orientMeshes(meshes: readonly MeshData[], assetUp: UpAxis): MeshData[];
|
|
44
|
+
/**
|
|
45
|
+
* Which way round a format's coordinate system is.
|
|
46
|
+
*
|
|
47
|
+
* **Every format read here is right-handed**, including the one that was declared left-handed for
|
|
48
|
+
* two releases on the strength of the graphics API its authoring tool targets, and whose frame is
|
|
49
|
+
* measured in the reader that reports it. The pair below is what a genuinely left-handed format
|
|
50
|
+
* gets.
|
|
51
|
+
*/
|
|
52
|
+
export type Handedness = 'right' | 'left';
|
|
53
|
+
/**
|
|
54
|
+
* Mirror an asset into the engine's right-handed frame, in place.
|
|
55
|
+
*
|
|
56
|
+
* **This is the one thing the `TURNS` table above deliberately cannot do**, and the paragraph at
|
|
57
|
+
* the top of this file explaining why is still correct: an up-axis correction must be a rotation,
|
|
58
|
+
* or it flips geometry that was already the right way round. A *handedness* conversion is the
|
|
59
|
+
* opposite case — the geometry is genuinely mirrored, and refusing to mirror it is what would
|
|
60
|
+
* leave it wrong. Keeping the two apart is what lets each stay honest about what it does.
|
|
61
|
+
*
|
|
62
|
+
* **Three changes, and all three or none.** Negating X alone turns every triangle inside out;
|
|
63
|
+
* reversing the winding without flipping the bitangent sign lights every normal-mapped surface
|
|
64
|
+
* backwards. Doing them together, here, is why this is a function and not three lines inside a
|
|
65
|
+
* reader — the next left-handed format gets the same conversion and the same tests.
|
|
66
|
+
*
|
|
67
|
+
* UVs are untouched. A mirror moves the surface, not its parameterisation.
|
|
68
|
+
*/
|
|
69
|
+
export declare function convertHandedness(meshes: readonly MeshData[]): void;
|
|
70
|
+
/**
|
|
71
|
+
* Mirror an asset's hierarchy by the same reflection `convertHandedness` mirrors its geometry by.
|
|
72
|
+
*
|
|
73
|
+
* **The pair to `convertHandedness`, exactly as `orientNodes` is the pair to `orientMeshes`**, and
|
|
74
|
+
* it did not exist: the one caller that mirrored a graph negated each node's translation X inline
|
|
75
|
+
* and left the rotations alone. A hierarchy with rotations then places its parts where the
|
|
76
|
+
* geometry is not — measured on a real car at **1.385 m apart and on the other side of the
|
|
77
|
+
* model** — while both halves stay individually well formed, which is the worst shape this can
|
|
78
|
+
* take and the reason the two functions now sit together.
|
|
79
|
+
*
|
|
80
|
+
* **The rotation is conjugated, not negated.** Reflecting a rotation `R` by a mirror `M` gives
|
|
81
|
+
* `M R M`, which is a rotation about the mirrored axis in the opposite sense; as a quaternion that
|
|
82
|
+
* is `(x, −y, −z, w)` for a reflection about the YZ plane. A scale is untouched, because `M S M`
|
|
83
|
+
* is `S` for any diagonal.
|
|
84
|
+
*
|
|
85
|
+
* **Every node, not only the roots**, which is the one place this differs from `orientNodes`. A
|
|
86
|
+
* rotation applied to a root already carries its descendants, so applying it to a child would
|
|
87
|
+
* apply it twice. A reflection is a conjugation and conjugation distributes over the product —
|
|
88
|
+
* `M (A B) M = (M A M)(M B M)` — so leaving a child alone drops the mirror out of the composition
|
|
89
|
+
* rather than avoiding a double application.
|
|
90
|
+
*/
|
|
91
|
+
export declare function mirrorNodes(nodes: readonly DrftNode[]): DrftNode[];
|
|
92
|
+
/**
|
|
93
|
+
* Turn an asset's hierarchy by the same rotation `orientMeshes` turns its geometry by.
|
|
94
|
+
*
|
|
95
|
+
* **Without this, `--up` silently desynchronises the two.** The meshes rotate and the graph does
|
|
96
|
+
* not, so every node then claims a part is somewhere it no longer is — and nothing downstream can
|
|
97
|
+
* detect it, because both halves are individually well formed. That is the worst shape a bug can
|
|
98
|
+
* take in a bake, so the two turns happen together or the hierarchy is wrong.
|
|
99
|
+
*
|
|
100
|
+
* **Only roots are turned**, and that is not an optimisation: a child's transform is relative to
|
|
101
|
+
* its parent, so rotating the parent already carries every descendant with it. Turning a child too
|
|
102
|
+
* would apply the rotation twice.
|
|
103
|
+
*/
|
|
104
|
+
export declare function orientNodes(nodes: readonly DrftNode[], assetUp: UpAxis): DrftNode[];
|
package/dist/orient.js
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turn an imported asset into the engine's frame. One responsibility: orientation.
|
|
3
|
+
*
|
|
4
|
+
* **This lives on the intermediate, not in a reader, and that placement is the point.** Every
|
|
5
|
+
* format arrives here eventually — glTF, OBJ, STL, FBX, and whatever is added next — and each
|
|
6
|
+
* one has its own idea of which way is up: glTF specifies Y-up and means it, STL specifies
|
|
7
|
+
* nothing at all, FBX records a convention per file and its exporters disagree about how to
|
|
8
|
+
* honour it. If each reader carried its own correction, the engine would slowly acquire one
|
|
9
|
+
* format's habits as its defaults, and the next reader would be written to match the last
|
|
10
|
+
* one's quirks rather than its own specification. So a reader's job is to report what its
|
|
11
|
+
* file says, faithfully and with no opinion, and the single opinionated step happens once,
|
|
12
|
+
* here, where it is written down and testable.
|
|
13
|
+
*
|
|
14
|
+
* **Why an override exists at all.** A file can be internally consistent and still disagree
|
|
15
|
+
* with itself: an asset whose `GlobalSettings` declare Y-up while every node in it was
|
|
16
|
+
* exported a half turn away is not malformed, and no amount of reading it more carefully
|
|
17
|
+
* reveals which of the two statements the author meant. That is not a defect a reader can
|
|
18
|
+
* repair, because the information is genuinely absent from the file — so it is supplied from
|
|
19
|
+
* outside, by somebody who has looked at the model, and the baker prints what it was told.
|
|
20
|
+
*
|
|
21
|
+
* Every turn here is a **rotation**, never a mirror. A reflection would flip the handedness
|
|
22
|
+
* of the geometry, which turns every triangle inside out and shades the model dark where it
|
|
23
|
+
* should be lit, and it is not obvious in a bounding box — the numbers look right and the
|
|
24
|
+
* render does not. Each entry in `TURNS` has determinant +1, so the winding a reader produced
|
|
25
|
+
* is still the winding the rasteriser wants, and the index buffers are passed through
|
|
26
|
+
* untouched.
|
|
27
|
+
*/
|
|
28
|
+
import { DrftError } from '@driftengine/drft';
|
|
29
|
+
export const UP_AXES = ['+x', '-x', '+y', '-y', '+z', '-z'];
|
|
30
|
+
/**
|
|
31
|
+
* The rotation that carries each axis onto the engine's `+y`, row-major 3x3.
|
|
32
|
+
*
|
|
33
|
+
* `-y` is the one genuinely ambiguous case: a half turn about X and a half turn about Z both
|
|
34
|
+
* stand the asset up, and they differ by a half turn of yaw, which no amount of measurement
|
|
35
|
+
* on the model can decide. X is the choice, because for an asset whose parts were exported
|
|
36
|
+
* with a quarter turn already baked into them it reduces those to exactly the conventional
|
|
37
|
+
* Z-up conversion rather than to that conversion plus an unexplained yaw.
|
|
38
|
+
*/
|
|
39
|
+
const TURNS = {
|
|
40
|
+
'+y': { m: [1, 0, 0, 0, 1, 0, 0, 0, 1], says: 'already the engine frame, so nothing is turned' },
|
|
41
|
+
'-y': { m: [1, 0, 0, 0, -1, 0, 0, 0, -1], says: 'a half turn about X' },
|
|
42
|
+
'+z': {
|
|
43
|
+
m: [1, 0, 0, 0, 0, 1, 0, -1, 0],
|
|
44
|
+
says: 'a quarter turn about X, the conventional Z-up conversion',
|
|
45
|
+
},
|
|
46
|
+
'-z': { m: [1, 0, 0, 0, 0, -1, 0, 1, 0], says: 'a quarter turn about X, the other way' },
|
|
47
|
+
'+x': { m: [0, -1, 0, 1, 0, 0, 0, 0, 1], says: 'a quarter turn about Z' },
|
|
48
|
+
'-x': { m: [0, 1, 0, -1, 0, 0, 0, 0, 1], says: 'a quarter turn about Z, the other way' },
|
|
49
|
+
};
|
|
50
|
+
/** Read an axis a person typed. `z` and `+z` mean the same thing; anything else is refused. */
|
|
51
|
+
export function parseUpAxis(text) {
|
|
52
|
+
const cleaned = text.trim().toLowerCase();
|
|
53
|
+
const signed = cleaned.startsWith('+') || cleaned.startsWith('-') ? cleaned : `+${cleaned}`;
|
|
54
|
+
const found = UP_AXES.find((axis) => axis === signed);
|
|
55
|
+
if (found === undefined) {
|
|
56
|
+
throw new DrftError(`"${text}" is not an axis. Use one of: ${UP_AXES.join(', ')}.`);
|
|
57
|
+
}
|
|
58
|
+
return found;
|
|
59
|
+
}
|
|
60
|
+
/** What the turn does, in words, for the line the baker prints. */
|
|
61
|
+
export function describeUpAxis(up) {
|
|
62
|
+
return TURNS[up].says;
|
|
63
|
+
}
|
|
64
|
+
function turnTriples(source, m) {
|
|
65
|
+
const out = new Float32Array(source.length);
|
|
66
|
+
const m0 = m[0];
|
|
67
|
+
const m1 = m[1];
|
|
68
|
+
const m2 = m[2];
|
|
69
|
+
const m3 = m[3];
|
|
70
|
+
const m4 = m[4];
|
|
71
|
+
const m5 = m[5];
|
|
72
|
+
const m6 = m[6];
|
|
73
|
+
const m7 = m[7];
|
|
74
|
+
const m8 = m[8];
|
|
75
|
+
for (let i = 0; i + 2 < source.length; i += 3) {
|
|
76
|
+
const x = source[i];
|
|
77
|
+
const y = source[i + 1];
|
|
78
|
+
const z = source[i + 2];
|
|
79
|
+
out[i] = m0 * x + m1 * y + m2 * z;
|
|
80
|
+
out[i + 1] = m3 * x + m4 * y + m5 * z;
|
|
81
|
+
out[i + 2] = m6 * x + m7 * y + m8 * z;
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Stand an asset up, given which of its axes points at the sky.
|
|
87
|
+
*
|
|
88
|
+
* Normals take the same matrix as positions rather than an inverse transpose, and that is
|
|
89
|
+
* exact rather than an approximation: for a rotation the two are the same matrix. There is
|
|
90
|
+
* no scale here by construction, which is the other half of why.
|
|
91
|
+
*/
|
|
92
|
+
export function orientMeshes(meshes, assetUp) {
|
|
93
|
+
const { m } = TURNS[assetUp];
|
|
94
|
+
return meshes.map((mesh) => ({
|
|
95
|
+
...mesh,
|
|
96
|
+
positions: turnTriples(mesh.positions, m),
|
|
97
|
+
normals: turnTriples(mesh.normals, m),
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Mirror an asset into the engine's right-handed frame, in place.
|
|
102
|
+
*
|
|
103
|
+
* **This is the one thing the `TURNS` table above deliberately cannot do**, and the paragraph at
|
|
104
|
+
* the top of this file explaining why is still correct: an up-axis correction must be a rotation,
|
|
105
|
+
* or it flips geometry that was already the right way round. A *handedness* conversion is the
|
|
106
|
+
* opposite case — the geometry is genuinely mirrored, and refusing to mirror it is what would
|
|
107
|
+
* leave it wrong. Keeping the two apart is what lets each stay honest about what it does.
|
|
108
|
+
*
|
|
109
|
+
* **Three changes, and all three or none.** Negating X alone turns every triangle inside out;
|
|
110
|
+
* reversing the winding without flipping the bitangent sign lights every normal-mapped surface
|
|
111
|
+
* backwards. Doing them together, here, is why this is a function and not three lines inside a
|
|
112
|
+
* reader — the next left-handed format gets the same conversion and the same tests.
|
|
113
|
+
*
|
|
114
|
+
* UVs are untouched. A mirror moves the surface, not its parameterisation.
|
|
115
|
+
*/
|
|
116
|
+
export function convertHandedness(meshes) {
|
|
117
|
+
for (const mesh of meshes) {
|
|
118
|
+
for (let at = 0; at < mesh.positions.length; at += 3) {
|
|
119
|
+
mesh.positions[at] = -mesh.positions[at];
|
|
120
|
+
}
|
|
121
|
+
for (let at = 0; at < mesh.normals.length; at += 3) {
|
|
122
|
+
mesh.normals[at] = -mesh.normals[at];
|
|
123
|
+
}
|
|
124
|
+
const tangents = mesh.tangents;
|
|
125
|
+
if (tangents !== undefined) {
|
|
126
|
+
for (let at = 0; at < tangents.length; at += 4) {
|
|
127
|
+
tangents[at] = -tangents[at];
|
|
128
|
+
tangents[at + 3] = -tangents[at + 3];
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
for (let at = 0; at + 2 < mesh.indices.length; at += 3) {
|
|
132
|
+
const middle = mesh.indices[at + 1];
|
|
133
|
+
mesh.indices[at + 1] = mesh.indices[at + 2];
|
|
134
|
+
mesh.indices[at + 2] = middle;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Mirror an asset's hierarchy by the same reflection `convertHandedness` mirrors its geometry by.
|
|
140
|
+
*
|
|
141
|
+
* **The pair to `convertHandedness`, exactly as `orientNodes` is the pair to `orientMeshes`**, and
|
|
142
|
+
* it did not exist: the one caller that mirrored a graph negated each node's translation X inline
|
|
143
|
+
* and left the rotations alone. A hierarchy with rotations then places its parts where the
|
|
144
|
+
* geometry is not — measured on a real car at **1.385 m apart and on the other side of the
|
|
145
|
+
* model** — while both halves stay individually well formed, which is the worst shape this can
|
|
146
|
+
* take and the reason the two functions now sit together.
|
|
147
|
+
*
|
|
148
|
+
* **The rotation is conjugated, not negated.** Reflecting a rotation `R` by a mirror `M` gives
|
|
149
|
+
* `M R M`, which is a rotation about the mirrored axis in the opposite sense; as a quaternion that
|
|
150
|
+
* is `(x, −y, −z, w)` for a reflection about the YZ plane. A scale is untouched, because `M S M`
|
|
151
|
+
* is `S` for any diagonal.
|
|
152
|
+
*
|
|
153
|
+
* **Every node, not only the roots**, which is the one place this differs from `orientNodes`. A
|
|
154
|
+
* rotation applied to a root already carries its descendants, so applying it to a child would
|
|
155
|
+
* apply it twice. A reflection is a conjugation and conjugation distributes over the product —
|
|
156
|
+
* `M (A B) M = (M A M)(M B M)` — so leaving a child alone drops the mirror out of the composition
|
|
157
|
+
* rather than avoiding a double application.
|
|
158
|
+
*/
|
|
159
|
+
export function mirrorNodes(nodes) {
|
|
160
|
+
return nodes.map((node) => ({
|
|
161
|
+
...node,
|
|
162
|
+
translation: [-node.translation[0], node.translation[1], node.translation[2]],
|
|
163
|
+
rotation: [node.rotation[0], -node.rotation[1], -node.rotation[2], node.rotation[3]],
|
|
164
|
+
}));
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Turn an asset's hierarchy by the same rotation `orientMeshes` turns its geometry by.
|
|
168
|
+
*
|
|
169
|
+
* **Without this, `--up` silently desynchronises the two.** The meshes rotate and the graph does
|
|
170
|
+
* not, so every node then claims a part is somewhere it no longer is — and nothing downstream can
|
|
171
|
+
* detect it, because both halves are individually well formed. That is the worst shape a bug can
|
|
172
|
+
* take in a bake, so the two turns happen together or the hierarchy is wrong.
|
|
173
|
+
*
|
|
174
|
+
* **Only roots are turned**, and that is not an optimisation: a child's transform is relative to
|
|
175
|
+
* its parent, so rotating the parent already carries every descendant with it. Turning a child too
|
|
176
|
+
* would apply the rotation twice.
|
|
177
|
+
*/
|
|
178
|
+
export function orientNodes(nodes, assetUp) {
|
|
179
|
+
const { m } = TURNS[assetUp];
|
|
180
|
+
if (assetUp === '+y')
|
|
181
|
+
return [...nodes];
|
|
182
|
+
const turn = (v) => [
|
|
183
|
+
m[0] * v[0] + m[1] * v[1] + m[2] * v[2],
|
|
184
|
+
m[3] * v[0] + m[4] * v[1] + m[5] * v[2],
|
|
185
|
+
m[6] * v[0] + m[7] * v[1] + m[8] * v[2],
|
|
186
|
+
];
|
|
187
|
+
/* The turn as a quaternion, so a root's own rotation composes with it. */
|
|
188
|
+
const trace = m[0] + m[4] + m[8];
|
|
189
|
+
let q;
|
|
190
|
+
if (trace > 0) {
|
|
191
|
+
const s = Math.sqrt(trace + 1) * 2;
|
|
192
|
+
q = [
|
|
193
|
+
(m[7] - m[5]) / s,
|
|
194
|
+
(m[2] - m[6]) / s,
|
|
195
|
+
(m[3] - m[1]) / s,
|
|
196
|
+
s / 4,
|
|
197
|
+
];
|
|
198
|
+
}
|
|
199
|
+
else if (m[0] > m[4] && m[0] > m[8]) {
|
|
200
|
+
const s = Math.sqrt(1 + m[0] - m[4] - m[8]) * 2;
|
|
201
|
+
q = [
|
|
202
|
+
s / 4,
|
|
203
|
+
(m[1] + m[3]) / s,
|
|
204
|
+
(m[2] + m[6]) / s,
|
|
205
|
+
(m[7] - m[5]) / s,
|
|
206
|
+
];
|
|
207
|
+
}
|
|
208
|
+
else if (m[4] > m[8]) {
|
|
209
|
+
const s = Math.sqrt(1 + m[4] - m[0] - m[8]) * 2;
|
|
210
|
+
q = [
|
|
211
|
+
(m[1] + m[3]) / s,
|
|
212
|
+
s / 4,
|
|
213
|
+
(m[5] + m[7]) / s,
|
|
214
|
+
(m[2] - m[6]) / s,
|
|
215
|
+
];
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
const s = Math.sqrt(1 + m[8] - m[0] - m[4]) * 2;
|
|
219
|
+
q = [
|
|
220
|
+
(m[2] + m[6]) / s,
|
|
221
|
+
(m[5] + m[7]) / s,
|
|
222
|
+
s / 4,
|
|
223
|
+
(m[3] - m[1]) / s,
|
|
224
|
+
];
|
|
225
|
+
}
|
|
226
|
+
return nodes.map((node) => {
|
|
227
|
+
if (node.parent >= 0)
|
|
228
|
+
return node;
|
|
229
|
+
const [bx, by, bz, bw] = node.rotation;
|
|
230
|
+
return {
|
|
231
|
+
...node,
|
|
232
|
+
translation: turn(node.translation),
|
|
233
|
+
rotation: [
|
|
234
|
+
q[3] * bx + q[0] * bw + q[1] * bz - q[2] * by,
|
|
235
|
+
q[3] * by - q[0] * bz + q[1] * bw + q[2] * bx,
|
|
236
|
+
q[3] * bz + q[0] * by - q[1] * bx + q[2] * bw,
|
|
237
|
+
q[3] * bw - q[0] * bx - q[1] * by - q[2] * bz,
|
|
238
|
+
],
|
|
239
|
+
};
|
|
240
|
+
});
|
|
241
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** The Radiance `.hdr` container (RGBE), decoded to linear floats. */
|
|
2
|
+
/**
|
|
3
|
+
* An equirectangular image, linear, three floats a texel, row 0 at the top.
|
|
4
|
+
*
|
|
5
|
+
* Three rather than four because an environment has no alpha and never will: the fourth channel
|
|
6
|
+
* would be a byte a texel of ones, and at 2048x1024 that is eight megabytes of nothing.
|
|
7
|
+
*/
|
|
8
|
+
export interface RadianceImage {
|
|
9
|
+
readonly width: number;
|
|
10
|
+
readonly height: number;
|
|
11
|
+
/** `width * height * 3` linear values, row-major from the top. */
|
|
12
|
+
readonly data: Float32Array;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Read a Radiance RGBE file into linear radiance.
|
|
16
|
+
*
|
|
17
|
+
* **Pure, and it takes bytes rather than a URL.** It knows nothing about a device, a fetch or a
|
|
18
|
+
* texture, which is what lets it be tested exactly and what keeps the fetching decision — cache
|
|
19
|
+
* headers, retries, where the file lives — with the consumer who has to make it.
|
|
20
|
+
*
|
|
21
|
+
* **Both scanline encodings, because only one of them is optional in practice.** A file narrower
|
|
22
|
+
* than 8 or wider than 0x7fff stores flat RGBE quadruples; everything else is new-style
|
|
23
|
+
* run-length, and that is every environment map anybody actually has. A decoder that handles only
|
|
24
|
+
* the flat path passes a hand-built test and fails on every real file.
|
|
25
|
+
*
|
|
26
|
+
* **Every refusal names what it found.** `AGENTS.md` requires failing loudly at load with an
|
|
27
|
+
* actionable message: a decoder that quietly answers black for a header it does not recognise
|
|
28
|
+
* hands a scene an environment of nothing, which is indistinguishable from a probe that has not
|
|
29
|
+
* baked yet and is diagnosed as the renderer's fault.
|
|
30
|
+
*
|
|
31
|
+
* **What it costs** is `width * height * 12` bytes for the result, which at 2048x1024 is 25 MB
|
|
32
|
+
* held while the faces are built. **What would make it wrong** is a consumer streaming an
|
|
33
|
+
* environment larger than memory; this reads whole files, and a tiled format is what that consumer
|
|
34
|
+
* wants — which is one of the reasons KTX2 is named and deliberately not taken.
|
|
35
|
+
*/
|
|
36
|
+
export declare function readRadianceHdr(bytes: Uint8Array): RadianceImage;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/** The Radiance `.hdr` container (RGBE), decoded to linear floats. */
|
|
2
|
+
/**
|
|
3
|
+
* Where a decoded byte sits inside its bucket.
|
|
4
|
+
*
|
|
5
|
+
* Radiance's own `colr_color` is `ldexp(mantissa + 0.5, exponent - (128 + 8))`, and the half is
|
|
6
|
+
* the part every reimplementation drops. Dropping it makes an image about 0.2% dark, which is
|
|
7
|
+
* invisible on its own, survives every eyeball comparison, and is exactly the kind of thing that
|
|
8
|
+
* is never found. It is written as a named constant so that nobody removes it as a typo.
|
|
9
|
+
*/
|
|
10
|
+
const BUCKET_CENTRE = 0.5;
|
|
11
|
+
/** `2^-(128 + 8)`, the scale a stored exponent of zero would otherwise land on. */
|
|
12
|
+
const EXPONENT_BIAS = 128 + 8;
|
|
13
|
+
/**
|
|
14
|
+
* Read a Radiance RGBE file into linear radiance.
|
|
15
|
+
*
|
|
16
|
+
* **Pure, and it takes bytes rather than a URL.** It knows nothing about a device, a fetch or a
|
|
17
|
+
* texture, which is what lets it be tested exactly and what keeps the fetching decision — cache
|
|
18
|
+
* headers, retries, where the file lives — with the consumer who has to make it.
|
|
19
|
+
*
|
|
20
|
+
* **Both scanline encodings, because only one of them is optional in practice.** A file narrower
|
|
21
|
+
* than 8 or wider than 0x7fff stores flat RGBE quadruples; everything else is new-style
|
|
22
|
+
* run-length, and that is every environment map anybody actually has. A decoder that handles only
|
|
23
|
+
* the flat path passes a hand-built test and fails on every real file.
|
|
24
|
+
*
|
|
25
|
+
* **Every refusal names what it found.** `AGENTS.md` requires failing loudly at load with an
|
|
26
|
+
* actionable message: a decoder that quietly answers black for a header it does not recognise
|
|
27
|
+
* hands a scene an environment of nothing, which is indistinguishable from a probe that has not
|
|
28
|
+
* baked yet and is diagnosed as the renderer's fault.
|
|
29
|
+
*
|
|
30
|
+
* **What it costs** is `width * height * 12` bytes for the result, which at 2048x1024 is 25 MB
|
|
31
|
+
* held while the faces are built. **What would make it wrong** is a consumer streaming an
|
|
32
|
+
* environment larger than memory; this reads whole files, and a tiled format is what that consumer
|
|
33
|
+
* wants — which is one of the reasons KTX2 is named and deliberately not taken.
|
|
34
|
+
*/
|
|
35
|
+
export function readRadianceHdr(bytes) {
|
|
36
|
+
const signature = readLine(bytes, 0);
|
|
37
|
+
if (!signature.text.startsWith('#?RADIANCE') && !signature.text.startsWith('#?RGBE')) {
|
|
38
|
+
throw new Error(`readRadianceHdr: not a Radiance file — expected a "#?RADIANCE" signature, found ` +
|
|
39
|
+
`"${signature.text.slice(0, 20)}"`);
|
|
40
|
+
}
|
|
41
|
+
let at = signature.next;
|
|
42
|
+
let format = null;
|
|
43
|
+
/* The header runs to a blank line, and the resolution line is the one after it. */
|
|
44
|
+
for (;;) {
|
|
45
|
+
const line = readLine(bytes, at);
|
|
46
|
+
at = line.next;
|
|
47
|
+
if (line.text === '')
|
|
48
|
+
break;
|
|
49
|
+
if (line.text.startsWith('FORMAT='))
|
|
50
|
+
format = line.text.slice('FORMAT='.length).trim();
|
|
51
|
+
if (at >= bytes.length)
|
|
52
|
+
throw new Error('readRadianceHdr: the header never ended');
|
|
53
|
+
}
|
|
54
|
+
if (format !== null && format !== '32-bit_rle_rgbe') {
|
|
55
|
+
throw new Error(`readRadianceHdr: only 32-bit_rle_rgbe is decoded here, and this file declares ` +
|
|
56
|
+
`"${format}". An XYZE file carries CIE primaries rather than RGB and converting it is a ` +
|
|
57
|
+
'colour decision rather than a decoding one.');
|
|
58
|
+
}
|
|
59
|
+
const resolution = readLine(bytes, at);
|
|
60
|
+
at = resolution.next;
|
|
61
|
+
/*
|
|
62
|
+
* `-Y height +X width` only. Radiance permits eight orientations, six of which nothing has
|
|
63
|
+
* produced in decades, and treating one as another silently returns an image that is flipped —
|
|
64
|
+
* which in an environment map puts the sky underfoot and is the most confusing available failure.
|
|
65
|
+
*/
|
|
66
|
+
const match = /^-Y\s+(\d+)\s+\+X\s+(\d+)$/.exec(resolution.text.trim());
|
|
67
|
+
if (match === null) {
|
|
68
|
+
throw new Error(`readRadianceHdr: only "-Y height +X width" is handled, and this file says ` +
|
|
69
|
+
`"${resolution.text.trim()}". Any other orientation would decode to a flipped image.`);
|
|
70
|
+
}
|
|
71
|
+
const height = Number(match[1]);
|
|
72
|
+
const width = Number(match[2]);
|
|
73
|
+
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
|
|
74
|
+
throw new Error(`readRadianceHdr: a ${width} by ${height} image is not decodable`);
|
|
75
|
+
}
|
|
76
|
+
const data = new Float32Array(width * height * 3);
|
|
77
|
+
/* One scanline of RGBE, reused, because a 2048-wide image would otherwise allocate per row. */
|
|
78
|
+
const scanline = new Uint8Array(width * 4);
|
|
79
|
+
for (let y = 0; y < height; y++) {
|
|
80
|
+
at = readScanline(bytes, at, scanline, width);
|
|
81
|
+
for (let x = 0; x < width; x++) {
|
|
82
|
+
const exponent = scanline[x * 4 + 3] ?? 0;
|
|
83
|
+
const out = (y * width + x) * 3;
|
|
84
|
+
/*
|
|
85
|
+
* A stored exponent of zero is exactly black and Radiance reserves it for that. Running it
|
|
86
|
+
* through the same arithmetic gives `0.5 * 2^-136`: denormal, nonzero, and enough to make
|
|
87
|
+
* every black texel of a night sky something a prefilter will average into a grey haze.
|
|
88
|
+
*/
|
|
89
|
+
if (exponent === 0) {
|
|
90
|
+
data[out] = 0;
|
|
91
|
+
data[out + 1] = 0;
|
|
92
|
+
data[out + 2] = 0;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const scale = 2 ** (exponent - EXPONENT_BIAS);
|
|
96
|
+
data[out] = ((scanline[x * 4] ?? 0) + BUCKET_CENTRE) * scale;
|
|
97
|
+
data[out + 1] = ((scanline[x * 4 + 1] ?? 0) + BUCKET_CENTRE) * scale;
|
|
98
|
+
data[out + 2] = ((scanline[x * 4 + 2] ?? 0) + BUCKET_CENTRE) * scale;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return { width, height, data };
|
|
102
|
+
}
|
|
103
|
+
/** One newline-terminated line as text, and where the next one starts. */
|
|
104
|
+
function readLine(bytes, from) {
|
|
105
|
+
let end = from;
|
|
106
|
+
while (end < bytes.length && bytes[end] !== 0x0a)
|
|
107
|
+
end++;
|
|
108
|
+
let text = '';
|
|
109
|
+
for (let i = from; i < end; i++)
|
|
110
|
+
text += String.fromCharCode(bytes[i] ?? 0);
|
|
111
|
+
return { text: text.replace(/\r$/, ''), next: end + 1 };
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* One scanline of RGBE into `out`, in either encoding, returning where the next one starts.
|
|
115
|
+
*
|
|
116
|
+
* The new-style marker is `2, 2, hi, lo` with `hi<<8 | lo` equal to the width. That third and
|
|
117
|
+
* fourth byte being the width is what distinguishes it from an old-style scanline whose first
|
|
118
|
+
* pixel happens to be `(2, 2, ...)`, which is a real colour and does occur.
|
|
119
|
+
*/
|
|
120
|
+
function readScanline(bytes, from, out, width) {
|
|
121
|
+
const declared = ((bytes[from + 2] ?? 0) << 8) | (bytes[from + 3] ?? 0);
|
|
122
|
+
const isRle = width >= 8 &&
|
|
123
|
+
width < 0x8000 &&
|
|
124
|
+
bytes[from] === 2 &&
|
|
125
|
+
bytes[from + 1] === 2 &&
|
|
126
|
+
declared === width;
|
|
127
|
+
if (!isRle) {
|
|
128
|
+
/* Flat quadruples, which is what a narrow image and an old encoder both produce. */
|
|
129
|
+
for (let x = 0; x < width; x++) {
|
|
130
|
+
out[x * 4] = bytes[from + x * 4] ?? 0;
|
|
131
|
+
out[x * 4 + 1] = bytes[from + x * 4 + 1] ?? 0;
|
|
132
|
+
out[x * 4 + 2] = bytes[from + x * 4 + 2] ?? 0;
|
|
133
|
+
out[x * 4 + 3] = bytes[from + x * 4 + 3] ?? 0;
|
|
134
|
+
}
|
|
135
|
+
return from + width * 4;
|
|
136
|
+
}
|
|
137
|
+
let at = from + 4;
|
|
138
|
+
/*
|
|
139
|
+
* The four channels are stored one after another rather than interleaved, which is the whole
|
|
140
|
+
* reason this encoding compresses: a sky's red channel is a smooth ramp and its exponent channel
|
|
141
|
+
* is nearly constant, and neither is compressible while they are interleaved with the others.
|
|
142
|
+
*/
|
|
143
|
+
for (let channel = 0; channel < 4; channel++) {
|
|
144
|
+
let x = 0;
|
|
145
|
+
while (x < width) {
|
|
146
|
+
const count = bytes[at] ?? 0;
|
|
147
|
+
at++;
|
|
148
|
+
if (count > 128) {
|
|
149
|
+
/* A run: repeat the next byte this many times, minus the flag bit. */
|
|
150
|
+
const value = bytes[at] ?? 0;
|
|
151
|
+
at++;
|
|
152
|
+
const run = count - 128;
|
|
153
|
+
if (x + run > width)
|
|
154
|
+
throw new Error('readRadianceHdr: a run overran its scanline');
|
|
155
|
+
for (let i = 0; i < run; i++)
|
|
156
|
+
out[(x + i) * 4 + channel] = value;
|
|
157
|
+
x += run;
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
if (count === 0)
|
|
161
|
+
throw new Error('readRadianceHdr: a zero-length literal run');
|
|
162
|
+
if (x + count > width)
|
|
163
|
+
throw new Error('readRadianceHdr: a literal run overran its scanline');
|
|
164
|
+
for (let i = 0; i < count; i++)
|
|
165
|
+
out[(x + i) * 4 + channel] = bytes[at + i] ?? 0;
|
|
166
|
+
at += count;
|
|
167
|
+
x += count;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return at;
|
|
172
|
+
}
|