· Create the world is committed to creating the first “cloud CAD” collaborative design platform integrating viewing, modeling, assembly and rendering in China.
At the request of readers, we hope to set up a professional WEBGL and Threejs industry QQ communication group for front-end developers in Chengdu-Chongqing area to facilitate discussion. There are webGL and Threejs in the group, welcome to join! — Click the link to join the group chat [three.js/ webGL Chongqing Union Group] : jq.qq.com/?_wv=1027&k…
One of the most common problems in three.js is rendering multiple scenes. For example, when you want to create a commercial website consisting of multiple 3D images, the easy solution is to create a Canvas for each 3D image and add a Renderer for each Canvas.
But then you run into two obvious problems:
- The browser limits the WebGL context
(WebGL contexts)
The number of.Typically, browsers limit this to eight, and once this number is exceeded, the WebGL context that was created in the first place is automatically deprecated.
- There is no way to share resources in different WebGL contexts.
No resources can be shared between different WebGL contexts, which means that if you want to load a 10Mb model on each Canvas, and each model has a 20Mb texture, the model and texture will be loaded twice. Therefore, initialization, shader compilation, and so on will all be run twice, and the situation will get worse as the number of canvases increases or decreases.
So how do we solve this problem?
Basic method
The solution is to fill the entire viewport background with a single Canvas and use some other elements to represent each virtual Canvas, i.e. load only one Renderer on one Canvas and create a Scene for each virtual Canvas. We just need to make sure that each virtual Canvas is correctly positioned, and three.js will render them in their respective positions on the screen.
In this way, since we only added one Canvas, so only one WebGL context, we solved the resource sharing problem and did not cause the limitation of the number of WebGL contexts.
Take a simple demo with just two scenes. First, create the HTML structure:
<canvas id="c"></canvas>
<p>
<span id="box" class="diagram left"></span>
I love boxes. Presents come in boxes.
When I find a new box I'm always excited to find out what's inside.
</p>
<p>
<span id="pyramid" class="diagram right"></span>
When I was a kid I dreamed of going on an expedition inside a pyramid
and finding a undiscovered tomb full of mummies and treasure.
</p>
Copy the code
Then set some basic styles for it:
#c {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
display: block;
z-index: -1;
}
.diagram {
display: inline-block;
width: 5em;
height: 3em;
border: 1px solid black;
}
.left {
float: left;
margin-right:.25em;
}
.right {
float: right;
margin-left:.25em;
}
Copy the code
We set the Canvas Canvas to fill the screen and set its Z-index to -1 so that it is always behind the other elements. Of course, we need to set the virtual Canvas to the appropriate width and height, since there is no content to support it at this point.
Now create two scenes, one with a cube and the other with a diamond, and add a Light and a Camera to each Scene.
function makeScene(elem) {
const scene = new THREE.Scene();
const fov = 45;
const aspect = 2; // the canvas default
const near = 0.1;
const far = 5;
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
camera.position.z = 2;
camera.position.set(0.1.2);
camera.lookAt(0.0.0);
{
const color = 0xFFFFFF;
const intensity = 1;
const light = new THREE.DirectionalLight(color, intensity);
light.position.set(-1.2.4);
scene.add(light);
}
return {scene, camera, elem};
}
function setupScene1() {
const sceneInfo = makeScene(document.querySelector('#box'));
const geometry = new THREE.BoxGeometry(1.1.1);
const material = new THREE.MeshPhongMaterial({color: 'red'});
const mesh = new THREE.Mesh(geometry, material);
sceneInfo.scene.add(mesh);
sceneInfo.mesh = mesh;
return sceneInfo;
}
function setupScene2() {
const sceneInfo = makeScene(document.querySelector('#pyramid'));
const radius = 8.;
const widthSegments = 4;
const heightSegments = 2;
const geometry = new THREE.SphereGeometry(radius, widthSegments, heightSegments);
const material = new THREE.MeshPhongMaterial({
color: 'blue'.flatShading: true});const mesh = new THREE.Mesh(geometry, material);
sceneInfo.scene.add(mesh);
sceneInfo.mesh = mesh;
return sceneInfo;
}
const sceneInfo1 = setupScene1();
const sceneInfo2 = setupScene2();
Copy the code
RenderSceneInfo () and render() are used to render the Scene where the virtual Canvas element appears in the viewable area. By calling the clipping area of Three. js to check the renderer.setscissortest method, Three. js can render only part of the canvas, and at the same time, We need to call renderer.setViewPort and renderer.setscissor to set the viewport size and clipping area, respectively.
The parameters are described as follows:
Renderer.setScissorTest( boolean : Boolean ) : null;
// Enable or disable clipping detection. When enabled, only pixels within the clipping region defined are affected by subsequent renderers.
Renderer.setScissor ( x : Integer, y : Integer, width : Integer, height : Integer ) : null;
// Set the clipping area to (x, y) to (x + width, y + height)
Renderer.### [setViewport]() ( x : Integer, y : Integer, width : Integer, height : Integer ) : null
// Set the viewport size from (x, y) to (x + width, y + height).
Copy the code
View information acquisition functions are as follows:
function renderSceneInfo(sceneInfo) {
const {scene, camera, elem} = sceneInfo;
// get the viewport relative position of this element
const {left, right, top, bottom, width, height} =
elem.getBoundingClientRect();
const isOffscreen =
bottom < 0 ||
top > renderer.domElement.clientHeight ||
right < 0 ||
left > renderer.domElement.clientWidth;
if (isOffscreen) {
return;
}
camera.aspect = width / height;
camera.updateProjectionMatrix();
const positiveYUpBottom = canvasRect.height - bottom;
renderer.setScissor(left, positiveYUpBottom, width, height);
renderer.setViewport(left, positiveYUpBottom, width, height);
renderer.render(scene, camera);
}
Copy the code
The view rendering function is as follows:
function render(time) {
time *= 0.001;
resizeRendererToDisplaySize(renderer);
renderer.setScissorTest(false);
renderer.clear(true.true);
renderer.setScissorTest(true);
sceneInfo1.mesh.rotation.y = time * 1.;
sceneInfo2.mesh.rotation.y = time * 1.;
renderSceneInfo(sceneInfo1);
renderSceneInfo(sceneInfo2);
requestAnimationFrame(render);
}
Copy the code
The final effect is as follows:
Click on the new TAB to see the effect
As you can see, the two objects are rendered in their respective positions.
Synchronous rolling
Although we have realized the function of the render multiple scenarios at the same time, but the above code remains a problem, if the Scene is too complex, or for other reasons need longer time to render, then the position of the Scene rendering in the canvas is always lag behind the other elements of the page, such as the page scrolling occurs obvious lag.
In order to observe this phenomenon more intuitively, we put a border on each Scene and set the background color:
.diagram {
display: inline-block;
width: 5em;
height: 3em;
border: 1px solid black;
}
Copy the code
const scene = new THREE.Scene();
scene.background = new THREE.Color('red');
Copy the code
At this point, we quickly scroll the screen to see the problem. The scrolling animation slowed down by a factor of ten looks like this:
To solve this problem, first change the positioning mode of Canvas from position: fixed to position: Absolute.
#c {
- position: fixed;
+ position: absolute;
Copy the code
Next, we use the Transform property to move the canvas so that the top of the canvas is always aligned with the top of the page.
function render(time) {...const transform = `translateY(The ${window.scrollY}px)`;
renderer.domElement.style.transform = transform;
Copy the code
Position: Fixed disables scrolling of the canvas completely, regardless of whether other elements have been rolled over it; Position: Absolute keeps the canvas scrolling with the rest of the page, meaning that anything we draw will scroll with the page, even if it’s not completely rendered. When the scene is rendered and the canvas is moved, the scene will match the scrolling position of the page and be re-rendered, which means that only the edges of the window will show some unrendered data, not the scene on the page at the time. Below is the effect of using the above method (the animation is also slowed down by 10 times).
encapsulation
Now that we have the ability to render multiple scenes in one Canvas, let’s make it a little more usable.
We can encapsulate a main render function to manage the entire Canvas and define a list of scene elements and their corresponding scene initializer functions. For each element, it checks to see if the element has been scrolled to the visible area and calls the appropriate scene initialization function. Thus we build a rendering system in which each individual scene is rendered independently in their own defined space without affecting each other.
The main render function is as follows:
const sceneElements = [];
function addScene(elem, fn) {
sceneElements.push({elem, fn});
}
function render(time) {
time *= 0.001;
resizeRendererToDisplaySize(renderer);
renderer.setScissorTest(false);
renderer.setClearColor(clearColor, 0);
renderer.clear(true.true);
renderer.setScissorTest(true);
const transform = `translateY(The ${window.scrollY}px)`;
renderer.domElement.style.transform = transform;
for (const {elem, fn} of sceneElements) {
// get the viewport relative position of this element
const rect = elem.getBoundingClientRect();
const {left, right, top, bottom, width, height} = rect;
const isOffscreen =
bottom < 0 ||
top > renderer.domElement.clientHeight ||
right < 0 ||
left > renderer.domElement.clientWidth;
if(! isOffscreen) {const positiveYUpBottom = renderer.domElement.clientHeight - bottom;
renderer.setScissor(left, positiveYUpBottom, width, height);
renderer.setViewport(left, positiveYUpBottom, width, height);
fn(time, rect);
}
}
requestAnimationFrame(render);
}
Copy the code
As you can see, this function iterates through each array object containing all of the Scene elements, each with its own ELEm and FN attributes.
This function checks if each Scene element is in the viewable area, and when it is, calls its Scene initialization function, passing it the current time and the corresponding size and location.
Now add the information for each Scene to the arraylist:
{
const elem = document.querySelector('#box');
const {scene, camera} = makeScene();
const geometry = new THREE.BoxGeometry(1.1.1);
const material = new THREE.MeshPhongMaterial({color: 'red'});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
addScene(elem, (time, rect) = > {
camera.aspect = rect.width / rect.height;
camera.updateProjectionMatrix();
mesh.rotation.y = time * 1.;
renderer.render(scene, camera);
});
}
{
const elem = document.querySelector('#pyramid');
const {scene, camera} = makeScene();
const radius = 8.;
const widthSegments = 4;
const heightSegments = 2;
const geometry = new THREE.SphereGeometry(radius, widthSegments, heightSegments);
const material = new THREE.MeshPhongMaterial({
color: 'blue'.flatShading: true});const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
addScene(elem, (time, rect) = > {
camera.aspect = rect.width / rect.height;
camera.updateProjectionMatrix();
mesh.rotation.y = time * 1.;
renderer.render(scene, camera);
});
}
Copy the code
At this point, we no longer need to define sceneInfo1 and sceneInfo2 respectively, but the scene initialization functions for each scene are in effect.
Click on the new TAB to see the effect
Using HTML Dataset
The final step is to use the HTML dataset, which is a way to add your own data to an HTML element. Instead of using id=”…” Instead, use data-diagram=”…” , like this:
<canvas id="c"></canvas>
<p>
<! --<span id="box" class="diagram left"></span>-->
<span data-diagram="box" class="left"></span>
I love boxes. Presents come in boxes.
When I find a new box I'm always excited to find out what's inside.
</p>
<p>
<! --<span id="pyramid" class="diagram left"></span>-->
<span data-diagram="pyramid" class="right"></span>
When I was a kid I dreamed of going on an expedition inside a pyramid
and finding a undiscovered tomb full of mummies and treasure.
</p>
Copy the code
Also modify the CSS selector:
/* .diagram */
*[data-diagram] {
display: inline-block;
width: 5em;
height: 3em;
}
Copy the code
Now we build an object that maps the scene initialization function for each scene and returns a scene rendering function.
const sceneInitFunctionsByName = {
'box': () = > {
const {scene, camera} = makeScene();
const geometry = new THREE.BoxGeometry(1.1.1);
const material = new THREE.MeshPhongMaterial({color: 'red'});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
return (time, rect) = > {
mesh.rotation.y = time * 1.;
camera.aspect = rect.width / rect.height;
camera.updateProjectionMatrix();
renderer.render(scene, camera);
};
},
'pyramid': () = > {
const {scene, camera} = makeScene();
const radius = 8.;
const widthSegments = 4;
const heightSegments = 2;
const geometry = new THREE.SphereGeometry(radius, widthSegments, heightSegments);
const material = new THREE.MeshPhongMaterial({
color: 'blue'.flatShading: true});const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
return (time, rect) = > {
mesh.rotation.y = time * 1.; camera.aspect = rect.width / rect.height; camera.updateProjectionMatrix(); renderer.render(scene, camera); }; }};Copy the code
We also need to get all the diagrams and call the initialization function.
document.querySelectorAll('[data-diagram]').forEach((elem) = > {
const sceneName = elem.dataset.diagram;
const sceneInitFunction = sceneInitFunctionsByName[sceneName];
const sceneRenderFunction = sceneInitFunction(elem);
addScene(elem, sceneRenderFunction);
});
Copy the code
The rendering of the page remains the same, but the code is more generic.
interaction
When interaction is required, we need to add interactive controls, such as TrackballControls, for each scene individually. First, you need to introduce the control.
import {TrackballControls} from './resources/threejs/r132/examples/jsm/controls/TrackballControls.js';
Copy the code
Next, add the control:
// function makeScene() {
function makeScene(elem) {
const scene = new THREE.Scene();
const fov = 45;
const aspect = 2; // the canvas default
const near = 0.1;
const far = 5;
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
camera.position.set(0.1.2);
camera.lookAt(0.0.0);
scene.add(camera);
const controls = new TrackballControls(camera, elem);
controls.noZoom = true;
controls.noPan = true;
{
const color = 0xFFFFFF;
const intensity = 1;
const light = new THREE.DirectionalLight(color, intensity);
light.position.set(-1.2.4);
// scene.add(light);
camera.add(light);
}
// return {scene, camera};
return {scene, camera, controls};
}
Copy the code
As you can see, we add the camera to the scene and the light to the camera so that the light is always associated with the camera. Therefore, when we rotate the camera’s perspective through the controller, the light will always illuminate that perspective.
We also need to update these controls in the render function:
const sceneInitFunctionsByName = {
// 'box': () => {
// const {scene, camera} = makeScene();
'box': (elem) = > {
const {scene, camera, controls} = makeScene(elem);
const geometry = new THREE.BoxGeometry(1.1.1);
const material = new THREE.MeshPhongMaterial({color: 'red'});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
return (time, rect) = > {
mesh.rotation.y = time * 1.;
camera.aspect = rect.width / rect.height;
camera.updateProjectionMatrix();
controls.handleResize();
controls.update();
renderer.render(scene, camera);
};
},
// 'pyramid': () => {
// const {scene, camera} = makeScene();
'pyramid': (elem) = > {
const {scene, camera, controls} = makeScene(elem);
const radius = 8.;
const widthSegments = 4;
const heightSegments = 2;
const geometry = new THREE.SphereGeometry(radius, widthSegments, heightSegments);
const material = new THREE.MeshPhongMaterial({
color: 'blue'.flatShading: true});const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
return (time, rect) = > {
mesh.rotation.y = time * 1.; camera.aspect = rect.width / rect.height; camera.updateProjectionMatrix(); controls.handleResize(); controls.update(); renderer.render(scene, camera); }; }};Copy the code
Now, the controller is in effect;
Click on the new TAB to see the effect
The method mentioned above can be found in many examples on this website, such as Three. Js primitives and Three. Js textures.
Another way
Another way to achieve this effect is by rendering to an off-screen canvas and copying the result to the corresponding 2D canvas. The advantage of this approach is that there are no restrictions on how to combine each individual area, so you can just write normal HTML. The first method requires a Canvas in the background.
The downside of this approach, however, is that it is slow because every region has to be replicated, so it depends on the browser and GPU performance.
This approach requires very little code change.
First, we no longer need the Canvas element in HTML:
<body>
-
. </body>Copy the code
The canvas style also needs to be changed:
- #c {
- position: absolute;
- left: 0;
- top: 0;
- width: 100%;
- height: 100%;
- display: block;
- z-index: -1;
-}
+ canvas {
+ width: 100%;
+ height: 100%;
+ display: block;
+}
[data-diagram] {
display: inline-block;
width: 5em;
height: 3em;
}
Copy the code
This ensures that all canvases fill their containers.
Instead of looking for the Canvas element, you need to create one and start with visual range detection enabled:
function main() {
- const canvas = document.querySelector('#c');
+ const canvas = document.createElement('canvas');
+ const renderer = new THREE.WebGLRenderer({canvas, alpha: true});
+ renderer.setScissorTest(true);.Copy the code
Then, for each scene, we create a 2d render context and add its canvas to the scene’s corresponding element:
const sceneElements = [];
function addScene(elem, fn) {
+ const ctx = document.createElement('canvas').getContext('2d');
+ elem.appendChild(ctx.canvas);
- sceneElements.push({elem, fn});
+ sceneElements.push({elem, ctx, fn});
}
Copy the code
During rendering, if the renderer’s canvas is not large enough to render in this area, increase its size; If the canvas in this area is the wrong size, change its size. Finally, set the clipping area and viewport size, render the scene for that area, and copy the result onto the canvas for that area.
Function render(time) {time *= 0.001;- resizeRendererToDisplaySize(renderer);
- renderer.setScissorTest(false);
- renderer.setClearColor(clearColor, 0);
- renderer.clear(true, true);
- renderer.setScissorTest(true);
- const transform = `translateY(${window.scrollY}px)`;
- renderer.domElement.style.transform = transform;
- for (const {elem, fn} of sceneElements) {
+ for (const {elem, fn, ctx} of sceneElements) {
// get the viewport relative position of this element
const rect = elem.getBoundingClientRect();
const {left, right, top, bottom, width, height} = rect;
+ const rendererCanvas = renderer.domElement;
const isOffscreen =
bottom < 0 ||
- top > renderer.domElement.clientHeight ||
+ top > window.innerHeight ||
right < 0 ||
- left > renderer.domElement.clientWidth;
+ left > window.innerWidth;if (! isOffscreen) {- const positiveYUpBottom = renderer.domElement.clientHeight - bottom;
- renderer.setScissor(left, positiveYUpBottom, width, height);
- renderer.setViewport(left, positiveYUpBottom, width, height);
+ // make sure the renderer's canvas is big enough
+ if (rendererCanvas.width < width || rendererCanvas.height < height) {
+ renderer.setSize(width, height, false);
+}
+
+ // make sure the canvas for this area is the same size as the area
+ if (ctx.canvas.width !== width || ctx.canvas.height !== height) {
+ ctx.canvas.width = width;
+ ctx.canvas.height = height;
+}
+
+ renderer.setScissor(0, 0, width, height);
+ renderer.setViewport(0, 0, width, height);
- fn(time, rect);
+ // copy the rendered scene to this element's canvas
+ ctx.globalCompositeOperation = 'copy';
+ ctx.drawImage(
+ rendererCanvas,
+ 0, rendererCanvas.height - height, width, height, // src rect
+ 0, 0, width, height); // dst rect
}
}
requestAnimationFrame(render);
}
Copy the code
The end result is the same as method 1:
Click on the new TAB to see the effect
Update method
There is also a way to use the Jobs Video vas method, but as of July 2020, this method is only supported by Chrome, and interested users can click through to see the documentation.
Three. Js Multiple Canvases Multiple Scenes