@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.
Files changed (100) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +9 -0
  3. package/README.md +9 -0
  4. package/dist/assetPath.d.ts +51 -0
  5. package/dist/assetPath.js +59 -0
  6. package/dist/coarseLevel.d.ts +94 -0
  7. package/dist/coarseLevel.js +606 -0
  8. package/dist/dds.d.ts +41 -0
  9. package/dist/dds.js +425 -0
  10. package/dist/drawKey.d.ts +64 -0
  11. package/dist/drawKey.js +42 -0
  12. package/dist/drftLoader.d.ts +420 -0
  13. package/dist/drftLoader.js +996 -0
  14. package/dist/drftTextures.d.ts +87 -0
  15. package/dist/drftTextures.js +138 -0
  16. package/dist/fbx.d.ts +86 -0
  17. package/dist/fbx.js +792 -0
  18. package/dist/fbxHarness.d.ts +68 -0
  19. package/dist/fbxHarness.js +185 -0
  20. package/dist/fbxInflate.d.ts +60 -0
  21. package/dist/fbxInflate.js +204 -0
  22. package/dist/fbxMaterial.d.ts +87 -0
  23. package/dist/fbxMaterial.js +345 -0
  24. package/dist/fbxRig.d.ts +130 -0
  25. package/dist/fbxRig.js +601 -0
  26. package/dist/gltf.d.ts +242 -0
  27. package/dist/gltf.js +904 -0
  28. package/dist/gltfAccessor.d.ts +2 -0
  29. package/dist/gltfAccessor.js +106 -0
  30. package/dist/gltfSkin.d.ts +32 -0
  31. package/dist/gltfSkin.js +144 -0
  32. package/dist/iesPhotometry.d.ts +44 -0
  33. package/dist/iesPhotometry.js +94 -0
  34. package/dist/imageInfo.d.ts +31 -0
  35. package/dist/imageInfo.js +119 -0
  36. package/dist/index.d.ts +58 -0
  37. package/dist/index.js +132 -0
  38. package/dist/kn5.d.ts +158 -0
  39. package/dist/kn5.js +557 -0
  40. package/dist/loadProgress.d.ts +118 -0
  41. package/dist/loadProgress.js +1 -0
  42. package/dist/localise.d.ts +35 -0
  43. package/dist/localise.js +253 -0
  44. package/dist/obj.d.ts +54 -0
  45. package/dist/obj.js +335 -0
  46. package/dist/orient.d.ts +104 -0
  47. package/dist/orient.js +241 -0
  48. package/dist/radianceHdr.d.ts +36 -0
  49. package/dist/radianceHdr.js +172 -0
  50. package/dist/readModel.d.ts +153 -0
  51. package/dist/readModel.js +386 -0
  52. package/dist/recognise.d.ts +38 -0
  53. package/dist/recognise.js +97 -0
  54. package/dist/stl.d.ts +22 -0
  55. package/dist/stl.js +131 -0
  56. package/dist/tangentOrder.d.ts +24 -0
  57. package/dist/tangentOrder.js +31 -0
  58. package/dist/threemf.d.ts +46 -0
  59. package/dist/threemf.js +254 -0
  60. package/dist/uploadBudget.d.ts +82 -0
  61. package/dist/uploadBudget.js +86 -0
  62. package/dist/usd.d.ts +49 -0
  63. package/dist/usd.js +542 -0
  64. package/dist/weld.d.ts +44 -0
  65. package/dist/weld.js +237 -0
  66. package/dist/zip.d.ts +35 -0
  67. package/dist/zip.js +110 -0
  68. package/package.json +61 -0
  69. package/src/assetPath.ts +79 -0
  70. package/src/coarseLevel.ts +720 -0
  71. package/src/dds.ts +507 -0
  72. package/src/drawKey.ts +91 -0
  73. package/src/drftLoader.ts +1197 -0
  74. package/src/drftTextures.ts +153 -0
  75. package/src/fbx.ts +934 -0
  76. package/src/fbxHarness.ts +215 -0
  77. package/src/fbxInflate.ts +258 -0
  78. package/src/fbxMaterial.ts +410 -0
  79. package/src/fbxRig.ts +652 -0
  80. package/src/gltf.ts +1161 -0
  81. package/src/gltfAccessor.ts +127 -0
  82. package/src/gltfSkin.ts +210 -0
  83. package/src/iesPhotometry.ts +136 -0
  84. package/src/imageInfo.ts +130 -0
  85. package/src/index.ts +162 -0
  86. package/src/kn5.ts +705 -0
  87. package/src/loadProgress.ts +131 -0
  88. package/src/localise.ts +275 -0
  89. package/src/obj.ts +386 -0
  90. package/src/orient.ts +262 -0
  91. package/src/radianceHdr.ts +195 -0
  92. package/src/readModel.ts +515 -0
  93. package/src/recognise.ts +124 -0
  94. package/src/stl.ts +147 -0
  95. package/src/tangentOrder.ts +32 -0
  96. package/src/threemf.ts +303 -0
  97. package/src/uploadBudget.ts +90 -0
  98. package/src/usd.ts +642 -0
  99. package/src/weld.ts +264 -0
  100. package/src/zip.ts +130 -0
@@ -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
+ }