poppygl 0.0.33 → 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 CHANGED
@@ -86,10 +86,12 @@ Realistic mode supports base-color textures and scalar glTF metallic/roughness
86
86
  values. Normal maps, metallic/roughness textures, and custom HDR environments
87
87
  are not yet supported. Transparent and alpha-cutout surfaces do not cast shadows.
88
88
 
89
- Use realistic mode for saved images. At 600×600 with 2× supersampling, example
90
- models took 21–39 seconds to render, compared with 0.6–1.6 seconds in regular
91
- mode on the same machine. Reduce resolution or supersampling for faster output;
92
- render times vary with model complexity and hardware.
89
+ Use realistic mode for saved images. At 600×600 with 2× supersampling, the RP2040
90
+ motor-controller benchmark takes 2.26–5.49 seconds per warm render across three
91
+ views, a 2.11–2.76× speedup over the previous realistic renderer on the same
92
+ machine. Blender comparison error increases by at most 0.47%. See
93
+ [benchmark results and reproduction steps](BENCHMARK.md#rp2040-motor-controller-500-sample-blender-baseline-and-2-gate).
94
+ Render times vary with model complexity and hardware.
93
95
 
94
96
  ### Camera and background
95
97
 
@@ -131,10 +133,43 @@ const png = await renderGLTFToPNGFromGLB(glb, {
131
133
  - `cull`: enable back-face culling (default `true`).
132
134
  - `backgroundColor`: a hex string such as `"#e5e7eb"`, or an RGB tuple with values from 0 to 1.
133
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`).
134
141
  - `fetchImpl`: optional override for resource loading (must match the `fetch` signature).
135
142
 
136
143
  You can inspect the defaults via `getDefaultRenderOptions()` or reuse the internal merge logic with `resolveRenderOptions()`.
137
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
+
138
173
  ### Hidden edges for selected sub-models
139
174
 
140
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;
@@ -181,9 +193,13 @@ declare class StudioLighting {
181
193
  private eye;
182
194
  private shadowDirections;
183
195
  private diffuseCache;
196
+ private reflectionSamples;
197
+ private normalDiffuseCache;
198
+ private specularCache;
199
+ private blendedSpecular;
184
200
  constructor(drawCalls: DrawCall[], camera: Camera);
185
201
  private bounceRadiance;
186
- shade(position: V3, n: V3, base: V3, material: Material, sampleIndex?: number): V3;
202
+ shade(position: V3, n: V3, base: V3, material: Material, pixelX?: number, pixelY?: number): V3;
187
203
  }
188
204
 
189
205
  interface LightSettings {
@@ -191,6 +207,8 @@ interface LightSettings {
191
207
  ambient: number;
192
208
  }
193
209
  interface LineRenderOptions {
210
+ /** Stroke width in this renderer's raster pixels. Defaults to one. */
211
+ width?: number;
194
212
  depthCompare?: "less" | "less-equal" | "greater";
195
213
  depthWrite?: boolean;
196
214
  depthBias?: number;
@@ -212,7 +230,11 @@ declare class SoftwareRenderer {
212
230
  drawLines(mesh: DrawCall, camera: Camera, gammaOut?: boolean, options?: LineRenderOptions): void;
213
231
  sampleTextureNearest(img: BitmapLike | null, u: number, v: number): MutableRGBA;
214
232
  perspInterp(attrs: number[][], invWs: number[], lambdas: number[]): number[];
215
- drawMesh(mesh: DrawCall, camera: Camera, light: LightSettings, material: Material, cullBackFaces?: boolean, gammaOut?: boolean, studio?: StudioLighting): void;
233
+ drawMesh(mesh: DrawCall, camera: Camera, light: LightSettings, material: Material, cullBackFaces?: boolean, gammaOut?: boolean, studio?: StudioLighting, visibility?: {
234
+ winners: Int32Array;
235
+ triangleOffset: number;
236
+ depthOnly: boolean;
237
+ }): number;
216
238
  }
217
239
 
218
240
  /**
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, { indices, normals: normal ? [normal] : [] });
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 };
@@ -598,14 +611,18 @@ var GeometryDenoiser = class {
598
611
  this.albedo = new Float32Array(count * 3);
599
612
  this.radiance = new Float32Array(count * 3);
600
613
  this.distance = new Float32Array(count);
614
+ this.horizontal = new Float32Array(count * 3);
601
615
  }
602
616
  ids;
603
617
  normals;
604
618
  albedo;
605
619
  radiance;
606
620
  distance;
621
+ horizontal;
607
622
  materials = /* @__PURE__ */ new WeakMap();
608
623
  nextId = 1;
624
+ first = Infinity;
625
+ last = -1;
609
626
  materialId(material) {
610
627
  let id = this.materials.get(material);
611
628
  if (!id) {
@@ -615,6 +632,8 @@ var GeometryDenoiser = class {
615
632
  return id;
616
633
  }
617
634
  record(index, id, n, albedo, radiance, distance) {
635
+ if (index < this.first) this.first = index;
636
+ if (index > this.last) this.last = index;
618
637
  this.ids[index] = id;
619
638
  this.normals.set(n, index * 3);
620
639
  this.albedo.set(albedo, index * 3);
@@ -622,43 +641,149 @@ var GeometryDenoiser = class {
622
641
  this.distance[index] = distance;
623
642
  }
624
643
  apply(output, gamma) {
625
- const kernel = [1, 4, 6, 4, 1];
626
- for (let y = 0; y < this.height; y++)
627
- for (let x = 0; x < this.width; x++) {
628
- const index = y * this.width + x, id = this.ids[index];
629
- if (id === 0) continue;
630
- const k = index * 3, n = this.normals, a = this.albedo;
631
- let r = 0, g = 0, b = 0, total = 0;
632
- for (let dy = -2; dy <= 2; dy++)
633
- for (let dx = -2; dx <= 2; dx++) {
634
- const px = x + dx, py = y + dy;
644
+ const kernel = [1, 8, 28, 56, 70, 56, 28, 8, 1];
645
+ const radius = 4;
646
+ const n = this.normals, a = this.albedo, ids = this.ids;
647
+ const firstRow = Math.floor(this.first / this.width);
648
+ const lastRow = Math.floor(this.last / this.width);
649
+ for (let pass = 0; pass < 2; pass++) {
650
+ const source = pass === 0 ? this.radiance : this.horizontal;
651
+ for (let y = firstRow; y <= lastRow; y++)
652
+ for (let x = 0; x < this.width; x++) {
653
+ const index = y * this.width + x, id = ids[index];
654
+ if (id === 0) continue;
655
+ const k = index * 3, distance = this.distance[index];
656
+ const nx = n[k], ny = n[k + 1], nz = n[k + 2];
657
+ const ar = a[k], ag = a[k + 1], ab = a[k + 2];
658
+ let r = 0, g = 0, b = 0, total = 0;
659
+ for (let delta = -radius; delta <= radius; delta++) {
660
+ const px = pass === 0 ? x + delta : x, py = pass === 0 ? y : y + delta;
635
661
  if (px < 0 || py < 0 || px >= this.width || py >= this.height)
636
662
  continue;
637
663
  const neighbor = py * this.width + px, p = neighbor * 3;
638
- if (this.ids[neighbor] !== id) continue;
639
- if (Math.abs(this.distance[index] - this.distance[neighbor]) > this.distance[index] * 0.02)
640
- continue;
641
- if (n[k] * n[p] + n[k + 1] * n[p + 1] + n[k + 2] * n[p + 2] < 0.98)
664
+ if (ids[neighbor] !== id) continue;
665
+ if (Math.abs(distance - this.distance[neighbor]) > distance * 0.02)
642
666
  continue;
643
- if (Math.abs(a[k] - a[p]) + Math.abs(a[k + 1] - a[p + 1]) + Math.abs(a[k + 2] - a[p + 2]) > 0.03)
667
+ if (nx * n[p] + ny * n[p + 1] + nz * n[p + 2] < 0.98) continue;
668
+ if (Math.abs(ar - a[p]) + Math.abs(ag - a[p + 1]) + Math.abs(ab - a[p + 2]) > 0.03)
644
669
  continue;
645
- const weight = kernel[dy + 2] * kernel[dx + 2];
670
+ const weight = kernel[delta + radius];
646
671
  total += weight;
647
- r += this.radiance[p] * weight;
648
- g += this.radiance[p + 1] * weight;
649
- b += this.radiance[p + 2] * weight;
672
+ r += source[p] * weight;
673
+ g += source[p + 1] * weight;
674
+ b += source[p + 2] * weight;
650
675
  }
651
- if (!total) continue;
652
- const encode2 = (value) => Math.floor(
653
- 255 * (gamma ? srgbEncodeLinear01(Math.max(0, Math.min(1, value / total))) : Math.max(0, Math.min(1, value / total)))
654
- );
655
- output[index * 4] = encode2(r);
656
- output[index * 4 + 1] = encode2(g);
657
- output[index * 4 + 2] = encode2(b);
658
- }
676
+ if (pass === 0) {
677
+ this.horizontal[k] = r / total;
678
+ this.horizontal[k + 1] = g / total;
679
+ this.horizontal[k + 2] = b / total;
680
+ } else {
681
+ const encode2 = (value) => Math.floor(
682
+ 255 * (gamma ? srgbEncodeLinear01(
683
+ Math.max(0, Math.min(1, value / total))
684
+ ) : Math.max(0, Math.min(1, value / total)))
685
+ );
686
+ output[index * 4] = encode2(r);
687
+ output[index * 4 + 1] = encode2(g);
688
+ output[index * 4 + 2] = encode2(b);
689
+ }
690
+ }
691
+ }
659
692
  }
660
693
  };
661
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
+
662
787
  // lib/render/SoftwareRenderer.ts
663
788
  var SoftwareRenderer = class {
664
789
  width;
@@ -711,6 +836,7 @@ var SoftwareRenderer = class {
711
836
  const vScreen = new Array(vertexCount);
712
837
  const vInvW = new Float32Array(vertexCount);
713
838
  const vNDCz = new Float32Array(vertexCount);
839
+ const clipVertices = [];
714
840
  for (let i = 0; i < vertexCount; i++) {
715
841
  const p = vec42.fromValues(
716
842
  positions[i * 3 + 0],
@@ -720,6 +846,7 @@ var SoftwareRenderer = class {
720
846
  );
721
847
  const c = vec42.create();
722
848
  vec42.transformMat4(c, p, mvp);
849
+ clipVertices.push(c);
723
850
  const invW = 1 / c[3];
724
851
  vInvW[i] = invW;
725
852
  if (!isFinite(invW)) {
@@ -754,9 +881,76 @@ var SoftwareRenderer = class {
754
881
  const depthWrite = options.depthWrite ?? true;
755
882
  const depthBias = options.depthBias ?? 0;
756
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
+ };
757
907
  for (let i = 0; i < indices.length; i += 2) {
758
908
  const i0 = indices[i + 0];
759
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
+ }
760
954
  const v0s = vScreen[i0];
761
955
  const v1s = vScreen[i1];
762
956
  const z0_ndc = vNDCz[i0];
@@ -789,20 +983,7 @@ var SoftwareRenderer = class {
789
983
  const xi = Math.round(x);
790
984
  const yi = Math.round(y);
791
985
  if (xi >= 0 && xi < this.width && yi >= 0 && yi < this.height && z >= 0 && z <= 1) {
792
- const di = yi * this.width + xi;
793
- const currentDepth = this.depth[di];
794
- const passesDepth = depthCompare === "greater" ? z > currentDepth + depthBias : depthCompare === "less-equal" ? z <= currentDepth + depthBias : z < currentDepth - depthBias;
795
- if (passesDepth) {
796
- if (depthWrite) this.depth[di] = z;
797
- const pixelIndex = di * 4;
798
- const destinationAlpha = this.buffer[pixelIndex + 3] / 255;
799
- const oneMinusSourceAlpha = 1 - sourceAlpha;
800
- const outAlpha = sourceAlpha + destinationAlpha * oneMinusSourceAlpha;
801
- const outR = outAlpha === 0 ? 0 : (r255 * sourceAlpha + this.buffer[pixelIndex + 0] * destinationAlpha * oneMinusSourceAlpha) / outAlpha;
802
- const outG = outAlpha === 0 ? 0 : (g255 * sourceAlpha + this.buffer[pixelIndex + 1] * destinationAlpha * oneMinusSourceAlpha) / outAlpha;
803
- const outB = outAlpha === 0 ? 0 : (b255 * sourceAlpha + this.buffer[pixelIndex + 2] * destinationAlpha * oneMinusSourceAlpha) / outAlpha;
804
- this.setPixel(xi, yi, outR, outG, outB, outAlpha * 255);
805
- }
986
+ drawFragment(xi, yi, z);
806
987
  }
807
988
  x += xinc;
808
989
  y += yinc;
@@ -835,9 +1016,15 @@ var SoftwareRenderer = class {
835
1016
  }
836
1017
  return out;
837
1018
  }
838
- drawMesh(mesh, camera, light, material, cullBackFaces = true, gammaOut = true, studio) {
1019
+ drawMesh(mesh, camera, light, material, cullBackFaces = true, gammaOut = true, studio, visibility) {
839
1020
  let { positions, normals, uvs, indices, model, colors } = mesh;
840
1021
  const materialId = this.denoiser?.materialId(material) ?? 0;
1022
+ const lightDir = light.dir ?? DEFAULT_LIGHT_DIR;
1023
+ const ambient = clamp(light.ambient ?? DEFAULT_RENDER_OPTIONS.ambient, 0, 1);
1024
+ const L = studio ? null : vec32.normalize(
1025
+ vec32.create(),
1026
+ vec32.fromValues(lightDir[0], lightDir[1], lightDir[2])
1027
+ );
841
1028
  const view = camera.view;
842
1029
  const proj = camera.proj;
843
1030
  const mvp = mat43.create();
@@ -864,12 +1051,17 @@ var SoftwareRenderer = class {
864
1051
  colors = clipped.colors;
865
1052
  idx = clipped.indices;
866
1053
  vertexCount = positions.length / 3;
1054
+ const depthOnly = visibility?.depthOnly ?? false;
867
1055
  const vScreen = new Array(vertexCount);
868
1056
  const vInvW = new Float32Array(vertexCount);
869
1057
  const vNDCz = new Float32Array(vertexCount);
870
- const vWorldN = new Array(vertexCount);
871
- const vWorldP = studio ? new Array(vertexCount) : null;
872
- const vColor = new Array(vertexCount);
1058
+ const vWorldN = new Array(
1059
+ depthOnly ? 0 : vertexCount
1060
+ );
1061
+ const vWorldP = studio && !depthOnly ? new Array(vertexCount) : null;
1062
+ const vColor = new Array(
1063
+ depthOnly ? 0 : vertexCount
1064
+ );
873
1065
  for (let i = 0; i < vertexCount; i++) {
874
1066
  const p = vec42.fromValues(
875
1067
  positions[i * 3 + 0],
@@ -892,6 +1084,7 @@ var SoftwareRenderer = class {
892
1084
  vScreen[i] = [sx, sy];
893
1085
  vInvW[i] = invW;
894
1086
  vNDCz[i] = ndcZ;
1087
+ if (depthOnly) continue;
895
1088
  const n = vec32.fromValues(
896
1089
  useNormals[i * 3 + 0],
897
1090
  useNormals[i * 3 + 1],
@@ -969,9 +1162,15 @@ var SoftwareRenderer = class {
969
1162
  const z01 = zndc * 0.5 + 0.5;
970
1163
  if (z01 < 0 || z01 > 1) continue;
971
1164
  const di = y * this.width + x;
1165
+ if (visibility && !visibility.depthOnly && visibility.winners[di] !== visibility.triangleOffset + i / 3)
1166
+ continue;
972
1167
  const depth = this.depth;
973
1168
  if (z01 >= depth[di]) continue;
974
1169
  depth[di] = z01;
1170
+ if (visibility?.depthOnly) {
1171
+ visibility.winners[di] = visibility.triangleOffset + i / 3;
1172
+ continue;
1173
+ }
975
1174
  let baseColor = [
976
1175
  material.baseColorFactor[0],
977
1176
  material.baseColorFactor[1],
@@ -1006,24 +1205,7 @@ var SoftwareRenderer = class {
1006
1205
  np1 / nlen,
1007
1206
  np2 / nlen
1008
1207
  ];
1009
- const lightDir = light.dir ?? DEFAULT_LIGHT_DIR;
1010
- const ambient = clamp(
1011
- light.ambient ?? DEFAULT_RENDER_OPTIONS.ambient,
1012
- 0,
1013
- 1
1014
- );
1015
- const L = vec32.normalize(
1016
- vec32.create(),
1017
- vec32.fromValues(lightDir[0], lightDir[1], lightDir[2])
1018
- );
1019
- const ndotl = Math.max(
1020
- 0,
1021
- nrm[0] * -L[0] + nrm[1] * -L[1] + nrm[2] * -L[2]
1022
- );
1023
- const lit = ambient + (1 - ambient) * ndotl;
1024
- let r = baseColor[0] * lit;
1025
- let g = baseColor[1] * lit;
1026
- let b = baseColor[2] * lit;
1208
+ let r = 0, g = 0, b = 0;
1027
1209
  if (studio && worldPositions) {
1028
1210
  const position = this.perspInterp(worldPositions, invW, [
1029
1211
  l0,
@@ -1035,8 +1217,18 @@ var SoftwareRenderer = class {
1035
1217
  nrm,
1036
1218
  [baseColor[0], baseColor[1], baseColor[2]],
1037
1219
  material,
1038
- di
1220
+ x,
1221
+ y
1039
1222
  );
1223
+ } else {
1224
+ const ndotl = Math.max(
1225
+ 0,
1226
+ nrm[0] * -L[0] + nrm[1] * -L[1] + nrm[2] * -L[2]
1227
+ );
1228
+ const lit = ambient + (1 - ambient) * ndotl;
1229
+ r = baseColor[0] * lit;
1230
+ g = baseColor[1] * lit;
1231
+ b = baseColor[2] * lit;
1040
1232
  }
1041
1233
  const emission = material.emissiveFactor ?? [0, 0, 0];
1042
1234
  const strength = material.emissiveStrength ?? 1;
@@ -1117,6 +1309,7 @@ var SoftwareRenderer = class {
1117
1309
  }
1118
1310
  }
1119
1311
  }
1312
+ return idx.length / 3;
1120
1313
  }
1121
1314
  };
1122
1315
  function edge(a, b, p) {
@@ -1463,9 +1656,17 @@ function resolveRenderOptions(options = {}) {
1463
1656
  camPos: options.camPos !== void 0 ? options.camPos : DEFAULT_RENDER_OPTIONS.camPos,
1464
1657
  lookAt: options.lookAt !== void 0 ? options.lookAt : DEFAULT_RENDER_OPTIONS.lookAt,
1465
1658
  up: resolveCameraUp(options.up),
1466
- 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
1467
1665
  };
1468
1666
  }
1667
+ function resolveWidth(width) {
1668
+ return typeof width === "number" && Number.isFinite(width) && width > 0 ? width : 1;
1669
+ }
1469
1670
 
1470
1671
  // lib/render/StudioLighting.ts
1471
1672
  import { mat4 as mat46 } from "gl-matrix";
@@ -1502,15 +1703,16 @@ var RayOcclusion = class {
1502
1703
  if ((dc.mode ?? 4) !== 4 || dc.material.alphaMode === "BLEND") continue;
1503
1704
  if (dc.material.alphaMode === "MASK") continue;
1504
1705
  const count = dc.indices?.length ?? dc.positions.length / 3;
1505
- const vertex = (index) => {
1506
- const i = (dc.indices?.[index] ?? index) * 3;
1706
+ const worldVertices = [];
1707
+ for (let i = 0; i < dc.positions.length; i += 3) {
1507
1708
  const p = vec33.transformMat4(
1508
1709
  vec33.create(),
1509
1710
  [dc.positions[i], dc.positions[i + 1], dc.positions[i + 2]],
1510
1711
  dc.model
1511
1712
  );
1512
- return [p[0], p[1], p[2]];
1513
- };
1713
+ worldVertices.push([p[0], p[1], p[2]]);
1714
+ }
1715
+ const vertex = (index) => worldVertices[dc.indices?.[index] ?? index];
1514
1716
  for (let i = 0; i < count; i += 3) {
1515
1717
  const p = vertex(i), q = vertex(i + 1), r = vertex(i + 2);
1516
1718
  const min = [0, 1, 2].map(
@@ -1586,52 +1788,97 @@ var RayOcclusion = class {
1586
1788
  }
1587
1789
  }
1588
1790
  build(triangles) {
1589
- const min = [Infinity, Infinity, Infinity], max = [-Infinity, -Infinity, -Infinity];
1791
+ const min = [Infinity, Infinity, Infinity], max = [-Infinity, -Infinity, -Infinity], centerMin = [Infinity, Infinity, Infinity], centerMax = [-Infinity, -Infinity, -Infinity];
1590
1792
  for (const t of triangles)
1591
1793
  for (let a = 0; a < 3; a++) {
1592
1794
  min[a] = Math.min(min[a], t.min[a]);
1593
1795
  max[a] = Math.max(max[a], t.max[a]);
1796
+ centerMin[a] = Math.min(centerMin[a], t.center[a]);
1797
+ centerMax[a] = Math.max(centerMax[a], t.center[a]);
1594
1798
  }
1595
1799
  if (triangles.length <= 8) return { min, max, triangles };
1800
+ const binCount = 16;
1596
1801
  const area = (lo, hi) => {
1597
1802
  const x = hi[0] - lo[0], y = hi[1] - lo[1], z = hi[2] - lo[2];
1598
1803
  return 2 * (x * y + y * z + z * x);
1599
1804
  };
1600
- let bestCost = Infinity, middle = triangles.length >> 1, best = triangles;
1805
+ let bestCost = Infinity, bestAxis = -1, bestSplit = 0, bestMin = 0, bestScale = 0;
1806
+ let longestAxis = 0;
1807
+ for (let a = 1; a < 3; a++)
1808
+ if (centerMax[a] - centerMin[a] > centerMax[longestAxis] - centerMin[longestAxis])
1809
+ longestAxis = a;
1601
1810
  for (let axis = 0; axis < 3; axis++) {
1602
- const sorted = triangles.slice().sort((a, b) => a.center[axis] - b.center[axis]);
1603
- const suffix = new Float64Array(sorted.length);
1604
- let lo = [Infinity, Infinity, Infinity], hi = [-Infinity, -Infinity, -Infinity];
1605
- for (let i = sorted.length - 1; i >= 0; i--) {
1811
+ if (triangles.length <= 4096 && axis !== longestAxis) continue;
1812
+ const loCenter = centerMin[axis], hiCenter = centerMax[axis];
1813
+ if (loCenter === hiCenter) continue;
1814
+ const scale = binCount / (hiCenter - loCenter);
1815
+ const counts = new Uint32Array(binCount);
1816
+ const bounds = new Float64Array(binCount * 6);
1817
+ for (let i = 0; i < binCount; i++) {
1818
+ bounds.fill(Infinity, i * 6, i * 6 + 3);
1819
+ bounds.fill(-Infinity, i * 6 + 3, i * 6 + 6);
1820
+ }
1821
+ for (const t of triangles) {
1822
+ const bin = Math.min(
1823
+ binCount - 1,
1824
+ Math.floor((t.center[axis] - loCenter) * scale)
1825
+ );
1826
+ counts[bin]++;
1827
+ const offset = bin * 6;
1606
1828
  for (let a = 0; a < 3; a++) {
1607
- lo[a] = Math.min(lo[a], sorted[i].min[a]);
1608
- hi[a] = Math.max(hi[a], sorted[i].max[a]);
1829
+ bounds[offset + a] = Math.min(bounds[offset + a], t.min[a]);
1830
+ bounds[offset + a + 3] = Math.max(bounds[offset + a + 3], t.max[a]);
1609
1831
  }
1610
- suffix[i] = area(lo, hi);
1832
+ }
1833
+ const suffixCost = new Float64Array(binCount);
1834
+ let lo = [Infinity, Infinity, Infinity], hi = [-Infinity, -Infinity, -Infinity], count = 0;
1835
+ for (let i = binCount - 1; i >= 0; i--) {
1836
+ count += counts[i];
1837
+ if (counts[i])
1838
+ for (let a = 0; a < 3; a++) {
1839
+ lo[a] = Math.min(lo[a], bounds[i * 6 + a]);
1840
+ hi[a] = Math.max(hi[a], bounds[i * 6 + a + 3]);
1841
+ }
1842
+ suffixCost[i] = count ? area(lo, hi) * count : 0;
1611
1843
  }
1612
1844
  lo = [Infinity, Infinity, Infinity];
1613
1845
  hi = [-Infinity, -Infinity, -Infinity];
1614
- for (let i = 1; i < sorted.length; i++) {
1615
- const triangle = sorted[i - 1];
1616
- for (let a = 0; a < 3; a++) {
1617
- lo[a] = Math.min(lo[a], triangle.min[a]);
1618
- hi[a] = Math.max(hi[a], triangle.max[a]);
1619
- }
1620
- if (triangle.center[axis] === sorted[i].center[axis]) continue;
1621
- const cost = area(lo, hi) * i + suffix[i] * (sorted.length - i);
1846
+ count = 0;
1847
+ for (let i = 0; i < binCount - 1; i++) {
1848
+ count += counts[i];
1849
+ if (counts[i])
1850
+ for (let a = 0; a < 3; a++) {
1851
+ lo[a] = Math.min(lo[a], bounds[i * 6 + a]);
1852
+ hi[a] = Math.max(hi[a], bounds[i * 6 + a + 3]);
1853
+ }
1854
+ if (!count || count === triangles.length) continue;
1855
+ const cost = area(lo, hi) * count + suffixCost[i + 1];
1622
1856
  if (cost < bestCost) {
1623
1857
  bestCost = cost;
1624
- middle = i;
1625
- best = sorted;
1858
+ bestAxis = axis;
1859
+ bestSplit = i;
1860
+ bestMin = loCenter;
1861
+ bestScale = scale;
1626
1862
  }
1627
1863
  }
1628
1864
  }
1629
- return {
1630
- min,
1631
- max,
1632
- left: this.build(best.slice(0, middle)),
1633
- right: this.build(best.slice(middle))
1634
- };
1865
+ let left, right;
1866
+ if (bestAxis < 0) {
1867
+ const middle = triangles.length >> 1;
1868
+ left = triangles.slice(0, middle);
1869
+ right = triangles.slice(middle);
1870
+ } else {
1871
+ left = [];
1872
+ right = [];
1873
+ for (const t of triangles) {
1874
+ const bin = Math.min(
1875
+ binCount - 1,
1876
+ Math.floor((t.center[bestAxis] - bestMin) * bestScale)
1877
+ );
1878
+ (bin <= bestSplit ? left : right).push(t);
1879
+ }
1880
+ }
1881
+ return { min, max, left: this.build(left), right: this.build(right) };
1635
1882
  }
1636
1883
  prepareRay(direction) {
1637
1884
  this.parallelAxes = 0;
@@ -1766,6 +2013,9 @@ var W = 96;
1766
2013
  var H = 48;
1767
2014
  var SAMPLES = 128;
1768
2015
  var ROUGHNESSES = [0.04, 0.1, 0.18, 0.28, 0.4, 0.55, 0.7, 0.85, 1];
2016
+ var SHADOW_SAMPLES = 4;
2017
+ var REFLECTION_SAMPLES = 4;
2018
+ var DIFFUSE_SAMPLES = 2;
1769
2019
  var lobes = studio_environment_default.lobes.map((l) => ({
1770
2020
  ...l,
1771
2021
  direction: normalize(l.direction),
@@ -1898,6 +2148,22 @@ var StudioLighting = class {
1898
2148
  eye;
1899
2149
  shadowDirections;
1900
2150
  diffuseCache = /* @__PURE__ */ new WeakMap();
2151
+ reflectionSamples = /* @__PURE__ */ new Map();
2152
+ normalDiffuseCache = /* @__PURE__ */ new WeakMap();
2153
+ specularCache = /* @__PURE__ */ new Map();
2154
+ blendedSpecular(rough, level, blend) {
2155
+ const cached = this.specularCache.get(rough);
2156
+ if (cached) return cached;
2157
+ if (this.specularCache.size >= 16) return null;
2158
+ const tables = lobes.map((_, j) => {
2159
+ const lo = this.tables.specular[level][j], hi = this.tables.specular[level + 1][j], table = new Float64Array(W * H);
2160
+ for (let i = 0; i < table.length; i++)
2161
+ table[i] = lo[i] * (1 - blend) + hi[i] * blend;
2162
+ return table;
2163
+ });
2164
+ this.specularCache.set(rough, tables);
2165
+ return tables;
2166
+ }
1901
2167
  constructor(drawCalls, camera) {
1902
2168
  this.occlusion = new RayOcclusion(drawCalls);
1903
2169
  const world = mat46.invert(mat46.create(), camera.view);
@@ -1906,8 +2172,8 @@ var StudioLighting = class {
1906
2172
  this.eye = [world[12], world[13], world[14]];
1907
2173
  this.shadowDirections = lobes.map((l) => {
1908
2174
  const [t, b] = basis(l.direction);
1909
- return Array.from({ length: 8 }, (_, i) => {
1910
- const u = (i + 0.5) / 8, cos = 1 + Math.log(1 - u) / l.sharpness, sin = Math.sqrt(Math.max(0, 1 - cos * cos)), phi = 2 * Math.PI * radicalInverse(i);
2175
+ return Array.from({ length: SHADOW_SAMPLES }, (_, i) => {
2176
+ const u = (i + 0.5) / SHADOW_SAMPLES, cos = 1 + Math.log(1 - u) / l.sharpness, sin = Math.sqrt(Math.max(0, 1 - cos * cos)), phi = 2 * Math.PI * radicalInverse(i);
1911
2177
  const direction = orient(
1912
2178
  sin * Math.cos(phi),
1913
2179
  sin * Math.sin(phi),
@@ -1965,15 +2231,16 @@ var StudioLighting = class {
1965
2231
  );
1966
2232
  const pref = [...studio_environment_default.ambient];
1967
2233
  const reflectedLookup = tableLookup(reflected);
2234
+ const specularTables = this.blendedSpecular(rough, level, blend);
1968
2235
  for (let j = 0; j < lobes.length; j++) {
1969
- const value = sampleTable(this.tables.specular[level][j], reflectedLookup) * (1 - blend) + sampleTable(this.tables.specular[level + 1][j], reflectedLookup) * blend;
2236
+ const value = specularTables ? sampleTable(specularTables[j], reflectedLookup) : sampleTable(this.tables.specular[level][j], reflectedLookup) * (1 - blend) + sampleTable(this.tables.specular[level + 1][j], reflectedLookup) * blend;
1970
2237
  for (let k = 0; k < 3; k++) pref[k] += value * lobes[j].color[k];
1971
2238
  }
1972
2239
  for (let k = 0; k < 3; k++)
1973
2240
  radiance[k] += pref[k] * ((0.04 * (1 - metal) + base[k] * metal) * A + B);
1974
2241
  return radiance;
1975
2242
  }
1976
- shade(position, n, base, material, sampleIndex = 0) {
2243
+ shade(position, n, base, material, pixelX = 0, pixelY = 0) {
1977
2244
  const v = normalize([
1978
2245
  this.eye[0] - position[0],
1979
2246
  this.eye[1] - position[1],
@@ -2005,12 +2272,20 @@ var StudioLighting = class {
2005
2272
  const specColor = f0.map(
2006
2273
  (c, j) => studio_environment_default.ambient[j] * (c * A + B)
2007
2274
  );
2008
- let bits = sampleIndex + 1;
2009
- bits = Math.imul(bits ^ bits >>> 16, 2146121005);
2010
- bits = Math.imul(bits ^ bits >>> 15, 2221713035);
2011
- const hash = ((bits ^ bits >>> 16) >>> 0) / 4294967296;
2275
+ const gradient = (0.06711056 * pixelX + 583715e-8 * pixelY) % 1;
2276
+ const hash = 52.9829189 * gradient % 1;
2012
2277
  const phase = hash * 2 * Math.PI, cosPhase = Math.cos(phase), sinPhase = Math.sin(phase);
2013
- const normalLookup = tableLookup(n), reflectedLookup = tableLookup(reflected);
2278
+ let normalDiffuse = this.normalDiffuseCache.get(material);
2279
+ if (!normalDiffuse || normalDiffuse.normal.some((value, k) => !Object.is(value, n[k]))) {
2280
+ const lookup = tableLookup(n);
2281
+ normalDiffuse = {
2282
+ normal: [...n],
2283
+ values: this.tables.diffuse.map((table) => sampleTable(table, lookup))
2284
+ };
2285
+ this.normalDiffuseCache.set(material, normalDiffuse);
2286
+ }
2287
+ const reflectedLookup = tableLookup(reflected);
2288
+ const specularTables = this.blendedSpecular(rough, level, blend);
2014
2289
  for (let j = 0; j < lobes.length; j++) {
2015
2290
  const dirs = this.shadowDirections[j];
2016
2291
  let visible = 0, total = 0;
@@ -2025,8 +2300,8 @@ var StudioLighting = class {
2025
2300
  if (weight > 0 && !this.occlusion.occluded(origin, l)) visible += weight;
2026
2301
  }
2027
2302
  const visibility = total > 0 ? visible / total : 1;
2028
- const diffuse = sampleTable(this.tables.diffuse[j], normalLookup) * visibility;
2029
- const specular = (sampleTable(this.tables.specular[level][j], reflectedLookup) * (1 - blend) + sampleTable(this.tables.specular[level + 1][j], reflectedLookup) * blend) * visibility;
2303
+ const diffuse = normalDiffuse.values[j] * visibility;
2304
+ const specular = (specularTables ? sampleTable(specularTables[j], reflectedLookup) : sampleTable(this.tables.specular[level][j], reflectedLookup) * (1 - blend) + sampleTable(this.tables.specular[level + 1][j], reflectedLookup) * blend) * visibility;
2030
2305
  for (let k = 0; k < 3; k++) {
2031
2306
  color[k] += lobes[j].color[k] * base[k] * (1 - metal) * 0.96 * diffuse;
2032
2307
  specColor[k] += lobes[j].color[k] * (f0[k] * A + B) * specular;
@@ -2034,8 +2309,8 @@ var StudioLighting = class {
2034
2309
  }
2035
2310
  const [t, b] = basis(n);
2036
2311
  let blocked = 0;
2037
- for (let i = 0; metal < 1 && i < 4; i++) {
2038
- const phi = i * Math.PI / 2 + phase, l = orient(
2312
+ for (let i = 0; metal < 1 && i < DIFFUSE_SAMPLES; i++) {
2313
+ const phi = i * 2 * Math.PI / DIFFUSE_SAMPLES + phase, l = orient(
2039
2314
  0.7 * Math.cos(phi),
2040
2315
  0.7 * Math.sin(phi),
2041
2316
  Math.sqrt(0.51),
@@ -2048,20 +2323,26 @@ var StudioLighting = class {
2048
2323
  blocked++;
2049
2324
  const bounce2 = this.bounceRadiance(hit, l);
2050
2325
  for (let k = 0; k < 3; k++)
2051
- color[k] += bounce2[k] * base[k] * (1 - metal) * 0.96 / 4;
2326
+ color[k] += bounce2[k] * base[k] * (1 - metal) * 0.96 / DIFFUSE_SAMPLES;
2052
2327
  }
2053
2328
  }
2054
- const ao = 1 - blocked / 4;
2329
+ const ao = 1 - blocked / DIFFUSE_SAMPLES;
2055
2330
  for (let k = 0; k < 3; k++)
2056
2331
  color[k] -= studio_environment_default.ambient[k] * (1 - ao) * base[k] * (1 - metal) * 0.96;
2057
2332
  const [rt, rb] = basis(reflected), bounce = [0, 0, 0];
2058
2333
  let hitWeight = 0, totalWeight = 0;
2059
- for (let i = 0; i < 8; i++) {
2060
- const u = (i + 0.5) / 8, phi = 2 * Math.PI * radicalInverse(i) + phase, a = rough * rough;
2061
- const nh = Math.sqrt((1 - u) / (1 + (a * a - 1) * u)), sin = Math.sqrt(1 - nh * nh);
2334
+ let samples = this.reflectionSamples.get(rough);
2335
+ if (!samples) {
2336
+ samples = Array.from({ length: REFLECTION_SAMPLES }, (_, i) => {
2337
+ const u = (i + 0.5) / REFLECTION_SAMPLES, phi = 2 * Math.PI * radicalInverse(i), a = rough * rough, nh = Math.sqrt((1 - u) / (1 + (a * a - 1) * u)), sin = Math.sqrt(1 - nh * nh);
2338
+ return [sin * Math.cos(phi), sin * Math.sin(phi), nh];
2339
+ });
2340
+ this.reflectionSamples.set(rough, samples);
2341
+ }
2342
+ for (const [sx, sy, nh] of samples) {
2062
2343
  const h = orient(
2063
- sin * Math.cos(phi),
2064
- sin * Math.sin(phi),
2344
+ sx * cosPhase - sy * sinPhase,
2345
+ sx * sinPhase + sy * cosPhase,
2065
2346
  nh,
2066
2347
  reflected,
2067
2348
  rt,
@@ -2170,10 +2451,33 @@ function renderDrawCalls(drawCalls, optionsInput = {}, imageFactory = createUint
2170
2451
  (dc) => (dc.material.alphaMode ?? "OPAQUE") === "BLEND"
2171
2452
  );
2172
2453
  const studio = options.realistic ? new StudioLighting(allDrawCalls, camera) : void 0;
2173
- const renderGroup = (dcs) => {
2174
- for (const dc of dcs) {
2454
+ let winners;
2455
+ const triangleOffsets = [];
2456
+ if (studio && opaqueDraws.length > 1 && opaqueDraws.every((dc) => dc.mode !== 1)) {
2457
+ winners = new Int32Array(renderWidth * renderHeight).fill(-1);
2458
+ let triangleOffset = 0;
2459
+ for (const dc of opaqueDraws) {
2460
+ triangleOffsets.push(triangleOffset);
2461
+ triangleOffset += renderer.drawMesh(
2462
+ dc,
2463
+ camera,
2464
+ { dir: options.lightDir, ambient: options.ambient },
2465
+ dc.material,
2466
+ options.cull,
2467
+ options.gamma,
2468
+ studio,
2469
+ { winners, triangleOffset, depthOnly: true }
2470
+ );
2471
+ }
2472
+ renderer.depth.fill(Infinity);
2473
+ }
2474
+ const renderGroup = (dcs, opaque = false) => {
2475
+ for (let i = 0; i < dcs.length; i++) {
2476
+ const dc = dcs[i];
2175
2477
  if (dc.mode === 1) {
2176
- renderer.drawLines(dc, camera, options.gamma);
2478
+ renderer.drawLines(dc, camera, options.gamma, {
2479
+ width: options.lineWidth * options.supersampling
2480
+ });
2177
2481
  } else {
2178
2482
  renderer.drawMesh(
2179
2483
  dc,
@@ -2182,12 +2486,13 @@ function renderDrawCalls(drawCalls, optionsInput = {}, imageFactory = createUint
2182
2486
  dc.material,
2183
2487
  options.cull,
2184
2488
  options.gamma,
2185
- studio
2489
+ studio,
2490
+ opaque && winners ? { winners, triangleOffset: triangleOffsets[i], depthOnly: false } : void 0
2186
2491
  );
2187
2492
  }
2188
2493
  }
2189
2494
  };
2190
- renderGroup(opaqueDraws);
2495
+ renderGroup(opaqueDraws, true);
2191
2496
  renderGroup(maskDraws);
2192
2497
  if (infiniteGridOptions) {
2193
2498
  const aabb = computeWorldAABB(drawCalls);
@@ -2208,21 +2513,25 @@ function renderDrawCalls(drawCalls, optionsInput = {}, imageFactory = createUint
2208
2513
  renderGroup(blendDraws);
2209
2514
  renderer.denoise(options.gamma);
2210
2515
  for (const dc of drawCalls) {
2211
- if (!dc.showHiddenEdges) continue;
2516
+ const showHiddenEdges = options.showHiddenEdges ?? dc.showHiddenEdges;
2517
+ if (!options.showEdges && !showHiddenEdges) continue;
2212
2518
  const edgeDrawCall = createEdgeDrawCall(dc);
2213
2519
  if (!edgeDrawCall) continue;
2214
2520
  renderer.drawLines(edgeDrawCall, camera, options.gamma, {
2215
2521
  depthCompare: "less-equal",
2216
2522
  depthWrite: false,
2217
- depthBias: 1e-4
2523
+ depthBias: 1e-4,
2524
+ width: options.edgeWidth * options.supersampling
2218
2525
  });
2526
+ if (!showHiddenEdges) continue;
2219
2527
  renderer.drawLines(edgeDrawCall, camera, options.gamma, {
2220
2528
  depthCompare: "greater",
2221
2529
  depthWrite: false,
2222
2530
  depthBias: 1e-4,
2223
2531
  dashed: true,
2224
2532
  dashLength: 4 * options.supersampling,
2225
- opacity: 0.35
2533
+ opacity: options.hiddenEdgeOpacity,
2534
+ width: options.edgeWidth * options.supersampling
2226
2535
  });
2227
2536
  }
2228
2537
  if (options.debugPoints?.length) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "poppygl",
3
3
  "main": "dist/index.js",
4
- "version": "0.0.33",
4
+ "version": "0.0.35",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "https://github.com/tscircuit/poppygl.git"