Просмотр исходного кода

Renamed to Volume and added InstancedVolume.

Mr.doob 3 месяцев назад
Родитель
Сommit
f6258efa15

+ 265 - 0
examples/jsm/utils/InstancedVolume.js

@@ -0,0 +1,265 @@
+import { InstancedMesh, BoxGeometry, Data3DTexture, RGBAFormat, FloatType, LinearFilter, Matrix4, Vector3, Vector2, Quaternion, Ray, DoubleSide, Triangle } from 'three';
+import { VolumeStandardMaterial } from './VolumeStandardMaterial.js';
+
+export class InstancedVolume extends InstancedMesh {
+
+	constructor( count, params = {} ) {
+
+		const geometry = new BoxGeometry( 1, 1, 1 );
+		const material = new VolumeStandardMaterial( {
+			roughness: params.roughness !== undefined ? params.roughness : 1.0,
+			metalness: params.metalness !== undefined ? params.metalness : 1.0
+		} );
+
+		super( geometry, material, count );
+
+		this.resolution = params.resolution !== undefined ? params.resolution : 100;
+		this.margin = params.margin !== undefined ? params.margin : 0.05;
+		this.surface = params.surface !== undefined ? params.surface : 0.0;
+
+		this.sdfTexture = null;
+		this.inverseBoundsMatrix = new Matrix4();
+
+	}
+
+	async generate( sourceMesh ) {
+
+		const dim = this.resolution;
+		const geometry = sourceMesh.geometry;
+
+		// Ensure BVH is computed
+		if ( ! geometry.boundsTree ) {
+
+			throw new Error( 'Source mesh geometry must have a BVH. Call geometry.computeBoundsTree() first.' );
+
+		}
+
+		const bvh = geometry.boundsTree;
+
+		const matrix = new Matrix4();
+		const center = new Vector3();
+		const quat = new Quaternion();
+		const scale = new Vector3();
+
+		// Compute the bounding box of the geometry including the margin
+		if ( ! geometry.boundingBox ) geometry.computeBoundingBox();
+
+		geometry.boundingBox.getCenter( center );
+		scale.subVectors( geometry.boundingBox.max, geometry.boundingBox.min );
+		scale.x += 2 * this.margin;
+		scale.y += 2 * this.margin;
+		scale.z += 2 * this.margin;
+		matrix.compose( center, quat, scale );
+		this.inverseBoundsMatrix.copy( matrix ).invert();
+
+		// Dispose of the existing SDF texture
+		if ( this.sdfTexture ) {
+
+			this.sdfTexture.dispose();
+
+		}
+
+		const pxWidth = 1 / dim;
+		const halfWidth = 0.5 * pxWidth;
+
+		console.log( `Generating ${dim}x${dim}x${dim} SDF texture...` );
+
+		// Create a new 3D data texture
+		this.sdfTexture = new Data3DTexture( new Float32Array( dim ** 3 * 4 ), dim, dim, dim );
+		this.sdfTexture.format = RGBAFormat;
+		this.sdfTexture.type = FloatType;
+		this.sdfTexture.minFilter = LinearFilter;
+		this.sdfTexture.magFilter = LinearFilter;
+
+		const point = new Vector3();
+		const target = {
+			point: new Vector3(),
+			distance: 0,
+			faceIndex: - 1
+		};
+		const uvAttr = geometry.attributes.uv;
+
+		// Reusable objects to avoid allocations in the loop
+		const ray = new Ray();
+		const directions = [
+			new Vector3( 1, 0, 0 ),
+			new Vector3( - 1, 0, 0 ),
+			new Vector3( 0, 1, 0 ),
+			new Vector3( 0, - 1, 0 ),
+			new Vector3( 0, 0, 1 ),
+			new Vector3( 0, 0, - 1 )
+		];
+		const v0 = new Vector3();
+		const v1 = new Vector3();
+		const v2 = new Vector3();
+		const barycoord = new Vector3();
+		const uv0 = new Vector2();
+		const uv1 = new Vector2();
+		const uv2 = new Vector2();
+
+		// Iterate over all pixels and check distance
+		for ( let x = 0; x < dim; x ++ ) {
+
+			if ( x % 10 === 0 ) {
+
+				console.log( `Processing slice ${x}/${dim}...` );
+
+			}
+
+			for ( let y = 0; y < dim; y ++ ) {
+
+				for ( let z = 0; z < dim; z ++ ) {
+
+					const index = ( x + dim * ( y + dim * z ) ) * 4;
+
+					// Adjust by half width of the pixel so we sample the pixel center
+					// and offset by half the box size
+					point.set(
+						halfWidth + x * pxWidth - 0.5,
+						halfWidth + y * pxWidth - 0.5,
+						halfWidth + z * pxWidth - 0.5,
+					).applyMatrix4( matrix );
+
+					// Get the distance to the geometry
+					bvh.closestPointToPoint( point, target );
+					const dist = target.distance;
+
+					// Check if the point is inside or outside by raycasting
+					// Skip expensive raycasts for points far from surface (definitely outside)
+					let isInside = false;
+
+					if ( dist < this.margin ) {
+
+						// If we hit a back face then we're inside
+						let insideCount = 0;
+						ray.origin.copy( point );
+
+						for ( let i = 0; i < 6; i ++ ) {
+
+							ray.direction.copy( directions[ i ] );
+							const hit = bvh.raycastFirst( ray, DoubleSide );
+							if ( hit && hit.face.normal.dot( ray.direction ) > 0.0 ) {
+
+								insideCount ++;
+
+							}
+
+						}
+
+						isInside = insideCount > 3;
+
+					}
+
+					// Set the distance in the texture data
+					this.sdfTexture.image.data[ index + 0 ] = isInside ? - dist : dist;
+
+					// Get UV from closest point
+					let u = 0, v = 0;
+
+					if ( uvAttr && target.faceIndex !== undefined ) {
+
+						const faceIndex = target.faceIndex;
+						const indexAttr = geometry.index;
+						const i0 = indexAttr.getX( faceIndex * 3 + 0 );
+						const i1 = indexAttr.getX( faceIndex * 3 + 1 );
+						const i2 = indexAttr.getX( faceIndex * 3 + 2 );
+
+						v0.fromBufferAttribute( geometry.attributes.position, i0 );
+						v1.fromBufferAttribute( geometry.attributes.position, i1 );
+						v2.fromBufferAttribute( geometry.attributes.position, i2 );
+
+						Triangle.getBarycoord( target.point, v0, v1, v2, barycoord );
+
+						uv0.fromBufferAttribute( uvAttr, i0 );
+						uv1.fromBufferAttribute( uvAttr, i1 );
+						uv2.fromBufferAttribute( uvAttr, i2 );
+
+						u = uv0.x * barycoord.x + uv1.x * barycoord.y + uv2.x * barycoord.z;
+						v = uv0.y * barycoord.x + uv1.y * barycoord.y + uv2.y * barycoord.z;
+
+					}
+
+					// Store UV in G and B channels
+					this.sdfTexture.image.data[ index + 1 ] = u;
+					this.sdfTexture.image.data[ index + 2 ] = v;
+					this.sdfTexture.image.data[ index + 3 ] = 0; // Alpha unused
+
+				}
+
+			}
+
+		}
+
+		this.sdfTexture.needsUpdate = true;
+
+		console.log( 'SDF generation completed' );
+
+		// Copy textures from source mesh material if available
+		if ( sourceMesh.material ) {
+
+			const mat = sourceMesh.material;
+			if ( mat.map ) this.material.map = mat.map;
+			if ( mat.normalMap ) this.material.normalMap = mat.normalMap;
+			if ( mat.metalnessMap ) this.material.metalnessMap = mat.metalnessMap;
+			if ( mat.roughnessMap ) this.material.roughnessMap = mat.roughnessMap;
+			if ( mat.aoMap ) this.material.aoMap = mat.aoMap;
+			if ( mat.envMap ) this.material.envMap = mat.envMap;
+			this.material.needsUpdate = true;
+
+		}
+
+		// Set the mesh's scale to match SDF bounds
+		const sdfBoundsMatrix = this.inverseBoundsMatrix.clone().invert();
+		const boundsCenter = new Vector3();
+		const boundsQuat = new Quaternion();
+		const boundsScale = new Vector3();
+		sdfBoundsMatrix.decompose( boundsCenter, boundsQuat, boundsScale );
+
+		// For instanced mesh, we set the base scale
+		// Individual instances can be positioned using setMatrixAt
+		this.scale.copy( boundsScale );
+		this.position.copy( boundsCenter );
+		this.updateMatrix();
+
+	}
+
+	onBeforeRender( renderer, scene, camera ) {
+
+		if ( ! this.sdfTexture ) return;
+
+		// Update matrices
+		camera.updateMatrixWorld();
+		this.updateMatrixWorld();
+
+		const depth = 1 / this.resolution;
+
+		// Update custom uniforms
+		this.material.uniforms.sdfTex.value = this.sdfTexture;
+		this.material.uniforms.normalStep.value.set( depth, depth, depth );
+		this.material.uniforms.surface.value = this.surface;
+
+		// Automatically use scene.environment if available
+		if ( scene.environment && ! this.material.envMap ) {
+
+			this.material.envMap = scene.environment;
+			this.material.needsUpdate = true;
+
+		}
+
+	}
+
+	dispose() {
+
+		if ( this.sdfTexture ) {
+
+			this.sdfTexture.dispose();
+			this.sdfTexture = null;
+
+		}
+
+		this.geometry.dispose();
+		this.material.dispose();
+
+	}
+
+}

+ 1 - 1
examples/jsm/utils/VolumeMesh.js → examples/jsm/utils/Volume.js

@@ -1,7 +1,7 @@
 import { Mesh, BoxGeometry, Data3DTexture, RGBAFormat, FloatType, LinearFilter, Matrix4, Vector3, Vector2, Quaternion, Ray, DoubleSide, Triangle } from 'three';
 import { VolumeStandardMaterial } from './VolumeStandardMaterial.js';
 
-export class VolumeMesh extends Mesh {
+export class Volume extends Mesh {
 
 	constructor( params = {} ) {
 

+ 16 - 7
examples/jsm/utils/VolumeStandardMaterial.js

@@ -35,14 +35,21 @@ export class VolumeStandardMaterial extends MeshStandardMaterial {
 				'#include <common>',
 				`#include <common>
 				varying vec3 vLocalPosition;
-				varying vec3 vLocalRayOrigin;`
+				varying vec3 vLocalRayOrigin;
+				varying mat4 vInstanceMatrix;`
 			);
 
 			shader.vertexShader = shader.vertexShader.replace(
 				'#include <worldpos_vertex>',
 				`#include <worldpos_vertex>
-				// Transform camera position to local space
-				vLocalRayOrigin = ( inverse( modelMatrix ) * vec4( cameraPosition, 1.0 ) ).xyz;
+				// Get the instance matrix (identity for non-instanced meshes)
+				#ifdef USE_INSTANCING
+					vInstanceMatrix = instanceMatrix;
+				#else
+					vInstanceMatrix = mat4( 1.0 );
+				#endif
+				// Transform camera position to local space (accounting for instance transform)
+				vLocalRayOrigin = ( inverse( modelMatrix * vInstanceMatrix ) * vec4( cameraPosition, 1.0 ) ).xyz;
 				// Vertex position is already in local space
 				vLocalPosition = position;`
 			);
@@ -61,6 +68,7 @@ export class VolumeStandardMaterial extends MeshStandardMaterial {
 
 				varying vec3 vLocalPosition;
 				varying vec3 vLocalRayOrigin;
+				varying mat4 vInstanceMatrix;
 
 				vec2 rayBoxDist( vec3 boundsMin, vec3 boundsMax, vec3 rayOrigin, vec3 rayDir ) {
 					vec3 t0 = ( boundsMin - rayOrigin ) / rayDir;
@@ -126,8 +134,8 @@ export class VolumeStandardMaterial extends MeshStandardMaterial {
 					discard;
 				}
 
-				// Write correct depth for the raymarched surface
-				vec4 viewPos = modelViewMatrix * vec4( localPoint, 1.0 );
+				// Write correct depth for the raymarched surface (accounting for instance transform)
+				vec4 viewPos = modelViewMatrix * vInstanceMatrix * vec4( localPoint, 1.0 );
 				vec4 clipPos = projectionMatrix * viewPos;
 				float ndcDepth = clipPos.z / clipPos.w;
 				gl_FragDepth = ndcDepth * 0.5 + 0.5;
@@ -143,8 +151,9 @@ export class VolumeStandardMaterial extends MeshStandardMaterial {
 				float dz = texture( sdfTex, sdfUV + vec3( 0.0, 0.0, normalStep.z ) ).r - texture( sdfTex, sdfUV - vec3( 0.0, 0.0, normalStep.z ) ).r;
 				vec3 sdfNormalLocal = normalize( vec3( dx, dy, dz ) );
 
-				// Transform normal from SDF local space to view space
-				vec3 sdfNormal = normalize( normalMatrix * sdfNormalLocal );
+				// Transform normal from SDF local space to view space (accounting for instance transform)
+				mat3 instanceNormalMatrix = mat3( transpose( inverse( vInstanceMatrix ) ) );
+				vec3 sdfNormal = normalize( normalMatrix * instanceNormalMatrix * sdfNormalLocal );
 				`
 			);
 

+ 147 - 47
examples/webgl_volume_mesh.html

@@ -1,7 +1,7 @@
 <!DOCTYPE html>
 <html lang="en">
 	<head>
-		<title>three.js webgl - VolumeMesh</title>
+		<title>three.js webgl - MeshVolume</title>
 		<meta charset="utf-8">
 		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
 		<link type="text/css" rel="stylesheet" href="main.css">
@@ -9,7 +9,7 @@
 	<body>
 
 		<div id="info">
-			<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> webgl - VolumeMesh<br/>
+			<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> webgl - MeshVolume<br/>
 			Generation time: <span id="output">-</span>
 		</div>
 
@@ -33,7 +33,8 @@
 			import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
 			import * as BufferGeometryUtils from 'three/addons/utils/BufferGeometryUtils.js';
 			import { computeBoundsTree, disposeBoundsTree, acceleratedRaycast } from 'three-mesh-bvh';
-			import { VolumeMesh } from 'three/addons/utils/VolumeMesh.js';
+			import { Volume } from 'three/addons/utils/Volume.js';
+			import { InstancedVolume } from 'three/addons/utils/InstancedVolume.js';
 			import { RenderSDFLayerMaterial } from 'three/addons/utils/RenderSDFLayerMaterial.js';
 
 
@@ -43,7 +44,7 @@
 			THREE.Mesh.prototype.raycast = acceleratedRaycast;
 
 			const params = {
-				resolution: 100,
+				resolution: 64,
 				margin: 0.05,
 				surface: 0.0,
 				regenerate: () => regenerateVolume(),
@@ -56,7 +57,9 @@
 			let outputContainer;
 			let sourceMesh, sourceMaterial;
 			let volumeMeshes = [];
+			let instancedVolumeMesh;
 			let layerPass;
+			let pointLight, blueLight;
 
 			init();
 
@@ -69,7 +72,7 @@
 				renderer.setPixelRatio( window.devicePixelRatio );
 				renderer.setSize( window.innerWidth, window.innerHeight );
 				renderer.setAnimationLoop( render );
-				renderer.toneMapping = THREE.NeutralToneMapping;
+				renderer.toneMapping = THREE.ACESFilmicToneMapping;
 				document.body.appendChild( renderer.domElement );
 
 				// scene setup
@@ -80,15 +83,75 @@
 				const pmremGenerator = new THREE.PMREMGenerator( renderer );
 				const environment = new RoomEnvironment();
 				const envMapRT = pmremGenerator.fromScene( environment );
-				scene.environment = envMapRT.texture;
+				// scene.environment = envMapRT.texture;
+				scene.environmentIntensity = 0.2;
 				environment.dispose();
 				pmremGenerator.dispose();
 
+				// Helper function to create radial gradient texture (grayscale)
+				function createRadialGradientTexture() {
+
+					const canvas = document.createElement( 'canvas' );
+					canvas.width = 128;
+					canvas.height = 128;
+					const context = canvas.getContext( '2d' );
+					const gradient = context.createRadialGradient( 64, 64, 0, 64, 64, 64 );
+
+					// HDR-like center with exponential falloff for realistic glow
+					gradient.addColorStop( 0, 'rgba(255, 255, 255, 1.0)' );
+					gradient.addColorStop( 0.15, 'rgba(255, 255, 255, 0.8)' );
+					gradient.addColorStop( 0.35, 'rgba(255, 255, 255, 0.4)' );
+					gradient.addColorStop( 0.6, 'rgba(128, 128, 128, 0.15)' );
+					gradient.addColorStop( 1, 'rgba(0, 0, 0, 0)' );
+
+					context.fillStyle = gradient;
+					context.fillRect( 0, 0, 128, 128 );
+					return new THREE.CanvasTexture( canvas );
+
+				}
+
+				const gradientTexture = createRadialGradientTexture();
+
+				// Add point light
+				pointLight = new THREE.PointLight( 0xffffffbb, 20, 20 );
+				pointLight.position.set( 2, 2, 2 );
+				const whiteSprite = new THREE.Sprite(
+					new THREE.SpriteMaterial( {
+						map: gradientTexture,
+						color: pointLight.color,
+						blending: THREE.AdditiveBlending,
+						depthWrite: false
+					} )
+				);
+				whiteSprite.scale.setScalar( 0.25 );
+				pointLight.add( whiteSprite );
+				scene.add( pointLight );
+
+				// Add blue point light
+				blueLight = new THREE.PointLight( 0x00ffcc, 20, 20 );
+				blueLight.position.set( - 2, 2, - 2 );
+				const blueSprite = new THREE.Sprite(
+					new THREE.SpriteMaterial( {
+						map: gradientTexture,
+						color: blueLight.color,
+						blending: THREE.AdditiveBlending,
+						depthWrite: false
+					} )
+				);
+				blueSprite.scale.setScalar( 0.25 );
+				blueLight.add( blueSprite );
+				scene.add( blueLight );
+
+				const dirLight = new THREE.DirectionalLight( 0xffffff, 1.0 );
+				dirLight.position.set( 5, 10, 7.5 );
+				scene.add( dirLight );
+
 				// camera setup
 				camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 50 );
 				camera.position.set( 1, 1, 2 );
 				camera.far = 100;
 				camera.updateProjectionMatrix();
+				scene.add( camera );
 
 				new OrbitControls( camera, renderer.domElement );
 
@@ -147,6 +210,7 @@
 				gui.add( params, 'surface', - 0.2, 0.5 ).name( 'Surface' ).onChange( () => {
 
 					volumeMeshes.forEach( v => v.surface = params.surface );
+					if ( instancedVolumeMesh ) instancedVolumeMesh.surface = params.surface;
 
 				} );
 				gui.add( params, 'regenerate' ).name( 'Regenerate' );
@@ -185,8 +249,17 @@
 				} );
 				volumeMeshes = [];
 
-				// Create new VolumeMesh - this is all you need!
-				const volume = new VolumeMesh( {
+				// Remove instanced mesh if it exists
+				if ( instancedVolumeMesh ) {
+
+					scene.remove( instancedVolumeMesh );
+					instancedVolumeMesh.dispose();
+					instancedVolumeMesh = null;
+
+				}
+
+				// Create new Volume - this is all you need!
+				const volume = new Volume( {
 					resolution: params.resolution,
 					margin: params.margin,
 					surface: params.surface,
@@ -218,67 +291,82 @@
 
 				if ( volumeMeshes.length === 0 || ! sourceMesh ) return;
 
-				// Position the first one
-				volumeMeshes[ 0 ].position.x = 0;
+				// Remove the single volume mesh
+				scene.remove( volumeMeshes[ 0 ] );
+
+				// Create instanced volume mesh
+				const count = 1000;
+				instancedVolumeMesh = new InstancedVolume( count, {
+					resolution: params.resolution,
+					margin: params.margin,
+					surface: params.surface,
+					roughness: 1.0,
+					metalness: 1.0
+				} );
+
+				// Reuse the SDF texture from the first volume
+				instancedVolumeMesh.sdfTexture = volumeMeshes[ 0 ].sdfTexture;
+				instancedVolumeMesh.inverseBoundsMatrix.copy( volumeMeshes[ 0 ].inverseBoundsMatrix );
 
-				// Create 2 more volumes
-				for ( let i = 1; i < 100; i ++ ) {
+				// Copy material properties
+				if ( sourceMesh.material ) {
 
-					const volume = new VolumeMesh( {
-						resolution: params.resolution,
-						margin: params.margin,
-						surface: params.surface,
-						roughness: 1.0,
-						metalness: 1.0
-					} );
+					const mat = sourceMesh.material;
+					if ( mat.map ) instancedVolumeMesh.material.map = mat.map;
+					if ( mat.normalMap ) instancedVolumeMesh.material.normalMap = mat.normalMap;
+					if ( mat.metalnessMap ) instancedVolumeMesh.material.metalnessMap = mat.metalnessMap;
+					if ( mat.roughnessMap ) instancedVolumeMesh.material.roughnessMap = mat.roughnessMap;
+					if ( mat.aoMap ) instancedVolumeMesh.material.aoMap = mat.aoMap;
+					instancedVolumeMesh.material.needsUpdate = true;
 
-					// Reuse the SDF texture from the first volume
-					volume.sdfTexture = volumeMeshes[ 0 ].sdfTexture;
-					volume.inverseBoundsMatrix.copy( volumeMeshes[ 0 ].inverseBoundsMatrix );
+				}
 
-					// Copy material properties
-					if ( sourceMesh.material ) {
+				// Set up instance matrices
+				const transform = new THREE.Object3D();
 
-						const mat = sourceMesh.material;
-						if ( mat.map ) volume.material.map = mat.map;
-						if ( mat.normalMap ) volume.material.normalMap = mat.normalMap;
-						if ( mat.metalnessMap ) volume.material.metalnessMap = mat.metalnessMap;
-						if ( mat.roughnessMap ) volume.material.roughnessMap = mat.roughnessMap;
-						if ( mat.aoMap ) volume.material.aoMap = mat.aoMap;
-						volume.material.needsUpdate = true;
+				for ( let i = 0; i < count; i ++ ) {
 
-					}
+					transform.position.set(
+						( Math.random() - 0.5 ) * 18,
+						( Math.random() - 0.5 ) * 18,
+						( Math.random() - 0.5 ) * 18
+					);
 
-					volume.position.set(
-						( Math.random() - 0.5 ) * 8,
-						( Math.random() - 0.5 ) * 8,
-						( Math.random() - 0.5 ) * 8
+					transform.rotation.set(
+						Math.random() * Math.PI,
+						Math.random() * Math.PI,
+						Math.random() * Math.PI
 					);
-					volume.rotation.set( Math.random() * Math.PI, Math.random() * Math.PI, Math.random() * Math.PI );
 
-					scene.add( volume );
-					volumeMeshes.push( volume );
+					transform.updateMatrix();
+					instancedVolumeMesh.setMatrixAt( i, transform.matrix );
 
 				}
 
+				instancedVolumeMesh.instanceMatrix.needsUpdate = true;
+
+				scene.add( instancedVolumeMesh );
+
 			}
 
 			function removeExtraVolumes() {
 
-				// Keep only the first volume
-				while ( volumeMeshes.length > 1 ) {
+				// Remove instanced mesh if it exists
+				if ( instancedVolumeMesh ) {
 
-					const v = volumeMeshes.pop();
-					scene.remove( v );
-					// Don't dispose the shared texture, only dispose materials
-					v.geometry.dispose();
-					v.material.dispose();
+					scene.remove( instancedVolumeMesh );
+					// Don't dispose the shared texture
+					instancedVolumeMesh.geometry.dispose();
+					instancedVolumeMesh.material.dispose();
+					instancedVolumeMesh = null;
 
 				}
 
-				// Reset position of the first one
+				// Add back the single volume mesh
 				if ( volumeMeshes.length > 0 ) {
 
+					scene.add( volumeMeshes[ 0 ] );
+
 					const sdfBoundsMatrix = volumeMeshes[ 0 ].inverseBoundsMatrix.clone().invert();
 					const boundsCenter = new THREE.Vector3();
 					const boundsQuat = new THREE.Quaternion();
@@ -302,6 +390,18 @@
 
 			function render() {
 
+				// Animate point light in a circle
+				const time = Date.now() * 0.001;
+				const radius = 2;
+				pointLight.position.x = Math.cos( time ) * radius;
+				pointLight.position.z = Math.sin( time ) * radius;
+				pointLight.position.y = Math.sin( time * 0.5 ) * radius;
+
+				// Animate blue light in a different pattern
+				blueLight.position.x = Math.sin( time * 1.3 ) * radius;
+				blueLight.position.z = Math.cos( time * 1.3 ) * radius;
+				blueLight.position.y = Math.cos( time * 0.7 ) * radius;
+
 				renderer.render( scene, camera );
 
 				if ( params.showLayers && volumeMeshes.length > 0 ) {

粤ICP备19079148号