poppygl 0.0.26 → 0.0.27
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 +24 -0
- package/dist/index.d.ts +18 -2
- package/dist/index.js +136 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -67,6 +67,30 @@ const png = await renderGLTFToPNGFromGLB(glb, {
|
|
|
67
67
|
|
|
68
68
|
You can inspect the defaults via `getDefaultRenderOptions()` or reuse the internal merge logic with `resolveRenderOptions()`.
|
|
69
69
|
|
|
70
|
+
### Hidden edges for selected sub-models
|
|
71
|
+
|
|
72
|
+
Set `showHiddenEdges` in a glTF node's `extras` to render that node with solid
|
|
73
|
+
visible edges and faint dashed edges where its geometry is occluded:
|
|
74
|
+
|
|
75
|
+
```json
|
|
76
|
+
{
|
|
77
|
+
"nodes": [
|
|
78
|
+
{ "name": "Housing", "mesh": 0 },
|
|
79
|
+
{
|
|
80
|
+
"name": "InternalBracket",
|
|
81
|
+
"mesh": 1,
|
|
82
|
+
"extras": { "showHiddenEdges": true }
|
|
83
|
+
}
|
|
84
|
+
]
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The namespaced form `{ "extras": { "poppygl": { "showHiddenEdges": true } } }`
|
|
89
|
+
is also supported. The setting can be placed on a mesh or primitive; primitive
|
|
90
|
+
extras take precedence over node extras, which take precedence over mesh extras.
|
|
91
|
+
PoppyGL derives boundary and crease edges from triangle geometry, so no line
|
|
92
|
+
primitives need to be added to the asset.
|
|
93
|
+
|
|
70
94
|
## If you already have the GLTF JSON
|
|
71
95
|
|
|
72
96
|
When the GLTF JSON object is already in memory (for example, bundled with your app), skip the network loader and supply resources directly:
|
package/dist/index.d.ts
CHANGED
|
@@ -25,6 +25,7 @@ interface DrawCall {
|
|
|
25
25
|
material: Material;
|
|
26
26
|
colors?: Float32Array | null;
|
|
27
27
|
mode?: number;
|
|
28
|
+
showHiddenEdges?: boolean;
|
|
28
29
|
}
|
|
29
30
|
interface GridOptions {
|
|
30
31
|
size?: number | readonly [number, number, number];
|
|
@@ -114,6 +115,13 @@ declare function resolveRenderOptions(options?: RenderOptionsInput): RenderOptio
|
|
|
114
115
|
|
|
115
116
|
declare function createGrid(options?: GridOptions): DrawCall;
|
|
116
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Builds a line draw call from the boundary and crease edges of a triangle
|
|
120
|
+
* mesh. Vertices are welded by position so glTF meshes that duplicate vertices
|
|
121
|
+
* for normals or UV seams do not produce coplanar triangulation lines.
|
|
122
|
+
*/
|
|
123
|
+
declare function createEdgeDrawCall(mesh: DrawCall, creaseAngleDegrees?: number): DrawCall | null;
|
|
124
|
+
|
|
117
125
|
/**
|
|
118
126
|
* Creates an infinite grid that matches the appearance of the 3d-viewer reference.
|
|
119
127
|
* This grid uses a special rendering approach with procedural generation and fading.
|
|
@@ -163,6 +171,14 @@ interface LightSettings {
|
|
|
163
171
|
dir: readonly [number, number, number];
|
|
164
172
|
ambient: number;
|
|
165
173
|
}
|
|
174
|
+
interface LineRenderOptions {
|
|
175
|
+
depthCompare?: "less" | "less-equal" | "greater";
|
|
176
|
+
depthWrite?: boolean;
|
|
177
|
+
depthBias?: number;
|
|
178
|
+
dashed?: boolean;
|
|
179
|
+
dashLength?: number;
|
|
180
|
+
opacity?: number;
|
|
181
|
+
}
|
|
166
182
|
declare class SoftwareRenderer {
|
|
167
183
|
readonly width: number;
|
|
168
184
|
readonly height: number;
|
|
@@ -172,7 +188,7 @@ declare class SoftwareRenderer {
|
|
|
172
188
|
get buffer(): Uint8Array<ArrayBufferLike> | Uint8ClampedArray<ArrayBufferLike>;
|
|
173
189
|
clear(colorRGBA?: [number, number, number, number]): void;
|
|
174
190
|
setPixel(x: number, y: number, r: number, g: number, b: number, a: number): void;
|
|
175
|
-
drawLines(mesh: DrawCall, camera: Camera, gammaOut?: boolean): void;
|
|
191
|
+
drawLines(mesh: DrawCall, camera: Camera, gammaOut?: boolean, options?: LineRenderOptions): void;
|
|
176
192
|
sampleTextureNearest(img: BitmapLike | null, u: number, v: number): MutableRGBA;
|
|
177
193
|
perspInterp(attrs: number[][], invWs: number[], lambdas: number[]): number[];
|
|
178
194
|
drawMesh(mesh: DrawCall, camera: Camera, light: LightSettings, material: Material, cullBackFaces?: boolean, gammaOut?: boolean): void;
|
|
@@ -204,4 +220,4 @@ declare function renderGLTFToPNGFromGLB(glb: ArrayBuffer | Uint8Array, options?:
|
|
|
204
220
|
|
|
205
221
|
declare function renderSceneFromGLTF(scene: GLTFScene, options?: RenderOptionsInput, imageFactory?: ImageFactory): RenderResult;
|
|
206
222
|
|
|
207
|
-
export { type BitmapLike, type Camera, type CameraRotation, type CameraUp, DEFAULT_LIGHT_DIR, DEFAULT_RENDER_OPTIONS, type DebugPoint, type DrawCall, type FetchLike, type GLTFResources, type GLTFScene, type GridOptions, type ImageFactory, type LightSettings, type LoadGLTFWithResourcesFromURLOptions, type Material, type MutableRGBA, type RGBA, type RenderGLTFToPNGFromGLBOptions, type RenderGLTFToPNGFromURLOptions, type RenderOptions, type RenderOptionsInput, type RenderResult, SoftwareRenderer, bufferFromDataURI, buildCamera, computeSmoothNormals, computeWorldAABB, createGrid, createInfiniteGrid, createSceneFromGLTF, createUint8Bitmap, decodeImageFromBuffer, drawInfiniteGrid, encodePNG, getDefaultRenderOptions, hexToRgb, isJPG, isPNG, loadGLTFWithResourcesFromURL, renderDrawCalls, renderGLTFToPNGFromGLB, renderGLTFToPNGFromURL, renderSceneFromGLTF, resolveRenderOptions };
|
|
223
|
+
export { type BitmapLike, type Camera, type CameraRotation, type CameraUp, DEFAULT_LIGHT_DIR, DEFAULT_RENDER_OPTIONS, type DebugPoint, type DrawCall, type FetchLike, type GLTFResources, type GLTFScene, type GridOptions, type ImageFactory, type LightSettings, type LineRenderOptions, type LoadGLTFWithResourcesFromURLOptions, type Material, type MutableRGBA, type RGBA, type RenderGLTFToPNGFromGLBOptions, type RenderGLTFToPNGFromURLOptions, type RenderOptions, type RenderOptionsInput, type RenderResult, SoftwareRenderer, bufferFromDataURI, buildCamera, computeSmoothNormals, computeWorldAABB, createEdgeDrawCall, createGrid, createInfiniteGrid, createSceneFromGLTF, createUint8Bitmap, decodeImageFromBuffer, drawInfiniteGrid, encodePNG, getDefaultRenderOptions, hexToRgb, isJPG, isPNG, loadGLTFWithResourcesFromURL, renderDrawCalls, renderGLTFToPNGFromGLB, renderGLTFToPNGFromURL, renderSceneFromGLTF, resolveRenderOptions };
|
package/dist/index.js
CHANGED
|
@@ -249,6 +249,87 @@ function createGrid(options = {}) {
|
|
|
249
249
|
};
|
|
250
250
|
}
|
|
251
251
|
|
|
252
|
+
// lib/gltf/createEdgeDrawCall.ts
|
|
253
|
+
var DEFAULT_CREASE_ANGLE_DEGREES = 30;
|
|
254
|
+
function positionKey(positions, index) {
|
|
255
|
+
return `${positions[index * 3 + 0]},${positions[index * 3 + 1]},${positions[index * 3 + 2]}`;
|
|
256
|
+
}
|
|
257
|
+
function triangleNormal(positions, i0, i1, i2) {
|
|
258
|
+
const ax = positions[i1 * 3 + 0] - positions[i0 * 3 + 0];
|
|
259
|
+
const ay = positions[i1 * 3 + 1] - positions[i0 * 3 + 1];
|
|
260
|
+
const az = positions[i1 * 3 + 2] - positions[i0 * 3 + 2];
|
|
261
|
+
const bx = positions[i2 * 3 + 0] - positions[i0 * 3 + 0];
|
|
262
|
+
const by = positions[i2 * 3 + 1] - positions[i0 * 3 + 1];
|
|
263
|
+
const bz = positions[i2 * 3 + 2] - positions[i0 * 3 + 2];
|
|
264
|
+
const nx = ay * bz - az * by;
|
|
265
|
+
const ny = az * bx - ax * bz;
|
|
266
|
+
const nz = ax * by - ay * bx;
|
|
267
|
+
const length = Math.hypot(nx, ny, nz);
|
|
268
|
+
if (length === 0) return null;
|
|
269
|
+
return [nx / length, ny / length, nz / length];
|
|
270
|
+
}
|
|
271
|
+
function addEdge(edges, positions, i0, i1, normal) {
|
|
272
|
+
const key0 = positionKey(positions, i0);
|
|
273
|
+
const key1 = positionKey(positions, i1);
|
|
274
|
+
const isForward = key0 < key1;
|
|
275
|
+
const key = isForward ? `${key0}|${key1}` : `${key1}|${key0}`;
|
|
276
|
+
const indices = isForward ? [i0, i1] : [i1, i0];
|
|
277
|
+
const existing = edges.get(key);
|
|
278
|
+
if (existing) {
|
|
279
|
+
if (normal) existing.normals.push(normal);
|
|
280
|
+
} else {
|
|
281
|
+
edges.set(key, { indices, normals: normal ? [normal] : [] });
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
function hasCrease(normals, minimumDot) {
|
|
285
|
+
if (normals.length < 2) return true;
|
|
286
|
+
for (let i = 0; i < normals.length; i++) {
|
|
287
|
+
for (let j = i + 1; j < normals.length; j++) {
|
|
288
|
+
const a = normals[i];
|
|
289
|
+
const b = normals[j];
|
|
290
|
+
const dot = a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
291
|
+
if (dot < minimumDot) return true;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
function createEdgeDrawCall(mesh, creaseAngleDegrees = DEFAULT_CREASE_ANGLE_DEGREES) {
|
|
297
|
+
if (mesh.mode != null && mesh.mode !== 4) return null;
|
|
298
|
+
const vertexCount = Math.floor(mesh.positions.length / 3);
|
|
299
|
+
const triangleIndices = mesh.indices ?? Uint32Array.from({ length: vertexCount }, (_, i) => i);
|
|
300
|
+
if (triangleIndices.length < 3) return null;
|
|
301
|
+
const edges = /* @__PURE__ */ new Map();
|
|
302
|
+
for (let i = 0; i + 2 < triangleIndices.length; i += 3) {
|
|
303
|
+
const i0 = triangleIndices[i];
|
|
304
|
+
const i1 = triangleIndices[i + 1];
|
|
305
|
+
const i2 = triangleIndices[i + 2];
|
|
306
|
+
const normal = triangleNormal(mesh.positions, i0, i1, i2);
|
|
307
|
+
addEdge(edges, mesh.positions, i0, i1, normal);
|
|
308
|
+
addEdge(edges, mesh.positions, i1, i2, normal);
|
|
309
|
+
addEdge(edges, mesh.positions, i2, i0, normal);
|
|
310
|
+
}
|
|
311
|
+
const minimumDot = Math.cos(creaseAngleDegrees * Math.PI / 180);
|
|
312
|
+
const edgeIndices = [];
|
|
313
|
+
for (const edge2 of edges.values()) {
|
|
314
|
+
if (!hasCrease(edge2.normals, minimumDot)) continue;
|
|
315
|
+
edgeIndices.push(edge2.indices[0], edge2.indices[1]);
|
|
316
|
+
}
|
|
317
|
+
if (edgeIndices.length === 0) return null;
|
|
318
|
+
return {
|
|
319
|
+
positions: mesh.positions,
|
|
320
|
+
normals: null,
|
|
321
|
+
uvs: null,
|
|
322
|
+
indices: new Uint32Array(edgeIndices),
|
|
323
|
+
model: mesh.model,
|
|
324
|
+
material: {
|
|
325
|
+
baseColorFactor: [0, 0, 0, 1],
|
|
326
|
+
baseColorTexture: null,
|
|
327
|
+
alphaMode: "BLEND"
|
|
328
|
+
},
|
|
329
|
+
mode: 1
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
252
333
|
// lib/image/createUint8Bitmap.ts
|
|
253
334
|
var createUint8Bitmap = (width, height) => ({
|
|
254
335
|
width,
|
|
@@ -459,7 +540,7 @@ var SoftwareRenderer = class {
|
|
|
459
540
|
this.buffer[idx + 2] = b;
|
|
460
541
|
this.buffer[idx + 3] = a;
|
|
461
542
|
}
|
|
462
|
-
drawLines(mesh, camera, gammaOut = true) {
|
|
543
|
+
drawLines(mesh, camera, gammaOut = true, options = {}) {
|
|
463
544
|
const { positions, indices, model, material } = mesh;
|
|
464
545
|
if (!indices) return;
|
|
465
546
|
const view = camera.view;
|
|
@@ -507,7 +588,12 @@ var SoftwareRenderer = class {
|
|
|
507
588
|
const r255 = r * 255 | 0;
|
|
508
589
|
const g255 = g * 255 | 0;
|
|
509
590
|
const b255 = b * 255 | 0;
|
|
510
|
-
const
|
|
591
|
+
const opacity = clamp(options.opacity ?? 1, 0, 1);
|
|
592
|
+
const sourceAlpha = clamp(a, 0, 1) * opacity;
|
|
593
|
+
const depthCompare = options.depthCompare ?? "less";
|
|
594
|
+
const depthWrite = options.depthWrite ?? true;
|
|
595
|
+
const depthBias = options.depthBias ?? 0;
|
|
596
|
+
const dashLength = Math.max(1, options.dashLength ?? 4);
|
|
511
597
|
for (let i = 0; i < indices.length; i += 2) {
|
|
512
598
|
const i0 = indices[i + 0];
|
|
513
599
|
const i1 = indices[i + 1];
|
|
@@ -534,13 +620,28 @@ var SoftwareRenderer = class {
|
|
|
534
620
|
let y = y0;
|
|
535
621
|
let z = z0_01;
|
|
536
622
|
for (let k = 0; k <= steps; k++) {
|
|
623
|
+
if (options.dashed && Math.floor(k / dashLength) % 2 === 1) {
|
|
624
|
+
x += xinc;
|
|
625
|
+
y += yinc;
|
|
626
|
+
z += zinc;
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
537
629
|
const xi = Math.round(x);
|
|
538
630
|
const yi = Math.round(y);
|
|
539
631
|
if (xi >= 0 && xi < this.width && yi >= 0 && yi < this.height && z >= 0 && z <= 1) {
|
|
540
632
|
const di = yi * this.width + xi;
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
633
|
+
const currentDepth = this.depth[di];
|
|
634
|
+
const passesDepth = depthCompare === "greater" ? z > currentDepth + depthBias : depthCompare === "less-equal" ? z <= currentDepth + depthBias : z < currentDepth - depthBias;
|
|
635
|
+
if (passesDepth) {
|
|
636
|
+
if (depthWrite) this.depth[di] = z;
|
|
637
|
+
const pixelIndex = di * 4;
|
|
638
|
+
const destinationAlpha = this.buffer[pixelIndex + 3] / 255;
|
|
639
|
+
const oneMinusSourceAlpha = 1 - sourceAlpha;
|
|
640
|
+
const outAlpha = sourceAlpha + destinationAlpha * oneMinusSourceAlpha;
|
|
641
|
+
const outR = outAlpha === 0 ? 0 : (r255 * sourceAlpha + this.buffer[pixelIndex + 0] * destinationAlpha * oneMinusSourceAlpha) / outAlpha;
|
|
642
|
+
const outG = outAlpha === 0 ? 0 : (g255 * sourceAlpha + this.buffer[pixelIndex + 1] * destinationAlpha * oneMinusSourceAlpha) / outAlpha;
|
|
643
|
+
const outB = outAlpha === 0 ? 0 : (b255 * sourceAlpha + this.buffer[pixelIndex + 2] * destinationAlpha * oneMinusSourceAlpha) / outAlpha;
|
|
644
|
+
this.setPixel(xi, yi, outR, outG, outB, outAlpha * 255);
|
|
544
645
|
}
|
|
545
646
|
}
|
|
546
647
|
x += xinc;
|
|
@@ -1249,6 +1350,24 @@ function renderDrawCalls(drawCalls, optionsInput = {}, imageFactory = createUint
|
|
|
1249
1350
|
});
|
|
1250
1351
|
}
|
|
1251
1352
|
renderGroup(blendDraws);
|
|
1353
|
+
for (const dc of drawCalls) {
|
|
1354
|
+
if (!dc.showHiddenEdges) continue;
|
|
1355
|
+
const edgeDrawCall = createEdgeDrawCall(dc);
|
|
1356
|
+
if (!edgeDrawCall) continue;
|
|
1357
|
+
renderer.drawLines(edgeDrawCall, camera, options.gamma, {
|
|
1358
|
+
depthCompare: "less-equal",
|
|
1359
|
+
depthWrite: false,
|
|
1360
|
+
depthBias: 1e-4
|
|
1361
|
+
});
|
|
1362
|
+
renderer.drawLines(edgeDrawCall, camera, options.gamma, {
|
|
1363
|
+
depthCompare: "greater",
|
|
1364
|
+
depthWrite: false,
|
|
1365
|
+
depthBias: 1e-4,
|
|
1366
|
+
dashed: true,
|
|
1367
|
+
dashLength: 4 * options.supersampling,
|
|
1368
|
+
opacity: 0.35
|
|
1369
|
+
});
|
|
1370
|
+
}
|
|
1252
1371
|
if (options.debugPoints?.length) {
|
|
1253
1372
|
drawDebugPoints(
|
|
1254
1373
|
renderer,
|
|
@@ -1363,6 +1482,15 @@ var NUM_COMPONENTS = {
|
|
|
1363
1482
|
MAT3: 9,
|
|
1364
1483
|
MAT4: 16
|
|
1365
1484
|
};
|
|
1485
|
+
function readShowHiddenEdges(extras) {
|
|
1486
|
+
if (typeof extras?.poppygl?.showHiddenEdges === "boolean") {
|
|
1487
|
+
return extras.poppygl.showHiddenEdges;
|
|
1488
|
+
}
|
|
1489
|
+
if (typeof extras?.showHiddenEdges === "boolean") {
|
|
1490
|
+
return extras.showHiddenEdges;
|
|
1491
|
+
}
|
|
1492
|
+
return void 0;
|
|
1493
|
+
}
|
|
1366
1494
|
function getBufferViewSlice(ctx, bufferViewIndex, byteOffset) {
|
|
1367
1495
|
const { gltf, buffers } = ctx;
|
|
1368
1496
|
const bufferView = gltf.bufferViews?.[bufferViewIndex];
|
|
@@ -1596,7 +1724,8 @@ function createSceneFromGLTF(gltf, resources) {
|
|
|
1596
1724
|
indices,
|
|
1597
1725
|
model: world,
|
|
1598
1726
|
material,
|
|
1599
|
-
colors
|
|
1727
|
+
colors,
|
|
1728
|
+
showHiddenEdges: readShowHiddenEdges(primitive.extras) ?? readShowHiddenEdges(node.extras) ?? readShowHiddenEdges(mesh.extras) ?? false
|
|
1600
1729
|
});
|
|
1601
1730
|
}
|
|
1602
1731
|
}
|
|
@@ -1954,6 +2083,7 @@ export {
|
|
|
1954
2083
|
buildCamera,
|
|
1955
2084
|
computeSmoothNormals,
|
|
1956
2085
|
computeWorldAABB,
|
|
2086
|
+
createEdgeDrawCall,
|
|
1957
2087
|
createGrid,
|
|
1958
2088
|
createInfiniteGrid,
|
|
1959
2089
|
createSceneFromGLTF,
|