UNPKG

three

Version:

JavaScript 3D library

1,431 lines (955 loc) 46.4 kB
import { Box3, Vector3, Vector4, Matrix4, Layers, Storage3DTexture, StorageBufferAttribute, StorageTexture3DNode, CubeTextureNode, RendererUtils, HalfFloatType, UnsignedByteType, RGBAFormat, LinearFilter, LinearMipmapLinearFilter, ClampToEdgeWrapping, MathUtils } from 'three/webgpu'; import { Fn, If, Loop, nodeObject, uniform, uniformArray, storage, instanceIndex, textureStore, texture3D, texture, float, int, uint, vec2, vec3, vec4, ivec3, uvec3, max, min, abs, dot, cross, normalize, floor, sign, select, countOneBits, atomicOr, smoothstep, hash, fract, sqrt, cos, sin, length, PI, getDistanceAttenuation, viewZToPerspectiveDepth, viewZToReversedPerspectiveDepth } from 'three/tsl'; import { collectSceneTriangles, computeSceneBounds, TRIANGLE_STRIDE } from './VXGISceneCollector.js'; import { createConeTracer } from './VXGIConeTracer.js'; const _box = /*@__PURE__*/ new Box3(); const _size = /*@__PURE__*/ new Vector3(); const _position = /*@__PURE__*/ new Vector3(); const _target = /*@__PURE__*/ new Vector3(); const MAX_EDGE_SUBVOXELS = 16; const BOUNCE_CONE_COUNT = 8; // the six directions a ray can travel, as (axis, sign) pairs const DIRECTIONS = [[ 0, 1 ], [ 0, - 1 ], [ 1, 1 ], [ 1, - 1 ], [ 2, 1 ], [ 2, - 1 ]]; let _rendererState; /** * Storage texture node bound to a single mip level. Texture nodes normally share one binding * per texture; this node keys the binding by access and mip level so a kernel can read one * level while writing another. * * @private */ class MipStorageTexture3DNode extends StorageTexture3DNode { constructor( value, access, mipLevel ) { super( value, null, null ); this.setAccess( access ); this.setMipLevel( mipLevel ); } getUniformHash() { return `${ this.value.uuid }:${ this.access }:${ this.mipLevel }`; } } /** * Cube texture node without the material's environment rotation, which requires a material and * scene context that compute passes lack. Used for point light shadow maps. * * @private */ class PlainCubeTextureNode extends CubeTextureNode { setupUV( builder, uvNode ) { if ( this.value.isDepthTexture === true ) { return vec3( uvNode.x, uvNode.y.negate(), uvNode.z ); } return vec3( uvNode.x.negate(), uvNode.yz ); } } /** * Holds the voxel representation of a scene for {@link VXGINode}: an anisotropic opacity * mip chain, a direct radiance volume and a radiance volume with cached bounces. * * The representation is a dense variant of the pre-filtered voxel hierarchy of Crassin et al. 2011 * (a dense mip chain instead of a sparse octree, which keeps cone samples to two texture fetches): * opacity stores visibility per major axis and is filtered directionally (volumetric integration * along the axis, averaging across it), radiance is stored opacity-premultiplied and indirect bounces * are cached in the volume via cone tracing. Only direct lights are injected. * Voxelization uses conservative rasterization along the dominant triangle axis in a compute shader. * * Direct light is injected per voxel instead of splatting photons from a light-view map as in the * paper: the shadow maps rendered by the renderer are the light-view maps, and every occupied voxel * pulls its visibility from them (2D maps for directional and spot lights, cube maps for point * lights) and evaluates its irradiance analytically. This reuses the existing shadow passes, makes * the injected shadows match the direct lighting exactly and needs neither atomics nor a * normalization by photon density. Lights without a shadow map fall back to a visibility cone * traced through the volume. The trade-off is a cost proportional to the number of occupied voxels * times lights rather than to the light-view resolution, and that only outgoing diffuse radiance * is stored (no incoming direction distribution for glossy cones). * * References: * - {@link https://research.nvidia.com/publication/2011-09_interactive-indirect-illumination-using-voxel-cone-tracing}: * Crassin et al., Interactive Indirect Illumination Using Voxel Cone Tracing, Pacific Graphics 2011. * - {@link https://developer.nvidia.com/content/basics-gpu-voxelization}: Basics of GPU voxelization. * * Note: This class can only be used with `WebGPURenderer` and a WebGPU backend. * * @three_import import { VXGIVolume } from 'three/addons/lighting/vxgi/VXGIVolume.js'; */ class VXGIVolume { /** * Constructs a new volume. * * @param {number} [resolution=128] - Number of voxels along the longest axis of the bounds. Should not exceed `256`, higher values exceed the maximum storage buffer size of the voxelizer. */ constructor( resolution = 128 ) { /** * Number of voxels along the longest axis of the bounds. Should not exceed `256`, higher values exceed the maximum storage buffer size of the voxelizer. * * @type {number} * @default 128 */ this.resolution = resolution; /** * The requested world-space bounds of the volume. If empty (the default), the bounds are * computed from the scene at voxelization. See {@link VXGIVolume#worldBounds} for the * effective bounds. * * @type {Box3} */ this.bounds = new Box3(); /** * The effective world-space bounds of the voxel grid, updated at voxelization. * * @type {Box3} * @readonly */ this.worldBounds = new Box3(); /** * Only meshes that pass this layer test are voxelized. * * @type {Layers} */ this.layers = new Layers(); /** * Number of cached indirect bounces stored in the volume. Should be in the range `[0, 2]`. * * @type {number} * @default 1 */ this.bounces = 1; /** * Triangles with a lower opacity are not voxelized. * * @type {number} * @default 0.1 */ this.minOpacity = 0.1; /** * Whether the coarser radiance levels are filtered directionally: along each axis the finer * voxels are composited front to back using their surface normals, so a cone only gathers * the surfaces facing it. This reduces light bleeding through thin walls and floors (e.g. a * sunlit floor brightening the ceiling of the room below) at the cost of additional memory * and a more expensive radiance lookup. Changing it triggers a re-voxelization. * * @type {boolean} * @default false */ this.directionalRadiance = false; /** * Maximum number of lights injected into the volume. * * @type {number} * @default 8 */ this.maxLights = 8; /** * Set to `true` to re-voxelize the scene in the next update. * * @type {boolean} * @default true */ this.needsUpdate = true; /** * Set to `true` to re-inject lighting in the next update. Changes of the lights and of the * injection parameters are detected automatically, so this is rarely needed. * * @type {boolean} * @default true */ this.lightingNeedsUpdate = true; /** * Maximum cone length in world units. `0` means unbounded. * * @type {UniformNode<float>} * @default 0 */ this.maxDistance = uniform( 0 ); /** * Step size relative to the texel size of the sampled mip level. * * @type {UniformNode<float>} * @default 0.5 */ this.stepScale = uniform( 0.5 ); /** * Aperture in degrees of the visibility cones traced towards lights that do not provide a * shadow map. Wider cones are cheaper but soften the injected shadows. * * @type {UniformNode<float>} * @default 10 */ this.shadowConeAngle = uniform( 10 ); /** * Aperture of the cones used for the cached bounces in degrees. * * @type {UniformNode<float>} * @default 60 */ this.bounceConeAngle = uniform( 60 ); /** * The minimum corner of the volume. * * @type {UniformNode<vec3>} */ this.boundsMinNode = uniform( new Vector3() ); /** * The size of the volume. * * @type {UniformNode<vec3>} */ this.volumeSizeNode = uniform( new Vector3( 1, 1, 1 ) ); /** * The size of a voxel. * * @type {UniformNode<float>} */ this.voxelSizeNode = uniform( 1 ); /** * The highest valid mip level of the voxel textures. * * @type {UniformNode<float>} */ this.maxLevelNode = uniform( 0 ); /** * The per-axis opacity of the scene (`xyz`) and the occupancy (`w`) as a mip chain. * * @type {Storage3DTexture} */ this.opacityTexture = this._createTexture( 'VXGI.Opacity', UnsignedByteType, true, 1, 1, 1 ); /** * The radiance of the scene including cached bounces, premultiplied by occupancy, as a mip chain. * With {@link VXGIVolume#directionalRadiance} only the finest level is used and the coarser * levels live in {@link VXGIVolume#directionalTexture}. * * @type {Storage3DTexture} */ this.radianceTexture = this._createTexture( 'VXGI.Radiance', HalfFloatType, true, 1, 1, 1 ); /** * Texture node of {@link VXGIVolume#opacityTexture}. Stays valid across re-allocations of the volume. * * @type {Texture3DNode} */ this.opacityNode = texture3D( this.opacityTexture ); /** * Texture node of {@link VXGIVolume#radianceTexture}. Stays valid across re-allocations of the volume. * * @type {Texture3DNode} */ this.radianceNode = texture3D( this.radianceTexture ); /** * The coarser radiance levels filtered directionally for the six directions a ray can travel * (+x, -x, +y, -y, +z, -z), only allocated with {@link VXGIVolume#directionalRadiance}. Each * texel holds the radiance of the surfaces facing the direction premultiplied by their weight * (`rgb`) and the weight (`a`); occlusion comes from the opacity mip chain. The six directions * are stored side by side along x in one half-resolution mip chain; level `n` of the volume is * its level `n - 1`. * * @type {Storage3DTexture} */ this.directionalTexture = this._createTexture( 'VXGI.Directional', HalfFloatType, true, 6, 1, 1 ); /** * Texture node of {@link VXGIVolume#directionalTexture}. Stays valid across re-allocations of the volume. * * @type {Texture3DNode} */ this.directionalNode = texture3D( this.directionalTexture ); /** * The width of one direction block of the directional texture in texels (at its level 0). * * @type {UniformNode<float>} */ this.directionalWidthNode = uniform( 1 ); // private this._traceDistance = this.maxDistance.greaterThan( 0 ).select( this.maxDistance, float( 1e10 ) ); this._allocated = false; this._gridSize = new Vector3(); this._levels = 1; this._voxelCount = 0; this._gridSizeNode = [ uniform( 1, 'uint' ), uniform( 1, 'uint' ), uniform( 1, 'uint' ) ]; this._voxelCountNode = uniform( 0, 'uint' ); this._pingPongTexture = null; this._directTexture = null; this._pingPongDirectionalTexture = null; this._normalTexture = null; this._directional = false; this._occupancyAttribute = null; this._triangleIdAttribute = null; this._triangleAttribute = null; this._trianglesNode = null; this._triangleCount = 0; this._triangleCountNode = uniform( 0, 'uint' ); this._kernels = null; this._lightsArray = []; for ( let i = 0; i < this.maxLights * 4; i ++ ) this._lightsArray.push( new Vector4() ); this._lightsNode = uniformArray( this._lightsArray, 'vec4' ); this._lightCountNode = uniform( 0, 'uint' ); this._lightKey = ''; // per light slot: type, shadow map (2D depth or cube depth) and its matrix / bias / near / far this._lightTypes = []; this._lightShadows = []; this._shadowMatrices = []; this._shadowParams = []; for ( let i = 0; i < this.maxLights; i ++ ) { this._shadowMatrices.push( uniform( new Matrix4() ) ); this._shadowParams.push( uniform( new Vector4() ) ); } this._lightsKey = ''; this._exclude = new Set(); } /** * The effective maximum cone length as a node. * * @type {Node<float>} */ get traceDistanceNode() { return this._traceDistance; } /** * Updates the volume if required. Voxelizes the scene when `needsUpdate` is set and re-injects * lighting when `lightingNeedsUpdate` is set or a light has changed. * * @param {Renderer} renderer - The renderer. * @param {Scene} scene - The scene. */ update( renderer, scene ) { _rendererState = RendererUtils.resetRendererState( renderer, _rendererState ); // the volume might be updated before the scene was rendered for the first time if ( scene.matrixWorldAutoUpdate === true ) scene.updateMatrixWorld(); if ( this.needsUpdate === true || this._allocated === false || this._directional !== this.directionalRadiance ) { this._voxelize( renderer, scene ); this.needsUpdate = false; this.lightingNeedsUpdate = true; } const lightKey = this._collectLights( renderer, scene ); if ( this.lightingNeedsUpdate === true || lightKey !== this._lightKey ) { this._lightKey = lightKey; this._updateLighting( renderer ); this.lightingNeedsUpdate = false; } RendererUtils.restoreRendererState( renderer, _rendererState ); } /** * Frees internal resources. */ dispose() { this._disposeGrid(); } // geometry _voxelize( renderer, scene ) { this._exclude.clear(); const bounds = this.bounds.isEmpty() ? computeSceneBounds( scene, this.layers, this._exclude, _box ) : _box.copy( this.bounds ); if ( bounds.isEmpty() ) bounds.min.set( - 1, - 1, - 1 ), bounds.max.set( 1, 1, 1 ); // grid setup: the longest axis gets `resolution` voxels, all dimensions are padded by // one voxel and rounded up so every mip level has integer dimensions bounds.getSize( _size ); const resolution = Math.max( 8, this.resolution ); const voxelSize = Math.max( _size.x, _size.y, _size.z ) / resolution; let levels = MathUtils.clamp( Math.floor( Math.log2( resolution ) ) - 2, 1, 8 ); const multiple = 2 ** ( levels - 1 ); const gridSize = _size.clone(); gridSize.x = Math.ceil( ( Math.ceil( _size.x / voxelSize ) + 2 ) / multiple ) * multiple; gridSize.y = Math.ceil( ( Math.ceil( _size.y / voxelSize ) + 2 ) / multiple ) * multiple; gridSize.z = Math.ceil( ( Math.ceil( _size.z / voxelSize ) + 2 ) / multiple ) * multiple; levels = Math.min( levels, Math.floor( Math.log2( Math.max( gridSize.x, gridSize.y ) ) ) + 1 ); this.boundsMinNode.value.copy( bounds.min ).subScalar( voxelSize ); this.volumeSizeNode.value.copy( gridSize ).multiplyScalar( voxelSize ); this.worldBounds.min.copy( this.boundsMinNode.value ); this.worldBounds.max.copy( this.boundsMinNode.value ).add( this.volumeSizeNode.value ); this.voxelSizeNode.value = voxelSize; this.maxLevelNode.value = levels - 1; if ( gridSize.equals( this._gridSize ) === false || levels !== this._levels || this._directional !== this.directionalRadiance ) { this._allocateGrid( gridSize, levels ); } // collect triangles _box.min.copy( this.boundsMinNode.value ); _box.max.copy( this.boundsMinNode.value ).add( this.volumeSizeNode.value ); const { data, count } = collectSceneTriangles( scene, { bounds: _box, layers: this.layers, exclude: this._exclude, subVoxelSize: voxelSize * 0.5, maxEdge: MAX_EDGE_SUBVOXELS, minOpacity: this.minOpacity } ); this._triangleCount = count; this._triangleCountNode.value = count; this._triangleAttribute = new StorageBufferAttribute( count > 0 ? data : new Float32Array( TRIANGLE_STRIDE ), 4 ); this._trianglesNode = storage( this._triangleAttribute, 'vec4', this._triangleAttribute.count ).toReadOnly(); // kernels depend on the triangle buffer and grid this._kernels = null; const kernels = this._getKernels(); renderer.compute( kernels.clear ); if ( count > 0 ) renderer.compute( kernels.voxelize ); renderer.compute( kernels.resolve ); for ( const kernel of kernels.opacityMips ) renderer.compute( kernel ); } _allocateGrid( gridSize, levels ) { this._disposeGrid(); const { x, y, z } = gridSize; this._gridSize.copy( gridSize ); this._levels = levels; this._voxelCount = x * y * z; this._gridSizeNode[ 0 ].value = x; this._gridSizeNode[ 1 ].value = y; this._gridSizeNode[ 2 ].value = z; this._voxelCountNode.value = this._voxelCount; const directional = this._directional = this.directionalRadiance; this.opacityTexture = this._createTexture( 'VXGI.Opacity', UnsignedByteType, true, x, y, z ); this.radianceTexture = this._createTexture( 'VXGI.Radiance', HalfFloatType, directional === false, x, y, z ); this._pingPongTexture = this._createTexture( 'VXGI.RadiancePingPong', HalfFloatType, directional === false, x, y, z ); this._directTexture = this._createTexture( 'VXGI.Direct', HalfFloatType, false, x, y, z ); // the directional levels start at level 1, i.e. at half resolution, six direction blocks along x if ( directional === true ) { const hx = Math.ceil( x / 2 ), hy = Math.ceil( y / 2 ), hz = Math.ceil( z / 2 ); this.directionalTexture = this._createTexture( 'VXGI.Directional', HalfFloatType, true, 6 * hx, hy, hz ); this._pingPongDirectionalTexture = this._createTexture( 'VXGI.DirectionalPingPong', HalfFloatType, true, 6 * hx, hy, hz ); this._normalTexture = this._createTexture( 'VXGI.Normal', UnsignedByteType, false, x, y, z ); this.directionalWidthNode.value = hx; } else { this.directionalTexture = this._createTexture( 'VXGI.Directional', HalfFloatType, true, 6, 1, 1 ); this.directionalWidthNode.value = 1; } this.opacityNode.value = this.opacityTexture; this.radianceNode.value = this.radianceTexture; this.directionalNode.value = this.directionalTexture; this._allocated = true; this._occupancyAttribute = new StorageBufferAttribute( this._voxelCount, 1, Uint32Array ); this._triangleIdAttribute = new StorageBufferAttribute( this._voxelCount, 1, Uint32Array ); } _createTexture( name, type, mipmaps, x, y, z ) { const texture = new Storage3DTexture( x, y, z ); texture.name = name; texture.type = type; texture.format = RGBAFormat; texture.generateMipmaps = mipmaps; texture.mipmapsAutoUpdate = false; texture.minFilter = mipmaps ? LinearMipmapLinearFilter : LinearFilter; texture.magFilter = LinearFilter; texture.wrapS = texture.wrapT = texture.wrapR = ClampToEdgeWrapping; return texture; } _disposeGrid() { this.opacityTexture.dispose(); this.radianceTexture.dispose(); this.directionalTexture.dispose(); if ( this._pingPongTexture !== null ) { this._pingPongTexture.dispose(); this._directTexture.dispose(); this._pingPongTexture = null; this._directTexture = null; } if ( this._normalTexture !== null ) { this._normalTexture.dispose(); this._pingPongDirectionalTexture.dispose(); this._normalTexture = null; this._pingPongDirectionalTexture = null; } this._occupancyAttribute = null; this._triangleIdAttribute = null; this._kernels = null; this._allocated = false; } // lighting _collectLights( renderer, scene ) { const lights = this._lightsArray; let count = 0; let key = renderer.shadowMap.enabled ? 'S' : 'N'; this._lightTypes.length = 0; this._lightShadows.length = 0; scene.traverseVisible( ( object ) => { if ( object.isLight !== true || count >= this.maxLights ) return; let type; if ( object.isDirectionalLight === true ) type = 0; else if ( object.isSpotLight === true ) type = 2; else if ( object.isPointLight === true ) type = 1; else return; const l0 = lights[ count * 4 ], l1 = lights[ count * 4 + 1 ], l2 = lights[ count * 4 + 2 ], l3 = lights[ count * 4 + 3 ]; _position.setFromMatrixPosition( object.matrixWorld ); l0.set( _position.x, _position.y, _position.z, type ); if ( type === 0 || type === 2 ) { _target.setFromMatrixPosition( object.target.matrixWorld ); if ( type === 0 ) { _target.subVectors( _position, _target ).normalize(); // direction towards the light } else { _target.subVectors( _target, _position ).normalize(); // spot axis } } else { _target.set( 0, 0, 0 ); } l1.set( _target.x, _target.y, _target.z, object.distance || 0 ); const color = object.color, intensity = object.intensity; l2.set( color.r * intensity, color.g * intensity, color.b * intensity, object.decay !== undefined ? object.decay : 2 ); // the light's shadow map is used for the injected visibility when available let shadowTexture = null; if ( object.castShadow === true && renderer.shadowMap.enabled === true && object.shadow.map !== null && object.shadow.map.depthTexture !== undefined ) { const shadow = object.shadow; shadowTexture = shadow.map.depthTexture; this._shadowMatrices[ count ].value.copy( shadow.matrix ); this._shadowParams[ count ].value.set( shadow.bias, shadow.camera.near, shadow.camera.far, 0 ); } this._lightTypes.push( type ); this._lightShadows.push( shadowTexture ); if ( type === 2 ) { l3.set( Math.cos( object.angle ), Math.cos( object.angle * ( 1 - object.penumbra ) ), 0, 0 ); } else { l3.set( 0, 0, 0, 0 ); } key += `|${ type },${ l0.x.toFixed( 3 ) },${ l0.y.toFixed( 3 ) },${ l0.z.toFixed( 3 ) },${ l1.x.toFixed( 4 ) },${ l1.y.toFixed( 4 ) },${ l1.z.toFixed( 4 ) },${ l1.w },${ l2.x.toFixed( 3 ) },${ l2.y.toFixed( 3 ) },${ l2.z.toFixed( 3 ) },${ l2.w },${ l3.x.toFixed( 4 ) },${ l3.y.toFixed( 4 ) },${ shadowTexture !== null ? shadowTexture.uuid : '-' }`; count ++; } ); this._lightCountNode.value = count; // injection parameters that are baked into the cached radiance key += `|${ this.bounces },${ this.bounceConeAngle.value },${ this.shadowConeAngle.value },${ this.stepScale.value },${ this.maxDistance.value }`; // the inject kernels are specialized for the set of lights and their shadow maps this._lightsKey = this._lightTypes.map( ( type, i ) => type + ':' + ( this._lightShadows[ i ] !== null ? this._lightShadows[ i ].uuid : '-' ) ).join( '|' ); return key; } _updateLighting( renderer ) { const kernels = this._getKernels(); const bounces = Math.max( 0, Math.round( this.bounces ) ); // the ping-pong order is chosen so the final result always ends up in `radianceTexture` let index = bounces % 2; if ( kernels.inject[ index ] === null || kernels.injectKey[ index ] !== this._lightsKey ) { kernels.inject[ index ] = this._createInjectKernel( renderer, index ); kernels.injectKey[ index ] = this._lightsKey; } renderer.compute( kernels.inject[ index ] ); for ( const kernel of kernels.radianceMips[ index ] ) renderer.compute( kernel ); for ( let i = 0; i < bounces; i ++ ) { const target = 1 - index; renderer.compute( kernels.bounce[ target ] ); for ( const kernel of kernels.radianceMips[ target ] ) renderer.compute( kernel ); index = target; } } // kernels _getRadianceTexture( index ) { return index === 0 ? this.radianceTexture : this._pingPongTexture; } _getDirectionalTexture( index ) { return index === 0 ? this.directionalTexture : this._pingPongDirectionalTexture; } _getKernels() { if ( this._kernels !== null ) return this._kernels; const kernels = { clear: this._createClearKernel(), voxelize: this._createVoxelizeKernel(), resolve: this._createResolveKernel(), opacityMips: [], radianceMips: [[], []], inject: [ null, null ], injectKey: [ null, null ], bounce: [ null, null ] }; for ( let level = 1; level < this._levels; level ++ ) { kernels.opacityMips.push( this._createOpacityMipKernel( level ) ); // the coarser radiance levels are either isotropic mips or directionally filtered for ( let index = 0; index < 2; index ++ ) { kernels.radianceMips[ index ].push( this._directional === true ? this._createDirectionalKernel( index, level ) : this._createRadianceMipKernel( this._getRadianceTexture( index ), level ) ); } } kernels.bounce[ 0 ] = this._createBounceKernel( 0 ); kernels.bounce[ 1 ] = this._createBounceKernel( 1 ); this._kernels = kernels; return kernels; } _getLevelSize( level ) { return [ this._gridSize.x >> level, this._gridSize.y >> level, this._gridSize.z >> level ]; } /** * Returns the 3D coordinates of the given linear voxel index. * * @private */ _coords( index, size ) { const [ x, y ] = size; return uvec3( index.mod( uint( x ) ), index.div( uint( x ) ).mod( uint( y ) ), index.div( uint( x * y ) ) ); } _createClearKernel() { const occupancy = storage( this._occupancyAttribute, 'uint', this._voxelCount ); const triangleIds = storage( this._triangleIdAttribute, 'uint', this._voxelCount ); const voxelCount = this._voxelCountNode; return Fn( () => { If( instanceIndex.lessThan( voxelCount ), () => { occupancy.element( instanceIndex ).assign( uint( 0 ) ); triangleIds.element( instanceIndex ).assign( uint( 0 ) ); } ); } )().compute( this._voxelCount ).setName( 'VXGI.Clear' ); } _createVoxelizeKernel() { const occupancy = storage( this._occupancyAttribute, 'uint', this._voxelCount ).toAtomic(); const triangleIds = storage( this._triangleIdAttribute, 'uint', this._voxelCount ); const triangles = this._trianglesNode; const triangleCount = this._triangleCountNode; const boundsMin = this.boundsMinNode; const invSubVoxel = float( 2 ).div( this.voxelSizeNode ); const [ gx, gy ] = this._gridSizeNode; const subSize = vec3( this._gridSize.x * 2, this._gridSize.y * 2, this._gridSize.z * 2 ); return Fn( () => { const triangleIndex = instanceIndex; If( triangleIndex.lessThan( triangleCount ), () => { const base = triangleIndex.mul( 5 ).toConst(); // triangle in sub-voxel space const p0 = triangles.element( base ).xyz.sub( boundsMin ).mul( invSubVoxel ).toConst(); const p1 = triangles.element( base.add( 1 ) ).xyz.sub( boundsMin ).mul( invSubVoxel ).toConst(); const p2 = triangles.element( base.add( 2 ) ).xyz.sub( boundsMin ).mul( invSubVoxel ).toConst(); const n = cross( p1.sub( p0 ), p2.sub( p0 ) ).toConst(); const an = abs( n ).toConst(); // project along the dominant axis: swizzle so the dominant axis becomes z const isZ = an.z.greaterThanEqual( an.x ).and( an.z.greaterThanEqual( an.y ) ).toConst(); const isY = isZ.not().and( an.y.greaterThanEqual( an.x ) ).toConst(); const swizzle = ( v ) => select( isZ, v.xyz, select( isY, v.zxy, v.yzx ) ); const q0 = swizzle( p0 ).toConst(); const q1 = swizzle( p1 ).toConst(); const q2 = swizzle( p2 ).toConst(); const nq = swizzle( n ).toConst(); const sd = swizzle( subSize ).toConst(); const qmin = min( q0, min( q1, q2 ) ).toConst(); const qmax = max( q0, max( q1, q2 ) ).toConst(); const i0 = int( floor( qmin.x ) ).max( 0 ).toConst(); const i1 = int( floor( qmax.x ) ).min( int( sd.x ).sub( 1 ) ).toConst(); const j0 = int( floor( qmin.y ) ).max( 0 ).toConst(); const j1 = int( floor( qmax.y ) ).min( int( sd.y ).sub( 1 ) ).toConst(); // conservative edge functions const areaSign = sign( nq.z ).toConst(); const edge = ( a, b ) => { const normal = vec2( a.y.sub( b.y ), b.x.sub( a.x ) ).mul( areaSign ).toConst(); const bias = float( 0.5 ).mul( abs( normal.x ).add( abs( normal.y ) ) ).toConst(); return { a, normal, bias }; }; const e0 = edge( q0.xy, q1.xy ); const e1 = edge( q1.xy, q2.xy ); const e2 = edge( q2.xy, q0.xy ); const halfExtent = float( 0.5 ).mul( abs( nq.x ).add( abs( nq.y ) ) ).div( abs( nq.z ) ).toConst(); Loop( { start: i0, end: i1, type: 'int', condition: '<=', name: 'i' }, { start: j0, end: j1, type: 'int', condition: '<=', name: 'j' }, ( { i, j } ) => { const c = vec2( float( i ).add( 0.5 ), float( j ).add( 0.5 ) ).toConst(); const inside = dot( e0.normal, c.sub( e0.a ) ).add( e0.bias ).greaterThanEqual( 0 ) .and( dot( e1.normal, c.sub( e1.a ) ).add( e1.bias ).greaterThanEqual( 0 ) ) .and( dot( e2.normal, c.sub( e2.a ) ).add( e2.bias ).greaterThanEqual( 0 ) ); If( inside, () => { // depth range of the triangle plane within this column const wc = q0.z.sub( nq.x.mul( c.x.sub( q0.x ) ).add( nq.y.mul( c.y.sub( q0.y ) ) ).div( nq.z ) ).toConst(); const k0 = int( floor( max( wc.sub( halfExtent ), qmin.z ) ) ).max( 0 ).toConst(); const k1 = int( floor( min( wc.add( halfExtent ), qmax.z ) ) ).min( int( sd.z ).sub( 1 ) ).toConst(); Loop( { start: k0, end: k1, type: 'int', condition: '<=', name: 'k' }, ( { k } ) => { const s = select( isZ, ivec3( i, j, k ), select( isY, ivec3( j, k, i ), ivec3( k, i, j ) ) ).toConst(); const voxel = uvec3( s.div( 2 ) ).toConst(); const bit = uint( s.x.bitAnd( 1 ) ).bitOr( uint( s.y.bitAnd( 1 ) ).shiftLeft( uint( 1 ) ) ).bitOr( uint( s.z.bitAnd( 1 ) ).shiftLeft( uint( 2 ) ) ).toConst(); const voxelIndex = voxel.x.add( gx.mul( voxel.y.add( gy.mul( voxel.z ) ) ) ).toConst(); atomicOr( occupancy.element( voxelIndex ), uint( 1 ).shiftLeft( bit ) ); triangleIds.element( voxelIndex ).assign( triangleIndex.add( 1 ) ); } ); } ); } ); } ); } )().compute( this._triangleCount ).setName( 'VXGI.Voxelize' ); } _createResolveKernel() { const occupancy = storage( this._occupancyAttribute, 'uint', this._voxelCount ).toReadOnly(); const opacityTexture = this.opacityTexture; const voxelCount = this._voxelCountNode; const size = this._getLevelSize( 0 ); return Fn( () => { If( instanceIndex.lessThan( voxelCount ), () => { const coords = this._coords( instanceIndex, size ).toConst(); const bits = occupancy.element( instanceIndex ).toConst(); // per-axis coverage from the 2x2x2 sub-voxel occupancy const coverage = ( m0, m1, m2, m3 ) => { return select( bits.bitAnd( uint( m0 ) ).notEqual( uint( 0 ) ), 0.25, 0 ) .add( select( bits.bitAnd( uint( m1 ) ).notEqual( uint( 0 ) ), 0.25, 0 ) ) .add( select( bits.bitAnd( uint( m2 ) ).notEqual( uint( 0 ) ), 0.25, 0 ) ) .add( select( bits.bitAnd( uint( m3 ) ).notEqual( uint( 0 ) ), 0.25, 0 ) ); }; const opacityX = coverage( 0x03, 0x0C, 0x30, 0xC0 ); const opacityY = coverage( 0x05, 0x0A, 0x50, 0xA0 ); const opacityZ = coverage( 0x11, 0x22, 0x44, 0x88 ); const occupied = float( countOneBits( bits ) ).div( 8 ); textureStore( opacityTexture, coords, vec4( opacityX, opacityY, opacityZ, occupied ) ); } ); } )().compute( this._voxelCount ).setName( 'VXGI.Resolve' ); } _createOpacityMipKernel( level ) { const size = this._getLevelSize( level ); const count = size[ 0 ] * size[ 1 ] * size[ 2 ]; const source = new MipStorageTexture3DNode( this.opacityTexture, 'readOnly', level - 1 ); const target = new MipStorageTexture3DNode( this.opacityTexture, 'writeOnly', level ); return Fn( () => { If( instanceIndex.lessThan( uint( count ) ), () => { const coords = this._coords( instanceIndex, size ).toConst(); const base = ivec3( coords.mul( uint( 2 ) ) ).toConst(); const children = []; for ( let z = 0; z < 2; z ++ ) { for ( let y = 0; y < 2; y ++ ) { for ( let x = 0; x < 2; x ++ ) { children[ x + y * 2 + z * 4 ] = source.load( base.add( ivec3( x, y, z ) ) ).toConst(); } } } // along each axis the two children are combined, across the axis they are averaged const child = ( x, y, z ) => children[ x + y * 2 + z * 4 ]; const combine = ( a, b ) => a.oneMinus().mul( b.oneMinus() ).oneMinus(); let opacityX = float( 0 ), opacityY = float( 0 ), opacityZ = float( 0 ), occupied = float( 0 ); for ( let a = 0; a < 2; a ++ ) { for ( let b = 0; b < 2; b ++ ) { opacityX = opacityX.add( combine( child( 0, a, b ).x, child( 1, a, b ).x ) ); opacityY = opacityY.add( combine( child( a, 0, b ).y, child( a, 1, b ).y ) ); opacityZ = opacityZ.add( combine( child( a, b, 0 ).z, child( a, b, 1 ).z ) ); } } for ( let i = 0; i < 8; i ++ ) occupied = occupied.add( children[ i ].w ); textureStore( target, coords, vec4( opacityX.mul( 0.25 ), opacityY.mul( 0.25 ), opacityZ.mul( 0.25 ), occupied.mul( 0.125 ) ) ); } ); } )().compute( count ).setName( 'VXGI.OpacityMip' + level ); } _createRadianceMipKernel( radianceTexture, level ) { const size = this._getLevelSize( level ); const count = size[ 0 ] * size[ 1 ] * size[ 2 ]; const source = new MipStorageTexture3DNode( radianceTexture, 'readOnly', level - 1 ); const target = new MipStorageTexture3DNode( radianceTexture, 'writeOnly', level ); return Fn( () => { If( instanceIndex.lessThan( uint( count ) ), () => { const coords = this._coords( instanceIndex, size ).toConst(); const base = ivec3( coords.mul( uint( 2 ) ) ).toConst(); let sum = vec4( 0 ); for ( let z = 0; z < 2; z ++ ) { for ( let y = 0; y < 2; y ++ ) { for ( let x = 0; x < 2; x ++ ) { sum = sum.add( source.load( base.add( ivec3( x, y, z ) ) ) ); } } } textureStore( target, coords, sum.mul( 0.125 ) ); } ); } )().compute( count ).setName( 'VXGI.RadianceMip' + level ); } /** * Creates the kernel that filters the six direction blocks of the directional texture at a * level from the next finer level: along the direction's axis the two finer voxels are * composited front to back with the front voxel's opacity along that axis, across the axis * they are averaged. Level 1 is filtered from the isotropic finest level, where a voxel only * contributes to the directions its surface faces against. * * @private */ _createDirectionalKernel( index, level ) { const size = this._getLevelSize( level ); const count = size[ 0 ] * size[ 1 ] * size[ 2 ]; const texture = this._getDirectionalTexture( index ); const opacity = texture3D( this.opacityTexture ); const radiance = texture3D( this._getRadianceTexture( index ) ); const normals = texture3D( this._normalTexture ); // level n of the volume is level n - 1 of the half resolution directional texture const source = level === 1 ? null : new MipStorageTexture3DNode( texture, 'readOnly', level - 2 ); const target = new MipStorageTexture3DNode( texture, 'writeOnly', level - 1 ); const blockWidth = size[ 0 ]; return Fn( () => { If( instanceIndex.lessThan( uint( count ) ), () => { const coords = this._coords( instanceIndex, size ).toConst(); const base = ivec3( coords.mul( uint( 2 ) ) ).toConst(); const offset = ( i ) => ivec3( i & 1, ( i >> 1 ) & 1, ( i >> 2 ) & 1 ); const childOpacity = []; const childNormal = []; const childRadiance = []; for ( let i = 0; i < 8; i ++ ) { childOpacity.push( opacity.load( base.add( offset( i ) ) ).level( level - 1 ).toConst() ); if ( level === 1 ) { childNormal.push( normals.load( base.add( offset( i ) ) ).xyz.mul( 2 ).sub( 1 ).toConst() ); childRadiance.push( radiance.load( base.add( offset( i ) ) ).toConst() ); } } for ( let direction = 0; direction < 6; direction ++ ) { const [ axis, sign ] = DIRECTIONS[ direction ]; const travel = vec3( axis === 0 ? sign : 0, axis === 1 ? sign : 0, axis === 2 ? sign : 0 ); const block = ivec3( direction * blockWidth * 2, 0, 0 ); // block offset in the finer level // a child holds the radiance of its surfaces facing the direction premultiplied by // their weight (rgb) and the weight (a); at the finest level a voxel only counts for a // direction if its surface faces against it, so a surface parallel to the axis does // not mix in. The front child hides the back child by its opacity along the axis. const child = ( i ) => level === 1 ? childRadiance[ i ].mul( smoothstep( 0, 0.3, dot( childNormal[ i ], travel.negate() ) ) ) : source.load( base.add( block ).add( offset( i ) ) ); const axisOpacity = ( i ) => [ childOpacity[ i ].x, childOpacity[ i ].y, childOpacity[ i ].z ][ axis ]; let sum = vec4( 0 ); for ( let i = 0; i < 8; i ++ ) { // each column along the axis is composited once, starting at its front voxel if ( ( ( i >> axis ) & 1 ) !== ( sign > 0 ? 0 : 1 ) ) continue; const front = i; const back = i ^ ( 1 << axis ); sum = sum.add( child( front ) ).add( child( back ).mul( axisOpacity( front ).oneMinus() ) ); } textureStore( target, ivec3( coords ).add( ivec3( direction * blockWidth, 0, 0 ) ), sum.mul( 0.25 ) ); } } ); } )().compute( count ).setName( 'VXGI.Directional' + level ); } /** * Emits the code for reading the surface data of the given voxel. * * @private */ _surface( coords, triangleId ) { const triangles = this._trianglesNode; const base = triangleId.sub( 1 ).mul( 5 ).toConst(); const a = triangles.element( base ).xyz.toConst(); const b = triangles.element( base.add( 1 ) ).xyz.toConst(); const c = triangles.element( base.add( 2 ) ).xyz.toConst(); const albedo = triangles.element( base.add( 3 ) ).toConst(); const emissive = triangles.element( base.add( 4 ) ).xyz.toConst(); const normal = normalize( cross( b.sub( a ), c.sub( a ) ) ).toVar(); normal.assign( select( albedo.w.equal( 1 ), normal.negate(), normal ) ); // back side const position = this.boundsMinNode.add( vec3( coords ).add( 0.5 ).mul( this.voxelSizeNode ) ).toConst(); return { position, normal, albedo: albedo.xyz, side: albedo.w, emissive }; } _createInjectKernel( renderer, index ) { const occupancy = storage( this._occupancyAttribute, 'uint', this._voxelCount ).toReadOnly(); const triangleIds = storage( this._triangleIdAttribute, 'uint', this._voxelCount ).toReadOnly(); const voxelCount = this._voxelCountNode; const size = this._getLevelSize( 0 ); const voxelSize = this.voxelSizeNode; const lights = this._lightsNode; const directTexture = this._directTexture; const normalTexture = this._normalTexture; const radianceTexture = this._getRadianceTexture( index ); const lightTypes = this._lightTypes.slice(); const lightShadows = this._lightShadows.slice(); const shadowMatrices = this._shadowMatrices; const shadowParams = this._shadowParams; const reversedDepth = renderer.reversedDepthBuffer === true; const shadowTanHalfAngle = this.shadowConeAngle.mul( 0.5 ).radians().tan(); const trace = createConeTracer( this, { maxSteps: 256 } ); return Fn( () => { If( instanceIndex.lessThan( voxelCount ), () => { const coords = this._coords( instanceIndex, size ).toConst(); const bits = occupancy.element( instanceIndex ).toConst(); const result = vec4( 0 ).toVar(); const encodedNormal = normalTexture !== null ? vec4( 0 ).toVar() : null; If( bits.notEqual( uint( 0 ) ), () => { const surface = this._surface( coords, triangleIds.element( instanceIndex ) ); const { position, normal, albedo, side, emissive } = surface; if ( encodedNormal !== null ) encodedNormal.assign( vec4( normal.mul( 0.5 ).add( 0.5 ), 1 ) ); const irradiance = vec3( 0 ).toVar(); // one specialized block per light; the kernel is rebuilt when the light set changes for ( let i = 0; i < lightTypes.length; i ++ ) { const type = lightTypes[ i ]; const shadowTexture = lightShadows[ i ]; const l0 = lights.element( i * 4 ).toConst(); const l1 = lights.element( i * 4 + 1 ).toConst(); const l2 = lights.element( i * 4 + 2 ).toConst(); const l3 = lights.element( i * 4 + 3 ).toConst(); const lightDirection = vec3( 0 ).toVar(); const lightDistance = float( 1e10 ).toVar(); const attenuation = float( 1 ).toVar(); if ( type === 0 ) { lightDirection.assign( l1.xyz ); } else { const lightVector = l0.xyz.sub( position ).toConst(); lightDistance.assign( length( lightVector ) ); lightDirection.assign( lightVector.div( lightDistance ) ); attenuation.assign( getDistanceAttenuation( { lightDistance, cutoffDistance: l1.w, decayExponent: l2.w } ) ); if ( type === 2 ) { attenuation.mulAssign( smoothstep( l3.x, l3.y, dot( l1.xyz, lightDirection.negate() ) ) ); } } const ndl = dot( normal, lightDirection ).toVar(); ndl.assign( select( side.equal( 2 ), abs( ndl ), max( ndl, 0 ) ) ); If( ndl.greaterThan( 0 ).and( attenuation.greaterThan( 0 ) ), () => { const visibility = float( 1 ).toVar(); const shadowPosition = position.add( normal.mul( voxelSize ) ).toConst(); if ( shadowTexture === null ) { // no shadow map: trace a visibility cone through the volume const origin = position.add( normal.mul( voxelSize.mul( 1.5 ) ) ); const occlusion = trace( origin, lightDirection, shadowTanHalfAngle, lightDistance.sub( voxelSize ) ); visibility.assign( occlusion.alpha.oneMinus() ); } else if ( type === 1 ) { // point light: cube shadow map addressed by the light-to-voxel vector const params = shadowParams[ i ]; const bias = params.x, near = params.y, far = params.z; const lightToVoxel = shadowPosition.sub( l0.xyz ).toConst(); const absVector = abs( lightToVoxel ).toConst(); const viewZ = max( max( absVector.x, absVector.y ), absVector.z ).toConst(); If( viewZ.greaterThanEqual( near ).and( viewZ.lessThanEqual( far ) ), () => { const depth = nodeObject( new PlainCubeTextureNode( shadowTexture, normalize( lightToVoxel ) ) ).r; // depth cubes need an integer level, which the compute path emits by default if ( reversedDepth ) { const reference = viewZToReversedPerspectiveDepth( viewZ.negate(), near, far ).sub( bias ); visibility.assign( select( reference.greaterThanEqual( depth ), 1, 0 ) ); } else { const reference = viewZToPerspectiveDepth( viewZ.negate(), near, far ).add( bias ); visibility.assign( select( reference.lessThanEqual( depth ), 1, 0 ) ); } } ); } else { // directional and spot lights: 2D shadow map addressed by the shadow matrix const bias = shadowParams[ i ].x; const clip = shadowMatrices[ i ].mul( vec4( shadowPosition, 1 ) ).toConst(); const coord = clip.xyz.div( clip.w ).toConst(); const uv = vec2( coord.x, coord.y.oneMinus() ).toConst(); const inside = uv.x.greaterThanEqual( 0 ).and( uv.x.lessThanEqual( 1 ) ).and( uv.y.greaterThanEqual( 0 ) ).and( uv.y.lessThanEqual( 1 ) ).and( coord.z.greaterThanEqual( 0 ) ).and( coord.z.lessThanEqual( 1 ) ).toConst(); If( inside, () => { const depth = texture( shadowTexture, uv ).r; if ( reversedDepth ) { visibility.assign( select( coord.z.sub( bias ).greaterThanEqual( depth ), 1, 0 ) ); } else { visibility.assign( select( coord.z.add( bias ).lessThanEqual( depth ), 1, 0 ) ); } } ); } irradiance.addAssign( l2.xyz.mul( attenuation.mul( ndl ).mul( visibility ) ) ); } ); } const radiance = albedo.mul( irradiance ).div( PI ).add( emissive ); const occupied = float( countOneBits( bits ) ).div( 8 ); result.assign( vec4( radiance.mul( occupied ), occupied ) ); } ); textureStore( directTexture, coords, result ); textureStore( radianceTexture, coords, result ); if ( encodedNormal !== null ) textureStore( normalTexture, coords, encodedNormal ); } ); } )().compute( this._voxelCount ).setName( 'VXGI.Inject' ); } _createBounceKernel( index ) { const occupancy = storage( this._occupancyAttribute, 'uint', this._voxelCount ).toReadOnly(); const triangleIds = storage( this._triangleIdAttribute, 'uint', this._voxelCount ).toReadOnly(); const voxelCount = this._voxelCountNode; const size = this._getLevelSize( 0 ); const voxelSize = this.voxelSizeNode; const directTexture = this._directTexture; const sourceTexture = this._getRadianceTexture( 1 - index ); const targetTexture = this._getRadianceTexture( index ); const trace = createConeTracer( this, { radianceNode: texture3D( sourceTexture ), directionalNode: this._directional === true ? texture3D( this._getDirectionalTexture( 1 - index ) ) : null } ); const traceDistance = this._traceDistance; const tanHalfAngle = this.bounceConeAngle.mul( 0.5 ).radians().tan(); return Fn( () => { If( instanceIndex.lessThan( voxelCount ), () => { const coords = this._coords( instanceIndex, size ).toConst(); const bits = occupancy.element( instanceIndex ).toConst(); const result = vec4( 0 ).toVar(); If( bits.notEqual( uint( 0 ) ), () => { const { position, normal, albedo } = this._surface( coords, triangleIds.element( instanceIndex ) ); const direct = texture3D( directTexture, vec3( coords ).add( 0.5 ).div( vec3( size[ 0 ], size[ 1 ], size[ 2 ] ) ), float( 0 ) ).toConst(); // tangent frame const up = select( abs( normal.y ).lessThan( 0.99 ), vec3( 0, 1, 0 ), vec3( 1, 0, 0 ) ); const tangent = normalize( cross( normal, up ) ).toConst(); const bitangent = cross( normal, tangent ).toConst(); const rotation = hash( instanceIndex ).toConst(); const gathered = vec3( 0 ).toVar(); Loop( { start: 0, end: BOUNCE_CONE_COUNT, type: 'int', condition: '<', name: 'c' }, ( { c } ) => { // cosine-weighted directions, rotated per voxel const u1 = float( c ).add( 0.5 ).div( BOUNCE_CONE_COUNT ).toConst(); const u2 = fract( float( c ).mul( 0.618034 ).add( rotation ) ).toConst(); const sinTheta = sqrt( u1 ).toConst(); const cosTheta = sqrt( u1.oneMinus() ).toConst(); const phi = u2.mul( PI.mul( 2 ) ).toConst(); const direction = normalize( tangent.mul( cos( phi ).mul( sinTheta ) ).add( bitangent.mul( sin( phi ).mul( sinTheta ) ) ).add( normal.mul( cosTheta ) ) ).toConst(); const origin = position.add( normal.mul( voxelSize.mul( 1.5 ) ) ).toConst(); const cone = trace( origin, direction, tanHalfAngle, traceDistance ); gathered.addAssign( cone.color ); } ); const bounce = albedo.mul( gathered.div( BOUNCE_CONE_COUNT ) ); result.assign( vec4( direct.rgb.add( bounce.mul( direct.a ) ), direct.a ) ); } ); textureStore( targetTexture, coords, result ); } ); } )().compute( this._voxelCount ).setName( 'VXGI.Bounce' ); } } export { VXGIVolume };