diff --git a/examples/files.json b/examples/files.json index 6d709d43a492a1..90942731909e0f 100644 --- a/examples/files.json +++ b/examples/files.json @@ -354,6 +354,7 @@ "webgpu_lensflares", "webgpu_lightprobe", "webgpu_lightprobe_cubecamera", + "webgpu_lights_clustered", "webgpu_lights_custom", "webgpu_lights_dynamic", "webgpu_lights_ies_spotlight", diff --git a/examples/jsm/lighting/ClusteredLighting.js b/examples/jsm/lighting/ClusteredLighting.js new file mode 100644 index 00000000000000..a4e32e6bdc0b9d --- /dev/null +++ b/examples/jsm/lighting/ClusteredLighting.js @@ -0,0 +1,55 @@ +import { Lighting } from 'three/webgpu'; +import ClusteredLightsNode from '../tsl/lighting/ClusteredLightsNode.js'; + +/** + * A custom lighting implementation based on Forward+ Clustered Shading that + * overwrites the default lighting system in {@link WebGPURenderer}. Suitable + * for 3D scenes with many point lights and real depth complexity — the view + * frustum is partitioned into a 3D cluster grid so only the lights actually + * reaching each fragment are evaluated. + * + * ```js + * const lighting = new ClusteredLighting(); + * renderer.lighting = lighting; // set lighting system + * ``` + * + * @augments Lighting + * @three_import import { ClusteredLighting } from 'three/addons/lighting/ClusteredLighting.js'; + */ +export class ClusteredLighting extends Lighting { + + /** + * Constructs a new clustered lighting system. + * + * @param {number} [maxLights=1024] - Maximum number of point lights. + * @param {number} [tileSize=32] - Screen tile size in pixels (cluster XY size). + * @param {number} [zSlices=24] - Number of exponential depth slices. + * @param {number} [maxLightsPerCluster=64] - Per-cluster light-list capacity. + */ + constructor( maxLights = 1024, tileSize = 32, zSlices = 24, maxLightsPerCluster = 64 ) { + + super(); + + this.maxLights = maxLights; + this.tileSize = tileSize; + this.zSlices = zSlices; + this.maxLightsPerCluster = maxLightsPerCluster; + + } + + /** + * Creates a new clustered lights node for the given array of lights. + * + * This method is called internally by the renderer and must be overwritten by + * all custom lighting implementations. + * + * @param {Array} lights - The lights. + * @return {ClusteredLightsNode} The clustered lights node. + */ + createNode( lights = [] ) { + + return new ClusteredLightsNode( this.maxLights, this.tileSize, this.zSlices, this.maxLightsPerCluster ).setLights( lights ); + + } + +} diff --git a/examples/jsm/tsl/lighting/ClusteredLightsNode.js b/examples/jsm/tsl/lighting/ClusteredLightsNode.js new file mode 100644 index 00000000000000..18f80c439a81d9 --- /dev/null +++ b/examples/jsm/tsl/lighting/ClusteredLightsNode.js @@ -0,0 +1,613 @@ +import { DataTexture, FloatType, RGBAFormat, Vector2, Vector3, LightsNode, NodeUpdateType } from 'three/webgpu'; + +import { + attributeArray, nodeProxy, int, float, vec3, ivec2, ivec4, uniform, Break, Loop, positionView, + Fn, If, Return, textureLoad, instanceIndex, screenCoordinate, directPointLight, + renderGroup, + min, max, pow, log, clamp, dot +} from 'three/tsl'; + +const _vector3 = /*@__PURE__*/ new Vector3(); +const _size = /*@__PURE__*/ new Vector2(); + +/** + * A custom version of `LightsNode` implementing Forward+ clustered shading: + * the view frustum is subdivided into a 3D grid of clusters (X × Y screen tiles + * times an exponentially-spaced set of Z depth slices), and each cluster holds + * only the point lights whose spheres intersect it. At shading time each fragment + * looks up its cluster and loops over just that cluster's lights. Unlike 2D tiled + * lighting, clustered shading culls lights that share screen pixels but lie at + * different depths — suitable for 3D scenes with real depth complexity. + * + * @augments LightsNode + * @three_import import { clusteredLights } from 'three/addons/tsl/lighting/ClusteredLightsNode.js'; + */ +class ClusteredLightsNode extends LightsNode { + + static get type() { + + return 'ClusteredLightsNode'; + + } + + /** + * Constructs a new clustered lights node. + * + * @param {number} [maxLights=1024] - Maximum number of point lights. + * @param {number} [tileSize=32] - Screen tile size in pixels (cluster XY size). + * @param {number} [zSlices=24] - Number of exponential depth slices. + * @param {number} [maxLightsPerCluster=64] - Per-cluster light-list capacity. + */ + constructor( maxLights = 1024, tileSize = 32, zSlices = 24, maxLightsPerCluster = 64 ) { + + super(); + + this.materialLights = []; + this.clusteredLights = []; + + this.maxLights = maxLights; + this.tileSize = tileSize; + this.zSlices = zSlices; + this.maxLightsPerCluster = maxLightsPerCluster; + + this._chunksPerCluster = Math.ceil( maxLightsPerCluster / 4 ); + + this._bufferSize = null; + this._lightIndexes = null; + this._screenClusterIndex = null; + this._compute = null; + this._lightsTexture = null; + this._zSliceRangesTexture = null; + this._zSliceRangesData = null; + this._lightViewZ = new Float32Array( maxLights ); + this._lightSortOrder = []; + + this._lightsCount = uniform( 0, 'int' ); + + // Render-group uniforms: shared between compute and fragment passes, + // updated manually each frame in updateBefore (compute lacks a camera context). + this._cameraNear = uniform( 0 ).setName( 'clusteredCameraNear' ).setGroup( renderGroup ); + this._cameraFar = uniform( 0 ).setName( 'clusteredCameraFar' ).setGroup( renderGroup ); + this._cameraViewMatrix = uniform( 'mat4' ).setName( 'clusteredCameraViewMatrix' ).setGroup( renderGroup ); + this._cameraProjectionMatrix = uniform( 'mat4' ).setName( 'clusteredCameraProjectionMatrix' ).setGroup( renderGroup ); + + this._gridDimensions = uniform( new Vector2() ); + + this.updateBeforeType = NodeUpdateType.RENDER; + + } + + customCacheKey() { + + return this._compute.getCacheKey() + super.customCacheKey(); + + } + + updateLightsTexture( camera ) { + + const { _lightsTexture: lightsTexture, clusteredLights } = this; + + const data = lightsTexture.image.data; + const lineSize = lightsTexture.image.width * 4; + const count = clusteredLights.length; + + this._lightsCount.value = count; + + // Sort lights by view-space depth for Z-culling + + const viewZ = this._lightViewZ; + const order = this._lightSortOrder; + + for ( let i = 0; i < count; i ++ ) { + + _vector3.setFromMatrixPosition( clusteredLights[ i ].matrixWorld ); + _vector3.applyMatrix4( camera.matrixWorldInverse ); + viewZ[ i ] = _vector3.z; + order[ i ] = i; + + } + + order.length = count; + order.sort( ( a, b ) => viewZ[ a ] - viewZ[ b ] ); + + // Write sorted lights to texture + + for ( let i = 0; i < count; i ++ ) { + + const light = clusteredLights[ order[ i ] ]; + + _vector3.setFromMatrixPosition( light.matrixWorld ); + + const offset = i * 4; + + data[ offset + 0 ] = _vector3.x; + data[ offset + 1 ] = _vector3.y; + data[ offset + 2 ] = _vector3.z; + data[ offset + 3 ] = light.distance; + + data[ lineSize + offset + 0 ] = light.color.r * light.intensity; + data[ lineSize + offset + 1 ] = light.color.g * light.intensity; + data[ lineSize + offset + 2 ] = light.color.b * light.intensity; + data[ lineSize + offset + 3 ] = light.decay; + + } + + lightsTexture.needsUpdate = true; + + // Compute per Z-slice light ranges + + const zRanges = this._zSliceRangesData; + + if ( zRanges === null ) return; + + const near = camera.near; + const far = camera.far; + const NZ = this.zSlices; + + for ( let z = 0; z < NZ; z ++ ) { + + // Exponential Z-slice bounds (view-space, negative values) + const sliceNear = - ( near * Math.pow( far / near, z / NZ ) ); + const sliceFar = - ( near * Math.pow( far / near, ( z + 1 ) / NZ ) ); + + let rangeStart = count; + let rangeEnd = 0; + + for ( let i = 0; i < count; i ++ ) { + + const vz = viewZ[ order[ i ] ]; + const r = clusteredLights[ order[ i ] ].distance; + const radius = r > 0 ? r : far; + + // Light sphere Z: [vz - radius, vz + radius] + // Slice Z: [sliceFar, sliceNear] (both negative, sliceFar < sliceNear) + if ( vz + radius >= sliceFar && vz - radius <= sliceNear ) { + + if ( i < rangeStart ) rangeStart = i; + if ( i + 1 > rangeEnd ) rangeEnd = i + 1; + + } + + } + + if ( rangeStart >= count ) { + + rangeStart = 0; + rangeEnd = 0; + + } + + zRanges[ z * 4 ] = rangeStart; + zRanges[ z * 4 + 1 ] = rangeEnd; + + } + + this._zSliceRangesTexture.needsUpdate = true; + + } + + updateBefore( frame ) { + + const { renderer, camera } = frame; + + this.updateProgram( renderer ); + + this.updateLightsTexture( camera ); + + this._cameraNear.value = camera.near; + this._cameraFar.value = camera.far; + this._cameraViewMatrix.value = camera.matrixWorldInverse; + this._cameraProjectionMatrix.value = camera.projectionMatrix; + + renderer.compute( this._compute ); + + } + + setLights( lights ) { + + const { clusteredLights, materialLights } = this; + + let materialIndex = 0; + let clusteredIndex = 0; + + for ( const light of lights ) { + + if ( light.isPointLight === true ) { + + clusteredLights[ clusteredIndex ++ ] = light; + + } else { + + materialLights[ materialIndex ++ ] = light; + + } + + } + + materialLights.length = materialIndex; + clusteredLights.length = clusteredIndex; + + return super.setLights( materialLights ); + + } + + getBlock() { + + return this._lightIndexes.element( this._screenClusterIndex.mul( int( this._chunksPerCluster ) ) ); + + } + + getTile( element ) { + + element = int( element ); + + const stride = int( 4 ); + const chunkOffset = element.div( stride ); + const idx = this._screenClusterIndex.mul( int( this._chunksPerCluster ) ).add( chunkOffset ); + + return this._lightIndexes.element( idx ).element( element.mod( stride ) ); + + } + + getClusterLightCount( zSliceNode ) { + + const getCount = Fn( ( [ zSliceNode ] ) => { + + const count = int( 0 ).toVar(); + + const debugClusterIndex = this._screenClusterIndex.toVar(); + + If( zSliceNode.greaterThanEqual( int( 0 ) ), () => { + + const tileSize = int( this.tileSize ); + const screenTile = screenCoordinate.div( tileSize ).floor(); + const NX = int( this._gridDimensions.x ); + const NY = int( this._gridDimensions.y ); + + debugClusterIndex.assign( + int( screenTile.x ) + .add( int( screenTile.y ).mul( NX ) ) + .add( zSliceNode.mul( NX.mul( NY ) ) ) + ); + + } ); + + Loop( this.maxLightsPerCluster, ( { i } ) => { + + const element = int( i ); + const stride = int( 4 ); + const chunkOffset = element.div( stride ); + const idx = debugClusterIndex.mul( int( this._chunksPerCluster ) ).add( chunkOffset ); + const lightIndex = this._lightIndexes.element( idx ).element( element.mod( stride ) ); + + If( lightIndex.equal( int( 0 ) ), () => { + + Break(); + + } ); + + count.addAssign( int( 1 ) ); + + } ); + + return count; + + } ); + + return getCount( zSliceNode ); + + } + + getLightData( index ) { + + index = int( index ); + + const dataA = textureLoad( this._lightsTexture, ivec2( index, 0 ) ); + const dataB = textureLoad( this._lightsTexture, ivec2( index, 1 ) ); + + const position = dataA.xyz; + const viewPosition = this._cameraViewMatrix.mul( position ); + const distance = dataA.w; + const color = dataB.rgb; + const decay = dataB.w; + + return { + position, + viewPosition, + distance, + color, + decay + }; + + } + + setupLights( builder, lightNodes ) { + + this.updateProgram( builder.renderer ); + + // + + const lightingModel = builder.context.reflectedLight; + + lightingModel.directDiffuse.toStack(); + lightingModel.directSpecular.toStack(); + + super.setupLights( builder, lightNodes ); + + Fn( () => { + + Loop( this.maxLightsPerCluster, ( { i } ) => { + + const lightIndex = this.getTile( i ); + + If( lightIndex.equal( int( 0 ) ), () => { + + Break(); + + } ); + + const { color, decay, viewPosition, distance } = this.getLightData( lightIndex.sub( 1 ) ); + + const lightVector = viewPosition.sub( positionView ); + + // Early-out: skip full BRDF if fragment is beyond the light's cutoff + If( distance.equal( 0 ).or( dot( lightVector, lightVector ).lessThanEqual( distance.mul( distance ) ) ), () => { + + builder.lightsNode.setupDirectLight( builder, this, directPointLight( { + color, + lightVector, + cutoffDistance: distance, + decayExponent: decay + } ) ); + + } ); + + } ); + + }, 'void' )(); + + } + + getBufferFitSize( value ) { + + const multiple = this.tileSize; + + return Math.ceil( value / multiple ) * multiple; + + } + + setSize( width, height ) { + + width = this.getBufferFitSize( width ); + height = this.getBufferFitSize( height ); + + if ( ! this._bufferSize || this._bufferSize.width !== width || this._bufferSize.height !== height ) { + + this.create( width, height ); + + } + + return this; + + } + + updateProgram( renderer ) { + + renderer.getDrawingBufferSize( _size ); + + const width = this.getBufferFitSize( _size.width ); + const height = this.getBufferFitSize( _size.height ); + + if ( this._bufferSize === null ) { + + this.create( width, height ); + + } else if ( this._bufferSize.width !== width || this._bufferSize.height !== height ) { + + this.create( width, height ); + + } + + } + + create( width, height ) { + + const { tileSize, maxLights, zSlices, maxLightsPerCluster, _chunksPerCluster: chunksPerCluster } = this; + + const bufferSize = new Vector2( width, height ); + + const NX = Math.floor( bufferSize.width / tileSize ); + const NY = Math.floor( bufferSize.height / tileSize ); + const NZ = zSlices; + const clusterCount = NX * NY * NZ; + + this._gridDimensions.value.set( NX, NY ); + + // Lights data texture (same layout as TiledLightsNode) + + const lightsData = new Float32Array( maxLights * 4 * 2 ); + const lightsTexture = new DataTexture( lightsData, lightsData.length / 8, 2, RGBAFormat, FloatType ); + + // Per Z-slice light range for Z-culling (CPU-sorted, uploaded each frame) + + const zSliceRangesData = new Float32Array( NZ * 4 ); + const zSliceRangesTexture = new DataTexture( zSliceRangesData, NZ, 1, RGBAFormat, FloatType ); + + // Per-cluster light-index storage (ivec4 chunks) + + const lightIndexesArray = new Int32Array( clusterCount * chunksPerCluster * 4 ); + const lightIndexes = attributeArray( lightIndexesArray, 'ivec4' ).setName( 'lightIndexes' ); + + // compute-side accessors (use instanceIndex) + + const getClusterChunk = ( chunkIdx ) => { + + const idx = instanceIndex.mul( int( chunksPerCluster ) ).add( int( chunkIdx ) ); + + return lightIndexes.element( idx ); + + }; + + const getClusterSlot = ( slotIdx ) => { + + slotIdx = int( slotIdx ); + + const stride = int( 4 ); + const chunkOffset = slotIdx.div( stride ); + const idx = instanceIndex.mul( int( chunksPerCluster ) ).add( chunkOffset ); + + return lightIndexes.element( idx ).element( slotIdx.mod( stride ) ); + + }; + + // compute: one thread per cluster + + const compute = Fn( () => { + + // view-space scale factors derived from the projection matrix: + // view_x = ndc_x * (-view_z) / focal_x = ndc_x * (-view_z) * invFocalX + // view_y = ndc_y * (-view_z) / focal_y = ndc_y * (-view_z) * invFocalY + // where focal_x = projMatrix[0][0] and focal_y = projMatrix[1][1]. + const invFocalX = float( 1 ).div( this._cameraProjectionMatrix.element( 0 ).element( 0 ) ); + const invFocalY = float( 1 ).div( this._cameraProjectionMatrix.element( 1 ).element( 1 ) ); + + // 3D cluster coordinates from instanceIndex + const cx = instanceIndex.mod( NX ); + const cy = instanceIndex.div( NX ).mod( NY ); + const cz = instanceIndex.div( NX * NY ); + + // NDC X/Y bounds of the cluster. + // Y is flipped: cy=0 is the top screen row (fragment y=0), which is NDC y=+1. + const ndcXmin = float( cx ).mul( 2.0 / NX ).sub( 1.0 ); + const ndcXmax = float( cx.add( int( 1 ) ) ).mul( 2.0 / NX ).sub( 1.0 ); + const ndcYmax = float( 1 ).sub( float( cy ).mul( 2.0 / NY ) ); + const ndcYmin = float( 1 ).sub( float( cy.add( int( 1 ) ) ).mul( 2.0 / NY ) ); + + // View-space Z bounds (negative, exponential slicing) + const farOverNear = this._cameraFar.div( this._cameraNear ); + const zNearCluster = this._cameraNear.mul( pow( farOverNear, float( cz ).mul( 1.0 / NZ ) ) ).negate(); + const zFarCluster = this._cameraNear.mul( pow( farOverNear, float( cz.add( int( 1 ) ) ).mul( 1.0 / NZ ) ) ).negate(); + + const scaleNearX = zNearCluster.negate().mul( invFocalX ); + const scaleFarX = zFarCluster.negate().mul( invFocalX ); + const scaleNearY = zNearCluster.negate().mul( invFocalY ); + const scaleFarY = zFarCluster.negate().mul( invFocalY ); + + const xMinNear = ndcXmin.mul( scaleNearX ); + const xMaxNear = ndcXmax.mul( scaleNearX ); + const xMinFar = ndcXmin.mul( scaleFarX ); + const xMaxFar = ndcXmax.mul( scaleFarX ); + + const yMinNear = ndcYmin.mul( scaleNearY ); + const yMaxNear = ndcYmax.mul( scaleNearY ); + const yMinFar = ndcYmin.mul( scaleFarY ); + const yMaxFar = ndcYmax.mul( scaleFarY ); + + // AABB of the 8 view-space corners (tile boundaries can straddle the view axis) + const aabbMinX = min( xMinNear, xMinFar ); + const aabbMaxX = max( xMaxNear, xMaxFar ); + const aabbMinY = min( yMinNear, yMinFar ); + const aabbMaxY = max( yMaxNear, yMaxFar ); + + const aabbMin = vec3( aabbMinX, aabbMinY, zFarCluster ); + const aabbMax = vec3( aabbMaxX, aabbMaxY, zNearCluster ); + + // clear stale data from previous frame + Loop( chunksPerCluster, ( { i } ) => { + + getClusterChunk( i ).assign( ivec4( 0 ) ); + + } ); + + const index = int( 0 ).toVar(); + + // Z-culling: only test lights that can reach this cluster's Z-slice + const zRange = textureLoad( zSliceRangesTexture, ivec2( cz, 0 ) ); + const rangeStart = int( zRange.x ); + const rangeEnd = int( zRange.y ); + + Loop( this.maxLights, ( { i } ) => { + + const lightIdx = rangeStart.add( i ); + + If( index.greaterThanEqual( int( maxLightsPerCluster ) ).or( lightIdx.greaterThanEqual( rangeEnd ) ), () => { + + Return(); + + } ); + + const { viewPosition, distance } = this.getLightData( lightIdx ); + + // sphere-AABB intersection in view space + const pos = viewPosition.xyz; + const closest = max( aabbMin, min( pos, aabbMax ) ); + const diff = pos.sub( closest ); + const distSq = dot( diff, diff ); + + If( distSq.lessThanEqual( distance.mul( distance ) ), () => { + + getClusterSlot( index ).assign( lightIdx.add( int( 1 ) ) ); + index.addAssign( int( 1 ) ); + + } ); + + } ); + + } )().compute( clusterCount ).setName( 'Update Clustered Lights' ); + + // shading-side: fragment → cluster index + + const getScreenClusterIndex = Fn( () => { + + const screenTile = screenCoordinate.div( tileSize ).floor(); + + // view-space depth from positionView (negative in front); take magnitude + const viewDepth = positionView.z.negate(); + + // exponential Z slice: tz = floor( log(depth/near) / log(far/near) * NZ ) + const invLogFarOverNear = float( 1 ).div( log( this._cameraFar.div( this._cameraNear ) ) ); + const sliceFloat = log( viewDepth.div( this._cameraNear ) ).mul( invLogFarOverNear ).mul( float( NZ ) ); + const zSlice = clamp( sliceFloat.floor(), float( 0 ), float( NZ - 1 ) ); + + return int( screenTile.x ) + .add( int( screenTile.y ).mul( int( NX ) ) ) + .add( int( zSlice ).mul( int( NX * NY ) ) ); + + } ); + + const screenClusterIndex = getScreenClusterIndex().toVar(); + + // assigns + + this._bufferSize = bufferSize; + this._lightIndexes = lightIndexes; + this._screenClusterIndex = screenClusterIndex; + this._compute = compute; + this._lightsTexture = lightsTexture; + this._zSliceRangesTexture = zSliceRangesTexture; + this._zSliceRangesData = zSliceRangesData; + + } + + get hasLights() { + + return super.hasLights || this.clusteredLights.length > 0; + + } + +} + +export default ClusteredLightsNode; + +/** + * TSL function that creates a clustered lights node. + * + * @tsl + * @function + * @param {number} [maxLights=1024] - Maximum number of point lights. + * @param {number} [tileSize=32] - Screen tile size in pixels. + * @param {number} [zSlices=24] - Depth slice count. + * @param {number} [maxLightsPerCluster=64] - Per-cluster light-list capacity. + * @return {ClusteredLightsNode} The clustered lights node. + */ +export const clusteredLights = /*@__PURE__*/ nodeProxy( ClusteredLightsNode ); diff --git a/examples/screenshots/webgpu_lights_clustered.jpg b/examples/screenshots/webgpu_lights_clustered.jpg new file mode 100644 index 00000000000000..b153f2409ff3fb Binary files /dev/null and b/examples/screenshots/webgpu_lights_clustered.jpg differ diff --git a/examples/webgpu_lights_clustered.html b/examples/webgpu_lights_clustered.html new file mode 100644 index 00000000000000..46439ffb6a1b59 --- /dev/null +++ b/examples/webgpu_lights_clustered.html @@ -0,0 +1,299 @@ + + + + three.js webgpu - clustered lighting + + + + + + +
+ + +
+ three.jsClustered Lighting +
+ + + Forward+ clustered lighting. + +
+ + + + + + diff --git a/src/Three.TSL.js b/src/Three.TSL.js index 589b13885fc808..48ef7125d7297b 100644 --- a/src/Three.TSL.js +++ b/src/Three.TSL.js @@ -518,6 +518,7 @@ export const stepElement = TSL.stepElement; export const storage = TSL.storage; export const storageBarrier = TSL.storageBarrier; export const storageTexture = TSL.storageTexture; +export const storageTexture3D = TSL.storageTexture3D; export const struct = TSL.struct; export const sub = TSL.sub; export const subgroupAdd = TSL.subgroupAdd; diff --git a/src/nodes/accessors/StorageTexture3DNode.js b/src/nodes/accessors/StorageTexture3DNode.js new file mode 100644 index 00000000000000..f0479ce9b4c549 --- /dev/null +++ b/src/nodes/accessors/StorageTexture3DNode.js @@ -0,0 +1,100 @@ +import { nodeProxy, vec3 } from '../tsl/TSLBase.js'; +import StorageTextureNode from './StorageTextureNode.js'; + +/** + * This special version of a texture node can be used to + * write data into a 3D storage texture with a compute shader. + * + * @augments StorageTextureNode + */ +class StorageTexture3DNode extends StorageTextureNode { + + static get type() { + + return 'StorageTexture3DNode'; + + } + + /** + * Constructs a new 3D storage texture node. + * + * @param {Storage3DTexture} value - The 3D storage texture. + * @param {Node} uvNode - The uv node. + * @param {?Node} [storeNode=null] - The value node that should be stored in the texture. + */ + constructor( value, uvNode, storeNode = null ) { + + super( value, uvNode, storeNode ); + + /** + * This flag can be used for type testing. + * + * @type {boolean} + * @readonly + * @default true + */ + this.isStorageTexture3DNode = true; + + } + + /** + * Returns a default uv node which is in context of 3D textures a three-dimensional + * uv node. + * + * @return {Node} The default uv node. + */ + getDefaultUV() { + + return vec3( 0.5, 0.5, 0.5 ); + + } + + /** + * Overwritten with an empty implementation since the `updateMatrix` flag is ignored + * for 3D textures. The uv transformation matrix is not applied to 3D textures. + * + * @param {boolean} value - The update toggle. + */ + setUpdateMatrix( /*value*/ ) { } // Ignore .updateMatrix for 3d TextureNode + + /** + * Generates the uv code snippet. + * + * @param {NodeBuilder} builder - The current node builder. + * @param {Node} uvNode - The uv node to generate code for. + * @return {string} The generated code snippet. + */ + generateUV( builder, uvNode ) { + + return uvNode.build( builder, this.sampler === true ? 'vec3' : 'ivec3' ); + + } + + /** + * Generates the offset code snippet. + * + * @param {NodeBuilder} builder - The current node builder. + * @param {Node} offsetNode - The offset node to generate code for. + * @return {string} The generated code snippet. + */ + generateOffset( builder, offsetNode ) { + + return offsetNode.build( builder, 'ivec3' ); + + } + +} + +export default StorageTexture3DNode; + +/** + * TSL function for creating a 3D storage texture node. + * + * @tsl + * @function + * @param {Storage3DTexture} value - The 3D storage texture. + * @param {?Node} [uvNode=null] - The uv node. + * @param {?Node} [storeNode=null] - The value node that should be stored in the texture. + * @returns {StorageTexture3DNode} + */ +export const storageTexture3D = /*@__PURE__*/ nodeProxy( StorageTexture3DNode ).setParameterLength( 1, 3 ); diff --git a/src/nodes/math/MathNode.js b/src/nodes/math/MathNode.js index 3688c7bd55f338..33498b83fe4aa7 100644 --- a/src/nodes/math/MathNode.js +++ b/src/nodes/math/MathNode.js @@ -40,7 +40,7 @@ class MathNode extends TempNode { let finalOp = new MathNode( method, aNode, bNode ); - for ( let i = 2; i < arguments.length - 1; i ++ ) { + for ( let i = 3; i < arguments.length - 1; i ++ ) { finalOp = new MathNode( method, finalOp, arguments[ i ] ); diff --git a/src/nodes/math/OperatorNode.js b/src/nodes/math/OperatorNode.js index 2d044b495cee3c..bf53b92c7f9f5b 100644 --- a/src/nodes/math/OperatorNode.js +++ b/src/nodes/math/OperatorNode.js @@ -128,10 +128,16 @@ class OperatorNode extends TempNode { return builder.getIntegerType( typeA ); - } else if ( op === '!' || op === '&&' || op === '||' || op === '^^' ) { + } else if ( op === '&&' || op === '||' || op === '^^' ) { return 'bool'; + } else if ( op === '!' ) { + + const typeLength = builder.getTypeLength( typeA ); + + return typeLength > 1 ? `bvec${ typeLength }` : 'bool'; + } else if ( op === '==' || op === '!=' || op === '<' || op === '>' || op === '<=' || op === '>=' ) { const typeLength = Math.max( builder.getTypeLength( typeA ), builder.getTypeLength( typeB ) ); @@ -330,9 +336,23 @@ class OperatorNode extends TempNode { } - } else if ( op === '!' || op === '~' ) { + } else if ( op === '!' ) { + + if ( isGLSL && builder.isVector( typeA ) ) { + + return builder.format( `not( ${a} )`, output ); + + } else { + + // WGSL and scalars on GLSL + + return builder.format( `( ${op} ${a} )`, typeA, output ); + + } + + } else if ( op === '~' ) { - return builder.format( `(${op}${a})`, typeA, output ); + return builder.format( `( ${op} ${a} )`, typeA, output ); } else if ( fnOpSnippet ) {