Browse Source

Added HTMLTexture (#31233)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
mrdoob 6 days ago
parent
commit
bb42b15d02

+ 2 - 0
examples/files.json

@@ -153,6 +153,7 @@
 		"webgl_materials_texture_anisotropy",
 		"webgl_materials_texture_canvas",
 		"webgl_materials_texture_filters",
+		"webgl_materials_texture_html",
 		"webgl_materials_texture_manualmipmap",
 		"webgl_materials_texture_partialupdate",
 		"webgl_materials_texture_rotation",
@@ -385,6 +386,7 @@
 		"webgpu_materials_lightmap",
 		"webgpu_materials_matcap",
 		"webgpu_materials_sss",
+		"webgpu_materials_texture_html",
 		"webgpu_materials_texture_manualmipmap",
 		"webgpu_materials_transmission",
 		"webgpu_materials_toon",

+ 226 - 0
examples/jsm/interaction/InteractionManager.js

@@ -0,0 +1,226 @@
+import {
+	Matrix4,
+	Vector3
+} from 'three';
+
+const _pixelToLocal = new Matrix4();
+const _mvp = new Matrix4();
+const _viewport = new Matrix4();
+const _size = new Vector3();
+
+/**
+ * Manages interaction for 3D objects independently of the scene graph.
+ *
+ * For objects with an {@link HTMLTexture}, the manager computes CSS `matrix3d`
+ * transforms each frame so the underlying HTML elements stay aligned with
+ * their meshes. Because the elements are children of the canvas, the browser
+ * dispatches pointer events to them natively.
+ *
+ * ```js
+ * const interactions = new InteractionManager();
+ * interactions.connect( renderer, camera );
+ *
+ * // Objects live anywhere in the scene graph
+ * scene.add( mesh );
+ *
+ * // Register for interaction separately
+ * interactions.add( mesh );
+ *
+ * // In the animation loop
+ * interactions.update();
+ * ```
+ * @three_import import { InteractionManager } from 'three/addons/interaction/InteractionManager.js';
+ */
+class InteractionManager {
+
+	constructor() {
+
+		/**
+		 * The registered interactive objects.
+		 *
+		 * @type {Array<Object3D>}
+		 */
+		this.objects = [];
+
+		/**
+		 * The canvas element.
+		 *
+		 * @type {?HTMLCanvasElement}
+		 * @default null
+		 */
+		this.element = null;
+
+		/**
+		 * The camera used for computing the element transforms.
+		 *
+		 * @type {?Camera}
+		 * @default null
+		 */
+		this.camera = null;
+
+		this._cachedCssW = - 1;
+		this._cachedCssH = - 1;
+
+	}
+
+	/**
+	 * Adds one or more objects to the manager.
+	 *
+	 * @param {...Object3D} objects - The objects to add.
+	 * @return {this}
+	 */
+	add( ...objects ) {
+
+		for ( const object of objects ) {
+
+			if ( this.objects.indexOf( object ) === - 1 ) {
+
+				this.objects.push( object );
+
+			}
+
+		}
+
+		return this;
+
+	}
+
+	/**
+	 * Removes one or more objects from the manager.
+	 *
+	 * @param {...Object3D} objects - The objects to remove.
+	 * @return {this}
+	 */
+	remove( ...objects ) {
+
+		for ( const object of objects ) {
+
+			const index = this.objects.indexOf( object );
+
+			if ( index !== - 1 ) {
+
+				this.objects.splice( index, 1 );
+
+			}
+
+		}
+
+		return this;
+
+	}
+
+	/**
+	 * Stores the renderer and camera needed for computing element transforms.
+	 *
+	 * @param {(WebGPURenderer|WebGLRenderer)} renderer - The renderer.
+	 * @param {Camera} camera - The camera.
+	 */
+	connect( renderer, camera ) {
+
+		this.camera = camera;
+		this.element = renderer.domElement;
+
+	}
+
+	/**
+	 * Updates the element transforms for all registered objects.
+	 * Call this once per frame in the animation loop.
+	 */
+	update() {
+
+		const canvas = this.element;
+		const camera = this.camera;
+
+		if ( canvas === null || camera === null ) return;
+
+		// Viewport: NDC (-1,1) to canvas CSS pixels, Y flipped.
+		// Using CSS pixels (clientWidth/clientHeight) so the resulting matrix
+		// can be applied directly as a CSS transform without DPR conversion.
+
+		const cssW = canvas.clientWidth;
+		const cssH = canvas.clientHeight;
+
+		if ( cssW !== this._cachedCssW || cssH !== this._cachedCssH ) {
+
+			_viewport.set(
+				cssW / 2, 0, 0, cssW / 2,
+				0, - cssH / 2, 0, cssH / 2,
+				0, 0, 1, 0,
+				0, 0, 0, 1
+			);
+
+			this._cachedCssW = cssW;
+			this._cachedCssH = cssH;
+
+		}
+
+		for ( const object of this.objects ) {
+
+			const texture = object.material.map;
+
+			if ( ! texture || ! texture.isHTMLTexture ) continue;
+
+			const element = texture.image;
+
+			if ( ! element ) continue;
+
+			// Position at canvas origin so the CSS matrix3d maps correctly.
+			element.style.position = 'absolute';
+			element.style.left = '0';
+			element.style.top = '0';
+			element.style.transformOrigin = '0 0';
+
+			const elemW = element.offsetWidth;
+			const elemH = element.offsetHeight;
+
+			// Get mesh dimensions from geometry bounding box
+
+			const geometry = object.geometry;
+
+			if ( ! geometry.boundingBox ) geometry.computeBoundingBox();
+
+			geometry.boundingBox.getSize( _size );
+
+			// Map element pixel coords (0,0)-(elemW,elemH) to mesh local coords.
+			// Front face: top-left at (-sizeX/2, sizeY/2, maxZ), bottom-right at (sizeX/2, -sizeY/2, maxZ).
+
+			_pixelToLocal.set(
+				_size.x / elemW, 0, 0, - _size.x / 2,
+				0, - _size.y / elemH, 0, _size.y / 2,
+				0, 0, 1, geometry.boundingBox.max.z,
+				0, 0, 0, 1
+			);
+
+			// Model-View-Projection
+
+			_mvp.multiplyMatrices( camera.projectionMatrix, camera.matrixWorldInverse );
+			_mvp.multiply( object.matrixWorld );
+			_mvp.multiply( _pixelToLocal );
+
+			// Apply viewport
+
+			_mvp.premultiply( _viewport );
+
+			// The browser performs the perspective divide (by w) when applying the matrix3d.
+
+			element.style.transform = 'matrix3d(' + _mvp.elements.join( ',' ) + ')';
+
+		}
+
+	}
+
+	/**
+	 * Disconnects this manager, clearing the renderer and camera references.
+	 */
+	disconnect() {
+
+		this.camera = null;
+		this.element = null;
+		this._cachedCssW = - 1;
+		this._cachedCssH = - 1;
+
+	}
+
+}
+
+export { InteractionManager };

BIN
examples/screenshots/webgl_materials_texture_html.jpg


BIN
examples/screenshots/webgpu_materials_texture_html.jpg


+ 175 - 0
examples/webgl_materials_texture_html.html

@@ -0,0 +1,175 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js webgl - materials - html texture</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>
+			body {
+				background-color: #ffffff;
+			}
+			#draw_element {
+				width: 600px;
+				background-color: #aaaaaa;
+				color: #000000;
+				font-family: sans-serif;
+				font-size: 30px;
+				line-height: 1.5;
+				text-align: center;
+				padding: 30px;
+				/* border: 10px solid #cccccc; */
+			}
+			#draw_element img {
+			  animation: swing 1s ease-in-out infinite alternate;
+			}
+			#draw_element input[type="text"] {
+				font-size: 24px;
+				padding: 8px 12px;
+				border: 2px solid #888;
+				border-radius: 6px;
+				width: 80%;
+				margin-top: 10px;
+			}
+			#draw_element button {
+				font-size: 24px;
+				padding: 8px 20px;
+				margin-top: 10px;
+				border: none;
+				border-radius: 6px;
+				background-color: #4CAF50;
+				color: white;
+				cursor: pointer;
+			}
+			#draw_element button:hover {
+				background-color: #2196F3;
+			}
+			@keyframes swing {
+				from { transform: rotate(-15deg); }
+				to { transform: rotate(15deg); }
+			}
+		</style>
+	</head>
+	<body>
+
+		<div id="info">
+			<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> - webgl - HTMLTexture
+		</div>
+
+		<script type="importmap">
+			{
+				"imports": {
+					"three": "../build/three.module.js",
+					"three/addons/": "./jsm/"
+				}
+			}
+		</script>
+
+		<script type="module">
+
+			import * as THREE from 'three';
+			import { RoundedBoxGeometry } from 'three/addons/geometries/RoundedBoxGeometry.js';
+			import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js';
+			import { InteractionManager } from 'three/addons/interaction/InteractionManager.js';
+
+			let camera, scene, renderer, mesh, interactions;
+
+			init();
+
+			function init() {
+
+				renderer = new THREE.WebGLRenderer( { antialias: true } );
+
+				if ( renderer.getContext().texElementImage2D === undefined ) {
+
+					info.innerHTML += '<br>This browser does not support the <a href="https://github.com/WICG/html-in-canvas" target="_blank">HTML-in-Canvas API</a>.';
+
+				}
+
+				renderer.toneMapping = THREE.NeutralToneMapping;
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				renderer.setAnimationLoop( animate );
+				document.body.appendChild( renderer.domElement );
+
+				camera = new THREE.PerspectiveCamera( 50, window.innerWidth / window.innerHeight, 1, 2000 );
+				camera.position.z = 500;
+
+				scene = new THREE.Scene();
+				scene.background = new THREE.Color( 0xaaaaaa );
+				scene.environment = new THREE.PMREMGenerator( renderer ).fromScene( new RoomEnvironment(), 0.02 ).texture;
+
+				// HTML element
+
+				const element = document.createElement( 'div' );
+				element.id = 'draw_element';
+				element.innerHTML = `
+					Hello world!<br>I'm multi-line, <b>formatted</b>,
+					rotated text with emoji (&#128512;), RTL text
+					<span dir=rtl>من فارسی صحبت میکنم</span>,
+					vertical text,
+					<p style="writing-mode: vertical-rl;">
+					这是垂直文本
+					</p>
+					an inline image (<img width="150" src="textures/758px-Canestra_di_frutta_(Caravaggio).jpg">), and
+					<svg width="50" height="50">
+					<circle cx="25" cy="25" r="20" fill="green" />
+					<text x="25" y="30" font-size="15" text-anchor="middle" fill="#fff">
+						SVG
+					</text>
+					</svg>!
+					<br>
+					<input type="text" placeholder="Type here...">
+					<button>Click me</button>
+				`;
+
+				const geometry = new RoundedBoxGeometry( 200, 200, 200, 10, 10 );
+
+				const material = new THREE.MeshStandardMaterial( { roughness: 0, metalness: 0.5 } );
+				material.map = new THREE.HTMLTexture( element );
+
+				mesh = new THREE.Mesh( geometry, material );
+				scene.add( mesh );
+
+				// Interaction
+
+				interactions = new InteractionManager();
+				interactions.connect( renderer, camera );
+				interactions.add( mesh );
+
+				// Button click handler
+
+				element.querySelector( 'button' ).addEventListener( 'click', function () {
+
+					this.textContent = 'Clicked!';
+
+				} );
+
+				window.addEventListener( 'resize', onWindowResize );
+
+			}
+
+			function onWindowResize() {
+
+				camera.aspect = window.innerWidth / window.innerHeight;
+				camera.updateProjectionMatrix();
+
+				renderer.setSize( window.innerWidth, window.innerHeight );
+
+			}
+
+			function animate( time ) {
+
+				mesh.rotation.x = Math.sin( time * 0.0005 ) * 0.5;
+				mesh.rotation.y = Math.cos( time * 0.0008 ) * 0.5;
+
+				interactions.update();
+
+				renderer.render( scene, camera );
+
+			}
+
+		</script>
+
+	</body>
+</html>

+ 177 - 0
examples/webgpu_materials_texture_html.html

@@ -0,0 +1,177 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js webgpu - materials - html texture</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>
+			body {
+				background-color: #ffffff;
+			}
+			#draw_element {
+				width: 600px;
+				background-color: #aaaaaa;
+				color: #000000;
+				font-family: sans-serif;
+				font-size: 30px;
+				line-height: 1.5;
+				text-align: center;
+				padding: 30px;
+				/* border: 10px solid #cccccc; */
+			}
+			#draw_element img {
+			  animation: swing 1s ease-in-out infinite alternate;
+			}
+			#draw_element input[type="text"] {
+				font-size: 24px;
+				padding: 8px 12px;
+				border: 2px solid #888;
+				border-radius: 6px;
+				width: 80%;
+				margin-top: 10px;
+			}
+			#draw_element button {
+				font-size: 24px;
+				padding: 8px 20px;
+				margin-top: 10px;
+				border: none;
+				border-radius: 6px;
+				background-color: #4CAF50;
+				color: white;
+				cursor: pointer;
+			}
+			#draw_element button:hover {
+				background-color: #2196F3;
+			}
+			@keyframes swing {
+				from { transform: rotate(-15deg); }
+				to { transform: rotate(15deg); }
+			}
+		</style>
+	</head>
+	<body>
+
+		<div id="info">
+			<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> - webgpu - HTMLTexture
+		</div>
+
+		<script type="importmap">
+			{
+				"imports": {
+					"three": "../build/three.webgpu.js",
+					"three/addons/": "./jsm/",
+					"three/tsl": "../build/three.tsl.js"
+				}
+			}
+		</script>
+
+		<script type="module">
+
+			import * as THREE from 'three';
+			import { RoundedBoxGeometry } from 'three/addons/geometries/RoundedBoxGeometry.js';
+			import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js';
+			import { InteractionManager } from 'three/addons/interaction/InteractionManager.js';
+
+			let camera, scene, renderer, mesh, interactions;
+
+			init();
+
+			async function init() {
+
+				renderer = new THREE.WebGPURenderer( { antialias: true } );
+				await renderer.init();
+
+				if ( ! ( 'requestPaint' in renderer.domElement ) ) {
+
+					info.innerHTML += '<br>This browser does not support the <a href="https://github.com/WICG/html-in-canvas" target="_blank">HTML-in-Canvas API</a>.';
+
+				}
+
+				renderer.toneMapping = THREE.NeutralToneMapping;
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				renderer.setAnimationLoop( animate );
+				document.body.appendChild( renderer.domElement );
+
+				camera = new THREE.PerspectiveCamera( 50, window.innerWidth / window.innerHeight, 1, 2000 );
+				camera.position.z = 500;
+
+				scene = new THREE.Scene();
+				scene.background = new THREE.Color( 0xaaaaaa );
+				scene.environment = new THREE.PMREMGenerator( renderer ).fromScene( new RoomEnvironment(), 0.02 ).texture;
+
+				// HTML element
+
+				const element = document.createElement( 'div' );
+				element.id = 'draw_element';
+				element.innerHTML = `
+					Hello world!<br>I'm multi-line, <b>formatted</b>,
+					rotated text with emoji (&#128512;), RTL text
+					<span dir=rtl>من فارسی صحبت میکنم</span>,
+					vertical text,
+					<p style="writing-mode: vertical-rl;">
+					这是垂直文本
+					</p>
+					an inline image (<img width="150" src="textures/758px-Canestra_di_frutta_(Caravaggio).jpg">), and
+					<svg width="50" height="50">
+					<circle cx="25" cy="25" r="20" fill="green" />
+					<text x="25" y="30" font-size="15" text-anchor="middle" fill="#fff">
+						SVG
+					</text>
+					</svg>!
+					<br>
+					<input type="text" placeholder="Type here...">
+					<button>Click me</button>
+				`;
+
+				const geometry = new RoundedBoxGeometry( 200, 200, 200, 10, 10 );
+
+				const material = new THREE.MeshStandardMaterial( { roughness: 0, metalness: 0.5 } );
+				material.map = new THREE.HTMLTexture( element );
+
+				mesh = new THREE.Mesh( geometry, material );
+				scene.add( mesh );
+
+				// Interaction
+
+				interactions = new InteractionManager();
+				interactions.connect( renderer, camera );
+				interactions.add( mesh );
+
+				// Button click handler
+
+				element.querySelector( 'button' ).addEventListener( 'click', function () {
+
+					this.textContent = 'Clicked!';
+
+				} );
+
+				window.addEventListener( 'resize', onWindowResize );
+
+			}
+
+			function onWindowResize() {
+
+				camera.aspect = window.innerWidth / window.innerHeight;
+				camera.updateProjectionMatrix();
+
+				renderer.setSize( window.innerWidth, window.innerHeight );
+
+			}
+
+			function animate( time ) {
+
+				mesh.rotation.x = Math.sin( time * 0.0005 ) * 0.5;
+				mesh.rotation.y = Math.cos( time * 0.0008 ) * 0.5;
+
+				interactions.update();
+
+				renderer.render( scene, camera );
+
+			}
+
+		</script>
+
+	</body>
+</html>

+ 1 - 0
src/Three.Core.js

@@ -33,6 +33,7 @@ export { CompressedArrayTexture } from './textures/CompressedArrayTexture.js';
 export { CompressedCubeTexture } from './textures/CompressedCubeTexture.js';
 export { CubeTexture } from './textures/CubeTexture.js';
 export { CanvasTexture } from './textures/CanvasTexture.js';
+export { HTMLTexture } from './textures/HTMLTexture.js';
 export { DepthTexture } from './textures/DepthTexture.js';
 export { CubeDepthTexture } from './textures/CubeDepthTexture.js';
 export { ExternalTexture } from './textures/ExternalTexture.js';

+ 31 - 1
src/renderers/common/Textures.js

@@ -220,6 +220,30 @@ class Textures extends DataMap {
 
 		}
 
+		// Ensure HTMLTexture elements are in the canvas before measuring size.
+
+		if ( texture.isHTMLTexture && texture.image ) {
+
+			const canvas = this.renderer.domElement;
+
+			if ( 'requestPaint' in canvas ) {
+
+				if ( ! canvas.hasAttribute( 'layoutsubtree' ) ) {
+
+					canvas.setAttribute( 'layoutsubtree', 'true' );
+
+				}
+
+				if ( texture.image.parentNode !== canvas ) {
+
+					canvas.appendChild( texture.image );
+
+				}
+
+			}
+
+		}
+
 		//
 
 		const { width, height, depth } = this.getSize( texture );
@@ -390,7 +414,13 @@ class Textures extends DataMap {
 
 			if ( image.image !== undefined ) image = image.image;
 
-			if ( ( typeof HTMLVideoElement !== 'undefined' ) && ( image instanceof HTMLVideoElement ) ) {
+			if ( texture.isHTMLTexture ) {
+
+				target.width = image.offsetWidth || 1;
+				target.height = image.offsetHeight || 1;
+				target.depth = 1;
+
+			} else if ( ( typeof HTMLVideoElement !== 'undefined' ) && ( image instanceof HTMLVideoElement ) ) {
 
 				target.width = image.videoWidth || 1;
 				target.height = image.videoHeight || 1;

+ 60 - 0
src/renderers/webgl/WebGLTextures.js

@@ -11,6 +11,7 @@ function WebGLTextures( _gl, extensions, state, properties, capabilities, utils,
 
 	const _imageDimensions = new Vector2();
 	const _videoTextures = new WeakMap();
+	const _htmlTextures = new Set();
 	let _canvas;
 
 	const _sources = new WeakMap(); // maps WebglTexture objects to instances of Source
@@ -334,6 +335,12 @@ function WebGLTextures( _gl, extensions, state, properties, capabilities, utils,
 
 		}
 
+		if ( texture.isHTMLTexture ) {
+
+			_htmlTextures.delete( texture );
+
+		}
+
 	}
 
 	function onRenderTargetDispose( event ) {
@@ -1247,6 +1254,59 @@ function WebGLTextures( _gl, extensions, state, properties, capabilities, utils,
 
 				}
 
+			} else if ( texture.isHTMLTexture ) {
+
+				if ( 'texElement2D' in _gl ) {
+
+					const canvas = _gl.canvas;
+
+					// Ensure the canvas supports HTML-in-Canvas and the element is a child.
+					if ( ! canvas.hasAttribute( 'layoutsubtree' ) ) {
+
+						canvas.setAttribute( 'layoutsubtree', 'true' );
+
+					}
+
+					if ( image.parentNode !== canvas ) {
+
+						canvas.appendChild( image );
+
+						// Register and set up a shared paint callback for all HTMLTextures.
+						_htmlTextures.add( texture );
+
+						canvas.onpaint = ( event ) => {
+
+							const changed = event.changedElements;
+
+							for ( const t of _htmlTextures ) {
+
+								if ( changed.includes( t.image ) ) {
+
+									t.needsUpdate = true;
+
+								}
+
+							}
+
+						};
+
+						canvas.requestPaint();
+						return;
+
+					}
+
+					const level = 0;
+					const internalFormat = _gl.RGBA;
+					const srcFormat = _gl.RGBA;
+					const srcType = _gl.UNSIGNED_BYTE;
+
+					_gl.texElementImage2D( _gl.TEXTURE_2D, level, internalFormat, srcFormat, srcType, image );
+					_gl.texParameteri( _gl.TEXTURE_2D, _gl.TEXTURE_MIN_FILTER, _gl.LINEAR );
+					_gl.texParameteri( _gl.TEXTURE_2D, _gl.TEXTURE_WRAP_S, _gl.CLAMP_TO_EDGE );
+					_gl.texParameteri( _gl.TEXTURE_2D, _gl.TEXTURE_WRAP_T, _gl.CLAMP_TO_EDGE );
+
+				}
+
 			} else {
 
 				// regular Texture (image, video, canvas)

+ 78 - 0
src/renderers/webgpu/utils/WebGPUTextureUtils.js

@@ -95,6 +95,13 @@ class WebGPUTextureUtils {
 		 */
 		this._samplerCache = new Map();
 
+		/**
+		 * A set of HTMLTextures that need paint updates.
+		 *
+		 * @type {Set<HTMLTexture>}
+		 */
+		this._htmlTextures = new Set();
+
 	}
 
 	/**
@@ -357,6 +364,8 @@ class WebGPUTextureUtils {
 
 		if ( textureData.msaaTexture !== undefined ) textureData.msaaTexture.destroy();
 
+		this._htmlTextures.delete( texture );
+
 		backend.delete( texture );
 
 	}
@@ -566,6 +575,41 @@ class WebGPUTextureUtils {
 
 			this._copyCubeMapToTexture( texture, textureData.texture, textureDescriptorGPU );
 
+		} else if ( texture.isHTMLTexture ) {
+
+			const device = this.backend.device;
+			const canvas = this.backend.renderer.domElement;
+			const image = texture.image;
+
+			if ( typeof device.queue.copyElementImageToTexture !== 'function' ) return;
+
+			// Set up paint callback if not already done.
+			if ( ! textureData.hasPaintCallback ) {
+
+				textureData.hasPaintCallback = true;
+
+				this._addHTMLTexture( texture );
+
+				// Wait for the browser to paint the element before uploading.
+				canvas.requestPaint();
+				return;
+
+			}
+
+			const width = textureDescriptorGPU.size.width;
+			const height = textureDescriptorGPU.size.height;
+
+			device.queue.copyElementImageToTexture(
+				image, width, height,
+				{ texture: textureData.texture }
+			);
+
+			if ( texture.flipY ) {
+
+				this._flipY( textureData.texture, textureDescriptorGPU );
+
+			}
+
 		} else {
 
 			if ( mipmaps.length > 0 ) {
@@ -654,12 +698,46 @@ class WebGPUTextureUtils {
 
 	}
 
+	/**
+	 * Registers an HTMLTexture for paint updates.
+	 * Sets up a single shared `onpaint` handler on the canvas
+	 * that notifies all registered HTMLTextures.
+	 *
+	 * @private
+	 * @param {HTMLTexture} texture - The HTMLTexture to register.
+	 */
+	_addHTMLTexture( texture ) {
+
+		this._htmlTextures.add( texture );
+
+		const canvas = this.backend.renderer.domElement;
+		const htmlTextures = this._htmlTextures;
+
+		canvas.onpaint = ( event ) => {
+
+			const changed = event.changedElements;
+
+			for ( const t of htmlTextures ) {
+
+				if ( changed.includes( t.image ) ) {
+
+					t.needsUpdate = true;
+
+				}
+
+			}
+
+		};
+
+	}
+
 	/**
 	 * Frees all internal resources.
 	 */
 	dispose() {
 
 		this._samplerCache.clear();
+		this._htmlTextures.clear();
 
 	}
 

+ 74 - 0
src/textures/HTMLTexture.js

@@ -0,0 +1,74 @@
+import { Texture } from './Texture.js';
+
+/**
+ * Creates a texture from an HTML element.
+ *
+ * This is almost the same as the base texture class, except that it sets {@link Texture#needsUpdate}
+ * to `true` immediately and listens for the parent canvas's paint events to trigger updates.
+ *
+ * @augments Texture
+ */
+class HTMLTexture extends Texture {
+
+	/**
+	 * Constructs a new texture.
+	 *
+	 * @param {HTMLElement} [element] - The HTML element.
+	 * @param {number} [mapping=Texture.DEFAULT_MAPPING] - The texture mapping.
+	 * @param {number} [wrapS=ClampToEdgeWrapping] - The wrapS value.
+	 * @param {number} [wrapT=ClampToEdgeWrapping] - The wrapT value.
+	 * @param {number} [magFilter=LinearFilter] - The mag filter value.
+	 * @param {number} [minFilter=LinearMipmapLinearFilter] - The min filter value.
+	 * @param {number} [format=RGBAFormat] - The texture format.
+	 * @param {number} [type=UnsignedByteType] - The texture type.
+	 * @param {number} [anisotropy=Texture.DEFAULT_ANISOTROPY] - The anisotropy value.
+	 */
+	constructor( element, mapping, wrapS, wrapT, magFilter, minFilter, format, type, anisotropy ) {
+
+		super( element, mapping, wrapS, wrapT, magFilter, minFilter, format, type, anisotropy );
+
+		/**
+		 * This flag can be used for type testing.
+		 *
+		 * @type {boolean}
+		 * @readonly
+		 * @default true
+		 */
+		this.isHTMLTexture = true;
+		this.generateMipmaps = false;
+
+		this.needsUpdate = true;
+
+		const parent = element ? element.parentNode : null;
+
+		if ( parent !== null && 'requestPaint' in parent ) {
+
+			parent.onpaint = () => {
+
+				this.needsUpdate = true;
+
+			};
+
+			parent.requestPaint();
+
+		}
+
+	}
+
+	dispose() {
+
+		const parent = this.image ? this.image.parentNode : null;
+
+		if ( parent !== null && 'onpaint' in parent ) {
+
+			parent.onpaint = null;
+
+		}
+
+		super.dispose();
+
+	}
+
+}
+
+export { HTMLTexture };

粤ICP备19079148号