webgpu_tsl_wood.html 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="utf-8">
  5. <title>Three.js WebGPU - Procedural Wood Materials</title>
  6. <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
  7. <meta name="author" content="Logan Seeley"/>
  8. <link type="text/css" rel="stylesheet" href="main.css">
  9. <style>
  10. body {
  11. color:rgb(55, 55, 55);
  12. }
  13. #info a {
  14. color:#1cdfe2;
  15. }
  16. </style>
  17. </head>
  18. <body>
  19. <div id="info">
  20. <a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> webgpu - tsl procedural wood materials<br/>
  21. by Logan Seeley, based on <a href="https://www.youtube.com/watch?v=n7e0vxgBS8A">Lance Phan's Blender tutorial</a>
  22. </div>
  23. <script type="importmap">
  24. {
  25. "imports": {
  26. "three": "../build/three.webgpu.js",
  27. "three/webgpu": "../build/three.webgpu.js",
  28. "three/tsl": "../build/three.tsl.js",
  29. "three/addons/": "./jsm/"
  30. }
  31. }
  32. </script>
  33. <script type="module">
  34. import * as THREE from 'three';
  35. import * as TSL from 'three/tsl';
  36. import Stats from 'three/addons/libs/stats.module.js';
  37. import WebGPU from 'three/addons/capabilities/WebGPU.js';
  38. import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
  39. import { HDRLoader } from 'three/addons/loaders/HDRLoader.js';
  40. import { FontLoader } from 'three/addons/loaders/FontLoader.js';
  41. import { TextGeometry } from 'three/addons/geometries/TextGeometry.js';
  42. import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
  43. import { RoundedBoxGeometry } from 'three/addons/geometries/RoundedBoxGeometry.js';
  44. import { WoodNodeMaterial, WoodGenuses, Finishes } from 'three/addons/materials/WoodNodeMaterial.js';
  45. let scene, base, camera, renderer, controls, stats, font, blockGeometry, gui;
  46. // Helper function to get grid position
  47. function getGridPosition( woodIndex, finishIndex ) {
  48. return {
  49. x: 0,
  50. y: ( finishIndex - Finishes.length / 2 ) * 1.0,
  51. z: ( woodIndex - WoodGenuses.length / 2 + 0.45 ) * 1.0
  52. };
  53. }
  54. // Helper function to create the grid plane
  55. function createGridPlane() {
  56. const material = new THREE.MeshBasicNodeMaterial();
  57. const gridXZ = TSL.Fn( ( [ gridSize = TSL.float( 1.0 ), dotWidth = TSL.float( 0.1 ), lineWidth = TSL.float( 0.02 ) ] ) => {
  58. const coord = TSL.positionWorld.xz.div( gridSize );
  59. const grid = TSL.fract( coord );
  60. // Screen-space derivative for automatic antialiasing
  61. const fw = TSL.fwidth( coord );
  62. const smoothing = TSL.max( fw.x, fw.y ).mul( 0.5 );
  63. // Create squares at cell centers
  64. const squareDist = TSL.max( TSL.abs( grid.x.sub( 0.5 ) ), TSL.abs( grid.y.sub( 0.5 ) ) );
  65. const dots = TSL.smoothstep( dotWidth.add( smoothing ), dotWidth.sub( smoothing ), squareDist );
  66. // Create grid lines
  67. const lineX = TSL.smoothstep( lineWidth.add( smoothing ), lineWidth.sub( smoothing ), TSL.abs( grid.x.sub( 0.5 ) ) );
  68. const lineZ = TSL.smoothstep( lineWidth.add( smoothing ), lineWidth.sub( smoothing ), TSL.abs( grid.y.sub( 0.5 ) ) );
  69. const lines = TSL.max( lineX, lineZ );
  70. return TSL.max( dots, lines );
  71. } );
  72. const radialGradient = TSL.Fn( ( [ radius = TSL.float( 10.0 ), falloff = TSL.float( 1.0 ) ] ) => {
  73. return TSL.smoothstep( radius, radius.sub( falloff ), TSL.length( TSL.positionWorld ) );
  74. } );
  75. // Create grid pattern
  76. const gridPattern = gridXZ( 1.0, 0.03, 0.005 );
  77. const baseColor = TSL.vec4( 1.0, 1.0, 1.0, 0.0 );
  78. const gridColor = TSL.vec4( 0.5, 0.5, 0.5, 1.0 );
  79. // Mix base color with grid lines
  80. material.colorNode = gridPattern.mix( baseColor, gridColor ).mul( radialGradient( 30.0, 20.0 ) );
  81. material.transparent = true;
  82. const plane = new THREE.Mesh( new THREE.CircleGeometry( 40 ), material );
  83. plane.rotation.x = - Math.PI / 2;
  84. plane.renderOrder = - 1;
  85. return plane;
  86. }
  87. // Helper function to create and position labels
  88. function createLabel( text, font, material, position ) {
  89. const txt_geo = new TextGeometry( text, {
  90. font: font,
  91. size: 0.1,
  92. depth: 0.001,
  93. curveSegments: 12,
  94. bevelEnabled: false
  95. } );
  96. txt_geo.computeBoundingBox();
  97. const offx = - 0.5 * ( txt_geo.boundingBox.max.x - txt_geo.boundingBox.min.x );
  98. const offy = - 0.5 * ( txt_geo.boundingBox.max.y - txt_geo.boundingBox.min.y );
  99. const offz = - 0.5 * ( txt_geo.boundingBox.max.z - txt_geo.boundingBox.min.z );
  100. txt_geo.translate( offx, offy, offz );
  101. const label = new THREE.Group();
  102. const mesh = new THREE.Mesh( txt_geo );
  103. label.add( mesh );
  104. // Apply default rotation for labels
  105. label.rotateY( - Math.PI / 2 );
  106. label.children[ 0 ].material = material;
  107. label.position.copy( position );
  108. base.add( label );
  109. }
  110. async function init() {
  111. scene = new THREE.Scene();
  112. scene.background = new THREE.Color( 0xffffff );
  113. camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );
  114. camera.position.set( - 0.1, 5, 0.548 );
  115. renderer = new THREE.WebGPURenderer( { antialias: true } );
  116. renderer.setPixelRatio( 1.0 ); // important for performance
  117. renderer.setSize( window.innerWidth, window.innerHeight );
  118. renderer.toneMapping = THREE.NeutralToneMapping;
  119. renderer.toneMappingExposure = 1.0;
  120. renderer.setAnimationLoop( render );
  121. document.body.appendChild( renderer.domElement );
  122. controls = new OrbitControls( camera, renderer.domElement );
  123. controls.target.set( 0, 0, 0.548 );
  124. stats = new Stats();
  125. document.body.appendChild( stats.dom );
  126. gui = new GUI();
  127. font = await new FontLoader().loadAsync( './fonts/helvetiker_regular.typeface.json' );
  128. // Create shared block geometry
  129. blockGeometry = new RoundedBoxGeometry( 0.125, 0.9, 0.9, 10, 0.02 );
  130. base = new THREE.Group();
  131. base.rotation.set( 0, 0, - Math.PI / 2 );
  132. base.position.set( 0, 0, 0.548 );
  133. scene.add( base );
  134. const text_mat = new THREE.MeshStandardMaterial();
  135. text_mat.colorNode = TSL.color( '#000000' );
  136. // Create finish labels (using negative wood index for left column)
  137. for ( let y = 0; y < Finishes.length; y ++ ) {
  138. createLabel( Finishes[ y ], font, text_mat, getGridPosition( - 1, y ) );
  139. }
  140. // Create and add the grid plane
  141. const plane = createGridPlane();
  142. scene.add( plane );
  143. await new HDRLoader()
  144. .setPath( 'textures/equirectangular/' )
  145. .loadAsync( 'san_giuseppe_bridge_2k.hdr' ).then( ( texture ) => {
  146. texture.mapping = THREE.EquirectangularReflectionMapping;
  147. scene.environment = texture;
  148. scene.environmentIntensity = 2;
  149. } );
  150. // Create wood labels (using negative finish index for top row)
  151. for ( let x = 0; x < WoodGenuses.length; x ++ ) {
  152. createLabel( WoodGenuses[ x ], font, text_mat, getGridPosition( x, - 1 ) );
  153. }
  154. // Create wood blocks
  155. for ( let x = 0; x < WoodGenuses.length; x ++ ) {
  156. for ( let y = 0; y < Finishes.length; y ++ ) {
  157. const material = WoodNodeMaterial.fromPreset( WoodGenuses[ x ], Finishes[ y ] );
  158. const cube = new THREE.Mesh( blockGeometry, material );
  159. cube.position.copy( getGridPosition( x, y ) );
  160. material.transformationMatrix = new THREE.Matrix4().setPosition( new THREE.Vector3( -0.1, 0, Math.random() ) );
  161. base.add( cube );
  162. await new Promise( resolve => setTimeout( resolve, 0 ) );
  163. }
  164. }
  165. add_custom_wood( text_mat );
  166. }
  167. function render() {
  168. controls.update();
  169. stats.update();
  170. renderer.render( scene, camera );
  171. }
  172. window.addEventListener( 'resize', () => {
  173. camera.aspect = window.innerWidth / window.innerHeight;
  174. camera.updateProjectionMatrix();
  175. renderer.setSize( window.innerWidth, window.innerHeight );
  176. } );
  177. if ( WebGPU.isAvailable() ) {
  178. init();
  179. } else {
  180. document.body.appendChild( WebGPU.getErrorMessage() );
  181. }
  182. function add_custom_wood( text_mat ) {
  183. // Add "Custom" label (positioned at the end of the grid)
  184. createLabel( 'custom', font, text_mat, getGridPosition( Math.round( WoodGenuses.length / 2 - 1 ), 5 ) );
  185. // Create custom wood material with unique parameters
  186. const customMaterial = new WoodNodeMaterial( {
  187. centerSize: 1.11,
  188. largeWarpScale: 0.32,
  189. largeGrainStretch: 0.24,
  190. smallWarpStrength: 0.059,
  191. smallWarpScale: 2,
  192. fineWarpStrength: 0.006,
  193. fineWarpScale: 32.8,
  194. ringThickness: 1/34,
  195. ringBias: 0.03,
  196. ringSizeVariance: 0.03,
  197. ringVarianceScale: 4.4,
  198. barkThickness: 0.3,
  199. splotchScale: 0.2,
  200. splotchIntensity: 0.541,
  201. cellScale: 910,
  202. cellSize: 0.1,
  203. darkGrainColor: new THREE.Color( '#0c0504') ,
  204. lightGrainColor: new THREE.Color( '#926c50' ),
  205. clearcoat: 1,
  206. clearcoatRoughness: 0.2
  207. } );
  208. gui.add( customMaterial, 'centerSize', 0.0, 2.0, 0.01 ).name( 'centerSize' );
  209. gui.add( customMaterial, 'largeWarpScale', 0.0, 1.0, 0.001 ).name( 'largeWarpScale' );
  210. gui.add( customMaterial, 'largeGrainStretch', 0.0, 1.0, 0.001 ).name( 'largeGrainStretch' );
  211. gui.add( customMaterial, 'smallWarpStrength', 0.0, 0.2, 0.001 ).name( 'smallWarpStrength' );
  212. gui.add( customMaterial, 'smallWarpScale', 0.0, 5.0, 0.01 ).name( 'smallWarpScale' );
  213. gui.add( customMaterial, 'fineWarpStrength', 0.0, 0.05, 0.001 ).name( 'fineWarpStrength' );
  214. gui.add( customMaterial, 'fineWarpScale', 0.0, 50.0, 0.1 ).name( 'fineWarpScale' );
  215. gui.add( customMaterial, 'ringThickness', 0.0, 0.1, 0.001 ).name( 'ringThickness' );
  216. gui.add( customMaterial, 'ringBias', -0.2, 0.2, 0.001 ).name( 'ringBias' );
  217. gui.add( customMaterial, 'ringSizeVariance', 0.0, 0.2, 0.001 ).name( 'ringSizeVariance' );
  218. gui.add( customMaterial, 'ringVarianceScale', 0.0, 10.0, 0.1 ).name( 'ringVarianceScale' );
  219. gui.add( customMaterial, 'barkThickness', 0.0, 1.0, 0.01 ).name( 'barkThickness' );
  220. gui.add( customMaterial, 'splotchScale', 0.0, 1.0, 0.01 ).name( 'splotchScale' );
  221. gui.add( customMaterial, 'splotchIntensity', 0.0, 1.0, 0.01 ).name( 'splotchIntensity' );
  222. gui.add( customMaterial, 'cellScale', 100, 2000, 1 ).name( 'cellScale' );
  223. gui.add( customMaterial, 'cellSize', 0.01, 0.5, 0.001 ).name( 'cellSize' );
  224. gui.addColor( { darkGrainColor: '#0c0504' }, 'darkGrainColor' ).onChange( v => customMaterial.darkGrainColor.set( v ) );
  225. gui.addColor( { lightGrainColor: '#926c50' }, 'lightGrainColor' ).onChange( v => customMaterial.lightGrainColor.set( v ) );
  226. gui.add( customMaterial, 'clearcoat', 0.0, 1.0, 0.01 ).name( 'clearcoat' );
  227. gui.add( customMaterial, 'clearcoatRoughness', 0.0, 1.0, 0.01 ).name( 'clearcoatRoughness' );
  228. const cube = new THREE.Mesh( blockGeometry, customMaterial );
  229. customMaterial.transformationMatrix = new THREE.Matrix4().setPosition( new THREE.Vector3( -0.1, 0, Math.random() ) );
  230. cube.position.copy( getGridPosition( Math.round( WoodGenuses.length / 2 ), 5 ) );
  231. base.add( cube );
  232. }
  233. </script>
  234. </body>
  235. </html>
粤ICP备19079148号