WoodNodeMaterial.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431
  1. import * as THREE from 'three';
  2. import * as TSL from 'three/tsl';
  3. // some helpers below are ported from Blender and converted to TSL
  4. const mapRange = TSL.Fn( ( [ x, fromMin, fromMax, toMin, toMax, clmp ] ) => {
  5. const factor = x.sub( fromMin ).div( fromMax.sub( fromMin ) );
  6. const result = toMin.add( factor.mul( toMax.sub( toMin ) ) );
  7. return TSL.select( clmp, TSL.max( TSL.min( result, toMax ), toMin ), result );
  8. } );
  9. const voronoi3d = TSL.wgslFn( `
  10. fn voronoi3d(x: vec3<f32>, smoothness: f32, randomness: f32) -> f32
  11. {
  12. let p = floor(x);
  13. let f = fract(x);
  14. var res = 0.0;
  15. var totalWeight = 0.0;
  16. for (var k = -1; k <= 1; k++)
  17. {
  18. for (var j = -1; j <= 1; j++)
  19. {
  20. for (var i = -1; i <= 1; i++)
  21. {
  22. let b = vec3<f32>(f32(i), f32(j), f32(k));
  23. let hashOffset = hash3d(p + b) * randomness;
  24. let r = b - f + hashOffset;
  25. let d = length(r);
  26. let weight = exp(-d * d / max(smoothness * smoothness, 0.001));
  27. res += d * weight;
  28. totalWeight += weight;
  29. }
  30. }
  31. }
  32. if (totalWeight > 0.0)
  33. {
  34. res /= totalWeight;
  35. }
  36. return smoothstep(0.0, 1.0, res);
  37. }
  38. fn hash3d(p: vec3<f32>) -> vec3<f32>
  39. {
  40. var p3 = fract(p * vec3<f32>(0.1031, 0.1030, 0.0973));
  41. p3 += dot(p3, p3.yzx + 33.33);
  42. return fract((p3.xxy + p3.yzz) * p3.zyx);
  43. }
  44. ` );
  45. // const hash3d = TSL.Fn( ( [ p ] ) => {
  46. // const p3 = p.mul( TSL.vec3( 0.1031, 0.1030, 0.0973 ) ).fract();
  47. // const dotProduct = p3.dot( p3.yzx.add( 33.33 ) );
  48. // p3.addAssign( dotProduct );
  49. // return p3.xxy.add( p3.yzz ).mul( p3.zyx ).fract();
  50. // } );
  51. // const voronoi3d = TSL.Fn( ( [ x, smoothness, randomness ] ) => {
  52. // let p = TSL.floor(x);
  53. // let f = TSL.fract(x);
  54. // var res = TSL.float(0.0);
  55. // var totalWeight = TSL.float(0.0);
  56. // TSL.Loop( 3, 3, 3, ( { k, j, i } ) => {
  57. // let b = TSL.vec3(TSL.float(i).sub(1), TSL.float(j).sub(1), TSL.float(k).sub(1));
  58. // let hashOffset = hash3d(p.add(b)).mul(randomness);
  59. // let r = b.sub(f).add(hashOffset);
  60. // let d = TSL.length(r);
  61. // let weight = TSL.exp(d.negate().mul(d).div(TSL.max(smoothness.mul(smoothness), 0.001)));
  62. // res.addAssign(d.mul(weight));
  63. // totalWeight.addAssign(weight);
  64. // } );
  65. // res.assign(TSL.select(totalWeight.greaterThan(0.0), res.div(totalWeight), res));
  66. // return TSL.smoothstep(0.0, 1.0, res);
  67. // } );
  68. const softLightMix = TSL.Fn( ( [ t, col1, col2 ] ) => {
  69. const tm = TSL.float( 1.0 ).sub( t );
  70. const one = TSL.vec3( 1.0 );
  71. const scr = one.sub( one.sub( col2 ).mul( one.sub( col1 ) ) );
  72. return tm.mul( col1 ).add( t.mul( one.sub( col1 ).mul( col2 ).mul( col1 ).add( col1.mul( scr ) ) ) );
  73. } );
  74. // single-octave normalized noise — equivalent to fbm with detail = 1
  75. const noise1Norm = TSL.Fn( ( [ p ] ) => TSL.mx_noise_float( p ).mul( 0.5 ).add( 0.5 ) );
  76. const noise3Norm = TSL.Fn( ( [ p ] ) => TSL.mx_noise_vec3( p ).mul( 0.5 ).add( 0.5 ) );
  77. const woodCenter = TSL.Fn( ( [ p, centerSize ] ) => {
  78. const pxyCenter = p.mul( TSL.vec3( 1, 1, 0 ) ).length();
  79. const center = mapRange( pxyCenter, 0, 1, 0, centerSize, true );
  80. return center;
  81. } );
  82. const spaceWarp = TSL.Fn( ( [ p, warpStrength, xyScale, zScale ] ) => {
  83. const combinedXyz = TSL.vec3( xyScale, xyScale, zScale ).mul( p );
  84. const noise = noise3Norm( combinedXyz.mul( 1.6 * 1.5 ) ).sub( 0.5 ).mul( warpStrength );
  85. const pXy = p.mul( TSL.vec3( 1, 1, 0 ) );
  86. const normalizedXy = pXy.normalize();
  87. const warp = noise.mul( normalizedXy ).add( pXy );
  88. return warp;
  89. } );
  90. const woodRings = TSL.Fn( ( [ w, ringThickness, ringBias, ringSizeVariance, ringVarianceScale, barkThickness ] ) => {
  91. const rings = noise1Norm( w.mul( ringVarianceScale ) ).mul( ringSizeVariance ).add( w ).mul( ringThickness ).fract().mul( barkThickness );
  92. const sharpRings = TSL.min( mapRange( rings, 0, ringBias, 0, 1, TSL.bool( true ) ), mapRange( rings, ringBias, 1, 1, 0, TSL.bool( true ) ) );
  93. const blurAmount = TSL.max( TSL.positionView.length().div( 10 ), 1 );
  94. const blurredRings = TSL.smoothstep( blurAmount.negate(), blurAmount, sharpRings.sub( 0.5 ) ).mul( 0.5 ).add( 0.5 );
  95. return blurredRings;
  96. } );
  97. const woodDetail = TSL.Fn( ( [ warp, p, y, splotchScale ] ) => {
  98. const radialCoords = TSL.clamp( TSL.atan( warp.y, warp.x ).div( TSL.PI2 ).add( 0.5 ), 0, 1 ).mul( TSL.PI2.mul( 3 ) );
  99. const combinedXyz = TSL.vec3( radialCoords.sin(), y, radialCoords.cos().mul( p.z ) );
  100. const scaled = TSL.vec3( 0.1, 1.19, 0.05 ).mul( combinedXyz );
  101. return noise1Norm( scaled.mul( splotchScale ) );
  102. } );
  103. const cellStructure = TSL.Fn( ( [ p, cellScale, cellSize ] ) => {
  104. const warp = spaceWarp( p.mul( cellScale.div( 50 ) ), cellScale.div( 1000 ), 0.1, 1.77 );
  105. const cells = voronoi3d( warp.xy.mul( 75 ), 0.5, 1 );
  106. return mapRange( cells, cellSize, cellSize.add( 0.21 ), 0, 1, TSL.bool( true ) );
  107. } );
  108. const wood = TSL.Fn( ( [
  109. p,
  110. centerSize,
  111. largeWarpScale,
  112. largeGrainStretch,
  113. smallWarpStrength,
  114. smallWarpScale,
  115. fineWarpStrength,
  116. fineWarpScale,
  117. ringThickness,
  118. ringBias,
  119. ringSizeVariance,
  120. ringVarianceScale,
  121. barkThickness,
  122. splotchScale,
  123. splotchIntensity,
  124. cellScale,
  125. cellSize,
  126. darkGrainColor,
  127. lightGrainColor
  128. ] ) => {
  129. const center = woodCenter( p, centerSize );
  130. const mainWarp = spaceWarp( spaceWarp( p, center, largeWarpScale, largeGrainStretch ), smallWarpStrength, smallWarpScale, 0.17 );
  131. const detailWarp = spaceWarp( mainWarp, fineWarpStrength, fineWarpScale, 0.17 );
  132. const rings = woodRings( detailWarp.length(), TSL.float( 1 ).div( ringThickness ), ringBias, ringSizeVariance, ringVarianceScale, barkThickness );
  133. const detail = woodDetail( detailWarp, p, detailWarp.length(), splotchScale );
  134. const cells = cellStructure( mainWarp, cellScale, cellSize.div( TSL.max( TSL.positionView.length().mul( 10 ), 1 ) ) );
  135. const baseColor = TSL.mix( darkGrainColor, lightGrainColor, rings );
  136. return softLightMix( splotchIntensity, softLightMix( 0.407, baseColor, cells ), detail );
  137. } );
  138. const woodParams = {
  139. teak: {
  140. transformationMatrix: new THREE.Matrix4().identity(),
  141. centerSize: 1.11, largeWarpScale: 0.32, largeGrainStretch: 0.24, smallWarpStrength: 0.059,
  142. smallWarpScale: 2, fineWarpStrength: 0.006, fineWarpScale: 32.8, ringThickness: 1 / 34,
  143. ringBias: 0.03, ringSizeVariance: 0.03, ringVarianceScale: 4.4, barkThickness: 0.3,
  144. splotchScale: 0.2, splotchIntensity: 0.541, cellScale: 910, cellSize: 0.1,
  145. darkGrainColor: '#0c0504', lightGrainColor: '#926c50'
  146. },
  147. walnut: {
  148. transformationMatrix: new THREE.Matrix4().identity(),
  149. centerSize: 1.07, largeWarpScale: 0.42, largeGrainStretch: 0.34, smallWarpStrength: 0.016,
  150. smallWarpScale: 10.3, fineWarpStrength: 0.028, fineWarpScale: 12.7, ringThickness: 1 / 32,
  151. ringBias: 0.08, ringSizeVariance: 0.03, ringVarianceScale: 5.5, barkThickness: 0.98,
  152. splotchScale: 1.84, splotchIntensity: 0.97, cellScale: 710, cellSize: 0.31,
  153. darkGrainColor: '#311e13', lightGrainColor: '#523424'
  154. },
  155. white_oak: {
  156. transformationMatrix: new THREE.Matrix4().identity(),
  157. centerSize: 1.23, largeWarpScale: 0.21, largeGrainStretch: 0.21, smallWarpStrength: 0.034,
  158. smallWarpScale: 2.44, fineWarpStrength: 0.01, fineWarpScale: 14.3, ringThickness: 1 / 34,
  159. ringBias: 0.82, ringSizeVariance: 0.16, ringVarianceScale: 1.4, barkThickness: 0.7,
  160. splotchScale: 0.2, splotchIntensity: 0.541, cellScale: 800, cellSize: 0.28,
  161. darkGrainColor: '#8b4c21', lightGrainColor: '#c57e43'
  162. },
  163. pine: {
  164. transformationMatrix: new THREE.Matrix4().identity(),
  165. centerSize: 1.23, largeWarpScale: 0.21, largeGrainStretch: 0.18, smallWarpStrength: 0.041,
  166. smallWarpScale: 2.44, fineWarpStrength: 0.006, fineWarpScale: 23.2, ringThickness: 1 / 24,
  167. ringBias: 0.1, ringSizeVariance: 0.07, ringVarianceScale: 5, barkThickness: 0.35,
  168. splotchScale: 0.51, splotchIntensity: 3.32, cellScale: 1480, cellSize: 0.07,
  169. darkGrainColor: '#c58355', lightGrainColor: '#d19d61'
  170. },
  171. poplar: {
  172. transformationMatrix: new THREE.Matrix4().identity(),
  173. centerSize: 1.43, largeWarpScale: 0.33, largeGrainStretch: 0.18, smallWarpStrength: 0.04,
  174. smallWarpScale: 4.3, fineWarpStrength: 0.004, fineWarpScale: 33.6, ringThickness: 1 / 37,
  175. ringBias: 0.07, ringSizeVariance: 0.03, ringVarianceScale: 3.8, barkThickness: 0.3,
  176. splotchScale: 1.92, splotchIntensity: 0.71, cellScale: 830, cellSize: 0.04,
  177. darkGrainColor: '#716347', lightGrainColor: '#998966'
  178. },
  179. maple: {
  180. transformationMatrix: new THREE.Matrix4().identity(),
  181. centerSize: 1.4, largeWarpScale: 0.38, largeGrainStretch: 0.25, smallWarpStrength: 0.067,
  182. smallWarpScale: 2.5, fineWarpStrength: 0.005, fineWarpScale: 33.6, ringThickness: 1 / 35,
  183. ringBias: 0.1, ringSizeVariance: 0.07, ringVarianceScale: 4.6, barkThickness: 0.61,
  184. splotchScale: 0.46, splotchIntensity: 1.49, cellScale: 800, cellSize: 0.03,
  185. darkGrainColor: '#b08969', lightGrainColor: '#bc9d7d'
  186. },
  187. red_oak: {
  188. transformationMatrix: new THREE.Matrix4().identity(),
  189. centerSize: 1.21, largeWarpScale: 0.24, largeGrainStretch: 0.25, smallWarpStrength: 0.044,
  190. smallWarpScale: 2.54, fineWarpStrength: 0.01, fineWarpScale: 14.5, ringThickness: 1 / 34,
  191. ringBias: 0.92, ringSizeVariance: 0.03, ringVarianceScale: 5.6, barkThickness: 1.01,
  192. splotchScale: 0.28, splotchIntensity: 3.48, cellScale: 800, cellSize: 0.25,
  193. darkGrainColor: '#af613b', lightGrainColor: '#e0a27a'
  194. },
  195. cherry: {
  196. transformationMatrix: new THREE.Matrix4().identity(),
  197. centerSize: 1.33, largeWarpScale: 0.11, largeGrainStretch: 0.33, smallWarpStrength: 0.024,
  198. smallWarpScale: 2.48, fineWarpStrength: 0.01, fineWarpScale: 15.3, ringThickness: 1 / 36,
  199. ringBias: 0.02, ringSizeVariance: 0.04, ringVarianceScale: 6.5, barkThickness: 0.09,
  200. splotchScale: 1.27, splotchIntensity: 1.24, cellScale: 1530, cellSize: 0.15,
  201. darkGrainColor: '#913f27', lightGrainColor: '#b45837'
  202. },
  203. cedar: {
  204. transformationMatrix: new THREE.Matrix4().identity(),
  205. centerSize: 1.11, largeWarpScale: 0.39, largeGrainStretch: 0.12, smallWarpStrength: 0.061,
  206. smallWarpScale: 1.9, fineWarpStrength: 0.006, fineWarpScale: 4.8, ringThickness: 1 / 25,
  207. ringBias: 0.01, ringSizeVariance: 0.07, ringVarianceScale: 6.7, barkThickness: 0.1,
  208. splotchScale: 0.61, splotchIntensity: 2.54, cellScale: 630, cellSize: 0.19,
  209. darkGrainColor: '#9a5b49', lightGrainColor: '#ae745e'
  210. },
  211. mahogany: {
  212. transformationMatrix: new THREE.Matrix4().identity(),
  213. centerSize: 1.25, largeWarpScale: 0.26, largeGrainStretch: 0.29, smallWarpStrength: 0.044,
  214. smallWarpScale: 2.54, fineWarpStrength: 0.01, fineWarpScale: 15.3, ringThickness: 1 / 38,
  215. ringBias: 0.01, ringSizeVariance: 0.33, ringVarianceScale: 1.2, barkThickness: 0.07,
  216. splotchScale: 0.77, splotchIntensity: 1.39, cellScale: 1400, cellSize: 0.23,
  217. darkGrainColor: '#501d12', lightGrainColor: '#6d3722'
  218. }
  219. };
  220. export const WoodGenuses = [ 'teak', 'walnut', 'white_oak', 'pine', 'poplar', 'maple', 'red_oak', 'cherry', 'cedar', 'mahogany' ];
  221. export const Finishes = [ 'raw', 'matte', 'semigloss', 'gloss' ];
  222. export function GetWoodPreset( genus, finish ) {
  223. const params = woodParams[ genus ];
  224. let clearcoat, clearcoatRoughness, clearcoatDarken;
  225. switch ( finish ) {
  226. case 'gloss':
  227. clearcoatDarken = 0.2; clearcoatRoughness = 0.1; clearcoat = 1;
  228. break;
  229. case 'semigloss':
  230. clearcoatDarken = 0.4; clearcoatRoughness = 0.4; clearcoat = 1;
  231. break;
  232. case 'matte':
  233. clearcoatDarken = 0.6; clearcoatRoughness = 1; clearcoat = 1;
  234. break;
  235. case 'raw':
  236. default:
  237. clearcoatDarken = 1; clearcoatRoughness = 0; clearcoat = 0;
  238. }
  239. return { ...params, transformationMatrix: new THREE.Matrix4().copy( params.transformationMatrix ), genus, finish, clearcoat, clearcoatRoughness, clearcoatDarken };
  240. }
  241. const params = GetWoodPreset( WoodGenuses[ 0 ], Finishes[ 0 ] );
  242. const uniforms = {};
  243. uniforms.centerSize = TSL.uniform( params.centerSize ).onObjectUpdate( ( { material } ) => material.centerSize );
  244. uniforms.largeWarpScale = TSL.uniform( params.largeWarpScale ).onObjectUpdate( ( { material } ) => material.largeWarpScale );
  245. uniforms.largeGrainStretch = TSL.uniform( params.largeGrainStretch ).onObjectUpdate( ( { material } ) => material.largeGrainStretch );
  246. uniforms.smallWarpStrength = TSL.uniform( params.smallWarpStrength ).onObjectUpdate( ( { material } ) => material.smallWarpStrength );
  247. uniforms.smallWarpScale = TSL.uniform( params.smallWarpScale ).onObjectUpdate( ( { material } ) => material.smallWarpScale );
  248. uniforms.fineWarpStrength = TSL.uniform( params.fineWarpStrength ).onObjectUpdate( ( { material } ) => material.fineWarpStrength );
  249. uniforms.fineWarpScale = TSL.uniform( params.fineWarpScale ).onObjectUpdate( ( { material } ) => material.fineWarpScale );
  250. uniforms.ringThickness = TSL.uniform( params.ringThickness ).onObjectUpdate( ( { material } ) => material.ringThickness );
  251. uniforms.ringBias = TSL.uniform( params.ringBias ).onObjectUpdate( ( { material } ) => material.ringBias );
  252. uniforms.ringSizeVariance = TSL.uniform( params.ringSizeVariance ).onObjectUpdate( ( { material } ) => material.ringSizeVariance );
  253. uniforms.ringVarianceScale = TSL.uniform( params.ringVarianceScale ).onObjectUpdate( ( { material } ) => material.ringVarianceScale );
  254. uniforms.barkThickness = TSL.uniform( params.barkThickness ).onObjectUpdate( ( { material } ) => material.barkThickness );
  255. uniforms.splotchScale = TSL.uniform( params.splotchScale ).onObjectUpdate( ( { material } ) => material.splotchScale );
  256. uniforms.splotchIntensity = TSL.uniform( params.splotchIntensity ).onObjectUpdate( ( { material } ) => material.splotchIntensity );
  257. uniforms.cellScale = TSL.uniform( params.cellScale ).onObjectUpdate( ( { material } ) => material.cellScale );
  258. uniforms.cellSize = TSL.uniform( params.cellSize ).onObjectUpdate( ( { material } ) => material.cellSize );
  259. uniforms.darkGrainColor = TSL.uniform( new THREE.Color( params.darkGrainColor ) ).onObjectUpdate( ( { material }, self ) => self.value.set( material.darkGrainColor ) );
  260. uniforms.lightGrainColor = TSL.uniform( new THREE.Color( params.lightGrainColor ) ).onObjectUpdate( ( { material }, self ) => self.value.set( material.lightGrainColor ) );
  261. uniforms.transformationMatrix = TSL.uniform( new THREE.Matrix4().copy( params.transformationMatrix ) ).onObjectUpdate( ( { material } ) => material.transformationMatrix );
  262. const colorNode = wood(
  263. uniforms.transformationMatrix.mul( TSL.vec4( TSL.positionLocal, 1 ) ).xyz,
  264. uniforms.centerSize,
  265. uniforms.largeWarpScale,
  266. uniforms.largeGrainStretch,
  267. uniforms.smallWarpStrength,
  268. uniforms.smallWarpScale,
  269. uniforms.fineWarpStrength,
  270. uniforms.fineWarpScale,
  271. uniforms.ringThickness,
  272. uniforms.ringBias,
  273. uniforms.ringSizeVariance,
  274. uniforms.ringVarianceScale,
  275. uniforms.barkThickness,
  276. uniforms.splotchScale,
  277. uniforms.splotchIntensity,
  278. uniforms.cellScale,
  279. uniforms.cellSize,
  280. uniforms.darkGrainColor,
  281. uniforms.lightGrainColor
  282. ).mul( params.clearcoatDarken );
  283. /**
  284. * Procedural wood material using TSL (Three.js Shading Language).
  285. *
  286. * Usage examples:
  287. *
  288. * // Using presets (recommended for common wood types)
  289. * const material = WoodNodeMaterial.fromPreset('walnut', 'gloss');
  290. *
  291. * // Using custom parameters (for advanced customization)
  292. * const material = new WoodNodeMaterial({
  293. * centerSize: 1.2,
  294. * ringThickness: 1/40,
  295. * darkGrainColor: new THREE.Color('#2a1a0a'),
  296. * lightGrainColor: new THREE.Color('#8b4513'),
  297. * clearcoat: 1,
  298. * clearcoatRoughness: 0.3
  299. * });
  300. *
  301. * // Mixing presets with custom overrides
  302. * const walnutParams = GetWoodPreset('walnut', 'raw');
  303. * const material = new WoodNodeMaterial({
  304. * ...walnutParams,
  305. * ringThickness: 1/50, // Override specific parameter
  306. * clearcoat: 1 // Add finish
  307. * });
  308. */
  309. export class WoodNodeMaterial extends THREE.MeshPhysicalMaterial {
  310. static get type() {
  311. return 'WoodNodeMaterial';
  312. }
  313. constructor( params = {} ) {
  314. super();
  315. this.isWoodNodeMaterial = true;
  316. // Get default parameters from teak/raw preset
  317. const defaultParams = GetWoodPreset( 'teak', 'raw' );
  318. // Merge default params with provided params
  319. const finalParams = { ...defaultParams, ...params };
  320. for ( const key in finalParams ) {
  321. if ( key === 'genus' || key === 'finish' ) continue;
  322. if ( typeof finalParams[ key ] === 'string' ) {
  323. this[ key ] = new THREE.Color( finalParams[ key ] );
  324. } else {
  325. this[ key ] = finalParams[ key ];
  326. }
  327. }
  328. this.colorNode = colorNode;
  329. this.clearcoatNode = finalParams.clearcoat;
  330. this.clearcoatRoughness = finalParams.clearcoatRoughness;
  331. }
  332. // Static method to create material from preset
  333. static fromPreset( genus = 'teak', finish = 'raw' ) {
  334. const params = GetWoodPreset( genus, finish );
  335. return new WoodNodeMaterial( params );
  336. }
  337. }
粤ICP备19079148号