poppygl 0.0.31 → 0.0.32

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/dist/index.d.ts CHANGED
@@ -177,6 +177,7 @@ declare class StudioLighting {
177
177
  private occlusion;
178
178
  private eye;
179
179
  private shadowDirections;
180
+ private diffuseCache;
180
181
  constructor(drawCalls: DrawCall[], camera: Camera);
181
182
  private bounceRadiance;
182
183
  shade(position: V3, n: V3, base: V3, material: Material, sampleIndex?: number): V3;
package/dist/index.js CHANGED
@@ -1466,8 +1466,19 @@ var studio_environment_default = {
1466
1466
  // lib/render/RayOcclusion.ts
1467
1467
  import { vec3 as vec33 } from "gl-matrix";
1468
1468
  var RayOcclusion = class {
1469
- root;
1469
+ bounds = new Float64Array(0);
1470
+ escapes = new Uint32Array(0);
1471
+ starts = new Uint32Array(0);
1472
+ counts = new Uint8Array(0);
1473
+ triangleData = new Float64Array(0);
1474
+ triangles = [];
1470
1475
  bias;
1476
+ inverse = [0, 0, 0];
1477
+ parallelAxes = 0;
1478
+ closest = {
1479
+ hit: null,
1480
+ limit: Infinity
1481
+ };
1471
1482
  constructor(drawCalls) {
1472
1483
  const triangles = [];
1473
1484
  for (const dc of drawCalls) {
@@ -1491,10 +1502,19 @@ var RayOcclusion = class {
1491
1502
  const max = [0, 1, 2].map(
1492
1503
  (a) => Math.max(p[a], q[a], r[a])
1493
1504
  );
1505
+ const e1 = q.map((v, a) => v - p[a]), e2 = r.map((v, a) => v - p[a]);
1506
+ const normal = [
1507
+ e1[1] * e2[2] - e1[2] * e2[1],
1508
+ e1[2] * e2[0] - e1[0] * e2[2],
1509
+ e1[0] * e2[1] - e1[1] * e2[0]
1510
+ ];
1511
+ const length = Math.hypot(...normal) || 1;
1512
+ for (let a = 0; a < 3; a++) normal[a] /= length;
1494
1513
  triangles.push({
1495
1514
  p,
1496
- e1: q.map((v, a) => v - p[a]),
1497
- e2: r.map((v, a) => v - p[a]),
1515
+ e1,
1516
+ e2,
1517
+ normal,
1498
1518
  min,
1499
1519
  max,
1500
1520
  center: min.map((v, a) => (v + max[a]) / 2),
@@ -1502,10 +1522,52 @@ var RayOcclusion = class {
1502
1522
  });
1503
1523
  }
1504
1524
  }
1505
- this.root = triangles.length ? this.build(triangles) : null;
1506
- const extent = this.root ? Math.hypot(...this.root.max.map((v, a) => v - this.root.min[a])) : 1;
1525
+ const root = triangles.length ? this.build(triangles) : null;
1526
+ if (root) this.flatten(root, triangles.length);
1527
+ const extent = root ? Math.hypot(...root.max.map((v, a) => v - root.min[a])) : 1;
1507
1528
  this.bias = Math.max(1e-7, extent * 1e-5);
1508
1529
  }
1530
+ /** Preorder bounds with subtree escape offsets retain the existing SAH
1531
+ * primitive order, while avoiding recursive calls and pointer-heavy nodes
1532
+ * during millions of ray queries. Float64 retains the original precision.
1533
+ */
1534
+ flatten(root, triangleCount) {
1535
+ const nodes = [], escapes = [];
1536
+ const visit = (node) => {
1537
+ const index = nodes.length;
1538
+ nodes.push(node);
1539
+ escapes.push(0);
1540
+ if (!node.triangles) {
1541
+ visit(node.left);
1542
+ visit(node.right);
1543
+ }
1544
+ escapes[index] = nodes.length;
1545
+ };
1546
+ visit(root);
1547
+ this.bounds = new Float64Array(nodes.length * 6);
1548
+ this.escapes = Uint32Array.from(escapes);
1549
+ this.starts = new Uint32Array(nodes.length);
1550
+ this.counts = new Uint8Array(nodes.length);
1551
+ this.triangleData = new Float64Array(triangleCount * 9);
1552
+ for (let i = 0; i < nodes.length; i++) {
1553
+ const node = nodes[i];
1554
+ this.bounds.set(node.min, i * 6);
1555
+ this.bounds.set(node.max, i * 6 + 3);
1556
+ if (!node.triangles) continue;
1557
+ this.starts[i] = this.triangles.length;
1558
+ this.counts[i] = node.triangles.length;
1559
+ for (const triangle of node.triangles) {
1560
+ const offset = this.triangles.length * 9;
1561
+ this.triangleData.set(triangle.p, offset);
1562
+ this.triangleData.set(triangle.e1, offset + 3);
1563
+ this.triangleData.set(triangle.e2, offset + 6);
1564
+ this.triangles.push({
1565
+ normal: triangle.normal,
1566
+ material: triangle.material
1567
+ });
1568
+ }
1569
+ }
1570
+ }
1509
1571
  build(triangles) {
1510
1572
  const min = [Infinity, Infinity, Infinity], max = [-Infinity, -Infinity, -Infinity];
1511
1573
  for (const t of triangles)
@@ -1554,78 +1616,128 @@ var RayOcclusion = class {
1554
1616
  right: this.build(best.slice(middle))
1555
1617
  };
1556
1618
  }
1619
+ prepareRay(direction) {
1620
+ this.parallelAxes = 0;
1621
+ for (let a = 0; a < 3; a++) {
1622
+ this.inverse[a] = 1 / direction[a];
1623
+ if (Math.abs(direction[a]) < 1e-12) this.parallelAxes |= 1 << a;
1624
+ }
1625
+ }
1557
1626
  occluded(origin, direction, maxDistance = Infinity) {
1558
- return this.root ? this.intersect(this.root, origin, direction, maxDistance) : false;
1627
+ this.prepareRay(direction);
1628
+ return this.intersect(origin, direction, maxDistance);
1559
1629
  }
1560
1630
  /** Closest opaque triangle for one-bounce reflections and diffuse fill. */
1561
1631
  trace(origin, direction, maxDistance = Infinity) {
1562
- const state = {
1563
- hit: null,
1564
- limit: maxDistance
1632
+ this.prepareRay(direction);
1633
+ const state = this.closest;
1634
+ state.hit = null;
1635
+ state.limit = maxDistance;
1636
+ this.intersect(origin, direction, maxDistance, state);
1637
+ if (!state.hit) return null;
1638
+ const { normal, material } = state.hit;
1639
+ const sign = dotNormal(normal, direction) > 0 ? -1 : 1;
1640
+ return {
1641
+ position: [
1642
+ origin[0] + direction[0] * state.limit,
1643
+ origin[1] + direction[1] * state.limit,
1644
+ origin[2] + direction[2] * state.limit
1645
+ ],
1646
+ normal: [normal[0] * sign, normal[1] * sign, normal[2] * sign],
1647
+ material,
1648
+ distance: state.limit
1565
1649
  };
1566
- if (this.root)
1567
- this.intersect(this.root, origin, direction, maxDistance, state);
1568
- return state.hit;
1569
1650
  }
1570
- intersect(node, o, d, limit, state) {
1571
- if (state) limit = state.limit;
1572
- let near = 0, far = limit;
1573
- for (let a = 0; a < 3; a++) {
1574
- if (Math.abs(d[a]) < 1e-12) {
1575
- if (o[a] < node.min[a] || o[a] > node.max[a]) return false;
1576
- continue;
1651
+ intersect(o, d, maxDistance, state) {
1652
+ const ox = o[0], oy = o[1], oz = o[2], dx = d[0], dy = d[1], dz = d[2], ix = this.inverse[0], iy = this.inverse[1], iz = this.inverse[2], parallel = this.parallelAxes, bounds = this.bounds, data = this.triangleData;
1653
+ let node = 0;
1654
+ while (node < this.counts.length) {
1655
+ const offset = node * 6;
1656
+ let limit = state ? state.limit : maxDistance, near = 0, far = limit;
1657
+ if (parallel & 2) {
1658
+ if (oy < bounds[offset + 1] || oy > bounds[offset + 4]) {
1659
+ node = this.escapes[node];
1660
+ continue;
1661
+ }
1662
+ } else {
1663
+ let t0 = (bounds[offset + 1] - oy) * iy, t1 = (bounds[offset + 4] - oy) * iy;
1664
+ if (t0 > t1) {
1665
+ const swap = t0;
1666
+ t0 = t1;
1667
+ t1 = swap;
1668
+ }
1669
+ if (t0 > near) near = t0;
1670
+ if (t1 < far) far = t1;
1671
+ if (far < near) {
1672
+ node = this.escapes[node];
1673
+ continue;
1674
+ }
1577
1675
  }
1578
- const t0 = (node.min[a] - o[a]) / d[a], t1 = (node.max[a] - o[a]) / d[a];
1579
- near = Math.max(near, Math.min(t0, t1));
1580
- far = Math.min(far, Math.max(t0, t1));
1581
- if (far < near) return false;
1582
- }
1583
- if (node.triangles) {
1584
- for (const { p, e1, e2, material } of node.triangles) {
1585
- const px = d[1] * e2[2] - d[2] * e2[1], py = d[2] * e2[0] - d[0] * e2[2], pz = d[0] * e2[1] - d[1] * e2[0];
1586
- const det = e1[0] * px + e1[1] * py + e1[2] * pz;
1676
+ if (parallel & 1) {
1677
+ if (ox < bounds[offset + 0] || ox > bounds[offset + 3]) {
1678
+ node = this.escapes[node];
1679
+ continue;
1680
+ }
1681
+ } else {
1682
+ let t0 = (bounds[offset + 0] - ox) * ix, t1 = (bounds[offset + 3] - ox) * ix;
1683
+ if (t0 > t1) {
1684
+ const swap = t0;
1685
+ t0 = t1;
1686
+ t1 = swap;
1687
+ }
1688
+ if (t0 > near) near = t0;
1689
+ if (t1 < far) far = t1;
1690
+ if (far < near) {
1691
+ node = this.escapes[node];
1692
+ continue;
1693
+ }
1694
+ }
1695
+ if (parallel & 4) {
1696
+ if (oz < bounds[offset + 2] || oz > bounds[offset + 5]) {
1697
+ node = this.escapes[node];
1698
+ continue;
1699
+ }
1700
+ } else {
1701
+ let t0 = (bounds[offset + 2] - oz) * iz, t1 = (bounds[offset + 5] - oz) * iz;
1702
+ if (t0 > t1) {
1703
+ const swap = t0;
1704
+ t0 = t1;
1705
+ t1 = swap;
1706
+ }
1707
+ if (t0 > near) near = t0;
1708
+ if (t1 < far) far = t1;
1709
+ if (far < near) {
1710
+ node = this.escapes[node];
1711
+ continue;
1712
+ }
1713
+ }
1714
+ const start = this.starts[node], end = start + this.counts[node];
1715
+ for (let i = start; i < end; i++) {
1716
+ const offset2 = i * 9, p0 = data[offset2], p1 = data[offset2 + 1], p2 = data[offset2 + 2], e10 = data[offset2 + 3], e11 = data[offset2 + 4], e12 = data[offset2 + 5], e20 = data[offset2 + 6], e21 = data[offset2 + 7], e22 = data[offset2 + 8];
1717
+ const px = dy * e22 - dz * e21, py = dz * e20 - dx * e22, pz = dx * e21 - dy * e20;
1718
+ const det = e10 * px + e11 * py + e12 * pz;
1587
1719
  if (Math.abs(det) < 1e-12) continue;
1588
- const inv = 1 / det, tx = o[0] - p[0], ty = o[1] - p[1], tz = o[2] - p[2];
1720
+ const inv = 1 / det, tx = ox - p0, ty = oy - p1, tz = oz - p2;
1589
1721
  const u = (tx * px + ty * py + tz * pz) * inv;
1590
1722
  if (u < 0 || u > 1) continue;
1591
- const qx = ty * e1[2] - tz * e1[1], qy = tz * e1[0] - tx * e1[2], qz = tx * e1[1] - ty * e1[0];
1592
- const v = (d[0] * qx + d[1] * qy + d[2] * qz) * inv;
1723
+ const qx = ty * e12 - tz * e11, qy = tz * e10 - tx * e12, qz = tx * e11 - ty * e10;
1724
+ const v = (dx * qx + dy * qy + dz * qz) * inv;
1593
1725
  if (v < 0 || u + v > 1) continue;
1594
- const distance = (e2[0] * qx + e2[1] * qy + e2[2] * qz) * inv;
1726
+ const distance = (e20 * qx + e21 * qy + e22 * qz) * inv;
1595
1727
  if (distance > this.bias * 0.25 && distance < limit) {
1596
1728
  if (!state) return true;
1597
- const normal = [
1598
- e1[1] * e2[2] - e1[2] * e2[1],
1599
- e1[2] * e2[0] - e1[0] * e2[2],
1600
- e1[0] * e2[1] - e1[1] * e2[0]
1601
- ];
1602
- const length = Math.hypot(...normal) || 1;
1603
- for (let a = 0; a < 3; a++) normal[a] /= length;
1604
- if (normal[0] * d[0] + normal[1] * d[1] + normal[2] * d[2] > 0)
1605
- for (let a = 0; a < 3; a++) normal[a] *= -1;
1606
- state.hit = {
1607
- position: [
1608
- o[0] + d[0] * distance,
1609
- o[1] + d[1] * distance,
1610
- o[2] + d[2] * distance
1611
- ],
1612
- normal,
1613
- material,
1614
- distance
1615
- };
1729
+ state.hit = this.triangles[i];
1616
1730
  state.limit = limit = distance;
1617
1731
  }
1618
1732
  }
1619
- return false;
1620
- }
1621
- if (state) {
1622
- this.intersect(node.left, o, d, limit, state);
1623
- this.intersect(node.right, o, d, state.limit, state);
1624
- return state.hit !== null;
1733
+ node++;
1625
1734
  }
1626
- return this.intersect(node.left, o, d, limit) || this.intersect(node.right, o, d, limit);
1735
+ return state?.hit != null;
1627
1736
  }
1628
1737
  };
1738
+ function dotNormal(normal, direction) {
1739
+ return normal[0] * direction[0] + normal[1] * direction[1] + normal[2] * direction[2];
1740
+ }
1629
1741
 
1630
1742
  // lib/render/StudioLighting.ts
1631
1743
  var dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
@@ -1669,7 +1781,7 @@ function orient(x, y, z, n, t, b) {
1669
1781
  t[2] * x + b[2] * y + n[2] * z
1670
1782
  ];
1671
1783
  }
1672
- function sampleTable(table, n) {
1784
+ function tableLookup(n) {
1673
1785
  const x = (Math.atan2(n[2], n[0]) / (2 * Math.PI) + 0.5) * W - 0.5;
1674
1786
  const y = Math.max(
1675
1787
  0,
@@ -1680,7 +1792,11 @@ function sampleTable(table, n) {
1680
1792
  );
1681
1793
  const ix = Math.floor(x), iy = Math.floor(y), fx = x - ix, fy = y - iy;
1682
1794
  const a = (ix % W + W) % W, b = (a + 1) % W, c = Math.min(H - 1, iy + 1);
1683
- return (table[iy * W + a] * (1 - fx) + table[iy * W + b] * fx) * (1 - fy) + (table[c * W + a] * (1 - fx) + table[c * W + b] * fx) * fy;
1795
+ return [iy * W + a, iy * W + b, c * W + a, c * W + b, fx, fy];
1796
+ }
1797
+ function sampleTable(table, lookup) {
1798
+ const [i00, i10, i01, i11, fx, fy] = lookup;
1799
+ return (table[i00] * (1 - fx) + table[i10] * fx) * (1 - fy) + (table[i01] * (1 - fx) + table[i11] * fx) * fy;
1684
1800
  }
1685
1801
  var cachedTables;
1686
1802
  function getTables() {
@@ -1764,6 +1880,7 @@ var StudioLighting = class {
1764
1880
  occlusion;
1765
1881
  eye;
1766
1882
  shadowDirections;
1883
+ diffuseCache = /* @__PURE__ */ new WeakMap();
1767
1884
  constructor(drawCalls, camera) {
1768
1885
  this.occlusion = new RayOcclusion(drawCalls);
1769
1886
  const world = mat46.invert(mat46.create(), camera.view);
@@ -1774,7 +1891,7 @@ var StudioLighting = class {
1774
1891
  const [t, b] = basis(l.direction);
1775
1892
  return Array.from({ length: 8 }, (_, i) => {
1776
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);
1777
- return orient(
1894
+ const direction = orient(
1778
1895
  sin * Math.cos(phi),
1779
1896
  sin * Math.sin(phi),
1780
1897
  cos,
@@ -1782,16 +1899,36 @@ var StudioLighting = class {
1782
1899
  t,
1783
1900
  b
1784
1901
  );
1902
+ const parallel = dot(direction, l.direction);
1903
+ return {
1904
+ parallel: l.direction.map((v) => v * parallel),
1905
+ perpendicular: direction.map(
1906
+ (v, k) => v - l.direction[k] * parallel
1907
+ ),
1908
+ cross: [
1909
+ l.direction[1] * direction[2] - l.direction[2] * direction[1],
1910
+ l.direction[2] * direction[0] - l.direction[0] * direction[2],
1911
+ l.direction[0] * direction[1] - l.direction[1] * direction[0]
1912
+ ]
1913
+ };
1785
1914
  });
1786
1915
  });
1787
1916
  }
1788
1917
  bounceRadiance(hit, incoming) {
1789
1918
  const metal = Math.max(0, Math.min(1, hit.material.metallicFactor ?? 1));
1790
- const irradiance = [...studio_environment_default.ambient];
1791
- for (let j = 0; j < lobes.length; j++) {
1792
- const value = sampleTable(this.tables.diffuse[j], hit.normal);
1793
- for (let k = 0; k < 3; k++) irradiance[k] += value * lobes[j].color[k];
1919
+ let cached = this.diffuseCache.get(hit.material);
1920
+ if (!cached || cached.normal.some((v, k) => !Object.is(v, hit.normal[k]))) {
1921
+ const irradiance2 = [...studio_environment_default.ambient];
1922
+ const lookup = tableLookup(hit.normal);
1923
+ for (let j = 0; j < lobes.length; j++) {
1924
+ const value = sampleTable(this.tables.diffuse[j], lookup);
1925
+ for (let k = 0; k < 3; k++)
1926
+ irradiance2[k] += value * lobes[j].color[k];
1927
+ }
1928
+ cached = { normal: [...hit.normal], irradiance: irradiance2 };
1929
+ this.diffuseCache.set(hit.material, cached);
1794
1930
  }
1931
+ const irradiance = cached.irradiance;
1795
1932
  const base = hit.material.baseColorFactor;
1796
1933
  const rough = Math.max(0.04, Math.min(1, hit.material.roughnessFactor ?? 1));
1797
1934
  const nv = Math.max(1e-3, -dot(hit.normal, incoming));
@@ -1810,8 +1947,9 @@ var StudioLighting = class {
1810
1947
  (c, k) => c * base[k] * (1 - metal) * 0.96
1811
1948
  );
1812
1949
  const pref = [...studio_environment_default.ambient];
1950
+ const reflectedLookup = tableLookup(reflected);
1813
1951
  for (let j = 0; j < lobes.length; j++) {
1814
- const value = sampleTable(this.tables.specular[level][j], reflected) * (1 - blend) + sampleTable(this.tables.specular[level + 1][j], reflected) * blend;
1952
+ const value = sampleTable(this.tables.specular[level][j], reflectedLookup) * (1 - blend) + sampleTable(this.tables.specular[level + 1][j], reflectedLookup) * blend;
1815
1953
  for (let k = 0; k < 3; k++) pref[k] += value * lobes[j].color[k];
1816
1954
  }
1817
1955
  for (let k = 0; k < 3; k++)
@@ -1855,27 +1993,23 @@ var StudioLighting = class {
1855
1993
  bits = Math.imul(bits ^ bits >>> 15, 2221713035);
1856
1994
  const hash = ((bits ^ bits >>> 16) >>> 0) / 4294967296;
1857
1995
  const phase = hash * 2 * Math.PI, cosPhase = Math.cos(phase), sinPhase = Math.sin(phase);
1996
+ const normalLookup = tableLookup(n), reflectedLookup = tableLookup(reflected);
1858
1997
  for (let j = 0; j < lobes.length; j++) {
1859
1998
  const dirs = this.shadowDirections[j];
1860
1999
  let visible = 0, total = 0;
1861
- const axis = lobes[j].direction;
1862
- for (const direction of dirs) {
1863
- const parallel = dot(direction, axis);
1864
- const cross = [
1865
- axis[1] * direction[2] - axis[2] * direction[1],
1866
- axis[2] * direction[0] - axis[0] * direction[2],
1867
- axis[0] * direction[1] - axis[1] * direction[0]
2000
+ for (const { parallel, perpendicular, cross } of dirs) {
2001
+ const l = [
2002
+ parallel[0] + perpendicular[0] * cosPhase + cross[0] * sinPhase,
2003
+ parallel[1] + perpendicular[1] * cosPhase + cross[1] * sinPhase,
2004
+ parallel[2] + perpendicular[2] * cosPhase + cross[2] * sinPhase
1868
2005
  ];
1869
- const l = direction.map(
1870
- (value, k) => axis[k] * parallel + (value - axis[k] * parallel) * cosPhase + cross[k] * sinPhase
1871
- );
1872
2006
  const weight = Math.max(0, dot(n, l));
1873
2007
  total += weight;
1874
2008
  if (weight > 0 && !this.occlusion.occluded(origin, l)) visible += weight;
1875
2009
  }
1876
2010
  const visibility = total > 0 ? visible / total : 1;
1877
- const diffuse = sampleTable(this.tables.diffuse[j], n) * visibility;
1878
- const specular = (sampleTable(this.tables.specular[level][j], reflected) * (1 - blend) + sampleTable(this.tables.specular[level + 1][j], reflected) * blend) * visibility;
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;
1879
2013
  for (let k = 0; k < 3; k++) {
1880
2014
  color[k] += lobes[j].color[k] * base[k] * (1 - metal) * 0.96 * diffuse;
1881
2015
  specColor[k] += lobes[j].color[k] * (f0[k] * A + B) * specular;
@@ -1883,7 +2017,7 @@ var StudioLighting = class {
1883
2017
  }
1884
2018
  const [t, b] = basis(n);
1885
2019
  let blocked = 0;
1886
- for (let i = 0; i < 4; i++) {
2020
+ for (let i = 0; metal < 1 && i < 4; i++) {
1887
2021
  const phi = i * Math.PI / 2 + phase, l = orient(
1888
2022
  0.7 * Math.cos(phi),
1889
2023
  0.7 * Math.sin(phi),
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "poppygl",
3
3
  "main": "dist/index.js",
4
- "version": "0.0.31",
4
+ "version": "0.0.32",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "https://github.com/tscircuit/poppygl.git"