diff --git a/examples/files.json b/examples/files.json index 43f9d04e09837e..4509551e4d2365 100644 --- a/examples/files.json +++ b/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", diff --git a/examples/jsm/nodes/Nodes.js b/examples/jsm/nodes/Nodes.js index cf6c2ce42ee5e7..a37b01cec4d739 100644 --- a/examples/jsm/nodes/Nodes.js +++ b/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'; diff --git a/examples/jsm/nodes/accessors/ReferenceNode.js b/examples/jsm/nodes/accessors/ReferenceNode.js index 5f95110c44d979..0069581b52732b 100644 --- a/examples/jsm/nodes/accessors/ReferenceNode.js +++ b/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 ); } diff --git a/examples/jsm/nodes/accessors/Texture3DNode.js b/examples/jsm/nodes/accessors/Texture3DNode.js new file mode 100644 index 00000000000000..9662279ecf45af --- /dev/null +++ b/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 ); diff --git a/examples/jsm/nodes/core/NodeBuilder.js b/examples/jsm/nodes/core/NodeBuilder.js index 0a7929ebe7bc27..a496c6d6152c0c 100644 --- a/examples/jsm/nodes/core/NodeBuilder.js +++ b/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; diff --git a/examples/jsm/nodes/materials/Materials.js b/examples/jsm/nodes/materials/Materials.js index 7c1073c3bc627e..ce02b53641aed1 100644 --- a/examples/jsm/nodes/materials/Materials.js +++ b/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'; diff --git a/examples/jsm/nodes/materials/VolumeNodeMaterial.js b/examples/jsm/nodes/materials/VolumeNodeMaterial.js new file mode 100644 index 00000000000000..9c53f35f899462 --- /dev/null +++ b/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 ); diff --git a/examples/jsm/renderers/common/nodes/NodeSampledTexture.js b/examples/jsm/renderers/common/nodes/NodeSampledTexture.js index 6d830d27e975b8..9f85016adabd8a 100644 --- a/examples/jsm/renderers/common/nodes/NodeSampledTexture.js +++ b/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 }; diff --git a/examples/jsm/renderers/webgl/nodes/GLSLNodeBuilder.js b/examples/jsm/renderers/webgl/nodes/GLSLNodeBuilder.js index deffeaef783078..08ed6b223d9c83 100644 --- a/examples/jsm/renderers/webgl/nodes/GLSLNodeBuilder.js +++ b/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 }`; diff --git a/examples/jsm/renderers/webgl/utils/WebGLTextureUtils.js b/examples/jsm/renderers/webgl/utils/WebGLTextureUtils.js index 64a8d44cab3a4f..9d9fcd97dce0a0 100644 --- a/examples/jsm/renderers/webgl/utils/WebGLTextureUtils.js +++ b/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(); diff --git a/examples/jsm/renderers/webgpu/nodes/WGSLNodeBuilder.js b/examples/jsm/renderers/webgpu/nodes/WGSLNodeBuilder.js index 8413e2fadeaf21..5988065e948668 100644 --- a/examples/jsm/renderers/webgpu/nodes/WGSLNodeBuilder.js +++ b/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'; + } else if ( uniform.node.isStoreTextureNode === true ) { const format = getFormat( texture ); diff --git a/examples/jsm/renderers/webgpu/utils/WebGPUBindingUtils.js b/examples/jsm/renderers/webgpu/utils/WebGPUBindingUtils.js index cc59d7074c60c0..c9ea8e251089bb 100644 --- a/examples/jsm/renderers/webgpu/utils/WebGPUBindingUtils.js +++ b/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; diff --git a/examples/jsm/renderers/webgpu/utils/WebGPUTextureUtils.js b/examples/jsm/renderers/webgpu/utils/WebGPUTextureUtils.js index 614a62a4a928bc..4edf9df0d8cb39 100644 --- a/examples/jsm/renderers/webgpu/utils/WebGPUTextureUtils.js +++ b/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 ++ ) { diff --git a/examples/screenshots/webgpu_volume_cloud.jpg b/examples/screenshots/webgpu_volume_cloud.jpg new file mode 100644 index 00000000000000..b1abafb33263e6 Binary files /dev/null and b/examples/screenshots/webgpu_volume_cloud.jpg differ diff --git a/examples/screenshots/webgpu_volume_perlin.jpg b/examples/screenshots/webgpu_volume_perlin.jpg new file mode 100644 index 00000000000000..e7f5380a7f5ae4 Binary files /dev/null and b/examples/screenshots/webgpu_volume_perlin.jpg differ diff --git a/examples/webgpu_volume_cloud.html b/examples/webgpu_volume_cloud.html new file mode 100644 index 00000000000000..9206bd35253c17 --- /dev/null +++ b/examples/webgpu_volume_cloud.html @@ -0,0 +1,203 @@ + + + + three.js webgpu - volume - cloud + + + + + + +
+ three.js webgpu - volume - cloud +
+ + + + + + + diff --git a/examples/webgpu_volume_perlin.html b/examples/webgpu_volume_perlin.html new file mode 100644 index 00000000000000..489c7bf950ab90 --- /dev/null +++ b/examples/webgpu_volume_perlin.html @@ -0,0 +1,159 @@ + + + + three.js webgpu - volume - perlin + + + + + + +
+ three.js webgpu - volume - perlin +
+ + + + + + +