@selvajs/visualization 1.6.0-beta.9 → 1.6.0

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/parse.d.cts CHANGED
@@ -104,13 +104,26 @@ interface SerializableMaterial {
104
104
  * when the writer sent UVs in mm. Omitted: maps stretch once over the UVs.
105
105
  */
106
106
  mapSize?: number;
107
- /** Procedural scratch fibres along the grain, from UVs in mm. */
108
- finish?: 'brushed' | 'rolled' | 'wood' | 'concrete' | 'plaster' | 'brick' | 'corten';
107
+ /** A procedural surface drawn from UVs in mm: grain on metal, a whole material, or `molded` plastic grain. */
108
+ finish?: 'brushed' | 'rolled' | 'wood' | 'oak' | 'maple' | 'wenge' | 'walnut' | 'concrete' | 'plaster' | 'brick' | 'corten' | 'molded' | 'limestone' | 'travertine' | 'granite' | 'marble' | 'slate' | 'terrazzo' | 'paving' | 'setts' | 'gravel';
109
109
  /** Scales the finish's streaks; 0 draws none. Omitted: 1. */
110
110
  finishStrength?: number;
111
111
  /** Light passing through (glass); unlike `opacity` it keeps reflections. */
112
112
  transmission?: number;
113
113
  ior?: number;
114
+ /**
115
+ * Glass body depth, in model units. Above 0 the glass refracts and tints, at the cost of drawing
116
+ * the scene twice a frame. Omitted: a thin pane.
117
+ */
118
+ thickness?: number;
119
+ /** Hex colour light takes on after crossing `thickness` of glass. Omitted: clear. */
120
+ attenuationColor?: string;
121
+ /** 0 to 1. The soft glow at grazing angles of rubber, soft-touch plastic and cloth. Omitted: none. */
122
+ sheen?: number;
123
+ /** Hex colour of the sheen. Omitted: white. */
124
+ sheenColor?: string;
125
+ /** 0 to 1. Omitted: 0.5. */
126
+ sheenRoughness?: number;
114
127
  }
115
128
  /**
116
129
  * `vertexStart`/`vertexCount` and `indexStart`/`indexCount` are in **element units** of the
package/dist/parse.d.ts CHANGED
@@ -104,13 +104,26 @@ interface SerializableMaterial {
104
104
  * when the writer sent UVs in mm. Omitted: maps stretch once over the UVs.
105
105
  */
106
106
  mapSize?: number;
107
- /** Procedural scratch fibres along the grain, from UVs in mm. */
108
- finish?: 'brushed' | 'rolled' | 'wood' | 'concrete' | 'plaster' | 'brick' | 'corten';
107
+ /** A procedural surface drawn from UVs in mm: grain on metal, a whole material, or `molded` plastic grain. */
108
+ finish?: 'brushed' | 'rolled' | 'wood' | 'oak' | 'maple' | 'wenge' | 'walnut' | 'concrete' | 'plaster' | 'brick' | 'corten' | 'molded' | 'limestone' | 'travertine' | 'granite' | 'marble' | 'slate' | 'terrazzo' | 'paving' | 'setts' | 'gravel';
109
109
  /** Scales the finish's streaks; 0 draws none. Omitted: 1. */
110
110
  finishStrength?: number;
111
111
  /** Light passing through (glass); unlike `opacity` it keeps reflections. */
112
112
  transmission?: number;
113
113
  ior?: number;
114
+ /**
115
+ * Glass body depth, in model units. Above 0 the glass refracts and tints, at the cost of drawing
116
+ * the scene twice a frame. Omitted: a thin pane.
117
+ */
118
+ thickness?: number;
119
+ /** Hex colour light takes on after crossing `thickness` of glass. Omitted: clear. */
120
+ attenuationColor?: string;
121
+ /** 0 to 1. The soft glow at grazing angles of rubber, soft-touch plastic and cloth. Omitted: none. */
122
+ sheen?: number;
123
+ /** Hex colour of the sheen. Omitted: white. */
124
+ sheenColor?: string;
125
+ /** 0 to 1. Omitted: 0.5. */
126
+ sheenRoughness?: number;
114
127
  }
115
128
  /**
116
129
  * `vertexStart`/`vertexCount` and `indexStart`/`indexCount` are in **element units** of the
package/dist/parse.js CHANGED
@@ -1,4 +1,4 @@
1
- import { a as computeCombinedBoundingBox, c as EMISSIVE_LIGHT, d as OWN_ENV_MAP_INTENSITY, f as lookEnvMapIntensity, g as getLogger, i as applyOffset, n as observeMaxAnisotropy, o as parseColor, t as disposeObjectTree, u as OWN_EMISSIVE, v as ErrorCodes, y as VisualizationError } from "./gpu-dispose-Cl3RXm-v.js";
1
+ import { S as VisualizationError, a as observeMaxAnisotropy, c as computeCombinedBoundingBox, d as EMISSIVE_LIGHT, h as lookEnvMapIntensity, i as isGlass, l as parseColor, m as OWN_ENV_MAP_INTENSITY, n as GLASS, p as OWN_EMISSIVE, s as applyOffset, t as disposeObjectTree, x as ErrorCodes, y as getLogger } from "./gpu-dispose-Di_nxeXF.js";
2
2
  import { a as shareGeometry, o as unshareGeometry } from "./gpu-ownership-Bs9l7hrU.js";
3
3
  import * as THREE from "three";
4
4
  import { Line2 } from "three/addons/lines/Line2.js";
@@ -1679,6 +1679,32 @@ function applyTexture(material, url, slot, mapSize) {
1679
1679
  });
1680
1680
  }
1681
1681
  // src/parse/webdisplay/batch/surface-finish.ts
1682
+ const f$1 = (n) => n.toFixed(4);
1683
+ const PITH = 120;
1684
+ const PITH_SPREAD = 120;
1685
+ const PLANK = 180;
1686
+ const LEAN = .0105;
1687
+ const wood = ({ ring, latewood, band, pores, streaks }) => `
1688
+ float plank = floor( uv.y / ${f$1(PLANK)} );
1689
+ float s = uv.y - ( plank + 0.5 ) * ${f$1(PLANK)};
1690
+ float pith = ${f$1(PITH)} + selvaHash( vec2( plank, 2.0 ) ) * ${f$1(PITH_SPREAD)};
1691
+ // Rings wander a few mm and vary in spacing, as growth does from year to year.
1692
+ float wander = ( selvaFbm( uv * vec2( 0.003, 0.008 ) ) - 0.5 ) * ${f$1(ring * 2)};
1693
+ float r = sqrt( pith * pith + s * s ) + uv.x * ${f$1(LEAN)} + wander;
1694
+ r += selvaFbm( vec2( r * 0.02, plank ) ) * ${f$1(ring * 3)} + selvaHash( vec2( plank, 1.0 ) ) * ${f$1(ring * 20)};
1695
+ float year = fract( r / ${f$1(ring)} );
1696
+ float late = smoothstep( ${f$1(1 - band)}, ${f$1(1 - band * .4)}, year ) * ( 1.0 - smoothstep( 0.95, 1.0, year ) );
1697
+ // Open-pored wood shows its rings as a broken band of pores, not a clean line.
1698
+ late *= 1.0 - ${f$1(pores * .6)} * ( 1.0 - smoothstep( 0.3, 0.7, selvaNoise( uv * vec2( 0.25, 2.0 ) ) ) );
1699
+ late *= selvaDetail( ${f$1(ring)} );
1700
+ float fibre = ( selvaNoise( uv * vec2( 0.04, 1.6 ) ) - 0.5 ) * selvaDetail( 1.0 );
1701
+ float pore = smoothstep( 0.62, 0.8, selvaNoise( uv * vec2( 0.3, 2.5 ) ) ) * ( 1.0 - 0.6 * late );
1702
+ pore *= ${f$1(pores)} * selvaDetail( 0.8 );
1703
+ float streak = ( selvaFbm( uv * vec2( 0.0015, 0.012 ) ) - 0.5 ) * ${f$1(streaks)};
1704
+ float shade = 1.0 + ${f$1(latewood * band)} - late * ${f$1(latewood)} + fibre * 0.1 - pore * 0.45 + streak * 0.6;
1705
+ tone = vec3( shade );
1706
+ gloss = 1.0 + late * 0.08 + pore * 0.4 - fibre * 0.2;
1707
+ height = ( fibre * 0.00008 - pore * 0.00015 ) * selvaDetail( 0.6 );`;
1682
1708
  /**
1683
1709
  * Procedural surfaces, so a preset looks like its material with no texture files to ship or upload.
1684
1710
  * UVs are millimetres, so a brick is brick-sized at any model scale. Each recipe sets `tone` (albedo
@@ -1687,15 +1713,41 @@ function applyTexture(material, url, slot, mapSize) {
1687
1713
  * samples to static, so a whole building has to settle to its flat colour.
1688
1714
  */
1689
1715
  const RECIPES$1 = {
1690
- wood: `
1691
- float warp = selvaFbm( uv * vec2( 0.003, 0.02 ) );
1692
- float rings = 0.5 + 0.5 * sin( ( uv.y + warp * 90.0 ) * 0.28 );
1693
- float fibre = selvaNoise( uv * vec2( 0.03, 1.4 ) );
1694
- float grain = ( rings * 0.65 + fibre * 0.35 ) - 0.5;
1695
- grain *= selvaDetail( 4.0 );
1696
- tone = vec3( 1.0 + grain * 0.42 );
1697
- gloss = 1.0 + grain * 0.35;
1698
- height = fibre * 0.00012 * selvaDetail( 1.0 );`,
1716
+ wood: wood({
1717
+ ring: 4,
1718
+ latewood: .3,
1719
+ band: .4,
1720
+ pores: .4,
1721
+ streaks: .35
1722
+ }),
1723
+ walnut: wood({
1724
+ ring: 5,
1725
+ latewood: .2,
1726
+ band: .4,
1727
+ pores: 1,
1728
+ streaks: .45
1729
+ }),
1730
+ oak: wood({
1731
+ ring: 3,
1732
+ latewood: .3,
1733
+ band: .35,
1734
+ pores: .9,
1735
+ streaks: .25
1736
+ }),
1737
+ maple: wood({
1738
+ ring: 3,
1739
+ latewood: .2,
1740
+ band: .1,
1741
+ pores: 0,
1742
+ streaks: .12
1743
+ }),
1744
+ wenge: wood({
1745
+ ring: 1.5,
1746
+ latewood: .7,
1747
+ band: .5,
1748
+ pores: .6,
1749
+ streaks: .4
1750
+ }),
1699
1751
  concrete: `
1700
1752
  float blotch = ( selvaFbm( uv * 0.0035 ) - 0.5 ) * selvaDetail( 40.0 );
1701
1753
  float holes = smoothstep( 0.8, 0.88, selvaNoise( uv * 0.22 ) ) * selvaDetail( 5.0 );
@@ -1720,6 +1772,127 @@ const RECIPES$1 = {
1720
1772
  tone = mix( vec3( face ), vec3( 1.5, 1.7, 1.8 ), joint );
1721
1773
  gloss = 1.0 + joint * 0.15;
1722
1774
  height = -joint * 0.005;`,
1775
+ limestone: `
1776
+ float cloud = ( selvaFbm( uv * 0.004 ) - 0.5 ) * selvaDetail( 60.0 );
1777
+ float mottle = ( selvaFbm( uv * 0.025 + vec2( 3.0, 7.0 ) ) - 0.5 ) * selvaDetail( 12.0 );
1778
+ float bed = ( selvaNoise( vec2( uv.x * 0.003, uv.y * 0.05 ) ) - 0.5 ) * selvaDetail( 10.0 );
1779
+ float fleck = smoothstep( 0.8, 0.9, selvaNoise( uv * 0.5 ) ) * selvaDetail( 2.0 );
1780
+ vec3 fossil = selvaCell( uv / 14.0 );
1781
+ float shell = ( 1.0 - smoothstep( 0.08, 0.16, fossil.x ) ) * step( 0.85, fossil.z ) * selvaDetail( 6.0 );
1782
+ tone = vec3( 1.0 + cloud * 0.14 + mottle * 0.1 + bed * 0.03 - fleck * 0.12 - shell * 0.1 );
1783
+ gloss = 1.0 + cloud * 0.1;
1784
+ height = -( fleck + shell ) * 0.00005;`,
1785
+ travertine: `
1786
+ float band = selvaFbm( vec2( uv.x * 0.002, uv.y * 0.012 ) );
1787
+ float layer = ( band - 0.5 ) * selvaDetail( 25.0 );
1788
+ float pocket = selvaNoise( uv * vec2( 0.025, 0.15 ) ) * 0.7 + selvaNoise( uv * vec2( 0.08, 0.4 ) ) * 0.3;
1789
+ float voids = smoothstep( 0.68, 0.78, pocket ) * smoothstep( 0.35, 0.6, band ) * selvaDetail( 4.0 );
1790
+ float grain = ( selvaNoise( uv * 0.6 ) - 0.5 ) * selvaDetail( 1.5 );
1791
+ tone = vec3( 1.0 + layer * 0.2 + grain * 0.04 ) * mix( vec3( 1.0 ), vec3( 0.62, 0.56, 0.48 ), voids );
1792
+ gloss = 1.0 + voids * 0.3;
1793
+ height = -voids * 0.002;`,
1794
+ granite: `
1795
+ vec3 crystal = selvaCell( uv / 4.0 );
1796
+ // Feldspar, quartz and biotite in about the shares of a grey granite. The dark crystals
1797
+ // gather in clusters, which is what still reads as granite from a few metres.
1798
+ float cluster = selvaFbm( uv * 0.04 ) - 0.5;
1799
+ float dark = 0.2 + cluster * 0.2;
1800
+ float mineral = crystal.z < dark ? 0.35 : ( crystal.z < dark + 0.3 ? 1.35 : 1.0 );
1801
+ mineral *= 1.0 + ( selvaHash( vec2( crystal.z, 3.0 ) ) - 0.5 ) * 0.15;
1802
+ mineral = mix( 1.0 - cluster * 0.25 * selvaDetail( 15.0 ), mineral, selvaDetail( 4.0 ) );
1803
+ float cloud = ( selvaFbm( uv * 0.005 ) - 0.5 ) * selvaDetail( 50.0 );
1804
+ tone = vec3( mineral + cloud * 0.06 );
1805
+ gloss = 1.0 - ( mineral - 1.0 ) * 0.15;
1806
+ height = ( selvaNoise( uv * 0.8 ) - 0.5 ) * 0.0002 * selvaDetail( 1.2 );`,
1807
+ marble: `
1808
+ vec2 w = vec2( uv.x * 0.7 + uv.y * 0.7, uv.y * 0.7 - uv.x * 0.7 ) * vec2( 0.0012, 0.004 );
1809
+ w += vec2( selvaFbm( w * 2.0 ), selvaFbm( w * 2.0 + vec2( 4.0, 2.0 ) ) ) * 0.6;
1810
+ float feather = smoothstep( 0.3, 0.7, selvaFbm( uv * 0.006 + vec2( 2.0, 5.0 ) ) );
1811
+ float major = 1.0 - smoothstep( 0.0, 0.05, abs( selvaFbm( w ) - 0.5 ) );
1812
+ float fine = 1.0 - smoothstep( 0.0, 0.02, abs( selvaFbm( w * 3.0 + vec2( 9.0, 1.0 ) ) - 0.5 ) );
1813
+ float cloud = ( selvaFbm( uv * 0.003 ) - 0.5 ) * selvaDetail( 60.0 );
1814
+ float vein = ( major * 0.35 * selvaDetail( 6.0 ) + fine * 0.2 * selvaDetail( 2.0 ) ) * feather;
1815
+ tone = vec3( 1.0 + cloud * 0.06 - vein - feather * 0.04 );
1816
+ gloss = 1.0 + vein * 0.3;`,
1817
+ slate: `
1818
+ float cleft = selvaFbm( uv * vec2( 0.012, 0.02 ) );
1819
+ float flake = mix( cleft, floor( cleft * 4.0 ) / 4.0, 0.6 );
1820
+ float seen = selvaDetail( 6.0 );
1821
+ float glint = ( selvaNoise( uv * 0.3 ) - 0.5 ) * selvaDetail( 3.0 );
1822
+ tone = vec3( 1.0 + ( flake - 0.5 ) * 0.18 * seen + glint * 0.06 );
1823
+ gloss = 1.0 - ( flake - 0.5 ) * 0.3 * seen + glint * 0.2;
1824
+ height = flake * 0.0015 * seen;`,
1825
+ terrazzo: `
1826
+ vec3 chipCell = selvaCell( uv / 9.0 );
1827
+ float chip = smoothstep( 0.1, 0.16, chipCell.y - chipCell.x ) * step( 0.12, selvaHash( vec2( chipCell.z, 5.0 ) ) );
1828
+ float k = selvaHash( vec2( chipCell.z, 7.0 ) );
1829
+ vec3 stone = k < 0.6 ? vec3( 1.1 ) : k < 0.75 ? vec3( 0.65 ) : k < 0.85 ? vec3( 0.22 ) : k < 0.9 ? vec3( 0.85, 0.69, 0.44 ) : k < 0.95 ? vec3( 0.64, 0.35, 0.28 ) : vec3( 0.93, 0.72, 0.66 );
1830
+ vec3 gritCell = selvaCell( uv / 2.5 );
1831
+ float grit = smoothstep( 0.1, 0.2, gritCell.y - gritCell.x ) * step( 0.6, selvaHash( vec2( gritCell.z, 2.0 ) ) );
1832
+ float g = selvaHash( vec2( gritCell.z, 9.0 ) ) < 0.5 ? 0.4 : 1.1;
1833
+ vec3 near = mix( vec3( 1.0 ), stone, chip );
1834
+ near = mix( near, vec3( g ), grit * ( 1.0 - chip ) * selvaDetail( 2.5 ) );
1835
+ // From afar the chips average to a slightly darker matrix.
1836
+ tone = mix( vec3( 0.92 ), near, selvaDetail( 9.0 ) );
1837
+ gloss = 1.0 + chip * 0.1;`,
1838
+ paving: `
1839
+ vec2 size = vec2( 600.0, 400.0 );
1840
+ float row = floor( uv.y / size.y );
1841
+ vec2 laid = vec2( uv.x + mod( row, 2.0 ) * size.x * 0.5, uv.y );
1842
+ vec2 id = floor( laid / size );
1843
+ vec2 edge = abs( fract( laid / size ) - 0.5 ) * size;
1844
+ float inset = min( size.x * 0.5 - edge.x, size.y * 0.5 - edge.y );
1845
+ float joint = ( 1.0 - smoothstep( 2.5, 3.5, inset ) ) * selvaDetail( 5.0 );
1846
+ float chamfer = ( 1.0 - smoothstep( 2.5, 6.0, inset ) ) * selvaDetail( 4.0 );
1847
+ float slab = ( selvaHash( id ) - 0.5 ) * 0.12 * selvaDetail( 100.0 );
1848
+ float blotch = ( selvaFbm( uv * 0.006 ) - 0.5 ) * 0.12 * selvaDetail( 40.0 );
1849
+ float speck = ( selvaNoise( uv * 0.7 ) - 0.5 ) * 0.06 * selvaDetail( 1.5 );
1850
+ tone = vec3( 1.0 + slab + blotch + speck ) * mix( 1.0, 0.55, joint );
1851
+ gloss = 1.0 + joint * 0.1;
1852
+ height = -chamfer * 0.002 - joint * 0.003;`,
1853
+ setts: `
1854
+ vec2 size = vec2( 110.0, 100.0 );
1855
+ vec2 warped = uv + ( vec2( selvaNoise( uv * 0.03 ), selvaNoise( uv * 0.03 + vec2( 3.0, 8.0 ) ) ) - 0.5 ) * 10.0;
1856
+ float row = floor( warped.y / size.y );
1857
+ vec2 laid = vec2( warped.x + selvaHash( vec2( row, 4.0 ) ) * size.x, warped.y );
1858
+ vec2 id = floor( laid / size );
1859
+ vec2 edge = abs( fract( laid / size ) - 0.5 ) * size;
1860
+ float inset = min( size.x * 0.5 - edge.x, size.y * 0.5 - edge.y );
1861
+ float joint = ( 1.0 - smoothstep( 4.0, 6.0, inset ) ) * selvaDetail( 10.0 );
1862
+ float dome = smoothstep( 4.0, 25.0, inset );
1863
+ vec3 crystal = selvaCell( uv / 4.0 );
1864
+ // Feldspar, quartz and biotite in about the shares of a grey granite. The dark crystals
1865
+ // gather in clusters, which is what still reads as granite from a few metres.
1866
+ float cluster = selvaFbm( uv * 0.04 ) - 0.5;
1867
+ float dark = 0.2 + cluster * 0.2;
1868
+ float mineral = crystal.z < dark ? 0.35 : ( crystal.z < dark + 0.3 ? 1.35 : 1.0 );
1869
+ mineral *= 1.0 + ( selvaHash( vec2( crystal.z, 3.0 ) ) - 0.5 ) * 0.15;
1870
+ mineral = mix( 1.0 - cluster * 0.25 * selvaDetail( 15.0 ), mineral, selvaDetail( 4.0 ) );
1871
+ float tint = selvaHash( id + vec2( 5.0, 2.0 ) ) - 0.5;
1872
+ vec3 sett = ( 1.0 + ( selvaHash( id ) - 0.5 ) * 0.16 ) * ( 1.0 + vec3( 0.05, 0.0, -0.06 ) * tint * 2.0 );
1873
+ sett = mix( vec3( 1.0 ), sett, selvaDetail( 100.0 ) );
1874
+ tone = mix( sett * mineral, vec3( 1.05, 0.97, 0.82 ), joint );
1875
+ gloss = 1.0 + joint * 0.1;
1876
+ height = ( dome * 0.006 + ( selvaNoise( uv * 0.15 ) - 0.5 ) * 0.001 ) * selvaDetail( 10.0 );`,
1877
+ gravel: `
1878
+ vec3 pebble = selvaCell( uv / 12.0 );
1879
+ float gap = 1.0 - smoothstep( 0.04, 0.18, pebble.y - pebble.x );
1880
+ float dome = sqrt( clamp( 1.0 - pebble.x * 1.4, 0.0, 1.0 ) );
1881
+ float k = selvaHash( vec2( pebble.z, 3.0 ) );
1882
+ vec3 stone = k < 0.3 ? vec3( 1.13, 1.14, 1.11 ) : k < 0.55 ? vec3( 0.96, 0.85, 0.71 ) : k < 0.7 ? vec3( 0.84, 0.64, 0.59 ) : k < 0.9 ? vec3( 0.74, 0.8, 0.89 ) : vec3( 0.47, 0.5, 0.56 );
1883
+ stone *= 1.0 + ( selvaHash( vec2( pebble.z, 8.0 ) ) - 0.5 ) * 0.15;
1884
+ float seen = selvaDetail( 12.0 );
1885
+ tone = mix( vec3( 0.85 ), stone * mix( 1.0, 0.35, gap ) * ( 0.85 + 0.15 * dome ), seen );
1886
+ gloss = 1.0 + gap * 0.1 - dome * 0.1;
1887
+ height = dome * 0.004 * seen;`,
1888
+ molded: `
1889
+ float pit = selvaNoise( uv * 1.6 );
1890
+ float mottle = selvaNoise( uv * 0.5 + vec2( 7.0, 3.0 ) );
1891
+ float grit = ( pit * 0.65 + mottle * 0.35 ) - 0.5;
1892
+ float seen = selvaDetail( 0.6 );
1893
+ tone = vec3( 1.0 + grit * 0.1 * seen );
1894
+ gloss = 1.0 + grit * 0.9 * seen;
1895
+ height = ( grit + 0.5 ) * 0.00008 * seen;`,
1723
1896
  corten: `
1724
1897
  float bloom = 0.5 + ( selvaFbm( uv * vec2( 0.006, 0.0025 ) ) - 0.5 ) * selvaDetail( 25.0 );
1725
1898
  float scale = 0.5 + ( selvaFbm( uv * 0.06 ) - 0.5 ) * selvaDetail( 8.0 );
@@ -1728,7 +1901,6 @@ const RECIPES$1 = {
1728
1901
  gloss = 1.0 + ( scale - 0.5 ) * 0.25;
1729
1902
  height = scale * 0.0004;`
1730
1903
  };
1731
- const f$1 = (n) => n.toFixed(4);
1732
1904
  /** Null for the finishes `finish.ts` draws itself. */
1733
1905
  function surfacePatch(finish, strength) {
1734
1906
  const recipe = RECIPES$1[finish];
@@ -1762,6 +1934,30 @@ function surfacePatch(finish, strength) {
1762
1934
  }
1763
1935
  return sum / 0.9375;
1764
1936
  }
1937
+ // Cellular noise: distance to the nearest and second-nearest jittered point, and the
1938
+ // nearest one's id. y - x is small along a cell border.
1939
+ vec3 selvaCell( vec2 p ) {
1940
+ vec2 i = floor( p );
1941
+ vec2 fp = fract( p );
1942
+ float f1 = 8.0;
1943
+ float f2 = 8.0;
1944
+ float id = 0.0;
1945
+ for ( int y = -1; y <= 1; y ++ ) {
1946
+ for ( int x = -1; x <= 1; x ++ ) {
1947
+ vec2 g = vec2( float( x ), float( y ) );
1948
+ vec2 o = vec2( selvaHash( i + g ), selvaHash( i + g + vec2( 19.0, 7.0 ) ) );
1949
+ float d = length( g + o * 0.8 + 0.1 - fp );
1950
+ if ( d < f1 ) {
1951
+ f2 = f1;
1952
+ f1 = d;
1953
+ id = selvaHash( i + g + vec2( 3.0, 11.0 ) );
1954
+ } else if ( d < f2 ) {
1955
+ f2 = d;
1956
+ }
1957
+ }
1958
+ }
1959
+ return vec3( f1, f2, id );
1960
+ }
1765
1961
  float selvaDetail( float featureMm ) {
1766
1962
  return 1.0 - smoothstep( 0.5, 1.5, selvaFootprint / featureMm );
1767
1963
  }`).replace("#include <color_fragment>", `#include <color_fragment>
@@ -1898,6 +2094,10 @@ function cullsBackFaces(matData, options) {
1898
2094
  if (options?.appearance?.cullBackfaces) return true;
1899
2095
  return (options?.closed ?? false) && !matData.transparent && matData.opacity >= 1;
1900
2096
  }
2097
+ const FROSTED = .2;
2098
+ function isPane(matData) {
2099
+ return (matData.transmission ?? 0) > 0 && !((matData.thickness ?? 0) > 0) && matData.roughness < FROSTED;
2100
+ }
1901
2101
  function createMaterialPicker(materialsSrc, options) {
1902
2102
  const built = /* @__PURE__ */ new Map();
1903
2103
  return (materialId, closed) => {
@@ -1950,15 +2150,18 @@ function createMaterial(matData, options) {
1950
2150
  material.clearcoat = matData.clearcoat;
1951
2151
  material.clearcoatRoughness = matData.clearcoatRoughness ?? 0;
1952
2152
  }
2153
+ if (matData.sheen != null && matData.sheen > 0) {
2154
+ material.sheen = matData.sheen;
2155
+ material.sheenColor.set(matData.sheenColor ?? "#ffffff");
2156
+ material.sheenRoughness = matData.sheenRoughness ?? .5;
2157
+ }
1953
2158
  if (matData.anisotropy != null && matData.anisotropy > 0 && options?.hasUvs) {
1954
2159
  material.anisotropy = matData.anisotropy;
1955
2160
  material.anisotropyRotation = matData.anisotropyRotation ?? 0;
1956
2161
  }
1957
- if (matData.transmission != null) {
1958
- material.transmission = matData.transmission;
1959
- material.thickness = 0;
1960
- }
1961
2162
  if (matData.ior != null) material.ior = matData.ior;
2163
+ if (matData.transmission != null && matData.transmission > 0) applyGlass(material, matData);
2164
+ if (matData.metalness > 0) dimSunOnMetal(material);
1962
2165
  if (vertexColors) applyVertexColorSRGBDecode(material);
1963
2166
  const finish = matData.finish && options?.hasUvs ? finishPatch(matData.finish, matData.finishStrength ?? 1) : null;
1964
2167
  if (finish) {
@@ -1974,6 +2177,50 @@ function createMaterial(matData, options) {
1974
2177
  if (matData.normalMap) applyTexture(material, matData.normalMap, "normalMap", mapSize);
1975
2178
  return material;
1976
2179
  }
2180
+ const SUN_ON_METAL = .1;
2181
+ const SUN_BEGIN = "#if ( NUM_DIR_LIGHTS > 0 ) && defined( RE_Direct )";
2182
+ const SUN_END = "#if ( NUM_RECT_AREA_LIGHTS > 0 ) && defined( RE_Direct_RectArea )";
2183
+ /** Scales the directional lights' specular by metalness; point lights keep theirs. */
2184
+ function dimSunOnMetal(material) {
2185
+ addShaderPatch(material, "sun-on-metal", (shader) => {
2186
+ shader.fragmentShader = shader.fragmentShader.replace("#include <lights_fragment_begin>", THREE.ShaderChunk.lights_fragment_begin.replace(SUN_BEGIN, `vec3 specularBeforeSun = reflectedLight.directSpecular;
2187
+ ${SUN_BEGIN}`).replace(SUN_END, `reflectedLight.directSpecular = specularBeforeSun + ( reflectedLight.directSpecular - specularBeforeSun ) * mix( 1.0, ${SUN_ON_METAL.toFixed(2)}, metalnessFactor );
2188
+ ${SUN_END}`));
2189
+ });
2190
+ }
2191
+ /**
2192
+ * three's transmission draws every opaque object a second time into its own buffer each frame, and
2193
+ * that buffer never holds other glass: one pane seen through another vanishes. So only glass that
2194
+ * needs it gets it: with a thickness, to refract and tint, or frosted, to blur. A pane blends
2195
+ * instead: cheap, and panes stack.
2196
+ */
2197
+ function applyGlass(material, matData) {
2198
+ const transmission = Math.min(1, matData.transmission);
2199
+ material.userData[GLASS] = true;
2200
+ if (!isPane(matData)) {
2201
+ const thickness = matData.thickness ?? 0;
2202
+ material.transmission = transmission;
2203
+ material.thickness = thickness;
2204
+ if (matData.attenuationColor && thickness > 0) {
2205
+ material.attenuationColor.set(matData.attenuationColor);
2206
+ material.attenuationDistance = thickness;
2207
+ addShaderPatch(material, "glass-attenuation", (shader) => {
2208
+ shader.fragmentShader = shader.fragmentShader.replace("#include <transmission_fragment>", THREE.ShaderChunk.transmission_fragment.replace("material.attenuationDistance = attenuationDistance;", "material.attenuationDistance = attenuationDistance * length( modelMatrix[ 0 ].xyz );"));
2209
+ });
2210
+ }
2211
+ return;
2212
+ }
2213
+ material.transparent = true;
2214
+ material.opacity = matData.opacity * (1 - transmission * .96);
2215
+ material.depthWrite = false;
2216
+ material.blending = THREE.CustomBlending;
2217
+ material.blendSrc = THREE.OneFactor;
2218
+ material.blendDst = THREE.OneMinusSrcAlphaFactor;
2219
+ addShaderPatch(material, "glass-pane", (shader) => {
2220
+ shader.fragmentShader = shader.fragmentShader.replace("#include <opaque_fragment>", `vec3 paneSpecular = reflectedLight.directSpecular + reflectedLight.indirectSpecular;
2221
+ gl_FragColor = vec4( ( outgoingLight - paneSpecular ) * diffuseColor.a + paneSpecular, diffuseColor.a );`);
2222
+ });
2223
+ }
1977
2224
  /**
1978
2225
  * Chains `patch` after any earlier one. three caches compiled programs by `customProgramCacheKey`,
1979
2226
  * which defaults to the `onBeforeCompile` source; a chained wrapper's source is the same whatever
@@ -2208,7 +2455,7 @@ function finalizeMergedMesh(geometry, group, material) {
2208
2455
  const firstMesh = group.meshes[0];
2209
2456
  const meshNames = group.meshes.map((m) => m.name).filter((name) => name && name.length > 0);
2210
2457
  threeMesh.name = meshNames.length > 0 ? meshNames[0] : `merged_material_${group.materialId}`;
2211
- threeMesh.castShadow = true;
2458
+ threeMesh.castShadow = !isGlass(material);
2212
2459
  threeMesh.receiveShadow = true;
2213
2460
  threeMesh.userData = {
2214
2461
  source: "compute",
@@ -2259,7 +2506,7 @@ function finalizeSingleMesh(geometry, meshMeta, material) {
2259
2506
  trackingKey: meshMeta.id,
2260
2507
  metadata: meshMeta.metadata ?? {}
2261
2508
  };
2262
- mesh.castShadow = true;
2509
+ mesh.castShadow = !isGlass(material);
2263
2510
  mesh.receiveShadow = true;
2264
2511
  return mesh;
2265
2512
  }