Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/files.json
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,7 @@
"webgpu_morphtargets",
"webgpu_morphtargets_face",
"webgpu_mrt",
"webgpu_multiple_canvas",
"webgpu_multiple_elements",
"webgpu_mrt_mask",
"webgpu_multiple_rendertargets",
Expand Down
Binary file added examples/screenshots/webgpu_multiple_canvas.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
200 changes: 200 additions & 0 deletions examples/webgpu_multiple_canvas.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>three.js webgpu - multiple canvas</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>

* {
box-sizing: border-box;
-moz-box-sizing: border-box;
}

body {
background-color: #fff;
color: #444;
}

a {
color: #08f;
}

#content {
position: absolute;
top: 0; width: 100%;
z-index: 1;
padding: 3em 0 0 0;
}

.list-item {
display: inline-block;
margin: 1em;
padding: 1em;
box-shadow: 1px 2px 4px 0px rgba(0,0,0,0.25);
}

.list-item > canvas:nth-child(1) {
width: 200px;
height: 200px;
}

.list-item > div:nth-child(2) {
color: #888;
font-family: sans-serif;
font-size: large;
width: 200px;
margin-top: 0.5em;
}

</style>
</head>
<body>

<div id="content">
<div id="info"><a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> - multiple canvas</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 { color } from 'three/tsl';

import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

import WebGPU from 'three/addons/capabilities/WebGPU.js';

//

if ( WebGPU.isAvailable() === false ) {

document.body.appendChild( WebGPU.getErrorMessage() );

throw new Error( 'No WebGPU support' );

}

//

let renderer;

const scenes = [];

init();

function init() {

const geometries = [
new THREE.BoxGeometry( 1, 1, 1 ),
new THREE.SphereGeometry( 0.5, 12, 8 ),
new THREE.DodecahedronGeometry( 0.5 ),
new THREE.CylinderGeometry( 0.5, 0.5, 1, 12 )
];

const content = document.getElementById( 'content' );

for ( let i = 0; i < 40; i ++ ) {

const scene = new THREE.Scene();
scene.backgroundNode = color( 0xeeeeee );

// make a list item
const element = document.createElement( 'div' );
element.className = 'list-item';

const sceneCanvas = document.createElement( 'canvas' );
sceneCanvas.width = Math.floor( 200 * window.devicePixelRatio );
sceneCanvas.height = Math.floor( 200 * window.devicePixelRatio );
element.appendChild( sceneCanvas );

const descriptionElement = document.createElement( 'div' );
descriptionElement.innerText = 'Scene ' + ( i + 1 );
element.appendChild( descriptionElement );

// the element that represents the area we want to render the scene
scene.userData.canvasRenderTarget = new THREE.CanvasRenderTarget( sceneCanvas );
content.appendChild( element );

const camera = new THREE.PerspectiveCamera( 50, 1, 1, 10 );
camera.position.z = 2;
scene.userData.camera = camera;

const controls = new OrbitControls( scene.userData.camera, scene.userData.canvasRenderTarget.canvas );
controls.minDistance = 2;
controls.maxDistance = 5;
controls.enablePan = false;
controls.enableZoom = false;
scene.userData.controls = controls;

// add one random mesh to each scene
const geometry = geometries[ geometries.length * Math.random() | 0 ];

const material = new THREE.MeshStandardMaterial( {

color: new THREE.Color().setHSL( Math.random(), 1, 0.75, THREE.SRGBColorSpace ),
roughness: 0.5,
metalness: 0,
flatShading: true

} );

scene.add( new THREE.Mesh( geometry, material ) );

scene.add( new THREE.HemisphereLight( 0xaaaaaa, 0x444444, 3 ) );

const light = new THREE.DirectionalLight( 0xffffff, 1.5 );
light.position.set( 1, 1, 1 );
scene.add( light );

scenes.push( scene );

}

renderer = new THREE.WebGPURenderer( { antialias: true } );
renderer.setClearColor( 0xffffff, 1 );
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setAnimationLoop( animate );

}

function animate() {

scenes.forEach( function ( scene ) {

// so something moves
//scene.children[ 0 ].rotation.y = Date.now() * 0.001;

// get the canvas render target
const canvasRenderTarget = scene.userData.canvasRenderTarget;

// get the camera
const camera = scene.userData.camera;

//camera.aspect = width / height; // not changing in this example
//camera.updateProjectionMatrix();

//scene.userData.controls.update();

renderer.setOutputRenderTarget( canvasRenderTarget );
renderer.render( scene, camera );

} );

}

</script>

</body>
</html>
1 change: 1 addition & 0 deletions src/Three.WebGPU.Nodes.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export { default as NodeObjectLoader } from './loaders/nodes/NodeObjectLoader.js
export { default as NodeMaterialLoader } from './loaders/nodes/NodeMaterialLoader.js';
export { default as InspectorBase } from './renderers/common/InspectorBase.js';
export { ClippingGroup } from './objects/ClippingGroup.js';
export { CanvasRenderTarget } from './core/CanvasRenderTarget.js';
export * from './nodes/Nodes.js';
import * as TSL from './nodes/TSL.js';
export { TSL };
1 change: 1 addition & 0 deletions src/Three.WebGPU.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export { default as NodeObjectLoader } from './loaders/nodes/NodeObjectLoader.js
export { default as NodeMaterialLoader } from './loaders/nodes/NodeMaterialLoader.js';
export { default as InspectorBase } from './renderers/common/InspectorBase.js';
export { ClippingGroup } from './objects/ClippingGroup.js';
export { CanvasRenderTarget } from './core/CanvasRenderTarget.js';
export * from './nodes/Nodes.js';
import * as TSL from './nodes/TSL.js';
export { TSL };
46 changes: 46 additions & 0 deletions src/core/CanvasRenderTarget.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { RenderTarget } from './RenderTarget.js';
import { REVISION } from '../constants.js';

/**
* CanvasRenderTarget is a specialized RenderTarget for rendering to a Canvas element.
*
* @augments RenderTarget
*/
class CanvasRenderTarget extends RenderTarget {

/**
* @param {HTMLCanvasElement|OffscreenCanvas} canvas - The canvas to use as the render target.
* @param {Object} [options={}] - Optional parameters for the render target.
*/
constructor( canvas, options = {} ) {

super( canvas.width, canvas.height, options );

// OffscreenCanvas does not have setAttribute, see #22811
if ( 'setAttribute' in canvas ) canvas.setAttribute( 'data-engine', `three.js r${ REVISION } webgpu` );

this.canvas = canvas;

this.isCanvasRenderTarget = true;

}

/**
* Sets the size of the canvas.
*
* @param {number} width - The new width of the canvas.
* @param {number} height - The new height of the canvas.
* @returns {CanvasRenderTarget} The updated render target.
*/
setSize( width, height ) {

this.canvas.width = width;
this.canvas.height = height;

return super.setSize( width, height );

}

}

export { CanvasRenderTarget };
2 changes: 1 addition & 1 deletion src/renderers/common/Backend.js
Original file line number Diff line number Diff line change
Expand Up @@ -638,7 +638,7 @@ class Backend {
domElement = ( this.parameters.canvas !== undefined ) ? this.parameters.canvas : createCanvasElement();

// OffscreenCanvas does not have setAttribute, see #22811
if ( 'setAttribute' in domElement ) domElement.setAttribute( 'data-engine', `three.js r${REVISION} webgpu` );
if ( 'setAttribute' in domElement ) domElement.setAttribute( 'data-engine', `three.js r${ REVISION } webgpu` );

this.domElement = domElement;

Expand Down
11 changes: 10 additions & 1 deletion src/renderers/common/Renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -1305,6 +1305,7 @@ class Renderer {

frameBufferTarget.depthBuffer = depth;
frameBufferTarget.stencilBuffer = stencil;

if ( outputRenderTarget !== null ) {

frameBufferTarget.setSize( outputRenderTarget.width, outputRenderTarget.height, outputRenderTarget.depth );
Expand Down Expand Up @@ -1352,6 +1353,7 @@ class Renderer {
const previousRenderId = nodeFrame.renderId;
const previousRenderContext = this._currentRenderContext;
const previousRenderObjectFunction = this._currentRenderObjectFunction;
const previousRenderTarget = this._renderTarget;

//

Expand All @@ -1376,6 +1378,12 @@ class Renderer {

renderTarget = outputRenderTarget;

if ( outputRenderTarget && outputRenderTarget.isCanvasRenderTarget ) {

outputRenderTarget.samples = this.samples;

}

}

//
Expand Down Expand Up @@ -1590,6 +1598,8 @@ class Renderer {

this._renderOutput( renderTarget );

this.setRenderTarget( previousRenderTarget, activeCubeFace, activeMipmapLevel );
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mind explaining why you have added this line?

We should check if this addition does not break XR.

Copy link
Collaborator Author

@sunag sunag Aug 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After setting setRenderTarget() as outputRenderTarget it does not restore the previous state, making the current getRenderTarget() the same as getOutputRenderTarget() after the render, thus being a bug.

Copy link
Collaborator

@Mugen87 Mugen87 Aug 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I try to make a test with a Quest 2 tomorrorw. Just to be on the safe side since r180 is released next week.


}

//
Expand Down Expand Up @@ -1645,7 +1655,6 @@ class Renderer {
this.autoClear = currentAutoClear;
this.xr.enabled = currentXR;


}

/**
Expand Down
Loading
Loading