poppygl 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +107 -8
- package/dist/index.js +806 -568
- package/package.json +13 -2
package/dist/index.js
CHANGED
|
@@ -1,12 +1,86 @@
|
|
|
1
|
-
|
|
1
|
+
// lib/camera/buildCamera.ts
|
|
2
|
+
import { mat4, vec3 } from "gl-matrix";
|
|
2
3
|
|
|
3
|
-
// lib/
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
4
|
+
// lib/gltf/computeWorldAABB.ts
|
|
5
|
+
import { vec4 } from "gl-matrix";
|
|
6
|
+
function computeWorldAABB(drawCalls) {
|
|
7
|
+
const min = [Infinity, Infinity, Infinity];
|
|
8
|
+
const max = [-Infinity, -Infinity, -Infinity];
|
|
9
|
+
const tmp = vec4.create();
|
|
10
|
+
for (const dc of drawCalls) {
|
|
11
|
+
const { positions, model } = dc;
|
|
12
|
+
for (let i = 0; i < positions.length; i += 3) {
|
|
13
|
+
vec4.set(tmp, positions[i], positions[i + 1], positions[i + 2], 1);
|
|
14
|
+
vec4.transformMat4(tmp, tmp, model);
|
|
15
|
+
min[0] = Math.min(min[0], tmp[0]);
|
|
16
|
+
min[1] = Math.min(min[1], tmp[1]);
|
|
17
|
+
min[2] = Math.min(min[2], tmp[2]);
|
|
18
|
+
max[0] = Math.max(max[0], tmp[0]);
|
|
19
|
+
max[1] = Math.max(max[1], tmp[1]);
|
|
20
|
+
max[2] = Math.max(max[2], tmp[2]);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return { min, max };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// lib/utils/toRad.ts
|
|
27
|
+
var toRad = (degrees) => degrees * Math.PI / 180;
|
|
28
|
+
|
|
29
|
+
// lib/camera/buildCamera.ts
|
|
30
|
+
function buildCamera(drawCalls, width, height, fovDeg, camPos, lookAt) {
|
|
31
|
+
const aspect = width / height;
|
|
32
|
+
const near = 0.01;
|
|
33
|
+
const far = 1e3;
|
|
34
|
+
const proj = mat4.create();
|
|
35
|
+
mat4.perspective(proj, toRad(fovDeg), aspect, near, far);
|
|
36
|
+
let eye;
|
|
37
|
+
let center;
|
|
38
|
+
if (camPos) {
|
|
39
|
+
eye = vec3.fromValues(camPos[0], camPos[1], camPos[2]);
|
|
40
|
+
if (lookAt) {
|
|
41
|
+
center = vec3.fromValues(lookAt[0], lookAt[1], lookAt[2]);
|
|
42
|
+
} else {
|
|
43
|
+
const aabb = computeWorldAABB(drawCalls);
|
|
44
|
+
center = vec3.fromValues(
|
|
45
|
+
0.5 * (aabb.min[0] + aabb.max[0]),
|
|
46
|
+
0.5 * (aabb.min[1] + aabb.max[1]),
|
|
47
|
+
0.5 * (aabb.min[2] + aabb.max[2])
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
} else {
|
|
51
|
+
const aabb = computeWorldAABB(drawCalls);
|
|
52
|
+
center = vec3.fromValues(
|
|
53
|
+
0.5 * (aabb.min[0] + aabb.max[0]),
|
|
54
|
+
0.5 * (aabb.min[1] + aabb.max[1]),
|
|
55
|
+
0.5 * (aabb.min[2] + aabb.max[2])
|
|
56
|
+
);
|
|
57
|
+
const diag = vec3.distance(
|
|
58
|
+
vec3.fromValues(aabb.min[0], aabb.min[1], aabb.min[2]),
|
|
59
|
+
vec3.fromValues(aabb.max[0], aabb.max[1], aabb.max[2])
|
|
60
|
+
);
|
|
61
|
+
const radius = diag * 0.5;
|
|
62
|
+
const fov = toRad(fovDeg);
|
|
63
|
+
const dist = radius / Math.tan(fov * 0.5) + radius * 0.5;
|
|
64
|
+
eye = vec3.fromValues(
|
|
65
|
+
center[0] + dist,
|
|
66
|
+
center[1] + dist * 0.3,
|
|
67
|
+
center[2] + dist
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
const up = vec3.fromValues(0, 1, 0);
|
|
71
|
+
const view = mat4.create();
|
|
72
|
+
mat4.lookAt(view, eye, center, up);
|
|
73
|
+
return { view, proj };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// lib/image/createUint8Bitmap.ts
|
|
77
|
+
var createUint8Bitmap = (width, height) => ({
|
|
78
|
+
width,
|
|
79
|
+
height,
|
|
80
|
+
data: new Uint8ClampedArray(width * height * 4)
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// lib/render/getDefaultRenderOptions.ts
|
|
10
84
|
var DEFAULT_LIGHT_DIR = [-0.4, -0.9, -0.2];
|
|
11
85
|
var DEFAULT_RENDER_OPTIONS = {
|
|
12
86
|
width: 800,
|
|
@@ -19,359 +93,117 @@ var DEFAULT_RENDER_OPTIONS = {
|
|
|
19
93
|
camPos: null,
|
|
20
94
|
lookAt: null
|
|
21
95
|
};
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
function bufferFromDataURI(uri) {
|
|
25
|
-
const m = uri.match(/^data:.*?;base64,(.*)$/);
|
|
26
|
-
if (!m) throw new Error(`Unsupported data URI: ${uri.slice(0, 64)}...`);
|
|
27
|
-
return Buffer.from(m[1], "base64");
|
|
28
|
-
}
|
|
29
|
-
function isPNG(filenameOrUri) {
|
|
30
|
-
return /\.png(\?|$)/i.test(filenameOrUri) || /image\/png/i.test(filenameOrUri);
|
|
96
|
+
function getDefaultRenderOptions() {
|
|
97
|
+
return { ...DEFAULT_RENDER_OPTIONS };
|
|
31
98
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
99
|
+
|
|
100
|
+
// lib/render/SoftwareRenderer.ts
|
|
101
|
+
import { mat3, mat4 as mat42, vec3 as vec32, vec4 as vec42 } from "gl-matrix";
|
|
102
|
+
|
|
103
|
+
// lib/gltf/computeSmoothNormals.ts
|
|
104
|
+
function computeSmoothNormals(positions, indices) {
|
|
105
|
+
const normals = new Float32Array(positions.length);
|
|
106
|
+
const vertexCount = positions.length / 3 | 0;
|
|
107
|
+
const indexArray = indices ?? (() => {
|
|
108
|
+
const arr = new Uint32Array(vertexCount);
|
|
109
|
+
for (let i = 0; i < vertexCount; i++) arr[i] = i;
|
|
110
|
+
return arr;
|
|
111
|
+
})();
|
|
112
|
+
for (let i = 0; i < indexArray.length; i += 3) {
|
|
113
|
+
const i0 = indexArray[i + 0];
|
|
114
|
+
const i1 = indexArray[i + 1];
|
|
115
|
+
const i2 = indexArray[i + 2];
|
|
116
|
+
const p0 = [
|
|
117
|
+
positions[i0 * 3 + 0],
|
|
118
|
+
positions[i0 * 3 + 1],
|
|
119
|
+
positions[i0 * 3 + 2]
|
|
120
|
+
];
|
|
121
|
+
const p1 = [
|
|
122
|
+
positions[i1 * 3 + 0],
|
|
123
|
+
positions[i1 * 3 + 1],
|
|
124
|
+
positions[i1 * 3 + 2]
|
|
125
|
+
];
|
|
126
|
+
const p2 = [
|
|
127
|
+
positions[i2 * 3 + 0],
|
|
128
|
+
positions[i2 * 3 + 1],
|
|
129
|
+
positions[i2 * 3 + 2]
|
|
130
|
+
];
|
|
131
|
+
const v10 = [p1[0] - p0[0], p1[1] - p0[1], p1[2] - p0[2]];
|
|
132
|
+
const v20 = [p2[0] - p0[0], p2[1] - p0[1], p2[2] - p0[2]];
|
|
133
|
+
const nx = v10[1] * v20[2] - v10[2] * v20[1];
|
|
134
|
+
const ny = v10[2] * v20[0] - v10[0] * v20[2];
|
|
135
|
+
const nz = v10[0] * v20[1] - v10[1] * v20[0];
|
|
136
|
+
normals[i0 * 3 + 0] += nx;
|
|
137
|
+
normals[i0 * 3 + 1] += ny;
|
|
138
|
+
normals[i0 * 3 + 2] += nz;
|
|
139
|
+
normals[i1 * 3 + 0] += nx;
|
|
140
|
+
normals[i1 * 3 + 1] += ny;
|
|
141
|
+
normals[i1 * 3 + 2] += nz;
|
|
142
|
+
normals[i2 * 3 + 0] += nx;
|
|
143
|
+
normals[i2 * 3 + 1] += ny;
|
|
144
|
+
normals[i2 * 3 + 2] += nz;
|
|
145
|
+
}
|
|
146
|
+
for (let i = 0; i < vertexCount; i++) {
|
|
147
|
+
const nx = normals[i * 3 + 0];
|
|
148
|
+
const ny = normals[i * 3 + 1];
|
|
149
|
+
const nz = normals[i * 3 + 2];
|
|
150
|
+
const invLength = 1 / (Math.hypot(nx, ny, nz) || 1);
|
|
151
|
+
normals[i * 3 + 0] = nx * invLength;
|
|
152
|
+
normals[i * 3 + 1] = ny * invLength;
|
|
153
|
+
normals[i * 3 + 2] = nz * invLength;
|
|
154
|
+
}
|
|
155
|
+
return normals;
|
|
56
156
|
}
|
|
157
|
+
|
|
158
|
+
// lib/utils/mulColor.ts
|
|
57
159
|
function mulColor(a, b) {
|
|
58
160
|
return [a[0] * b[0], a[1] * b[1], a[2] * b[2], a[3] * b[3]];
|
|
59
161
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (b.uri && b.uri.startsWith("data:")) {
|
|
66
|
-
return bufferFromDataURI(b.uri);
|
|
67
|
-
} else if (b.uri) {
|
|
68
|
-
const p = path.resolve(baseDir, decodeURIComponent(b.uri));
|
|
69
|
-
return fs.readFileSync(p);
|
|
70
|
-
} else {
|
|
71
|
-
throw new Error(
|
|
72
|
-
"Buffer without uri not supported in this lightweight loader."
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
})
|
|
76
|
-
);
|
|
77
|
-
function detectMimeTypeFromBuffer(buf, hint) {
|
|
78
|
-
if (hint) return hint;
|
|
79
|
-
if (buf.length >= 8 && buf[0] === 137 && buf[1] === 80 && buf[2] === 78 && buf[3] === 71 && buf[4] === 13 && buf[5] === 10 && buf[6] === 26 && buf[7] === 10)
|
|
80
|
-
return "image/png";
|
|
81
|
-
if (buf.length >= 2 && buf[0] === 255 && buf[1] === 216)
|
|
82
|
-
return "image/jpeg";
|
|
83
|
-
return null;
|
|
84
|
-
}
|
|
85
|
-
async function decodeImageFromBuffer(buf, mimeType) {
|
|
86
|
-
const type = detectMimeTypeFromBuffer(buf, mimeType);
|
|
87
|
-
if (type === "image/png")
|
|
88
|
-
return PImage.decodePNGFromStream(bufferToStream(buf));
|
|
89
|
-
if (type === "image/jpeg" || type === "image/jpg")
|
|
90
|
-
return PImage.decodeJPEGFromStream(bufferToStream(buf));
|
|
91
|
-
throw new Error("Unsupported embedded image mimeType: " + (mimeType || "unknown"));
|
|
92
|
-
}
|
|
93
|
-
const images = await Promise.all(
|
|
94
|
-
(gltf.images || []).map(async (img) => {
|
|
95
|
-
if (img.uri) {
|
|
96
|
-
if (img.uri.startsWith("data:")) {
|
|
97
|
-
const buf = bufferFromDataURI(img.uri);
|
|
98
|
-
if (isPNG(img.uri))
|
|
99
|
-
return PImage.decodePNGFromStream(bufferToStream(buf));
|
|
100
|
-
if (isJPG(img.uri))
|
|
101
|
-
return PImage.decodeJPEGFromStream(bufferToStream(buf));
|
|
102
|
-
throw new Error(
|
|
103
|
-
`Unsupported data-URI image type for ${img.uri.slice(0, 32)}...`
|
|
104
|
-
);
|
|
105
|
-
}
|
|
106
|
-
const fpath = path.resolve(baseDir, decodeURIComponent(img.uri));
|
|
107
|
-
if (isPNG(img.uri))
|
|
108
|
-
return PImage.decodePNGFromStream(fs.createReadStream(fpath));
|
|
109
|
-
if (isJPG(img.uri))
|
|
110
|
-
return PImage.decodeJPEGFromStream(fs.createReadStream(fpath));
|
|
111
|
-
return PImage.decodePNGFromStream(fs.createReadStream(fpath)).catch(
|
|
112
|
-
() => PImage.decodeJPEGFromStream(fs.createReadStream(fpath))
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
|
-
if (typeof img.bufferView === "number") {
|
|
116
|
-
const bv = gltf.bufferViews[img.bufferView];
|
|
117
|
-
if (!bv) throw new Error(`Invalid image bufferView index ${img.bufferView}`);
|
|
118
|
-
const buffer = buffers[bv.buffer];
|
|
119
|
-
if (!buffer) throw new Error(`Missing buffer for image bufferView ${img.bufferView}`);
|
|
120
|
-
const byteOffset = bv.byteOffset || 0;
|
|
121
|
-
const byteLength = bv.byteLength;
|
|
122
|
-
if (typeof byteLength !== "number")
|
|
123
|
-
throw new Error(`bufferView ${img.bufferView} missing byteLength for image.`);
|
|
124
|
-
const slice = buffer.slice(byteOffset, byteOffset + byteLength);
|
|
125
|
-
return decodeImageFromBuffer(slice, img.mimeType);
|
|
126
|
-
}
|
|
127
|
-
throw new Error(
|
|
128
|
-
"images[*] entry missing uri or bufferView; unsupported in this lightweight loader."
|
|
129
|
-
);
|
|
130
|
-
})
|
|
131
|
-
);
|
|
132
|
-
const COMPONENT_INFO = {
|
|
133
|
-
5120: {
|
|
134
|
-
name: "BYTE",
|
|
135
|
-
size: 1,
|
|
136
|
-
array: Int8Array,
|
|
137
|
-
norm: (v) => Math.max(-1, v / 127)
|
|
138
|
-
},
|
|
139
|
-
5121: {
|
|
140
|
-
name: "UNSIGNED_BYTE",
|
|
141
|
-
size: 1,
|
|
142
|
-
array: Uint8Array,
|
|
143
|
-
norm: (v) => v / 255
|
|
144
|
-
},
|
|
145
|
-
5122: {
|
|
146
|
-
name: "SHORT",
|
|
147
|
-
size: 2,
|
|
148
|
-
array: Int16Array,
|
|
149
|
-
norm: (v) => Math.max(-1, v / 32767)
|
|
150
|
-
},
|
|
151
|
-
5123: {
|
|
152
|
-
name: "UNSIGNED_SHORT",
|
|
153
|
-
size: 2,
|
|
154
|
-
array: Uint16Array,
|
|
155
|
-
norm: (v) => v / 65535
|
|
156
|
-
},
|
|
157
|
-
5125: {
|
|
158
|
-
name: "UNSIGNED_INT",
|
|
159
|
-
size: 4,
|
|
160
|
-
array: Uint32Array,
|
|
161
|
-
norm: (v) => v / 4294967295
|
|
162
|
-
},
|
|
163
|
-
5126: { name: "FLOAT", size: 4, array: Float32Array, norm: (v) => v }
|
|
164
|
-
};
|
|
165
|
-
const NUM_COMP = {
|
|
166
|
-
SCALAR: 1,
|
|
167
|
-
VEC2: 2,
|
|
168
|
-
VEC3: 3,
|
|
169
|
-
VEC4: 4,
|
|
170
|
-
MAT2: 4,
|
|
171
|
-
MAT3: 9,
|
|
172
|
-
MAT4: 16
|
|
173
|
-
};
|
|
174
|
-
function readAccessorAsFloat32(accIndex) {
|
|
175
|
-
const acc = gltf.accessors[accIndex];
|
|
176
|
-
if (acc.sparse)
|
|
177
|
-
throw new Error("Sparse accessors not supported (lightweight).");
|
|
178
|
-
const bv = gltf.bufferViews[acc.bufferView];
|
|
179
|
-
const comp = COMPONENT_INFO[acc.componentType];
|
|
180
|
-
const ncomp = NUM_COMP[acc.type];
|
|
181
|
-
const stride = bv.byteStride || comp.size * ncomp;
|
|
182
|
-
const count = acc.count;
|
|
183
|
-
const buf = buffers[bv.buffer];
|
|
184
|
-
const base = (bv.byteOffset || 0) + (acc.byteOffset || 0);
|
|
185
|
-
const src = new Uint8Array(
|
|
186
|
-
buf.buffer,
|
|
187
|
-
buf.byteOffset + base,
|
|
188
|
-
bv.byteLength - (acc.byteOffset || 0)
|
|
189
|
-
);
|
|
190
|
-
const out = new Float32Array(count * ncomp);
|
|
191
|
-
const canUseTightView = stride === comp.size * ncomp && src.byteOffset % comp.size === 0;
|
|
192
|
-
if (canUseTightView) {
|
|
193
|
-
const Typed = comp.array;
|
|
194
|
-
const tarr = new Typed(src.buffer, src.byteOffset, count * ncomp);
|
|
195
|
-
if (comp.name === "FLOAT" && !acc.normalized)
|
|
196
|
-
return new Float32Array(tarr.buffer, tarr.byteOffset, tarr.length);
|
|
197
|
-
for (let i = 0; i < tarr.length; i++)
|
|
198
|
-
out[i] = acc.normalized ? comp.norm(tarr[i]) : tarr[i];
|
|
199
|
-
return out;
|
|
200
|
-
}
|
|
201
|
-
const dv = new DataView(src.buffer, src.byteOffset, src.byteLength);
|
|
202
|
-
let off = 0;
|
|
203
|
-
for (let i = 0; i < count; i++) {
|
|
204
|
-
let o = i * stride;
|
|
205
|
-
for (let c = 0; c < ncomp; c++) {
|
|
206
|
-
let v;
|
|
207
|
-
switch (acc.componentType) {
|
|
208
|
-
case 5120:
|
|
209
|
-
v = dv.getInt8(o + c * comp.size);
|
|
210
|
-
break;
|
|
211
|
-
case 5121:
|
|
212
|
-
v = dv.getUint8(o + c * comp.size);
|
|
213
|
-
break;
|
|
214
|
-
case 5122:
|
|
215
|
-
v = dv.getInt16(o + c * comp.size, true);
|
|
216
|
-
break;
|
|
217
|
-
case 5123:
|
|
218
|
-
v = dv.getUint16(o + c * comp.size, true);
|
|
219
|
-
break;
|
|
220
|
-
case 5125:
|
|
221
|
-
v = dv.getUint32(o + c * comp.size, true);
|
|
222
|
-
break;
|
|
223
|
-
case 5126:
|
|
224
|
-
v = dv.getFloat32(o + c * comp.size, true);
|
|
225
|
-
break;
|
|
226
|
-
default:
|
|
227
|
-
throw new Error("Unknown componentType");
|
|
228
|
-
}
|
|
229
|
-
out[off++] = acc.normalized ? COMPONENT_INFO[acc.componentType].norm(v) : v;
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
return out;
|
|
233
|
-
}
|
|
234
|
-
function readIndices(accIndex) {
|
|
235
|
-
const acc = gltf.accessors[accIndex];
|
|
236
|
-
if (acc.type !== "SCALAR")
|
|
237
|
-
throw new Error("Indices accessor must be SCALAR");
|
|
238
|
-
const bv = gltf.bufferViews[acc.bufferView];
|
|
239
|
-
const comp = COMPONENT_INFO[acc.componentType];
|
|
240
|
-
if (acc.componentType !== 5121 && acc.componentType !== 5123 && acc.componentType !== 5125) {
|
|
241
|
-
throw new Error("Index componentType must be UNSIGNED_BYTE/SHORT/INT");
|
|
242
|
-
}
|
|
243
|
-
const stride = bv.byteStride || comp.size;
|
|
244
|
-
const count = acc.count;
|
|
245
|
-
const buf = buffers[bv.buffer];
|
|
246
|
-
const base = (bv.byteOffset || 0) + (acc.byteOffset || 0);
|
|
247
|
-
const src = new Uint8Array(
|
|
248
|
-
buf.buffer,
|
|
249
|
-
buf.byteOffset + base,
|
|
250
|
-
bv.byteLength - (acc.byteOffset || 0)
|
|
251
|
-
);
|
|
252
|
-
const canUseTightView = stride === comp.size && src.byteOffset % comp.size === 0;
|
|
253
|
-
if (canUseTightView) {
|
|
254
|
-
const Typed = comp.array;
|
|
255
|
-
const tarr = new Typed(src.buffer, src.byteOffset, count);
|
|
256
|
-
return acc.componentType === 5125 ? new Uint32Array(tarr.buffer, tarr.byteOffset, tarr.length) : new Uint32Array(tarr);
|
|
257
|
-
}
|
|
258
|
-
const dv = new DataView(src.buffer, src.byteOffset, src.byteLength);
|
|
259
|
-
const out = new Uint32Array(count);
|
|
260
|
-
for (let i = 0; i < count; i++) {
|
|
261
|
-
const o = i * stride;
|
|
262
|
-
let v;
|
|
263
|
-
switch (acc.componentType) {
|
|
264
|
-
case 5121:
|
|
265
|
-
v = dv.getUint8(o);
|
|
266
|
-
break;
|
|
267
|
-
case 5123:
|
|
268
|
-
v = dv.getUint16(o, true);
|
|
269
|
-
break;
|
|
270
|
-
case 5125:
|
|
271
|
-
v = dv.getUint32(o, true);
|
|
272
|
-
break;
|
|
273
|
-
}
|
|
274
|
-
out[i] = v >>> 0;
|
|
275
|
-
}
|
|
276
|
-
return out;
|
|
277
|
-
}
|
|
278
|
-
const textures = gltf.textures || [];
|
|
279
|
-
function getMaterial(matIndex) {
|
|
280
|
-
const m = (gltf.materials || [])[matIndex] || {};
|
|
281
|
-
const pbr = m.pbrMetallicRoughness || {};
|
|
282
|
-
const factor = pbr.baseColorFactor || [1, 1, 1, 1];
|
|
283
|
-
let texImg = null;
|
|
284
|
-
if (pbr.baseColorTexture && Number.isInteger(pbr.baseColorTexture.index)) {
|
|
285
|
-
const t = textures[pbr.baseColorTexture.index];
|
|
286
|
-
if (t && Number.isInteger(t.source)) texImg = images[t.source] || null;
|
|
287
|
-
}
|
|
288
|
-
return { baseColorFactor: factor, baseColorTexture: texImg };
|
|
289
|
-
}
|
|
290
|
-
const nodes = gltf.nodes || [];
|
|
291
|
-
const meshes = gltf.meshes || [];
|
|
292
|
-
const scenes = gltf.scenes || [];
|
|
293
|
-
const sceneIndex = Number.isInteger(gltf.scene) ? gltf.scene : 0;
|
|
294
|
-
const scene = scenes[sceneIndex] || { nodes: [] };
|
|
295
|
-
const drawCalls = [];
|
|
296
|
-
function nodeLocalMatrix(n) {
|
|
297
|
-
if (n.matrix) {
|
|
298
|
-
const m2 = mat4.create();
|
|
299
|
-
for (let i = 0; i < 16; i++) m2[i] = n.matrix[i];
|
|
300
|
-
return m2;
|
|
301
|
-
}
|
|
302
|
-
const t = n.translation || [0, 0, 0];
|
|
303
|
-
const r = n.rotation || [0, 0, 0, 1];
|
|
304
|
-
const s = n.scale || [1, 1, 1];
|
|
305
|
-
const m = mat4.create();
|
|
306
|
-
const mq = mat4.create();
|
|
307
|
-
mat4.fromRotationTranslationScale(mq, r, t, s);
|
|
308
|
-
mat4.copy(m, mq);
|
|
309
|
-
return m;
|
|
310
|
-
}
|
|
311
|
-
function traverse(nodeIndex, parentMatrix) {
|
|
312
|
-
const n = nodes[nodeIndex];
|
|
313
|
-
const local = nodeLocalMatrix(n);
|
|
314
|
-
const world = mat4.create();
|
|
315
|
-
mat4.multiply(world, parentMatrix, local);
|
|
316
|
-
if (Number.isInteger(n.mesh)) {
|
|
317
|
-
const mesh = meshes[n.mesh];
|
|
318
|
-
for (const prim of mesh.primitives) {
|
|
319
|
-
if (prim.mode != null && prim.mode !== 4) continue;
|
|
320
|
-
const posAcc = prim.attributes.POSITION;
|
|
321
|
-
if (posAcc == null) continue;
|
|
322
|
-
const norAcc = prim.attributes.NORMAL;
|
|
323
|
-
const uvAcc = prim.attributes.TEXCOORD_0;
|
|
324
|
-
const positions = readAccessorAsFloat32(posAcc);
|
|
325
|
-
const normals = norAcc != null ? readAccessorAsFloat32(norAcc) : null;
|
|
326
|
-
const uvs = uvAcc != null ? readAccessorAsFloat32(uvAcc) : null;
|
|
327
|
-
const indices = prim.indices != null ? readIndices(prim.indices) : null;
|
|
328
|
-
const material = getMaterial(prim.material);
|
|
329
|
-
drawCalls.push({
|
|
330
|
-
positions,
|
|
331
|
-
normals,
|
|
332
|
-
uvs,
|
|
333
|
-
indices,
|
|
334
|
-
model: world,
|
|
335
|
-
material
|
|
336
|
-
});
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
for (const c of n.children || []) traverse(c, world);
|
|
340
|
-
}
|
|
341
|
-
const I = mat4.create();
|
|
342
|
-
for (const root of scene.nodes || []) traverse(root, I);
|
|
343
|
-
return { gltf, drawCalls };
|
|
162
|
+
|
|
163
|
+
// lib/utils/srgbEncodeLinear01.ts
|
|
164
|
+
function srgbEncodeLinear01(linear) {
|
|
165
|
+
if (linear <= 31308e-7) return 12.92 * linear;
|
|
166
|
+
return 1.055 * Math.pow(linear, 1 / 2.4) - 0.055;
|
|
344
167
|
}
|
|
168
|
+
|
|
169
|
+
// lib/utils/clamp.ts
|
|
170
|
+
var clamp = (value, min, max) => Math.max(min, Math.min(max, value));
|
|
171
|
+
|
|
172
|
+
// lib/render/SoftwareRenderer.ts
|
|
345
173
|
var SoftwareRenderer = class {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
this.
|
|
352
|
-
this.
|
|
174
|
+
width;
|
|
175
|
+
height;
|
|
176
|
+
bitmap;
|
|
177
|
+
depth;
|
|
178
|
+
constructor(width, height, imageFactory = createUint8Bitmap) {
|
|
179
|
+
this.width = width;
|
|
180
|
+
this.height = height;
|
|
181
|
+
this.bitmap = imageFactory(width, height);
|
|
182
|
+
this.depth = new Float32Array(width * height);
|
|
353
183
|
this.clear([0, 0, 0, 255]);
|
|
354
184
|
}
|
|
185
|
+
get buffer() {
|
|
186
|
+
return this.bitmap.data;
|
|
187
|
+
}
|
|
355
188
|
clear(colorRGBA = [0, 0, 0, 255]) {
|
|
356
189
|
const [r, g, b, a] = colorRGBA;
|
|
357
|
-
for (let i = 0; i < this.
|
|
190
|
+
for (let i = 0; i < this.width * this.height; i++) {
|
|
358
191
|
const j = i * 4;
|
|
359
|
-
this.
|
|
360
|
-
this.
|
|
361
|
-
this.
|
|
362
|
-
this.
|
|
192
|
+
this.buffer[j + 0] = r;
|
|
193
|
+
this.buffer[j + 1] = g;
|
|
194
|
+
this.buffer[j + 2] = b;
|
|
195
|
+
this.buffer[j + 3] = a;
|
|
363
196
|
this.depth[i] = Infinity;
|
|
364
197
|
}
|
|
365
198
|
}
|
|
366
199
|
setPixel(x, y, r, g, b, a) {
|
|
367
|
-
if (x < 0 || y < 0 || x >= this.
|
|
368
|
-
const idx = (y * this.
|
|
369
|
-
this.
|
|
370
|
-
this.
|
|
371
|
-
this.
|
|
372
|
-
this.
|
|
200
|
+
if (x < 0 || y < 0 || x >= this.width || y >= this.height) return;
|
|
201
|
+
const idx = (y * this.width + x) * 4;
|
|
202
|
+
this.buffer[idx + 0] = r;
|
|
203
|
+
this.buffer[idx + 1] = g;
|
|
204
|
+
this.buffer[idx + 2] = b;
|
|
205
|
+
this.buffer[idx + 3] = a;
|
|
373
206
|
}
|
|
374
|
-
// Sample nearest from pureimage bitmap (u,v in [0,1])
|
|
375
207
|
sampleTextureNearest(img, u, v) {
|
|
376
208
|
if (!img) return [1, 1, 1, 1];
|
|
377
209
|
const x = clamp(Math.floor(u * (img.width - 1)), 0, img.width - 1);
|
|
@@ -385,7 +217,6 @@ var SoftwareRenderer = class {
|
|
|
385
217
|
d[idx + 3] / 255
|
|
386
218
|
];
|
|
387
219
|
}
|
|
388
|
-
// p: screen-space barycentric weights; invW: per-vertex 1/w; attrs: array of VecN
|
|
389
220
|
perspInterp(attrs, invWs, lambdas) {
|
|
390
221
|
const denom = lambdas[0] * invWs[0] + lambdas[1] * invWs[1] + lambdas[2] * invWs[2];
|
|
391
222
|
const n = attrs[0].length;
|
|
@@ -399,50 +230,50 @@ var SoftwareRenderer = class {
|
|
|
399
230
|
const { positions, normals, uvs, indices, model } = mesh;
|
|
400
231
|
const view = camera.view;
|
|
401
232
|
const proj = camera.proj;
|
|
402
|
-
const mvp =
|
|
403
|
-
|
|
233
|
+
const mvp = mat42.create();
|
|
234
|
+
mat42.multiply(mvp, proj, mat42.multiply(mat42.create(), view, model));
|
|
404
235
|
const normalMat = mat3.create();
|
|
405
236
|
mat3.normalFromMat4(normalMat, model);
|
|
406
|
-
const
|
|
407
|
-
const idx = indices
|
|
408
|
-
const a = new Uint32Array(
|
|
409
|
-
for (let i = 0; i <
|
|
237
|
+
const vertexCount = positions.length / 3 | 0;
|
|
238
|
+
const idx = indices ?? (() => {
|
|
239
|
+
const a = new Uint32Array(vertexCount);
|
|
240
|
+
for (let i = 0; i < vertexCount; i++) a[i] = i;
|
|
410
241
|
return a;
|
|
411
242
|
})();
|
|
412
243
|
let useNormals = normals;
|
|
413
244
|
if (!useNormals) {
|
|
414
245
|
useNormals = computeSmoothNormals(positions, idx);
|
|
415
246
|
}
|
|
416
|
-
const vScreen = new Array(
|
|
417
|
-
const vInvW = new Float32Array(
|
|
418
|
-
const vNDCz = new Float32Array(
|
|
419
|
-
const vWorldN = new Array(
|
|
420
|
-
for (let i = 0; i <
|
|
421
|
-
const p =
|
|
247
|
+
const vScreen = new Array(vertexCount);
|
|
248
|
+
const vInvW = new Float32Array(vertexCount);
|
|
249
|
+
const vNDCz = new Float32Array(vertexCount);
|
|
250
|
+
const vWorldN = new Array(vertexCount);
|
|
251
|
+
for (let i = 0; i < vertexCount; i++) {
|
|
252
|
+
const p = vec42.fromValues(
|
|
422
253
|
positions[i * 3 + 0],
|
|
423
254
|
positions[i * 3 + 1],
|
|
424
255
|
positions[i * 3 + 2],
|
|
425
256
|
1
|
|
426
257
|
);
|
|
427
|
-
const c =
|
|
428
|
-
|
|
258
|
+
const c = vec42.create();
|
|
259
|
+
vec42.transformMat4(c, p, mvp);
|
|
429
260
|
const invW = 1 / c[3];
|
|
430
261
|
const ndcX = c[0] * invW;
|
|
431
262
|
const ndcY = c[1] * invW;
|
|
432
263
|
const ndcZ = c[2] * invW;
|
|
433
|
-
const sx = Math.round((ndcX * 0.5 + 0.5) * (this.
|
|
434
|
-
const sy = Math.round((1 - (ndcY * 0.5 + 0.5)) * (this.
|
|
264
|
+
const sx = Math.round((ndcX * 0.5 + 0.5) * (this.width - 1));
|
|
265
|
+
const sy = Math.round((1 - (ndcY * 0.5 + 0.5)) * (this.height - 1));
|
|
435
266
|
vScreen[i] = [sx, sy];
|
|
436
267
|
vInvW[i] = invW;
|
|
437
268
|
vNDCz[i] = ndcZ;
|
|
438
|
-
const n =
|
|
269
|
+
const n = vec32.fromValues(
|
|
439
270
|
useNormals[i * 3 + 0],
|
|
440
271
|
useNormals[i * 3 + 1],
|
|
441
272
|
useNormals[i * 3 + 2]
|
|
442
273
|
);
|
|
443
|
-
const nw =
|
|
444
|
-
|
|
445
|
-
vWorldN[i] = nw;
|
|
274
|
+
const nw = vec32.create();
|
|
275
|
+
vec32.transformMat3(nw, n, normalMat);
|
|
276
|
+
vWorldN[i] = [nw[0], nw[1], nw[2]];
|
|
446
277
|
}
|
|
447
278
|
for (let i = 0; i < idx.length; i += 3) {
|
|
448
279
|
const i0 = idx[i + 0] >>> 0;
|
|
@@ -450,14 +281,16 @@ var SoftwareRenderer = class {
|
|
|
450
281
|
const i2 = idx[i + 2] >>> 0;
|
|
451
282
|
if (!(isFinite(vInvW[i0]) && isFinite(vInvW[i1]) && isFinite(vInvW[i2])))
|
|
452
283
|
continue;
|
|
453
|
-
const v0 = vScreen[i0]
|
|
284
|
+
const v0 = vScreen[i0];
|
|
285
|
+
const v1 = vScreen[i1];
|
|
286
|
+
const v2 = vScreen[i2];
|
|
454
287
|
const area = edge(v0, v1, v2);
|
|
455
288
|
if (area === 0) continue;
|
|
456
289
|
if (cullBackFaces && area < 0) continue;
|
|
457
290
|
let minX = Math.max(0, Math.min(v0[0], v1[0], v2[0]) | 0);
|
|
458
|
-
let maxX = Math.min(this.
|
|
291
|
+
let maxX = Math.min(this.width - 1, Math.max(v0[0], v1[0], v2[0]) | 0);
|
|
459
292
|
let minY = Math.max(0, Math.min(v0[1], v1[1], v2[1]) | 0);
|
|
460
|
-
let maxY = Math.min(this.
|
|
293
|
+
let maxY = Math.min(this.height - 1, Math.max(v0[1], v1[1], v2[1]) | 0);
|
|
461
294
|
const invW = [vInvW[i0], vInvW[i1], vInvW[i2]];
|
|
462
295
|
const ndcZ = [vNDCz[i0], vNDCz[i1], vNDCz[i2]];
|
|
463
296
|
const nws = [vWorldN[i0], vWorldN[i1], vWorldN[i2]];
|
|
@@ -474,13 +307,20 @@ var SoftwareRenderer = class {
|
|
|
474
307
|
const w2 = edge(v0, v1, p);
|
|
475
308
|
if (w0 < 0 || w1 < 0 || w2 < 0) continue;
|
|
476
309
|
const invArea = 1 / area;
|
|
477
|
-
const l0 = w0 * invArea
|
|
310
|
+
const l0 = w0 * invArea;
|
|
311
|
+
const l1 = w1 * invArea;
|
|
312
|
+
const l2 = w2 * invArea;
|
|
478
313
|
const zndc = l0 * ndcZ[0] + l1 * ndcZ[1] + l2 * ndcZ[2];
|
|
479
314
|
const z01 = zndc * 0.5 + 0.5;
|
|
480
|
-
const di = y * this.
|
|
315
|
+
const di = y * this.width + x;
|
|
481
316
|
if (z01 >= this.depth[di]) continue;
|
|
482
317
|
this.depth[di] = z01;
|
|
483
|
-
let baseColor =
|
|
318
|
+
let baseColor = [
|
|
319
|
+
material.baseColorFactor[0],
|
|
320
|
+
material.baseColorFactor[1],
|
|
321
|
+
material.baseColorFactor[2],
|
|
322
|
+
material.baseColorFactor[3]
|
|
323
|
+
];
|
|
484
324
|
if (uv && material.baseColorTexture) {
|
|
485
325
|
const uvp = this.perspInterp(uv, invW, [l0, l1, l2]);
|
|
486
326
|
const texel = this.sampleTextureNearest(
|
|
@@ -493,15 +333,15 @@ var SoftwareRenderer = class {
|
|
|
493
333
|
const np = this.perspInterp(nws, invW, [l0, l1, l2]);
|
|
494
334
|
const nlen = Math.hypot(np[0], np[1], np[2]) || 1;
|
|
495
335
|
const nrm = [np[0] / nlen, np[1] / nlen, np[2] / nlen];
|
|
496
|
-
const lightDir = light
|
|
336
|
+
const lightDir = light.dir ?? DEFAULT_LIGHT_DIR;
|
|
497
337
|
const ambient = clamp(
|
|
498
|
-
light
|
|
338
|
+
light.ambient ?? DEFAULT_RENDER_OPTIONS.ambient,
|
|
499
339
|
0,
|
|
500
340
|
1
|
|
501
341
|
);
|
|
502
|
-
const L =
|
|
503
|
-
|
|
504
|
-
|
|
342
|
+
const L = vec32.normalize(
|
|
343
|
+
vec32.create(),
|
|
344
|
+
vec32.fromValues(lightDir[0], lightDir[1], lightDir[2])
|
|
505
345
|
);
|
|
506
346
|
const ndotl = Math.max(
|
|
507
347
|
0,
|
|
@@ -537,233 +377,631 @@ var SoftwareRenderer = class {
|
|
|
537
377
|
function edge(a, b, p) {
|
|
538
378
|
return (p[0] - a[0]) * (b[1] - a[1]) - (p[1] - a[1]) * (b[0] - a[0]);
|
|
539
379
|
}
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
const a = new Uint32Array(vc);
|
|
545
|
-
for (let i = 0; i < vc; i++) a[i] = i;
|
|
546
|
-
return a;
|
|
547
|
-
})();
|
|
548
|
-
for (let i = 0; i < idx.length; i += 3) {
|
|
549
|
-
const i0 = idx[i + 0], i1 = idx[i + 1], i2 = idx[i + 2];
|
|
550
|
-
const p0 = [
|
|
551
|
-
positions[i0 * 3 + 0],
|
|
552
|
-
positions[i0 * 3 + 1],
|
|
553
|
-
positions[i0 * 3 + 2]
|
|
554
|
-
];
|
|
555
|
-
const p1 = [
|
|
556
|
-
positions[i1 * 3 + 0],
|
|
557
|
-
positions[i1 * 3 + 1],
|
|
558
|
-
positions[i1 * 3 + 2]
|
|
559
|
-
];
|
|
560
|
-
const p2 = [
|
|
561
|
-
positions[i2 * 3 + 0],
|
|
562
|
-
positions[i2 * 3 + 1],
|
|
563
|
-
positions[i2 * 3 + 2]
|
|
564
|
-
];
|
|
565
|
-
const v10 = [p1[0] - p0[0], p1[1] - p0[1], p1[2] - p0[2]];
|
|
566
|
-
const v20 = [p2[0] - p0[0], p2[1] - p0[1], p2[2] - p0[2]];
|
|
567
|
-
const nx = v10[1] * v20[2] - v10[2] * v20[1];
|
|
568
|
-
const ny = v10[2] * v20[0] - v10[0] * v20[2];
|
|
569
|
-
const nz = v10[0] * v20[1] - v10[1] * v20[0];
|
|
570
|
-
n[i0 * 3 + 0] += nx;
|
|
571
|
-
n[i0 * 3 + 1] += ny;
|
|
572
|
-
n[i0 * 3 + 2] += nz;
|
|
573
|
-
n[i1 * 3 + 0] += nx;
|
|
574
|
-
n[i1 * 3 + 1] += ny;
|
|
575
|
-
n[i1 * 3 + 2] += nz;
|
|
576
|
-
n[i2 * 3 + 0] += nx;
|
|
577
|
-
n[i2 * 3 + 1] += ny;
|
|
578
|
-
n[i2 * 3 + 2] += nz;
|
|
579
|
-
}
|
|
580
|
-
for (let i = 0; i < vc; i++) {
|
|
581
|
-
const nx = n[i * 3 + 0], ny = n[i * 3 + 1], nz = n[i * 3 + 2];
|
|
582
|
-
const inv = 1 / (Math.hypot(nx, ny, nz) || 1);
|
|
583
|
-
n[i * 3 + 0] = nx * inv;
|
|
584
|
-
n[i * 3 + 1] = ny * inv;
|
|
585
|
-
n[i * 3 + 2] = nz * inv;
|
|
586
|
-
}
|
|
587
|
-
return n;
|
|
588
|
-
}
|
|
589
|
-
function computeWorldAABB(drawCalls) {
|
|
590
|
-
const min = [Infinity, Infinity, Infinity];
|
|
591
|
-
const max = [-Infinity, -Infinity, -Infinity];
|
|
592
|
-
const tmp = vec4.create();
|
|
593
|
-
for (const dc of drawCalls) {
|
|
594
|
-
const { positions, model } = dc;
|
|
595
|
-
for (let i = 0; i < positions.length; i += 3) {
|
|
596
|
-
vec4.set(tmp, positions[i], positions[i + 1], positions[i + 2], 1);
|
|
597
|
-
vec4.transformMat4(tmp, tmp, model);
|
|
598
|
-
min[0] = Math.min(min[0], tmp[0]);
|
|
599
|
-
min[1] = Math.min(min[1], tmp[1]);
|
|
600
|
-
min[2] = Math.min(min[2], tmp[2]);
|
|
601
|
-
max[0] = Math.max(max[0], tmp[0]);
|
|
602
|
-
max[1] = Math.max(max[1], tmp[1]);
|
|
603
|
-
max[2] = Math.max(max[2], tmp[2]);
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
return { min, max };
|
|
607
|
-
}
|
|
608
|
-
function buildCamera(drawCalls, width, height, fovDeg, camPosOpt, lookAtOpt) {
|
|
609
|
-
const aspect = width / height;
|
|
610
|
-
const near = 0.01, far = 1e3;
|
|
611
|
-
const proj = mat4.create();
|
|
612
|
-
mat4.perspective(proj, toRad(fovDeg), aspect, near, far);
|
|
613
|
-
let eye, center;
|
|
614
|
-
if (camPosOpt) {
|
|
615
|
-
eye = vec3.fromValues(camPosOpt[0], camPosOpt[1], camPosOpt[2]);
|
|
616
|
-
if (lookAtOpt)
|
|
617
|
-
center = vec3.fromValues(lookAtOpt[0], lookAtOpt[1], lookAtOpt[2]);
|
|
618
|
-
else {
|
|
619
|
-
const aabb = computeWorldAABB(drawCalls);
|
|
620
|
-
center = vec3.fromValues(
|
|
621
|
-
0.5 * (aabb.min[0] + aabb.max[0]),
|
|
622
|
-
0.5 * (aabb.min[1] + aabb.max[1]),
|
|
623
|
-
0.5 * (aabb.min[2] + aabb.max[2])
|
|
624
|
-
);
|
|
625
|
-
}
|
|
626
|
-
} else {
|
|
627
|
-
const aabb = computeWorldAABB(drawCalls);
|
|
628
|
-
center = vec3.fromValues(
|
|
629
|
-
0.5 * (aabb.min[0] + aabb.max[0]),
|
|
630
|
-
0.5 * (aabb.min[1] + aabb.max[1]),
|
|
631
|
-
0.5 * (aabb.min[2] + aabb.max[2])
|
|
632
|
-
);
|
|
633
|
-
const diag = vec3.distance(
|
|
634
|
-
vec3.fromValues(aabb.min[0], aabb.min[1], aabb.min[2]),
|
|
635
|
-
vec3.fromValues(aabb.max[0], aabb.max[1], aabb.max[2])
|
|
636
|
-
);
|
|
637
|
-
const radius = diag * 0.5;
|
|
638
|
-
const fov = toRad(fovDeg);
|
|
639
|
-
const dist = radius / Math.tan(fov * 0.5) + radius * 0.5;
|
|
640
|
-
eye = vec3.fromValues(
|
|
641
|
-
center[0] + dist,
|
|
642
|
-
center[1] + dist * 0.3,
|
|
643
|
-
center[2] + dist
|
|
644
|
-
);
|
|
645
|
-
}
|
|
646
|
-
const up = vec3.fromValues(0, 1, 0);
|
|
647
|
-
const view = mat4.create();
|
|
648
|
-
mat4.lookAt(view, eye, center, up);
|
|
649
|
-
return { view, proj };
|
|
650
|
-
}
|
|
651
|
-
async function renderGLTFToPNGBuffer(gltfPath, options = {}) {
|
|
652
|
-
const merged = {
|
|
380
|
+
|
|
381
|
+
// lib/render/resolveRenderOptions.ts
|
|
382
|
+
function resolveRenderOptions(options = {}) {
|
|
383
|
+
return {
|
|
653
384
|
...DEFAULT_RENDER_OPTIONS,
|
|
654
385
|
...options,
|
|
655
386
|
lightDir: options.lightDir != null ? options.lightDir : DEFAULT_RENDER_OPTIONS.lightDir,
|
|
656
387
|
camPos: options.camPos !== void 0 ? options.camPos : DEFAULT_RENDER_OPTIONS.camPos,
|
|
657
388
|
lookAt: options.lookAt !== void 0 ? options.lookAt : DEFAULT_RENDER_OPTIONS.lookAt
|
|
658
389
|
};
|
|
659
|
-
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// lib/render/renderDrawCalls.ts
|
|
393
|
+
function renderDrawCalls(drawCalls, optionsInput = {}, imageFactory = createUint8Bitmap) {
|
|
394
|
+
const options = resolveRenderOptions(optionsInput);
|
|
660
395
|
const camera = buildCamera(
|
|
661
396
|
drawCalls,
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
397
|
+
options.width,
|
|
398
|
+
options.height,
|
|
399
|
+
options.fov,
|
|
400
|
+
options.camPos ?? null,
|
|
401
|
+
options.lookAt ?? null
|
|
402
|
+
);
|
|
403
|
+
const renderer = new SoftwareRenderer(
|
|
404
|
+
options.width,
|
|
405
|
+
options.height,
|
|
406
|
+
imageFactory
|
|
667
407
|
);
|
|
668
|
-
const renderer = new SoftwareRenderer(merged.width, merged.height);
|
|
669
408
|
for (const dc of drawCalls) {
|
|
670
409
|
renderer.drawMesh(
|
|
671
410
|
dc,
|
|
672
411
|
camera,
|
|
673
|
-
{ dir:
|
|
412
|
+
{ dir: options.lightDir, ambient: options.ambient },
|
|
674
413
|
dc.material,
|
|
675
|
-
|
|
676
|
-
|
|
414
|
+
options.cull,
|
|
415
|
+
options.gamma
|
|
677
416
|
);
|
|
678
417
|
}
|
|
679
|
-
return
|
|
418
|
+
return { bitmap: renderer.bitmap, camera, options };
|
|
680
419
|
}
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
420
|
+
|
|
421
|
+
// lib/gltf/createSceneFromGLTF.ts
|
|
422
|
+
import { mat4 as mat43 } from "gl-matrix";
|
|
423
|
+
var COMPONENT_INFO = {
|
|
424
|
+
5120: {
|
|
425
|
+
name: "BYTE",
|
|
426
|
+
size: 1,
|
|
427
|
+
array: Int8Array,
|
|
428
|
+
norm: (v) => Math.max(-1, v / 127)
|
|
429
|
+
},
|
|
430
|
+
5121: {
|
|
431
|
+
name: "UNSIGNED_BYTE",
|
|
432
|
+
size: 1,
|
|
433
|
+
array: Uint8Array,
|
|
434
|
+
norm: (v) => v / 255
|
|
435
|
+
},
|
|
436
|
+
5122: {
|
|
437
|
+
name: "SHORT",
|
|
438
|
+
size: 2,
|
|
439
|
+
array: Int16Array,
|
|
440
|
+
norm: (v) => Math.max(-1, v / 32767)
|
|
441
|
+
},
|
|
442
|
+
5123: {
|
|
443
|
+
name: "UNSIGNED_SHORT",
|
|
444
|
+
size: 2,
|
|
445
|
+
array: Uint16Array,
|
|
446
|
+
norm: (v) => v / 65535
|
|
447
|
+
},
|
|
448
|
+
5125: {
|
|
449
|
+
name: "UNSIGNED_INT",
|
|
450
|
+
size: 4,
|
|
451
|
+
array: Uint32Array,
|
|
452
|
+
norm: (v) => v / 4294967295
|
|
453
|
+
},
|
|
454
|
+
5126: { name: "FLOAT", size: 4, array: Float32Array, norm: (v) => v }
|
|
455
|
+
};
|
|
456
|
+
var NUM_COMPONENTS = {
|
|
457
|
+
SCALAR: 1,
|
|
458
|
+
VEC2: 2,
|
|
459
|
+
VEC3: 3,
|
|
460
|
+
VEC4: 4,
|
|
461
|
+
MAT2: 4,
|
|
462
|
+
MAT3: 9,
|
|
463
|
+
MAT4: 16
|
|
464
|
+
};
|
|
465
|
+
function getBufferViewSlice(ctx, bufferViewIndex, byteOffset) {
|
|
466
|
+
const { gltf, buffers } = ctx;
|
|
467
|
+
const bufferView = gltf.bufferViews?.[bufferViewIndex];
|
|
468
|
+
if (!bufferView)
|
|
469
|
+
throw new Error(`Invalid bufferView index ${bufferViewIndex}`);
|
|
470
|
+
const bufferIndex = bufferView.buffer;
|
|
471
|
+
const buffer = buffers[bufferIndex];
|
|
472
|
+
if (!buffer) throw new Error(`Missing buffer at index ${bufferIndex}`);
|
|
473
|
+
const viewOffset = bufferView.byteOffset ?? 0;
|
|
474
|
+
const totalByteLength = bufferView.byteLength ?? buffer.byteLength - viewOffset;
|
|
475
|
+
const baseOffset = buffer.byteOffset + viewOffset + byteOffset;
|
|
476
|
+
const viewArray = new Uint8Array(
|
|
477
|
+
buffer.buffer,
|
|
478
|
+
buffer.byteOffset + viewOffset,
|
|
479
|
+
totalByteLength
|
|
480
|
+
);
|
|
481
|
+
return {
|
|
482
|
+
array: new Uint8Array(
|
|
483
|
+
viewArray.buffer,
|
|
484
|
+
baseOffset,
|
|
485
|
+
totalByteLength - byteOffset
|
|
486
|
+
),
|
|
487
|
+
stride: bufferView.byteStride ?? 0,
|
|
488
|
+
byteOffsetInView: baseOffset
|
|
489
|
+
};
|
|
685
490
|
}
|
|
686
|
-
function
|
|
687
|
-
const
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
491
|
+
function readAccessorAsFloat32(ctx, accessorIndex) {
|
|
492
|
+
const { gltf } = ctx;
|
|
493
|
+
const accessor = gltf.accessors?.[accessorIndex];
|
|
494
|
+
if (!accessor) throw new Error(`Invalid accessor index ${accessorIndex}`);
|
|
495
|
+
if (accessor.sparse) throw new Error("Sparse accessors are not supported.");
|
|
496
|
+
const compInfo = COMPONENT_INFO[accessor.componentType];
|
|
497
|
+
const numComponents = NUM_COMPONENTS[accessor.type];
|
|
498
|
+
if (!compInfo || !numComponents)
|
|
499
|
+
throw new Error(
|
|
500
|
+
`Unsupported accessor componentType ${accessor.componentType}`
|
|
501
|
+
);
|
|
502
|
+
const byteOffset = accessor.byteOffset ?? 0;
|
|
503
|
+
const {
|
|
504
|
+
array: src,
|
|
505
|
+
stride: bufferViewStride,
|
|
506
|
+
byteOffsetInView
|
|
507
|
+
} = getBufferViewSlice(ctx, accessor.bufferView, byteOffset);
|
|
508
|
+
const stride = bufferViewStride || compInfo.size * numComponents;
|
|
509
|
+
const count = accessor.count;
|
|
510
|
+
const out = new Float32Array(count * numComponents);
|
|
511
|
+
const absoluteByteOffset = byteOffsetInView;
|
|
512
|
+
const canUseTightView = stride === compInfo.size * numComponents && absoluteByteOffset % compInfo.size === 0;
|
|
513
|
+
if (canUseTightView) {
|
|
514
|
+
const TypedArrayCtor = compInfo.array;
|
|
515
|
+
const typed = new TypedArrayCtor(
|
|
516
|
+
src.buffer,
|
|
517
|
+
src.byteOffset,
|
|
518
|
+
count * numComponents
|
|
519
|
+
);
|
|
520
|
+
if (compInfo.name === "FLOAT" && !accessor.normalized) {
|
|
521
|
+
return new Float32Array(typed.buffer, typed.byteOffset, typed.length);
|
|
522
|
+
}
|
|
523
|
+
for (let i = 0; i < typed.length; i++) {
|
|
524
|
+
out[i] = accessor.normalized ? compInfo.norm(typed[i]) : typed[i];
|
|
525
|
+
}
|
|
526
|
+
return out;
|
|
527
|
+
}
|
|
528
|
+
const dataView = new DataView(src.buffer, src.byteOffset, src.byteLength);
|
|
529
|
+
let outIndex = 0;
|
|
530
|
+
for (let i = 0; i < count; i++) {
|
|
531
|
+
const elementOffset = i * stride;
|
|
532
|
+
for (let c = 0; c < numComponents; c++) {
|
|
533
|
+
const componentOffset = elementOffset + c * compInfo.size;
|
|
534
|
+
let value;
|
|
535
|
+
switch (accessor.componentType) {
|
|
536
|
+
case 5120:
|
|
537
|
+
value = dataView.getInt8(componentOffset);
|
|
538
|
+
break;
|
|
539
|
+
case 5121:
|
|
540
|
+
value = dataView.getUint8(componentOffset);
|
|
541
|
+
break;
|
|
542
|
+
case 5122:
|
|
543
|
+
value = dataView.getInt16(componentOffset, true);
|
|
544
|
+
break;
|
|
545
|
+
case 5123:
|
|
546
|
+
value = dataView.getUint16(componentOffset, true);
|
|
547
|
+
break;
|
|
548
|
+
case 5125:
|
|
549
|
+
value = dataView.getUint32(componentOffset, true);
|
|
550
|
+
break;
|
|
551
|
+
case 5126:
|
|
552
|
+
value = dataView.getFloat32(componentOffset, true);
|
|
553
|
+
break;
|
|
554
|
+
default:
|
|
555
|
+
throw new Error(`Unknown componentType ${accessor.componentType}`);
|
|
556
|
+
}
|
|
557
|
+
out[outIndex++] = accessor.normalized ? compInfo.norm(value) : value;
|
|
699
558
|
}
|
|
700
559
|
}
|
|
701
560
|
return out;
|
|
702
561
|
}
|
|
703
|
-
function
|
|
704
|
-
|
|
705
|
-
const
|
|
706
|
-
if (
|
|
707
|
-
|
|
562
|
+
function readIndices(ctx, accessorIndex) {
|
|
563
|
+
const { gltf } = ctx;
|
|
564
|
+
const accessor = gltf.accessors?.[accessorIndex];
|
|
565
|
+
if (!accessor) throw new Error(`Invalid accessor index ${accessorIndex}`);
|
|
566
|
+
if (accessor.type !== "SCALAR")
|
|
567
|
+
throw new Error("Index accessor must be SCALAR");
|
|
568
|
+
const compInfo = COMPONENT_INFO[accessor.componentType];
|
|
569
|
+
if (!compInfo)
|
|
570
|
+
throw new Error(
|
|
571
|
+
`Unsupported index component type ${accessor.componentType}`
|
|
572
|
+
);
|
|
573
|
+
if (accessor.componentType !== 5121 && accessor.componentType !== 5123 && accessor.componentType !== 5125) {
|
|
574
|
+
throw new Error("Index componentType must be UNSIGNED_BYTE/SHORT/INT");
|
|
575
|
+
}
|
|
576
|
+
const byteOffset = accessor.byteOffset ?? 0;
|
|
577
|
+
const {
|
|
578
|
+
array: src,
|
|
579
|
+
stride: bufferViewStride,
|
|
580
|
+
byteOffsetInView
|
|
581
|
+
} = getBufferViewSlice(ctx, accessor.bufferView, byteOffset);
|
|
582
|
+
const stride = bufferViewStride || compInfo.size;
|
|
583
|
+
const count = accessor.count;
|
|
584
|
+
const absoluteByteOffset = byteOffsetInView;
|
|
585
|
+
const canUseTightView = stride === compInfo.size && absoluteByteOffset % compInfo.size === 0;
|
|
586
|
+
if (canUseTightView) {
|
|
587
|
+
const TypedArrayCtor = compInfo.array;
|
|
588
|
+
const typed = new TypedArrayCtor(src.buffer, src.byteOffset, count);
|
|
589
|
+
if (accessor.componentType === 5125) {
|
|
590
|
+
return new Uint32Array(typed.buffer, typed.byteOffset, typed.length);
|
|
591
|
+
}
|
|
592
|
+
return new Uint32Array(typed);
|
|
593
|
+
}
|
|
594
|
+
const dataView = new DataView(src.buffer, src.byteOffset, src.byteLength);
|
|
595
|
+
const out = new Uint32Array(count);
|
|
596
|
+
for (let i = 0; i < count; i++) {
|
|
597
|
+
const elementOffset = i * stride;
|
|
598
|
+
switch (accessor.componentType) {
|
|
599
|
+
case 5121:
|
|
600
|
+
out[i] = dataView.getUint8(elementOffset);
|
|
601
|
+
break;
|
|
602
|
+
case 5123:
|
|
603
|
+
out[i] = dataView.getUint16(elementOffset, true);
|
|
604
|
+
break;
|
|
605
|
+
case 5125:
|
|
606
|
+
out[i] = dataView.getUint32(elementOffset, true);
|
|
607
|
+
break;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
return out;
|
|
708
611
|
}
|
|
709
|
-
function
|
|
710
|
-
if (
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
return entryHref === meta.url;
|
|
715
|
-
} catch {
|
|
716
|
-
return false;
|
|
612
|
+
function nodeLocalMatrix(node) {
|
|
613
|
+
if (node.matrix) {
|
|
614
|
+
const m2 = mat43.create();
|
|
615
|
+
for (let i = 0; i < 16; i++) m2[i] = node.matrix[i];
|
|
616
|
+
return m2;
|
|
717
617
|
}
|
|
618
|
+
const translation = node.translation || [0, 0, 0];
|
|
619
|
+
const rotation = node.rotation || [0, 0, 0, 1];
|
|
620
|
+
const scale = node.scale || [1, 1, 1];
|
|
621
|
+
const m = mat43.create();
|
|
622
|
+
const tmp = mat43.create();
|
|
623
|
+
mat43.fromRotationTranslationScale(tmp, rotation, translation, scale);
|
|
624
|
+
mat43.copy(m, tmp);
|
|
625
|
+
return m;
|
|
718
626
|
}
|
|
719
|
-
|
|
720
|
-
const
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
627
|
+
function getMaterial(gltf, textures, images, materialIndex) {
|
|
628
|
+
const material = (gltf.materials || [])[materialIndex ?? -1] || {};
|
|
629
|
+
const pbr = material.pbrMetallicRoughness || {};
|
|
630
|
+
const factor = pbr.baseColorFactor || [1, 1, 1, 1];
|
|
631
|
+
let texImg = null;
|
|
632
|
+
if (pbr.baseColorTexture && Number.isInteger(pbr.baseColorTexture.index)) {
|
|
633
|
+
const tex = textures[pbr.baseColorTexture.index];
|
|
634
|
+
if (tex && Number.isInteger(tex.source)) {
|
|
635
|
+
texImg = images[tex.source] || null;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
return { baseColorFactor: factor, baseColorTexture: texImg };
|
|
639
|
+
}
|
|
640
|
+
function createSceneFromGLTF(gltf, resources) {
|
|
641
|
+
const ctx = { gltf, buffers: resources.buffers };
|
|
642
|
+
const textures = gltf.textures || [];
|
|
643
|
+
const nodes = gltf.nodes || [];
|
|
644
|
+
const meshes = gltf.meshes || [];
|
|
645
|
+
const scenes = gltf.scenes || [];
|
|
646
|
+
const sceneIndex = Number.isInteger(gltf.scene) ? gltf.scene : 0;
|
|
647
|
+
const scene = scenes[sceneIndex] || { nodes: [] };
|
|
648
|
+
const drawCalls = [];
|
|
649
|
+
function traverse(nodeIndex, parentMatrix) {
|
|
650
|
+
const node = nodes[nodeIndex];
|
|
651
|
+
if (!node) return;
|
|
652
|
+
const local = nodeLocalMatrix(node);
|
|
653
|
+
const world = mat43.create();
|
|
654
|
+
mat43.multiply(world, parentMatrix, local);
|
|
655
|
+
if (Number.isInteger(node.mesh)) {
|
|
656
|
+
const mesh = meshes[node.mesh];
|
|
657
|
+
if (mesh) {
|
|
658
|
+
for (const primitive of mesh.primitives || []) {
|
|
659
|
+
if (primitive.mode != null && primitive.mode !== 4) continue;
|
|
660
|
+
const posAcc = primitive.attributes?.POSITION;
|
|
661
|
+
if (posAcc == null) continue;
|
|
662
|
+
const normalsAcc = primitive.attributes?.NORMAL;
|
|
663
|
+
const uvAcc = primitive.attributes?.TEXCOORD_0;
|
|
664
|
+
const positions = readAccessorAsFloat32(ctx, posAcc);
|
|
665
|
+
const normals = normalsAcc != null ? readAccessorAsFloat32(ctx, normalsAcc) : null;
|
|
666
|
+
const uvs = uvAcc != null ? readAccessorAsFloat32(ctx, uvAcc) : null;
|
|
667
|
+
const indices = primitive.indices != null ? readIndices(ctx, primitive.indices) : null;
|
|
668
|
+
const material = getMaterial(
|
|
669
|
+
gltf,
|
|
670
|
+
textures,
|
|
671
|
+
resources.images,
|
|
672
|
+
primitive.material
|
|
673
|
+
);
|
|
674
|
+
drawCalls.push({
|
|
675
|
+
positions,
|
|
676
|
+
normals,
|
|
677
|
+
uvs,
|
|
678
|
+
indices,
|
|
679
|
+
model: world,
|
|
680
|
+
material
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
for (const childIndex of node.children || []) {
|
|
686
|
+
traverse(childIndex, world);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
const identity = mat43.create();
|
|
690
|
+
for (const rootIndex of scene.nodes || []) {
|
|
691
|
+
traverse(rootIndex, identity);
|
|
692
|
+
}
|
|
693
|
+
return { gltf, drawCalls };
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// lib/gltf/resourceUtils.ts
|
|
697
|
+
import { PassThrough } from "stream";
|
|
698
|
+
import PImage from "pureimage";
|
|
699
|
+
function bufferFromDataURI(uri) {
|
|
700
|
+
const match = uri.match(/^data:.*?;base64,(.*)$/);
|
|
701
|
+
if (!match) throw new Error(`Unsupported data URI: ${uri.slice(0, 64)}...`);
|
|
702
|
+
return Buffer.from(match[1], "base64");
|
|
703
|
+
}
|
|
704
|
+
function isPNG(filenameOrUri) {
|
|
705
|
+
return /\.png(\?|$)/i.test(filenameOrUri) || /image\/png/i.test(filenameOrUri);
|
|
706
|
+
}
|
|
707
|
+
function isJPG(filenameOrUri) {
|
|
708
|
+
return /(\.jpe?g(\?|$)|image\/jpe?g)/i.test(filenameOrUri);
|
|
709
|
+
}
|
|
710
|
+
function detectMimeTypeFromBuffer(buf, hint) {
|
|
711
|
+
if (hint) return hint;
|
|
712
|
+
if (buf.length >= 8 && buf[0] === 137 && buf[1] === 80 && buf[2] === 78 && buf[3] === 71 && buf[4] === 13 && buf[5] === 10 && buf[6] === 26 && buf[7] === 10)
|
|
713
|
+
return "image/png";
|
|
714
|
+
if (buf.length >= 2 && buf[0] === 255 && buf[1] === 216) return "image/jpeg";
|
|
715
|
+
return null;
|
|
716
|
+
}
|
|
717
|
+
function bufferToStream(buf) {
|
|
718
|
+
const stream = new PassThrough();
|
|
719
|
+
stream.end(Buffer.from(buf));
|
|
720
|
+
return stream;
|
|
721
|
+
}
|
|
722
|
+
async function decodeImageFromBuffer(buf, mimeType) {
|
|
723
|
+
const type = detectMimeTypeFromBuffer(buf, mimeType);
|
|
724
|
+
if (type === "image/png")
|
|
725
|
+
return PImage.decodePNGFromStream(bufferToStream(buf));
|
|
726
|
+
if (type === "image/jpeg" || type === "image/jpg")
|
|
727
|
+
return PImage.decodeJPEGFromStream(bufferToStream(buf));
|
|
728
|
+
throw new Error(`Unsupported embedded image mimeType: ${mimeType ?? "unknown"}`);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// lib/gltf/parseGLB.ts
|
|
732
|
+
var MAGIC = 1179937895;
|
|
733
|
+
var JSON_CHUNK_TYPE = 1313821514;
|
|
734
|
+
var BIN_CHUNK_TYPE = 5130562;
|
|
735
|
+
function parseGLB(arrayBuffer) {
|
|
736
|
+
const view = new DataView(arrayBuffer);
|
|
737
|
+
const magic = view.getUint32(0, true);
|
|
738
|
+
if (magic !== MAGIC) throw new Error("Invalid GLB header magic.");
|
|
739
|
+
const version = view.getUint32(4, true);
|
|
740
|
+
if (version !== 2) throw new Error(`Unsupported GLB version ${version}.`);
|
|
741
|
+
const length = view.getUint32(8, true);
|
|
742
|
+
const decoder = new TextDecoder();
|
|
743
|
+
let json = null;
|
|
744
|
+
let binaryChunk = null;
|
|
745
|
+
const chunks = [];
|
|
746
|
+
let offset = 12;
|
|
747
|
+
while (offset < length) {
|
|
748
|
+
if (offset + 8 > length) throw new Error("Truncated GLB chunk header.");
|
|
749
|
+
const chunkLength = view.getUint32(offset, true);
|
|
750
|
+
const chunkType = view.getUint32(offset + 4, true);
|
|
751
|
+
offset += 8;
|
|
752
|
+
if (offset + chunkLength > length) throw new Error("Truncated GLB chunk data.");
|
|
753
|
+
const chunkData = new Uint8Array(arrayBuffer, offset, chunkLength);
|
|
754
|
+
offset += chunkLength;
|
|
755
|
+
if (chunkType === JSON_CHUNK_TYPE) {
|
|
756
|
+
json = JSON.parse(decoder.decode(chunkData));
|
|
757
|
+
} else if (chunkType === BIN_CHUNK_TYPE) {
|
|
758
|
+
binaryChunk = chunkData;
|
|
759
|
+
} else {
|
|
760
|
+
chunks.push({ type: chunkType, data: chunkData });
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
if (!json) throw new Error("GLB file is missing JSON chunk.");
|
|
764
|
+
return { gltf: json, binaryChunk, chunks };
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// lib/gltf/loadGLTFWithResourcesFromURL.ts
|
|
768
|
+
function ensureFetch(fetchImpl) {
|
|
769
|
+
const impl = fetchImpl ?? globalThis.fetch;
|
|
770
|
+
if (!impl) throw new Error("Global fetch API is not available; provide fetchImpl.");
|
|
771
|
+
return impl;
|
|
772
|
+
}
|
|
773
|
+
async function fetchBinary(fetchImpl, url) {
|
|
774
|
+
const res = await fetchImpl(url);
|
|
775
|
+
if (!res.ok) {
|
|
776
|
+
throw new Error(`Failed to fetch ${url}: ${res.status} ${res.statusText}`);
|
|
777
|
+
}
|
|
778
|
+
const buf = await res.arrayBuffer();
|
|
779
|
+
return new Uint8Array(buf);
|
|
780
|
+
}
|
|
781
|
+
async function loadGLTFWithResourcesFromURL(url, options = {}) {
|
|
782
|
+
const fetchImpl = ensureFetch(options.fetchImpl);
|
|
783
|
+
const response = await fetchImpl(url);
|
|
784
|
+
if (!response.ok)
|
|
785
|
+
throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
|
|
786
|
+
const resolvedURL = new URL(response.url || url);
|
|
787
|
+
const baseURL = resolvedURL;
|
|
788
|
+
const sourceBuffer = await response.arrayBuffer();
|
|
789
|
+
const header = sourceBuffer.byteLength >= 4 ? new DataView(sourceBuffer, 0, 4) : null;
|
|
790
|
+
const isGLB = header?.getUint32(0, true) === 1179937895;
|
|
791
|
+
let gltf;
|
|
792
|
+
const totalBuffers = () => Array.isArray(gltf?.buffers) ? gltf.buffers.length : 0;
|
|
793
|
+
let buffers = [];
|
|
794
|
+
if (isGLB) {
|
|
795
|
+
const parsed = parseGLB(sourceBuffer);
|
|
796
|
+
gltf = parsed.gltf;
|
|
797
|
+
buffers = new Array(totalBuffers()).fill(null);
|
|
798
|
+
if (parsed.binaryChunk) buffers[0] = parsed.binaryChunk;
|
|
799
|
+
} else {
|
|
800
|
+
const text = new TextDecoder().decode(new Uint8Array(sourceBuffer));
|
|
801
|
+
gltf = JSON.parse(text);
|
|
802
|
+
buffers = new Array(totalBuffers()).fill(null);
|
|
803
|
+
}
|
|
804
|
+
async function resolveBuffer(index, entry) {
|
|
805
|
+
if (buffers[index]) return buffers[index];
|
|
806
|
+
if (entry?.uri) {
|
|
807
|
+
if (entry.uri.startsWith("data:")) {
|
|
808
|
+
const buf2 = bufferFromDataURI(entry.uri);
|
|
809
|
+
buffers[index] = buf2;
|
|
810
|
+
return buf2;
|
|
811
|
+
}
|
|
812
|
+
const resourceURL = new URL(
|
|
813
|
+
decodeURIComponent(entry.uri),
|
|
814
|
+
baseURL
|
|
815
|
+
).toString();
|
|
816
|
+
const buf = await fetchBinary(fetchImpl, resourceURL);
|
|
817
|
+
buffers[index] = buf;
|
|
818
|
+
return buf;
|
|
819
|
+
}
|
|
820
|
+
throw new Error(`Buffer ${index} is missing a uri and no GLB chunk was provided.`);
|
|
821
|
+
}
|
|
822
|
+
await Promise.all(
|
|
823
|
+
(gltf.buffers || []).map(async (entry, idx) => {
|
|
824
|
+
await resolveBuffer(idx, entry);
|
|
825
|
+
})
|
|
826
|
+
);
|
|
827
|
+
async function resolveImage(img) {
|
|
828
|
+
if (img.uri) {
|
|
829
|
+
if (img.uri.startsWith("data:")) {
|
|
830
|
+
const buf2 = bufferFromDataURI(img.uri);
|
|
831
|
+
return decodeImageFromBuffer(buf2, img.mimeType);
|
|
832
|
+
}
|
|
833
|
+
const resourceURL = new URL(
|
|
834
|
+
decodeURIComponent(img.uri),
|
|
835
|
+
baseURL
|
|
836
|
+
).toString();
|
|
837
|
+
const hintedMime = img.mimeType ? img.mimeType : isPNG(img.uri) ? "image/png" : isJPG(img.uri) ? "image/jpeg" : null;
|
|
838
|
+
const buf = await fetchBinary(fetchImpl, resourceURL);
|
|
839
|
+
return decodeImageFromBuffer(buf, hintedMime);
|
|
840
|
+
}
|
|
841
|
+
if (typeof img.bufferView === "number") {
|
|
842
|
+
const bufferView = gltf.bufferViews?.[img.bufferView];
|
|
843
|
+
if (!bufferView)
|
|
844
|
+
throw new Error(`Invalid image bufferView index ${img.bufferView}`);
|
|
845
|
+
const buffer = await resolveBuffer(bufferView.buffer, gltf.buffers?.[bufferView.buffer]);
|
|
846
|
+
if (!buffer)
|
|
847
|
+
throw new Error(`Missing buffer for image bufferView ${img.bufferView}`);
|
|
848
|
+
const byteOffset = bufferView.byteOffset ?? 0;
|
|
849
|
+
const byteLength = bufferView.byteLength;
|
|
850
|
+
if (typeof byteLength !== "number") {
|
|
851
|
+
throw new Error(
|
|
852
|
+
`bufferView ${img.bufferView} missing byteLength for image.`
|
|
853
|
+
);
|
|
854
|
+
}
|
|
855
|
+
const slice = buffer.subarray(byteOffset, byteOffset + byteLength);
|
|
856
|
+
return decodeImageFromBuffer(slice, img.mimeType);
|
|
857
|
+
}
|
|
858
|
+
throw new Error(
|
|
859
|
+
"images[*] entry missing uri or bufferView; unsupported in this loader."
|
|
724
860
|
);
|
|
725
|
-
process.exit(1);
|
|
726
861
|
}
|
|
727
|
-
const
|
|
728
|
-
|
|
729
|
-
const width = parseInt(typeof argv.w === "string" ? argv.w : `${DEFAULT_RENDER_OPTIONS.width}`, 10);
|
|
730
|
-
const height = parseInt(typeof argv.h === "string" ? argv.h : `${DEFAULT_RENDER_OPTIONS.height}`, 10);
|
|
731
|
-
const fov = parseFloat(typeof argv.fov === "string" ? argv.fov : `${DEFAULT_RENDER_OPTIONS.fov}`);
|
|
732
|
-
const ambient = clamp(
|
|
733
|
-
parseFloat(typeof argv.ambient === "string" ? argv.ambient : `${DEFAULT_RENDER_OPTIONS.ambient}`),
|
|
734
|
-
0,
|
|
735
|
-
1
|
|
862
|
+
const images = await Promise.all(
|
|
863
|
+
(gltf.images || []).map((img) => resolveImage(img))
|
|
736
864
|
);
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
865
|
+
return {
|
|
866
|
+
gltf,
|
|
867
|
+
resources: {
|
|
868
|
+
buffers,
|
|
869
|
+
images
|
|
870
|
+
}
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// lib/image/pureImageFactory.ts
|
|
875
|
+
import PImage2 from "pureimage";
|
|
876
|
+
var pureImageFactory = (width, height) => PImage2.make(width, height);
|
|
877
|
+
|
|
878
|
+
// lib/image/encodePNGToBuffer.ts
|
|
879
|
+
import { PassThrough as PassThrough2 } from "stream";
|
|
880
|
+
import PImage3 from "pureimage";
|
|
881
|
+
async function encodePNGToBuffer(image) {
|
|
882
|
+
const passThrough = new PassThrough2();
|
|
883
|
+
const chunks = [];
|
|
884
|
+
passThrough.on("data", (chunk) => {
|
|
885
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
886
|
+
});
|
|
887
|
+
const resultPromise = new Promise((resolve, reject) => {
|
|
888
|
+
passThrough.on("end", () => resolve(Buffer.concat(chunks)));
|
|
889
|
+
passThrough.on("error", reject);
|
|
752
890
|
});
|
|
753
|
-
|
|
891
|
+
await PImage3.encodePNGToStream(image, passThrough);
|
|
892
|
+
return await resultPromise;
|
|
754
893
|
}
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
894
|
+
|
|
895
|
+
// lib/render/renderGLTFToPNGBufferFromURL.ts
|
|
896
|
+
async function renderGLTFToPNGBufferFromURL(url, options = {}) {
|
|
897
|
+
const { fetchImpl, ...renderOptions } = options;
|
|
898
|
+
const { gltf, resources } = await loadGLTFWithResourcesFromURL(url, {
|
|
899
|
+
fetchImpl
|
|
759
900
|
});
|
|
901
|
+
const scene = createSceneFromGLTF(gltf, resources);
|
|
902
|
+
const { bitmap } = renderDrawCalls(
|
|
903
|
+
scene.drawCalls,
|
|
904
|
+
renderOptions,
|
|
905
|
+
pureImageFactory
|
|
906
|
+
);
|
|
907
|
+
return encodePNGToBuffer(bitmap);
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// lib/render/renderGLTFToPNGBufferFromGLBBuffer.ts
|
|
911
|
+
function normalizeToArrayBuffer(source) {
|
|
912
|
+
if (source instanceof ArrayBuffer) return source;
|
|
913
|
+
const { buffer, byteOffset, byteLength } = source;
|
|
914
|
+
return buffer.slice(byteOffset, byteOffset + byteLength);
|
|
915
|
+
}
|
|
916
|
+
async function renderGLTFToPNGBufferFromGLBBuffer(glb, options = {}) {
|
|
917
|
+
const arrayBuffer = normalizeToArrayBuffer(glb);
|
|
918
|
+
const { gltf, binaryChunk } = parseGLB(arrayBuffer);
|
|
919
|
+
const totalBuffers = Array.isArray(gltf.buffers) ? gltf.buffers.length : 0;
|
|
920
|
+
const buffers = new Array(totalBuffers);
|
|
921
|
+
for (let index = 0; index < totalBuffers; index++) {
|
|
922
|
+
const entry = gltf.buffers[index];
|
|
923
|
+
if (entry?.uri) {
|
|
924
|
+
if (!entry.uri.startsWith("data:")) {
|
|
925
|
+
throw new Error(
|
|
926
|
+
`Buffer ${index} references external resource ${entry.uri}. Use renderGLTFToPNGBufferFromURL for GLBs with external buffer URIs.`
|
|
927
|
+
);
|
|
928
|
+
}
|
|
929
|
+
buffers[index] = bufferFromDataURI(entry.uri);
|
|
930
|
+
continue;
|
|
931
|
+
}
|
|
932
|
+
if (!binaryChunk) {
|
|
933
|
+
throw new Error(`GLB is missing the binary chunk required for buffer ${index}.`);
|
|
934
|
+
}
|
|
935
|
+
buffers[index] = binaryChunk;
|
|
936
|
+
}
|
|
937
|
+
const resolveBufferViewSlice = (bufferViewIndex) => {
|
|
938
|
+
const bufferView = gltf.bufferViews?.[bufferViewIndex];
|
|
939
|
+
if (!bufferView) {
|
|
940
|
+
throw new Error(`Invalid bufferView index ${bufferViewIndex}.`);
|
|
941
|
+
}
|
|
942
|
+
const bufferIndex = bufferView.buffer;
|
|
943
|
+
const buffer = buffers[bufferIndex];
|
|
944
|
+
if (!buffer) {
|
|
945
|
+
throw new Error(
|
|
946
|
+
`Missing buffer data for bufferView ${bufferViewIndex} (buffer ${bufferIndex}).`
|
|
947
|
+
);
|
|
948
|
+
}
|
|
949
|
+
const byteOffset = bufferView.byteOffset ?? 0;
|
|
950
|
+
const byteLength = bufferView.byteLength;
|
|
951
|
+
if (typeof byteLength !== "number") {
|
|
952
|
+
throw new Error(`bufferView ${bufferViewIndex} is missing byteLength.`);
|
|
953
|
+
}
|
|
954
|
+
return buffer.subarray(byteOffset, byteOffset + byteLength);
|
|
955
|
+
};
|
|
956
|
+
const images = await Promise.all(
|
|
957
|
+
(gltf.images ?? []).map(async (image, imageIndex) => {
|
|
958
|
+
if (image.uri) {
|
|
959
|
+
if (!image.uri.startsWith("data:")) {
|
|
960
|
+
throw new Error(
|
|
961
|
+
`Image ${imageIndex} references external resource ${image.uri}. Embed textures or load via renderGLTFToPNGBufferFromURL.`
|
|
962
|
+
);
|
|
963
|
+
}
|
|
964
|
+
const data = bufferFromDataURI(image.uri);
|
|
965
|
+
return decodeImageFromBuffer(data, image.mimeType);
|
|
966
|
+
}
|
|
967
|
+
if (typeof image.bufferView === "number") {
|
|
968
|
+
const slice = resolveBufferViewSlice(image.bufferView);
|
|
969
|
+
return decodeImageFromBuffer(slice, image.mimeType);
|
|
970
|
+
}
|
|
971
|
+
throw new Error(
|
|
972
|
+
`Image ${imageIndex} must supply a data URI or bufferView for GLB rendering.`
|
|
973
|
+
);
|
|
974
|
+
})
|
|
975
|
+
);
|
|
976
|
+
const scene = createSceneFromGLTF(gltf, { buffers, images });
|
|
977
|
+
const { bitmap } = renderDrawCalls(scene.drawCalls, options, pureImageFactory);
|
|
978
|
+
return encodePNGToBuffer(bitmap);
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// lib/index.ts
|
|
982
|
+
function renderSceneFromGLTF(scene, options, imageFactory) {
|
|
983
|
+
return renderDrawCalls(scene.drawCalls, options, imageFactory);
|
|
760
984
|
}
|
|
761
985
|
export {
|
|
762
986
|
DEFAULT_LIGHT_DIR,
|
|
763
987
|
DEFAULT_RENDER_OPTIONS,
|
|
764
988
|
SoftwareRenderer,
|
|
989
|
+
bufferFromDataURI,
|
|
765
990
|
buildCamera,
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
991
|
+
computeSmoothNormals,
|
|
992
|
+
computeWorldAABB,
|
|
993
|
+
createSceneFromGLTF,
|
|
994
|
+
createUint8Bitmap,
|
|
995
|
+
decodeImageFromBuffer,
|
|
996
|
+
encodePNGToBuffer,
|
|
997
|
+
getDefaultRenderOptions,
|
|
998
|
+
isJPG,
|
|
999
|
+
isPNG,
|
|
1000
|
+
loadGLTFWithResourcesFromURL,
|
|
1001
|
+
pureImageFactory,
|
|
1002
|
+
renderDrawCalls,
|
|
1003
|
+
renderGLTFToPNGBufferFromGLBBuffer,
|
|
1004
|
+
renderGLTFToPNGBufferFromURL,
|
|
1005
|
+
renderSceneFromGLTF,
|
|
1006
|
+
resolveRenderOptions
|
|
769
1007
|
};
|