Sfoglia il codice sorgente

Examples: Improve `webgpu_custom_fog_scattering`. (#33825)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
mrdoob 1 settimana fa
parent
commit
4d45bb6304

+ 377 - 0
examples/jsm/generators/TreeGenerator.js

@@ -0,0 +1,377 @@
+import {
+	BufferAttribute,
+	BufferGeometry,
+	Mesh,
+	Vector3
+} from 'three';
+
+import { MeshStandardNodeMaterial } from 'three/webgpu';
+import { color, float, mx_fractal_noise_float, positionLocal, vec3 } from 'three/tsl';
+
+// the golden angle ( 137.5° ): rolling each sibling branch by this much around the
+// parent axis spreads them like a real stem, so they never line up
+const GOLDEN_ANGLE = Math.PI * ( 3 - Math.sqrt( 5 ) );
+const DEG2RAD = Math.PI / 180;
+const TAU = Math.PI * 2;
+
+const UP = /*@__PURE__*/ new Vector3( 0, 1, 0 );
+const _axis = /*@__PURE__*/ new Vector3();
+
+// reusable scratch for one tube's ring vertices ( grows to the largest tube seen )
+let _ring = new Float32Array( 0 );
+
+/**
+ * Grows a procedural tree skeleton — trunk, branches and twigs, each swept as a tapered
+ * tube — and bakes it into one non-indexed {@link BufferGeometry} (position and normal
+ * only), ready to instance into a forest. It produces *branches only*; add foliage as a
+ * separate layer.
+ *
+ * The branching is deterministic for a given `seed`: a recursive sweep lays down gently
+ * curved tubes with a parallel-transport frame (so they never twist), forking by the
+ * pipe model (each child much thinner than its parent), spreading children along the
+ * upper part of each branch with a golden-angle roll, and pulling them back up toward
+ * the light. A flared root, non-linear taper and gravity droop fill in the character.
+ *
+ * Parameters are set with a fluent builder: a `set<Param>()` exists for every default
+ * ( `setSeed`, `setLevels`, `setChildren`, … ), each returning `this` for chaining.
+ *
+ * Each `build()` returns a fresh, independent mesh that the caller owns, so one
+ * generator can be re-parametrized and built repeatedly to grow a varied stand:
+ *
+ * ```js
+ * const generator = new TreeGenerator( material );
+ * const oak = generator.setSeed( 1 ).setLevels( 4 ).build();
+ * const pine = generator.setSeed( 2 ).setLevels( 5 ).build();
+ * ```
+ */
+class TreeGenerator {
+
+	constructor( material = null ) {
+
+		this.material = material;
+		this.parameters = {}; // overrides; defaults fill the rest at build time
+
+	}
+
+	build() {
+
+		const p = Object.assign( {}, TreeGenerator.defaults, this.parameters );
+		const random = createRandom( p.seed );
+
+		// grow the skeleton into a flat list of tubes, then size and fill the geometry in
+		// one pass — no per-vertex objects, no array growth
+
+		const tubes = [];
+		growBranch( tubes, new Vector3(), UP, p.trunkLength, p.trunkRadius, 0, p, random );
+
+		let vertexCount = 0;
+		for ( const tube of tubes ) vertexCount += ( tube.rings.length - 1 ) * tube.radial * 6;
+
+		const positions = new Float32Array( vertexCount * 3 );
+		const normals = new Float32Array( vertexCount * 3 );
+
+		let offset = 0;
+		for ( const tube of tubes ) offset = emitTube( positions, normals, offset, tube.rings, tube.radial );
+
+		const geometry = new BufferGeometry();
+		geometry.setAttribute( 'position', new BufferAttribute( positions, 3 ) );
+		geometry.setAttribute( 'normal', new BufferAttribute( normals, 3 ) );
+		geometry.computeBoundingSphere();
+
+		const mesh = new Mesh( geometry, this.material || createTreeMaterial() );
+		mesh.name = 'Tree';
+
+		return mesh;
+
+	}
+
+}
+
+TreeGenerator.defaults = {
+	seed: 1,
+	levels: 4, // recursion depth: trunk, branch, twig, sub-twig
+	children: [ 3, 12, 8 ], // sub-branches per level; density comes from many children spread along each parent, not depth
+	branchAngle: [ 38, 50, 58 ], // degrees a child tilts off its parent axis, per level
+	angleVariance: 14, // degrees of random jitter on the branch angle, breaks fractal regularity
+	lengthRatio: 0.62, // child length / parent length
+	trunkLength: 9, // trunk length in world units; sets the tree's height
+	trunkRadius: 0.42, // base radius of the trunk
+	taper: 0.55, // a branch thins to ( 1 - taper ) of its base radius along its own length
+	taperCurve: 0.7, // < 1 keeps the bole full then tapers ( real trees ), 1 = straight cone
+	rootFlare: 0.6, // how much the trunk swells at the very base
+	flareFrac: 0.18, // fraction of the trunk over which the flare acts
+	radiusExponent: 2.3, // pipe model ( da Vinci ): childBase = parentBase × ( 1 / children )^( 1 / radiusExponent )
+	minRadius: 0.05, // hair-thin floor so twigs don't taper to a sliver
+	minLength: 0.6, // branches shorter than this stop recursing
+	droop: 0.05, // gravity sag per branch ( ≈ droop × length ); the trunk stays upright
+	upPull: 0.3, // phototropism: 0 = a bare spread cone ( many branches aim down ), 1 = straight up
+	gnarl: [ 0.05, 0.16, 0.26, 0.32 ], // per-level random wobble on each tube segment
+	radialSegments: 6, // ring vertices around a tube ( drops by one per level for thin twigs )
+	sectionLength: 1.3, // world units per tube segment, so a tall trunk stays smooth
+	childStart: 0.12, // fraction up a sub-branch before children appear
+	trunkClear: 0.25 // fraction of the trunk kept bare before the crown ( raise for a tall clean bole )
+};
+
+// a fluent setter for every default — setSeed(), setLevels(), setChildren(), … — each
+// storing its value and returning `this`, so the API stays in sync with the parameters
+for ( const key of Object.keys( TreeGenerator.defaults ) ) {
+
+	TreeGenerator.prototype[ 'set' + key[ 0 ].toUpperCase() + key.slice( 1 ) ] = function ( value ) {
+
+		this.parameters[ key ] = value;
+		return this;
+
+	};
+
+}
+
+// --- skeleton ------------------------------------------------------------
+
+// Grows one branch as a gently curved, tapered tube and recurses, collecting the tube
+// into `tubes`. The tube is swept with a parallel-transport frame ( rotated by the same
+// rotation that bends the tangent each step ) so it never twists, unlike a naive Frenet
+// frame. Children fork off the upper part of the branch by the pipe model.
+function growBranch( tubes, base, dir, length, baseRadius, level, p, random ) {
+
+	const sections = Math.max( 3, Math.min( 24, Math.round( length / p.sectionLength ) ) ); // ring count tracks length
+	const radial = Math.max( 3, p.radialSegments - level );
+	const step = length / sections;
+	const gnarl = p.gnarl[ Math.min( level, p.gnarl.length - 1 ) ];
+	const start = level === 0 ? p.trunkClear : p.childStart; // the trunk carries a clean bole below its crown
+
+	let tangent = dir.clone().normalize();
+	const normal = perpendicular( tangent );
+
+	const rings = [];
+	const pos = base.clone();
+
+	for ( let s = 0; s <= sections; s ++ ) {
+
+		const t = s / sections;
+
+		// non-linear taper down to ( 1 - taper ) of the base, with a flared root on the trunk
+		let radius = baseRadius * ( ( 1 - p.taper ) + p.taper * Math.pow( 1 - t, p.taperCurve ) );
+		if ( level === 0 && p.rootFlare > 0 ) {
+
+			const flare = Math.max( 0, ( p.flareFrac - t ) / p.flareFrac );
+			radius *= 1 + p.rootFlare * flare * flare * flare; // sharp knee, confined to the base
+
+		}
+
+		rings.push( {
+			pos: pos.clone(),
+			tangent: tangent.clone(),
+			normal: normal.clone(),
+			binormal: new Vector3().crossVectors( tangent, normal ),
+			radius
+		} );
+
+		if ( s < sections ) {
+
+			const next = tangent.clone();
+			next.x += ( random() * 2 - 1 ) * gnarl;
+			next.y += ( random() * 2 - 1 ) * gnarl;
+			next.z += ( random() * 2 - 1 ) * gnarl;
+			if ( level > 0 ) next.y -= p.droop * step; // branches sag; the trunk stays vertical
+			next.normalize();
+
+			transport( tangent, next, normal ); // keep the frame torsion-free
+			pos.addScaledVector( next, step );
+			tangent = next;
+
+		}
+
+	}
+
+	tubes.push( { rings, radial } );
+
+	if ( level >= p.levels - 1 || length < p.minLength ) return;
+
+	// fork: children spread along the upper branch, each tilted off the local tangent,
+	// rolled by the golden angle, and much thinner than the parent ( pipe model )
+
+	const n = p.children[ Math.min( level, p.children.length - 1 ) ];
+	const angle = p.branchAngle[ Math.min( level, p.branchAngle.length - 1 ) ];
+	const pipeDrop = Math.pow( 1 / n, 1 / p.radiusExponent );
+
+	for ( let i = 0; i < n; i ++ ) {
+
+		const t = start + ( i + 0.5 + ( random() - 0.5 ) * 0.6 ) / n * ( 1 - start );
+		const ring = ringAt( rings, t );
+
+		const tilt = ( angle + ( random() * 2 - 1 ) * p.angleVariance ) * DEG2RAD;
+		const roll = i * GOLDEN_ANGLE + ( random() * 2 - 1 ) * 0.4;
+
+		// tilt off a perpendicular axis FIRST, then roll about the parent axis, then pull
+		// back toward the light ( else the roll sends half the children downward )
+		const childDir = ring.tangent.clone()
+			.applyAxisAngle( ring.normal, tilt )
+			.applyAxisAngle( ring.tangent, roll );
+		if ( p.upPull > 0 ) childDir.lerp( UP, p.upPull ).normalize();
+
+		// the pipe-model drop, but never fatter than the wood it leaves nor below the floor
+		const childBase = Math.max( p.minRadius, Math.min( baseRadius * pipeDrop, ring.radius ) );
+
+		growBranch( tubes, ring.pos, childDir, length * p.lengthRatio, childBase, level + 1, p, random );
+
+	}
+
+}
+
+// a unit vector perpendicular to v ( cross with the least-aligned axis )
+function perpendicular( v ) {
+
+	const a = Math.abs( v.x ) < 0.9 ? _axis.set( 1, 0, 0 ) : _axis.set( 0, 1, 0 );
+	return new Vector3().crossVectors( v, a ).normalize();
+
+}
+
+// rotate frame vector n by the rotation that maps tangent t0 onto t1
+function transport( t0, t1, n ) {
+
+	_axis.crossVectors( t0, t1 );
+	const sin = _axis.length();
+	if ( sin < 1e-6 ) return; // already parallel
+	_axis.divideScalar( sin );
+	n.applyAxisAngle( _axis, Math.atan2( sin, t0.dot( t1 ) ) );
+
+}
+
+// sample the branch frame at fraction t ( 0..1 ) for spawning a child
+function ringAt( rings, t ) {
+
+	const f = Math.max( 0, Math.min( 0.999, t ) ) * ( rings.length - 1 );
+	const i = Math.floor( f );
+	const frac = f - i;
+	const a = rings[ i ];
+	const b = rings[ Math.min( i + 1, rings.length - 1 ) ];
+
+	return {
+		pos: a.pos.clone().lerp( b.pos, frac ),
+		tangent: a.tangent.clone().lerp( b.tangent, frac ).normalize(),
+		normal: a.normal.clone().lerp( b.normal, frac ).normalize(),
+		radius: a.radius + ( b.radius - a.radius ) * frac
+	};
+
+}
+
+// --- geometry ------------------------------------------------------------
+
+// Sweeps a tube through the rings: each ring is a loop of `radial` vertices in its
+// ( normal, binormal ) plane, the outward radial direction being the vertex normal.
+// Ring vertices are computed once into a reused scratch, then stitched straight into the
+// preallocated geometry arrays — no per-vertex objects.
+function emitTube( positions, normals, offset, rings, radial ) {
+
+	const stride = ( radial + 1 ) * 6; // one ring loop: ( position, normal ) per vertex
+	const needed = rings.length * stride;
+	if ( _ring.length < needed ) _ring = new Float32Array( needed );
+
+	const ring = _ring;
+
+	for ( let r = 0; r < rings.length; r ++ ) {
+
+		const { pos, normal, binormal, radius } = rings[ r ];
+		let o = r * stride;
+
+		for ( let j = 0; j <= radial; j ++ ) {
+
+			const a = j / radial * TAU;
+			const c = Math.cos( a );
+			const s = Math.sin( a );
+			const nx = c * normal.x + s * binormal.x;
+			const ny = c * normal.y + s * binormal.y;
+			const nz = c * normal.z + s * binormal.z;
+
+			ring[ o ++ ] = pos.x + nx * radius;
+			ring[ o ++ ] = pos.y + ny * radius;
+			ring[ o ++ ] = pos.z + nz * radius;
+			ring[ o ++ ] = nx;
+			ring[ o ++ ] = ny;
+			ring[ o ++ ] = nz;
+
+		}
+
+	}
+
+	// stitch consecutive rings into quads ( two triangles ), wound so normals face out
+
+	for ( let r = 0; r < rings.length - 1; r ++ ) {
+
+		const a = r * stride;
+		const b = ( r + 1 ) * stride;
+
+		for ( let j = 0; j < radial; j ++ ) {
+
+			const aL = a + j * 6, aR = a + ( j + 1 ) * 6;
+			const bL = b + j * 6, bR = b + ( j + 1 ) * 6;
+
+			offset = copyVertex( positions, normals, offset, ring, aL );
+			offset = copyVertex( positions, normals, offset, ring, bR );
+			offset = copyVertex( positions, normals, offset, ring, bL );
+
+			offset = copyVertex( positions, normals, offset, ring, aL );
+			offset = copyVertex( positions, normals, offset, ring, aR );
+			offset = copyVertex( positions, normals, offset, ring, bR );
+
+		}
+
+	}
+
+	return offset;
+
+}
+
+// copies one ( position, normal ) vertex from the ring scratch into the geometry arrays
+function copyVertex( positions, normals, offset, ring, i ) {
+
+	const o = offset * 3;
+	positions[ o ] = ring[ i ]; positions[ o + 1 ] = ring[ i + 1 ]; positions[ o + 2 ] = ring[ i + 2 ];
+	normals[ o ] = ring[ i + 3 ]; normals[ o + 1 ] = ring[ i + 4 ]; normals[ o + 2 ] = ring[ i + 5 ];
+
+	return offset + 1;
+
+}
+
+// --- deterministic PRNG ( mulberry32 ) -----------------------------------
+
+function createRandom( seed ) {
+
+	let s = ( seed >>> 0 ) || 1;
+
+	return function () {
+
+		s = ( s + 0x6D2B79F5 ) | 0;
+		let t = Math.imul( s ^ ( s >>> 15 ), 1 | s );
+		t = ( t + Math.imul( t ^ ( t >>> 7 ), 61 | t ) ) ^ t;
+		return ( ( t ^ ( t >>> 14 ) ) >>> 0 ) / 4294967296;
+
+	};
+
+}
+
+// --- material ------------------------------------------------------------
+
+/**
+ * A simple bark material for a {@link TreeGenerator} mesh: a low-saturation brown with a
+ * faint, vertically-stretched grain, so trunks read near-black against bright fog.
+ *
+ * @param {Object} [parameters] - `barkColor` ( a hex, THREE.Color or TSL node ).
+ * @return {MeshStandardNodeMaterial}
+ */
+function createTreeMaterial( parameters = {} ) {
+
+	const c = parameters.barkColor;
+	const barkColor = c === undefined ? color( 0x4b3a2b ) : ( c.isColor || typeof c === 'number' ? color( c ) : c );
+
+	const material = new MeshStandardNodeMaterial();
+	const grain = mx_fractal_noise_float( positionLocal.mul( vec3( 2.5, 0.4, 2.5 ) ), 3 ).mul( 0.18 );
+	material.colorNode = barkColor.mul( grain.add( 0.9 ) );
+	material.roughnessNode = float( 0.95 );
+	material.metalnessNode = float( 0 );
+
+	return material;
+
+}
+
+export { TreeGenerator, createTreeMaterial };

BIN
examples/screenshots/webgpu_custom_fog_scattering.jpg


+ 90 - 54
examples/webgpu_custom_fog_scattering.html

@@ -38,14 +38,17 @@
 		<script type="module">
 
 			import * as THREE from 'three/webgpu';
-			import { positionWorld, densityFogFactor, pass, vec2, uniform, mix, color } from 'three/tsl';
+			import { densityFogFactor, mix, pass, reference, uniform, vec2 } from 'three/tsl';
 			import { gaussianBlur } from 'three/addons/tsl/display/GaussianBlurNode.js';
 
-			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+			import { FirstPersonControls } from 'three/addons/controls/FirstPersonControls.js';
 			import { Inspector } from 'three/addons/inspector/Inspector.js';
+			import { TreeGenerator } from 'three/addons/generators/TreeGenerator.js';
 
 			let camera, scene, renderer, renderPipeline, controls;
 
+			const timer = new THREE.Timer();
+
 			const params = {
 				scatteringEnabled: true
 			};
@@ -54,69 +57,110 @@
 
 			async function init() {
 
-				camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 1, 600 );
-				camera.position.set( 30, 10, 30 );
+				camera = new THREE.PerspectiveCamera( 55, window.innerWidth / window.innerHeight, 0.1, 120 );
+				camera.position.set( 0.4, 1.7, 9 ); // ~human eye height
 
 				scene = new THREE.Scene();
 
-				// fog
+				// fog: a bright, cool haze. the background matches it so the trunks read
+				// as dark silhouettes that dissolve into depth — the scattering subject
 
-				scene.fog = new THREE.FogExp2( 0xcccccc, 0.014 );
-				scene.background = new THREE.Color( 0xcccccc );
+				scene.fog = new THREE.FogExp2( 0xc6cace, 0.11 );
+				scene.background = new THREE.Color( 0xc6cace );
 
-				// builds
+				// everything is a flat black silhouette — the fog and the scattering blur
+				// do all the shaping, so no lights are needed ( MeshBasicMaterial is unlit )
 
-				const buildWindows = positionWorld.y.mul( 10 ).floor().mod( 4 ).sign().mix( color( 0x000066 ), color( 0xffffff ) );
+				const material = new THREE.MeshBasicMaterial( { color: 0x000000 } );
 
-				const buildGeometry = new THREE.BoxGeometry( 1, 1, 1 );
-				const buildMaterial = new THREE.MeshPhongNodeMaterial( {
-					colorNode: buildWindows
-				} );
+				// a few seeded variants of a tall scots pine: a clean bole rising into a
+				// high, open crown of fine bare branches
+
+				const variants = [];
+
+				const generator = new TreeGenerator( material );
+
+				for ( let v = 0; v < 6; v ++ ) {
+
+					const mesh = generator
+						.setSeed( v + 1 )
+						.setTrunkLength( 2.6 + v * 0.3 ) // the crowns ride high in the fog
+						.setTrunkRadius( 0.06 )
+						.setTaper( 0.28 ) // slender, tapers slowly
+						.setLevels( 4 )
+						.setChildren( [ 7, 5, 4 ] ) // a wide whorl of limbs ramifying into many fine twigs
+						.setBranchAngle( [ 55, 50, 46 ] ) // a rounded crown that turns up
+						.setAngleVariance( 22 )
+						.setLengthRatio( 0.46 ) // short crown limbs ( a modest pine crown )
+						.setMinLength( 0.04 )
+						.setDroop( 0.05 )
+						.setUpPull( 0.42 ) // crown branches reach up toward the light
+						.setGnarl( [ 0, 0.14, 0.24, 0.34 ] ) // dead-straight trunk, wispier twigs
+						.setSectionLength( 0.34 )
+						.setRadialSegments( 7 )
+						.setRadiusExponent( 2.5 ) // slender bole, whippy thin twigs
+						.setMinRadius( 0.0023 ) // very fine twigs
+						.setTrunkClear( 0.72 ) // tall clean bole; the crown sits only at the top
+						.build();
 
-				const buildMesh = new THREE.InstancedMesh( buildGeometry, buildMaterial, 4000 );
-				scene.add( buildMesh );
+					variants.push( mesh.geometry );
+
+				}
+
+				const placements = variants.map( () => [] );
 
 				const dummy = new THREE.Object3D();
-				const center = new THREE.Vector3();
+				const cols = 13, rows = 12, spacing = 1.9;
 
-				for ( let i = 0; i < buildMesh.count; i ++ ) {
+				for ( let i = 0; i < cols; i ++ ) {
 
-					const scaleY = Math.random() * 7 + .5;
+					for ( let j = 0; j < rows; j ++ ) {
 
-					dummy.position.x = Math.random() * 600 - 300;
-					dummy.position.z = Math.random() * 600 - 300;
+						const v = Math.floor( Math.random() * variants.length );
 
-					const distance = Math.max( dummy.position.distanceTo( center ) * .012, 1 );
+						const x = ( i - cols / 2 ) * spacing + ( Math.random() - 0.5 ) * spacing * 0.8;
+						const z = j * spacing - rows * spacing + 4.2 + ( Math.random() - 0.5 ) * spacing * 0.8;
 
-					dummy.position.y = .5 * scaleY * distance;
+						dummy.position.set( x, 0, z );
 
-					dummy.scale.x = dummy.scale.z = Math.random() * 3 + .5;
-					dummy.scale.y = scaleY * distance;
+						const scale = 0.85 + Math.random() * 0.4;
+						dummy.rotation.set( ( Math.random() - 0.5 ) * 0.05, Math.random() * Math.PI * 2, ( Math.random() - 0.5 ) * 0.05 ); // slight lean
+						dummy.scale.set( scale, scale * ( 0.9 + Math.random() * 0.3 ), scale );
+						dummy.updateMatrix();
 
-					dummy.updateMatrix();
+						placements[ v ].push( dummy.matrix.clone() );
 
-					buildMesh.setMatrixAt( i, dummy.matrix );
+					}
 
 				}
 
-				// lights
+				variants.forEach( ( geometry, v ) => {
 
-				scene.add( new THREE.HemisphereLight( 0xf0f5f5, 0xd0dee7, 0.5 ) );
+					const list = placements[ v ];
+					const mesh = new THREE.InstancedMesh( geometry, material, list.length );
+					for ( let k = 0; k < list.length; k ++ ) mesh.setMatrixAt( k, list[ k ] );
+					mesh.instanceMatrix.needsUpdate = true;
+					scene.add( mesh );
 
-				// geometry
+				} );
+
+				// a couple of dominant trunks close to the camera to anchor the depth
+
+				[ [ - 1.1, 4.9, 1.5, 1.1 ], [ 1.5, 4, 1.2, 0.3 ] ].forEach( ( [ x, z, s, ry ] ) => {
+
+					const hero = new THREE.Mesh( variants[ variants.length - 1 ], material );
+					hero.position.set( x, 0, z );
+					hero.rotation.y = ry;
+					hero.scale.setScalar( s );
+					scene.add( hero );
 
-				const planeGeometry = new THREE.PlaneGeometry( 200, 200 );
-				const planeMaterial = new THREE.MeshPhongMaterial( {
-					color: 0x999999
 				} );
 
-				const ground = new THREE.Mesh( planeGeometry, planeMaterial );
-				ground.rotation.x = - Math.PI / 2;
-				ground.scale.multiplyScalar( 3 );
-				ground.castShadow = true;
-				ground.receiveShadow = true;
+				// ground
+
+				const ground = new THREE.Mesh( new THREE.PlaneGeometry( 600, 600 ).rotateX( - Math.PI / 2 ), material );
 				scene.add( ground );
-			
+
 				// renderer
 
 				renderer = new THREE.WebGPURenderer( { antialias: true } );
@@ -128,21 +172,16 @@
 
 				// controls
 
-				controls = new OrbitControls( camera, renderer.domElement );
-				controls.target.set( 0, 2, 0 );
-				controls.minDistance = 7;
-				controls.maxDistance = 100;
-				controls.maxPolarAngle = Math.PI / 2;
-				controls.enableDamping = true;
-				controls.update();
-
-				//
+				controls = new FirstPersonControls( camera, renderer.domElement );
+				controls.movementSpeed = 2;
+				controls.lookSpeed = 0.1;
+				controls.lookAt( - 0.2, 1.7, - 8 );
 
 				renderPipeline = new THREE.RenderPipeline( renderer );
 
 				// uniforms
 
-				const density = uniform( 0.014 );
+				const density = reference( 'density', 'float', scene.fog );
 				const scattering = uniform( 2 );
 
 				// scene pass
@@ -167,11 +206,7 @@
 				// gui
 
 				const gui = renderer.inspector.createParameters( 'Settings' );
-				gui.add( density, 'value', 0.005, 0.03 ).step( 0.0001 ).name( 'fog density' ).onChange( ( value ) => {
-			
-					scene.fog.density = value;
-
-				} );
+				gui.add( scene.fog, 'density', 0.025, 0.16 ).step( 0.0005 ).name( 'fog density' );
 				gui.add( scattering, 'value', 0, 5 ).name( 'scattering factor' );
 				gui.add( params, 'scatteringEnabled' ).name( 'enable scattering' ).onChange( ( value ) => {
 			
@@ -204,7 +239,8 @@
 
 			function animate() {
 
-				controls.update();
+				timer.update();
+				controls.update( timer.getDelta() );
 
 				renderPipeline.render();
 

粤ICP备19079148号