poppygl 0.0.34 → 0.0.35
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 +33 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +201 -26
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -133,10 +133,43 @@ const png = await renderGLTFToPNGFromGLB(glb, {
|
|
|
133
133
|
- `cull`: enable back-face culling (default `true`).
|
|
134
134
|
- `backgroundColor`: a hex string such as `"#e5e7eb"`, or an RGB tuple with values from 0 to 1.
|
|
135
135
|
- `grid`: show a ground grid (default `false`), or provide grid options.
|
|
136
|
+
- `showEdges`: draw visible boundary and crease edges on all triangle meshes (default `false`).
|
|
137
|
+
- `showHiddenEdges`: draw visible edges plus faint dashed occluded edges on all meshes. Omit it or use `null` to preserve each model's flag; `false` disables those flags.
|
|
138
|
+
- `edgeWidth`: width of both visible and hidden mesh edges in output pixels (default `1`).
|
|
139
|
+
- `lineWidth`: width of glTF `LINES` and finite grid lines in output pixels (default `1`).
|
|
140
|
+
- `hiddenEdgeOpacity`: opacity of dashed occluded mesh edges, from `0` to `1` (default `0.35`).
|
|
136
141
|
- `fetchImpl`: optional override for resource loading (must match the `fetch` signature).
|
|
137
142
|
|
|
138
143
|
You can inspect the defaults via `getDefaultRenderOptions()` or reuse the internal merge logic with `resolveRenderOptions()`.
|
|
139
144
|
|
|
145
|
+
### Thicker edges and hidden geometry
|
|
146
|
+
|
|
147
|
+
Enable edges directly when rendering an ordinary glTF or GLB, without editing its metadata:
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
const png = await renderGLTFToPNGFromGLB(glb, {
|
|
151
|
+
showHiddenEdges: true,
|
|
152
|
+
edgeWidth: 2.5,
|
|
153
|
+
hiddenEdgeOpacity: 0.35,
|
|
154
|
+
supersampling: 2,
|
|
155
|
+
backgroundColor: "#ffffff",
|
|
156
|
+
})
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Use `showEdges: true` for solid visible edges alone. Hidden segments use the
|
|
160
|
+
scene depth buffer and appear dashed; edge overlays never replace geometry depth.
|
|
161
|
+
Boundary and crease edges are extracted from the original triangle geometry,
|
|
162
|
+
excluding coplanar triangulation diagonals. Smooth curved silhouettes are not
|
|
163
|
+
additional extracted edges.
|
|
164
|
+
|
|
165
|
+
Widths stay constant in the final image when supersampling changes. Positive,
|
|
166
|
+
finite fractional widths are supported; invalid widths fall back to one pixel.
|
|
167
|
+
Wider strokes use antialiased screen-space coverage with round caps in the
|
|
168
|
+
software renderer. They are clipped before the perspective divide and remain
|
|
169
|
+
depth tested across their full width. No GPU or shader runtime is required.
|
|
170
|
+
`lineWidth` also applies to ordinary glTF line primitives and finite grid lines;
|
|
171
|
+
the procedural infinite grid keeps its existing independent styling.
|
|
172
|
+
|
|
140
173
|
### Hidden edges for selected sub-models
|
|
141
174
|
|
|
142
175
|
Set `showHiddenEdges` in a glTF node's `extras` to render that node with solid
|
package/dist/index.d.ts
CHANGED
|
@@ -31,6 +31,8 @@ interface DrawCall {
|
|
|
31
31
|
colors?: Float32Array | null;
|
|
32
32
|
mode?: number;
|
|
33
33
|
showHiddenEdges?: boolean;
|
|
34
|
+
/** Source triangle third vertices for each extracted edge segment. */
|
|
35
|
+
edgeAdjacentVertices?: number[][];
|
|
34
36
|
}
|
|
35
37
|
interface GridOptions {
|
|
36
38
|
size?: number | readonly [number, number, number];
|
|
@@ -100,6 +102,16 @@ interface RenderOptions {
|
|
|
100
102
|
debugFontSize?: number | null;
|
|
101
103
|
debugPointColor?: readonly [number, number, number] | null;
|
|
102
104
|
debugLabelColor?: readonly [number, number, number] | null;
|
|
105
|
+
/** Draw visible boundary and crease edges on every triangle mesh. */
|
|
106
|
+
showEdges: boolean;
|
|
107
|
+
/** Override model showHiddenEdges flags; null preserves each model's flag. */
|
|
108
|
+
showHiddenEdges: boolean | null;
|
|
109
|
+
/** Width of visible and hidden mesh edges in output pixels. */
|
|
110
|
+
edgeWidth: number;
|
|
111
|
+
/** Width of glTF LINES and finite grid lines in output pixels. */
|
|
112
|
+
lineWidth: number;
|
|
113
|
+
/** Opacity of dashed, occluded mesh edges, from 0 to 1. */
|
|
114
|
+
hiddenEdgeOpacity: number;
|
|
103
115
|
}
|
|
104
116
|
type RenderOptionsInput = Partial<RenderOptions>;
|
|
105
117
|
declare const DEFAULT_RENDER_OPTIONS: RenderOptions;
|
|
@@ -195,6 +207,8 @@ interface LightSettings {
|
|
|
195
207
|
ambient: number;
|
|
196
208
|
}
|
|
197
209
|
interface LineRenderOptions {
|
|
210
|
+
/** Stroke width in this renderer's raster pixels. Defaults to one. */
|
|
211
|
+
width?: number;
|
|
198
212
|
depthCompare?: "less" | "less-equal" | "greater";
|
|
199
213
|
depthWrite?: boolean;
|
|
200
214
|
depthBias?: number;
|
package/dist/index.js
CHANGED
|
@@ -271,7 +271,7 @@ function triangleNormal(positions, i0, i1, i2) {
|
|
|
271
271
|
if (length === 0) return null;
|
|
272
272
|
return [nx / length, ny / length, nz / length];
|
|
273
273
|
}
|
|
274
|
-
function addEdge(edges, positions, i0, i1, normal) {
|
|
274
|
+
function addEdge(edges, positions, i0, i1, thirdVertex, normal) {
|
|
275
275
|
const key0 = positionKey(positions, i0);
|
|
276
276
|
const key1 = positionKey(positions, i1);
|
|
277
277
|
const isForward = key0 < key1;
|
|
@@ -280,8 +280,13 @@ function addEdge(edges, positions, i0, i1, normal) {
|
|
|
280
280
|
const existing = edges.get(key);
|
|
281
281
|
if (existing) {
|
|
282
282
|
if (normal) existing.normals.push(normal);
|
|
283
|
+
existing.adjacentVertices.push(thirdVertex);
|
|
283
284
|
} else {
|
|
284
|
-
edges.set(key, {
|
|
285
|
+
edges.set(key, {
|
|
286
|
+
indices,
|
|
287
|
+
normals: normal ? [normal] : [],
|
|
288
|
+
adjacentVertices: [thirdVertex]
|
|
289
|
+
});
|
|
285
290
|
}
|
|
286
291
|
}
|
|
287
292
|
function hasCrease(normals, minimumDot) {
|
|
@@ -307,15 +312,17 @@ function createEdgeDrawCall(mesh, creaseAngleDegrees = DEFAULT_CREASE_ANGLE_DEGR
|
|
|
307
312
|
const i1 = triangleIndices[i + 1];
|
|
308
313
|
const i2 = triangleIndices[i + 2];
|
|
309
314
|
const normal = triangleNormal(mesh.positions, i0, i1, i2);
|
|
310
|
-
addEdge(edges, mesh.positions, i0, i1, normal);
|
|
311
|
-
addEdge(edges, mesh.positions, i1, i2, normal);
|
|
312
|
-
addEdge(edges, mesh.positions, i2, i0, normal);
|
|
315
|
+
addEdge(edges, mesh.positions, i0, i1, i2, normal);
|
|
316
|
+
addEdge(edges, mesh.positions, i1, i2, i0, normal);
|
|
317
|
+
addEdge(edges, mesh.positions, i2, i0, i1, normal);
|
|
313
318
|
}
|
|
314
319
|
const minimumDot = Math.cos(creaseAngleDegrees * Math.PI / 180);
|
|
315
320
|
const edgeIndices = [];
|
|
321
|
+
const edgeAdjacentVertices = [];
|
|
316
322
|
for (const edge2 of edges.values()) {
|
|
317
323
|
if (!hasCrease(edge2.normals, minimumDot)) continue;
|
|
318
324
|
edgeIndices.push(edge2.indices[0], edge2.indices[1]);
|
|
325
|
+
edgeAdjacentVertices.push(edge2.adjacentVertices);
|
|
319
326
|
}
|
|
320
327
|
if (edgeIndices.length === 0) return null;
|
|
321
328
|
return {
|
|
@@ -329,7 +336,8 @@ function createEdgeDrawCall(mesh, creaseAngleDegrees = DEFAULT_CREASE_ANGLE_DEGR
|
|
|
329
336
|
baseColorTexture: null,
|
|
330
337
|
alphaMode: "BLEND"
|
|
331
338
|
},
|
|
332
|
-
mode: 1
|
|
339
|
+
mode: 1,
|
|
340
|
+
edgeAdjacentVertices
|
|
333
341
|
};
|
|
334
342
|
}
|
|
335
343
|
|
|
@@ -490,7 +498,12 @@ var DEFAULT_RENDER_OPTIONS = {
|
|
|
490
498
|
debugPoints: null,
|
|
491
499
|
debugFontSize: null,
|
|
492
500
|
debugPointColor: null,
|
|
493
|
-
debugLabelColor: null
|
|
501
|
+
debugLabelColor: null,
|
|
502
|
+
showEdges: false,
|
|
503
|
+
showHiddenEdges: null,
|
|
504
|
+
edgeWidth: 1,
|
|
505
|
+
lineWidth: 1,
|
|
506
|
+
hiddenEdgeOpacity: 0.35
|
|
494
507
|
};
|
|
495
508
|
function getDefaultRenderOptions() {
|
|
496
509
|
return { ...DEFAULT_RENDER_OPTIONS };
|
|
@@ -679,6 +692,98 @@ var GeometryDenoiser = class {
|
|
|
679
692
|
}
|
|
680
693
|
};
|
|
681
694
|
|
|
695
|
+
// lib/render/rasterizeWideLine.ts
|
|
696
|
+
function clipLine(start, end, marginX, marginY) {
|
|
697
|
+
if (![...start, ...end].every(Number.isFinite)) return null;
|
|
698
|
+
const distances = (p) => [
|
|
699
|
+
p[3] * (1 + marginX) + p[0],
|
|
700
|
+
p[3] * (1 + marginX) - p[0],
|
|
701
|
+
p[3] * (1 + marginY) + p[1],
|
|
702
|
+
p[3] * (1 + marginY) - p[1],
|
|
703
|
+
p[3] + p[2],
|
|
704
|
+
p[3] - p[2]
|
|
705
|
+
];
|
|
706
|
+
const a = distances(start);
|
|
707
|
+
const b = distances(end);
|
|
708
|
+
let from = 0;
|
|
709
|
+
let to = 1;
|
|
710
|
+
for (let plane = 0; plane < 6; plane++) {
|
|
711
|
+
if (a[plane] < 0 && b[plane] < 0) return null;
|
|
712
|
+
if (a[plane] < 0 !== b[plane] < 0) {
|
|
713
|
+
const t = a[plane] / (a[plane] - b[plane]);
|
|
714
|
+
if (a[plane] < 0) from = Math.max(from, t);
|
|
715
|
+
else to = Math.min(to, t);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
if (from > to) return null;
|
|
719
|
+
return [from, to].map(
|
|
720
|
+
(t) => Array.from(start, (value, axis) => value + t * (end[axis] - value))
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
function rasterizeWideLine(start, end, width, height, strokeWidth, dashed, dashLength, fragment) {
|
|
724
|
+
const radius = strokeWidth / 2;
|
|
725
|
+
const padding = radius + 0.5;
|
|
726
|
+
const clipped = clipLine(
|
|
727
|
+
start,
|
|
728
|
+
end,
|
|
729
|
+
2 * padding / Math.max(1, width - 1),
|
|
730
|
+
2 * padding / Math.max(1, height - 1)
|
|
731
|
+
);
|
|
732
|
+
if (!clipped) return;
|
|
733
|
+
const project = (p) => [
|
|
734
|
+
(p[0] / p[3] * 0.5 + 0.5) * (width - 1),
|
|
735
|
+
(0.5 - p[1] / p[3] * 0.5) * (height - 1),
|
|
736
|
+
// Clipping guarantees this range; absorb near/far-plane roundoff so an
|
|
737
|
+
// endpoint's round cap is not discarded at z = -epsilon.
|
|
738
|
+
Math.max(0, Math.min(1, p[2] / p[3] * 0.5 + 0.5))
|
|
739
|
+
];
|
|
740
|
+
const a = project(clipped[0]);
|
|
741
|
+
const b = project(clipped[1]);
|
|
742
|
+
if (![...a, ...b].every(Number.isFinite)) return;
|
|
743
|
+
const [x0, y0, z0] = a;
|
|
744
|
+
const [x1, y1, z1] = b;
|
|
745
|
+
const dx = x1 - x0;
|
|
746
|
+
const dy = y1 - y0;
|
|
747
|
+
const lengthSquared = dx * dx + dy * dy;
|
|
748
|
+
const length = Math.sqrt(lengthSquared);
|
|
749
|
+
const original = start[3] > 0 ? project(start) : a;
|
|
750
|
+
const phase = Math.hypot(x0 - original[0], y0 - original[1]);
|
|
751
|
+
const drawPixel = (x, y) => {
|
|
752
|
+
const t = lengthSquared === 0 ? 0 : Math.max(
|
|
753
|
+
0,
|
|
754
|
+
Math.min(1, ((x - x0) * dx + (y - y0) * dy) / lengthSquared)
|
|
755
|
+
);
|
|
756
|
+
if (dashed && Math.floor((phase + t * length) / dashLength) % 2 === 1)
|
|
757
|
+
return;
|
|
758
|
+
const distance = Math.hypot(x - (x0 + t * dx), y - (y0 + t * dy));
|
|
759
|
+
const coverage = Math.min(1, padding - distance);
|
|
760
|
+
if (coverage > 0) fragment(x, y, z0 + t * (z1 - z0), coverage);
|
|
761
|
+
};
|
|
762
|
+
if (Math.abs(dx) >= Math.abs(dy)) {
|
|
763
|
+
const band = length === 0 ? padding : padding * length / Math.abs(dx);
|
|
764
|
+
const first = Math.max(0, Math.ceil(Math.min(x0, x1) - padding));
|
|
765
|
+
const last = Math.min(width - 1, Math.floor(Math.max(x0, x1) + padding));
|
|
766
|
+
for (let x = first; x <= last; x++) {
|
|
767
|
+
const t = dx === 0 ? 0 : Math.max(0, Math.min(1, (x - x0) / dx));
|
|
768
|
+
const centerY = y0 + t * dy;
|
|
769
|
+
const top = Math.max(0, Math.ceil(centerY - band));
|
|
770
|
+
const bottom = Math.min(height - 1, Math.floor(centerY + band));
|
|
771
|
+
for (let y = top; y <= bottom; y++) drawPixel(x, y);
|
|
772
|
+
}
|
|
773
|
+
} else {
|
|
774
|
+
const band = padding * length / Math.abs(dy);
|
|
775
|
+
const first = Math.max(0, Math.ceil(Math.min(y0, y1) - padding));
|
|
776
|
+
const last = Math.min(height - 1, Math.floor(Math.max(y0, y1) + padding));
|
|
777
|
+
for (let y = first; y <= last; y++) {
|
|
778
|
+
const t = Math.max(0, Math.min(1, (y - y0) / dy));
|
|
779
|
+
const centerX = x0 + t * dx;
|
|
780
|
+
const left = Math.max(0, Math.ceil(centerX - band));
|
|
781
|
+
const right = Math.min(width - 1, Math.floor(centerX + band));
|
|
782
|
+
for (let x = left; x <= right; x++) drawPixel(x, y);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
682
787
|
// lib/render/SoftwareRenderer.ts
|
|
683
788
|
var SoftwareRenderer = class {
|
|
684
789
|
width;
|
|
@@ -731,6 +836,7 @@ var SoftwareRenderer = class {
|
|
|
731
836
|
const vScreen = new Array(vertexCount);
|
|
732
837
|
const vInvW = new Float32Array(vertexCount);
|
|
733
838
|
const vNDCz = new Float32Array(vertexCount);
|
|
839
|
+
const clipVertices = [];
|
|
734
840
|
for (let i = 0; i < vertexCount; i++) {
|
|
735
841
|
const p = vec42.fromValues(
|
|
736
842
|
positions[i * 3 + 0],
|
|
@@ -740,6 +846,7 @@ var SoftwareRenderer = class {
|
|
|
740
846
|
);
|
|
741
847
|
const c = vec42.create();
|
|
742
848
|
vec42.transformMat4(c, p, mvp);
|
|
849
|
+
clipVertices.push(c);
|
|
743
850
|
const invW = 1 / c[3];
|
|
744
851
|
vInvW[i] = invW;
|
|
745
852
|
if (!isFinite(invW)) {
|
|
@@ -774,9 +881,76 @@ var SoftwareRenderer = class {
|
|
|
774
881
|
const depthWrite = options.depthWrite ?? true;
|
|
775
882
|
const depthBias = options.depthBias ?? 0;
|
|
776
883
|
const dashLength = Math.max(1, options.dashLength ?? 4);
|
|
884
|
+
const strokeWidth = typeof options.width === "number" && Number.isFinite(options.width) && options.width > 0 ? options.width : 1;
|
|
885
|
+
const drawFragment = (xi, yi, z, coverage = 1) => {
|
|
886
|
+
if (z < 0 || z > 1) return;
|
|
887
|
+
const di = yi * this.width + xi;
|
|
888
|
+
const currentDepth = this.depth[di];
|
|
889
|
+
const passesDepth = depthCompare === "greater" ? z > currentDepth + depthBias : depthCompare === "less-equal" ? z <= currentDepth + depthBias : z < currentDepth - depthBias;
|
|
890
|
+
if (!passesDepth) return;
|
|
891
|
+
if (depthWrite) this.depth[di] = z;
|
|
892
|
+
const pixelIndex = di * 4;
|
|
893
|
+
const destinationAlpha = this.buffer[pixelIndex + 3] / 255;
|
|
894
|
+
const alpha = sourceAlpha * coverage;
|
|
895
|
+
const oneMinusSourceAlpha = 1 - alpha;
|
|
896
|
+
const outAlpha = alpha + destinationAlpha * oneMinusSourceAlpha;
|
|
897
|
+
const blend = (channel, offset) => outAlpha === 0 ? 0 : (channel * alpha + this.buffer[pixelIndex + offset] * destinationAlpha * oneMinusSourceAlpha) / outAlpha;
|
|
898
|
+
this.setPixel(
|
|
899
|
+
xi,
|
|
900
|
+
yi,
|
|
901
|
+
blend(r255, 0),
|
|
902
|
+
blend(g255, 1),
|
|
903
|
+
blend(b255, 2),
|
|
904
|
+
outAlpha * 255
|
|
905
|
+
);
|
|
906
|
+
};
|
|
777
907
|
for (let i = 0; i < indices.length; i += 2) {
|
|
778
908
|
const i0 = indices[i + 0];
|
|
779
909
|
const i1 = indices[i + 1];
|
|
910
|
+
if (strokeWidth !== 1) {
|
|
911
|
+
const sourceFaces = (mesh.edgeAdjacentVertices?.[i / 2] ?? []).flatMap(
|
|
912
|
+
(third) => {
|
|
913
|
+
if (clipVertices[i0][3] <= 0 || clipVertices[i1][3] <= 0 || clipVertices[third][3] <= 0)
|
|
914
|
+
return [];
|
|
915
|
+
const a2 = vScreen[i0];
|
|
916
|
+
const b2 = vScreen[i1];
|
|
917
|
+
const c = vScreen[third];
|
|
918
|
+
const area = edge(a2, b2, c);
|
|
919
|
+
if (!Number.isFinite(area) || area === 0) return [];
|
|
920
|
+
return [{ a: a2, b: b2, c, inverseArea: 1 / area, third }];
|
|
921
|
+
}
|
|
922
|
+
);
|
|
923
|
+
rasterizeWideLine(
|
|
924
|
+
clipVertices[i0],
|
|
925
|
+
clipVertices[i1],
|
|
926
|
+
this.width,
|
|
927
|
+
this.height,
|
|
928
|
+
strokeWidth,
|
|
929
|
+
options.dashed ?? false,
|
|
930
|
+
dashLength,
|
|
931
|
+
sourceFaces.length === 0 ? drawFragment : (x2, y2, z2, coverage) => {
|
|
932
|
+
let surfaceDepth = Infinity;
|
|
933
|
+
const sample = [x2 + 0.5, y2 + 0.5];
|
|
934
|
+
for (const face of sourceFaces) {
|
|
935
|
+
const l0 = edge(face.b, face.c, sample) * face.inverseArea;
|
|
936
|
+
const l1 = edge(face.c, face.a, sample) * face.inverseArea;
|
|
937
|
+
const l2 = 1 - l0 - l1;
|
|
938
|
+
if (l0 < -1e-6 || l1 < -1e-6 || l2 < -1e-6) continue;
|
|
939
|
+
const depth = (l0 * vNDCz[i0] + l1 * vNDCz[i1] + l2 * vNDCz[face.third]) * 0.5 + 0.5;
|
|
940
|
+
if (depth >= 0 && depth <= 1) {
|
|
941
|
+
surfaceDepth = Math.min(surfaceDepth, depth);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
drawFragment(
|
|
945
|
+
x2,
|
|
946
|
+
y2,
|
|
947
|
+
surfaceDepth === Infinity ? z2 : surfaceDepth,
|
|
948
|
+
coverage
|
|
949
|
+
);
|
|
950
|
+
}
|
|
951
|
+
);
|
|
952
|
+
continue;
|
|
953
|
+
}
|
|
780
954
|
const v0s = vScreen[i0];
|
|
781
955
|
const v1s = vScreen[i1];
|
|
782
956
|
const z0_ndc = vNDCz[i0];
|
|
@@ -809,20 +983,7 @@ var SoftwareRenderer = class {
|
|
|
809
983
|
const xi = Math.round(x);
|
|
810
984
|
const yi = Math.round(y);
|
|
811
985
|
if (xi >= 0 && xi < this.width && yi >= 0 && yi < this.height && z >= 0 && z <= 1) {
|
|
812
|
-
|
|
813
|
-
const currentDepth = this.depth[di];
|
|
814
|
-
const passesDepth = depthCompare === "greater" ? z > currentDepth + depthBias : depthCompare === "less-equal" ? z <= currentDepth + depthBias : z < currentDepth - depthBias;
|
|
815
|
-
if (passesDepth) {
|
|
816
|
-
if (depthWrite) this.depth[di] = z;
|
|
817
|
-
const pixelIndex = di * 4;
|
|
818
|
-
const destinationAlpha = this.buffer[pixelIndex + 3] / 255;
|
|
819
|
-
const oneMinusSourceAlpha = 1 - sourceAlpha;
|
|
820
|
-
const outAlpha = sourceAlpha + destinationAlpha * oneMinusSourceAlpha;
|
|
821
|
-
const outR = outAlpha === 0 ? 0 : (r255 * sourceAlpha + this.buffer[pixelIndex + 0] * destinationAlpha * oneMinusSourceAlpha) / outAlpha;
|
|
822
|
-
const outG = outAlpha === 0 ? 0 : (g255 * sourceAlpha + this.buffer[pixelIndex + 1] * destinationAlpha * oneMinusSourceAlpha) / outAlpha;
|
|
823
|
-
const outB = outAlpha === 0 ? 0 : (b255 * sourceAlpha + this.buffer[pixelIndex + 2] * destinationAlpha * oneMinusSourceAlpha) / outAlpha;
|
|
824
|
-
this.setPixel(xi, yi, outR, outG, outB, outAlpha * 255);
|
|
825
|
-
}
|
|
986
|
+
drawFragment(xi, yi, z);
|
|
826
987
|
}
|
|
827
988
|
x += xinc;
|
|
828
989
|
y += yinc;
|
|
@@ -1495,9 +1656,17 @@ function resolveRenderOptions(options = {}) {
|
|
|
1495
1656
|
camPos: options.camPos !== void 0 ? options.camPos : DEFAULT_RENDER_OPTIONS.camPos,
|
|
1496
1657
|
lookAt: options.lookAt !== void 0 ? options.lookAt : DEFAULT_RENDER_OPTIONS.lookAt,
|
|
1497
1658
|
up: resolveCameraUp(options.up),
|
|
1498
|
-
cameraRotation: resolveCameraRotation(options.cameraRotation)
|
|
1659
|
+
cameraRotation: resolveCameraRotation(options.cameraRotation),
|
|
1660
|
+
showEdges: options.showEdges === true,
|
|
1661
|
+
showHiddenEdges: typeof options.showHiddenEdges === "boolean" ? options.showHiddenEdges : null,
|
|
1662
|
+
edgeWidth: resolveWidth(options.edgeWidth),
|
|
1663
|
+
lineWidth: resolveWidth(options.lineWidth),
|
|
1664
|
+
hiddenEdgeOpacity: typeof options.hiddenEdgeOpacity === "number" && Number.isFinite(options.hiddenEdgeOpacity) ? Math.max(0, Math.min(1, options.hiddenEdgeOpacity)) : DEFAULT_RENDER_OPTIONS.hiddenEdgeOpacity
|
|
1499
1665
|
};
|
|
1500
1666
|
}
|
|
1667
|
+
function resolveWidth(width) {
|
|
1668
|
+
return typeof width === "number" && Number.isFinite(width) && width > 0 ? width : 1;
|
|
1669
|
+
}
|
|
1501
1670
|
|
|
1502
1671
|
// lib/render/StudioLighting.ts
|
|
1503
1672
|
import { mat4 as mat46 } from "gl-matrix";
|
|
@@ -2306,7 +2475,9 @@ function renderDrawCalls(drawCalls, optionsInput = {}, imageFactory = createUint
|
|
|
2306
2475
|
for (let i = 0; i < dcs.length; i++) {
|
|
2307
2476
|
const dc = dcs[i];
|
|
2308
2477
|
if (dc.mode === 1) {
|
|
2309
|
-
renderer.drawLines(dc, camera, options.gamma
|
|
2478
|
+
renderer.drawLines(dc, camera, options.gamma, {
|
|
2479
|
+
width: options.lineWidth * options.supersampling
|
|
2480
|
+
});
|
|
2310
2481
|
} else {
|
|
2311
2482
|
renderer.drawMesh(
|
|
2312
2483
|
dc,
|
|
@@ -2342,21 +2513,25 @@ function renderDrawCalls(drawCalls, optionsInput = {}, imageFactory = createUint
|
|
|
2342
2513
|
renderGroup(blendDraws);
|
|
2343
2514
|
renderer.denoise(options.gamma);
|
|
2344
2515
|
for (const dc of drawCalls) {
|
|
2345
|
-
|
|
2516
|
+
const showHiddenEdges = options.showHiddenEdges ?? dc.showHiddenEdges;
|
|
2517
|
+
if (!options.showEdges && !showHiddenEdges) continue;
|
|
2346
2518
|
const edgeDrawCall = createEdgeDrawCall(dc);
|
|
2347
2519
|
if (!edgeDrawCall) continue;
|
|
2348
2520
|
renderer.drawLines(edgeDrawCall, camera, options.gamma, {
|
|
2349
2521
|
depthCompare: "less-equal",
|
|
2350
2522
|
depthWrite: false,
|
|
2351
|
-
depthBias: 1e-4
|
|
2523
|
+
depthBias: 1e-4,
|
|
2524
|
+
width: options.edgeWidth * options.supersampling
|
|
2352
2525
|
});
|
|
2526
|
+
if (!showHiddenEdges) continue;
|
|
2353
2527
|
renderer.drawLines(edgeDrawCall, camera, options.gamma, {
|
|
2354
2528
|
depthCompare: "greater",
|
|
2355
2529
|
depthWrite: false,
|
|
2356
2530
|
depthBias: 1e-4,
|
|
2357
2531
|
dashed: true,
|
|
2358
2532
|
dashLength: 4 * options.supersampling,
|
|
2359
|
-
opacity:
|
|
2533
|
+
opacity: options.hiddenEdgeOpacity,
|
|
2534
|
+
width: options.edgeWidth * options.supersampling
|
|
2360
2535
|
});
|
|
2361
2536
|
}
|
|
2362
2537
|
if (options.debugPoints?.length) {
|