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