Accueil / npm / three@0.186.1 / examples / jsm / lighting / vxgi / VXGIVolume.js

VXGIVolume.js

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 };