From ef58888296cba376df8ae86508460198159345a9 Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Sat, 15 Aug 2026 21:44:23 +0900 Subject: [PATCH 1/3] GaussianSplatMesh: Add support for per-mesh bounding box, frustum culling (#34254) --- examples/jsm/objects/GaussianSplatMesh.js | 146 +++++++++++++++++++++- src/math/MathUtils.js | 2 + 2 files changed, 144 insertions(+), 4 deletions(-) diff --git a/examples/jsm/objects/GaussianSplatMesh.js b/examples/jsm/objects/GaussianSplatMesh.js index f81363ed5ac55d..0bb59dafd8052c 100644 --- a/examples/jsm/objects/GaussianSplatMesh.js +++ b/examples/jsm/objects/GaussianSplatMesh.js @@ -1,9 +1,13 @@ import { + Box3, BufferAttribute, + Color, InstancedBufferGeometry, + Matrix3, Matrix4, Mesh, NodeMaterial, + Sphere, StorageBufferAttribute, Vector2, Vector3 @@ -48,6 +52,7 @@ const BIN_COUNT = 4096; const WORKGROUP_SIZE = 256; const SORT_DIRECTION_THRESHOLD = 0.9995; const KERNEL_2D_SIZE = 0.3; +const SPLAT_KERNEL_CUTOFF = 2; const MAX_SCREEN_SPACE_SPLAT_SIZE = 1024; const CLIP_XY = 1.4; @@ -58,6 +63,8 @@ const _sortDirection = /*@__PURE__*/ new Vector3(); const _sortDepthRange = /*@__PURE__*/ new Vector2(); const _worldMatrixInverse = /*@__PURE__*/ new Matrix4(); const _modelViewMatrix = /*@__PURE__*/ new Matrix4(); +const _splatBox = /*@__PURE__*/ new Box3(); +const _splat = {}; /** * A minimal renderer for 3D Gaussian splat geometry. @@ -128,6 +135,22 @@ class GaussianSplatMesh extends Mesh { */ this.splatGeometry = splatGeometry; + /** + * The bounding box of the splats. Can be computed via {@link GaussianSplatMesh#computeBoundingBox}. + * + * @type {?Box3} + * @default null + */ + this.boundingBox = null; + + /** + * The bounding sphere of the splats. Can be computed via {@link GaussianSplatMesh#computeBoundingSphere}. + * + * @type {?Sphere} + * @default null + */ + this.boundingSphere = null; + /** * Whether to sort automatically in `onBeforeRender`. * @@ -135,8 +158,6 @@ class GaussianSplatMesh extends Mesh { */ this.autoSort = autoSort; - this.frustumCulled = false; - this._buffers = buffers; this._sort = sort; this._sortMatrix = uniform( new Matrix4() ); @@ -239,6 +260,121 @@ class GaussianSplatMesh extends Mesh { } + /** + * Returns the splat at the given index. + * + * Members that the target does not already have are created, so an empty object can be passed + * and then reused across calls to avoid allocating. + * + * @param {number} index - The splat index. + * @param {Object} [target] - The object the splat is written to. + * @return {Object} The target, with `position`, `covariance`, `color`, `opacity` and `radius` set. + */ + getSplat( index, target = {} ) { + + const geometry = this.splatGeometry; + const positionAttribute = geometry.getAttribute( 'position' ); + const covarianceAttribute = geometry.getAttribute( 'covariance' ); + const colorAttribute = geometry.getAttribute( 'color' ); + + if ( target.position === undefined ) { + + target.position = new Vector3(); + + } + + if ( target.covariance === undefined ) { + + target.covariance = new Matrix3(); + + } + + if ( target.color === undefined ) { + + target.color = new Color(); + + } + + target.position.fromBufferAttribute( positionAttribute, index ); + + const c00 = covarianceAttribute.getComponent( index, 0 ); + const c01 = covarianceAttribute.getComponent( index, 1 ); + const c02 = covarianceAttribute.getComponent( index, 2 ); + const c11 = covarianceAttribute.getComponent( index, 3 ); + const c12 = covarianceAttribute.getComponent( index, 4 ); + const c22 = covarianceAttribute.getComponent( index, 5 ); + + // the attribute holds the upper triangle of the symmetric covariance + target.covariance.set( + c00, c01, c02, + c01, c11, c12, + c02, c12, c22 + ); + + target.color.fromBufferAttribute( colorAttribute, index ); + target.opacity = colorAttribute.getW( index ); + + // the radius of the drawn largest extent + target.radius = SPLAT_KERNEL_CUTOFF * Math.sqrt( Math.max( c00, c11, c22, 0 ) ); + + return target; + + } + + /** + * Computes the bounding box of the splats, updating {@link GaussianSplatMesh#boundingBox}. + * + * Each splat is expanded by its own extent rather than treated as a point, so the bounds cover + * what is drawn. + */ + computeBoundingBox() { + + if ( this.boundingBox === null ) this.boundingBox = new Box3(); + + this.boundingBox.makeEmpty(); + + const count = this.splatGeometry.getAttribute( 'position' ).count; + + for ( let i = 0; i < count; i ++ ) { + + this.getSplat( i, _splat ); + + _splatBox.set( _splat.position, _splat.position ).expandByScalar( _splat.radius ); + this.boundingBox.union( _splatBox ); + + } + + } + + /** + * Computes the bounding sphere of the splats, updating {@link GaussianSplatMesh#boundingSphere}. + * + * Each splat is expanded by its own extent rather than treated as a point, so the bounds cover + * what is drawn. + */ + computeBoundingSphere() { + + if ( this.boundingSphere === null ) this.boundingSphere = new Sphere(); + + this.computeBoundingBox(); + this.boundingBox.getBoundingSphere( this.boundingSphere ); + + let maxRadius = 0; + const center = this.boundingSphere.center; + const count = this.splatGeometry.getAttribute( 'position' ).count; + + for ( let i = 0; i < count; i ++ ) { + + this.getSplat( i, _splat ); + + maxRadius = Math.max( maxRadius, center.distanceTo( _splat.position ) + _splat.radius ); + + } + + this.boundingSphere.radius = maxRadius; + + } + /** * Updates the draw order if the camera or mesh orientation has changed enough * to need a new sort. @@ -295,12 +431,14 @@ class GaussianSplatMesh extends Mesh { this._sortMatrix.value.multiplyMatrices( camera.matrixWorldInverse, this.matrixWorld ); - _worldCenter.copy( this.splatGeometry.boundingSphere.center ).applyMatrix4( this.matrixWorld ); + if ( this.boundingSphere === null ) this.computeBoundingSphere(); + + _worldCenter.copy( this.boundingSphere.center ).applyMatrix4( this.matrixWorld ); _viewCenter.copy( _worldCenter ).applyMatrix4( camera.matrixWorldInverse ); _worldScale.setFromMatrixScale( this.matrixWorld ); - const radius = this.splatGeometry.boundingSphere.radius * Math.max( _worldScale.x, _worldScale.y, _worldScale.z ); + const radius = this.boundingSphere.radius * Math.max( _worldScale.x, _worldScale.y, _worldScale.z ); const depth = - _viewCenter.z; const nearDepth = Math.max( camera.near, depth - radius ); const farDepth = Math.max( nearDepth + 0.0001, depth + radius ); diff --git a/src/math/MathUtils.js b/src/math/MathUtils.js index c4013a93574e7c..1bff57c25209e3 100644 --- a/src/math/MathUtils.js +++ b/src/math/MathUtils.js @@ -402,6 +402,7 @@ function denormalize( value, array ) { return value / 65535.0; case Uint8Array: + case Uint8ClampedArray: return value / 255.0; @@ -449,6 +450,7 @@ function normalize( value, array ) { return Math.round( value * 65535.0 ); case Uint8Array: + case Uint8ClampedArray: return Math.round( value * 255.0 ); From 7e715e6c73f52e0710212dd5f3754224f831083f Mon Sep 17 00:00:00 2001 From: mrdoob Date: Sun, 16 Aug 2026 00:16:52 +0900 Subject: [PATCH 2/3] SunLight: Clean up. (#34258) Co-authored-by: Claude Fable 5 --- src/Three.Core.js | 1 - src/lights/SunLight.js | 9 +++-- src/lights/SunLightShadow.js | 40 ++++++++++++++----- .../ShaderChunk/lights_pars_begin.glsl.js | 1 + .../shadowmap_pars_fragment.glsl.js | 4 +- src/renderers/webgl/WebGLLights.js | 8 ++-- src/renderers/webgl/WebGLShadowMap.js | 3 +- 7 files changed, 43 insertions(+), 23 deletions(-) diff --git a/src/Three.Core.js b/src/Three.Core.js index 3186280a6c5ffc..211c3d49581798 100644 --- a/src/Three.Core.js +++ b/src/Three.Core.js @@ -61,7 +61,6 @@ export { PointLight } from './lights/PointLight.js'; export { RectAreaLight } from './lights/RectAreaLight.js'; export { HemisphereLight } from './lights/HemisphereLight.js'; export { SunLight } from './lights/SunLight.js'; -export { SunLightShadow } from './lights/SunLightShadow.js'; export { DirectionalLight } from './lights/DirectionalLight.js'; export { AmbientLight } from './lights/AmbientLight.js'; export { Light } from './lights/Light.js'; diff --git a/src/lights/SunLight.js b/src/lights/SunLight.js index dd38710367d32d..189a50d4ced37b 100644 --- a/src/lights/SunLight.js +++ b/src/lights/SunLight.js @@ -1,5 +1,6 @@ import { Light } from './Light.js'; import { SunLightShadow } from './SunLightShadow.js'; +import { Object3D } from '../core/Object3D.js'; /** * A sun-like light that gets emitted in a specific direction, with rays that @@ -46,16 +47,16 @@ class SunLight extends Light { this.type = 'SunLight'; + this.position.copy( Object3D.DEFAULT_UP ); + this.updateMatrix(); + /** - * The light's shadow configuration. + * This property holds the light's shadow configuration. * * @type {SunLightShadow} */ this.shadow = new SunLightShadow(); - this.position.set( 0, 1, 0 ); - this.updateMatrix(); - } dispose() { diff --git a/src/lights/SunLightShadow.js b/src/lights/SunLightShadow.js index 85a2db5c4fd39d..bd4f791ceed6f9 100644 --- a/src/lights/SunLightShadow.js +++ b/src/lights/SunLightShadow.js @@ -10,10 +10,31 @@ const _viewToLightMatrix = /*@__PURE__*/ new Matrix4(); const _lightDirection = /*@__PURE__*/ new Vector3(); const _up = /*@__PURE__*/ new Vector3(); const _center = /*@__PURE__*/ new Vector3(); -const _corner = /*@__PURE__*/ new Vector3(); -const _nearCorners = [ new Vector3(), new Vector3(), new Vector3(), new Vector3() ]; -const _farCorners = [ new Vector3(), new Vector3(), new Vector3(), new Vector3() ]; -const _cascadeCorners = [ new Vector3(), new Vector3(), new Vector3(), new Vector3(), new Vector3(), new Vector3(), new Vector3(), new Vector3() ]; + +const _nearCorners = [ + /*@__PURE__*/ new Vector3(), + /*@__PURE__*/ new Vector3(), + /*@__PURE__*/ new Vector3(), + /*@__PURE__*/ new Vector3() +]; + +const _farCorners = [ + /*@__PURE__*/ new Vector3(), + /*@__PURE__*/ new Vector3(), + /*@__PURE__*/ new Vector3(), + /*@__PURE__*/ new Vector3() +]; + +const _cascadeCorners = [ + /*@__PURE__*/ new Vector3(), + /*@__PURE__*/ new Vector3(), + /*@__PURE__*/ new Vector3(), + /*@__PURE__*/ new Vector3(), + /*@__PURE__*/ new Vector3(), + /*@__PURE__*/ new Vector3(), + /*@__PURE__*/ new Vector3(), + /*@__PURE__*/ new Vector3() +]; // must match the cascade count in the sun shadow shader chunks @@ -140,6 +161,10 @@ class SunLightShadow extends LightShadow { } + const resolutionX = this.mapSize.x * ( 1 - 2 * insetX ); + const resolutionY = this.mapSize.y * ( 1 - 2 * insetY ); + const resolution = Math.min( resolutionX, resolutionY ); + const camera = this.camera; const cameraNear = viewCamera.near; const cameraFar = Math.max( cameraNear + 1e-6, Math.min( camera.far, viewCamera.far ) ); @@ -249,10 +274,6 @@ class SunLightShadow extends LightShadow { // snap to the texel grid to avoid shimmering when the view camera moves - const resolutionX = this.mapSize.width * this._viewports[ i ].z; - const resolutionY = this.mapSize.height * this._viewports[ i ].w; - const resolution = Math.min( resolutionX, resolutionY ); - if ( resolution > 1 ) { // pad by half a texel so snapping cannot clip a frustum corner @@ -271,8 +292,7 @@ class SunLightShadow extends LightShadow { const cascadeCamera = this._cameras[ i ]; cascadeCamera.position.copy( _center ); - cascadeCamera.up.copy( _up ); - cascadeCamera.lookAt( _corner.copy( _center ).add( _lightDirection ) ); + cascadeCamera.quaternion.setFromRotationMatrix( _lightOrientationMatrix ); cascadeCamera.left = - radius; cascadeCamera.right = radius; cascadeCamera.top = radius; diff --git a/src/renderers/shaders/ShaderChunk/lights_pars_begin.glsl.js b/src/renderers/shaders/ShaderChunk/lights_pars_begin.glsl.js index 943d78279cf0b7..7f81748bf3fd9e 100644 --- a/src/renderers/shaders/ShaderChunk/lights_pars_begin.glsl.js +++ b/src/renderers/shaders/ShaderChunk/lights_pars_begin.glsl.js @@ -95,6 +95,7 @@ float getSpotAttenuation( const in float coneCosine, const in float penumbraCosi #endif + #if NUM_DIR_LIGHTS > 0 struct DirectionalLight { diff --git a/src/renderers/shaders/ShaderChunk/shadowmap_pars_fragment.glsl.js b/src/renderers/shaders/ShaderChunk/shadowmap_pars_fragment.glsl.js index 7cc8fc00f79457..748b3beaa7b001 100644 --- a/src/renderers/shaders/ShaderChunk/shadowmap_pars_fragment.glsl.js +++ b/src/renderers/shaders/ShaderChunk/shadowmap_pars_fragment.glsl.js @@ -296,7 +296,7 @@ export default /* glsl */` sampler2D shadowMap, #endif SunLightShadow sunLightShadow, - const in int shadowIndex + int shadowIndex ) { vec4 shadowWorldPosition = vec4( vSunShadowWorldPosition.xyz + vSunShadowWorldNormal * sunLightShadow.shadowNormalBias, 1.0 ); @@ -317,7 +317,7 @@ export default /* glsl */` float cascadeShadow = getShadow( shadowMap, sunLightShadow.shadowMapSize, sunLightShadow.shadowIntensity, sunLightShadow.shadowBias, sunLightShadow.shadowRadius, sunShadowMatrix[ cascadeOffset + i ] * shadowWorldPosition ); - shadow = viewDepth < cascade.z ? cascadeShadow : mix( cascadeShadow, shadow, smoothstep( cascade.z, cascade.y, viewDepth ) ); + shadow = mix( cascadeShadow, shadow, smoothstep( cascade.z, cascade.y, viewDepth ) ); } diff --git a/src/renderers/webgl/WebGLLights.js b/src/renderers/webgl/WebGLLights.js index 52c5ce19766757..5c0855221f1974 100644 --- a/src/renderers/webgl/WebGLLights.js +++ b/src/renderers/webgl/WebGLLights.js @@ -308,10 +308,10 @@ function WebGLLights( extensions ) { // four cascades per sun light, matching the sun shadow shader chunks - for ( let i = 0; i < 4; i ++ ) { + for ( let j = 0; j < 4; j ++ ) { - state.sunShadowMatrix[ numSunShadows * 4 + i ] = shadow.getMatrix( i ); - state.sunShadowCascade[ numSunShadows * 4 + i ] = shadow._cascadeData[ i ]; + state.sunShadowMatrix[ numSunShadows * 4 + j ] = shadow.getMatrix( j ); + state.sunShadowCascade[ numSunShadows * 4 + j ] = shadow._cascadeData[ j ]; } @@ -517,9 +517,9 @@ function WebGLLights( extensions ) { state.directionalShadowMatrix.length = numDirectionalShadows; state.pointShadow.length = numPointShadows; state.pointShadowMap.length = numPointShadows; + state.pointShadowMatrix.length = numPointShadows; state.spotShadow.length = numSpotShadows; state.spotShadowMap.length = numSpotShadows; - state.pointShadowMatrix.length = numPointShadows; state.spotLightMatrix.length = numSpotShadows + numSpotMaps - numSpotShadowsWithMaps; state.spotLightMap.length = numSpotMaps; state.numSpotLightShadowsWithMaps = numSpotShadowsWithMaps; diff --git a/src/renderers/webgl/WebGLShadowMap.js b/src/renderers/webgl/WebGLShadowMap.js index dcc5b906355fdc..a94ac763d67fdc 100644 --- a/src/renderers/webgl/WebGLShadowMap.js +++ b/src/renderers/webgl/WebGLShadowMap.js @@ -171,7 +171,6 @@ function WebGLShadowMap( renderer, objects, capabilities ) { _shadowMapSize.copy( shadow.mapSize ); - const viewportCount = shadow.getViewportCount(); const shadowFrameExtents = shadow.getFrameExtents(); _shadowMapSize.multiply( shadowFrameExtents ); @@ -287,7 +286,7 @@ function WebGLShadowMap( renderer, objects, capabilities ) { // For cube render targets (PointLights), render all 6 faces. Sun lights // render one atlas viewport per cascade. - const faceCount = shadow.map.isWebGLCubeRenderTarget ? 6 : viewportCount; + const faceCount = shadow.map.isWebGLCubeRenderTarget ? 6 : shadow.getViewportCount(); if ( light.isPointLight !== true ) shadow.updateMatrices( light, camera ); From ac7be9191a5dcb7270c871381f1e6e6bb2fff27a Mon Sep 17 00:00:00 2001 From: Michael Herzog Date: Sat, 15 Aug 2026 17:48:38 +0200 Subject: [PATCH 3/3] OITPassNode: Add node for Order Independent Transparency. (#34253) --- examples/files.json | 1 + examples/jsm/tsl/display/OITPassNode.js | 331 ++++++++++++++++++++++++ examples/screenshots/webgpu_oit.jpg | Bin 0 -> 12997 bytes examples/webgpu_oit.html | 205 +++++++++++++++ 4 files changed, 537 insertions(+) create mode 100644 examples/jsm/tsl/display/OITPassNode.js create mode 100644 examples/screenshots/webgpu_oit.jpg create mode 100644 examples/webgpu_oit.html diff --git a/examples/files.json b/examples/files.json index 8ab08300fb3e07..0f28782c997e8c 100644 --- a/examples/files.json +++ b/examples/files.json @@ -421,6 +421,7 @@ "webgpu_multisampled_renderbuffers", "webgpu_occlusion", "webgpu_ocean", + "webgpu_oit", "webgpu_parallax_uv", "webgpu_particles", "webgpu_particles_soft", diff --git a/examples/jsm/tsl/display/OITPassNode.js b/examples/jsm/tsl/display/OITPassNode.js new file mode 100644 index 00000000000000..ef9170afee683c --- /dev/null +++ b/examples/jsm/tsl/display/OITPassNode.js @@ -0,0 +1,331 @@ +import { PassNode, RenderTarget, BlendMode, RendererUtils, Vector2, HalfFloatType, UnsignedByteType, RedFormat, CustomBlending, NormalBlending, OneFactor, ZeroFactor, OneMinusSrcColorFactor } from 'three/webgpu'; +import { float, mix, mrt, output, positionView, texture, vec4 } from 'three/tsl'; + +const _size = /*@__PURE__*/ new Vector2(); + +let _rendererState, _sceneState; + +/** + * A render pass node that renders the scene with Order-Independent Transparency + * based on the Weighted Blended OIT technique by McGuire and Bavoil. + * + * Transparent objects are rendered in a separate pass into two accumulation + * targets (a weighted color sum and the pixel's revealage) which are + * then composited over the rest of the scene. Since the result does not depend + * on the draw order, artifacts from sorting-based transparency like popping or + * incorrectly resolved intersecting geometry are avoided. + * + * Only transparent materials using `NormalBlending` and no transmission qualify + * for OIT. All other objects are rendered as usual. MSAA is not supported. + * + * MRT configurations assigned via `setMRT()` apply to the default pass only. + * OIT-qualified objects contribute to the color output but not to custom + * MRT outputs since a pixel may accumulate multiple transparent surfaces. + * + * ```js + * const renderPipeline = new THREE.RenderPipeline( renderer ); + * renderPipeline.outputNode = oitPass( scene, camera ); + * ``` + * + * References: + * - {@link https://jcgt.org/published/0002/02/09/} + * - {@link https://casual-effects.blogspot.com/2014/03/weighted-blended-order-independent.html} + * + * @augments PassNode + * @three_import import { oitPass } from 'three/addons/tsl/display/OITPassNode.js'; + */ +class OITPassNode extends PassNode { + + static get type() { + + return 'OITPassNode'; + + } + + /** + * Constructs a new OIT pass node. + * + * @param {Scene} scene - The scene to render. + * @param {Camera} camera - The camera to render the scene with. + * @param {Object} [options={}] - Options for the internal render target. + */ + constructor( scene, camera, options = {} ) { + + super( PassNode.COLOR, scene, camera, { ...options, samples: 0 } ); + + /** + * This flag can be used for type testing. + * + * @type {boolean} + * @readonly + * @default true + */ + this.isOITPassNode = true; + + /** + * The depth-based weight of a transparent fragment, see equations (7) to (9) + * in the paper. When `null`, equation (9) is used. Must be assigned before + * the first render. + * + * @type {?Node} + * @default null + */ + this.weightNode = null; + + // the accumulation target shares the depth of the default pass so transparent + // fragments are depth-tested against the opaque scene (without depth writes) + + const oitRenderTarget = new RenderTarget( 1, 1, { count: 2 } ); + oitRenderTarget.depthTexture = this.renderTarget.depthTexture; + + const accumTexture = oitRenderTarget.textures[ 0 ]; // RGBA16 + accumTexture.name = 'accum'; + accumTexture.type = HalfFloatType; + + const revealageTexture = oitRenderTarget.textures[ 1 ]; // R8 + revealageTexture.name = 'revealage'; + revealageTexture.format = RedFormat; + revealageTexture.type = UnsignedByteType; + + /** + * The render target holding the OIT accumulation textures. + * + * @private + * @type {RenderTarget} + */ + this._oitRenderTarget = oitRenderTarget; + + /** + * The MRT configuration for the OIT pass. + * + * @private + * @type {?MRTNode} + */ + this._oitMRTNode = null; + + /** + * The renderer of the current frame. + * + * @private + * @type {?Renderer} + */ + this._renderer = null; + + /** + * Renders opaque objects and transparent objects that do not qualify for OIT. + * + * @private + * @type {Function} + */ + this._defaultRenderObjectFunction = ( object, scene, camera, geometry, material, group, lightsNode, clippingContext, passId ) => { + + if ( isOITCapable( material ) === false ) { + + this._renderer.renderObject( object, scene, camera, geometry, material, group, lightsNode, clippingContext, passId ); + + } + + }; + + /** + * Renders OIT-qualified objects into the accumulation targets. + * + * @private + * @type {Function} + */ + this._oitRenderObjectFunction = ( object, scene, camera, geometry, material, group, lightsNode, clippingContext, passId ) => { + + if ( isOITCapable( material ) === true ) { + + const currentDepthWrite = material.depthWrite; + + material.depthWrite = false; + + this._renderer.renderObject( object, scene, camera, geometry, material, group, lightsNode, clippingContext, passId ); + + material.depthWrite = currentDepthWrite; + + } + + }; + + } + + /** + * Returns the MRT configuration for the OIT pass. + * + * @private + * @return {MRTNode} The MRT node. + */ + _getMRTNode() { + + if ( this._oitMRTNode === null ) { + + const alpha = output.a; + + let weight = this.weightNode; + + if ( weight === null ) { + + // equation (9) from the paper, based on the linear eye-space depth + + const z = positionView.z.negate(); + + weight = alpha.mul( float( 0.03 ).div( z.div( 200 ).pow( 4 ).add( 1e-5 ) ).clamp( 1e-2, 3e3 ) ); + + } + + // since the revealage target is single-channel, the alpha must be blended + // via its red channel + + const accumBlending = new BlendMode( CustomBlending ); + accumBlending.blendSrc = OneFactor; + accumBlending.blendDst = OneFactor; + + const revealageBlending = new BlendMode( CustomBlending ); + revealageBlending.blendSrc = ZeroFactor; + revealageBlending.blendDst = OneMinusSrcColorFactor; + + this._oitMRTNode = mrt( { + accum: vec4( output.rgb.mul( alpha ), alpha ).mul( weight ), + revealage: alpha + } ).setBlendMode( 'accum', accumBlending ).setBlendMode( 'revealage', revealageBlending ) + .setClearColor( 'accum', 0x000000, 0 ).setClearColor( 'revealage', 0xffffff, 1 ); + + } + + return this._oitMRTNode; + + } + + setSize( width, height ) { + + super.setSize( width, height ); + + this._oitRenderTarget.setSize( this.renderTarget.width, this.renderTarget.height ); + + } + + setup( builder ) { + + const beautyNode = super.setup( builder ); + + const accumNode = texture( this._oitRenderTarget.textures[ 0 ] ); + const revealageNode = texture( this._oitRenderTarget.textures[ 1 ] ).r; + + const accumColor = accumNode.rgb.div( accumNode.a.max( 1e-5 ) ); + + return vec4( mix( accumColor, beautyNode.rgb, revealageNode ), beautyNode.a ); + + } + + updateBefore( frame ) { + + const { renderer } = frame; + const { scene, camera } = this; + + this._renderer = renderer; + + renderer.getDrawingBufferSize( _size ); + this.setSize( _size.width, _size.height ); + + _rendererState = RendererUtils.saveRendererState( renderer, _rendererState ); + + const currentAutoClearColor = renderer.autoClearColor; + const currentAutoClearDepth = renderer.autoClearDepth; + const currentAutoClearStencil = renderer.autoClearStencil; + const currentTransparent = renderer.transparent; + const currentOpaque = renderer.opaque; + const currentMask = camera.layers.mask; + + this._cameraNear.value = camera.near; + this._cameraFar.value = camera.far; + + if ( this._layers !== null ) { + + camera.layers.mask = this._layers.mask; + + } + + renderer.autoClear = this.autoClear; + renderer.autoClearColor = this.autoClearColor; + renderer.autoClearDepth = this.autoClearDepth; + renderer.autoClearStencil = this.autoClearStencil; + + // default pass: opaque objects and transparent objects that do not qualify for OIT + + renderer.setMRT( this._mrt ); + renderer.setRenderTarget( this.renderTarget ); + renderer.setRenderObjectFunction( this._defaultRenderObjectFunction ); + + renderer.render( scene, camera ); + + // OIT pass: accumulate the weighted colors and the revealage of all OIT-qualified objects + + _sceneState = RendererUtils.resetSceneState( scene, _sceneState ); // the background must not affect the accumulation targets + + renderer.setRenderTarget( this._oitRenderTarget ); + renderer.setMRT( this._getMRTNode() ); + renderer.setRenderObjectFunction( this._oitRenderObjectFunction ); + renderer.autoClearDepth = false; // the depth buffer is shared with the default pass + renderer.opaque = false; + renderer.transparent = true; + + renderer.render( scene, camera ); + + // restore + + RendererUtils.restoreSceneState( scene, _sceneState ); + RendererUtils.restoreRendererState( renderer, _rendererState ); + + renderer.autoClearColor = currentAutoClearColor; + renderer.autoClearDepth = currentAutoClearDepth; + renderer.autoClearStencil = currentAutoClearStencil; + renderer.transparent = currentTransparent; + renderer.opaque = currentOpaque; + + camera.layers.mask = currentMask; + + this._renderer = null; + + } + + dispose() { + + super.dispose(); + + this._oitRenderTarget.dispose(); + + } + +} + +/** + * Returns `true` if the given material qualifies for OIT. + * + * @param {Material} material - The material to check. + * @return {boolean} Whether the material qualifies for OIT or not. + */ +function isOITCapable( material ) { + + return material.transparent === true && material.blending === NormalBlending && + ( material.transmission > 0 ) === false && + ! ( material.transmissionNode && material.transmissionNode.isNode ) && + ! ( material.backdropNode && material.backdropNode.isNode ); + +} + + +export default OITPassNode; + +/** + * TSL function for creating an OIT pass node. + * + * @tsl + * @function + * @param {Scene} scene - The scene to render. + * @param {Camera} camera - The camera to render the scene with. + * @param {Object} [options={}] - Options for the internal render target. + * @returns {OITPassNode} + */ +export const oitPass = ( scene, camera, options ) => new OITPassNode( scene, camera, options ); diff --git a/examples/screenshots/webgpu_oit.jpg b/examples/screenshots/webgpu_oit.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5ce4fbe8144060ef9a82592edf78b39f1c9c0d89 GIT binary patch literal 12997 zcmeHtcQjmI*Z+tF5g{Ui5E3QIC_(fxAwdvC??#k}9-_BN^eza#di1ChJp`jguQ5z? z2BS0j3{!r-&-4D?cm39HJ?nj+{QaK0?zuDT-m~uB=j`*@dw=dG{w6K~ZmB7&Dg#JJ z005H913;VwgaWRR{N4WUul(JvlK$OayLy%MD%mwMvVWZ9*D1)zualFJQQV-oPWgAc z{6%$xlIrh2{~UJZ8Y$^DN^&ytf874JI$}G3mI822@`;p$7I1}@gp`(q*iDjuDdV+& zh;}LHKO4yv(yQ0V$S*~>aqCiSQqoIduU$%d`DEbbJ-}7kYquW?E0ED?zb0pQyCd>3 zF^_^nvATm^XB^EbYULh$o$@XNBhx*uN8CKTd{4y0B_yS!pFLMnR(YYSrmLrKU}$7) zVr^sl#_p}X!+Q@;FK-`TzmU+d@QBE$=%nP&DXD3HrDx<96c!bid@U`jsjaJTX#C#P z+}YLrv!}POf8f^yd~yo$dwOOWxw5*3THn~*IygK!J~=(ZV9!Yar2kasvj6||xm29w z%HMNN{Vq#u($8`=-9W=d_`#9xY&L_)U z2Y;*d4~_n33I+d>M*peMf9gY=0Z@^WTrLRt1U`tNDHf7CalqZqRPL2J_p3GBM5sBc@d_7mknLATF!IlZ+0pt#&X)6O97BIX{wwbddad^2wl zmwN7p#oG^JWlhZJZo4%A>#q9ax+!O*2&UW>8CcM-^_#9#---$t4SCW#p}I$>rtbt6&j8#+x(d|r5G`6L3CpT?zN)quTI$KJfThzr&3QeqW3iV z9+(20l|{oh2P1@J8H^7dVs{NdfiThFMhZ8jt6ASX<+a_3fL|_pP3VXi{?B6TVJD0s zEWD`$N~0*C(aPgwjDr$xyvy|A=;KZ4?bAJw3@rJf}Ych&BlcD{Xf`6zo6Zu>2~ z1Qb^6X3%N%ZBx2#jrmL?$hK0nDd^rim_96V*`Gu-G=0p-z1r zQmQ86$x)_-1fzrlxR8yOQOj{plimSpk~s596DXh=4H-5RMvz;tSxxt@~#j81T5Q%F~r^yvW}F3O$D(L0be4c zVI)D1Yp=1Av@uCCQIexH*k+8_u$WLxLBucE6bFU~__RnDK!qt;+NFlv_DSv}0^Za# z%&u#kZm_waC?yR!aYN*!t#{tD-1m#lmNJKN6P|UriL3l&WgXEqiOPc74sz8vZy{6r zR?EMi1^*Gck-9For4?-F$d4au1YPH>ZG~Gkrn5U2?pqdQ#Zo2aIw9l*J$aW)Bwdmr zZm4zoYDTB3dWW05Jp$!eV6G;lOBhFeIRhL-0G}ZU=ia7r0Tw1eIv|Od(ew3g*bxyx z1aL(V0qlH1Lom|ukc6vLA&J?_s|kJm!)!CykPApK5%5r66lqYoO9UJmK@K0ShqsXh zaD;UG1&sSpc9;uU*;+;fvsj)~$r@P4rQWrN`-A%S`Xoytt#e=B>mDFX z&MO3^?;dUuOu$~jz73Bkf0c8u4G{rDQ71An4cpcJJYpxm%_6RSh?_yBd7lgB=BBT= zD5=HIvK!@vJNZ@vt&7*l={?saae`ewPU4RirYH?&HbFzF8bI}2yjP|RX0w@I5)WJw z7TfNnlhJ_so&-)KWFt7bJLP9@Xd29*V;UDCZP9ySyn~yMsUTjDVFT4hb7o$d7b6Z? zOr3v)XQB0ucj?*ikGe&D)Vpc`!^J#Afah-rhe@w+PY;0zSR<&2fvF{dws4{Er7Zmx#$~>V7CVC*HJ_cg_Z77cs;L%fj!}?#uqJuI<>pc_WZ7!1 zkH%?AFOaQ@W}HfvRa*qqc%Nt|tQ|>$XBd_}+o0%1?eSz&pCI@cUsD53T~s0w&>|xU zgb&pA>-H$SWbXzwY;KI2hRagsQ9mp)2$w6O2bSt;-~?PsShv*!Mq^s6moY6sq$!l9 z>ucawlkd8qlv=l$>sWWU;1}DoV&^V2w+?*XPPx^eTcEH6L#kxS@{eI!Z%gZ%DhIu7 z7SjC1r_N_kww|n#uSlL=1Ka`pTbFx80L?zER@OJ)!NPzDsNFjKwKE?#YpWZ3x@lx( zJsO_nUxH;}suXfgL6rEvS5)7Rk)MZ-CZLwS+_9hrq{y4-hBkR{ML1#}Ygp72}` zw!4q#&GCe+44!XeC)D7QoIB;XmK8~@@gN~GoZ>zTLoED(-O7hMPaf^ZuD9Vj_Yvc( ztEn{=4B8`0pOVNVPsKzvU)eJ+JM_dqqUWkJfxMTqY7@?#q@~P>cu8@gBYo?YNBg?i zD3(ft2q3xi9ZEsfUAc$s1w?==WK&0WPUT>6@KC{V%uwIj7C*^_geJ#aA$HofGX>NfXWTri~5jmrLz_{p_-txTAnJyG_AZy(v-mor{gL}O4O@$mA zeOv>_*9L)C10yID@P6)_gV=h4N~ipj9AGZv7UHq^YO-#2Vs&f?2Q9eHGJhqwh@m+a z!U>T-xcS0h^uY7MUXek8MZ8PTpBm{kK;Xm|b1nZoV~ctD%{O&%t4<`TeAi|D5$pOo zajIPNxkJCdD(CPOa?~WNuMT>WkvwJnPQmeD^&X}q4{uJE(M*{WyBWS3SbCoafrfkx zFkabJzW{cca3YdoeZ$CZKlZz8-5F1#JJXOCQ5l&gAokvtr^7rmV|}Noe536_D&ptD z!*HpWZ;>)wf@FFSI_t56gkHo+Y;@>Xo8wJ&PY9kD20sE0OAqJomh2oXt-YeIE~Sfq zkqRQPH*ZbE2xhB3Ok1q&EMs~ibnA9|cJ;hU$1!tS$(n^c_f3zTQN=h%mw3lsHY!PJeCl$9CXvb-eaC$T-EH^4WwFJ5e%AIr^%*~8USvX1aGNYiY$ zdw6>fX?8C1=gKTLW3IF`q)(VttViYLhVO08cwB3HlcTI1-wuHM1=Z!(7~TH5rgrYA@nUi< zC-nKg!uV}z>d44NuaLJn))&tf6ddnnb3(g?%~T)USHxuzA`YR}yrCt$TvLNb@{3`Q zz8wpTm7oiA=L)CMDM7y>g8#M?`Hul?)&b;cBN))~*^vaYUGRNQXlxK;?=tv$I?%ez zFL$&>i`C*mH!!fEs*0n7d!lZ3*JPLf$?Tg9LlB=3cF8|uDvt7dbab|uS6`{loxEWQ z8~@&Ld4~DH{2BT3(y~71WWVn8^|yPF&RQZsuW)O02HpUD-f{su1bGi&alAY&yvX6xMeYeU zS2;iky^yj5zB;pEVqMNSAt4ghk)%5)jPtYm0`RKbSe7qP)d>W#iEi*tg+F}%bN)_? z9Wun4t}#8#lvd8mAbu|z9cKJh(jv3jCg3JxsqEQZ~(ySm_ z?t;cVEfWklPlN8hL}1Hb*ca}Dx$huF=KKS7U|5;-SCm*jLj3EJnDe6}#c$bj! zeMZ0N(^CDgx9^{yQ_{aSVHxLxsf}&Nug4SMhTn{5S`Kdq#NG3~Z&)=afw_0i5>4%C zm1fL^WiwBK8wD}D7u>_kxKGne8MS#-$W=B#m&Vzg#B zor0dT^|S#cHQvx+gI?gc4_>0*}BH`rc`Gz1EvKD=Np+9vP?j>X#2Rs5D|2G_`K2INP0m|1w0np8idewLYHv z<#R+P$bNZ$(FWt)*hGH@3<{43-$Barse9RedTQK%pIVaqh`q^FK8A)SVAoX7S={Yi z$*QcHDBSN7A4Yp7^dAI@n;YnrRSm5M37e_g6>G@crq@df1g;(=trbt7-l?}s^0C$m z@6wZh2C|MYd4tIVuqLe-YG z3i-s7_Wexvr*XjSSf$uEdA`wcZq<^@vcH=n*c1Z3t#bEpcrD(S=`zzv;nj{8SF7q( zM7t9lB|uu#JJv^Pn+>sykVohO_=YYAUp=1!)SsXS3FNanyq zst{fRSC0DX<6s^MKPAtz@^Z~jLQQans03{&3+8ejWyt7zY8sx?fq)Z5b3Mz z^OrRrMIQ)m_|YQPpq{ynogTf|6!CdWA64{+ipr$+Qv2%%<>R(6KpM!)O!^v(sWS?L|#58V@~{QXDEl zFD0y*>L6RL&KKQnTR+b^gO?TM9UATqE|1>w5g9ci0zzYyzc(85TgCHIK{%2IU_cK^ zPCN2wyiACjk693V+P4fZ=eBL-6Vrl;;Vpilh~HHZEgYqGG|u>7KByTY^~X+7HR$JA zIrj%o4atW?CLNy|d>$fWt%(2@PNWV<)BR|g!^Xl#22>b;>`HK}* z{yZ5lo_TJQs!SQ#sw%dkQ>4?XubI)36UBBjyvwl1ibFBhw}jg!&0rLa30#ta(9HA- zepecryG4QJIPHa$;>u6v(m0G{Wdnbp(w16U@DS3os{80#fp*<^4mNs&@~P3~ge0)z zBrZv2+yBBNl=HRX#}DKVrw)q3nW}8xEJe;ez9oiQ+cbbh9!;?~d^ZlU41u#cCbizv zw_M@!@PH4NPDbz}Gi0tri!KP78pmg>i6c=7}wu#mL5 z0F(rK*bR7o+hyW#!`arc%#Hn<`YG#>RTpz~cW*Z0!9_4|xKdNL;q7+lnty>VlBP?*b!oS65+@TVK!|1# z#fJK(mPoEbZXZ7Oizk$<0mn;2(_}ppR{cJg+$$SZwz{wAt93f^>1AS{x5n*5y)%xP zS_d%(5k9?S?#6?UJ_kPILYm%$&INTll^4oOE*r5@r&!<2o&St;ZXGVdOlc8Zl(0BL~<`~ z;^h%67JO5d;XqdbhBl1%ko3GMe*Pd$e8PsThvGpn<+lg7+Jh;s-f}Du8g|1fi%V8; z&T%sofG0B?m2=`;Jm1{mv8v&HRmt3AB6gV*f{#JmAcw{2Q6qe25RcZfD#|gNZ%3>r zD!`D{N$MhwFvIrw96jgq6Jq|(`_3z5R=#9trH-_wT(0Np8CYzL5IEvqmTn+C;rDZ! zVU6HOxQ^04iTLzyG7bMXNPHU9eYP{>8tJjZID6;x7ulYA6bTz<3Ac^%2^P`j7xUstGB8q{(61K#w@*IQk0 zrxN#!Y+P(k>3gF}2Mzj8zMc5mD{;DI#HYKu$~^q*J2|4#YNmSgYtPSJ@}Gl+J^XLl zV*}?5Govou1f7Oh-ZqSiAi%Yd`fbqvV%F>M%w-j7jdjBqqS7lRm)+t7a!NX_ziq`o zEUJaC6U2Oe0r&6Ds=c{M1l(R_8pS_JY7NLS`>hJv2lcbs+B(FHt*uP<6v!Hkw>@

tUdsDL2t=_L{o^QT9^3!mvZjgaj_Rg#_uF3R^@et#%g=CE`nj^=wt@n z8@bYKZz@b0GtQE+oQNSDEM}vp3cM@b0d3kmhknP9!3qw;US%cS^a2AHbqqE zB?zh;alVdsuV{yCylL}~WN*!uK#dN!sFW~-ca4_0gZ;p(3Ds?k3%%4~EWUtnr^`S_ zxh*aHQ9){lAHnH@je4t7R!T|m5Jn*9sB3`hQ8S-FtHPA5kDEQ?vL(7DXlED;vOn8h zB{ee4SZ_L(3RIG2jji-9W#Ec=ZPib!XnP`!eD<(ksDNW*(0dspEfNVj=D|K4gUC)ywhsAk=q6=;3evO=r!2iKJ)s|lt?-#q`^!0{$;R8;bpa=b$zMh_MaRAcJT0^ys=&vH z>q^4td`chWIQ0_&0?e>ho@KN2>5NJ3!A-$aTtdnvEM9tGgCyL8A7dt#Hn1PTPndsH z#Bd)z1D9bOQ%5iS=qn5FU+p2*A!C?uSRT#D9}$8)qzH($F$x@c z9#_`)+%;cD#EVno7CCwjB8@&;Z7XHK?~mt_B3Y~s{oEVM@^RKehbof`w1|;R-ulFE z749$-GnoXvE`f_T# z^0zA+1TjR+Xe6?S?V2y~@IK z$jWF{R{RA$CSFE}#_YFQe}h-^(F<-2V*m_s1dTBcb@PcTIyFzo{t2?z{c98QHkYoK zCaSVf>#sOr022{VoMbpoT7?%YC^Z+*5lol*WQKmF*1$L9|)Hpb6Vsa+k2Mvy?wHe z9i;t4^CxhgcOogWf;^Y?+g0Tf6zP)%LGkQ2x)&=gzlk(8m*=fO?gok5FR(`CAWszR zlG!A<*$B-ihdW7CkGx(?L<_rhEx#?9A_9WJr~vbWZRGCaPp#YKg@x18sdrbrsWe3v zi$QqL3)qcS?f!4B>Hdd{gMBz_@gY|6`n1k?>b9)IKeU-kvdd;O02YHTNpBqbmhG11 zvc%$lJi2V5>;?T8^cAxp6Nd@XoK@q7$NmTxQTe_L=D0qP1utj@F?1oy!z=IA&1bN& zMTyDrSXtV{dL;S>I&Qx69d!6LhnL_%ZcGJ-sn|nInHQK3YUWM*p0Tn(_$C-KdFq1l zy3$)5$1=?kt0tMC=TtKp}@Brz1}L0i?+7Tr!_vIZbYP(}3Eqdu^~VpWiQ^bs3;G;Q=k<)sr~A zgZegDg$Yclms*@?)~rZcyY>w^G?x)Pz|S`FRHx;p}DpL@5wy z$-$Q}0&M>|(s#V^ydBn>_Npc6&`Mp#E*YE-&n%UA6i9VD*6FFuCmV85Ht@&&TZ}_m zI$Cm@E*|Z{dowrcg-01*=ZMa3zSxM?dN1fqc1YLULj=U)rzPZv{m;NP!yY@ zjI#ks;Yb*_z2Qd1%f3UW%6l!NO;@~p=iRTglhW;FtE{9)&=3AOzkWNi)-Cj)8T}F} zIm$?BbQQ))s(kL;7v|flI@oNaVWzijbg!VMkkJx*H}zt>vxxt>?jB5Z89bx84T-Mc zZ-Jfo1>vW+n|@ofq1mQj=F5N0UYGJtTTR2}kjG3m)i&R$te07w(g8;ZA$^AfA7UT> zTOY{&D-R@r2i*A)K8Qp_pt7}$ba)86=GscPzru&5;*N}M?m z0apVP!DJA*YXA*9NMK1n>i2vJm) z&t^?W?vfkHIPWXIP&qGcyZnkriZM(yC5_6fDDa$j2)g76U6eA#?&)db*DSlsI*qE2iwgkdii2xI)jmQbO_Soi!0hO1^u-6I`bj&>&Kf`mE=M}i%OX1eG zJYg2L27=lRAFi*SQ!dhI>XtJD%L}FkzD<@}+l!H{3_%O(tDuMSO!&M#69`*{($hl^ z%oyz=>J1SvU4BBipCAYhI)vooX~GXoYRi983Ises!kE-gB-PFmhOhil7zg-r6B2w<&%K1=0iUi&5r5@4J0780AwT(ytu%}Y zze6rzYotLbihp-U(>7Wv0B%B zRu?y`xp0jkEEGB z4tLv6n8dK+&fsmP(A5Ub!8Ia)TzC6j^;&WCy@(i`K#`3DD|>C!hkoVn=JRf6?{n%q z?$MXjf#>{xJC!{7$+h#fzY15i7}P^na8TQx`n%? zV>D5&z7hYfk(>y4Hg8Tlh#p_qKR=XfmgWAegZW&vMFb4=+&_!K>6Ydm@87a`0cy6h zgqe{>Z^ltRp5Zy=S!)WUS^gD`1>#Yj4qiFr2Q!;h@>~|(dNk}juV@#~92PIcf~nGvxIgdT$@y^W%GgVSjvNy z_KmtnRwshZ|l=&&Q-d#_o~DXbR*%W_DW43|T2Fi}L;yHoF2!d+H_ zJp|vynvJ^&V%Hb_LzAl6yshY~yb*8sZ*~?B&ZeuRv%!H+tG0YuMIiKPqqqdf&32}F zSEOWz?+;jqSP3+5*gOc%u)u5geH_P4|23>3uc=97kExH7$INY(dmCK#PKajWya;SB zj$f;3)Jk8Jr!4vCrtc^Pu4zQg6V^7Yw?VA(95bfFzShvd2!|w?O_bicETEeXK?+5P zolK|dr@Vel&j*i?;PXNSI@_+5AIiNf;p$u#{**#(E3-c|aWq{3RK=IJaouWY6M?^( z`f?-KMUcs6J8=})d(pB|8AKPkoON|Jg~koPeHon6odY9Up7+>{Jg$r=(E*l4hh7Az z8gDL01X}Fu$qFCDw6}5Jxa|-;;w5<@cgi7k>a;7hLo!+)Kmb1KUG{WCnNPvc5YkiG z%mj?1lo}tjHuFME4sqzc=Dm>&ON0Y~Uu`WFn69$cuQ>%~IH+xyko|+sV3Q57Y;uG} zNmo>Sv9f(hbE<84Zn)9(#8+g%aq!4Fcficw7=gr@Wi+;=5l9((kEP0vM?d`wX8C_$ zYUd(Hr+sM8lu&!yWxV3~&eJx>$1kkx#e?6u_cGVW@2`V_Ea5Fh6-i)!LYv*H9!_?M zPv6CpgJMx*brp~hdUf~Qk!x97-d7zwWtNP^IFdh-45085cX%sLKu%u12zvE7yL z1_>kr`p~tXo4(*73IyTiX{XdO-^4?eZ$!Xlxg6hY>_O1zbt-AU{`qquj6h(Iuacm{ zNWlh7G#ffXlSc`dK_j|Bj6N^I#_LNB9{4tX+D{;0)1)Ih-w?u_^YndDD04b5vem z?B5{ZVH_t;xj%;l4D!;)-w}?*z>=*x$L>KYU*1}9qc2}jNtpbyg*F!y>6E~x;n!rL z$ZG+o95*662U9Cd2+eZ2eRy^L5%&sIT3v@hEFyW*uh;YF1`b1`(GUT`Y^UplIUnCq+z%`vjE&CxMz-;!7VXWD+|d() z{_=scfK{z8SDoDA56I8@Lm80)i$TiF?b2MZwBJ9Ak1iZDGo_|bzm}1S)H*Nb%7w<_ zQ$%9zSdD=NYGUQDxA;C6u5i_A4*NavYGcHlt;kXF-z(M7yya&WP-8tNN%(RCrKA63 zS-!0`7i1c^xMqDUo7qixIOt5JQQiR&zpRvOPb16~`mu$#Nn#kfuRinioAH?L862b6 z`tEkPvZWNT2W0PEK8Z$IfY)I2?=KgwJLlf8+(5F2-He~m=ad!K14$T1cZ;?cg1`lL z&2)Lwmhr5%L{N)$@{G~QU-k*dG#7?#Bz@RBdia1o8_JjL=UkCF(N|!a^Y#w|$N#4y z%Ne8QQQz3wl$_6Ajgv5(MaQ!O)0YoPEB;{s_NRf{|Bm}t6*wT`^#1|e$972o literal 0 HcmV?d00001 diff --git a/examples/webgpu_oit.html b/examples/webgpu_oit.html new file mode 100644 index 00000000000000..83a40e06f2e99f --- /dev/null +++ b/examples/webgpu_oit.html @@ -0,0 +1,205 @@ + + + + three.js webgpu - order-independent transparency + + + + + + + + + + +

+ + +
+ three.jsOrder-Independent Transparency +
+ + Weighted Blended OIT resolves intersecting and overlapping transparent surfaces per pixel. +
+ + + + + +