Przeglądaj źródła

TSL: Introduce `varying.setInterpolation()` (#30582)

* init branch

* add example and GLSL fallback

* refine example

* add screenshot

* try new screenshot

* try fix screenshot again

* add centroid sampling to puppeteer exception list

* rev

* update example

* updates

---------

Co-authored-by: sunag <sunagbrasil@gmail.com>
Christian Helgeson 9 miesięcy temu
rodzic
commit
32e12df85c

+ 1 - 0
examples/files.json

@@ -306,6 +306,7 @@
 		"webgpu_camera",
 		"webgpu_camera_array",
 		"webgpu_camera_logarithmicdepthbuffer",
+		"webgpu_centroid_sampling",
 		"webgpu_clearcoat",
 		"webgpu_clipping",
 		"webgpu_compute_audio",

BIN
examples/screenshots/webgpu_centroid_sampling.jpg


+ 290 - 0
examples/webgpu_centroid_sampling.html

@@ -0,0 +1,290 @@
+<html lang="en">
+	<head>
+		<title>three.js webgpu - centroid sampling</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="main.css">
+	</head>
+	<style>
+		body {
+			margin: 0;
+			overflow: hidden;
+			width: 100vw;
+			height: 100vh;
+		}
+
+		#demo {
+			display: flex;
+			flex-direction: row;
+			align-items: center;
+		}
+
+		.renderer-wrapper {
+			display: flex;
+			flex-direction: column;
+			align-items: center;
+		}
+
+		#antialising-disabled {
+			border-right: 1px solid black;
+		}
+
+		canvas {
+			width: 100%;
+			height: 100%;
+		}
+	</style>
+	<body>
+		<div id="demo">
+			<div id="antialising-disabled" class="renderer-wrapper">
+				<div>antialising disabled</div>
+			</div>
+			<div id="antialising-enabled" class="renderer-wrapper">
+				<div>antialising enabled</div>
+			</div>
+		</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';
+			import { varying, uv, texture, Fn } from 'three/tsl';
+
+			import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
+
+			let rendererAntialiasingEnabled;
+			let rendererAntialiasingDisabled;
+			let camera;
+			let scene;
+			let gui;
+
+			const effectController = {
+				sampling: 'normal'
+			};
+
+			const atlasCanvas = document.createElement( 'canvas' );
+			atlasCanvas.width = 16;
+			atlasCanvas.height = 16;
+
+			const ctx = atlasCanvas.getContext( '2d' );
+			ctx.fillStyle = 'red';
+			ctx.fillRect( 0, 0, 8, 8 );
+
+			const redUVs = [ 0, 1, 0.5, 1, 0.5, 0.5, 0, 0.5 ];
+			ctx.fillStyle = 'green';
+			ctx.fillRect( 8, 0, 8, 8 );
+
+			const greenUVs = [ 1, 1, 0.5, 1, 0.5, 0.5, 1, 0.5 ];
+
+			ctx.fillStyle = 'blue';
+			ctx.fillRect( 0, 8, 8, 8 );
+
+			const blueUVs = [ 0, 0, 0.5, 0, 0.5, 0.5, 0, 0.5 ];
+
+			ctx.fillStyle = 'yellow';
+			ctx.fillRect( 8, 8, 8, 8 );
+
+			const yellowUVs = [ 1, 0, 0.5, 0, 0.5, 0.5, 1, 0.5 ];
+
+			const faces = [ redUVs, greenUVs, blueUVs, yellowUVs ];
+
+			const canvasTexture = new THREE.CanvasTexture( atlasCanvas );
+			canvasTexture.colorSpace = THREE.SRGBColorSpace;
+			canvasTexture.mapping = THREE.UVMapping;
+			canvasTexture.wrapS = THREE.RepeatWrapping;
+			canvasTexture.wrapT = THREE.RepeatWrapping;
+			canvasTexture.magFilter = THREE.NearestFilter;
+			canvasTexture.minFilter = THREE.NearestFilter;
+			canvasTexture.format = THREE.RGBAFormat;
+			canvasTexture.type = THREE.UnsignedByteType;
+
+			const forceWebGL = false;
+
+			init();
+
+			function init() {
+
+				camera = new THREE.PerspectiveCamera();
+				camera.fov = 60;
+				camera.near = 1;
+				camera.far = 2100;
+				camera.position.z = 50;
+
+				scene = new THREE.Scene();
+
+				const makeFaceGeometry = ( uvs ) => {
+
+					const geometry = new THREE.BufferGeometry();
+					const positions = [ - 1, - 1, 0, 1, - 1, 0, 1, 1, 0, - 1, 1, 0 ];
+					geometry.setAttribute(
+						'position',
+						new THREE.BufferAttribute( new Float32Array( positions ), 3 )
+					);
+
+					const indices = [ 0, 1, 2, 2, 3, 0 ];
+					geometry.setIndex( indices );
+
+					geometry.setAttribute(
+						'uv',
+						new THREE.BufferAttribute( new Float32Array( uvs ), 2 )
+					);
+
+					return geometry;
+			
+				};
+
+				const material = new THREE.MeshBasicNodeMaterial();
+				const testUV = varying( uv(), 'testUV' );
+
+				const createShader = ( type, sampling ) => {
+
+					return Fn( () => {
+
+						testUV.setInterpolation( type, sampling );
+
+						return texture( canvasTexture, testUV ).rgb;
+
+					} );
+
+				};
+
+				const withFlatFirstShader = createShader( 'flat', 'first' );
+				const withFlatEitherShader = createShader( 'flat', 'either' );
+
+				const withSampleShader = Fn( () => {
+
+					testUV.setInterpolation( THREE.InterpolationSamplingType.PERSPECTIVE, THREE.InterpolationSamplingMode.SAMPLE );
+
+					return texture( canvasTexture, testUV ).rgb;
+
+				} );
+
+				const withInterpolationShader = Fn( () => {
+
+					testUV.setInterpolation( THREE.InterpolationSamplingType.PERSPECTIVE, THREE.InterpolationSamplingMode.CENTROID );
+
+					return texture( canvasTexture, testUV ).rgb;
+
+				} );
+
+				const withoutInterpolationShader = Fn( () => {
+
+					return texture( canvasTexture, uv() ).rgb;
+
+				} );
+
+				material.colorNode = withoutInterpolationShader();
+
+				const faceMeshes = [];
+
+				for ( let x = - 5; x < 5; x ++ ) {
+
+					for ( let y = - 5; y < 5; y ++ ) {
+
+						const face = faces[ Math.floor( Math.random() * faces.length ) ];
+						const geometry = makeFaceGeometry( face );
+						const mesh = new THREE.Mesh( geometry, material );
+						mesh.position.set( x * 2, y * 2, 0 );
+						faceMeshes.push( mesh );
+						scene.add( mesh );
+			
+					}
+			
+				}
+
+				// Create Standard Renderer
+				rendererAntialiasingDisabled = new THREE.WebGPURenderer( {
+					antialias: false,
+					forceWebGL: forceWebGL
+				} );
+
+				rendererAntialiasingDisabled.setPixelRatio( window.devicePixelRatio );
+				rendererAntialiasingDisabled.setSize( window.innerWidth / 2, window.innerHeight );
+				rendererAntialiasingDisabled.setAnimationLoop( animateStandard );
+
+				// Create antialiased renderer
+				rendererAntialiasingEnabled = new THREE.WebGPURenderer( {
+					antialias: true,
+					forceWebGL: forceWebGL
+				} );
+
+				document.body.querySelector( '#antialising-enabled' ).appendChild( rendererAntialiasingEnabled.domElement );
+				rendererAntialiasingEnabled.setPixelRatio( window.devicePixelRatio );
+				rendererAntialiasingEnabled.setSize( window.innerWidth / 2, window.innerHeight );
+				rendererAntialiasingEnabled.setAnimationLoop( animateAliased );
+			
+				document.body.querySelector( '#antialising-disabled' ).appendChild( rendererAntialiasingDisabled.domElement );
+				document.body.querySelector( '#antialising-disabled' ).appendChild( rendererAntialiasingDisabled.domElement );
+
+				onWindowResize();
+
+				window.addEventListener( 'resize', onWindowResize );
+
+				gui = new GUI();
+				gui.add( effectController, 'sampling', [
+					THREE.InterpolationSamplingMode.NORMAL,
+					THREE.InterpolationSamplingMode.CENTROID,
+					THREE.InterpolationSamplingMode.SAMPLE,
+					THREE.InterpolationSamplingMode.FLAT_FIRST,
+					THREE.InterpolationSamplingMode.FLAT_EITHER
+				] ).onChange( () => {
+
+					const interpolationShaderLib = {
+						[ THREE.InterpolationSamplingMode.NORMAL ]: withoutInterpolationShader,
+						[ THREE.InterpolationSamplingMode.CENTROID ]: withInterpolationShader,
+						[ THREE.InterpolationSamplingMode.SAMPLE ]: withSampleShader,
+						[ THREE.InterpolationSamplingMode.FLAT_FIRST ]: withFlatFirstShader,
+						[ THREE.InterpolationSamplingMode.FLAT_EITHER ]: withFlatEitherShader
+					};
+
+					const shader = interpolationShaderLib[ effectController.sampling ];
+
+					for ( let i = 0; i < faceMeshes.length; i ++ ) {
+
+						faceMeshes[ i ].material.colorNode = shader();
+						faceMeshes[ i ].material.needsUpdate = true;
+
+					}
+
+			
+				} );
+
+			}
+
+			function onWindowResize() {
+
+				const halfWidth = window.innerWidth / 2;
+				rendererAntialiasingDisabled.setSize( halfWidth, window.innerHeight );
+				rendererAntialiasingEnabled.setSize( halfWidth, window.innerHeight );
+				const aspect = ( halfWidth ) / window.innerHeight;
+			
+				camera.aspect = aspect;
+				camera.updateProjectionMatrix();
+
+			}
+
+			function animateStandard() {
+
+				rendererAntialiasingDisabled.render( scene, camera );
+			
+			}
+
+			function animateAliased() {
+
+				rendererAntialiasingEnabled.render( scene, camera );
+			
+			}
+
+		</script>
+	</body>
+</html>

+ 46 - 0
src/constants.js

@@ -1609,6 +1609,32 @@ export const TimestampQuery = {
 	RENDER: 'render'
 };
 
+/**
+ * Represents mouse buttons and interaction types in context of controls.
+ *
+ * @type {ConstantsInterpolationSamplingType}
+ * @constant
+ */
+export const InterpolationSamplingType = {
+	PERSPECTIVE: 'perspective',
+	LINEAR: 'linear',
+	FLAT: 'flat'
+};
+
+/**
+ * Represents the different interpolation sampling modes.
+ *
+ * @type {ConstantsInterpolationSamplingMode}
+ * @constant
+ */
+export const InterpolationSamplingMode = {
+	NORMAL: 'normal',
+	CENTROID: 'centroid',
+	SAMPLE: 'sample',
+	FLAT_FIRST: 'flat first',
+	FLAT_EITHER: 'flat either'
+};
+
 /**
  * This type represents mouse buttons and interaction types in context of controls.
  *
@@ -1638,3 +1664,23 @@ export const TimestampQuery = {
  * @property {string} COMPUTE - A `compute` timestamp query.
  * @property {string} RENDER - A `render` timestamp query.
  **/
+
+/**
+ * Represents the different interpolation sampling types.
+ *
+ * @typedef {Object} ConstantsInterpolationSamplingType
+ * @property {string} PERSPECTIVE - Perspective-correct interpolation.
+ * @property {string} LINEAR - Linear interpolation.
+ * @property {string} FLAT - Flat interpolation.
+ */
+
+/**
+ * Represents the different interpolation sampling modes.
+ *
+ * @typedef {Object} ConstantsInterpolationSamplingMode
+ * @property {string} NORMAL - Normal sampling mode.
+ * @property {string} CENTROID - Centroid sampling mode.
+ * @property {string} SAMPLE - Sample-specific sampling mode.
+ * @property {string} FLAT_FIRST - Flat interpolation using the first vertex.
+ * @property {string} FLAT_EITHER - Flat interpolation using either vertex.
+ */

+ 4 - 2
src/nodes/core/NodeBuilder.js

@@ -1848,9 +1848,11 @@ class NodeBuilder {
 	 * @param {(VaryingNode|PropertyNode)} node - The varying node.
 	 * @param {?string} name - The varying's name.
 	 * @param {string} [type=node.getNodeType( this )] - The varying's type.
+	 * @param {?string} interpolationType - The interpolation type of the varying.
+	 * @param {?string} interpolationSampling - The interpolation sampling type of the varying.
 	 * @return {NodeVar} The node varying.
 	 */
-	getVaryingFromNode( node, name = null, type = node.getNodeType( this ) ) {
+	getVaryingFromNode( node, name = null, type = node.getNodeType( this ), interpolationType = null, interpolationSampling = null ) {
 
 		const nodeData = this.getDataFromNode( node, 'any' );
 
@@ -1863,7 +1865,7 @@ class NodeBuilder {
 
 			if ( name === null ) name = 'nodeVarying' + index;
 
-			nodeVarying = new NodeVarying( name, type );
+			nodeVarying = new NodeVarying( name, type, interpolationType, interpolationSampling );
 
 			varyings.push( nodeVarying );
 

+ 19 - 1
src/nodes/core/NodeVarying.js

@@ -15,8 +15,10 @@ class NodeVarying extends NodeVar {
 	 *
 	 * @param {string} name - The name of the varying.
 	 * @param {string} type - The type of the varying.
+	 * @param {?string} interpolationType - The interpolation type of the varying.
+	 * @param {?string} interpolationSampling - The interpolation sampling type of the varying.
 	 */
-	constructor( name, type ) {
+	constructor( name, type, interpolationType = null, interpolationSampling = null ) {
 
 		super( name, type );
 
@@ -38,6 +40,22 @@ class NodeVarying extends NodeVar {
 		 */
 		this.isNodeVarying = true;
 
+		/**
+		 * The interpolation type of the varying data.
+		 *
+		 * @type {?string}
+		 * @default null
+		 */
+		this.interpolationType = interpolationType;
+
+		/**
+		 * The interpolation sampling type of varying data.
+		 *
+		 * @type {?string}
+		 * @default null
+		 */
+		this.interpolationSampling = interpolationSampling;
+
 	}
 
 }

+ 35 - 1
src/nodes/core/VaryingNode.js

@@ -55,6 +55,22 @@ class VaryingNode extends Node {
 		 */
 		this.isVaryingNode = true;
 
+		/**
+		 * The interpolation type of the varying data.
+		 *
+		 * @type {?string}
+		 * @default null
+		 */
+		this.interpolationType = null;
+
+		/**
+		 * The interpolation sampling type of varying data.
+		 *
+		 * @type {?string}
+		 * @default null
+		 */
+		this.interpolationSampling = null;
+
 	}
 
 	/**
@@ -69,6 +85,22 @@ class VaryingNode extends Node {
 
 	}
 
+
+	/**
+	 * Defines the interpolation type of the varying.
+	 *
+	 * @param {string} type - The interpolation type.
+	 * @param {?string} sampling - The interpolation sampling type
+	 * @return {VaryingNode} A reference to this node.
+	 */
+	setInterpolation( type, sampling = null ) {
+
+		this.interpolationType = type;
+		this.interpolationSampling = sampling;
+		return this;
+
+	}
+
 	getHash( builder ) {
 
 		return this.name || super.getHash( builder );
@@ -99,8 +131,10 @@ class VaryingNode extends Node {
 
 			const name = this.name;
 			const type = this.getNodeType( builder );
+			const interpolationType = this.interpolationType;
+			const interpolationSampling = this.interpolationSampling;
 
-			properties.varying = varying = builder.getVaryingFromNode( this, name, type );
+			properties.varying = varying = builder.getVaryingFromNode( this, name, type, interpolationType, interpolationSampling );
 			properties.node = this.node;
 
 		}

+ 39 - 4
src/renderers/webgl-fallback/nodes/GLSLNodeBuilder.js

@@ -24,6 +24,17 @@ const supports = {
 	storageBuffer: false
 };
 
+const interpolationTypeMap = {
+	perspective: 'smooth',
+	linear: 'noperspective'
+};
+
+const interpolationModeMap = {
+	'centroid': 'centroid',
+	'flat first': 'flat',
+	'flat either': 'flat'
+};
+
 const defaultPrecisions = `
 precision highp float;
 precision highp int;
@@ -783,9 +794,20 @@ ${ flowData.code }
 
 				if ( varying.needsInterpolation ) {
 
-					const flat = type.includes( 'int' ) || type.includes( 'uv' ) || type.includes( 'iv' ) ? 'flat ' : '';
+					if ( varying.interpolationType ) {
+
+						const interpolationType = interpolationTypeMap[ varying.interpolationType ] || varying.interpolationType;
+						const sampling = interpolationModeMap[ varying.interpolationSampling ] || '';
+
+						snippet += `${ interpolationType } ${ sampling } out ${ type } ${ varying.name };\n`;
+
+					} else {
+
+						const flat = type.includes( 'int' ) || type.includes( 'uv' ) || type.includes( 'iv' ) ? 'flat ' : '';
+
+						snippet += `${ flat }out ${ type } ${ varying.name };\n`;
 
-					snippet += `${flat}out ${type} ${varying.name};\n`;
+					}
 
 				} else {
 
@@ -802,9 +824,22 @@ ${ flowData.code }
 				if ( varying.needsInterpolation ) {
 
 					const type = this.getType( varying.type );
-					const flat = type.includes( 'int' ) || type.includes( 'uv' ) || type.includes( 'iv' ) ? 'flat ' : '';
 
-					snippet += `${flat}in ${type} ${varying.name};\n`;
+					if ( varying.interpolationType ) {
+
+						const interpolationType = interpolationTypeMap[ varying.interpolationType ] || varying.interpolationType;
+						const sampling = interpolationModeMap[ varying.interpolationSampling ] || '';
+
+						snippet += `${ interpolationType } ${ sampling } in ${ type } ${ varying.name };\n`;
+
+
+					} else {
+
+						const flat = type.includes( 'int' ) || type.includes( 'uv' ) || type.includes( 'iv' ) ? 'flat ' : '';
+
+						snippet += `${ flat }in ${ type } ${ varying.name };\n`;
+
+					}
 
 				}
 

+ 9 - 2
src/renderers/webgpu/nodes/WGSLNodeBuilder.js

@@ -1566,10 +1566,17 @@ ${ flowData.code }
 
 					let attributesSnippet = `@location( ${index} )`;
 
-					if ( /^(int|uint|ivec|uvec)/.test( varying.type ) ) {
+					if ( varying.interpolationType ) {
 
-						attributesSnippet += ' @interpolate( flat )';
+						const samplingSnippet = varying.interpolationSampling !== null ? `, ${ varying.interpolationSampling } )` : ' )';
+
+						attributesSnippet += ` @interpolate( ${ varying.interpolationType }${ samplingSnippet }`;
+
+						// Otherwise, optimize interpolation when sensible
 
+					} else if ( /^(int|uint|ivec|uvec)/.test( varying.type ) ) {
+
+						attributesSnippet += ' @interpolate( flat )';
 
 					}
 

+ 1 - 0
test/e2e/puppeteer.js

@@ -135,6 +135,7 @@ const exceptionList = [
 
 	// WebGPURenderer: Unknown problem
 	'webgpu_backdrop_water',
+	"webgpu_centroid_sampling",
 	'webgpu_camera_logarithmicdepthbuffer',
 	'webgpu_lightprobe_cubecamera',
 	'webgpu_loader_materialx',

粤ICP备19079148号