From 440824c2bae48717b1703d8495992a9223874eac Mon Sep 17 00:00:00 2001 From: Martin Valigursky Date: Fri, 29 May 2026 16:07:02 +0100 Subject: [PATCH 1/2] feat(webgpu): support HTML-in-Canvas textures via copyElementImageToTexture Adds WebGPU support for the HTML-in-Canvas API, previously WebGL-only. Generic HTML elements can now be used as live texture sources on the WebGPU backend. - WebGPU device reports supportsHtmlTextures = true when the browser exposes queue.copyElementImageToTexture - WebGPU 2D texture uploads route generic HTML elements through a new copyElementImageToTexture path - Updated html-texture and html-texture-configurator examples --- .../html-texture-configurator.example.mjs | 19 ++++++------ .../examples/misc/html-texture.example.mjs | 10 ++++--- .../graphics/webgpu/webgpu-graphics-device.js | 3 ++ .../graphics/webgpu/webgpu-texture.js | 30 ++++++++++++++++++- 4 files changed, 48 insertions(+), 14 deletions(-) diff --git a/examples/src/examples/misc/html-texture-configurator.example.mjs b/examples/src/examples/misc/html-texture-configurator.example.mjs index 15c359bc13b..5085217e5de 100644 --- a/examples/src/examples/misc/html-texture-configurator.example.mjs +++ b/examples/src/examples/misc/html-texture-configurator.example.mjs @@ -1,14 +1,14 @@ // @config // -// 3D product configurator with an HTML UI panel rendered as a WebGL texture via -// the {accent:HTML-in-Canvas} API. Uses {accent:getElementTransform} for -// interactive hit testing — clicks and hovers on the 3D panel are handled by -// the browser's native DOM event system. {accent:Click} to switch shoe -// material variants. +// 3D product configurator with an HTML UI panel rendered as a GPU texture via +// the {accent:HTML-in-Canvas} API, supported on both {accent:WebGL} and +// {accent:WebGPU}. Uses {accent:getElementTransform} for interactive hit +// testing — clicks and hovers on the 3D panel are handled by the browser's +// native DOM event system. {accent:Click} to switch shoe material variants. // // This example demonstrates a 3D product configurator where an interactive HTML -// panel (styled with CSS glassmorphism) is rendered as a WebGL texture on a 3D +// panel (styled with CSS glassmorphism) is rendered as a GPU texture on a 3D // plane next to a shoe model with glTF KHR_materials_variants. The HtmlSync // class keeps the DOM element's CSS transform in sync with the 3D projection so // the browser can hit-test clicks and hovers on the HTML buttons. @@ -160,7 +160,7 @@ window.addEventListener('resize', resize); // --- HTML UI Panel --- // The UI panel is a regular HTML
styled with CSS. When HTML-in-Canvas is -// supported it gets appended to the canvas (so it's composited into the WebGL +// supported it gets appended to the canvas (so it's composited into the GPU // surface and can be used as a texture). Otherwise it falls back to a fixed DOM // overlay on top of the canvas. const PANEL_WIDTH = 280; @@ -262,9 +262,10 @@ const updatePanel = () => { }; updatePanel(); -// --- HTML-to-WebGL texture pipeline --- +// --- HTML-to-GPU texture pipeline --- // When HTML-in-Canvas is available, the HTML panel is appended as a child of -// the canvas and captured into a WebGL texture via texElementImage2D. The +// the canvas and captured into a GPU texture (texElementImage2D on WebGL, +// copyElementImageToTexture on WebGPU). The // browser fires a "paint" event whenever the panel's visual content changes; // we respond by re-uploading the texture. The first paint uses setSource() to // bind the element, subsequent paints just call upload(). diff --git a/examples/src/examples/misc/html-texture.example.mjs b/examples/src/examples/misc/html-texture.example.mjs index 9e2d71f82bc..10932b25e22 100644 --- a/examples/src/examples/misc/html-texture.example.mjs +++ b/examples/src/examples/misc/html-texture.example.mjs @@ -1,13 +1,15 @@ // @config // -// Renders live HTML content directly as a WebGL texture via the -// {accent:HTML-in-Canvas} API ({accent:texElementImage2D}). Includes animated -// CSS gradients, text glow, and a pulsing circle — all driven by standard CSS. +// Renders live HTML content directly as a GPU texture via the +// {accent:HTML-in-Canvas} API, supported on both {accent:WebGL} and +// {accent:WebGPU}. Includes animated CSS gradients, text glow, and a pulsing +// circle — all driven by standard CSS. // // This example demonstrates the HTML-in-Canvas API: a styled HTML element with // CSS animations is appended to a canvas marked with the "layoutsubtree" -// attribute, then captured into a WebGL texture via texElementImage2D. +// attribute, then captured into a GPU texture (texElementImage2D on WebGL, +// copyElementImageToTexture on WebGPU). // // Fallback: when device.supportsHtmlTextures is false, a static 2D canvas with // hand-drawn placeholder graphics is used as the texture source instead. diff --git a/src/platform/graphics/webgpu/webgpu-graphics-device.js b/src/platform/graphics/webgpu/webgpu-graphics-device.js index 38148ac7266..0e6face1150 100644 --- a/src/platform/graphics/webgpu/webgpu-graphics-device.js +++ b/src/platform/graphics/webgpu/webgpu-graphics-device.js @@ -459,6 +459,9 @@ class WebgpuGraphicsDevice extends GraphicsDevice { */ this.wgpu = await this.gpuAdapter.requestDevice(deviceDescr); + // HTML-in-Canvas support (copyElementImageToTexture) + this.supportsHtmlTextures = typeof this.wgpu.queue?.copyElementImageToTexture === 'function'; + // handle lost device this.wgpu.lost?.then(this.handleDeviceLost.bind(this)); diff --git a/src/platform/graphics/webgpu/webgpu-texture.js b/src/platform/graphics/webgpu/webgpu-texture.js index 074348c00d6..2df7e01634f 100644 --- a/src/platform/graphics/webgpu/webgpu-texture.js +++ b/src/platform/graphics/webgpu/webgpu-texture.js @@ -436,7 +436,13 @@ class WebgpuTexture { } else { // 2d texture - if (this.isExternalImage(mipObject)) { + if (device._isHTMLElementInterface(mipObject) && device.supportsHtmlTextures) { + + // generic HTML element via the HTML-in-Canvas API + this.uploadElementImage(device, mipObject, mipLevel, 0); + anyUploads = true; + + } else if (this.isExternalImage(mipObject)) { this.uploadExternalImage(device, mipObject, mipLevel, 0); anyUploads = true; @@ -516,6 +522,28 @@ class WebgpuTexture { device.wgpu.queue.copyExternalImageToTexture(src, dst, copySize); } + // upload a generic HTML element via the HTML-in-Canvas API (copyElementImageToTexture) + uploadElementImage(device, element, mipLevel, index) { + + Debug.assert(mipLevel < this.desc.mipLevelCount, `Accessing mip level ${mipLevel} of texture with ${this.desc.mipLevelCount} mip levels`, this); + + const dst = { + texture: this.gpuTexture, + mipLevel: mipLevel, + origin: [0, 0, index], + aspect: 'all', // can be: "all", "stencil-only", "depth-only" + premultipliedAlpha: this.texture._premultiplyAlpha + }; + + // submit existing scheduled commands to the queue before copying to preserve the order + device.submit(); + + Debug.trace(TRACEID_RENDER_QUEUE, `ELEMENT-TO-TEX: mip:${mipLevel} index:${index} ${this.texture.name}`); + + // scale the element's rendered image to the texture dimensions + device.wgpu.queue.copyElementImageToTexture(element, this.desc.size.width, this.desc.size.height, dst); + } + uploadTypedArrayData(device, data, mipLevel, index) { const texture = this.texture; From d2368fe9d91ded8178a6e59a4d88218443058d73 Mon Sep 17 00:00:00 2001 From: Martin Valigursky Date: Fri, 29 May 2026 16:23:30 +0100 Subject: [PATCH 2/2] update --- src/platform/graphics/webgpu/webgpu-texture.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/platform/graphics/webgpu/webgpu-texture.js b/src/platform/graphics/webgpu/webgpu-texture.js index 2df7e01634f..ca04eaec626 100644 --- a/src/platform/graphics/webgpu/webgpu-texture.js +++ b/src/platform/graphics/webgpu/webgpu-texture.js @@ -535,13 +535,17 @@ class WebgpuTexture { premultipliedAlpha: this.texture._premultiplyAlpha }; + // texture dimensions at the specified mip level + const width = TextureUtils.calcLevelDimension(this.texture.width, mipLevel); + const height = TextureUtils.calcLevelDimension(this.texture.height, mipLevel); + // submit existing scheduled commands to the queue before copying to preserve the order device.submit(); Debug.trace(TRACEID_RENDER_QUEUE, `ELEMENT-TO-TEX: mip:${mipLevel} index:${index} ${this.texture.name}`); - // scale the element's rendered image to the texture dimensions - device.wgpu.queue.copyElementImageToTexture(element, this.desc.size.width, this.desc.size.height, dst); + // scale the element's rendered image to the mip level's dimensions + device.wgpu.queue.copyElementImageToTexture(element, width, height, dst); } uploadTypedArrayData(device, data, mipLevel, index) {