Browse Source

SSSNode: Add new node for Screen-Space Shadows. (#32050)

* SSSNode: Add new node for Screen-Space Shadows.

* Examples: Use `PCFSoftShadowMap` in SSS demo.
Michael Herzog 4 months ago
parent
commit
2cfd573955

+ 1 - 0
examples/files.json

@@ -426,6 +426,7 @@
 		"webgpu_postprocessing_ssaa",
 		"webgpu_postprocessing_ssgi",
 		"webgpu_postprocessing_ssr",
+		"webgpu_postprocessing_sss",
 		"webgpu_postprocessing_traa",
 		"webgpu_postprocessing_transition",
 		"webgpu_postprocessing",

+ 502 - 0
examples/jsm/tsl/display/SSSNode.js

@@ -0,0 +1,502 @@
+import { RedFormat, RenderTarget, Vector2, RendererUtils, QuadMesh, TempNode, NodeMaterial, NodeUpdateType, UnsignedByteType } from 'three/webgpu';
+import { reference, viewZToPerspectiveDepth, logarithmicDepthToViewZ, getScreenPosition, getViewPosition, float, Break, Loop, int, max, abs, If, dot, screenCoordinate, nodeObject, Fn, passTexture, uv, uniform, perspectiveDepthToViewZ, orthographicDepthToViewZ, vec2, lightPosition, lightTargetPosition, fract, rand, mix } from 'three/tsl';
+
+const _quadMesh = /*@__PURE__*/ new QuadMesh();
+const _size = /*@__PURE__*/ new Vector2();
+
+const _spatialOffsets = [ 0, 0.5, 0.25, 0.75 ];
+
+let _rendererState;
+
+/**
+ * Post processing node for applying Screen-Space Shadows (SSS) to a scene.
+ *
+ * Screen-Space Shadows (also known as Contact Shadows) should ideally be used to complement
+ * traditional shadow maps. They are best suited for rendering detailed shadows of smaller
+ * objects at a closer scale like intricate shadowing on highly detailed models. In other words:
+ * Use Shadow Maps for the foundation and Screen-Space Shadows for the details.
+ *
+ * The shadows produced by this implementation might have too hard edges for certain use cases.
+ * Use a box, gaussian or hash blur to soften the edges before doing the composite with the
+ * beauty pass. Code example:
+ *
+ * ```js
+ * const sssPass = sss( scenePassDepth, camera, mainLight );
+ *
+ * const sssBlur = boxBlur( sssPass.r, { size: 2, separation: 1 } ); // optional blur
+ * ```
+ *
+ * Limitations:
+ *
+ * - Ideally the maximum shadow length should not exceed `1` meter. Otherwise the effect gets
+ * computationally very expensive since more samples during the ray marching process are evaluated.
+ * You can mitigate this issue by reducing the `quality` paramter.
+ * - The effect can only be used with a single directional light, the main light of your scene.
+ * This main light usually represents the sun or daylight.
+ * - Like other Screen-Space techniques SSS can only honor objects in the shadowing computation that
+ * are currently visible within the camera's view.
+ *
+ * References:
+ * - {@link https://panoskarabelas.com/posts/screen_space_shadows/}.
+ * - {@link https://www.bendstudio.com/blog/inside-bend-screen-space-shadows/}.
+ *
+ * @augments TempNode
+ * @three_import import { sss } from 'three/addons/tsl/display/SSSNode.js';
+ */
+class SSSNode extends TempNode {
+
+	static get type() {
+
+		return 'SSSNode';
+
+	}
+
+	/**
+	 * Constructs a new SSS node.
+	 *
+	 * @param {TextureNode} depthNode - A texture node that represents the scene's depth.
+	 * @param {Camera} camera - The camera the scene is rendered with.
+	 * @param {DirectionalLight} mainLight - The main directional light of the scene.
+	 */
+	constructor( depthNode, camera, mainLight ) {
+
+		super( 'float' );
+
+		/**
+		 * A node that represents the beauty pass's depth.
+		 *
+		 * @type {TextureNode}
+		 */
+		this.depthNode = depthNode;
+
+		/**
+		 * Maximum shadow length in world units. Longer shadows result in more computational
+		 * overhead.
+		 *
+		 * @type {UniformNode<float>}
+		 * @default 0.1
+		 */
+		this.maxDistance = uniform( 0.1, 'float' );
+
+		/**
+		 * Depth testing thickness.
+		 *
+		 * @type {UniformNode<float>}
+		 * @default 0.01
+		 */
+		this.thickness = uniform( 0.01, 'float' );
+
+		/**
+		 * Shadow intensity. Must be in the range `[0, 1]`.
+		 *
+		 * @type {UniformNode<float>}
+		 * @default 0.5
+		 */
+		this.shadowIntensity = uniform( 0.5, 'float' );
+
+		/**
+		 * This parameter controls how detailed the raymarching process works.
+		 * The value ranges is `[0,1]` where `1` means best quality (the maximum number
+		 * of raymarching iterations/samples) and `0` means no samples at all.
+		 *
+		 * A quality of `0.5` is usually sufficient for most use cases. Try to keep
+		 * this parameter as low as possible. Larger values result in noticeable more
+		 * overhead.
+		 *
+		 * @type {UniformNode<float>}
+		 * @default 0.5
+		 */
+		this.quality = uniform( 0.5 );
+
+		/**
+		 * The resolution scale. Valid values are in the range
+		 * `[0,1]`. `1` means best quality but also results in
+		 * more computational overhead. Setting to `0.5` means
+		 * the effect is computed in half-resolution.
+		 *
+		 * @type {number}
+		 * @default 1
+		 */
+		this.resolutionScale = 1;
+
+		/**
+		 * Whether to use temporal filtering or not. Setting this property to
+		 * `true` requires the usage of `TRAANode`. This will help to reduce noice
+		 * although it introduces typical TAA artifacts like ghosting and temporal
+		 * instabilities.
+		 *
+		 * @type {boolean}
+		 * @default false
+		 */
+		this.useTemporalFiltering = false;
+
+		/**
+		 * 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;
+
+		// private uniforms
+
+		/**
+		 * Represents the view matrix of the scene's camera.
+		 *
+		 * @private
+		 * @type {UniformNode<mat4>}
+		 */
+		this._cameraViewMatrix = uniform( camera.matrixWorldInverse );
+
+		/**
+		 * Represents the projection matrix of the scene's camera.
+		 *
+		 * @private
+		 * @type {UniformNode<mat4>}
+		 */
+		this._cameraProjectionMatrix = uniform( camera.projectionMatrix );
+
+		/**
+		 * Represents the inverse projection matrix of the scene's camera.
+		 *
+		 * @private
+		 * @type {UniformNode<mat4>}
+		 */
+		this._cameraProjectionMatrixInverse = uniform( camera.projectionMatrixInverse );
+
+		/**
+		 * Represents the near value of the scene's camera.
+		 *
+		 * @private
+		 * @type {ReferenceNode<float>}
+		 */
+		this._cameraNear = reference( 'near', 'float', camera );
+
+		/**
+		 * Represents the far value of the scene's camera.
+		 *
+		 * @private
+		 * @type {ReferenceNode<float>}
+		 */
+		this._cameraFar = reference( 'far', 'float', camera );
+
+		/**
+		 * The resolution of the pass.
+		 *
+		 * @private
+		 * @type {UniformNode<vec2>}
+		 */
+		this._resolution = uniform( new Vector2() );
+
+		/**
+		 * Temporal offset added to the initial ray step.
+		 *
+		 * @type {UniformNode<float>}
+		 */
+		this._temporalOffset = uniform( 0 );
+
+		/**
+		 * The frame ID use when temporal filtering is enabled.
+		 *
+		 * @type {UniformNode<uint>}
+		 */
+		this._frameId = uniform( 0 );
+
+		/**
+		 * A reference to the scene's main light.
+		 *
+		 * @private
+		 * @type {DirectionalLight}
+		 */
+		this._mainLight = mainLight;
+
+		/**
+		 * The camera the scene is rendered with.
+		 *
+		 * @private
+		 * @type {Camera}
+		 */
+		this._camera = camera;
+
+		/**
+		 * The render target the SSS is rendered into.
+		 *
+		 * @private
+		 * @type {RenderTarget}
+		 */
+		this._sssRenderTarget = new RenderTarget( 1, 1, { depthBuffer: false, format: RedFormat, type: UnsignedByteType } );
+		this._sssRenderTarget.texture.name = 'SSS';
+
+		/**
+		 * The material that is used to render the effect.
+		 *
+		 * @private
+		 * @type {NodeMaterial}
+		 */
+		this._material = new NodeMaterial();
+		this._material.name = 'SSS';
+
+		/**
+		 * The result of the effect is represented as a separate texture node.
+		 *
+		 * @private
+		 * @type {PassTextureNode}
+		 */
+		this._textureNode = passTexture( this, this._sssRenderTarget.texture );
+
+
+	}
+
+	/**
+	 * Returns the result of the effect as a texture node.
+	 *
+	 * @return {PassTextureNode} A texture node that represents the result of the effect.
+	 */
+	getTextureNode() {
+
+		return this._textureNode;
+
+	}
+
+	/**
+	 * 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 ) {
+
+		width = Math.round( this.resolutionScale * width );
+		height = Math.round( this.resolutionScale * height );
+
+		this._resolution.value.set( width, height );
+		this._sssRenderTarget.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 );
+
+		//
+
+		const size = renderer.getDrawingBufferSize( _size );
+		this.setSize( size.width, size.height );
+
+		// update temporal uniforms
+
+		if ( this.useTemporalFiltering === true ) {
+
+			const frameId = frame.frameId;
+
+			this._temporalOffset.value = _spatialOffsets[ frameId % 4 ];
+			this._frameId = frame.frameId;
+
+		} else {
+
+			this._temporalOffset.value = 0;
+			this._frameId = 0;
+
+		}
+
+		//
+
+		_quadMesh.material = this._material;
+		_quadMesh.name = 'SSS';
+
+		// clear
+
+		renderer.setClearColor( 0xffffff, 1 );
+
+		// sss
+
+		renderer.setRenderTarget( this._sssRenderTarget );
+		_quadMesh.render( renderer );
+
+		// restore
+
+		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 ) {
+
+		const uvNode = uv();
+
+		const getViewZ = Fn( ( [ depth ] ) => {
+
+			let viewZNode;
+
+			if ( this._camera.isPerspectiveCamera ) {
+
+				viewZNode = perspectiveDepthToViewZ( depth, this._cameraNear, this._cameraFar );
+
+			} else {
+
+				viewZNode = orthographicDepthToViewZ( depth, this._cameraNear, this._cameraFar );
+
+			}
+
+			return viewZNode;
+
+		} );
+
+		const sampleDepth = ( uv ) => {
+
+			const depth = this.depthNode.sample( uv ).r;
+
+			if ( builder.renderer.logarithmicDepthBuffer === true ) {
+
+				const viewZ = logarithmicDepthToViewZ( depth, this._cameraNear, this._cameraFar );
+
+				return viewZToPerspectiveDepth( viewZ, this._cameraNear, this._cameraFar );
+
+			}
+
+			return depth;
+
+		};
+
+		// Interleaved gradient function from Jimenez 2014 http://goo.gl/eomGso
+
+		const gradientNoise = Fn( ( [ position ] ) => {
+
+			return fract( float( 52.9829189 ).mul( fract( dot( position, vec2( 0.06711056, 0.00583715 ) ) ) ) );
+
+		} ).setLayout( {
+			name: 'gradientNoise',
+			type: 'float',
+			inputs: [
+				{ name: 'position', type: 'vec2' }
+			]
+		} );
+
+		const sss = Fn( () => {
+
+			const depth = sampleDepth( uvNode ).toVar();
+			depth.greaterThanEqual( 1.0 ).discard();
+
+			// compute ray position and direction (in view-space)
+
+			const rayStartPosition = getViewPosition( uvNode, depth, this._cameraProjectionMatrixInverse ).toVar( 'rayStartPosition' );
+			const rayDirection = this._cameraViewMatrix.transformDirection( lightPosition( this._mainLight ).sub( lightTargetPosition( this._mainLight ) ) ).toConst( 'rayDirection' );
+			const rayEndPosition = rayStartPosition.add( rayDirection.mul( this.maxDistance ) ).toConst( 'rayEndPosition' );
+
+			// d0 and d1 are the start and maximum points of the ray in screen space
+			const d0 = screenCoordinate.xy.toVar();
+			const d1 = getScreenPosition( rayEndPosition, this._cameraProjectionMatrix ).mul( this._resolution ).toVar();
+
+			// below variables are used to control the raymarching process
+
+			// total length of the ray
+			const totalLen = d1.sub( d0 ).length().toVar();
+
+			// offset in x and y direction
+			const xLen = d1.x.sub( d0.x ).toVar();
+			const yLen = d1.y.sub( d0.y ).toVar();
+
+			// determine the larger delta
+			// The larger difference will help to determine how much to travel in the X and Y direction each iteration and
+			// how many iterations are needed to travel the entire ray
+			const totalStep = int( max( abs( xLen ), abs( yLen ) ).mul( this.quality.clamp() ) ).toConst();
+
+			// step sizes in the x and y directions
+			const xSpan = xLen.div( totalStep ).toVar();
+			const ySpan = yLen.div( totalStep ).toVar();
+
+			// compute noise based ray offset
+			const noise = gradientNoise( screenCoordinate );
+			const offset = fract( noise.add( this._temporalOffset ) ).add( rand( uvNode.add( this._frameId ) ) ).toConst( 'offset' );
+
+			const occlusion = float( 0 ).toVar();
+
+			Loop( totalStep, ( { i } ) => {
+
+				// advance on the ray by computing a new position in screen coordinates
+				const xy = vec2( d0.x.add( xSpan.mul( float( i ).add( offset ) ) ), d0.y.add( ySpan.mul( float( i ).add( offset ) ) ) ).toVar();
+
+				// stop processing if the new position lies outside of the screen
+				If( xy.x.lessThan( 0 ).or( xy.x.greaterThan( this._resolution.x ) ).or( xy.y.lessThan( 0 ) ).or( xy.y.greaterThan( this._resolution.y ) ), () => {
+
+					Break();
+
+				} );
+
+				// compute new uv, depth and viewZ for the next fragment
+
+				const uvNode = xy.div( this._resolution );
+				const fragmentDepth = sampleDepth( uvNode ).toConst();
+				const fragmentViewZ = getViewZ( fragmentDepth ).toConst( 'fragmentViewZ' );
+
+				const s = xy.sub( d0 ).length().div( totalLen ).toVar();
+				const rayPosition = mix( rayStartPosition, rayEndPosition, s );
+
+				const depthDelta = rayPosition.z.sub( fragmentViewZ ).negate(); // Port note: viewZ values are negative in three
+
+				// check if the camera can't "see" the ray (ray depth must be larger than the camera depth, so positive depth_delta)
+
+				If( depthDelta.greaterThan( 0 ).and( depthDelta.lessThan( this.thickness ) ), () => {
+
+					// mark as occluded
+
+					occlusion.assign( this.shadowIntensity );
+
+					Break();
+
+				} );
+
+
+			} );
+
+			return occlusion.oneMinus();
+
+		} );
+
+		this._material.fragmentNode = sss().context( builder.getSharedContext() );
+		this._material.needsUpdate = true;
+
+		return this._textureNode;
+
+	}
+
+	/**
+	 * Frees internal resources. This method should be called
+	 * when the effect is no longer required.
+	 */
+	dispose() {
+
+		this._sssRenderTarget.dispose();
+
+		this._material.dispose();
+
+	}
+
+}
+
+export default SSSNode;
+
+/**
+ * TSL function for creating a SSS effect.
+ *
+ * @tsl
+ * @function
+ * @param {TextureNode} depthNode - A texture node that represents the scene's depth.
+ * @param {Camera} camera - The camera the scene is rendered with.
+ * @param {DirectionalLight} mainLight - The main directional light of the scene.
+ * @returns {SSSNode}
+ */
+export const sss = ( depthNode, camera, mainLight ) => nodeObject( new SSSNode( depthNode, camera, mainLight ) );

BIN
examples/models/gltf/nemetona.glb


BIN
examples/screenshots/webgpu_postprocessing_sss.jpg


+ 232 - 0
examples/webgpu_postprocessing_sss.html

@@ -0,0 +1,232 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js webgpu - SSS</title>
+		<meta charset="utf-8">
+		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
+		<link type="text/css" rel="stylesheet" href="example.css">
+	</head>
+	<body>
+
+		<div id="info">
+			<a href="https://threejs.org/" target="_blank" rel="noopener" class="logo-link"></a>
+
+			<div class="title-wrapper">
+				<a href="https://threejs.org/" target="_blank" rel="noopener">three.js</a><span>SSS</span>
+			</div>
+
+			<small>
+				Screen-Space Shadows (SSS) combined with Shadow Maps.<br/>
+				<a href="https://skfb.ly/pAQvU" target="_blank" rel="noopener">Nemetona_NatureBeauty</a> by
+				<a href="https://sketchfab.com/JOJObrush" target="_blank" rel="noopener">JOJObrush</a> is licensed under <a href="https://creativecommons.org/licenses/by/4.0/" target="_blank" rel="noopener">Creative Commons Attribution</a>.<br />
+			</small>
+		</div>
+
+		<script type="importmap">
+			{
+				"imports": {
+					"three": "../build/three.webgpu.js",
+					"three/webgpu": "../build/three.webgpu.js",
+					"three/tsl": "../build/three.tsl.js",
+					"three/addons/": "./jsm/"
+				}
+			}
+		</script>
+
+		<script type="module">
+
+			import * as THREE from 'three/webgpu';
+			import { pass, vec3, vec4, mrt, output, velocity } from 'three/tsl';
+			import { sss } from 'three/addons/tsl/display/SSSNode.js';
+			import { traa } from 'three/addons/tsl/display/TRAANode.js';
+
+			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+			import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
+			import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
+			import { Inspector } from 'three/addons/inspector/Inspector.js';
+
+			let camera, scene, renderer, postProcessing, controls;
+
+			init();
+
+			async function init() {
+
+				camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 0.1, 100 );
+				camera.position.set( 1, 2.5, - 3.5 );
+
+				scene = new THREE.Scene();
+				scene.background = new THREE.Color( 0xa0a0a0 );
+				scene.fog = new THREE.Fog( 0xa0a0a0, 10, 50 );
+
+				// lights
+
+				const hemiLight = new THREE.HemisphereLight( 0xffffff, 0x8d8d8d, 2 );
+				hemiLight.position.set( 0, 20, 0 );
+				scene.add( hemiLight );
+
+				const dirLight = new THREE.DirectionalLight( 0xffffff, 3 );
+				dirLight.position.set( - 3, 10, - 10 );
+				dirLight.castShadow = true;
+				dirLight.shadow.camera.top = 4;
+				dirLight.shadow.camera.bottom = - 4;
+				dirLight.shadow.camera.left = - 4;
+				dirLight.shadow.camera.right = 4;
+				dirLight.shadow.camera.near = 0.1;
+				dirLight.shadow.camera.far = 40;
+				dirLight.shadow.bias = - 0.001;
+				dirLight.shadow.mapSize.width = 1024;
+				dirLight.shadow.mapSize.height = 1024;
+				scene.add( dirLight );
+
+				// scene.add( new THREE.CameraHelper( dirLight.shadow.camera ) );
+
+				// ground
+
+				const mesh = new THREE.Mesh( new THREE.PlaneGeometry( 100, 100 ), new THREE.MeshPhongMaterial( { color: 0xcbcbcb, depthWrite: false } ) );
+				mesh.rotation.x = - Math.PI / 2;
+				mesh.receiveShadow = true;
+				scene.add( mesh );
+
+				//
+
+				const dracoLoader = new DRACOLoader();
+				dracoLoader.setDecoderPath( 'jsm/libs/draco/gltf/' );
+
+				const loader = new GLTFLoader();
+				loader.setDRACOLoader( dracoLoader );
+				loader.load( 'models/gltf/nemetona.glb', function ( gltf ) {
+
+					const model = gltf.scene;
+					model.rotation.y = Math.PI;
+					model.scale.setScalar( 10 );
+					model.position.y = 0.45;
+			
+					scene.add( model );
+
+					model.traverse( function ( object ) {
+
+						if ( object.isMesh ) {
+			
+							object.castShadow = true;
+							object.receiveShadow = true;
+							object.material.aoMap = null; // remove AO to better see the effect of shadows
+
+						}
+
+					} );
+
+				} );
+
+				//
+
+				renderer = new THREE.WebGPURenderer();
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				renderer.setAnimationLoop( animate );
+				renderer.shadowMap.enabled = true;
+				renderer.shadowMap.type = THREE.PCFSoftShadowMap;
+				renderer.inspector = new Inspector();
+				document.body.appendChild( renderer.domElement );
+
+				postProcessing = new THREE.PostProcessing( renderer );
+
+				const scenePass = pass( scene, camera );
+				scenePass.setMRT( mrt( {
+					output: output,
+					velocity: velocity
+				} ) );
+
+				const scenePassColor = scenePass.getTextureNode( 'output' );
+				const scenePassVelocity = scenePass.getTextureNode( 'velocity' );
+				const scenePassDepth = scenePass.getTextureNode( 'depth' );
+
+				// sss
+
+				const sssPass = sss( scenePassDepth, camera, dirLight );
+				sssPass.shadowIntensity.value = 0.3;
+				sssPass.maxDistance.value = 0.2;
+				sssPass.useTemporalFiltering = true;
+
+				// composite
+
+				const compositePass = vec4( scenePassColor.rgb.mul( sssPass.r ), scenePassColor.a );
+				compositePass.name = 'Composite';
+			
+				// traa
+
+				const traaPass = traa( compositePass, scenePassDepth, scenePassVelocity, camera );
+				postProcessing.outputNode = traaPass;
+
+				//
+
+				controls = new OrbitControls( camera, renderer.domElement );
+				controls.minDistance = 1;
+				controls.maxDistance = 20;
+				controls.target.set( 0, 2, 0 );
+				controls.enableDamping = true;
+				controls.update();
+
+				//
+
+				const params = {
+					output: 0
+				};
+
+				const types = { 'Sceen with Shadow Maps + SSS': 0, 'Scene with Shadow Maps': 1, 'SSS': 2, };
+
+				const gui = renderer.inspector.createParameters( 'SSS settings' );
+				gui.add( params, 'output', types ).onChange( updatePostprocessing );
+				gui.add( sssPass.shadowIntensity, 'value', 0, 1 ).name( 'shadow intensity' );
+				gui.add( sssPass.maxDistance, 'value', 0.01, 1 ).name( 'max ray distance' );
+				gui.add( sssPass.quality, 'value', 0, 1 ).name( 'quality' );
+				gui.add( sssPass.thickness, 'value', 0.01, 0.1 ).name( 'thickness' );
+				gui.add( sssPass, 'useTemporalFiltering' ).name( 'temporal filtering' ).onChange( updatePostprocessing );
+
+				function updatePostprocessing( value ) {
+
+					if ( value === 1 ) {
+
+						postProcessing.outputNode = scenePassColor;
+
+					} else if ( value === 2 ) {
+
+						postProcessing.outputNode = vec4( vec3( sssPass.r ), 1 );
+
+					} else {
+
+						postProcessing.outputNode = sssPass.useTemporalFiltering ? traaPass : compositePass;
+
+					}
+
+					postProcessing.needsUpdate = true;
+
+
+				}
+
+				window.addEventListener( 'resize', onWindowResize );
+
+			}
+
+			function onWindowResize() {
+
+				const width = window.innerWidth;
+				const height = window.innerHeight;
+
+				camera.aspect = width / height;
+				camera.updateProjectionMatrix();
+
+				renderer.setSize( width, height );
+
+			}
+
+			function animate() {
+
+				controls.update();
+
+				postProcessing.render();
+
+			}
+
+		</script>
+	</body>
+</html>

+ 1 - 0
test/e2e/puppeteer.js

@@ -186,6 +186,7 @@ const exceptionList = [
 	'webgpu_postprocessing_afterimage',
 	'webgpu_postprocessing_ca',
 	'webgpu_postprocessing_ssgi',
+	'webgpu_postprocessing_sss',
 	'webgpu_xr_native_layers',
 	'webgpu_volume_caustics',
 	'webgpu_volume_lighting',

粤ICP备19079148号