poppygl 0.0.32 → 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
@@ -15,6 +15,9 @@ interface Material {
15
15
  baseColorTexture: BitmapLike | null;
16
16
  metallicFactor?: number;
17
17
  roughnessFactor?: number;
18
+ emissiveFactor?: [number, number, number];
19
+ emissiveTexture?: BitmapLike | null;
20
+ emissiveStrength?: number;
18
21
  alphaMode?: "OPAQUE" | "MASK" | "BLEND";
19
22
  alphaCutoff?: number;
20
23
  }
@@ -178,9 +181,13 @@ declare class StudioLighting {
178
181
  private eye;
179
182
  private shadowDirections;
180
183
  private diffuseCache;
184
+ private reflectionSamples;
185
+ private normalDiffuseCache;
186
+ private specularCache;
187
+ private blendedSpecular;
181
188
  constructor(drawCalls: DrawCall[], camera: Camera);
182
189
  private bounceRadiance;
183
- 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;
184
191
  }
185
192
 
186
193
  interface LightSettings {
@@ -209,7 +216,11 @@ declare class SoftwareRenderer {
209
216
  drawLines(mesh: DrawCall, camera: Camera, gammaOut?: boolean, options?: LineRenderOptions): void;
210
217
  sampleTextureNearest(img: BitmapLike | null, u: number, v: number): MutableRGBA;
211
218
  perspInterp(attrs: number[][], invWs: number[], lambdas: number[]): number[];
212
- 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;
213
224
  }
214
225
 
215
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,9 +1056,36 @@ 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]
1066
+ );
1067
+ const lit = ambient + (1 - ambient) * ndotl;
1068
+ r = baseColor[0] * lit;
1069
+ g = baseColor[1] * lit;
1070
+ b = baseColor[2] * lit;
1071
+ }
1072
+ const emission = material.emissiveFactor ?? [0, 0, 0];
1073
+ const strength = material.emissiveStrength ?? 1;
1074
+ let er = emission[0], eg = emission[1], eb = emission[2];
1075
+ if (uv && material.emissiveTexture) {
1076
+ const uvp = this.perspInterp(uv, invW, [l0, l1, l2]);
1077
+ const texel = this.sampleTextureNearest(
1078
+ material.emissiveTexture,
1079
+ uvp[0],
1080
+ uvp[1]
1039
1081
  );
1082
+ er *= srgbDecodeToLinear01(texel[0]);
1083
+ eg *= srgbDecodeToLinear01(texel[1]);
1084
+ eb *= srgbDecodeToLinear01(texel[2]);
1040
1085
  }
1086
+ r += er * strength;
1087
+ g += eg * strength;
1088
+ b += eb * strength;
1041
1089
  let a = baseColor[3];
1042
1090
  const linearR = r, linearG = g, linearB = b;
1043
1091
  const alphaMode = material.alphaMode ?? "OPAQUE";
@@ -1100,6 +1148,7 @@ var SoftwareRenderer = class {
1100
1148
  }
1101
1149
  }
1102
1150
  }
1151
+ return idx.length / 3;
1103
1152
  }
1104
1153
  };
1105
1154
  function edge(a, b, p) {
@@ -1485,15 +1534,16 @@ var RayOcclusion = class {
1485
1534
  if ((dc.mode ?? 4) !== 4 || dc.material.alphaMode === "BLEND") continue;
1486
1535
  if (dc.material.alphaMode === "MASK") continue;
1487
1536
  const count = dc.indices?.length ?? dc.positions.length / 3;
1488
- const vertex = (index) => {
1489
- const i = (dc.indices?.[index] ?? index) * 3;
1537
+ const worldVertices = [];
1538
+ for (let i = 0; i < dc.positions.length; i += 3) {
1490
1539
  const p = vec33.transformMat4(
1491
1540
  vec33.create(),
1492
1541
  [dc.positions[i], dc.positions[i + 1], dc.positions[i + 2]],
1493
1542
  dc.model
1494
1543
  );
1495
- return [p[0], p[1], p[2]];
1496
- };
1544
+ worldVertices.push([p[0], p[1], p[2]]);
1545
+ }
1546
+ const vertex = (index) => worldVertices[dc.indices?.[index] ?? index];
1497
1547
  for (let i = 0; i < count; i += 3) {
1498
1548
  const p = vertex(i), q = vertex(i + 1), r = vertex(i + 2);
1499
1549
  const min = [0, 1, 2].map(
@@ -1569,52 +1619,97 @@ var RayOcclusion = class {
1569
1619
  }
1570
1620
  }
1571
1621
  build(triangles) {
1572
- 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];
1573
1623
  for (const t of triangles)
1574
1624
  for (let a = 0; a < 3; a++) {
1575
1625
  min[a] = Math.min(min[a], t.min[a]);
1576
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]);
1577
1629
  }
1578
1630
  if (triangles.length <= 8) return { min, max, triangles };
1631
+ const binCount = 16;
1579
1632
  const area = (lo, hi) => {
1580
1633
  const x = hi[0] - lo[0], y = hi[1] - lo[1], z = hi[2] - lo[2];
1581
1634
  return 2 * (x * y + y * z + z * x);
1582
1635
  };
1583
- 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;
1584
1641
  for (let axis = 0; axis < 3; axis++) {
1585
- const sorted = triangles.slice().sort((a, b) => a.center[axis] - b.center[axis]);
1586
- const suffix = new Float64Array(sorted.length);
1587
- let lo = [Infinity, Infinity, Infinity], hi = [-Infinity, -Infinity, -Infinity];
1588
- 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;
1589
1659
  for (let a = 0; a < 3; a++) {
1590
- lo[a] = Math.min(lo[a], sorted[i].min[a]);
1591
- 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]);
1592
1662
  }
1593
- 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;
1594
1674
  }
1595
1675
  lo = [Infinity, Infinity, Infinity];
1596
1676
  hi = [-Infinity, -Infinity, -Infinity];
1597
- for (let i = 1; i < sorted.length; i++) {
1598
- const triangle = sorted[i - 1];
1599
- for (let a = 0; a < 3; a++) {
1600
- lo[a] = Math.min(lo[a], triangle.min[a]);
1601
- hi[a] = Math.max(hi[a], triangle.max[a]);
1602
- }
1603
- if (triangle.center[axis] === sorted[i].center[axis]) continue;
1604
- 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];
1605
1687
  if (cost < bestCost) {
1606
1688
  bestCost = cost;
1607
- middle = i;
1608
- best = sorted;
1689
+ bestAxis = axis;
1690
+ bestSplit = i;
1691
+ bestMin = loCenter;
1692
+ bestScale = scale;
1609
1693
  }
1610
1694
  }
1611
1695
  }
1612
- return {
1613
- min,
1614
- max,
1615
- left: this.build(best.slice(0, middle)),
1616
- right: this.build(best.slice(middle))
1617
- };
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) };
1618
1713
  }
1619
1714
  prepareRay(direction) {
1620
1715
  this.parallelAxes = 0;
@@ -1749,6 +1844,9 @@ var W = 96;
1749
1844
  var H = 48;
1750
1845
  var SAMPLES = 128;
1751
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;
1752
1850
  var lobes = studio_environment_default.lobes.map((l) => ({
1753
1851
  ...l,
1754
1852
  direction: normalize(l.direction),
@@ -1881,6 +1979,22 @@ var StudioLighting = class {
1881
1979
  eye;
1882
1980
  shadowDirections;
1883
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
+ }
1884
1998
  constructor(drawCalls, camera) {
1885
1999
  this.occlusion = new RayOcclusion(drawCalls);
1886
2000
  const world = mat46.invert(mat46.create(), camera.view);
@@ -1889,8 +2003,8 @@ var StudioLighting = class {
1889
2003
  this.eye = [world[12], world[13], world[14]];
1890
2004
  this.shadowDirections = lobes.map((l) => {
1891
2005
  const [t, b] = basis(l.direction);
1892
- return Array.from({ length: 8 }, (_, i) => {
1893
- 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);
1894
2008
  const direction = orient(
1895
2009
  sin * Math.cos(phi),
1896
2010
  sin * Math.sin(phi),
@@ -1948,15 +2062,16 @@ var StudioLighting = class {
1948
2062
  );
1949
2063
  const pref = [...studio_environment_default.ambient];
1950
2064
  const reflectedLookup = tableLookup(reflected);
2065
+ const specularTables = this.blendedSpecular(rough, level, blend);
1951
2066
  for (let j = 0; j < lobes.length; j++) {
1952
- 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;
1953
2068
  for (let k = 0; k < 3; k++) pref[k] += value * lobes[j].color[k];
1954
2069
  }
1955
2070
  for (let k = 0; k < 3; k++)
1956
2071
  radiance[k] += pref[k] * ((0.04 * (1 - metal) + base[k] * metal) * A + B);
1957
2072
  return radiance;
1958
2073
  }
1959
- shade(position, n, base, material, sampleIndex = 0) {
2074
+ shade(position, n, base, material, pixelX = 0, pixelY = 0) {
1960
2075
  const v = normalize([
1961
2076
  this.eye[0] - position[0],
1962
2077
  this.eye[1] - position[1],
@@ -1988,12 +2103,20 @@ var StudioLighting = class {
1988
2103
  const specColor = f0.map(
1989
2104
  (c, j) => studio_environment_default.ambient[j] * (c * A + B)
1990
2105
  );
1991
- let bits = sampleIndex + 1;
1992
- bits = Math.imul(bits ^ bits >>> 16, 2146121005);
1993
- bits = Math.imul(bits ^ bits >>> 15, 2221713035);
1994
- const hash = ((bits ^ bits >>> 16) >>> 0) / 4294967296;
2106
+ const gradient = (0.06711056 * pixelX + 583715e-8 * pixelY) % 1;
2107
+ const hash = 52.9829189 * gradient % 1;
1995
2108
  const phase = hash * 2 * Math.PI, cosPhase = Math.cos(phase), sinPhase = Math.sin(phase);
1996
- 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);
1997
2120
  for (let j = 0; j < lobes.length; j++) {
1998
2121
  const dirs = this.shadowDirections[j];
1999
2122
  let visible = 0, total = 0;
@@ -2008,8 +2131,8 @@ var StudioLighting = class {
2008
2131
  if (weight > 0 && !this.occlusion.occluded(origin, l)) visible += weight;
2009
2132
  }
2010
2133
  const visibility = total > 0 ? visible / total : 1;
2011
- const diffuse = sampleTable(this.tables.diffuse[j], normalLookup) * visibility;
2012
- 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;
2013
2136
  for (let k = 0; k < 3; k++) {
2014
2137
  color[k] += lobes[j].color[k] * base[k] * (1 - metal) * 0.96 * diffuse;
2015
2138
  specColor[k] += lobes[j].color[k] * (f0[k] * A + B) * specular;
@@ -2017,8 +2140,8 @@ var StudioLighting = class {
2017
2140
  }
2018
2141
  const [t, b] = basis(n);
2019
2142
  let blocked = 0;
2020
- for (let i = 0; metal < 1 && i < 4; i++) {
2021
- 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(
2022
2145
  0.7 * Math.cos(phi),
2023
2146
  0.7 * Math.sin(phi),
2024
2147
  Math.sqrt(0.51),
@@ -2031,20 +2154,26 @@ var StudioLighting = class {
2031
2154
  blocked++;
2032
2155
  const bounce2 = this.bounceRadiance(hit, l);
2033
2156
  for (let k = 0; k < 3; k++)
2034
- color[k] += bounce2[k] * base[k] * (1 - metal) * 0.96 / 4;
2157
+ color[k] += bounce2[k] * base[k] * (1 - metal) * 0.96 / DIFFUSE_SAMPLES;
2035
2158
  }
2036
2159
  }
2037
- const ao = 1 - blocked / 4;
2160
+ const ao = 1 - blocked / DIFFUSE_SAMPLES;
2038
2161
  for (let k = 0; k < 3; k++)
2039
2162
  color[k] -= studio_environment_default.ambient[k] * (1 - ao) * base[k] * (1 - metal) * 0.96;
2040
2163
  const [rt, rb] = basis(reflected), bounce = [0, 0, 0];
2041
2164
  let hitWeight = 0, totalWeight = 0;
2042
- for (let i = 0; i < 8; i++) {
2043
- const u = (i + 0.5) / 8, phi = 2 * Math.PI * radicalInverse(i) + phase, a = rough * rough;
2044
- 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) {
2045
2174
  const h = orient(
2046
- sin * Math.cos(phi),
2047
- sin * Math.sin(phi),
2175
+ sx * cosPhase - sy * sinPhase,
2176
+ sx * sinPhase + sy * cosPhase,
2048
2177
  nh,
2049
2178
  reflected,
2050
2179
  rt,
@@ -2153,8 +2282,29 @@ function renderDrawCalls(drawCalls, optionsInput = {}, imageFactory = createUint
2153
2282
  (dc) => (dc.material.alphaMode ?? "OPAQUE") === "BLEND"
2154
2283
  );
2155
2284
  const studio = options.realistic ? new StudioLighting(allDrawCalls, camera) : void 0;
2156
- const renderGroup = (dcs) => {
2157
- 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];
2158
2308
  if (dc.mode === 1) {
2159
2309
  renderer.drawLines(dc, camera, options.gamma);
2160
2310
  } else {
@@ -2165,12 +2315,13 @@ function renderDrawCalls(drawCalls, optionsInput = {}, imageFactory = createUint
2165
2315
  dc.material,
2166
2316
  options.cull,
2167
2317
  options.gamma,
2168
- studio
2318
+ studio,
2319
+ opaque && winners ? { winners, triangleOffset: triangleOffsets[i], depthOnly: false } : void 0
2169
2320
  );
2170
2321
  }
2171
2322
  }
2172
2323
  };
2173
- renderGroup(opaqueDraws);
2324
+ renderGroup(opaqueDraws, true);
2174
2325
  renderGroup(maskDraws);
2175
2326
  if (infiniteGridOptions) {
2176
2327
  const aabb = computeWorldAABB(drawCalls);
@@ -2519,6 +2670,9 @@ function getMaterial(gltf, textures, images, materialIndex) {
2519
2670
  baseColorTexture: texImg,
2520
2671
  metallicFactor: pbr.metallicFactor ?? 1,
2521
2672
  roughnessFactor: pbr.roughnessFactor ?? 1,
2673
+ emissiveFactor: material.emissiveFactor ?? [0, 0, 0],
2674
+ emissiveTexture: images[textures[material.emissiveTexture?.index]?.source] ?? null,
2675
+ emissiveStrength: material.extensions?.KHR_materials_emissive_strength?.emissiveStrength ?? 1,
2522
2676
  alphaMode,
2523
2677
  alphaCutoff
2524
2678
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "poppygl",
3
3
  "main": "dist/index.js",
4
- "version": "0.0.32",
4
+ "version": "0.0.34",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "https://github.com/tscircuit/poppygl.git"