poppygl 0.0.33 → 0.0.34

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
 
package/dist/index.d.ts CHANGED
@@ -181,9 +181,13 @@ declare class StudioLighting {
181
181
  private eye;
182
182
  private shadowDirections;
183
183
  private diffuseCache;
184
+ private reflectionSamples;
185
+ private normalDiffuseCache;
186
+ private specularCache;
187
+ private blendedSpecular;
184
188
  constructor(drawCalls: DrawCall[], camera: Camera);
185
189
  private bounceRadiance;
186
- shade(position: V3, n: V3, base: V3, material: Material, sampleIndex?: number): V3;
190
+ shade(position: V3, n: V3, base: V3, material: Material, pixelX?: number, pixelY?: number): V3;
187
191
  }
188
192
 
189
193
  interface LightSettings {
@@ -212,7 +216,11 @@ declare class SoftwareRenderer {
212
216
  drawLines(mesh: DrawCall, camera: Camera, gammaOut?: boolean, options?: LineRenderOptions): void;
213
217
  sampleTextureNearest(img: BitmapLike | null, u: number, v: number): MutableRGBA;
214
218
  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;
219
+ drawMesh(mesh: DrawCall, camera: Camera, light: LightSettings, material: Material, cullBackFaces?: boolean, gammaOut?: boolean, studio?: StudioLighting, visibility?: {
220
+ winners: Int32Array;
221
+ triangleOffset: number;
222
+ depthOnly: boolean;
223
+ }): number;
216
224
  }
217
225
 
218
226
  /**
package/dist/index.js CHANGED
@@ -598,14 +598,18 @@ var GeometryDenoiser = class {
598
598
  this.albedo = new Float32Array(count * 3);
599
599
  this.radiance = new Float32Array(count * 3);
600
600
  this.distance = new Float32Array(count);
601
+ this.horizontal = new Float32Array(count * 3);
601
602
  }
602
603
  ids;
603
604
  normals;
604
605
  albedo;
605
606
  radiance;
606
607
  distance;
608
+ horizontal;
607
609
  materials = /* @__PURE__ */ new WeakMap();
608
610
  nextId = 1;
611
+ first = Infinity;
612
+ last = -1;
609
613
  materialId(material) {
610
614
  let id = this.materials.get(material);
611
615
  if (!id) {
@@ -615,6 +619,8 @@ var GeometryDenoiser = class {
615
619
  return id;
616
620
  }
617
621
  record(index, id, n, albedo, radiance, distance) {
622
+ if (index < this.first) this.first = index;
623
+ if (index > this.last) this.last = index;
618
624
  this.ids[index] = id;
619
625
  this.normals.set(n, index * 3);
620
626
  this.albedo.set(albedo, index * 3);
@@ -622,40 +628,54 @@ var GeometryDenoiser = class {
622
628
  this.distance[index] = distance;
623
629
  }
624
630
  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;
631
+ const kernel = [1, 8, 28, 56, 70, 56, 28, 8, 1];
632
+ const radius = 4;
633
+ const n = this.normals, a = this.albedo, ids = this.ids;
634
+ const firstRow = Math.floor(this.first / this.width);
635
+ const lastRow = Math.floor(this.last / this.width);
636
+ for (let pass = 0; pass < 2; pass++) {
637
+ const source = pass === 0 ? this.radiance : this.horizontal;
638
+ for (let y = firstRow; y <= lastRow; y++)
639
+ for (let x = 0; x < this.width; x++) {
640
+ const index = y * this.width + x, id = ids[index];
641
+ if (id === 0) continue;
642
+ const k = index * 3, distance = this.distance[index];
643
+ const nx = n[k], ny = n[k + 1], nz = n[k + 2];
644
+ const ar = a[k], ag = a[k + 1], ab = a[k + 2];
645
+ let r = 0, g = 0, b = 0, total = 0;
646
+ for (let delta = -radius; delta <= radius; delta++) {
647
+ const px = pass === 0 ? x + delta : x, py = pass === 0 ? y : y + delta;
635
648
  if (px < 0 || py < 0 || px >= this.width || py >= this.height)
636
649
  continue;
637
650
  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)
651
+ if (ids[neighbor] !== id) continue;
652
+ if (Math.abs(distance - this.distance[neighbor]) > distance * 0.02)
640
653
  continue;
641
- if (n[k] * n[p] + n[k + 1] * n[p + 1] + n[k + 2] * n[p + 2] < 0.98)
654
+ if (nx * n[p] + ny * n[p + 1] + nz * n[p + 2] < 0.98) continue;
655
+ if (Math.abs(ar - a[p]) + Math.abs(ag - a[p + 1]) + Math.abs(ab - a[p + 2]) > 0.03)
642
656
  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)
644
- continue;
645
- const weight = kernel[dy + 2] * kernel[dx + 2];
657
+ const weight = kernel[delta + radius];
646
658
  total += weight;
647
- r += this.radiance[p] * weight;
648
- g += this.radiance[p + 1] * weight;
649
- b += this.radiance[p + 2] * weight;
659
+ r += source[p] * weight;
660
+ g += source[p + 1] * weight;
661
+ b += source[p + 2] * weight;
650
662
  }
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
- }
663
+ if (pass === 0) {
664
+ this.horizontal[k] = r / total;
665
+ this.horizontal[k + 1] = g / total;
666
+ this.horizontal[k + 2] = b / total;
667
+ } else {
668
+ const encode2 = (value) => Math.floor(
669
+ 255 * (gamma ? srgbEncodeLinear01(
670
+ Math.max(0, Math.min(1, value / total))
671
+ ) : Math.max(0, Math.min(1, value / total)))
672
+ );
673
+ output[index * 4] = encode2(r);
674
+ output[index * 4 + 1] = encode2(g);
675
+ output[index * 4 + 2] = encode2(b);
676
+ }
677
+ }
678
+ }
659
679
  }
660
680
  };
661
681
 
@@ -835,9 +855,15 @@ var SoftwareRenderer = class {
835
855
  }
836
856
  return out;
837
857
  }
838
- drawMesh(mesh, camera, light, material, cullBackFaces = true, gammaOut = true, studio) {
858
+ drawMesh(mesh, camera, light, material, cullBackFaces = true, gammaOut = true, studio, visibility) {
839
859
  let { positions, normals, uvs, indices, model, colors } = mesh;
840
860
  const materialId = this.denoiser?.materialId(material) ?? 0;
861
+ const lightDir = light.dir ?? DEFAULT_LIGHT_DIR;
862
+ const ambient = clamp(light.ambient ?? DEFAULT_RENDER_OPTIONS.ambient, 0, 1);
863
+ const L = studio ? null : vec32.normalize(
864
+ vec32.create(),
865
+ vec32.fromValues(lightDir[0], lightDir[1], lightDir[2])
866
+ );
841
867
  const view = camera.view;
842
868
  const proj = camera.proj;
843
869
  const mvp = mat43.create();
@@ -864,12 +890,17 @@ var SoftwareRenderer = class {
864
890
  colors = clipped.colors;
865
891
  idx = clipped.indices;
866
892
  vertexCount = positions.length / 3;
893
+ const depthOnly = visibility?.depthOnly ?? false;
867
894
  const vScreen = new Array(vertexCount);
868
895
  const vInvW = new Float32Array(vertexCount);
869
896
  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);
897
+ const vWorldN = new Array(
898
+ depthOnly ? 0 : vertexCount
899
+ );
900
+ const vWorldP = studio && !depthOnly ? new Array(vertexCount) : null;
901
+ const vColor = new Array(
902
+ depthOnly ? 0 : vertexCount
903
+ );
873
904
  for (let i = 0; i < vertexCount; i++) {
874
905
  const p = vec42.fromValues(
875
906
  positions[i * 3 + 0],
@@ -892,6 +923,7 @@ var SoftwareRenderer = class {
892
923
  vScreen[i] = [sx, sy];
893
924
  vInvW[i] = invW;
894
925
  vNDCz[i] = ndcZ;
926
+ if (depthOnly) continue;
895
927
  const n = vec32.fromValues(
896
928
  useNormals[i * 3 + 0],
897
929
  useNormals[i * 3 + 1],
@@ -969,9 +1001,15 @@ var SoftwareRenderer = class {
969
1001
  const z01 = zndc * 0.5 + 0.5;
970
1002
  if (z01 < 0 || z01 > 1) continue;
971
1003
  const di = y * this.width + x;
1004
+ if (visibility && !visibility.depthOnly && visibility.winners[di] !== visibility.triangleOffset + i / 3)
1005
+ continue;
972
1006
  const depth = this.depth;
973
1007
  if (z01 >= depth[di]) continue;
974
1008
  depth[di] = z01;
1009
+ if (visibility?.depthOnly) {
1010
+ visibility.winners[di] = visibility.triangleOffset + i / 3;
1011
+ continue;
1012
+ }
975
1013
  let baseColor = [
976
1014
  material.baseColorFactor[0],
977
1015
  material.baseColorFactor[1],
@@ -1006,24 +1044,7 @@ var SoftwareRenderer = class {
1006
1044
  np1 / nlen,
1007
1045
  np2 / nlen
1008
1046
  ];
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;
1047
+ let r = 0, g = 0, b = 0;
1027
1048
  if (studio && worldPositions) {
1028
1049
  const position = this.perspInterp(worldPositions, invW, [
1029
1050
  l0,
@@ -1035,8 +1056,18 @@ var SoftwareRenderer = class {
1035
1056
  nrm,
1036
1057
  [baseColor[0], baseColor[1], baseColor[2]],
1037
1058
  material,
1038
- di
1059
+ x,
1060
+ y
1061
+ );
1062
+ } else {
1063
+ const ndotl = Math.max(
1064
+ 0,
1065
+ nrm[0] * -L[0] + nrm[1] * -L[1] + nrm[2] * -L[2]
1039
1066
  );
1067
+ const lit = ambient + (1 - ambient) * ndotl;
1068
+ r = baseColor[0] * lit;
1069
+ g = baseColor[1] * lit;
1070
+ b = baseColor[2] * lit;
1040
1071
  }
1041
1072
  const emission = material.emissiveFactor ?? [0, 0, 0];
1042
1073
  const strength = material.emissiveStrength ?? 1;
@@ -1117,6 +1148,7 @@ var SoftwareRenderer = class {
1117
1148
  }
1118
1149
  }
1119
1150
  }
1151
+ return idx.length / 3;
1120
1152
  }
1121
1153
  };
1122
1154
  function edge(a, b, p) {
@@ -1502,15 +1534,16 @@ var RayOcclusion = class {
1502
1534
  if ((dc.mode ?? 4) !== 4 || dc.material.alphaMode === "BLEND") continue;
1503
1535
  if (dc.material.alphaMode === "MASK") continue;
1504
1536
  const count = dc.indices?.length ?? dc.positions.length / 3;
1505
- const vertex = (index) => {
1506
- const i = (dc.indices?.[index] ?? index) * 3;
1537
+ const worldVertices = [];
1538
+ for (let i = 0; i < dc.positions.length; i += 3) {
1507
1539
  const p = vec33.transformMat4(
1508
1540
  vec33.create(),
1509
1541
  [dc.positions[i], dc.positions[i + 1], dc.positions[i + 2]],
1510
1542
  dc.model
1511
1543
  );
1512
- return [p[0], p[1], p[2]];
1513
- };
1544
+ worldVertices.push([p[0], p[1], p[2]]);
1545
+ }
1546
+ const vertex = (index) => worldVertices[dc.indices?.[index] ?? index];
1514
1547
  for (let i = 0; i < count; i += 3) {
1515
1548
  const p = vertex(i), q = vertex(i + 1), r = vertex(i + 2);
1516
1549
  const min = [0, 1, 2].map(
@@ -1586,52 +1619,97 @@ var RayOcclusion = class {
1586
1619
  }
1587
1620
  }
1588
1621
  build(triangles) {
1589
- const min = [Infinity, Infinity, Infinity], max = [-Infinity, -Infinity, -Infinity];
1622
+ const min = [Infinity, Infinity, Infinity], max = [-Infinity, -Infinity, -Infinity], centerMin = [Infinity, Infinity, Infinity], centerMax = [-Infinity, -Infinity, -Infinity];
1590
1623
  for (const t of triangles)
1591
1624
  for (let a = 0; a < 3; a++) {
1592
1625
  min[a] = Math.min(min[a], t.min[a]);
1593
1626
  max[a] = Math.max(max[a], t.max[a]);
1627
+ centerMin[a] = Math.min(centerMin[a], t.center[a]);
1628
+ centerMax[a] = Math.max(centerMax[a], t.center[a]);
1594
1629
  }
1595
1630
  if (triangles.length <= 8) return { min, max, triangles };
1631
+ const binCount = 16;
1596
1632
  const area = (lo, hi) => {
1597
1633
  const x = hi[0] - lo[0], y = hi[1] - lo[1], z = hi[2] - lo[2];
1598
1634
  return 2 * (x * y + y * z + z * x);
1599
1635
  };
1600
- let bestCost = Infinity, middle = triangles.length >> 1, best = triangles;
1636
+ let bestCost = Infinity, bestAxis = -1, bestSplit = 0, bestMin = 0, bestScale = 0;
1637
+ let longestAxis = 0;
1638
+ for (let a = 1; a < 3; a++)
1639
+ if (centerMax[a] - centerMin[a] > centerMax[longestAxis] - centerMin[longestAxis])
1640
+ longestAxis = a;
1601
1641
  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--) {
1642
+ if (triangles.length <= 4096 && axis !== longestAxis) continue;
1643
+ const loCenter = centerMin[axis], hiCenter = centerMax[axis];
1644
+ if (loCenter === hiCenter) continue;
1645
+ const scale = binCount / (hiCenter - loCenter);
1646
+ const counts = new Uint32Array(binCount);
1647
+ const bounds = new Float64Array(binCount * 6);
1648
+ for (let i = 0; i < binCount; i++) {
1649
+ bounds.fill(Infinity, i * 6, i * 6 + 3);
1650
+ bounds.fill(-Infinity, i * 6 + 3, i * 6 + 6);
1651
+ }
1652
+ for (const t of triangles) {
1653
+ const bin = Math.min(
1654
+ binCount - 1,
1655
+ Math.floor((t.center[axis] - loCenter) * scale)
1656
+ );
1657
+ counts[bin]++;
1658
+ const offset = bin * 6;
1606
1659
  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]);
1660
+ bounds[offset + a] = Math.min(bounds[offset + a], t.min[a]);
1661
+ bounds[offset + a + 3] = Math.max(bounds[offset + a + 3], t.max[a]);
1609
1662
  }
1610
- suffix[i] = area(lo, hi);
1663
+ }
1664
+ const suffixCost = new Float64Array(binCount);
1665
+ let lo = [Infinity, Infinity, Infinity], hi = [-Infinity, -Infinity, -Infinity], count = 0;
1666
+ for (let i = binCount - 1; i >= 0; i--) {
1667
+ count += counts[i];
1668
+ if (counts[i])
1669
+ for (let a = 0; a < 3; a++) {
1670
+ lo[a] = Math.min(lo[a], bounds[i * 6 + a]);
1671
+ hi[a] = Math.max(hi[a], bounds[i * 6 + a + 3]);
1672
+ }
1673
+ suffixCost[i] = count ? area(lo, hi) * count : 0;
1611
1674
  }
1612
1675
  lo = [Infinity, Infinity, Infinity];
1613
1676
  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);
1677
+ count = 0;
1678
+ for (let i = 0; i < binCount - 1; i++) {
1679
+ count += counts[i];
1680
+ if (counts[i])
1681
+ for (let a = 0; a < 3; a++) {
1682
+ lo[a] = Math.min(lo[a], bounds[i * 6 + a]);
1683
+ hi[a] = Math.max(hi[a], bounds[i * 6 + a + 3]);
1684
+ }
1685
+ if (!count || count === triangles.length) continue;
1686
+ const cost = area(lo, hi) * count + suffixCost[i + 1];
1622
1687
  if (cost < bestCost) {
1623
1688
  bestCost = cost;
1624
- middle = i;
1625
- best = sorted;
1689
+ bestAxis = axis;
1690
+ bestSplit = i;
1691
+ bestMin = loCenter;
1692
+ bestScale = scale;
1626
1693
  }
1627
1694
  }
1628
1695
  }
1629
- return {
1630
- min,
1631
- max,
1632
- left: this.build(best.slice(0, middle)),
1633
- right: this.build(best.slice(middle))
1634
- };
1696
+ let left, right;
1697
+ if (bestAxis < 0) {
1698
+ const middle = triangles.length >> 1;
1699
+ left = triangles.slice(0, middle);
1700
+ right = triangles.slice(middle);
1701
+ } else {
1702
+ left = [];
1703
+ right = [];
1704
+ for (const t of triangles) {
1705
+ const bin = Math.min(
1706
+ binCount - 1,
1707
+ Math.floor((t.center[bestAxis] - bestMin) * bestScale)
1708
+ );
1709
+ (bin <= bestSplit ? left : right).push(t);
1710
+ }
1711
+ }
1712
+ return { min, max, left: this.build(left), right: this.build(right) };
1635
1713
  }
1636
1714
  prepareRay(direction) {
1637
1715
  this.parallelAxes = 0;
@@ -1766,6 +1844,9 @@ var W = 96;
1766
1844
  var H = 48;
1767
1845
  var SAMPLES = 128;
1768
1846
  var ROUGHNESSES = [0.04, 0.1, 0.18, 0.28, 0.4, 0.55, 0.7, 0.85, 1];
1847
+ var SHADOW_SAMPLES = 4;
1848
+ var REFLECTION_SAMPLES = 4;
1849
+ var DIFFUSE_SAMPLES = 2;
1769
1850
  var lobes = studio_environment_default.lobes.map((l) => ({
1770
1851
  ...l,
1771
1852
  direction: normalize(l.direction),
@@ -1898,6 +1979,22 @@ var StudioLighting = class {
1898
1979
  eye;
1899
1980
  shadowDirections;
1900
1981
  diffuseCache = /* @__PURE__ */ new WeakMap();
1982
+ reflectionSamples = /* @__PURE__ */ new Map();
1983
+ normalDiffuseCache = /* @__PURE__ */ new WeakMap();
1984
+ specularCache = /* @__PURE__ */ new Map();
1985
+ blendedSpecular(rough, level, blend) {
1986
+ const cached = this.specularCache.get(rough);
1987
+ if (cached) return cached;
1988
+ if (this.specularCache.size >= 16) return null;
1989
+ const tables = lobes.map((_, j) => {
1990
+ const lo = this.tables.specular[level][j], hi = this.tables.specular[level + 1][j], table = new Float64Array(W * H);
1991
+ for (let i = 0; i < table.length; i++)
1992
+ table[i] = lo[i] * (1 - blend) + hi[i] * blend;
1993
+ return table;
1994
+ });
1995
+ this.specularCache.set(rough, tables);
1996
+ return tables;
1997
+ }
1901
1998
  constructor(drawCalls, camera) {
1902
1999
  this.occlusion = new RayOcclusion(drawCalls);
1903
2000
  const world = mat46.invert(mat46.create(), camera.view);
@@ -1906,8 +2003,8 @@ var StudioLighting = class {
1906
2003
  this.eye = [world[12], world[13], world[14]];
1907
2004
  this.shadowDirections = lobes.map((l) => {
1908
2005
  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);
2006
+ return Array.from({ length: SHADOW_SAMPLES }, (_, i) => {
2007
+ 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
2008
  const direction = orient(
1912
2009
  sin * Math.cos(phi),
1913
2010
  sin * Math.sin(phi),
@@ -1965,15 +2062,16 @@ var StudioLighting = class {
1965
2062
  );
1966
2063
  const pref = [...studio_environment_default.ambient];
1967
2064
  const reflectedLookup = tableLookup(reflected);
2065
+ const specularTables = this.blendedSpecular(rough, level, blend);
1968
2066
  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;
2067
+ 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
2068
  for (let k = 0; k < 3; k++) pref[k] += value * lobes[j].color[k];
1971
2069
  }
1972
2070
  for (let k = 0; k < 3; k++)
1973
2071
  radiance[k] += pref[k] * ((0.04 * (1 - metal) + base[k] * metal) * A + B);
1974
2072
  return radiance;
1975
2073
  }
1976
- shade(position, n, base, material, sampleIndex = 0) {
2074
+ shade(position, n, base, material, pixelX = 0, pixelY = 0) {
1977
2075
  const v = normalize([
1978
2076
  this.eye[0] - position[0],
1979
2077
  this.eye[1] - position[1],
@@ -2005,12 +2103,20 @@ var StudioLighting = class {
2005
2103
  const specColor = f0.map(
2006
2104
  (c, j) => studio_environment_default.ambient[j] * (c * A + B)
2007
2105
  );
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;
2106
+ const gradient = (0.06711056 * pixelX + 583715e-8 * pixelY) % 1;
2107
+ const hash = 52.9829189 * gradient % 1;
2012
2108
  const phase = hash * 2 * Math.PI, cosPhase = Math.cos(phase), sinPhase = Math.sin(phase);
2013
- const normalLookup = tableLookup(n), reflectedLookup = tableLookup(reflected);
2109
+ let normalDiffuse = this.normalDiffuseCache.get(material);
2110
+ if (!normalDiffuse || normalDiffuse.normal.some((value, k) => !Object.is(value, n[k]))) {
2111
+ const lookup = tableLookup(n);
2112
+ normalDiffuse = {
2113
+ normal: [...n],
2114
+ values: this.tables.diffuse.map((table) => sampleTable(table, lookup))
2115
+ };
2116
+ this.normalDiffuseCache.set(material, normalDiffuse);
2117
+ }
2118
+ const reflectedLookup = tableLookup(reflected);
2119
+ const specularTables = this.blendedSpecular(rough, level, blend);
2014
2120
  for (let j = 0; j < lobes.length; j++) {
2015
2121
  const dirs = this.shadowDirections[j];
2016
2122
  let visible = 0, total = 0;
@@ -2025,8 +2131,8 @@ var StudioLighting = class {
2025
2131
  if (weight > 0 && !this.occlusion.occluded(origin, l)) visible += weight;
2026
2132
  }
2027
2133
  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;
2134
+ const diffuse = normalDiffuse.values[j] * visibility;
2135
+ 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
2136
  for (let k = 0; k < 3; k++) {
2031
2137
  color[k] += lobes[j].color[k] * base[k] * (1 - metal) * 0.96 * diffuse;
2032
2138
  specColor[k] += lobes[j].color[k] * (f0[k] * A + B) * specular;
@@ -2034,8 +2140,8 @@ var StudioLighting = class {
2034
2140
  }
2035
2141
  const [t, b] = basis(n);
2036
2142
  let blocked = 0;
2037
- for (let i = 0; metal < 1 && i < 4; i++) {
2038
- const phi = i * Math.PI / 2 + phase, l = orient(
2143
+ for (let i = 0; metal < 1 && i < DIFFUSE_SAMPLES; i++) {
2144
+ const phi = i * 2 * Math.PI / DIFFUSE_SAMPLES + phase, l = orient(
2039
2145
  0.7 * Math.cos(phi),
2040
2146
  0.7 * Math.sin(phi),
2041
2147
  Math.sqrt(0.51),
@@ -2048,20 +2154,26 @@ var StudioLighting = class {
2048
2154
  blocked++;
2049
2155
  const bounce2 = this.bounceRadiance(hit, l);
2050
2156
  for (let k = 0; k < 3; k++)
2051
- color[k] += bounce2[k] * base[k] * (1 - metal) * 0.96 / 4;
2157
+ color[k] += bounce2[k] * base[k] * (1 - metal) * 0.96 / DIFFUSE_SAMPLES;
2052
2158
  }
2053
2159
  }
2054
- const ao = 1 - blocked / 4;
2160
+ const ao = 1 - blocked / DIFFUSE_SAMPLES;
2055
2161
  for (let k = 0; k < 3; k++)
2056
2162
  color[k] -= studio_environment_default.ambient[k] * (1 - ao) * base[k] * (1 - metal) * 0.96;
2057
2163
  const [rt, rb] = basis(reflected), bounce = [0, 0, 0];
2058
2164
  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);
2165
+ let samples = this.reflectionSamples.get(rough);
2166
+ if (!samples) {
2167
+ samples = Array.from({ length: REFLECTION_SAMPLES }, (_, i) => {
2168
+ 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);
2169
+ return [sin * Math.cos(phi), sin * Math.sin(phi), nh];
2170
+ });
2171
+ this.reflectionSamples.set(rough, samples);
2172
+ }
2173
+ for (const [sx, sy, nh] of samples) {
2062
2174
  const h = orient(
2063
- sin * Math.cos(phi),
2064
- sin * Math.sin(phi),
2175
+ sx * cosPhase - sy * sinPhase,
2176
+ sx * sinPhase + sy * cosPhase,
2065
2177
  nh,
2066
2178
  reflected,
2067
2179
  rt,
@@ -2170,8 +2282,29 @@ function renderDrawCalls(drawCalls, optionsInput = {}, imageFactory = createUint
2170
2282
  (dc) => (dc.material.alphaMode ?? "OPAQUE") === "BLEND"
2171
2283
  );
2172
2284
  const studio = options.realistic ? new StudioLighting(allDrawCalls, camera) : void 0;
2173
- const renderGroup = (dcs) => {
2174
- for (const dc of dcs) {
2285
+ let winners;
2286
+ const triangleOffsets = [];
2287
+ if (studio && opaqueDraws.length > 1 && opaqueDraws.every((dc) => dc.mode !== 1)) {
2288
+ winners = new Int32Array(renderWidth * renderHeight).fill(-1);
2289
+ let triangleOffset = 0;
2290
+ for (const dc of opaqueDraws) {
2291
+ triangleOffsets.push(triangleOffset);
2292
+ triangleOffset += renderer.drawMesh(
2293
+ dc,
2294
+ camera,
2295
+ { dir: options.lightDir, ambient: options.ambient },
2296
+ dc.material,
2297
+ options.cull,
2298
+ options.gamma,
2299
+ studio,
2300
+ { winners, triangleOffset, depthOnly: true }
2301
+ );
2302
+ }
2303
+ renderer.depth.fill(Infinity);
2304
+ }
2305
+ const renderGroup = (dcs, opaque = false) => {
2306
+ for (let i = 0; i < dcs.length; i++) {
2307
+ const dc = dcs[i];
2175
2308
  if (dc.mode === 1) {
2176
2309
  renderer.drawLines(dc, camera, options.gamma);
2177
2310
  } else {
@@ -2182,12 +2315,13 @@ function renderDrawCalls(drawCalls, optionsInput = {}, imageFactory = createUint
2182
2315
  dc.material,
2183
2316
  options.cull,
2184
2317
  options.gamma,
2185
- studio
2318
+ studio,
2319
+ opaque && winners ? { winners, triangleOffset: triangleOffsets[i], depthOnly: false } : void 0
2186
2320
  );
2187
2321
  }
2188
2322
  }
2189
2323
  };
2190
- renderGroup(opaqueDraws);
2324
+ renderGroup(opaqueDraws, true);
2191
2325
  renderGroup(maskDraws);
2192
2326
  if (infiniteGridOptions) {
2193
2327
  const aabb = computeWorldAABB(drawCalls);
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.34",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "https://github.com/tscircuit/poppygl.git"