瀏覽代碼

WebGPURenderer: support using 3d textures in shaders with texture3d() and add VolumeNodeMaterial() and examples. (#28418)

* support 3d textures

* remove unused import

* whitespace fixes

* export texture3D

* use new tsl

* replacing to updateValue()

---------

Co-authored-by: aardgoose <[email protected]>
aardgoose 1 年之前
父節點
當前提交
b38ef166c7

+ 3 - 1
examples/files.json

@@ -384,7 +384,9 @@
 		"webgpu_storage_buffer",
 		"webgpu_mesh_batch",
 		"webgpu_instancing_morph",
-		"webgpu_texturegrad"
+		"webgpu_texturegrad",
+		"webgpu_volume_cloud",
+		"webgpu_volume_perlin"
 	],
 	"webaudio": [
 		"webaudio_orientation",

+ 1 - 0
examples/jsm/nodes/Nodes.js

@@ -104,6 +104,7 @@ export { default as StorageBufferNode, storage, storageObject } from './accessor
 export * from './accessors/TangentNode.js';
 export { default as TextureNode, texture, textureLoad, /*textureLevel,*/ sampler } from './accessors/TextureNode.js';
 export { default as TextureStoreNode, textureStore } from './accessors/TextureStoreNode.js';
+export { default as Texture3DNode } from './accessors/Texture3DNode.js';
 export { default as UVNode, uv } from './accessors/UVNode.js';
 export { default as UserDataNode, userData } from './accessors/UserDataNode.js';
 

+ 6 - 0
examples/jsm/nodes/accessors/ReferenceNode.js

@@ -90,6 +90,12 @@ class ReferenceNode extends Node {
 
 	getNodeType( builder ) {
 
+		if ( this.node === null ) {
+
+			this.updateValue();
+
+		}
+
 		return this.node.getNodeType( builder );
 
 	}

+ 100 - 0
examples/jsm/nodes/accessors/Texture3DNode.js

@@ -0,0 +1,100 @@
+import TextureNode from './TextureNode.js';
+import { addNodeClass } from '../core/Node.js';
+import { nodeProxy, vec3, tslFn, If } from '../shadernode/ShaderNode.js';
+
+const normal = tslFn( ( { texture, uv } ) => {
+
+	const epsilon = 0.0001;
+
+	const ret = vec3().temp();
+
+	If( uv.x.lessThan( epsilon ), () => {
+
+		ret.assign( vec3( 1, 0, 0 ) );
+
+	} ).elseif( uv.y.lessThan( epsilon ), () => {
+
+		ret.assign( vec3( 0, 1, 0 ) );
+
+	} ).elseif( uv.z.lessThan( epsilon ), () => {
+
+		ret.assign( vec3( 0, 0, 1 ) );
+
+	} ).elseif( uv.x.greaterThan( 1 - epsilon ), () => {
+
+		ret.assign( vec3( - 1, 0, 0 ) );
+
+	} ).elseif( uv.y.greaterThan( 1 - epsilon ), () => {
+
+		ret.assign( vec3( 0, - 1, 0 ) );
+
+	} ).elseif( uv.z.greaterThan( 1 - epsilon ), () => {
+
+		ret.assign( vec3( 0, 0, - 1 ) );
+
+	} ).else( () => {
+
+		const step = 0.01;
+
+		const x = texture.uv( uv.add( vec3( - step, 0.0, 0.0 ) ) ).r.sub( texture.uv( uv.add( vec3( step, 0.0, 0.0 ) ) ).r );
+		const y = texture.uv( uv.add( vec3( 0.0, - step, 0.0 ) ) ).r.sub( texture.uv( uv.add( vec3( 0.0, step, 0.0 ) ) ).r );
+		const z = texture.uv( uv.add( vec3( 0.0, 0.0, - step ) ) ).r.sub( texture.uv( uv.add( vec3( 0.0, 0.0, step ) ) ).r );
+
+		ret.assign( vec3( x, y, z ) );
+
+	} );
+
+	return ret.normalize();
+
+} );
+
+
+class Texture3DNode extends TextureNode {
+
+	constructor( value, uvNode = null, levelNode = null ) {
+
+		super( value, uvNode, levelNode );
+
+		this.isTexture3DNode = true;
+
+	}
+
+	getInputType( /*builder*/ ) {
+
+		return 'texture3D';
+
+	}
+
+	getDefaultUV() {
+
+		return vec3( 0.5, 0.5, 0.5 );
+
+	}
+
+	setUpdateMatrix( /*updateMatrix*/ ) { } // Ignore .updateMatrix for 3d TextureNode
+
+	setupUV( builder, uvNode ) {
+
+		return uvNode;
+
+	}
+
+	generateUV( builder, uvNode ) {
+
+		return uvNode.build( builder, 'vec3' );
+
+	}
+
+	normal( uvNode ) {
+
+		return normal( { texture: this, uv: uvNode } );
+
+	}
+
+}
+
+export default Texture3DNode;
+
+export const texture3D = nodeProxy( Texture3DNode );
+
+addNodeClass( 'Texture3DNode', Texture3DNode );

+ 2 - 2
examples/jsm/nodes/core/NodeBuilder.js

@@ -473,7 +473,7 @@ class NodeBuilder {
 
 	isReference( type ) {
 
-		return type === 'void' || type === 'property' || type === 'sampler' || type === 'texture' || type === 'cubeTexture' || type === 'storageTexture';
+		return type === 'void' || type === 'property' || type === 'sampler' || type === 'texture' || type === 'cubeTexture' || type === 'storageTexture' || type === 'texture3D';
 
 	}
 
@@ -529,7 +529,7 @@ class NodeBuilder {
 	getVectorType( type ) {
 
 		if ( type === 'color' ) return 'vec3';
-		if ( type === 'texture' || type === 'cubeTexture' || type === 'storageTexture' ) return 'vec4';
+		if ( type === 'texture' || type === 'cubeTexture' || type === 'storageTexture' || type === 'texture3D' ) return 'vec4';
 
 		return type;
 

+ 1 - 0
examples/jsm/nodes/materials/Materials.js

@@ -17,3 +17,4 @@ export { default as MeshMatcapNodeMaterial } from './MeshMatcapNodeMaterial.js';
 export { default as PointsNodeMaterial } from './PointsNodeMaterial.js';
 export { default as SpriteNodeMaterial } from './SpriteNodeMaterial.js';
 export { default as ShadowNodeMaterial } from './ShadowNodeMaterial.js';
+export { default as VolumeNodeMaterial } from './VolumeNodeMaterial.js';

+ 106 - 0
examples/jsm/nodes/materials/VolumeNodeMaterial.js

@@ -0,0 +1,106 @@
+import NodeMaterial, { addNodeMaterial } from './NodeMaterial.js';
+import { varying } from '../core/VaryingNode.js';
+import { property } from '../core/PropertyNode.js';
+import { materialReference } from '../accessors/MaterialReferenceNode.js';
+import { modelWorldMatrixInverse } from '../accessors/ModelNode.js';
+import { cameraPosition } from '../accessors/CameraNode.js';
+import { positionGeometry } from '../accessors/PositionNode.js';
+import { tslFn, vec2, vec3, vec4 } from '../shadernode/ShaderNode.js';
+import { min, max } from '../math/MathNode.js';
+import { loop, Break } from '../utils/LoopNode.js';
+import { texture3D } from '../accessors/Texture3DNode.js';
+
+class VolumeNodeMaterial extends NodeMaterial {
+
+	constructor( params = {} ) {
+
+		super();
+
+		this.normals = false;
+		this.lights = false;
+		this.isVolumeNodeMaterial = true;
+		this.testNode = null;
+
+		this.setValues( params );
+
+	}
+
+	setup( builder ) {
+
+		const map = texture3D( this.map, null, 0 );
+
+		const hitBox = tslFn( ( { orig, dir } ) => {
+
+			const box_min = vec3( - 0.5 );
+			const box_max = vec3( 0.5 );
+
+			const inv_dir = dir.reciprocal();
+
+			const tmin_tmp = box_min.sub( orig ).mul( inv_dir );
+			const tmax_tmp = box_max.sub( orig ).mul( inv_dir );
+
+			const tmin = min( tmin_tmp, tmax_tmp );
+			const tmax = max( tmin_tmp, tmax_tmp );
+
+			const t0 = max( tmin.x, max( tmin.y, tmin.z ) );
+			const t1 = min( tmax.x, min( tmax.y, tmax.z ) );
+
+			return vec2( t0, t1 );
+
+		} );
+
+		this.fragmentNode = tslFn( () => {
+
+			const vOrigin = varying( vec3( modelWorldMatrixInverse.mul( vec4( cameraPosition, 1.0 ) ) ) );
+			const vDirection = varying( positionGeometry.sub( vOrigin ) );
+
+			const rayDir = vDirection.normalize();
+			const bounds = property( 'vec2', 'bounds' ).assign( hitBox( { orig: vOrigin, dir: rayDir } ) );
+
+			bounds.x.greaterThan( bounds.y ).discard();
+
+			bounds.assign( vec2( max( bounds.x, 0.0 ), bounds.y ) );
+
+			const p = property( 'vec3', 'p' ).assign( vOrigin.add( bounds.x.mul( rayDir ) ) );
+			const inc = property( 'vec3', 'inc' ).assign( vec3( rayDir.abs().reciprocal() ) );
+			const delta = property( 'float', 'delta' ).assign( min( inc.x, min( inc.y, inc.z ) ) );
+
+			delta.divAssign( materialReference( 'steps', 'float' ) );
+
+			const ac = property( 'vec4', 'ac' ).assign( vec4( materialReference( 'base', 'color' ), 0.0 ) );
+
+			loop( { type: 'float', start: bounds.x, end: bounds.y, update: '+= delta' }, () => {
+
+				const d = property( 'float', 'd' ).assign( map.uv( p.add( 0.5 ) ).r );
+
+				if ( this.testNode !== null ) {
+
+					this.testNode( { map: map, mapValue: d, probe: p, finalColor: ac } ).append();
+
+				} else {
+
+					// default to show surface of mesh
+					ac.a.assign( 1 );
+					Break();
+
+				}
+
+				p.addAssign( rayDir.mul( delta ) );
+
+			} );
+
+			ac.a.equal( 0 ).discard();
+
+			return vec4( ac );
+
+		} )();
+
+		super.setup( builder );
+
+	}
+
+}
+
+export default VolumeNodeMaterial;
+
+addNodeMaterial( 'VolumeNodeMaterial', VolumeNodeMaterial );

+ 13 - 1
examples/jsm/renderers/common/nodes/NodeSampledTexture.js

@@ -46,4 +46,16 @@ class NodeSampledCubeTexture extends NodeSampledTexture {
 
 }
 
-export { NodeSampledTexture, NodeSampledCubeTexture };
+class NodeSampledTexture3D extends NodeSampledTexture {
+
+	constructor( name, textureNode ) {
+
+		super( name, textureNode );
+
+		this.isSampledTexture3D = true;
+
+	}
+
+}
+
+export { NodeSampledTexture, NodeSampledCubeTexture, NodeSampledTexture3D };

+ 11 - 1
examples/jsm/renderers/webgl/nodes/GLSLNodeBuilder.js

@@ -3,7 +3,7 @@ import { MathNode, GLSLNodeParser, NodeBuilder, UniformNode, vectorComponents }
 import NodeUniformBuffer from '../../common/nodes/NodeUniformBuffer.js';
 import NodeUniformsGroup from '../../common/nodes/NodeUniformsGroup.js';
 
-import { NodeSampledTexture, NodeSampledCubeTexture } from '../../common/nodes/NodeSampledTexture.js';
+import { NodeSampledTexture, NodeSampledCubeTexture, NodeSampledTexture3D } from '../../common/nodes/NodeSampledTexture.js';
 
 import { RedFormat, RGFormat, IntType, DataTexture, RGBFormat, RGBAFormat, FloatType } from 'three';
 
@@ -27,6 +27,7 @@ const supports = {
 const defaultPrecisions = `
 precision highp float;
 precision highp int;
+precision highp sampler3D;
 precision mediump sampler2DArray;
 precision lowp sampler2DShadow;
 `;
@@ -322,6 +323,10 @@ ${ flowData.code }
 
 				snippet = `samplerCube ${ uniform.name };`;
 
+			} else if ( uniform.type === 'texture3D' ) {
+
+				snippet = `sampler3D ${ uniform.name };`;
+
 			} else if ( uniform.type === 'buffer' ) {
 
 				const bufferNode = uniform.node;
@@ -759,6 +764,11 @@ void main() {
 
 				this.bindings[ shaderStage ].push( uniformGPU );
 
+			} else if ( type === 'texture3D' ) {
+
+				uniformGPU = new NodeSampledTexture3D( uniformNode.name, uniformNode.node );
+				this.bindings[ shaderStage ].push( uniformGPU );
+
 			} else if ( type === 'buffer' ) {
 
 				node.name = `NodeBuffer_${ node.id }`;

+ 14 - 0
examples/jsm/renderers/webgl/utils/WebGLTextureUtils.js

@@ -83,6 +83,10 @@ class WebGLTextureUtils {
 
 			glTextureType = gl.TEXTURE_2D_ARRAY;
 
+		} else if ( texture.isData3DTexture === true ) {
+
+			glTextureType = gl.TEXTURE_3D;
+
 		} else {
 
 			glTextureType = gl.TEXTURE_2D;
@@ -286,6 +290,10 @@ class WebGLTextureUtils {
 
 			gl.texStorage3D( gl.TEXTURE_2D_ARRAY, levels, glInternalFormat, width, height, depth );
 
+		} else if ( texture.isData3DTexture ) {
+
+			gl.texStorage3D( gl.TEXTURE_3D, levels, glInternalFormat, width, height, depth );
+
 		} else if ( ! texture.isVideoTexture ) {
 
 			gl.texStorage2D( glTextureType, levels, glInternalFormat, width, height );
@@ -429,6 +437,12 @@ class WebGLTextureUtils {
 
 			gl.texSubImage3D( gl.TEXTURE_2D_ARRAY, 0, 0, 0, 0, image.width, image.height, image.depth, glFormat, glType, image.data );
 
+		} else if ( texture.isData3DTexture ) {
+
+			const image = options.image;
+
+			gl.texSubImage3D( gl.TEXTURE_3D, 0, 0, 0, 0, image.width, image.height, image.depth, glFormat, glType, image.data );
+
 		} else if ( texture.isVideoTexture ) {
 
 			texture.update();

+ 13 - 5
examples/jsm/renderers/webgpu/nodes/WGSLNodeBuilder.js

@@ -3,7 +3,7 @@ import { NoColorSpace, FloatType } from 'three';
 import NodeUniformsGroup from '../../common/nodes/NodeUniformsGroup.js';
 
 import NodeSampler from '../../common/nodes/NodeSampler.js';
-import { NodeSampledTexture, NodeSampledCubeTexture } from '../../common/nodes/NodeSampledTexture.js';
+import { NodeSampledTexture, NodeSampledCubeTexture, NodeSampledTexture3D } from '../../common/nodes/NodeSampledTexture.js';
 
 import NodeUniformBuffer from '../../common/nodes/NodeUniformBuffer.js';
 import NodeStorageBuffer from '../../common/nodes/NodeStorageBuffer.js';
@@ -316,7 +316,7 @@ class WGSLNodeBuilder extends NodeBuilder {
 			const name = node.name;
 			const type = node.type;
 
-			if ( type === 'texture' || type === 'cubeTexture' || type === 'storageTexture' ) {
+			if ( type === 'texture' || type === 'cubeTexture' || type === 'storageTexture' || type === 'texture3D' ) {
 
 				return name;
 
@@ -369,7 +369,7 @@ class WGSLNodeBuilder extends NodeBuilder {
 
 			const bindings = this.bindings[ shaderStage ];
 
-			if ( type === 'texture' || type === 'cubeTexture' || type === 'storageTexture' ) {
+			if ( type === 'texture' || type === 'cubeTexture' || type === 'storageTexture' || type === 'texture3D' ) {
 
 				let texture = null;
 
@@ -381,6 +381,10 @@ class WGSLNodeBuilder extends NodeBuilder {
 
 					texture = new NodeSampledCubeTexture( uniformNode.name, uniformNode.node );
 
+				} else if ( type === 'texture3D' ) {
+
+					texture = new NodeSampledTexture3D( uniformNode.name, uniformNode.node );
+
 				}
 
 				texture.store = node.isStoreTextureNode === true;
@@ -455,7 +459,7 @@ class WGSLNodeBuilder extends NodeBuilder {
 
 	isReference( type ) {
 
-		return super.isReference( type ) || type === 'texture_2d' || type === 'texture_cube' || type === 'texture_depth_2d' || type === 'texture_storage_2d';
+		return super.isReference( type ) || type === 'texture_2d' || type === 'texture_cube' || type === 'texture_depth_2d' || type === 'texture_storage_2d' || type === 'texture_3d';
 
 	}
 
@@ -734,7 +738,7 @@ ${ flowData.code }
 
 		for ( const uniform of uniforms ) {
 
-			if ( uniform.type === 'texture' || uniform.type === 'cubeTexture' || uniform.type === 'storageTexture' ) {
+			if ( uniform.type === 'texture' || uniform.type === 'cubeTexture' || uniform.type === 'storageTexture' || uniform.type === 'texture3D' ) {
 
 				const texture = uniform.node.value;
 
@@ -770,6 +774,10 @@ ${ flowData.code }
 
 					textureType = 'texture_external';
 
+				} else if ( texture.isData3DTexture === true ) {
+
+					textureType = 'texture_3d<f32>';
+
 				} else if ( uniform.node.isStoreTextureNode === true ) {
 
 					const format = getFormat( texture );

+ 8 - 0
examples/jsm/renderers/webgpu/utils/WebGPUBindingUtils.js

@@ -103,6 +103,10 @@ class WebGPUBindingUtils {
 
 					texture.viewDimension = GPUTextureViewDimension.TwoDArray;
 
+				} else if ( binding.isSampledTexture3D ) {
+
+					texture.viewDimension = GPUTextureViewDimension.ThreeD;
+
 				}
 
 				bindingGPU.texture = texture;
@@ -214,6 +218,10 @@ class WebGPUBindingUtils {
 
 					dimensionViewGPU = GPUTextureViewDimension.Cube;
 
+				} else if ( binding.isSampledTexture3D ) {
+
+					dimensionViewGPU = GPUTextureViewDimension.ThreeD;
+
 				} else if ( binding.texture.isDataArrayTexture ) {
 
 					dimensionViewGPU = GPUTextureViewDimension.TwoDArray;

+ 2 - 2
examples/jsm/renderers/webgpu/utils/WebGPUTextureUtils.js

@@ -327,11 +327,11 @@ class WebGPUTextureUtils {
 
 		// transfer texture data
 
-		if ( texture.isDataTexture || texture.isData3DTexture ) {
+		if ( texture.isDataTexture ) {
 
 			this._copyBufferToTexture( options.image, textureData.texture, textureDescriptorGPU, 0, texture.flipY );
 
-		} else if ( texture.isDataArrayTexture ) {
+		} else if ( texture.isDataArrayTexture || texture.isData3DTexture ) {
 
 			for ( let i = 0; i < options.image.depth; i ++ ) {
 

二進制
examples/screenshots/webgpu_volume_cloud.jpg


二進制
examples/screenshots/webgpu_volume_perlin.jpg


+ 203 - 0
examples/webgpu_volume_cloud.html

@@ -0,0 +1,203 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js webgpu - volume - cloud</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">
+	</head>
+
+	<body>
+		<div id="info">
+			<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> webgpu - volume - cloud
+		</div>
+
+		<script type="importmap">
+			{
+				"imports": {
+					"three": "../build/three.module.js",
+					"three/addons/": "./jsm/",
+					"three/nodes": "./jsm/nodes/Nodes.js"
+				}
+			}
+		</script>
+
+		<script type="module">
+			import * as THREE from 'three';
+			import { MeshBasicNodeMaterial, VolumeNodeMaterial, vec3, materialReference, smoothstep, If, Break, tslFn } from 'three/nodes';
+
+			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+			import { ImprovedNoise } from 'three/addons/math/ImprovedNoise.js';
+
+			import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
+
+			import WebGPURenderer from 'three/addons/renderers/webgpu/WebGPURenderer.js';
+
+			let renderer, scene, camera;
+			let mesh;
+
+			init();
+
+			function init() {
+
+				renderer = new WebGPURenderer( { antialias: true } );
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				renderer.setAnimationLoop( animate );
+				document.body.appendChild( renderer.domElement );
+
+				scene = new THREE.Scene();
+
+				camera = new THREE.PerspectiveCamera( 60, window.innerWidth / window.innerHeight, 0.1, 100 );
+				camera.position.set( 0, 0, 1.5 );
+
+				new OrbitControls( camera, renderer.domElement );
+
+				// Sky
+
+				const canvas = document.createElement( 'canvas' );
+				canvas.width = 1;
+				canvas.height = 32;
+
+				const context = canvas.getContext( '2d' );
+				const gradient = context.createLinearGradient( 0, 0, 0, 32 );
+				gradient.addColorStop( 0.0, '#014a84' );
+				gradient.addColorStop( 0.5, '#0561a0' );
+				gradient.addColorStop( 1.0, '#437ab6' );
+				context.fillStyle = gradient;
+				context.fillRect( 0, 0, 1, 32 );
+
+				const skyMap = new THREE.CanvasTexture( canvas );
+				skyMap.colorSpace = THREE.SRGBColorSpace;
+
+				const sky = new THREE.Mesh(
+					new THREE.SphereGeometry( 10 ),
+					new MeshBasicNodeMaterial( { map: skyMap, side: THREE.BackSide } )
+				);
+				scene.add( sky );
+
+				// Texture
+
+				const size = 128;
+				const data = new Uint8Array( size * size * size );
+
+				let i = 0;
+				const scale = 0.05;
+				const perlin = new ImprovedNoise();
+				const vector = new THREE.Vector3();
+
+				for ( let z = 0; z < size; z ++ ) {
+
+					for ( let y = 0; y < size; y ++ ) {
+
+						for ( let x = 0; x < size; x ++ ) {
+
+							const d = 1.0 - vector.set( x, y, z ).subScalar( size / 2 ).divideScalar( size ).length();
+							data[ i ] = ( 128 + 128 * perlin.noise( x * scale / 1.5, y * scale, z * scale / 1.5 ) ) * d * d;
+							i ++;
+
+						}
+
+					}
+
+				}
+
+				const texture = new THREE.Data3DTexture( data, size, size, size );
+				texture.format = THREE.RedFormat;
+				texture.minFilter = THREE.LinearFilter;
+				texture.magFilter = THREE.LinearFilter;
+				texture.unpackAlignment = 1;
+				texture.needsUpdate = true;
+
+
+				const geometry = new THREE.BoxGeometry( 1, 1, 1 );
+
+				const material = new VolumeNodeMaterial( {
+					side: THREE.BackSide,
+					transparent: true
+				} );
+
+				material.map = texture;
+				material.base = new THREE.Color( 0x798aa0 );
+				material.steps = 100;
+				material.range = 0.1;
+				material.threshold = 0.25;
+				material.opacity = 0.25;
+
+				const range = materialReference( 'range', 'float' );
+				const threshold = materialReference( 'threshold', 'float' );
+				const opacity = materialReference( 'opacity', 'float' );
+
+				material.testNode = tslFn( ( { map, mapValue, probe, finalColor } ) => {
+
+					mapValue.assign( smoothstep( threshold.sub( range ), threshold.add( range ), mapValue ).mul( opacity ) );
+
+					const shading = map.uv( probe.add( vec3( - 0.01 ) ) ).r.sub( map.uv( probe.add( vec3( 0.01 ) ) ).r );
+
+					const col = shading.mul( 3.0 ).add( probe.x.add( probe.y ).mul( 0.25 ) ).add( 0.2 );
+
+					finalColor.rgb.addAssign( finalColor.a.oneMinus().mul( mapValue ).mul( col ) );
+
+					finalColor.a.addAssign( finalColor.a.oneMinus().mul( mapValue ) );
+
+					If( finalColor.a.greaterThanEqual(  0.95 ), () => {
+
+						Break();
+
+					} );
+
+				} );
+
+				mesh = new THREE.Mesh( geometry, material );
+
+				scene.add( mesh );
+
+				//
+
+				const parameters = {
+					threshold: 0.25,
+					opacity: 0.25,
+					range: 0.1,
+					steps: 100
+				};
+
+				function update() {
+
+					material.threshold = parameters.threshold;
+					material.opacity = parameters.opacity;
+					material.range = parameters.range;
+					material.steps = parameters.steps;
+
+				}
+
+				const gui = new GUI();
+				gui.add( parameters, 'threshold', 0, 1, 0.01 ).onChange( update );
+				gui.add( parameters, 'opacity', 0, 1, 0.01 ).onChange( update );
+				gui.add( parameters, 'range', 0, 1, 0.01 ).onChange( update );
+				gui.add( parameters, 'steps', 0, 200, 1 ).onChange( update );
+
+				window.addEventListener( 'resize', onWindowResize );
+
+			}
+
+			function onWindowResize() {
+
+				camera.aspect = window.innerWidth / window.innerHeight;
+				camera.updateProjectionMatrix();
+
+				renderer.setSize( window.innerWidth, window.innerHeight );
+
+			}
+
+			function animate() {
+
+				mesh.rotation.y = - performance.now() / 7500;
+
+				renderer.render( scene, camera );
+
+			}
+
+		</script>
+
+	</body>
+</html>

+ 159 - 0
examples/webgpu_volume_perlin.html

@@ -0,0 +1,159 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js webgpu - volume - perlin</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">
+	</head>
+
+	<body>
+		<div id="info">
+			<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> webgpu - volume - perlin
+		</div>
+
+		<script type="importmap">
+			{
+				"imports": {
+					"three": "../build/three.module.js",
+					"three/addons/": "./jsm/",
+					"three/nodes": "./jsm/nodes/Nodes.js"
+				}
+			}
+		</script>
+
+		<script type="module">
+			import * as THREE from 'three';
+			import { Break, If, VolumeNodeMaterial, vec3, materialReference, tslFn } from 'three/nodes';
+
+			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+			import { ImprovedNoise } from 'three/addons/math/ImprovedNoise.js';
+
+			import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
+
+			import WebGPURenderer from 'three/addons/renderers/webgpu/WebGPURenderer.js';
+
+			let renderer, scene, camera;
+			let mesh;
+
+			init();
+
+			function init() {
+
+				renderer = new WebGPURenderer( { antialias: true } );
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				renderer.setAnimationLoop( animate );
+				document.body.appendChild( renderer.domElement );
+
+				scene = new THREE.Scene();
+
+				camera = new THREE.PerspectiveCamera( 60, window.innerWidth / window.innerHeight, 0.1, 100 );
+				camera.position.set( 0, 0, 2 );
+
+				new OrbitControls( camera, renderer.domElement );
+
+				// Texture
+
+				const size = 128;
+				const data = new Uint8Array( size * size * size );
+
+				let i = 0;
+				const perlin = new ImprovedNoise();
+				const vector = new THREE.Vector3();
+
+				for ( let z = 0; z < size; z ++ ) {
+
+					for ( let y = 0; y < size; y ++ ) {
+
+						for ( let x = 0; x < size; x ++ ) {
+
+							vector.set( x, y, z ).divideScalar( size );
+
+							const d = perlin.noise( vector.x * 6.5, vector.y * 6.5, vector.z * 6.5 );
+
+							data[ i ++ ] = d * 128 + 128;
+
+						}
+
+					}
+
+				}
+
+				const texture = new THREE.Data3DTexture( data, size, size, size );
+				texture.format = THREE.RedFormat;
+				texture.minFilter = THREE.LinearFilter;
+				texture.magFilter = THREE.LinearFilter;
+				texture.unpackAlignment = 1;
+				texture.needsUpdate = true;
+
+				// Material
+
+				const geometry = new THREE.BoxGeometry( 1, 1, 1 );
+				const material = new VolumeNodeMaterial( {
+					side: THREE.BackSide,
+				} );
+
+				material.base = new THREE.Color( 0x798aa0 );
+				material.map = texture;
+				material.steps = 200;
+				material.threshold = 0.6;
+
+				const threshold = materialReference( 'threshold', 'float' );
+
+				material.testNode = tslFn( ( { map, mapValue, probe, finalColor } ) => {
+
+					If( mapValue.greaterThan( threshold ), () => {
+
+						const p = vec3().temp().assign( probe ).addAssign( 0.5 );
+
+						finalColor.rgb.assign( map.normal( p ).mul( 0.5 ).add( probe.mul( 1.5 ).add( 0.25 ) ) );
+						finalColor.a.assign( 1 );
+						Break();
+
+					} );
+
+				} );
+
+				mesh = new THREE.Mesh( geometry, material );
+
+				scene.add( mesh );
+
+				//
+
+				const parameters = { threshold: 0.6, steps: 200 };
+
+				function update() {
+
+					material.threshold = parameters.threshold;
+					material.steps = parameters.steps;
+
+				}
+
+				const gui = new GUI();
+				gui.add( parameters, 'threshold', 0, 1, 0.01 ).onChange( update );
+				gui.add( parameters, 'steps', 0, 300, 1 ).onChange( update );
+
+				window.addEventListener( 'resize', onWindowResize );
+
+			}
+
+			function onWindowResize() {
+
+				camera.aspect = window.innerWidth / window.innerHeight;
+				camera.updateProjectionMatrix();
+
+				renderer.setSize( window.innerWidth, window.innerHeight );
+
+			}
+
+			function animate() {
+
+				renderer.render( scene, camera );
+
+			}
+
+		</script>
+
+	</body>
+</html>