Pārlūkot izejas kodu

WebGPURenderer: Add support for manual mipmaps for regular textures. (#31637)

Michael Herzog 6 mēneši atpakaļ
vecāks
revīzija
fa4c2a9482

+ 1 - 0
examples/files.json

@@ -366,6 +366,7 @@
 		"webgpu_materials_lightmap",
 		"webgpu_materials_matcap",
 		"webgpu_materials_sss",
+		"webgpu_materials_texture_manualmipmap",
 		"webgpu_materials_transmission",
 		"webgpu_materials_toon",
 		"webgpu_materials_video",

BIN
examples/screenshots/webgpu_materials_texture_manualmipmap.jpg


+ 238 - 0
examples/webgpu_materials_texture_manualmipmap.html

@@ -0,0 +1,238 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js WebGPU - materials - manual mipmaping</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">
+		<style>
+
+			.lbl { color:#fff; font-size:16px; font-weight:bold; position: absolute; bottom:0px; z-index:100; text-shadow:#000 1px 1px 1px; background-color:rgba(0,0,0,0.85); padding:1em }
+			#lbl_left { text-align:left; left:0px }
+			#lbl_right { text-align:left; right:0px }
+
+			.g { color:#aaa }
+			.c { color:#fa0 }
+
+		</style>
+	</head>
+
+	<body>
+		<div id="info">
+			<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> - texture manual mipmapping example<br/>
+			painting by <a href="http://en.wikipedia.org/wiki/Basket_of_Fruit_%28Caravaggio%29">Caravaggio</a>
+		</div>
+
+		<div id="lbl_left" class="lbl">
+		Floor <span class="g">(128x128)</span><br/>
+		mag: <span class="c">Linear</span><br/>
+		min: <span class="c">LinearMipmapLinear</span><br/>
+		<br/>
+		Painting <span class="g">(748x600)</span><br/>
+		mag: <span class="c">Linear</span><br/>
+		min: <span class="c">Linear</span>
+		</div>
+
+		<div id="lbl_right" class="lbl">
+		Floor <br/>
+		mag: <span class="c">Nearest</span><br/>
+		min: <span class="c">NearestMipmapNearestFilter</span><br/>
+		<br/>
+		Painting <br/>
+		mag: <span class="c">Nearest</span><br/>
+		min: <span class="c">Nearest</span>
+		</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';
+
+			const SCREEN_WIDTH = window.innerWidth;
+			const SCREEN_HEIGHT = window.innerHeight;
+
+			let container;
+
+			let camera, scene1, scene2, renderer;
+
+			let mouseX = 0, mouseY = 0;
+
+			const windowHalfX = window.innerWidth / 2;
+			const windowHalfY = window.innerHeight / 2;
+
+			init();
+
+			async function init() {
+
+				container = document.createElement( 'div' );
+				document.body.appendChild( container );
+
+				camera = new THREE.PerspectiveCamera( 35, SCREEN_WIDTH / SCREEN_HEIGHT, 1, 5000 );
+				camera.position.z = 1500;
+
+				scene1 = new THREE.Scene();
+				scene1.background = new THREE.Color( 0x000000 );
+				scene1.fog = new THREE.Fog( 0x000000, 1500, 4000 );
+
+				scene2 = new THREE.Scene();
+				scene2.background = new THREE.Color( 0x000000 );
+				scene2.fog = new THREE.Fog( 0x000000, 1500, 4000 );
+
+				// GROUND
+
+				const canvas = mipmap( 128, '#f00' );
+				const textureCanvas1 = new THREE.CanvasTexture( canvas );
+				textureCanvas1.mipmaps[ 0 ] = canvas;
+				textureCanvas1.mipmaps[ 1 ] = mipmap( 64, '#0f0' );
+				textureCanvas1.mipmaps[ 2 ] = mipmap( 32, '#00f' );
+				textureCanvas1.mipmaps[ 3 ] = mipmap( 16, '#400' );
+				textureCanvas1.mipmaps[ 4 ] = mipmap( 8, '#040' );
+				textureCanvas1.mipmaps[ 5 ] = mipmap( 4, '#004' );
+				textureCanvas1.mipmaps[ 6 ] = mipmap( 2, '#044' );
+				textureCanvas1.mipmaps[ 7 ] = mipmap( 1, '#404' );
+				textureCanvas1.colorSpace = THREE.SRGBColorSpace;
+				textureCanvas1.repeat.set( 1000, 1000 );
+				textureCanvas1.wrapS = THREE.RepeatWrapping;
+				textureCanvas1.wrapT = THREE.RepeatWrapping;
+
+				const textureCanvas2 = textureCanvas1.clone();
+				textureCanvas2.magFilter = THREE.NearestFilter;
+				textureCanvas2.minFilter = THREE.NearestMipmapNearestFilter;
+
+				const materialCanvas1 = new THREE.MeshBasicMaterial( { map: textureCanvas1 } );
+				const materialCanvas2 = new THREE.MeshBasicMaterial( { color: 0xffccaa, map: textureCanvas2 } );
+
+				const geometry = new THREE.PlaneGeometry( 100, 100 );
+
+				const meshCanvas1 = new THREE.Mesh( geometry, materialCanvas1 );
+				meshCanvas1.rotation.x = - Math.PI / 2;
+				meshCanvas1.scale.set( 1000, 1000, 1000 );
+
+				const meshCanvas2 = new THREE.Mesh( geometry, materialCanvas2 );
+				meshCanvas2.rotation.x = - Math.PI / 2;
+				meshCanvas2.scale.set( 1000, 1000, 1000 );
+
+				scene1.add( meshCanvas1 );
+				scene2.add( meshCanvas2 );
+
+				// PAINTING
+
+				const texturePainting1 = await new THREE.TextureLoader().loadAsync( 'textures/758px-Canestra_di_frutta_(Caravaggio).jpg' );
+				const texturePainting2 = texturePainting1.clone();
+
+				texturePainting1.colorSpace = THREE.SRGBColorSpace;
+				texturePainting2.colorSpace = THREE.SRGBColorSpace;
+
+				texturePainting1.minFilter = texturePainting1.magFilter = THREE.LinearFilter;
+				texturePainting2.minFilter = texturePainting2.magFilter = THREE.NearestFilter;
+
+				const materialPainting1 = new THREE.MeshBasicMaterial( { color: 0xffffff, map: texturePainting1 } );
+				const materialPainting2 = new THREE.MeshBasicMaterial( { color: 0xffccaa, map: texturePainting2 } );
+
+				const geometryPainting = new THREE.PlaneGeometry( 100, 100 );
+				const mesh1 = new THREE.Mesh( geometryPainting, materialPainting1 );
+				const mesh2 = new THREE.Mesh( geometryPainting, materialPainting2 );
+
+				addPainting( scene1, mesh1 );
+				addPainting( scene2, mesh2 );
+
+				function addPainting( zscene, zmesh ) {
+
+					const image = texturePainting1.image;
+
+					zmesh.scale.x = image.width / 100;
+					zmesh.scale.y = image.height / 100;
+
+					zscene.add( zmesh );
+
+					const meshFrame = new THREE.Mesh( geometry, new THREE.MeshBasicMaterial( { color: 0x000000 } ) );
+					meshFrame.position.z = - 10.0;
+					meshFrame.scale.x = 1.1 * image.width / 100;
+					meshFrame.scale.y = 1.1 * image.height / 100;
+					zscene.add( meshFrame );
+
+					const meshShadow = new THREE.Mesh( geometry, new THREE.MeshBasicMaterial( { color: 0x000000, opacity: 0.75, transparent: true } ) );
+					meshShadow.position.y = - 1.1 * image.height / 2;
+					meshShadow.position.z = - 1.1 * image.height / 2;
+					meshShadow.rotation.x = - Math.PI / 2;
+					meshShadow.scale.x = 1.1 * image.width / 100;
+					meshShadow.scale.y = 1.1 * image.height / 100;
+					zscene.add( meshShadow );
+
+					const floorHeight = - 1.117 * image.height / 2;
+					meshCanvas1.position.y = meshCanvas2.position.y = floorHeight;
+
+				}
+
+				renderer = new THREE.WebGPURenderer( { antialias: true } );
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( SCREEN_WIDTH, SCREEN_HEIGHT );
+				renderer.setAnimationLoop( animate );
+				renderer.autoClear = false;
+
+				renderer.domElement.style.position = 'relative';
+				container.appendChild( renderer.domElement );
+
+				document.addEventListener( 'mousemove', onDocumentMouseMove );
+
+			}
+
+			function mipmap( size, color ) {
+
+				const imageCanvas = document.createElement( 'canvas' );
+				const context = imageCanvas.getContext( '2d' );
+
+				imageCanvas.width = imageCanvas.height = size;
+
+				context.fillStyle = '#444';
+				context.fillRect( 0, 0, size, size );
+
+				context.fillStyle = color;
+				context.fillRect( 0, 0, size / 2, size / 2 );
+				context.fillRect( size / 2, size / 2, size / 2, size / 2 );
+				return imageCanvas;
+
+			}
+
+			function onDocumentMouseMove( event ) {
+
+				mouseX = ( event.clientX - windowHalfX );
+				mouseY = ( event.clientY - windowHalfY );
+
+			}
+
+
+			function animate() {
+
+				camera.position.x += ( mouseX - camera.position.x ) * .05;
+				camera.position.y += ( - ( mouseY - 200 ) - camera.position.y ) * .05;
+
+				camera.lookAt( scene1.position );
+
+				renderer.clear();
+				renderer.setScissorTest( true );
+
+				renderer.setScissor( 0, 0, SCREEN_WIDTH / 2 - 2, SCREEN_HEIGHT );
+				renderer.render( scene1, camera );
+
+				renderer.setScissor( SCREEN_WIDTH / 2, 0, SCREEN_WIDTH / 2 - 2, SCREEN_HEIGHT );
+				renderer.render( scene2, camera );
+
+				renderer.setScissorTest( false );
+
+			}
+
+		</script>
+
+	</body>
+</html>

+ 13 - 9
src/renderers/common/Textures.js

@@ -418,21 +418,25 @@ class Textures extends DataMap {
 
 		let mipLevelCount;
 
-		if ( texture.isCompressedTexture ) {
+		if ( texture.mipmaps.length > 0 ) {
 
-			if ( texture.mipmaps ) {
+			mipLevelCount = texture.mipmaps.length;
 
-				mipLevelCount = texture.mipmaps.length;
+		} else {
 
-			} else {
+			if ( texture.isCompressedTexture === true ) {
+
+				// it is not possible to compute mipmaps for compressed textures. So
+				// when no mipmaps are defined in "texture.mipmaps", force a texture
+				// level of 1
 
 				mipLevelCount = 1;
 
-			}
+			} else {
 
-		} else {
+				mipLevelCount = Math.floor( Math.log2( Math.max( width, height ) ) ) + 1;
 
-			mipLevelCount = Math.floor( Math.log2( Math.max( width, height ) ) ) + 1;
+			}
 
 		}
 
@@ -441,14 +445,14 @@ class Textures extends DataMap {
 	}
 
 	/**
-	 * Returns `true` if the given texture requires mipmaps.
+	 * Returns `true` if the given texture makes use of mipmapping.
 	 *
 	 * @param {Texture} texture - The texture.
 	 * @return {boolean} Whether mipmaps are required or not.
 	 */
 	needsMipmaps( texture ) {
 
-		return texture.isCompressedTexture === true || texture.generateMipmaps;
+		return texture.generateMipmaps === true || texture.mipmaps.length > 0;
 
 	}
 

+ 19 - 2
src/renderers/webgl-fallback/utils/WebGLTextureUtils.js

@@ -603,9 +603,26 @@ class WebGLTextureUtils {
 
 		} else {
 
-			const image = getImage( options.image );
+			const mipmaps = texture.mipmaps;
+
+			if ( mipmaps.length > 0 ) {
+
+				for ( let i = 0, il = mipmaps.length; i < il; i ++ ) {
+
+					const mipmap = mipmaps[ i ];
+
+					const image = getImage( mipmap );
+					gl.texSubImage2D( glTextureType, i, 0, 0, mipmap.width, mipmap.height, glFormat, glType, image );
+
+				}
+
+			} else {
+
+				const image = getImage( options.image );
+				gl.texSubImage2D( glTextureType, 0, 0, 0, width, height, glFormat, glType, image );
+
+			}
 
-			gl.texSubImage2D( glTextureType, 0, 0, 0, width, height, glFormat, glType, image );
 
 		}
 

+ 45 - 9
src/renderers/webgpu/utils/WebGPUTextureUtils.js

@@ -434,6 +434,7 @@ class WebGPUTextureUtils {
 	updateTexture( texture, options ) {
 
 		const textureData = this.backend.get( texture );
+		const mipmaps = texture.mipmaps;
 
 		const { textureDescriptorGPU } = textureData;
 
@@ -444,7 +445,22 @@ class WebGPUTextureUtils {
 
 		if ( texture.isDataTexture ) {
 
-			this._copyBufferToTexture( options.image, textureData.texture, textureDescriptorGPU, 0, texture.flipY );
+			if ( mipmaps.length > 0 ) {
+
+				for ( let i = 0, il = mipmaps.length; i < il; i ++ ) {
+
+					const mipmap = mipmaps[ i ];
+
+					this._copyBufferToTexture( mipmap, textureData.texture, textureDescriptorGPU, 0, texture.flipY, 0, i );
+
+				}
+
+
+			} else {
+
+				this._copyBufferToTexture( options.image, textureData.texture, textureDescriptorGPU, 0, texture.flipY );
+
+			}
 
 		} else if ( texture.isArrayTexture || texture.isDataArrayTexture || texture.isData3DTexture ) {
 
@@ -464,7 +480,22 @@ class WebGPUTextureUtils {
 
 		} else {
 
-			this._copyImageToTexture( options.image, textureData.texture, textureDescriptorGPU, 0, texture.flipY, texture.premultiplyAlpha );
+			if ( mipmaps.length > 0 ) {
+
+				for ( let i = 0, il = mipmaps.length; i < il; i ++ ) {
+
+					const mipmap = mipmaps[ i ];
+
+					this._copyImageToTexture( mipmap, textureData.texture, textureDescriptorGPU, 0, texture.flipY, texture.premultiplyAlpha, i );
+
+				}
+
+
+			} else {
+
+				this._copyImageToTexture( options.image, textureData.texture, textureDescriptorGPU, 0, texture.flipY, texture.premultiplyAlpha );
+
+			}
 
 		}
 
@@ -633,23 +664,27 @@ class WebGPUTextureUtils {
 	 * @param {number} originDepth - The origin depth.
 	 * @param {boolean} flipY - Whether to flip texture data along their vertical axis or not.
 	 * @param {boolean} premultiplyAlpha - Whether the texture should have its RGB channels premultiplied by the alpha channel or not.
+	 * @param {number} [mipLevel=0] - The mip level where the data should be copied to.
 	 */
-	_copyImageToTexture( image, textureGPU, textureDescriptorGPU, originDepth, flipY, premultiplyAlpha ) {
+	_copyImageToTexture( image, textureGPU, textureDescriptorGPU, originDepth, flipY, premultiplyAlpha, mipLevel = 0 ) {
 
 		const device = this.backend.device;
 
+		const width = ( mipLevel > 0 ) ? image.width : textureDescriptorGPU.size.width;
+		const height = ( mipLevel > 0 ) ? image.height : textureDescriptorGPU.size.height;
+
 		device.queue.copyExternalImageToTexture(
 			{
 				source: image,
 				flipY: flipY
 			}, {
 				texture: textureGPU,
-				mipLevel: 0,
+				mipLevel: mipLevel,
 				origin: { x: 0, y: 0, z: originDepth },
 				premultipliedAlpha: premultiplyAlpha
 			}, {
-				width: textureDescriptorGPU.size.width,
-				height: textureDescriptorGPU.size.height,
+				width: width,
+				height: height,
 				depthOrArrayLayers: 1
 			}
 		);
@@ -713,9 +748,10 @@ class WebGPUTextureUtils {
 	 * @param {Object} textureDescriptorGPU - The GPU texture descriptor.
 	 * @param {number} originDepth - The origin depth.
 	 * @param {boolean} flipY - Whether to flip texture data along their vertical axis or not.
-	 * @param {number} [depth=0] - TODO.
+	 * @param {number} [depth=0] - The depth offset when copying array or 3D texture data.
+	 * @param {number} [mipLevel=0] - The mip level where the data should be copied to.
 	 */
-	_copyBufferToTexture( image, textureGPU, textureDescriptorGPU, originDepth, flipY, depth = 0 ) {
+	_copyBufferToTexture( image, textureGPU, textureDescriptorGPU, originDepth, flipY, depth = 0, mipLevel = 0 ) {
 
 		// @TODO: Consider to use GPUCommandEncoder.copyBufferToTexture()
 		// @TODO: Consider to support valid buffer layouts with other formats like RGB
@@ -730,7 +766,7 @@ class WebGPUTextureUtils {
 		device.queue.writeTexture(
 			{
 				texture: textureGPU,
-				mipLevel: 0,
+				mipLevel: mipLevel,
 				origin: { x: 0, y: 0, z: originDepth }
 			},
 			data,

粤ICP备19079148号