diff --git a/build/three.tsl.js b/build/three.tsl.js index b09f120b4cce9a..4984e48a465096 100644 --- a/build/three.tsl.js +++ b/build/three.tsl.js @@ -121,6 +121,7 @@ const buffer = TSL.buffer; const bufferAttribute = TSL.bufferAttribute; const builtin = TSL.builtin; const builtinAOContext = TSL.builtinAOContext; +const builtinGIContext = TSL.builtinGIContext; const builtinShadowContext = TSL.builtinShadowContext; const bumpMap = TSL.bumpMap; const bvec2 = TSL.bvec2; @@ -679,4 +680,4 @@ const workgroupId = TSL.workgroupId; const workingToColorSpace = TSL.workingToColorSpace; const xor = TSL.xor; -export { BRDF_GGX, BRDF_Lambert, BRDF_Sheen, BasicPointShadowFilter, BasicShadowFilter, Break, Const, Continue, DFGLUT, D_GGX, D_GGX_Anisotropic, Discard, EPSILON, EnvironmentBRDF, F_Schlick, Fn, HALF_PI, INFINITY, If, LTC_Evaluate, LTC_Evaluate_Volume, LTC_Uv, Loop, NodeAccess, NodeShaderStage, NodeType, NodeUpdateType, OnAfterObjectUpdate, OnAfterRenderPipeline, OnBeforeFrameUpdate, OnBeforeMaterialUpdate, OnBeforeObjectUpdate, OnBeforeRenderPipeline, OnFrameUpdate, OnMaterialUpdate, OnObjectUpdate, PCFShadowFilter, PI, PI2, PointShadowFilter, Return, Schlick_to_F0, ShaderNode, Stack, Switch, TBNViewMatrix, TWO_PI, VSMShadowFilter, V_GGX_SmithCorrelated, V_GGX_SmithCorrelated_Anisotropic, Var, VarIntent, abs, acesFilmicToneMapping, acos, acosh, add, addMethodChaining, addNodeElement, agxToneMapping, all, alphaT, ambientOcclusion, and, anisotropy, anisotropyB, anisotropyT, any, array, asin, asinh, assign, atan, atanh, atomicAdd, atomicAnd, atomicFunc, atomicLoad, atomicMax, atomicMin, atomicOr, atomicStore, atomicSub, atomicXor, attenuationColor, attenuationDistance, attribute, attributeArray, backgroundBlurriness, backgroundIntensity, backgroundRotation, batch, batchColor, batchIndirectIndex, bentNormalView, billboarding, bitAnd, bitNot, bitOr, bitXor, bitangentGeometry, bitangentLocal, bitangentView, bitangentViewFrame, bitangentWorld, bitcast, blendBurn, blendColor, blendDodge, blendOverlay, blendScreen, bool, buffer, bufferAttribute, builtin, builtinAOContext, builtinShadowContext, bumpMap, bvec2, bvec3, bvec4, bypass, cache, call, cameraFar, cameraIndex, cameraNear, cameraNormalMatrix, cameraPosition, cameraProjectionMatrix, cameraProjectionMatrixInverse, cameraViewMatrix, cameraViewport, cameraWorldMatrix, cbrt, cdl, ceil, checker, cineonToneMapping, clamp, clearcoat, clearcoatNormalView, clearcoatRoughness, clipSpace, clipping, clippingAlpha, code, color, colorSpaceToWorking, colorToDirection, compute, computeKernel, computeSkinning, context, convert, convertColorSpace, convertToTexture, cos, cosh, countLeadingZeros, countOneBits, countTrailingZeros, cross, cubeTexture, cubeTextureBase, dFdx, dFdy, dashSize, debug, decrement, decrementBefore, defaultBuildStages, defaultShaderStages, defined, degrees, deltaTime, densityFogFactor, depth, depthPass, determinant, difference, diffuseColor, diffuseContribution, directPointLight, directionToColor, directionToFaceDirection, dispersion, distance, div, dot, drawIndex, dynamicBufferAttribute, element, emissive, equal, equirectDirection, equirectUV, exp, exp2, exponentialHeightFogFactor, expression, faceDirection, faceForward, faceforward, float, floatBitsToInt, floatBitsToUint, floor, fog, fract, frameGroup, frameId, frontFacing, fwidth, gain, gapSize, getConstNodeType, getCurrentStack, getDistanceAttenuation, getGeometryRoughness, getNormalFromDepth, getParallaxCorrectNormal, getRoughness, getScreenPosition, getScreenPositionFromClip, getShIrradianceAt, getTextureIndex, getViewPosition, globalId, glsl, glslFn, grayscale, greaterThan, greaterThanEqual, hardwareClipping, hash, highpModelNormalViewMatrix, highpModelViewMatrix, hue, increment, incrementBefore, inspect, instance, instanceColor, instanceIndex, instancedArray, instancedBufferAttribute, instancedDynamicBufferAttribute, instancedMesh, int, intBitsToFloat, interleavedGradientNoise, inverse, inverseSqrt, inversesqrt, invocationLocalIndex, invocationSubgroupIndex, ior, iridescence, iridescenceIOR, iridescenceThickness, isolate, ivec2, ivec3, ivec4, js, label, length, lengthSq, lessThan, lessThanEqual, lightPosition, lightProjectionUV, lightShadowMatrix, lightTargetDirection, lightTargetPosition, lightViewPosition, lightingContext, lights, linearDepth, linearToneMapping, localId, log, log2, logarithmicDepthToViewZ, luminance, mat2, mat3, mat4, matcapUV, materialAO, materialAlphaTest, materialAnisotropy, materialAnisotropyVector, materialAttenuationColor, materialAttenuationDistance, materialClearcoat, materialClearcoatNormal, materialClearcoatRoughness, materialColor, materialDispersion, materialEmissive, materialEnvIntensity, materialEnvRotation, materialIOR, materialIridescence, materialIridescenceIOR, materialIridescenceThickness, materialLightMap, materialLineDashOffset, materialLineDashSize, materialLineGapSize, materialLineScale, materialLineWidth, materialMetalness, materialNormal, materialOpacity, materialPointSize, materialReference, materialReflectivity, materialRefractionRatio, materialRetroreflectivity, materialRotation, materialRoughness, materialSheen, materialSheenRoughness, materialShininess, materialSpecular, materialSpecularColor, materialSpecularIntensity, materialSpecularStrength, materialThickness, materialTransmission, max, maxMipLevel, mediumpModelViewMatrix, metalness, min, mix, mixElement, mod, modelDirection, modelNormalMatrix, modelPosition, modelRadius, modelScale, modelViewMatrix, modelViewPosition, modelViewProjection, modelWorldMatrix, modelWorldMatrixInverse, morphReference, mrt, mul, mx_aastep, mx_add, mx_atan2, mx_cell_noise_float, mx_cell_noise_vec3, mx_contrast, mx_divide, mx_fractal_noise_float, mx_fractal_noise_float_2d, mx_fractal_noise_vec2, mx_fractal_noise_vec3, mx_fractal_noise_vec4, mx_frame, mx_heighttonormal, mx_hsvtorgb, mx_ifequal, mx_ifgreater, mx_ifgreatereq, mx_invert, mx_modulo, mx_multiply, mx_noise_float, mx_noise_vec3, mx_noise_vec4, mx_place2d, mx_power, mx_ramp4, mx_ramplr, mx_ramptb, mx_rgbtohsv, mx_rotate2d, mx_rotate3d, mx_safepower, mx_separate, mx_smoothstep, mx_splitlr, mx_splittb, mx_srgb_texture_to_lin_rec709, mx_subtract, mx_timer, mx_transform_uv, mx_unifiednoise2d, mx_unifiednoise3d, mx_worley_noise_float, mx_worley_noise_float_2d, mx_worley_noise_float_3d, mx_worley_noise_vec2, mx_worley_noise_vec3, mx_worley_noise_vec3_style, negate, negateOnBackSide, neutralToneMapping, nodeArray, nodeImmutable, nodeObject, nodeObjectIntent, nodeObjects, nodeProxy, nodeProxyConstructor, nodeProxyIntent, normalFlat, normalGeometry, normalLocal, normalMap, normalView, normalViewGeometry, normalWorld, normalWorldGeometry, normalize, not, notEqual, numWorkgroups, objectDirection, objectGroup, objectPosition, objectRadius, objectScale, objectViewPosition, objectWorldMatrix, oneMinus, or, orthographicDepthToViewZ, oscSawtooth, oscSine, oscSquare, oscTriangle, output, outputStruct, overloadingFn, overrideNode, overrideNodes, packHalf2x16, packNormalToRGB, packSnorm2x16, packSnorm4x8, packUnorm2x16, packUnorm4x8, parabola, parallaxDirection, parallaxUV, parameter, pass, passTexture, pcurve, perspectiveDepthToViewZ, pmremTexture, pointShadow, pointUV, pointWidth, positionGeometry, positionLocal, positionPrevious, positionView, positionViewDirection, positionWorld, positionWorldDirection, posterize, pow, pow2, pow3, pow4, premultiplyAlpha, property, quadBroadcast, quadSwapDiagonal, quadSwapX, quadSwapY, radians, rand, range, rangeFogFactor, reciprocal, reference, referenceBuffer, reflect, reflectVector, reflectView, reflector, refract, refractVector, refractView, reinhardToneMapping, remap, remapClamp, renderGroup, renderOutput, rendererReference, replaceDefaultUV, retroreflectivity, rotate, rotateUV, roughness, round, rtt, sRGBTransferEOTF, sRGBTransferOETF, sample, sampler, samplerComparison, saturate, saturation, screenCoordinate, screenDPR, screenSize, screenUV, select, setCurrentStack, setName, shaderStages, shadow, shadowPositionWorld, shapeCircle, sharedUniformGroup, sheen, sheenRoughness, shiftLeft, shiftRight, shininess, sign, sin, sinc, sinh, skinning, smoothstep, smoothstepElement, specularColor, specularColorBlended, specularF90, spherizeUV, split, spritesheetUV, sqrt, stack, step, stepElement, storage, storageBarrier, storageElement, storageTexture, storageTexture3D, struct, sub, subBuild, subgroupAdd, subgroupAll, subgroupAnd, subgroupAny, subgroupBallot, subgroupBroadcast, subgroupBroadcastFirst, subgroupElect, subgroupExclusiveAdd, subgroupExclusiveMul, subgroupInclusiveAdd, subgroupInclusiveMul, subgroupIndex, subgroupMax, subgroupMin, subgroupMul, subgroupOr, subgroupShuffle, subgroupShuffleDown, subgroupShuffleUp, subgroupShuffleXor, subgroupSize, subgroupXor, tan, tangentGeometry, tangentLocal, tangentView, tangentViewFrame, tangentWorld, tanh, texture, texture3D, texture3DLevel, texture3DLoad, textureBarrier, textureBicubic, textureBicubicLevel, textureLevel, textureLoad, textureSize, textureStore, thickness, time, toneMapping, toneMappingExposure, toonOutlinePass, transformDirection, transformNormal, transformNormalByInverseViewMatrix, transformNormalByViewMatrix, transformNormalToView, transformedClearcoatNormalView, transformedNormalView, transformedNormalWorld, transmission, transpose, triNoise3D, triplanarTexture, triplanarTextures, trunc, uint, uintBitsToFloat, uniform, uniformArray, uniformCubeTexture, uniformFlow, uniformGroup, uniformTexture, unpackHalf2x16, unpackNormal, unpackRGBToNormal, unpackSnorm2x16, unpackSnorm4x8, unpackUnorm2x16, unpackUnorm4x8, unpremultiplyAlpha, userData, uv, uvec2, uvec3, uvec4, varying, varyingProperty, vec2, vec3, vec4, vectorComponents, velocity, vertexColor, vertexIndex, vertexStage, vibrance, viewZToLogarithmicDepth, viewZToOrthographicDepth, viewZToPerspectiveDepth, viewZToReversedOrthographicDepth, viewZToReversedPerspectiveDepth, viewport, viewportCoordinate, viewportDepthTexture, viewportLinearDepth, viewportMipTexture, viewportOpaqueMipTexture, viewportSafeUV, viewportSharedTexture, viewportSize, viewportTexture, viewportUV, vogelDiskSample, wgsl, wgslFn, workgroupArray, workgroupBarrier, workgroupId, workingToColorSpace, xor }; +export { BRDF_GGX, BRDF_Lambert, BRDF_Sheen, BasicPointShadowFilter, BasicShadowFilter, Break, Const, Continue, DFGLUT, D_GGX, D_GGX_Anisotropic, Discard, EPSILON, EnvironmentBRDF, F_Schlick, Fn, HALF_PI, INFINITY, If, LTC_Evaluate, LTC_Evaluate_Volume, LTC_Uv, Loop, NodeAccess, NodeShaderStage, NodeType, NodeUpdateType, OnAfterObjectUpdate, OnAfterRenderPipeline, OnBeforeFrameUpdate, OnBeforeMaterialUpdate, OnBeforeObjectUpdate, OnBeforeRenderPipeline, OnFrameUpdate, OnMaterialUpdate, OnObjectUpdate, PCFShadowFilter, PI, PI2, PointShadowFilter, Return, Schlick_to_F0, ShaderNode, Stack, Switch, TBNViewMatrix, TWO_PI, VSMShadowFilter, V_GGX_SmithCorrelated, V_GGX_SmithCorrelated_Anisotropic, Var, VarIntent, abs, acesFilmicToneMapping, acos, acosh, add, addMethodChaining, addNodeElement, agxToneMapping, all, alphaT, ambientOcclusion, and, anisotropy, anisotropyB, anisotropyT, any, array, asin, asinh, assign, atan, atanh, atomicAdd, atomicAnd, atomicFunc, atomicLoad, atomicMax, atomicMin, atomicOr, atomicStore, atomicSub, atomicXor, attenuationColor, attenuationDistance, attribute, attributeArray, backgroundBlurriness, backgroundIntensity, backgroundRotation, batch, batchColor, batchIndirectIndex, bentNormalView, billboarding, bitAnd, bitNot, bitOr, bitXor, bitangentGeometry, bitangentLocal, bitangentView, bitangentViewFrame, bitangentWorld, bitcast, blendBurn, blendColor, blendDodge, blendOverlay, blendScreen, bool, buffer, bufferAttribute, builtin, builtinAOContext, builtinGIContext, builtinShadowContext, bumpMap, bvec2, bvec3, bvec4, bypass, cache, call, cameraFar, cameraIndex, cameraNear, cameraNormalMatrix, cameraPosition, cameraProjectionMatrix, cameraProjectionMatrixInverse, cameraViewMatrix, cameraViewport, cameraWorldMatrix, cbrt, cdl, ceil, checker, cineonToneMapping, clamp, clearcoat, clearcoatNormalView, clearcoatRoughness, clipSpace, clipping, clippingAlpha, code, color, colorSpaceToWorking, colorToDirection, compute, computeKernel, computeSkinning, context, convert, convertColorSpace, convertToTexture, cos, cosh, countLeadingZeros, countOneBits, countTrailingZeros, cross, cubeTexture, cubeTextureBase, dFdx, dFdy, dashSize, debug, decrement, decrementBefore, defaultBuildStages, defaultShaderStages, defined, degrees, deltaTime, densityFogFactor, depth, depthPass, determinant, difference, diffuseColor, diffuseContribution, directPointLight, directionToColor, directionToFaceDirection, dispersion, distance, div, dot, drawIndex, dynamicBufferAttribute, element, emissive, equal, equirectDirection, equirectUV, exp, exp2, exponentialHeightFogFactor, expression, faceDirection, faceForward, faceforward, float, floatBitsToInt, floatBitsToUint, floor, fog, fract, frameGroup, frameId, frontFacing, fwidth, gain, gapSize, getConstNodeType, getCurrentStack, getDistanceAttenuation, getGeometryRoughness, getNormalFromDepth, getParallaxCorrectNormal, getRoughness, getScreenPosition, getScreenPositionFromClip, getShIrradianceAt, getTextureIndex, getViewPosition, globalId, glsl, glslFn, grayscale, greaterThan, greaterThanEqual, hardwareClipping, hash, highpModelNormalViewMatrix, highpModelViewMatrix, hue, increment, incrementBefore, inspect, instance, instanceColor, instanceIndex, instancedArray, instancedBufferAttribute, instancedDynamicBufferAttribute, instancedMesh, int, intBitsToFloat, interleavedGradientNoise, inverse, inverseSqrt, inversesqrt, invocationLocalIndex, invocationSubgroupIndex, ior, iridescence, iridescenceIOR, iridescenceThickness, isolate, ivec2, ivec3, ivec4, js, label, length, lengthSq, lessThan, lessThanEqual, lightPosition, lightProjectionUV, lightShadowMatrix, lightTargetDirection, lightTargetPosition, lightViewPosition, lightingContext, lights, linearDepth, linearToneMapping, localId, log, log2, logarithmicDepthToViewZ, luminance, mat2, mat3, mat4, matcapUV, materialAO, materialAlphaTest, materialAnisotropy, materialAnisotropyVector, materialAttenuationColor, materialAttenuationDistance, materialClearcoat, materialClearcoatNormal, materialClearcoatRoughness, materialColor, materialDispersion, materialEmissive, materialEnvIntensity, materialEnvRotation, materialIOR, materialIridescence, materialIridescenceIOR, materialIridescenceThickness, materialLightMap, materialLineDashOffset, materialLineDashSize, materialLineGapSize, materialLineScale, materialLineWidth, materialMetalness, materialNormal, materialOpacity, materialPointSize, materialReference, materialReflectivity, materialRefractionRatio, materialRetroreflectivity, materialRotation, materialRoughness, materialSheen, materialSheenRoughness, materialShininess, materialSpecular, materialSpecularColor, materialSpecularIntensity, materialSpecularStrength, materialThickness, materialTransmission, max, maxMipLevel, mediumpModelViewMatrix, metalness, min, mix, mixElement, mod, modelDirection, modelNormalMatrix, modelPosition, modelRadius, modelScale, modelViewMatrix, modelViewPosition, modelViewProjection, modelWorldMatrix, modelWorldMatrixInverse, morphReference, mrt, mul, mx_aastep, mx_add, mx_atan2, mx_cell_noise_float, mx_cell_noise_vec3, mx_contrast, mx_divide, mx_fractal_noise_float, mx_fractal_noise_float_2d, mx_fractal_noise_vec2, mx_fractal_noise_vec3, mx_fractal_noise_vec4, mx_frame, mx_heighttonormal, mx_hsvtorgb, mx_ifequal, mx_ifgreater, mx_ifgreatereq, mx_invert, mx_modulo, mx_multiply, mx_noise_float, mx_noise_vec3, mx_noise_vec4, mx_place2d, mx_power, mx_ramp4, mx_ramplr, mx_ramptb, mx_rgbtohsv, mx_rotate2d, mx_rotate3d, mx_safepower, mx_separate, mx_smoothstep, mx_splitlr, mx_splittb, mx_srgb_texture_to_lin_rec709, mx_subtract, mx_timer, mx_transform_uv, mx_unifiednoise2d, mx_unifiednoise3d, mx_worley_noise_float, mx_worley_noise_float_2d, mx_worley_noise_float_3d, mx_worley_noise_vec2, mx_worley_noise_vec3, mx_worley_noise_vec3_style, negate, negateOnBackSide, neutralToneMapping, nodeArray, nodeImmutable, nodeObject, nodeObjectIntent, nodeObjects, nodeProxy, nodeProxyConstructor, nodeProxyIntent, normalFlat, normalGeometry, normalLocal, normalMap, normalView, normalViewGeometry, normalWorld, normalWorldGeometry, normalize, not, notEqual, numWorkgroups, objectDirection, objectGroup, objectPosition, objectRadius, objectScale, objectViewPosition, objectWorldMatrix, oneMinus, or, orthographicDepthToViewZ, oscSawtooth, oscSine, oscSquare, oscTriangle, output, outputStruct, overloadingFn, overrideNode, overrideNodes, packHalf2x16, packNormalToRGB, packSnorm2x16, packSnorm4x8, packUnorm2x16, packUnorm4x8, parabola, parallaxDirection, parallaxUV, parameter, pass, passTexture, pcurve, perspectiveDepthToViewZ, pmremTexture, pointShadow, pointUV, pointWidth, positionGeometry, positionLocal, positionPrevious, positionView, positionViewDirection, positionWorld, positionWorldDirection, posterize, pow, pow2, pow3, pow4, premultiplyAlpha, property, quadBroadcast, quadSwapDiagonal, quadSwapX, quadSwapY, radians, rand, range, rangeFogFactor, reciprocal, reference, referenceBuffer, reflect, reflectVector, reflectView, reflector, refract, refractVector, refractView, reinhardToneMapping, remap, remapClamp, renderGroup, renderOutput, rendererReference, replaceDefaultUV, retroreflectivity, rotate, rotateUV, roughness, round, rtt, sRGBTransferEOTF, sRGBTransferOETF, sample, sampler, samplerComparison, saturate, saturation, screenCoordinate, screenDPR, screenSize, screenUV, select, setCurrentStack, setName, shaderStages, shadow, shadowPositionWorld, shapeCircle, sharedUniformGroup, sheen, sheenRoughness, shiftLeft, shiftRight, shininess, sign, sin, sinc, sinh, skinning, smoothstep, smoothstepElement, specularColor, specularColorBlended, specularF90, spherizeUV, split, spritesheetUV, sqrt, stack, step, stepElement, storage, storageBarrier, storageElement, storageTexture, storageTexture3D, struct, sub, subBuild, subgroupAdd, subgroupAll, subgroupAnd, subgroupAny, subgroupBallot, subgroupBroadcast, subgroupBroadcastFirst, subgroupElect, subgroupExclusiveAdd, subgroupExclusiveMul, subgroupInclusiveAdd, subgroupInclusiveMul, subgroupIndex, subgroupMax, subgroupMin, subgroupMul, subgroupOr, subgroupShuffle, subgroupShuffleDown, subgroupShuffleUp, subgroupShuffleXor, subgroupSize, subgroupXor, tan, tangentGeometry, tangentLocal, tangentView, tangentViewFrame, tangentWorld, tanh, texture, texture3D, texture3DLevel, texture3DLoad, textureBarrier, textureBicubic, textureBicubicLevel, textureLevel, textureLoad, textureSize, textureStore, thickness, time, toneMapping, toneMappingExposure, toonOutlinePass, transformDirection, transformNormal, transformNormalByInverseViewMatrix, transformNormalByViewMatrix, transformNormalToView, transformedClearcoatNormalView, transformedNormalView, transformedNormalWorld, transmission, transpose, triNoise3D, triplanarTexture, triplanarTextures, trunc, uint, uintBitsToFloat, uniform, uniformArray, uniformCubeTexture, uniformFlow, uniformGroup, uniformTexture, unpackHalf2x16, unpackNormal, unpackRGBToNormal, unpackSnorm2x16, unpackSnorm4x8, unpackUnorm2x16, unpackUnorm4x8, unpremultiplyAlpha, userData, uv, uvec2, uvec3, uvec4, varying, varyingProperty, vec2, vec3, vec4, vectorComponents, velocity, vertexColor, vertexIndex, vertexStage, vibrance, viewZToLogarithmicDepth, viewZToOrthographicDepth, viewZToPerspectiveDepth, viewZToReversedOrthographicDepth, viewZToReversedPerspectiveDepth, viewport, viewportCoordinate, viewportDepthTexture, viewportLinearDepth, viewportMipTexture, viewportOpaqueMipTexture, viewportSafeUV, viewportSharedTexture, viewportSize, viewportTexture, viewportUV, vogelDiskSample, wgsl, wgslFn, workgroupArray, workgroupBarrier, workgroupId, workingToColorSpace, xor }; diff --git a/build/three.webgpu.js b/build/three.webgpu.js index 4921c208d6357a..4370acc1f3da04 100644 --- a/build/three.webgpu.js +++ b/build/three.webgpu.js @@ -9066,6 +9066,50 @@ function builtinAOContext( aoNode, node = null ) { } +/** + * TSL function for defining a built-in global illumination context for a given node. The AO node + * modulates the indirect lighting of the materials, the GI node is added to their irradiance + * without being modulated by the AO since it already accounts for occlusion. + * + * @tsl + * @function + * @param {Node} aoNode - The ambient occlusion value node to apply. + * @param {Node} giNode - The indirect diffuse irradiance node to add. + * @param {Node} [node=null] - The node whose context should be modified. + * @returns {ContextNode} + */ +function builtinGIContext( aoNode, giNode, node = null ) { + + // the lighting models multiply the whole indirect diffuse term by the AO, which would occlude + // the GI a second time. Dividing the GI by the AO cancels that out so only the environment + // lighting is modulated. + + const ao = aoNode.max( 1e-3 ); + + return context( node, { + + getAO: ( inputNode, { material } ) => { + + if ( material.transparent === true ) return inputNode; + + return inputNode !== null ? inputNode.mul( ao ) : ao; + + }, + + getGI: ( inputNode, { material } ) => { + + if ( material.transparent === true ) return inputNode; + + const gi = giNode.div( ao ); + + return inputNode !== null ? inputNode.add( gi ) : gi; + + } + + } ); + +} + /** * TSL function for defining a label context value for a given node. * @@ -9090,6 +9134,7 @@ addMethodChaining( 'uniformFlow', uniformFlow ); addMethodChaining( 'setName', setName ); addMethodChaining( 'builtinShadowContext', ( node, shadowNode, light ) => builtinShadowContext( shadowNode, light, node ) ); addMethodChaining( 'builtinAOContext', ( node, aoValue ) => builtinAOContext( aoValue, node ) ); +addMethodChaining( 'builtinGIContext', ( node, aoValue, giValue ) => builtinGIContext( aoValue, giValue, node ) ); /** * Class for representing shader variables as nodes. Variables are created from @@ -22428,6 +22473,18 @@ class NodeMaterial extends Material { } + if ( builder.context.getGI ) { + + const giNode = builder.context.getGI( null, builder ); + + if ( giNode !== null ) { + + materialLightsNode.push( new IrradianceNode( giNode ) ); + + } + + } + return materialLightsNode; } @@ -49929,6 +49986,7 @@ var TSL = /*#__PURE__*/Object.freeze({ bufferAttribute: bufferAttribute, builtin: builtin, builtinAOContext: builtinAOContext, + builtinGIContext: builtinGIContext, builtinShadowContext: builtinShadowContext, bumpMap: bumpMap, bvec2: bvec2, @@ -53116,6 +53174,7 @@ class NodeBuilder { delete context.getOutput; delete context.getTextureLevel; delete context.getAO; + delete context.getGI; delete context.getShadow; return context; @@ -88694,6 +88753,7 @@ class WebGPUBackend extends Backend { data.stencilWrite !== material.stencilWrite || data.stencilFunc !== material.stencilFunc || data.stencilFail !== material.stencilFail || data.stencilZFail !== material.stencilZFail || data.stencilZPass !== material.stencilZPass || data.stencilFuncMask !== material.stencilFuncMask || data.stencilWriteMask !== material.stencilWriteMask || + data.polygonOffset !== material.polygonOffset || data.polygonOffsetFactor !== material.polygonOffsetFactor || data.polygonOffsetUnits !== material.polygonOffsetUnits || data.side !== material.side || data.alphaToCoverage !== material.alphaToCoverage || data.sampleCount !== sampleCount || data.colorSpace !== colorSpace || data.colorFormat !== colorFormat || data.depthStencilFormat !== depthStencilFormat || @@ -88711,6 +88771,7 @@ class WebGPUBackend extends Backend { data.stencilWrite = material.stencilWrite; data.stencilFunc = material.stencilFunc; data.stencilFail = material.stencilFail; data.stencilZFail = material.stencilZFail; data.stencilZPass = material.stencilZPass; data.stencilFuncMask = material.stencilFuncMask; data.stencilWriteMask = material.stencilWriteMask; + data.polygonOffset = material.polygonOffset; data.polygonOffsetFactor = material.polygonOffsetFactor; data.polygonOffsetUnits = material.polygonOffsetUnits; data.side = material.side; data.alphaToCoverage = material.alphaToCoverage; data.sampleCount = sampleCount; data.colorSpace = colorSpace; @@ -88755,7 +88816,8 @@ class WebGPUBackend extends Backend { material.stencilWrite, material.stencilFunc, material.stencilFail, material.stencilZFail, material.stencilZPass, material.stencilFuncMask, material.stencilWriteMask, - material.side, + material.polygonOffset, material.polygonOffsetFactor, material.polygonOffsetUnits, + material.side, material.alphaToCoverage, frontFaceCW, utils.getSampleCountRenderContext( renderContext ), utils.getCurrentColorSpace( renderContext ), utils.getCurrentColorFormat( renderContext ), utils.getCurrentDepthStencilFormat( renderContext ), diff --git a/build/three.webgpu.nodes.js b/build/three.webgpu.nodes.js index 378d79f687616b..96f74808ea517f 100644 --- a/build/three.webgpu.nodes.js +++ b/build/three.webgpu.nodes.js @@ -9066,6 +9066,50 @@ function builtinAOContext( aoNode, node = null ) { } +/** + * TSL function for defining a built-in global illumination context for a given node. The AO node + * modulates the indirect lighting of the materials, the GI node is added to their irradiance + * without being modulated by the AO since it already accounts for occlusion. + * + * @tsl + * @function + * @param {Node} aoNode - The ambient occlusion value node to apply. + * @param {Node} giNode - The indirect diffuse irradiance node to add. + * @param {Node} [node=null] - The node whose context should be modified. + * @returns {ContextNode} + */ +function builtinGIContext( aoNode, giNode, node = null ) { + + // the lighting models multiply the whole indirect diffuse term by the AO, which would occlude + // the GI a second time. Dividing the GI by the AO cancels that out so only the environment + // lighting is modulated. + + const ao = aoNode.max( 1e-3 ); + + return context( node, { + + getAO: ( inputNode, { material } ) => { + + if ( material.transparent === true ) return inputNode; + + return inputNode !== null ? inputNode.mul( ao ) : ao; + + }, + + getGI: ( inputNode, { material } ) => { + + if ( material.transparent === true ) return inputNode; + + const gi = giNode.div( ao ); + + return inputNode !== null ? inputNode.add( gi ) : gi; + + } + + } ); + +} + /** * TSL function for defining a label context value for a given node. * @@ -9090,6 +9134,7 @@ addMethodChaining( 'uniformFlow', uniformFlow ); addMethodChaining( 'setName', setName ); addMethodChaining( 'builtinShadowContext', ( node, shadowNode, light ) => builtinShadowContext( shadowNode, light, node ) ); addMethodChaining( 'builtinAOContext', ( node, aoValue ) => builtinAOContext( aoValue, node ) ); +addMethodChaining( 'builtinGIContext', ( node, aoValue, giValue ) => builtinGIContext( aoValue, giValue, node ) ); /** * Class for representing shader variables as nodes. Variables are created from @@ -22428,6 +22473,18 @@ class NodeMaterial extends Material { } + if ( builder.context.getGI ) { + + const giNode = builder.context.getGI( null, builder ); + + if ( giNode !== null ) { + + materialLightsNode.push( new IrradianceNode( giNode ) ); + + } + + } + return materialLightsNode; } @@ -49929,6 +49986,7 @@ var TSL = /*#__PURE__*/Object.freeze({ bufferAttribute: bufferAttribute, builtin: builtin, builtinAOContext: builtinAOContext, + builtinGIContext: builtinGIContext, builtinShadowContext: builtinShadowContext, bumpMap: bumpMap, bvec2: bvec2, @@ -53116,6 +53174,7 @@ class NodeBuilder { delete context.getOutput; delete context.getTextureLevel; delete context.getAO; + delete context.getGI; delete context.getShadow; return context; @@ -88694,6 +88753,7 @@ class WebGPUBackend extends Backend { data.stencilWrite !== material.stencilWrite || data.stencilFunc !== material.stencilFunc || data.stencilFail !== material.stencilFail || data.stencilZFail !== material.stencilZFail || data.stencilZPass !== material.stencilZPass || data.stencilFuncMask !== material.stencilFuncMask || data.stencilWriteMask !== material.stencilWriteMask || + data.polygonOffset !== material.polygonOffset || data.polygonOffsetFactor !== material.polygonOffsetFactor || data.polygonOffsetUnits !== material.polygonOffsetUnits || data.side !== material.side || data.alphaToCoverage !== material.alphaToCoverage || data.sampleCount !== sampleCount || data.colorSpace !== colorSpace || data.colorFormat !== colorFormat || data.depthStencilFormat !== depthStencilFormat || @@ -88711,6 +88771,7 @@ class WebGPUBackend extends Backend { data.stencilWrite = material.stencilWrite; data.stencilFunc = material.stencilFunc; data.stencilFail = material.stencilFail; data.stencilZFail = material.stencilZFail; data.stencilZPass = material.stencilZPass; data.stencilFuncMask = material.stencilFuncMask; data.stencilWriteMask = material.stencilWriteMask; + data.polygonOffset = material.polygonOffset; data.polygonOffsetFactor = material.polygonOffsetFactor; data.polygonOffsetUnits = material.polygonOffsetUnits; data.side = material.side; data.alphaToCoverage = material.alphaToCoverage; data.sampleCount = sampleCount; data.colorSpace = colorSpace; @@ -88755,7 +88816,8 @@ class WebGPUBackend extends Backend { material.stencilWrite, material.stencilFunc, material.stencilFail, material.stencilZFail, material.stencilZPass, material.stencilFuncMask, material.stencilWriteMask, - material.side, + material.polygonOffset, material.polygonOffsetFactor, material.polygonOffsetUnits, + material.side, material.alphaToCoverage, frontFaceCW, utils.getSampleCountRenderContext( renderContext ), utils.getCurrentColorSpace( renderContext ), utils.getCurrentColorFormat( renderContext ), utils.getCurrentDepthStencilFormat( renderContext ), diff --git a/examples/files.json b/examples/files.json index 6c28f7775ae399..ca64c734126760 100644 --- a/examples/files.json +++ b/examples/files.json @@ -526,6 +526,8 @@ "webgpu_volume_lighting_rectarea", "webgpu_volume_lighting_traa", "webgpu_volume_perlin", + "webgpu_vxgi", + "webgpu_vxgi_sponza", "webgpu_water", "webgpu_xr_rollercoaster", "webgpu_xr_cubes", diff --git a/examples/jsm/lighting/vxgi/VXGIConeTracer.js b/examples/jsm/lighting/vxgi/VXGIConeTracer.js new file mode 100644 index 00000000000000..676909d4eda69c --- /dev/null +++ b/examples/jsm/lighting/vxgi/VXGIConeTracer.js @@ -0,0 +1,194 @@ +import { float, vec3, vec4, If, Loop, Break, max, min, abs, dot, clamp, log2, exp2, select } from 'three/tsl'; + +/** + * Emits the intersection of a ray with the bounds of the given volume. + * + * @param {VXGIVolume} volume - The volume. + * @param {Node} origin - The ray origin. + * @param {Node} direction - The normalized ray direction. + * @return {{tEnter: Node, tExit: Node}} The entry and exit distances. The ray misses the volume if `tExit <= tEnter`. + */ +export function intersectVolume( volume, origin, direction ) { + + const boundsMin = volume.boundsMinNode; + const boundsMax = boundsMin.add( volume.volumeSizeNode ).toConst(); + + const safeDirection = vec3( + select( abs( direction.x ).lessThan( 1e-6 ), 1e-6, direction.x ), + select( abs( direction.y ).lessThan( 1e-6 ), 1e-6, direction.y ), + select( abs( direction.z ).lessThan( 1e-6 ), 1e-6, direction.z ) + ).toConst(); + + const invDirection = float( 1 ).div( safeDirection ).toConst(); + const t0 = boundsMin.sub( origin ).mul( invDirection ).toConst(); + const t1 = boundsMax.sub( origin ).mul( invDirection ).toConst(); + const tNear = min( t0, t1 ).toConst(); + const tFar = max( t0, t1 ).toConst(); + + const tEnter = max( max( tNear.x, tNear.y ), max( tNear.z, 0 ) ).toConst(); + const tExit = min( min( tFar.x, tFar.y ), tFar.z ).toConst(); + + return { tEnter, tExit }; + +} + +/** + * Emits a directional radiance lookup: for each axis the direction block facing the ray is + * sampled from the directional texture and the three samples are blended with the squared + * direction components. The block is selected arithmetically, so the lookup does not branch. + * + * @param {VXGIVolume} volume - The volume. + * @param {Texture3DNode} directionalNode - The directional radiance texture node. + * @param {Node} uvw - The texture coordinates within the volume. + * @param {Node} level - The mip level of the directional texture. + * @param {Node} direction - The normalized ray direction. + * @return {Node} The premultiplied radiance and its weight. + */ +export function sampleDirectional( volume, directionalNode, uvw, level, direction ) { + + const weights = direction.mul( direction ).toConst(); + + // keep the lookup inside its block: clamp x by half a texel of the coarser blended level + + const halfTexel = exp2( level.ceil() ).mul( 0.5 ).div( volume.directionalWidthNode ).toConst(); + const u = uvw.x.clamp( halfTexel, halfTexel.oneMinus() ).toConst(); + + const result = vec4( 0 ).toVar(); + + for ( let axis = 0; axis < 3; axis ++ ) { + + const component = [ 'x', 'y', 'z' ][ axis ]; + const block = float( axis * 2 ).add( select( direction[ component ].lessThan( 0 ), 1, 0 ) ); + const blockUVW = vec3( block.add( u ).div( 6 ), uvw.y, uvw.z ); + + result.addAssign( directionalNode.sample( blockUVW ).level( level ).mul( weights[ component ] ) ); + + } + + return result; + +} + +/** + * Creates a cone tracing function for a {@link VXGIVolume}. The returned function emits + * TSL code that marches a cone through the volume's opacity/radiance mip chain and returns the + * gathered radiance, the accumulated occlusion and a distance-weighted occlusion for AO. + * + * Implements the approximate voxel cone tracing of Crassin et al. 2011: the cone is sampled at + * the mip level matching its current diameter with quadrilinear interpolation, samples are + * composited front-to-back with the emission-absorption model, the opacity of a sample is + * corrected for the step size and the anisotropic opacity (and, if given, the directional + * radiance) is interpolated from the three directional values closest to the cone direction. + * Cones leaving the volume gather nothing. + * + * @param {VXGIVolume} volume - The volume to trace. + * @param {Object} [options={}] - Options. + * @param {?TextureNode} [options.radianceNode=null] - The radiance texture node to gather from. If `null`, only occlusion is computed. + * @param {?Texture3DNode} [options.directionalNode=null] - The directional radiance texture node of the coarser levels, see {@link VXGIVolume#directionalRadiance}. If `null`, the coarser levels are gathered from the radiance node's mips. + * @param {number} [options.maxSteps=128] - Upper bound of steps per cone. + * @return {Function} A function `( origin, direction, tanHalfAngle, maxDistance, aoDistance = null ) => { color, alpha, ao }`. AO is only computed if `aoDistance` is given. + */ +export function createConeTracer( volume, options = {} ) { + + const { radianceNode = null, directionalNode = null, maxSteps = 128 } = options; + + const opacityNode = volume.opacityNode; + const boundsMin = volume.boundsMinNode; + const volumeSize = volume.volumeSizeNode; + const voxelSize = volume.voxelSizeNode; + const maxLevel = volume.maxLevelNode; + const stepScale = volume.stepScale; + + return ( origin, direction, tanHalfAngle, maxDistance, aoDistance = null ) => { + + const color = vec3( 0 ).toVar(); + const alpha = float( 0 ).toVar(); + const ao = float( 0 ).toVar(); + + const { tEnter, tExit } = intersectVolume( volume, origin, direction ); + + // cones start one voxel away from their origin so the voxels the origin lies in are not sampled + + const t = max( tEnter, voxelSize ).toVar(); + const limit = min( tExit, maxDistance ).toConst(); + + // interpolation weights of the three directional opacity values (sum to one) + + const directionWeights = direction.mul( direction ).toConst(); + + // AO falloff 1 / ( 1 + r / aoDistance ) + + const aoFalloff = aoDistance !== null ? select( aoDistance.greaterThan( 0 ), float( 1 ).div( max( aoDistance, 1e-6 ) ), float( 0 ) ).toConst() : null; + + If( tExit.greaterThan( t ), () => { + + Loop( { start: 0, end: maxSteps, type: 'int', condition: '<', name: 's' }, () => { + + If( t.greaterThanEqual( limit ).or( alpha.greaterThanEqual( 0.98 ) ), () => { + + Break(); + + } ); + + const diameter = max( t.mul( 2 ).mul( tanHalfAngle ), voxelSize ).toConst(); + const lod = clamp( log2( diameter.div( voxelSize ) ), 0, maxLevel ).toConst(); + const position = origin.add( direction.mul( t ) ).toConst(); + const uvw = position.sub( boundsMin ).div( volumeSize ).toConst(); + + const opacity = opacityNode.sample( uvw ).level( lod ).toConst(); + + // directional opacity, corrected for a step that is a fraction of the texel size + + const a = clamp( dot( opacity.xyz, directionWeights ), 0, 1 ).oneMinus().pow( stepScale ).oneMinus().toConst(); + const weight = a.mul( alpha.oneMinus() ).toConst(); + + if ( radianceNode !== null ) { + + let radiance; + + if ( directionalNode === null ) { + + radiance = radianceNode.sample( uvw ).level( lod ).toConst(); + + } else { + + // the finest level is isotropic, coarser levels are stored directionally at half + // resolution; their weight only counts the surfaces facing the ray + + radiance = vec4( 0 ).toVar(); + + If( lod.lessThan( 1 ), () => { + + radiance.assign( radianceNode.sample( uvw ).level( float( 0 ) ) ); + + } ).Else( () => { + + radiance.assign( sampleDirectional( volume, directionalNode, uvw, lod.sub( 1 ), direction ) ); + + } ); + + } + + color.addAssign( radiance.rgb.div( max( radiance.a, 1e-4 ) ).mul( weight ) ); + + } + + alpha.addAssign( weight ); + + if ( aoFalloff !== null ) { + + ao.addAssign( a.mul( ao.oneMinus() ).div( t.mul( aoFalloff ).add( 1 ) ) ); + + } + + t.addAssign( voxelSize.mul( exp2( lod ) ).mul( stepScale ) ); + + } ); + + } ); + + return { color, alpha, ao }; + + }; + +} diff --git a/examples/jsm/lighting/vxgi/VXGINode.js b/examples/jsm/lighting/vxgi/VXGINode.js new file mode 100644 index 00000000000000..a28ea8aaf007cc --- /dev/null +++ b/examples/jsm/lighting/vxgi/VXGINode.js @@ -0,0 +1,658 @@ +import { RenderTarget, Vector2, Vector3, TempNode, QuadMesh, NodeMaterial, RendererUtils, HalfFloatType, RGBAFormat, RedFormat, UnsignedByteType } from 'three/webgpu'; +import { Fn, NodeUpdateType, uniform, reference, vec2, vec3, vec4, float, uint, uv, mix, passTexture, getViewPosition, getNormalFromDepth, logarithmicDepthToViewZ, viewZToPerspectiveDepth, screenCoordinate, interleavedGradientNoise, normalize, cross, abs, select, sqrt, fract, floor, cos, sin, exp2, pow, PI, Loop, If, Break, property, outputStruct, context } from 'three/tsl'; + +import { VXGIVolume } from './VXGIVolume.js'; +import { createConeTracer, intersectVolume, sampleDirectional } from './VXGIConeTracer.js'; + +const _quadMesh = /*@__PURE__*/ new QuadMesh(); +const _size = /*@__PURE__*/ new Vector2(); + +const TEMPORAL_SHIFT = 5.588238; +const TEMPORAL_CYCLE = 64; + +let _rendererState; + +const _neutralContext = /*@__PURE__*/ context(); + +/** + * Post processing node for voxel based global illumination. The scene is voxelized into a + * {@link VXGIVolume} and indirect diffuse light and ambient occlusion are gathered per + * pixel with approximate voxel cone tracing. + * + * The node is a middle path between SSGI and Light Probe Grids: + * + * - Compared to `SSGINode` it is free of screen-space artifacts and provides noticeably more + * consistent lighting, since off-screen surfaces and thin occluders contribute. + * However, it is less dynamic: objects should stay static, because geometry changes require + * a re-voxelization (`needsUpdate = true`) which is too expensive for per-frame animation. + * - Compared to `LightProbeGrid` it supports dynamic lighting without a new baking process and + * produces a better overall lighting quality with less light bleeding. However, it is more + * expensive and therefore less suitable for performance restricted use cases. + * + * The quality/performance of the effect mainly depend on the voxel resolution, the number of cones + * traced per pixel as well as the apeture of the cones. + * + * Lights and their shadow maps are picked up automatically. Only direct lights are injected. + * + * 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. + * + * Note: This node can only be used with `WebGPURenderer` and a WebGPU backend. + * + * @augments TempNode + * @three_import import { vxgi } from 'three/addons/lighting/vxgi/VXGINode.js'; + */ +class VXGINode extends TempNode { + + static get type() { + + return 'VXGINode'; + + } + + /** + * Constructs a new voxel GI node. + * + * @param {TextureNode} depthNode - A texture node that represents the scene's depth. + * @param {?TextureNode} normalNode - A texture node that represents the scene's view space normals. + * @param {Scene} scene - The scene to voxelize. + * @param {Camera} camera - The camera the scene is rendered with. + * @param {number} [resolution=128] - Number of voxels along the longest axis of the volume. Should not exceed `256`, higher values exceed the maximum storage buffer size of the voxelizer. + */ + constructor( depthNode, normalNode, scene, camera, resolution = 128 ) { + + super( 'vec4' ); + + /** + * A node that represents the scene's depth. + * + * @type {TextureNode} + */ + this.depthNode = depthNode; + + /** + * A node that represents the scene's normals. If `null`, normals are reconstructed from depth. + * + * @type {?TextureNode} + */ + this.normalNode = normalNode; + + /** + * The scene to voxelize. + * + * @type {Scene} + */ + this.scene = scene; + + /** + * The camera the scene is rendered with. + * + * @type {Camera} + */ + this.camera = camera; + + /** + * The voxel volume. Use it to configure bounds, layers and bounces. + * + * @type {VXGIVolume} + */ + this.volume = new VXGIVolume( resolution ); + + /** + * The `updateBeforeType` is set to `NodeUpdateType.FRAME` since the node renders + * its effect once per frame in `updateBefore()`. + * + * @type {string} + * @default 'frame' + */ + this.updateBeforeType = NodeUpdateType.FRAME; + + /** + * Number of cones traced per pixel. Should be in the range `[2, 8]`. + * + * Mainly defines the quality and precision of the Voxel Cone Tracing. A value of + * `2` - `4` is the recommended setting. Use `2` for performance restricted use cases. + * + * @type {UniformNode} + * @default 3 + */ + this.coneCount = uniform( 3, 'uint' ); + + /** + * Aperture of the diffuse cones in degrees. Wider cones are faster (fewer steps) but leak + * and over-occlude more, narrow cones are more precise but noisier and take more steps. + * Choose it together with {@link VXGINode#coneCount}. Should be in the range `[10, 90]`. + * + * @type {UniformNode} + * @default 40 + */ + this.coneAngle = uniform( 40 ); + + /** + * Intensity of the indirect diffuse irradiance. + * + * @type {UniformNode} + * @default 1 + */ + this.giIntensity = uniform( 1 ); + + /** + * Power function applied to AO to make it appear darker/lighter. + * + * @type {UniformNode} + * @default 1 + */ + this.aoIntensity = uniform( 1 ); + + /** + * The darkest value the ambient occlusion can reach. Lifts creases and contact regions out + * of pure black, which voxel-traced occlusion tends to overestimate at the resolution of + * a voxel. `0` keeps the full occlusion range. + * + * @type {UniformNode} + * @default 0 + */ + this.aoMinVisibility = uniform( 0 ); + + /** + * Occlusion is weighted by `1 / ( 1 + distance / aoDistance )` for AO, so occluders at this + * world-space distance count half. `0` disables the falloff. + * + * @type {UniformNode} + * @default 1 + */ + this.aoDistance = uniform( 1 ); + + /** + * Offset of the cone origins along the surface normal in voxels. The surface's own voxel + * can extend up to half a voxel above the surface and the trilinear footprint of a sample + * spans another half voxel, so 1.5 voxels avoid self-occlusion in every case. + * + * @type {UniformNode} + * @default 1.5 + */ + this.normalOffset = uniform( 1.5 ); + + /** + * Debug visualization of the volume: `0` = off, `1` = radiance voxels, `2` = per-axis opacity voxels. + * The visualization replaces the GI output. + * + * @type {UniformNode} + * @default 0 + */ + this.debug = uniform( 0, 'int' ); + + /** + * The mip level shown by the debug visualization. + * + * @type {UniformNode} + * @default 0 + */ + this.debugLevel = uniform( 0 ); + + /** + * Whether to use temporal filtering or not. Setting this property to `true` requires the + * usage of `TRAANode`. Cone directions are then rotated per frame to converge the noise. + * + * @type {boolean} + * @default true + */ + this.useTemporalFiltering = true; + + // private uniforms + + this._resolution = uniform( new Vector2() ); + this._frame = uniform( 0 ); + this._cameraProjectionMatrix = uniform( camera.projectionMatrix ); + this._cameraProjectionMatrixInverse = uniform( camera.projectionMatrixInverse ); + this._cameraWorldMatrix = uniform( camera.matrixWorld ); + this._cameraPosition = uniform( new Vector3() ).onRenderUpdate( ( _, self ) => self.value.setFromMatrixPosition( camera.matrixWorld ) ); + this._cameraNear = reference( 'near', 'float', camera ); + this._cameraFar = reference( 'far', 'float', camera ); + + /** + * The render target the effect is rendered into. The first texture holds the AO, + * the second one the GI. + * + * @private + * @type {RenderTarget} + */ + this._renderTarget = new RenderTarget( 1, 1, { depthBuffer: false, count: 2 } ); + + const aoTexture = this._renderTarget.textures[ 0 ]; + aoTexture.name = 'VXGI.AO'; + aoTexture.type = UnsignedByteType; + aoTexture.format = RedFormat; + + const giTexture = this._renderTarget.textures[ 1 ]; + giTexture.name = 'VXGI.GI'; + giTexture.type = HalfFloatType; + giTexture.format = RGBAFormat; + + this._material = new NodeMaterial(); + this._material.name = 'VXGI'; + + // the shared context of the last build and the directional setting the material was built with + + this._sharedContext = null; + this._directional = null; + + this._aoNode = passTexture( this, aoTexture ); + this._giNode = passTexture( this, giTexture ); + + } + + /** + * Number of cached indirect bounces. See {@link VXGIVolume#bounces}. + * + * @type {number} + */ + get bounces() { + + return this.volume.bounces; + + } + + set bounces( value ) { + + this.volume.bounces = value; + this.volume.lightingNeedsUpdate = true; + + } + + /** + * Whether the coarser radiance levels are filtered directionally to reduce light bleeding + * through thin walls and floors. Off by default since it costs memory and performance. + * See {@link VXGIVolume#directionalRadiance}. + * + * @type {boolean} + */ + get directionalRadiance() { + + return this.volume.directionalRadiance; + + } + + set directionalRadiance( value ) { + + this.volume.directionalRadiance = value; + + } + + /** + * Set to `true` to re-voxelize the scene in the next frame. + * + * @type {boolean} + */ + get needsUpdate() { + + return this.volume.needsUpdate; + + } + + set needsUpdate( value ) { + + this.volume.needsUpdate = value; + + } + + /** + * Set to `true` to re-inject lighting in the next frame. + * + * @type {boolean} + */ + get lightingNeedsUpdate() { + + return this.volume.lightingNeedsUpdate; + + } + + set lightingNeedsUpdate( value ) { + + this.volume.lightingNeedsUpdate = value; + + } + + /** + * Returns the AO result of the effect as a texture node. + * + * @return {PassTextureNode} A texture node that represents the AO result of the effect. + */ + getAONode() { + + return this._aoNode; + + } + + /** + * Returns the GI result of the effect as a texture node. The texture holds the indirect + * diffuse irradiance, ready to be added to the lighting via `builtinGIContext()`. + * + * @return {PassTextureNode} A texture node that represents the GI result of the effect. + */ + getGINode() { + + return this._giNode; + + } + + /** + * Sets the size of the effect. + * + * @param {number} width - The width of the effect. + * @param {number} height - The height of the effect. + */ + setSize( width, height ) { + + this._resolution.value.set( width, height ); + this._renderTarget.setSize( width, height ); + + } + + /** + * This method is used to render the effect once per frame. + * + * @param {NodeFrame} frame - The current node frame. + */ + updateBefore( frame ) { + + const { renderer } = frame; + + _rendererState = RendererUtils.resetRendererState( renderer, _rendererState ); + + // this node is usually updated while a scene pass renders whose context injects the + // AO/GI of this node into the materials; the nested renders below must not inherit it + + const currentContextNode = renderer.contextNode; + renderer.contextNode = _neutralContext; + + // + + const size = renderer.getDrawingBufferSize( _size ); + this.setSize( size.width, size.height ); + + // voxelization and light injection + + this.volume.update( renderer, this.scene ); + + // the sampling code depends on the volume's directional setting + + if ( this._sharedContext !== null && this._directional !== this.volume.directionalRadiance ) { + + this._setupMaterial( renderer ); + + } + + // update temporal uniforms + + if ( this.useTemporalFiltering === true ) { + + this._frame.value = frame.frameId % TEMPORAL_CYCLE; + + } else { + + this._frame.value = 0; + + } + + // + + _quadMesh.material = this._material; + _quadMesh.name = 'VXGI'; + + // clear (white for the AO attachment) + + renderer.setClearColor( 0xffffff, 1 ); + + renderer.setRenderTarget( this._renderTarget ); + _quadMesh.render( renderer ); + + // restore + + renderer.contextNode = currentContextNode; + + RendererUtils.restoreRendererState( renderer, _rendererState ); + + } + + /** + * This method is used to setup the effect's TSL code. + * + * @param {NodeBuilder} builder - The current node builder. + * @return {PassTextureNode} + */ + setup( builder ) { + + this._sharedContext = builder.getSharedContext(); + this._setupMaterial( builder.renderer ); + + return this._aoNode; + + } + + /** + * Builds the TSL code of the effect into the internal pass material. + * + * @private + * @param {Renderer} renderer - The renderer. + */ + _setupMaterial( renderer ) { + + const volume = this.volume; + const directional = this._directional = volume.directionalRadiance; + const uvNode = uv(); + + const sampleDepth = ( uv ) => { + + const depth = this.depthNode.sample( uv ).r; + + if ( renderer.logarithmicDepthBuffer === true ) { + + const viewZ = logarithmicDepthToViewZ( depth, this._cameraNear, this._cameraFar ); + + return viewZToPerspectiveDepth( viewZ, this._cameraNear, this._cameraFar ); + + } + + return depth; + + }; + + const sampleNormal = ( uv ) => ( this.normalNode !== null ) ? this.normalNode.sample( uv ).rgb.normalize() : getNormalFromDepth( uv, this.depthNode.value, this._cameraProjectionMatrixInverse ); + + const voxelSize = volume.voxelSizeNode; + const projectionMatrixInverse = this._cameraProjectionMatrixInverse; + + const trace = createConeTracer( volume, { + radianceNode: volume.radianceNode, + directionalNode: directional === true ? volume.directionalNode : null + } ); + + const aoField = property( 'float' ); + const giField = property( 'vec4' ); + + const outputNode = outputStruct( aoField, giField ); + + const gi = Fn( () => { + + const depth = sampleDepth( uvNode ).toConst(); + + depth.greaterThanEqual( 1.0 ).discard(); + + const viewPosition = getViewPosition( uvNode, depth, projectionMatrixInverse ).toConst(); + const worldPosition = this._cameraWorldMatrix.mul( vec4( viewPosition, 1 ) ).xyz.toConst(); + const viewNormal = sampleNormal( uvNode ).toConst(); + const worldNormal = normalize( this._cameraWorldMatrix.mul( vec4( viewNormal, 0 ) ).xyz ).toConst(); + + // interleaved gradient noise for both values: its spatial structure converges well under temporal filtering + + const temporalShift = this._frame.mul( TEMPORAL_SHIFT ).toConst(); + const rotationNoise = interleavedGradientNoise( screenCoordinate.add( temporalShift ) ).toConst(); + const elevationNoise = interleavedGradientNoise( screenCoordinate.add( temporalShift ).add( vec2( 5.588238, 3.14159 ) ) ).toConst(); + + // tangent frame + + const up = select( abs( worldNormal.y ).lessThan( 0.99 ), vec3( 0, 1, 0 ), vec3( 1, 0, 0 ) ); + const tangent = normalize( cross( worldNormal, up ) ).toConst(); + const bitangent = cross( worldNormal, tangent ).toConst(); + + const CONE_COUNT = this.coneCount.toConst(); + const tanHalfAngle = this.coneAngle.mul( 0.5 ).radians().tan().toConst(); + const traceDistance = volume.traceDistanceNode; + const aoDistance = this.aoDistance; + const originOffset = worldNormal.mul( voxelSize.mul( this.normalOffset ) ).toConst(); + + const color = vec3( 0 ).toVar(); + const occlusion = float( 0 ).toVar(); + + Loop( { start: uint( 0 ), end: CONE_COUNT, type: 'uint', condition: '<', name: 'c' }, ( { c } ) => { + + // stratified cosine-weighted directions + + const u1 = float( c ).add( elevationNoise ).div( float( CONE_COUNT ) ).toConst(); + const u2 = fract( float( c ).mul( 0.618034 ).add( rotationNoise ) ).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( worldNormal.mul( cosTheta ) ) ).toConst(); + const origin = worldPosition.add( originOffset ).toConst(); + + const cone = trace( origin, direction, tanHalfAngle, traceDistance, aoDistance ); + + color.addAssign( cone.color ); + occlusion.addAssign( cone.ao ); + + } ); + + // the mean radiance over the cosine-weighted hemisphere times PI is the irradiance + + color.divAssign( float( CONE_COUNT ) ); + color.mulAssign( this.giIntensity.mul( PI ) ); + + const ao = mix( this.aoMinVisibility, float( 1 ), pow( occlusion.div( float( CONE_COUNT ) ).oneMinus().clamp(), this.aoIntensity ) ).toVar(); + + // debug visualization: march the voxels from the camera + + If( this.debug.greaterThan( 0 ), () => { + + const cameraPosition = this._cameraPosition; + const direction = normalize( worldPosition.sub( cameraPosition ) ).toConst(); + const surfaceDistance = worldPosition.sub( cameraPosition ).length().toConst(); + const level = this.debugLevel.toConst(); + const texel = voxelSize.mul( exp2( level ) ).toConst(); + const levelSize = volume.volumeSizeNode.div( texel ).toConst(); + const boundsMin = volume.boundsMinNode; + + const { tEnter, tExit } = intersectVolume( volume, cameraPosition, direction ); + const tMax = tExit.min( surfaceDistance ).toConst(); + const t = tEnter.toVar(); + + color.assign( 0 ); + ao.assign( 1 ); + + Loop( { start: 0, end: 512, type: 'int', condition: '<', name: 's' }, () => { + + If( t.greaterThanEqual( tMax ), () => { + + Break(); + + } ); + + const uvw = cameraPosition.add( direction.mul( t ) ).sub( boundsMin ).div( volume.volumeSizeNode ); + const snapped = floor( uvw.mul( levelSize ) ).add( 0.5 ).div( levelSize ).toConst(); + + If( this.debug.equal( 1 ), () => { + + let radiance; + + if ( directional === false ) { + + radiance = volume.radianceNode.sample( snapped ).level( level ).toConst(); + + } else { + + radiance = vec4( 0 ).toVar(); + + If( level.lessThan( 1 ), () => { + + radiance.assign( volume.radianceNode.sample( snapped ).level( float( 0 ) ) ); + + } ).Else( () => { + + radiance.assign( sampleDirectional( volume, volume.directionalNode, snapped, level.sub( 1 ), direction ) ); + + } ); + + } + + If( radiance.a.greaterThan( 0.01 ), () => { + + color.assign( radiance.rgb.div( radiance.a ) ); + Break(); + + } ); + + } ).Else( () => { + + const opacity = volume.opacityNode.sample( snapped ).level( level ).toConst(); + + If( opacity.w.greaterThan( 0.01 ), () => { + + color.assign( opacity.xyz ); + Break(); + + } ); + + } ); + + t.addAssign( texel.mul( 0.25 ) ); + + } ); + + } ); + + aoField.assign( ao ); + giField.assign( vec4( color, 1 ) ); + + return vec4( 0 ); + + } ); + + this._material.contextNode = context( this._sharedContext ); + this._material.colorNode = gi(); + this._material.outputNode = outputNode; + this._material.needsUpdate = true; + + } + + /** + * Frees internal resources. This method should be called + * when the effect is no longer required. + */ + dispose() { + + this._renderTarget.dispose(); + this._material.dispose(); + this.volume.dispose(); + + } + +} + +export default VXGINode; + +/** + * TSL function for creating a voxel GI effect. + * + * @tsl + * @function + * @param {TextureNode} depthNode - A texture node that represents the scene's depth. + * @param {?TextureNode} normalNode - A texture node that represents the scene's view space normals. + * @param {Scene} scene - The scene to voxelize. + * @param {Camera} camera - The camera the scene is rendered with. + * @param {number} [resolution=128] - Number of voxels along the longest axis of the volume. Should not exceed `256`, higher values exceed the maximum storage buffer size of the voxelizer. + * @returns {VXGINode} + */ +export const vxgi = ( depthNode, normalNode, scene, camera, resolution ) => new VXGINode( depthNode, normalNode, scene, camera, resolution ); diff --git a/examples/jsm/lighting/vxgi/VXGISceneCollector.js b/examples/jsm/lighting/vxgi/VXGISceneCollector.js new file mode 100644 index 00000000000000..34eb69628332e1 --- /dev/null +++ b/examples/jsm/lighting/vxgi/VXGISceneCollector.js @@ -0,0 +1,478 @@ +import { Box3, Color, Matrix4, Vector2, Vector3, BackSide, DoubleSide, SRGBColorSpace } from 'three/webgpu'; + +const _matrix = /*@__PURE__*/ new Matrix4(); +const _box = /*@__PURE__*/ new Box3(); +const _va = /*@__PURE__*/ new Vector3(); +const _vb = /*@__PURE__*/ new Vector3(); +const _vc = /*@__PURE__*/ new Vector3(); +const _uva = /*@__PURE__*/ new Vector2(); +const _uvb = /*@__PURE__*/ new Vector2(); +const _uvc = /*@__PURE__*/ new Vector2(); +const _uv = /*@__PURE__*/ new Vector2(); +const _color = /*@__PURE__*/ new Color(); +const _albedo = /*@__PURE__*/ new Color(); +const _emissive = /*@__PURE__*/ new Color(); + +const IMAGE_SAMPLE_SIZE = 64; + +/** + * Number of floats per triangle record: three positions (vec4 each, w unused), + * albedo (rgb + side flag) and emissive (rgb + unused). + */ +export const TRIANGLE_STRIDE = 20; + +const _srgbLUT = new Float32Array( 256 ); + +for ( let i = 0; i < 256; i ++ ) { + + const c = i / 255; + _srgbLUT[ i ] = ( c < 0.04045 ) ? c * 0.0773993808 : Math.pow( c * 0.9478672986 + 0.0521327014, 2.4 ); + +} + +class GrowableFloat32Array { + + constructor( initialSize ) { + + this.array = new Float32Array( initialSize ); + this.length = 0; + + } + + ensure( count ) { + + if ( this.length + count > this.array.length ) { + + let size = this.array.length * 2; + while ( size < this.length + count ) size *= 2; + + const array = new Float32Array( size ); + array.set( this.array ); + this.array = array; + + } + + } + + push4( x, y, z, w ) { + + const a = this.array, i = this.length; + a[ i ] = x; a[ i + 1 ] = y; a[ i + 2 ] = z; a[ i + 3 ] = w; + this.length += 4; + + } + + result() { + + return this.array.slice( 0, this.length ); + + } + +} + +/** + * Downscales a texture image into a small pixel buffer so triangle colors can be + * looked up on the CPU. Returns `null` for images that cannot be read. + * + * @private + * @param {Texture} texture - The texture. + * @param {Map} cache - Cache keyed by texture uuid. + * @return {?{data: Uint8ClampedArray, width: number, height: number, srgb: boolean}} The image sampler. + */ +function getImageSampler( texture, cache ) { + + if ( cache.has( texture.uuid ) ) return cache.get( texture.uuid ); + + let sampler = null; + const image = texture.image; + + if ( image && texture.isCompressedTexture !== true ) { + + const srgb = texture.colorSpace === SRGBColorSpace; + + if ( image.data !== undefined && image.width !== undefined ) { + + // DataTexture (RGBA 8-bit only) + + if ( image.data.length === image.width * image.height * 4 && image.data.BYTES_PER_ELEMENT === 1 ) { + + sampler = { data: image.data, width: image.width, height: image.height, srgb }; + + } + + } else if ( image.width > 0 && image.height > 0 ) { + + try { + + const size = IMAGE_SAMPLE_SIZE; + const canvas = ( typeof OffscreenCanvas !== 'undefined' ) ? new OffscreenCanvas( size, size ) : document.createElement( 'canvas' ); + canvas.width = size; + canvas.height = size; + + const context = canvas.getContext( '2d', { willReadFrequently: true } ); + context.imageSmoothingEnabled = true; + context.imageSmoothingQuality = 'high'; + context.drawImage( image, 0, 0, size, size ); + + sampler = { data: context.getImageData( 0, 0, size, size ).data, width: size, height: size, srgb }; + + } catch ( e ) { + + sampler = null; + + } + + } + + } + + cache.set( texture.uuid, sampler ); + + return sampler; + +} + +/** + * Samples the given texture at the given uv coordinates. The result is written + * into `target` (linear rgb) and the alpha is returned. + * + * @private + */ +function sampleTexture( texture, sampler, uv, target ) { + + _uv.copy( uv ); + texture.transformUv( _uv ); + + const x = Math.min( sampler.width - 1, Math.max( 0, Math.floor( _uv.x * sampler.width ) ) ); + const y = Math.min( sampler.height - 1, Math.max( 0, Math.floor( _uv.y * sampler.height ) ) ); + const i = ( y * sampler.width + x ) * 4; + const data = sampler.data; + + if ( sampler.srgb ) { + + target.setRGB( _srgbLUT[ data[ i ] ], _srgbLUT[ data[ i + 1 ] ], _srgbLUT[ data[ i + 2 ] ] ); + + } else { + + target.setRGB( data[ i ] / 255, data[ i + 1 ] / 255, data[ i + 2 ] / 255 ); + + } + + return data[ i + 3 ] / 255; + +} + +function getMaterialInfo( material, cache, imageCache ) { + + if ( cache.has( material ) ) return cache.get( material ); + + const unlit = material.isMeshBasicMaterial === true || material.isMeshBasicNodeMaterial === true; + + const info = { + unlit, + side: material.side === BackSide ? 1 : ( material.side === DoubleSide ? 2 : 0 ), + opacity: material.transparent === true ? material.opacity : 1, + alphaTest: material.alphaTest || 0, + color: material.color ? material.color : null, + emissive: ( ! unlit && material.emissive ) ? material.emissive : null, + emissiveIntensity: material.emissiveIntensity !== undefined ? material.emissiveIntensity : 1, + map: null, + mapSampler: null, + emissiveMap: null, + emissiveMapSampler: null + }; + + if ( material.map && material.map.isTexture ) { + + info.mapSampler = getImageSampler( material.map, imageCache ); + if ( info.mapSampler !== null ) info.map = material.map; + + } + + if ( ! unlit && material.emissiveMap && material.emissiveMap.isTexture ) { + + info.emissiveMapSampler = getImageSampler( material.emissiveMap, imageCache ); + if ( info.emissiveMapSampler !== null ) info.emissiveMap = material.emissiveMap; + + } + + // the uv transform is normally updated by the renderer + + if ( info.map !== null && info.map.matrixAutoUpdate === true ) info.map.updateMatrix(); + if ( info.emissiveMap !== null && info.emissiveMap.matrixAutoUpdate === true ) info.emissiveMap.updateMatrix(); + + cache.set( material, info ); + + return info; + +} + +/** + * Returns `true` if the object should contribute geometry to the voxel volume. + * + * @private + */ +function isVoxelizable( object, layers, exclude ) { + + return object.isMesh === true && object.isSkyMesh !== true && object.isBatchedMesh !== true && + layers.test( object.layers ) === true && exclude.has( object ) === false && object.geometry !== undefined; + +} + +/** + * Computes the world-space bounding box of all voxelizable meshes. + * + * @param {Scene} scene - The scene. + * @param {Layers} layers - Only objects that pass the layer test are considered. + * @param {Set} exclude - Objects to skip. + * @param {Box3} target - The target box. + * @return {Box3} The bounding box. + */ +export function computeSceneBounds( scene, layers, exclude, target ) { + + target.makeEmpty(); + + scene.traverseVisible( ( object ) => { + + if ( isVoxelizable( object, layers, exclude ) ) { + + target.expandByObject( object, true ); + + } + + } ); + + return target; + +} + +/** + * Collects the triangles of all meshes in the scene as flat, world-space records suitable + * for GPU voxelization. Per-triangle albedo and emissive colors are resolved on the CPU + * (material color multiplied with a texture lookup at the triangle's centroid). Large + * triangles are subdivided so every record covers a bounded number of voxels. + * + * @param {Scene} scene - The scene. + * @param {Object} options - Options. + * @param {Box3} options.bounds - Triangles outside these bounds are skipped. + * @param {Layers} options.layers - Objects must pass this layer test. + * @param {Set} options.exclude - Objects to skip. + * @param {number} options.subVoxelSize - Size of a sub-voxel in world units. + * @param {number} options.maxEdge - Maximum triangle edge length in sub-voxels before subdivision. + * @param {number} options.minOpacity - Triangles with lower opacity are skipped. + * @return {{data: Float32Array, count: number}} The triangle records and triangle count. + */ +export function collectSceneTriangles( scene, options ) { + + const { bounds, layers, exclude, subVoxelSize, maxEdge, minOpacity } = options; + + const materialCache = new Map(); + const imageCache = new Map(); + const buffer = new GrowableFloat32Array( 1024 * TRIANGLE_STRIDE ); + + const maxEdgeSq = ( maxEdge * subVoxelSize ) * ( maxEdge * subVoxelSize ); + + // triangle emission with subdivision of large triangles + + const stack = []; + + function emit( a, b, c, albedo, emissive, side ) { + + stack.push( a.x, a.y, a.z, b.x, b.y, b.z, c.x, c.y, c.z ); + + while ( stack.length > 0 ) { + + const cz = stack.pop(), cy = stack.pop(), cx = stack.pop(); + const bz = stack.pop(), by = stack.pop(), bx = stack.pop(); + const az = stack.pop(), ay = stack.pop(), ax = stack.pop(); + + const abSq = ( bx - ax ) ** 2 + ( by - ay ) ** 2 + ( bz - az ) ** 2; + const bcSq = ( cx - bx ) ** 2 + ( cy - by ) ** 2 + ( cz - bz ) ** 2; + const caSq = ( ax - cx ) ** 2 + ( ay - cy ) ** 2 + ( az - cz ) ** 2; + const longest = Math.max( abSq, bcSq, caSq ); + + if ( longest > maxEdgeSq ) { + + // split along the longest edge + + if ( longest === abSq ) { + + const mx = ( ax + bx ) * 0.5, my = ( ay + by ) * 0.5, mz = ( az + bz ) * 0.5; + stack.push( ax, ay, az, mx, my, mz, cx, cy, cz ); + stack.push( mx, my, mz, bx, by, bz, cx, cy, cz ); + + } else if ( longest === bcSq ) { + + const mx = ( bx + cx ) * 0.5, my = ( by + cy ) * 0.5, mz = ( bz + cz ) * 0.5; + stack.push( ax, ay, az, bx, by, bz, mx, my, mz ); + stack.push( ax, ay, az, mx, my, mz, cx, cy, cz ); + + } else { + + const mx = ( cx + ax ) * 0.5, my = ( cy + ay ) * 0.5, mz = ( cz + az ) * 0.5; + stack.push( ax, ay, az, bx, by, bz, mx, my, mz ); + stack.push( mx, my, mz, bx, by, bz, cx, cy, cz ); + + } + + continue; + + } + + buffer.ensure( TRIANGLE_STRIDE ); + buffer.push4( ax, ay, az, 0 ); + buffer.push4( bx, by, bz, 0 ); + buffer.push4( cx, cy, cz, 0 ); + buffer.push4( albedo.r, albedo.g, albedo.b, side ); + buffer.push4( emissive.r, emissive.g, emissive.b, 0 ); + + } + + } + + function processMesh( mesh, matrixWorld ) { + + const geometry = mesh.geometry; + const positionAttribute = geometry.getAttribute( 'position' ); + + if ( positionAttribute === undefined ) return; + + const index = geometry.index; + const materials = Array.isArray( mesh.material ) ? mesh.material : [ mesh.material ]; + const vertexCount = index !== null ? index.count : positionAttribute.count; + + let groups = geometry.groups; + + if ( Array.isArray( mesh.material ) === false || groups.length === 0 ) { + + groups = [ { start: 0, count: vertexCount, materialIndex: 0 } ]; + + } + + const drawRange = geometry.drawRange; + const rangeStart = drawRange.start; + const rangeEnd = Math.min( vertexCount, drawRange.start + drawRange.count ); + + for ( const group of groups ) { + + const material = materials[ group.materialIndex ]; + + if ( material === undefined || material === null || material.visible === false ) continue; + + const info = getMaterialInfo( material, materialCache, imageCache ); + + if ( info.opacity < minOpacity ) continue; + + const uvAttribute = info.map !== null ? geometry.getAttribute( info.map.channel > 0 ? 'uv' + info.map.channel : 'uv' ) : null; + const emissiveUVAttribute = info.emissiveMap !== null ? geometry.getAttribute( info.emissiveMap.channel > 0 ? 'uv' + info.emissiveMap.channel : 'uv' ) : null; + + const start = Math.max( group.start, rangeStart ); + const end = Math.min( group.start + group.count, rangeEnd ); + + for ( let i = start; i + 2 < end; i += 3 ) { + + const ia = index !== null ? index.getX( i ) : i; + const ib = index !== null ? index.getX( i + 1 ) : i + 1; + const ic = index !== null ? index.getX( i + 2 ) : i + 2; + + _va.fromBufferAttribute( positionAttribute, ia ).applyMatrix4( matrixWorld ); + _vb.fromBufferAttribute( positionAttribute, ib ).applyMatrix4( matrixWorld ); + _vc.fromBufferAttribute( positionAttribute, ic ).applyMatrix4( matrixWorld ); + + // skip triangles outside the volume + + _box.makeEmpty().expandByPoint( _va ).expandByPoint( _vb ).expandByPoint( _vc ); + + if ( bounds.intersectsBox( _box ) === false ) continue; + + // skip degenerate triangles + + if ( _box.min.distanceToSquared( _box.max ) < 1e-14 ) continue; + + // resolve colors + + let alpha = info.opacity; + + if ( info.color !== null ) _albedo.copy( info.color ); else _albedo.setRGB( 1, 1, 1 ); + + if ( uvAttribute !== undefined && uvAttribute !== null ) { + + _uva.fromBufferAttribute( uvAttribute, ia ); + _uvb.fromBufferAttribute( uvAttribute, ib ); + _uvc.fromBufferAttribute( uvAttribute, ic ); + _uva.add( _uvb ).add( _uvc ).multiplyScalar( 1 / 3 ); + + alpha *= sampleTexture( info.map, info.mapSampler, _uva, _color ); + _albedo.multiply( _color ); + + } + + if ( info.alphaTest > 0 && alpha < info.alphaTest ) continue; + if ( alpha < minOpacity ) continue; + + if ( info.unlit ) { + + _emissive.copy( _albedo ); + _albedo.setRGB( 0, 0, 0 ); + + } else { + + if ( info.emissive !== null ) { + + _emissive.copy( info.emissive ).multiplyScalar( info.emissiveIntensity ); + + if ( emissiveUVAttribute !== undefined && emissiveUVAttribute !== null ) { + + _uva.fromBufferAttribute( emissiveUVAttribute, ia ); + _uvb.fromBufferAttribute( emissiveUVAttribute, ib ); + _uvc.fromBufferAttribute( emissiveUVAttribute, ic ); + _uva.add( _uvb ).add( _uvc ).multiplyScalar( 1 / 3 ); + + sampleTexture( info.emissiveMap, info.emissiveMapSampler, _uva, _color ); + _emissive.multiply( _color ); + + } + + } else { + + _emissive.setRGB( 0, 0, 0 ); + + } + + } + + emit( _va, _vb, _vc, _albedo, _emissive, info.side ); + + } + + } + + } + + scene.traverseVisible( ( object ) => { + + if ( isVoxelizable( object, layers, exclude ) === false ) return; + + if ( object.isInstancedMesh === true ) { + + for ( let i = 0; i < object.count; i ++ ) { + + object.getMatrixAt( i, _matrix ); + _matrix.premultiply( object.matrixWorld ); + processMesh( object, _matrix ); + + } + + } else { + + processMesh( object, object.matrixWorld ); + + } + + } ); + + const data = buffer.result(); + + return { data, count: data.length / TRIANGLE_STRIDE }; + +} diff --git a/examples/jsm/lighting/vxgi/VXGIVolume.js b/examples/jsm/lighting/vxgi/VXGIVolume.js new file mode 100644 index 00000000000000..e6fd357f65d285 --- /dev/null +++ b/examples/jsm/lighting/vxgi/VXGIVolume.js @@ -0,0 +1,1430 @@ +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} + * @default 0 + */ + this.maxDistance = uniform( 0 ); + + /** + * Step size relative to the texel size of the sampled mip level. + * + * @type {UniformNode} + * @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} + * @default 10 + */ + this.shadowConeAngle = uniform( 10 ); + + /** + * Aperture of the cones used for the cached bounces in degrees. + * + * @type {UniformNode} + * @default 60 + */ + this.bounceConeAngle = uniform( 60 ); + + /** + * The minimum corner of the volume. + * + * @type {UniformNode} + */ + this.boundsMinNode = uniform( new Vector3() ); + + /** + * The size of the volume. + * + * @type {UniformNode} + */ + this.volumeSizeNode = uniform( new Vector3( 1, 1, 1 ) ); + + /** + * The size of a voxel. + * + * @type {UniformNode} + */ + this.voxelSizeNode = uniform( 1 ); + + /** + * The highest valid mip level of the voxel textures. + * + * @type {UniformNode} + */ + 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} + */ + 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} + */ + 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 }; diff --git a/examples/screenshots/webgpu_vxgi.jpg b/examples/screenshots/webgpu_vxgi.jpg new file mode 100644 index 00000000000000..6cec2b73c69d14 Binary files /dev/null and b/examples/screenshots/webgpu_vxgi.jpg differ diff --git a/examples/screenshots/webgpu_vxgi_sponza.jpg b/examples/screenshots/webgpu_vxgi_sponza.jpg new file mode 100644 index 00000000000000..689a73eac27609 Binary files /dev/null and b/examples/screenshots/webgpu_vxgi_sponza.jpg differ diff --git a/examples/tags.json b/examples/tags.json index a4c8569cd47e31..5a859d1051c3f4 100644 --- a/examples/tags.json +++ b/examples/tags.json @@ -168,6 +168,8 @@ "webgpu_postprocessing_ssaa": [ "msaa", "multisampled" ], "webgpu_postprocessing_ssgi": [ "global illumination", "indirect diffuse" ], "webgpu_postprocessing_ssgi_ballpool": [ "community", "physics" ], + "webgpu_vxgi": [ "global illumination", "indirect diffuse", "voxel cone tracing" ], + "webgpu_vxgi_sponza": [ "global illumination", "indirect diffuse", "voxel cone tracing" ], "webgpu_postprocessing_sss": [ "shadow" ], "webgpu_refraction": [ "water" ], "webgpu_rtt": [ "renderTarget", "texture" ], diff --git a/examples/webgpu_vxgi.html b/examples/webgpu_vxgi.html new file mode 100644 index 00000000000000..3a28c78ae05925 --- /dev/null +++ b/examples/webgpu_vxgi.html @@ -0,0 +1,302 @@ + + + + three.js webgpu - VXGI + + + + + + + + + + +
+ + +
+ three.jsVXGI +
+ + Diffuse global illumination using Voxel Cone Tracing. +
+ + + + + + diff --git a/examples/webgpu_vxgi_sponza.html b/examples/webgpu_vxgi_sponza.html new file mode 100644 index 00000000000000..3a402c00f85382 --- /dev/null +++ b/examples/webgpu_vxgi_sponza.html @@ -0,0 +1,369 @@ + + + + three.js webgpu - VXGI (Sponza) + + + + + + + + + + +
+ + +
+ three.jsVXGI (Sponza) +
+ + Diffuse global illumination using Voxel Cone Tracing. WASD to move, mouse to look. +
+ + + + + + + + diff --git a/src/Three.TSL.js b/src/Three.TSL.js index 4baa9ef1603986..8f4ce9e61e1a8e 100644 --- a/src/Three.TSL.js +++ b/src/Three.TSL.js @@ -116,6 +116,7 @@ export const buffer = TSL.buffer; export const bufferAttribute = TSL.bufferAttribute; export const builtin = TSL.builtin; export const builtinAOContext = TSL.builtinAOContext; +export const builtinGIContext = TSL.builtinGIContext; export const builtinShadowContext = TSL.builtinShadowContext; export const bumpMap = TSL.bumpMap; export const bvec2 = TSL.bvec2; diff --git a/src/materials/nodes/NodeMaterial.js b/src/materials/nodes/NodeMaterial.js index fb13610027e5cf..558a15b256cf7f 100644 --- a/src/materials/nodes/NodeMaterial.js +++ b/src/materials/nodes/NodeMaterial.js @@ -1015,6 +1015,18 @@ class NodeMaterial extends Material { } + if ( builder.context.getGI ) { + + const giNode = builder.context.getGI( null, builder ); + + if ( giNode !== null ) { + + materialLightsNode.push( new IrradianceNode( giNode ) ); + + } + + } + return materialLightsNode; } diff --git a/src/nodes/core/ContextNode.js b/src/nodes/core/ContextNode.js index 0bee75f01ecf45..3de1528bf04b18 100644 --- a/src/nodes/core/ContextNode.js +++ b/src/nodes/core/ContextNode.js @@ -265,6 +265,50 @@ export function builtinAOContext( aoNode, node = null ) { } +/** + * TSL function for defining a built-in global illumination context for a given node. The AO node + * modulates the indirect lighting of the materials, the GI node is added to their irradiance + * without being modulated by the AO since it already accounts for occlusion. + * + * @tsl + * @function + * @param {Node} aoNode - The ambient occlusion value node to apply. + * @param {Node} giNode - The indirect diffuse irradiance node to add. + * @param {Node} [node=null] - The node whose context should be modified. + * @returns {ContextNode} + */ +export function builtinGIContext( aoNode, giNode, node = null ) { + + // the lighting models multiply the whole indirect diffuse term by the AO, which would occlude + // the GI a second time. Dividing the GI by the AO cancels that out so only the environment + // lighting is modulated. + + const ao = aoNode.max( 1e-3 ); + + return context( node, { + + getAO: ( inputNode, { material } ) => { + + if ( material.transparent === true ) return inputNode; + + return inputNode !== null ? inputNode.mul( ao ) : ao; + + }, + + getGI: ( inputNode, { material } ) => { + + if ( material.transparent === true ) return inputNode; + + const gi = giNode.div( ao ); + + return inputNode !== null ? inputNode.add( gi ) : gi; + + } + + } ); + +} + /** * TSL function for defining a label context value for a given node. * @@ -289,3 +333,4 @@ addMethodChaining( 'uniformFlow', uniformFlow ); addMethodChaining( 'setName', setName ); addMethodChaining( 'builtinShadowContext', ( node, shadowNode, light ) => builtinShadowContext( shadowNode, light, node ) ); addMethodChaining( 'builtinAOContext', ( node, aoValue ) => builtinAOContext( aoValue, node ) ); +addMethodChaining( 'builtinGIContext', ( node, aoValue, giValue ) => builtinGIContext( aoValue, giValue, node ) ); diff --git a/src/nodes/core/NodeBuilder.js b/src/nodes/core/NodeBuilder.js index 7ee73c20b025ea..eaa6f6347c5fce 100644 --- a/src/nodes/core/NodeBuilder.js +++ b/src/nodes/core/NodeBuilder.js @@ -1082,6 +1082,7 @@ class NodeBuilder { delete context.getOutput; delete context.getTextureLevel; delete context.getAO; + delete context.getGI; delete context.getShadow; return context; diff --git a/src/renderers/webgpu/WebGPUBackend.js b/src/renderers/webgpu/WebGPUBackend.js index a53977a43cba12..34b5451a82ccea 100644 --- a/src/renderers/webgpu/WebGPUBackend.js +++ b/src/renderers/webgpu/WebGPUBackend.js @@ -2396,6 +2396,7 @@ class WebGPUBackend extends Backend { data.stencilWrite !== material.stencilWrite || data.stencilFunc !== material.stencilFunc || data.stencilFail !== material.stencilFail || data.stencilZFail !== material.stencilZFail || data.stencilZPass !== material.stencilZPass || data.stencilFuncMask !== material.stencilFuncMask || data.stencilWriteMask !== material.stencilWriteMask || + data.polygonOffset !== material.polygonOffset || data.polygonOffsetFactor !== material.polygonOffsetFactor || data.polygonOffsetUnits !== material.polygonOffsetUnits || data.side !== material.side || data.alphaToCoverage !== material.alphaToCoverage || data.sampleCount !== sampleCount || data.colorSpace !== colorSpace || data.colorFormat !== colorFormat || data.depthStencilFormat !== depthStencilFormat || @@ -2413,6 +2414,7 @@ class WebGPUBackend extends Backend { data.stencilWrite = material.stencilWrite; data.stencilFunc = material.stencilFunc; data.stencilFail = material.stencilFail; data.stencilZFail = material.stencilZFail; data.stencilZPass = material.stencilZPass; data.stencilFuncMask = material.stencilFuncMask; data.stencilWriteMask = material.stencilWriteMask; + data.polygonOffset = material.polygonOffset; data.polygonOffsetFactor = material.polygonOffsetFactor; data.polygonOffsetUnits = material.polygonOffsetUnits; data.side = material.side; data.alphaToCoverage = material.alphaToCoverage; data.sampleCount = sampleCount; data.colorSpace = colorSpace; @@ -2457,7 +2459,8 @@ class WebGPUBackend extends Backend { material.stencilWrite, material.stencilFunc, material.stencilFail, material.stencilZFail, material.stencilZPass, material.stencilFuncMask, material.stencilWriteMask, - material.side, + material.polygonOffset, material.polygonOffsetFactor, material.polygonOffsetUnits, + material.side, material.alphaToCoverage, frontFaceCW, utils.getSampleCountRenderContext( renderContext ), utils.getCurrentColorSpace( renderContext ), utils.getCurrentColorFormat( renderContext ), utils.getCurrentDepthStencilFormat( renderContext ), diff --git a/test/e2e/puppeteer.js b/test/e2e/puppeteer.js index 5de20c544489e0..8d1ca924649b62 100644 --- a/test/e2e/puppeteer.js +++ b/test/e2e/puppeteer.js @@ -58,6 +58,8 @@ const exceptionList = [ 'webgpu_morphtargets_face', 'webgpu_shadowmap_progressive', 'webgpu_postprocessing_ssr_denoise', + 'webgpu_vxgi', + 'webgpu_vxgi_sponza', // Video hangs the CI? 'css3d_youtube', diff --git a/utils/build/preview.js b/utils/build/preview.js index d74a3acd8d4b67..fda8e7eab82299 100644 --- a/utils/build/preview.js +++ b/utils/build/preview.js @@ -1,20 +1,23 @@ import { spawn } from 'child_process'; +import { createRequire } from 'module'; import path from 'path'; import { fileURLToPath } from 'url'; const __dirname = path.dirname( fileURLToPath( import.meta.url ) ); const rootDir = path.resolve( __dirname, '../..' ); +const rollupBin = createRequire( import.meta.url ).resolve( 'rollup/dist/bin/rollup' ); + // Start rollup in watch mode -const rollup = spawn( 'npx', [ - 'rollup', +const rollup = spawn( process.execPath, [ + rollupBin, '-c', 'utils/build/rollup.config.js', '-w', '-m', 'inline' ], { cwd: rootDir, stdio: [ 'ignore', 'pipe', 'pipe' ], - shell: true + shell: false } ); // Start server