Răsfoiți Sursa

Examples: Add TSL Procedural Wood Material (#31640)

* Added webgpu_procedural_wood

(cherry picked from commit fb0d8832c2e703e01998e235b8604504e909dd3c)

* Robustness

(cherry picked from commit 5711d8f977e8d737a0c5c17f3c6dc74b4548c979)

* Fixed Cedar

(cherry picked from commit d1813a26467f64a5bb4340e20f2d24823648b41e)

* rename example, change description

(cherry picked from commit 2a3572593c06cd634feaf0dfbc3030b377371373)

* Update ProceduralWood.js

(cherry picked from commit 9ae3876a8313206c82668abf236d507ee186f6ca)

* Tried to fix uniforms

(cherry picked from commit 6fd32bb85bfd8d1e26ab460ceb2f6c987a3e8517)

* move uniforms.  simplify variable names.  adopt cap-case, format.

(cherry picked from commit ade4e9d6583ee44f3ed80a010b659ac94c92ef42)

* get rid of randomness

(cherry picked from commit eb5a62f66252956483c1eab700386848dbd4e283)

* fix procedural wood using MeshPhysicalNodeMaterial

(cherry picked from commit 9fad1ed547e3812217c4b912327e817718308967)

* typo

(cherry picked from commit bbd22da0e7a6d6292daff45e50b8558f3fbbab0a)

* boost HDRI

(cherry picked from commit 7236e5e1054df2a37d5532884d704e3f7caf44ca)

* allow async loading.

(cherry picked from commit 531d75c2d677ade892f5bf80d2bf75aa9c8d15a1)

* Optimizations

(cherry picked from commit dc8562d8f363851c8802940ec7e96ae57ce427ba)

* fix typos

(cherry picked from commit fdc290ac702799310570e24dd280b7bdea27a00b)

* Prettified

(cherry picked from commit e01ab4c3cb37cdee52608dbed37fa8c9b174f45a)

* Alias Fix

Wood rings and cell structure suffered from aliasing.

Improved by blurring rings based on camera distance and changing cell size based on camera distance.

(cherry picked from commit b49b44bb7f99879b47ce4526580a1fa3711050e4)

* tone map exposure to 1

(cherry picked from commit 469157994d0a747763d6cbbccc9d255ce88e7353)

* Update webgpu_tsl_wood.html

Fix code style.

(cherry picked from commit bbbca28e13873d4666b92571d165ca00e66cae6c)

* cache colorNode

(cherry picked from commit 640fca89b9617f23884dd57f61fc6b905ee3ddb9)

* Update webgpu_tsl_wood.html

(cherry picked from commit 37abf5d10f997a87a406361d1a8cbba1026aa7c3)

* Update ProceduralWood.js parameters for improved wood texture appearance

(cherry picked from commit 8aa231dd5106b872265a13b4dcc6d999aaec219b)

* Converted all instances of wgslFn to Fn

(cherry picked from commit 9119a500a5cc2e9a312cfe9b22c76cd572fafc33)

* improve performance - possible cachekey issue

(cherry picked from commit e52cf02c24182781b82f03299f080a742d81e24a)

* Created WoodNodeMaterial

Extended StandardMeshNodeMaterial to create WoodNodeMaterial

(cherry picked from commit 26f7487877a812bca95bef88e4e2c2f986bb6d3c)

* Reverted voronoi3d back to wgsl

Converting voronoi3d to Fn caused frame drop and input lag. Merging e9f7c8b had no effect.

(cherry picked from commit 72057d2560924f34bed4358664fc22dd2cc94a33)

* Added grain direction and made originOffset a uniform

(cherry picked from commit 5ee2693403712033f9d9b1b514ce5d716c46bdbd)

* Added back MeshPhysicalNodeMaterial

(cherry picked from commit 895ca13f171d1c3afc5ca26324091158a5101a7d)

* originOffset -> grainPosition to align with grainRotation.

* ensure one can create custom woods, add custom wood block to example.

* refactor tsl_wood example to clean code

* Updated outdated example image

* Fixed screenshot size

* add exception

* cleanup

* name `ProceduralWood` -> `WoodNodeMaterial`

---------

Co-authored-by: Ben Houston <neuralsoft@gmail.com>
Co-authored-by: Michael Herzog <michael.herzog@human-interactive.org>
Co-authored-by: sunag <sunagbrasil@gmail.com>
Logan Seeley 5 luni în urmă
părinte
comite
5271d63961

+ 1 - 0
examples/files.json

@@ -461,6 +461,7 @@
 		"webgpu_tsl_vfx_flames",
 		"webgpu_tsl_vfx_linkedparticles",
 		"webgpu_tsl_vfx_tornado",
+		"webgpu_tsl_wood",
 		"webgpu_video_frame",
 		"webgpu_video_panorama",
 		"webgpu_volume_caustics",

+ 544 - 0
examples/jsm/materials/WoodNodeMaterial.js

@@ -0,0 +1,544 @@
+import * as THREE from 'three';
+import * as TSL from 'three/tsl';
+
+// some helpers below are ported from Blender and converted to TSL
+
+const mapRange = TSL.Fn( ( [ x, fromMin, fromMax, toMin, toMax, clmp ] ) => {
+
+	const factor = x.sub( fromMin ).div( fromMax.sub( fromMin ) );
+	const result = toMin.add( factor.mul( toMax.sub( toMin ) ) );
+
+	return TSL.select( clmp, TSL.max( TSL.min( result, toMax ), toMin ), result );
+
+} );
+
+const voronoi3d = TSL.wgslFn( `
+    fn voronoi3d(x: vec3<f32>, smoothness: f32, randomness: f32) -> f32
+    {
+        let p = floor(x);
+        let f = fract(x);
+
+        var res = 0.0;
+        var totalWeight = 0.0;
+        
+        for (var k = -1; k <= 1; k++)
+        {
+            for (var j = -1; j <= 1; j++)
+            {
+                for (var i = -1; i <= 1; i++)
+                {
+                    let b = vec3<f32>(f32(i), f32(j), f32(k));
+                    let hashOffset = hash3d(p + b) * randomness;
+                    let r = b - f + hashOffset;
+                    let d = length(r);
+                    
+                    let weight = exp(-d * d / max(smoothness * smoothness, 0.001));
+                    res += d * weight;
+                    totalWeight += weight;
+                }
+            }
+        }
+        
+        if (totalWeight > 0.0)
+        {
+            res /= totalWeight;
+        }
+        
+        return smoothstep(0.0, 1.0, res);
+    }
+
+    fn hash3d(p: vec3<f32>) -> vec3<f32>
+    {
+        var p3 = fract(p * vec3<f32>(0.1031, 0.1030, 0.0973));
+        p3 += dot(p3, p3.yzx + 33.33);
+        return fract((p3.xxy + p3.yzz) * p3.zyx);
+    }
+` );
+
+// const hash3d = TSL.Fn( ( [ p ] ) => {
+
+// 	const p3 = p.mul( TSL.vec3( 0.1031, 0.1030, 0.0973 ) ).fract();
+// 	const dotProduct = p3.dot( p3.yzx.add( 33.33 ) );
+// 	p3.addAssign( dotProduct );
+
+// 	return p3.xxy.add( p3.yzz ).mul( p3.zyx ).fract();
+
+// } );
+
+// const voronoi3d = TSL.Fn( ( [ x, smoothness, randomness ] ) => {
+// 	let p = TSL.floor(x);
+// 	let f = TSL.fract(x);
+
+// 	var res = TSL.float(0.0);
+// 	var totalWeight = TSL.float(0.0);
+
+// 	TSL.Loop( 3, 3, 3, ( { k, j, i } ) => {
+// 		let b = TSL.vec3(TSL.float(i).sub(1), TSL.float(j).sub(1), TSL.float(k).sub(1));
+// 		let hashOffset = hash3d(p.add(b)).mul(randomness);
+// 		let r = b.sub(f).add(hashOffset);
+// 		let d = TSL.length(r);
+
+// 		let weight = TSL.exp(d.negate().mul(d).div(TSL.max(smoothness.mul(smoothness), 0.001)));
+// 		res.addAssign(d.mul(weight));
+// 		totalWeight.addAssign(weight);
+// 	} );
+
+// 	res.assign(TSL.select(totalWeight.greaterThan(0.0), res.div(totalWeight), res));
+
+// 	return TSL.smoothstep(0.0, 1.0, res);
+// } );
+
+const softLightMix = TSL.Fn( ( [ t, col1, col2 ] ) => {
+
+	const tm = TSL.float( 1.0 ).sub( t );
+
+	const one = TSL.vec3( 1.0 );
+	const scr = one.sub( one.sub( col2 ).mul( one.sub( col1 ) ) );
+
+	return tm.mul( col1 ).add( t.mul( one.sub( col1 ).mul( col2 ).mul( col1 ).add( col1.mul( scr ) ) ) );
+
+} );
+
+const noiseFbm = TSL.Fn( ( [ p, detail, roughness, lacunarity, useNormalize ] ) => {
+
+	const fscale = TSL.float( 1.0 ).toVar();
+	const amp = TSL.float( 1.0 ).toVar();
+	const maxamp = TSL.float( 0.0 ).toVar();
+	const sum = TSL.float( 0.0 ).toVar();
+
+	const iterations = detail.floor();
+
+	TSL.Loop( iterations, () => {
+
+		const t = TSL.mx_noise_float( p.mul( fscale ) );
+		sum.addAssign( t.mul( amp ) );
+		maxamp.addAssign( amp );
+		amp.mulAssign( roughness );
+		fscale.mulAssign( lacunarity );
+
+	} );
+
+	const rmd = detail.sub( iterations );
+	const hasRemainder = rmd.greaterThan( 0.001 );
+
+	return TSL.select(
+		hasRemainder,
+		TSL.select(
+			useNormalize.equal( 1 ),
+			( () => {
+
+				const t = TSL.mx_noise_float( p.mul( fscale ) );
+				const sum2 = sum.add( t.mul( amp ) );
+				const maxamp2 = maxamp.add( amp );
+				const normalizedSum = sum.div( maxamp ).mul( 0.5 ).add( 0.5 );
+				const normalizedSum2 = sum2.div( maxamp2 ).mul( 0.5 ).add( 0.5 );
+				return TSL.mix( normalizedSum, normalizedSum2, rmd );
+
+			} )(),
+			( () => {
+
+				const t = TSL.mx_noise_float( p.mul( fscale ) );
+				const sum2 = sum.add( t.mul( amp ) );
+				return TSL.mix( sum, sum2, rmd );
+
+			} )()
+		),
+		TSL.select(
+			useNormalize.equal( 1 ),
+			sum.div( maxamp ).mul( 0.5 ).add( 0.5 ),
+			sum
+		)
+	);
+
+} );
+
+const noiseFbm3d = TSL.Fn( ( [ p, detail, roughness, lacunarity, useNormalize ] ) => {
+
+	const fscale = TSL.float( 1.0 ).toVar();
+
+	const amp = TSL.float( 1.0 ).toVar();
+	const maxamp = TSL.float( 0.0 ).toVar();
+	const sum = TSL.vec3( 0.0 ).toVar();
+
+	const iterations = detail.floor();
+
+	TSL.Loop( iterations, () => {
+
+		const t = TSL.mx_noise_vec3( p.mul( fscale ) );
+		sum.addAssign( t.mul( amp ) );
+		maxamp.addAssign( amp );
+		amp.mulAssign( roughness );
+		fscale.mulAssign( lacunarity );
+
+	} );
+
+	const rmd = detail.sub( iterations );
+	const hasRemainder = rmd.greaterThan( 0.001 );
+
+	return TSL.select(
+		hasRemainder,
+		TSL.select(
+			useNormalize.equal( 1 ),
+			( () => {
+
+				const t = TSL.mx_noise_vec3( p.mul( fscale ) );
+				const sum2 = sum.add( t.mul( amp ) );
+				const maxamp2 = maxamp.add( amp );
+				const normalizedSum = sum.div( maxamp ).mul( 0.5 ).add( 0.5 );
+				const normalizedSum2 = sum2.div( maxamp2 ).mul( 0.5 ).add( 0.5 );
+				return TSL.mix( normalizedSum, normalizedSum2, rmd );
+
+			} )(),
+			( () => {
+
+				const t = TSL.mx_noise_vec3( p.mul( fscale ) );
+				const sum2 = sum.add( t.mul( amp ) );
+				return TSL.mix( sum, sum2, rmd );
+
+			} )()
+		),
+		TSL.select(
+			useNormalize.equal( 1 ),
+			sum.div( maxamp ).mul( 0.5 ).add( 0.5 ),
+			sum
+		)
+	);
+
+} );
+
+const woodCenter = TSL.Fn( ( [ p, centerSize ] ) => {
+
+	const pxyCenter = p.mul( TSL.vec3( 1, 1, 0 ) ).length();
+	const center = mapRange( pxyCenter, 0, 1, 0, centerSize, true );
+
+	return center;
+
+} );
+
+const spaceWarp = TSL.Fn( ( [ p, warpStrength, xyScale, zScale ] ) => {
+
+	const combinedXyz = TSL.vec3( xyScale, xyScale, zScale ).mul( p );
+	const noise = noiseFbm3d( combinedXyz.mul( 1.6 * 1.5 ), TSL.float( 1 ), TSL.float( 0.5 ), TSL.float( 2 ), TSL.int( 1 ) ).sub( 0.5 ).mul( warpStrength );
+	const pXy = p.mul( TSL.vec3( 1, 1, 0 ) );
+	const normalizedXy = pXy.normalize();
+	const warp = noise.mul( normalizedXy ).add( pXy );
+
+	return warp;
+
+} );
+
+const woodRings = TSL.Fn( ( [ w, ringCount, ringBias, ringSizeVariance, ringVarianceScale, barkThickness ] ) => {
+
+	const rings = noiseFbm( w.mul( ringVarianceScale ), TSL.float( 1 ), TSL.float( 0.5 ), TSL.float( 1 ), TSL.int( 1 ) ).mul( ringSizeVariance ).add( w ).mul( ringCount ).fract().mul( barkThickness );
+
+	const sharpRings = TSL.min( mapRange( rings, 0, ringBias, 0, 1, TSL.bool( true ) ), mapRange( rings, ringBias, 1, 1, 0, TSL.bool( true ) ) );
+
+	const blurAmount = TSL.max( TSL.positionView.length().div( 10 ), 1 );
+	const blurredRings = TSL.smoothstep( blurAmount.negate(), blurAmount, sharpRings.sub( 0.5 ) ).mul( 0.5 ).add( 0.5 );
+
+	return blurredRings;
+
+} );
+
+const woodDetail = TSL.Fn( ( [ warp, p, y, splotchScale ] ) => {
+
+	const radialCoords = TSL.clamp( TSL.atan( warp.y, warp.x ).div( TSL.PI2 ).add( 0.5 ), 0, 1 ).mul( TSL.PI2.mul( 3 ) );
+	const combinedXyz = TSL.vec3( radialCoords.sin(), y, radialCoords.cos().mul( p.z ) );
+	const scaled = TSL.vec3( 0.1, 1.19, 0.05 ).mul( combinedXyz );
+
+	return noiseFbm( scaled.mul( splotchScale ), TSL.float( 1 ), TSL.float( 0.5 ), TSL.float( 2 ), TSL.bool( true ) );
+
+} );
+
+const cellStructure = TSL.Fn( ( [ p, cellScale, cellSize ] ) => {
+
+	const warp = spaceWarp( p.mul( cellScale.div( 50 ) ), cellScale.div( 1000 ), 0.1, 1.77 );
+	const cells = voronoi3d( warp.xy.mul( 75 ), 0.5, 1 );
+
+	return mapRange( cells, cellSize, cellSize.add( 0.21 ), 0, 1, TSL.bool( true ) );
+
+} );
+
+const wood = TSL.Fn( ( [
+	p,
+	centerSize,
+	largeWarpScale,
+	largeGrainStretch,
+	smallWarpStrength,
+	smallWarpScale,
+	fineWarpStrength,
+	fineWarpScale,
+	ringCount,
+	ringBias,
+	ringSizeVariance,
+	ringVarianceScale,
+	barkThickness,
+	splotchScale,
+	splotchIntensity,
+	cellScale,
+	cellSize,
+	darkGrainColor,
+	lightGrainColor
+] ) => {
+
+	const center = woodCenter( p, centerSize );
+	const mainWarp = spaceWarp( spaceWarp( p, center, largeWarpScale, largeGrainStretch ), smallWarpStrength, smallWarpScale, 0.17 );
+	const detailWarp = spaceWarp( mainWarp, fineWarpStrength, fineWarpScale, 0.17 );
+	const rings = woodRings( detailWarp.length(), ringCount, ringBias, ringSizeVariance, ringVarianceScale, barkThickness );
+	const detail = woodDetail( detailWarp, p, detailWarp.length(), splotchScale );
+	const cells = cellStructure( mainWarp, cellScale, cellSize.div( TSL.max( TSL.positionView.length().mul( 10 ), 1 ) ) );
+	const baseColor = TSL.mix( darkGrainColor, lightGrainColor, rings );
+
+	return softLightMix( splotchIntensity, softLightMix( 0.407, baseColor, cells ), detail );
+
+} );
+
+const woodParams = {
+	teak: {
+		grainPosition: { x: - 0.4, y: 0, z: 0 },
+		grainRotation: { x: 0, y: 0, z: 0 },
+		centerSize: 1.11, largeWarpScale: 0.32, largeGrainStretch: 0.24, smallWarpStrength: 0.059,
+		smallWarpScale: 2, fineWarpStrength: 0.006, fineWarpScale: 32.8, ringCount: 34,
+		ringBias: 0.03, ringSizeVariance: 0.03, ringVarianceScale: 4.4, barkThickness: 0.3,
+		splotchScale: 0.2, splotchIntensity: 0.541, cellScale: 910, cellSize: 0.1,
+		darkGrainColor: '#0c0504', lightGrainColor: '#926c50'
+	},
+	walnut: {
+		grainPosition: { x: - 0.4, y: 0, z: 0 },
+		grainRotation: { x: 0, y: 0, z: 0 },
+		centerSize: 1.07, largeWarpScale: 0.42, largeGrainStretch: 0.34, smallWarpStrength: 0.016,
+		smallWarpScale: 10.3, fineWarpStrength: 0.028, fineWarpScale: 12.7, ringCount: 32,
+		ringBias: 0.08, ringSizeVariance: 0.03, ringVarianceScale: 5.5, barkThickness: 0.98,
+		splotchScale: 1.84, splotchIntensity: 0.97, cellScale: 710, cellSize: 0.31,
+		darkGrainColor: '#311e13', lightGrainColor: '#523424'
+	},
+	white_oak: {
+		grainPosition: { x: - 0.4, y: 0, z: 0 },
+		grainRotation: { x: 0, y: 0, z: 0 },
+		centerSize: 1.23, largeWarpScale: 0.21, largeGrainStretch: 0.21, smallWarpStrength: 0.034,
+		smallWarpScale: 2.44, fineWarpStrength: 0.01, fineWarpScale: 14.3, ringCount: 34,
+		ringBias: 0.82, ringSizeVariance: 0.16, ringVarianceScale: 1.4, barkThickness: 0.7,
+		splotchScale: 0.2, splotchIntensity: 0.541, cellScale: 800, cellSize: 0.28,
+		darkGrainColor: '#8b4c21', lightGrainColor: '#c57e43'
+	},
+	pine: {
+		grainPosition: { x: - 0.4, y: 0, z: - 0.2 },
+		grainRotation: { x: 0, y: 0, z: 0 },
+		centerSize: 1.23, largeWarpScale: 0.21, largeGrainStretch: 0.18, smallWarpStrength: 0.041,
+		smallWarpScale: 2.44, fineWarpStrength: 0.006, fineWarpScale: 23.2, ringCount: 24,
+		ringBias: 0.1, ringSizeVariance: 0.07, ringVarianceScale: 5, barkThickness: 0.35,
+		splotchScale: 0.51, splotchIntensity: 3.32, cellScale: 1480, cellSize: 0.07,
+		darkGrainColor: '#c58355', lightGrainColor: '#d19d61'
+	},
+	poplar: {
+		grainPosition: { x: - 0.4, y: 0, z: 0.2 },
+		grainRotation: { x: 0, y: 0, z: 0 },
+		centerSize: 1.43, largeWarpScale: 0.33, largeGrainStretch: 0.18, smallWarpStrength: 0.04,
+		smallWarpScale: 4.3, fineWarpStrength: 0.004, fineWarpScale: 33.6, ringCount: 37,
+		ringBias: 0.07, ringSizeVariance: 0.03, ringVarianceScale: 3.8, barkThickness: 0.3,
+		splotchScale: 1.92, splotchIntensity: 0.71, cellScale: 830, cellSize: 0.04,
+		darkGrainColor: '#716347', lightGrainColor: '#998966'
+	},
+	maple: {
+		grainPosition: { x: - 0.4, y: 0.3, z: - 0.2 },
+		grainRotation: { x: 0, y: 0, z: 0 },
+		centerSize: 1.4, largeWarpScale: 0.38, largeGrainStretch: 0.25, smallWarpStrength: 0.067,
+		smallWarpScale: 2.5, fineWarpStrength: 0.005, fineWarpScale: 33.6, ringCount: 35,
+		ringBias: 0.1, ringSizeVariance: 0.07, ringVarianceScale: 4.6, barkThickness: 0.61,
+		splotchScale: 0.46, splotchIntensity: 1.49, cellScale: 800, cellSize: 0.03,
+		darkGrainColor: '#b08969', lightGrainColor: '#bc9d7d'
+	},
+	red_oak: {
+		grainPosition: { x: - 0.4, y: 0, z: 0.4 },
+		grainRotation: { x: 0, y: 0, z: 0 },
+		centerSize: 1.21, largeWarpScale: 0.24, largeGrainStretch: 0.25, smallWarpStrength: 0.044,
+		smallWarpScale: 2.54, fineWarpStrength: 0.01, fineWarpScale: 14.5, ringCount: 34,
+		ringBias: 0.92, ringSizeVariance: 0.03, ringVarianceScale: 5.6, barkThickness: 1.01,
+		splotchScale: 0.28, splotchIntensity: 3.48, cellScale: 800, cellSize: 0.25,
+		darkGrainColor: '#af613b', lightGrainColor: '#e0a27a'
+	},
+	cherry: {
+		grainPosition: { x: - 0.4, y: 0.3, z: 0 },
+		grainRotation: { x: 0, y: 0, z: 0 },
+		centerSize: 1.33, largeWarpScale: 0.11, largeGrainStretch: 0.33, smallWarpStrength: 0.024,
+		smallWarpScale: 2.48, fineWarpStrength: 0.01, fineWarpScale: 15.3, ringCount: 36,
+		ringBias: 0.02, ringSizeVariance: 0.04, ringVarianceScale: 6.5, barkThickness: 0.09,
+		splotchScale: 1.27, splotchIntensity: 1.24, cellScale: 1530, cellSize: 0.15,
+		darkGrainColor: '#913f27', lightGrainColor: '#b45837'
+	},
+	cedar: {
+		grainPosition: { x: - 0.4, y: 0.1, z: 0.1 },
+		grainRotation: { x: 0, y: 0, z: 0 },
+		centerSize: 1.11, largeWarpScale: 0.39, largeGrainStretch: 0.12, smallWarpStrength: 0.061,
+		smallWarpScale: 1.9, fineWarpStrength: 0.006, fineWarpScale: 4.8, ringCount: 25,
+		ringBias: 0.01, ringSizeVariance: 0.07, ringVarianceScale: 6.7, barkThickness: 0.1,
+		splotchScale: 0.61, splotchIntensity: 2.54, cellScale: 630, cellSize: 0.19,
+		darkGrainColor: '#9a5b49', lightGrainColor: '#ae745e'
+	},
+	mahogany: {
+		grainPosition: { x: - 0.4, y: 0.2, z: 0 },
+		grainRotation: { x: 0, y: 0, z: 0 },
+		centerSize: 1.25, largeWarpScale: 0.26, largeGrainStretch: 0.29, smallWarpStrength: 0.044,
+		smallWarpScale: 2.54, fineWarpStrength: 0.01, fineWarpScale: 15.3, ringCount: 38,
+		ringBias: 0.01, ringSizeVariance: 0.33, ringVarianceScale: 1.2, barkThickness: 0.07,
+		splotchScale: 0.77, splotchIntensity: 1.39, cellScale: 1400, cellSize: 0.23,
+		darkGrainColor: '#501d12', lightGrainColor: '#6d3722'
+	}
+};
+
+export const WoodGenuses = [ 'teak', 'walnut', 'white_oak', 'pine', 'poplar', 'maple', 'red_oak', 'cherry', 'cedar', 'mahogany' ];
+export const Finishes = [ 'raw', 'matte', 'semigloss', 'gloss' ];
+
+export function GetWoodPreset( genus, finish ) {
+
+	const params = woodParams[ genus ];
+
+	let clearcoat, clearcoatRoughness, clearcoatDarken;
+
+	switch ( finish ) {
+
+		case 'gloss':
+			clearcoatDarken = 0.2; clearcoatRoughness = 0.1; clearcoat = 1;
+			break;
+
+		case 'semigloss':
+			clearcoatDarken = 0.4; clearcoatRoughness = 0.4; clearcoat = 1;
+			break;
+
+		case 'matte':
+			clearcoatDarken = 0.6; clearcoatRoughness = 1; clearcoat = 1;
+			break;
+
+		case 'raw':
+		default:
+			clearcoatDarken = 1; clearcoatRoughness = 0; clearcoat = 0;
+
+	}
+
+	return { ...params, grainPosition: new THREE.Vector3().copy( params.grainPosition ), grainRotation: new THREE.Vector3().copy( params.grainRotation ), genus, finish, clearcoat, clearcoatRoughness, clearcoatDarken };
+
+}
+
+const params = GetWoodPreset( WoodGenuses[ 0 ], Finishes[ 0 ] );
+const uniforms = {};
+
+uniforms.centerSize = TSL.uniform( params.centerSize ).onObjectUpdate( ( { material } ) => material.centerSize );
+uniforms.largeWarpScale = TSL.uniform( params.largeWarpScale ).onObjectUpdate( ( { material } ) => material.largeWarpScale );
+uniforms.largeGrainStretch = TSL.uniform( params.largeGrainStretch ).onObjectUpdate( ( { material } ) => material.largeGrainStretch );
+uniforms.smallWarpStrength = TSL.uniform( params.smallWarpStrength ).onObjectUpdate( ( { material } ) => material.smallWarpStrength );
+uniforms.smallWarpScale = TSL.uniform( params.smallWarpScale ).onObjectUpdate( ( { material } ) => material.smallWarpScale );
+uniforms.fineWarpStrength = TSL.uniform( params.fineWarpStrength ).onObjectUpdate( ( { material } ) => material.fineWarpStrength );
+uniforms.fineWarpScale = TSL.uniform( params.fineWarpScale ).onObjectUpdate( ( { material } ) => material.fineWarpScale );
+uniforms.ringCount = TSL.uniform( params.ringCount ).onObjectUpdate( ( { material } ) => material.ringCount );
+uniforms.ringBias = TSL.uniform( params.ringBias ).onObjectUpdate( ( { material } ) => material.ringBias );
+uniforms.ringSizeVariance = TSL.uniform( params.ringSizeVariance ).onObjectUpdate( ( { material } ) => material.ringSizeVariance );
+uniforms.ringVarianceScale = TSL.uniform( params.ringVarianceScale ).onObjectUpdate( ( { material } ) => material.ringVarianceScale );
+uniforms.barkThickness = TSL.uniform( params.barkThickness ).onObjectUpdate( ( { material } ) => material.barkThickness );
+uniforms.splotchScale = TSL.uniform( params.splotchScale ).onObjectUpdate( ( { material } ) => material.splotchScale );
+uniforms.splotchIntensity = TSL.uniform( params.splotchIntensity ).onObjectUpdate( ( { material } ) => material.splotchIntensity );
+uniforms.cellScale = TSL.uniform( params.cellScale ).onObjectUpdate( ( { material } ) => material.cellScale );
+uniforms.cellSize = TSL.uniform( params.cellSize ).onObjectUpdate( ( { material } ) => material.cellSize );
+uniforms.darkGrainColor = TSL.uniform( new THREE.Color( params.darkGrainColor ) ).onObjectUpdate( ( { material }, self ) => self.value.set( material.darkGrainColor ) );
+uniforms.lightGrainColor = TSL.uniform( new THREE.Color( params.lightGrainColor ) ).onObjectUpdate( ( { material }, self ) => self.value.set( material.lightGrainColor ) );
+uniforms.grainPosition = TSL.uniform( new THREE.Vector3().copy( params.grainPosition ) ).onObjectUpdate( ( { material } ) => material.grainPosition );
+uniforms.grainRotation = TSL.uniform( new THREE.Vector3().copy( params.grainRotation ) ).onObjectUpdate( ( { material } ) => material.grainRotation );
+
+const colorNode = wood(
+	TSL.rotate( TSL.positionLocal.add( uniforms.grainPosition ), uniforms.grainRotation ),
+	uniforms.centerSize,
+	uniforms.largeWarpScale,
+	uniforms.largeGrainStretch,
+	uniforms.smallWarpStrength,
+	uniforms.smallWarpScale,
+	uniforms.fineWarpStrength,
+	uniforms.fineWarpScale,
+	uniforms.ringCount,
+	uniforms.ringBias,
+	uniforms.ringSizeVariance,
+	uniforms.ringVarianceScale,
+	uniforms.barkThickness,
+	uniforms.splotchScale,
+	uniforms.splotchIntensity,
+	uniforms.cellScale,
+	uniforms.cellSize,
+	uniforms.darkGrainColor,
+	uniforms.lightGrainColor
+).mul( params.clearcoatDarken );
+
+/**
+ * Procedural wood material using TSL (Three.js Shading Language).
+ *
+ * Usage examples:
+ *
+ * // Using presets (recommended for common wood types)
+ * const material = WoodNodeMaterial.fromPreset('walnut', 'gloss');
+ *
+ * // Using custom parameters (for advanced customization)
+ * const material = new WoodNodeMaterial({
+ *   centerSize: 1.2,
+ *   ringCount: 40,
+ *   darkGrainColor: new THREE.Color('#2a1a0a'),
+ *   lightGrainColor: new THREE.Color('#8b4513'),
+ *   clearcoat: 1,
+ *   clearcoatRoughness: 0.3
+ * });
+ *
+ * // Mixing presets with custom overrides
+ * const walnutParams = GetWoodPreset('walnut', 'raw');
+ * const material = new WoodNodeMaterial({
+ *   ...walnutParams,
+ *   ringCount: 50,  // Override specific parameter
+ *   clearcoat: 1    // Add finish
+ * });
+ */
+export class WoodNodeMaterial extends THREE.MeshPhysicalNodeMaterial {
+
+	static get type() {
+
+		return 'WoodNodeMaterial';
+
+	}
+
+	constructor( params = {} ) {
+
+		super();
+
+		this.isWoodNodeMaterial = true;
+
+		// Get default parameters from teak/raw preset
+		const defaultParams = GetWoodPreset( 'teak', 'raw' );
+
+		// Merge default params with provided params
+		const finalParams = { ...defaultParams, ...params };
+
+		for ( const key in finalParams ) {
+
+			if ( key === 'genus' || key === 'finish' ) continue;
+
+			if ( typeof finalParams[ key ] === 'string' ) {
+
+				this[ key ] = new THREE.Color( finalParams[ key ] );
+
+			} else {
+
+				this[ key ] = finalParams[ key ];
+
+			}
+
+		}
+
+		this.colorNode = colorNode;
+		this.clearcoatNode = finalParams.clearcoat;
+		this.clearcoatRoughness = finalParams.clearcoatRoughness;
+
+	}
+
+	// Static method to create material from preset
+	static fromPreset( genus = 'teak', finish = 'raw' ) {
+
+		const params = GetWoodPreset( genus, finish );
+		return new WoodNodeMaterial( params );
+
+	}
+
+}

BIN
examples/screenshots/webgpu_tsl_wood.jpg


+ 289 - 0
examples/webgpu_tsl_wood.html

@@ -0,0 +1,289 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<meta charset="utf-8">
+		<title>Three.js WebGPU - Procedural Wood Materials</title>
+		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
+		<meta name="author" content="Logan Seeley"/>
+		<link type="text/css" rel="stylesheet" href="main.css">
+		<style>
+
+			body {
+				color:white;
+			}
+
+			#info a {
+				color:#1cdfe2;
+			}
+
+		</style>
+	</head>
+	<body>
+
+		<div id="info">
+			<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> webgpu - tsl procedural wood materials<br/>
+			by Logan Seeley, based on <a href="https://www.youtube.com/watch?v=n7e0vxgBS8A">Lance Phan's Blender tutorial</a>
+		</div>
+
+		<script type="importmap">
+			{
+				"imports": {
+					"three": "../build/three.webgpu.js",
+					"three/webgpu": "../build/three.webgpu.js",
+					"three/tsl": "../build/three.tsl.js",
+					"three/addons/": "./jsm/"
+				}
+			}
+		</script>
+
+		<script type="module">
+			import * as THREE from 'three';
+			import * as TSL from 'three/tsl';
+			import Stats from 'three/addons/libs/stats.module.js';
+			import WebGPU from 'three/addons/capabilities/WebGPU.js';
+			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+			import { HDRLoader } from 'three/addons/loaders/HDRLoader.js';
+			import { FontLoader } from 'three/addons/loaders/FontLoader.js';
+			import { TextGeometry } from 'three/addons/geometries/TextGeometry.js';
+			import { RoundedBoxGeometry } from 'three/addons/geometries/RoundedBoxGeometry.js';
+			import { WoodNodeMaterial, WoodGenuses, Finishes } from 'three/addons/materials/WoodNodeMaterial.js';
+
+			let scene, base, camera, renderer, controls, stats, font, blockGeometry;
+
+			// Helper function to get grid position
+			function getGridPosition( woodIndex, finishIndex ) {
+
+				return {
+					x: 0,
+					y: ( finishIndex - Finishes.length / 2 ) * 1.0,
+					z: ( woodIndex - WoodGenuses.length / 2 + 0.45 ) * 1.0
+				};
+
+			}
+
+			// Helper function to create the grid plane
+			function createGridPlane() {
+
+				const material = new THREE.MeshBasicNodeMaterial();
+
+				const gridXZ = TSL.Fn( ( [ gridSize = TSL.float( 1.0 ), dotWidth = TSL.float( 0.1 ), lineWidth = TSL.float( 0.02 ) ] ) => {
+
+					const coord = TSL.positionWorld.xz.div( gridSize );
+					const grid = TSL.fract( coord );
+
+					// Screen-space derivative for automatic antialiasing
+					const fw = TSL.fwidth( coord );
+					const smoothing = TSL.max( fw.x, fw.y ).mul( 0.5 );
+
+					// Create squares at cell centers
+					const squareDist = TSL.max( TSL.abs( grid.x.sub( 0.5 ) ), TSL.abs( grid.y.sub( 0.5 ) ) );
+					const dots = TSL.smoothstep( dotWidth.add( smoothing ), dotWidth.sub( smoothing ), squareDist );
+
+					// Create grid lines
+					const lineX = TSL.smoothstep( lineWidth.add( smoothing ), lineWidth.sub( smoothing ), TSL.abs( grid.x.sub( 0.5 ) ) );
+					const lineZ = TSL.smoothstep( lineWidth.add( smoothing ), lineWidth.sub( smoothing ), TSL.abs( grid.y.sub( 0.5 ) ) );
+					const lines = TSL.max( lineX, lineZ );
+
+					return TSL.max( dots, lines );
+
+				} );
+
+				const radialGradient = TSL.Fn( ( [ radius = TSL.float( 10.0 ), falloff = TSL.float( 1.0 ) ] ) => {
+
+					return TSL.smoothstep( radius, radius.sub( falloff ), TSL.length( TSL.positionWorld ) );
+
+				} );
+
+				// Create grid pattern
+				const gridPattern = gridXZ( 1.0, 0.03, 0.005 );
+				const baseColor = TSL.vec4( 1.0, 1.0, 1.0, 0.0 );
+				const gridColor = TSL.vec4( 0.5, 0.5, 0.5, 1.0 );
+
+				// Mix base color with grid lines
+				material.colorNode = gridPattern.mix( baseColor, gridColor ).mul( radialGradient( 30.0, 20.0 ) );
+				material.transparent = true;
+
+				const plane = new THREE.Mesh( new THREE.CircleGeometry( 40 ), material );
+				plane.rotation.x = - Math.PI / 2;
+				plane.renderOrder = - 1;
+
+				return plane;
+
+			}
+
+			// Helper function to create and position labels
+			function createLabel( text, font, material, position ) {
+
+				const txt_geo = new TextGeometry( text, {
+					font: font,
+					size: 0.1,
+					depth: 0.001,
+					curveSegments: 12,
+					bevelEnabled: false
+				} );
+
+				txt_geo.computeBoundingBox();
+				const offx = - 0.5 * ( txt_geo.boundingBox.max.x - txt_geo.boundingBox.min.x );
+				const offy = - 0.5 * ( txt_geo.boundingBox.max.y - txt_geo.boundingBox.min.y );
+				const offz = - 0.5 * ( txt_geo.boundingBox.max.z - txt_geo.boundingBox.min.z );
+				txt_geo.translate( offx, offy, offz );
+
+				const label = new THREE.Group();
+				const mesh = new THREE.Mesh( txt_geo );
+				label.add( mesh );
+
+				// Apply default rotation for labels
+				label.rotateY( - Math.PI / 2 );
+
+				label.children[ 0 ].material = material;
+				label.position.copy( position );
+				base.add( label );
+			
+			}
+
+			async function init() {
+
+				scene = new THREE.Scene();
+				scene.background = new THREE.Color( 0xffffff );
+			
+
+				camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );
+				camera.position.set( - 0.1, 5, 0.548 );
+
+				renderer = new THREE.WebGPURenderer( { antialias: true } );
+				renderer.setPixelRatio( 1.0 ); // important for performance
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				renderer.toneMapping = THREE.NeutralToneMapping;
+				renderer.toneMappingExposure = 1.0;
+				renderer.setAnimationLoop( render );
+				document.body.appendChild( renderer.domElement );
+
+				controls = new OrbitControls( camera, renderer.domElement );
+				controls.target.set( 0, 0, 0.548 );
+
+				stats = new Stats();
+				document.body.appendChild( stats.dom );
+			
+				font = await new FontLoader().loadAsync( './fonts/helvetiker_regular.typeface.json' );
+
+				// Create shared block geometry
+				blockGeometry = new RoundedBoxGeometry( 0.125, 0.9, 0.9, 10, 0.02 );
+
+				base = new THREE.Group();
+				base.rotation.set( 0, 0, - Math.PI / 2 );
+				base.position.set( 0, 0, 0.548 );
+				scene.add( base );
+
+				const text_mat = new THREE.MeshStandardMaterial();
+				text_mat.colorNode = TSL.color( '#000000' );
+
+				// Create finish labels (using negative wood index for left column)
+				for ( let y = 0; y < Finishes.length; y ++ ) {
+
+					createLabel( Finishes[ y ], font, text_mat, getGridPosition( - 1, y ) );
+
+				}
+
+				// Create and add the grid plane
+				const plane = createGridPlane();
+				scene.add( plane );
+
+				await new HDRLoader()
+					.setPath( 'textures/equirectangular/' )
+					.loadAsync( 'san_giuseppe_bridge_2k.hdr' ).then( ( texture ) => {
+
+						texture.mapping = THREE.EquirectangularReflectionMapping;
+
+						scene.environment = texture;
+						scene.environmentIntensity = 2;
+
+					} );
+
+				// Create wood labels (using negative finish index for top row)
+				for ( let x = 0; x < WoodGenuses.length; x ++ ) {
+
+					createLabel( WoodGenuses[ x ], font, text_mat, getGridPosition( x, - 1 ) );
+
+				}
+
+				// Create wood blocks
+				for ( let x = 0; x < WoodGenuses.length; x ++ ) {
+
+					for ( let y = 0; y < Finishes.length; y ++ ) {
+
+						const material = WoodNodeMaterial.fromPreset( WoodGenuses[ x ], Finishes[ y ] );
+						const cube = new THREE.Mesh( blockGeometry, material );
+						cube.position.copy( getGridPosition( x, y ) );
+						base.add( cube );
+
+						await new Promise( resolve => setTimeout( resolve, 0 ) );
+
+					}
+
+				}
+
+				 add_custom_wood( text_mat );
+
+			}
+
+			function render() {
+
+				controls.update();
+				stats.update();
+
+				renderer.render( scene, camera );
+			
+			}
+
+			window.addEventListener( 'resize', () => {
+
+				camera.aspect = window.innerWidth / window.innerHeight;
+				camera.updateProjectionMatrix();
+				renderer.setSize( window.innerWidth, window.innerHeight );
+
+			} );
+
+			if ( WebGPU.isAvailable() ) {
+
+				init();
+			
+			} else {
+
+				document.body.appendChild( WebGPU.getErrorMessage() );
+
+			}
+
+
+
+			function add_custom_wood( text_mat ) {
+
+				// Add "Custom" label (positioned at the end of the grid)
+				createLabel( 'custom', font, text_mat, getGridPosition( Math.round( WoodGenuses.length / 2 - 1 ), 5 ) );
+
+				// Create custom wood material with unique parameters
+				const customMaterial = new WoodNodeMaterial( {
+					centerSize: 1.8,
+					largeWarpScale: 0.5,
+					ringCount: 45,
+					ringBias: 0.15,
+					barkThickness: 0.8,
+					splotchScale: 2.5,
+					splotchIntensity: 1.8,
+					cellScale: 1200,
+					cellSize: 0.05,
+					darkGrainColor: new THREE.Color( '#0a0a0a' ),
+					lightGrainColor: new THREE.Color( '#8b4513' ),
+					clearcoat: 1,
+					clearcoatRoughness: 0.2
+				} );
+
+				const cube = new THREE.Mesh( blockGeometry, customMaterial );
+				cube.position.copy( getGridPosition( Math.round( WoodGenuses.length / 2 ), 5 ) );
+
+				base.add( cube );
+
+			}
+
+		</script>
+	</body>
+</html>

+ 1 - 0
test/e2e/puppeteer.js

@@ -137,6 +137,7 @@ const exceptionList = [
 	'webgpu_postprocessing_bloom_emissive',
 	'webgpu_lights_tiled',
 	'webgpu_postprocessing_traa',
+	'webgpu_tsl_wood',
 
 	// Awaiting for WebGPU Backend support in Puppeteer
 	'webgpu_storage_buffer',

粤ICP备19079148号