Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Threejs canvas size based on container

How can I calculate canvas size based on its container? To avoid scrolling.

If I set the size based on window the canvas is too big.

like image 955
Francesco1984 Avatar asked Apr 26 '15 22:04

Francesco1984


People also ask

What is the default canvas size?

By default, both the canvas element's size and the size of its drawing surface is 300 screen pixels wide and 150 screen pixels high.

What is a scene in three Js?

Scenes allow you to set up what and where is to be rendered by three. js. This is where you place objects, lights and cameras.


2 Answers

Arguably the best way to resize three.js use to code it so it just accepts whatever size the canvas is as set by CSS. That way, no matter how you use the canvas your code will work, no need to change it for different situations.

First off when setting the initial aspect ratio there's no reason to set it because we're going to set it in response to the size of the canvas being different so it's just a waste of code to set it twice

// There's no reason to set the aspect here because we're going // to set it every frame anyway so we'll set it to 2 since 2 // is the the aspect for the canvas default size (300w/150h = 2) const camera = new THREE.PerspectiveCamera(70, 2, 1, 1000); 

Then we need some code that will resize the canvas to match its display size

function resizeCanvasToDisplaySize() {   const canvas = renderer.domElement;   // look up the size the canvas is being displayed   const width = canvas.clientWidth;   const height = canvas.clientHeight;    // adjust displayBuffer size to match   if (canvas.width !== width || canvas.height !== height) {     // you must pass false here or three.js sadly fights the browser     renderer.setSize(width, height, false);     camera.aspect = width / height;     camera.updateProjectionMatrix();      // update any render target sizes here   } } 

Call this in your render loop before rendering

function animate(time) {   time *= 0.001;  // seconds    resizeCanvasToDisplaySize();    mesh.rotation.x = time * 0.5;   mesh.rotation.y = time * 1;    renderer.render(scene, camera);   requestAnimationFrame(animate); }  requestAnimationFrame(animate); 

Here's 3 examples, the only difference between the 3 examples is the CSS and whether we make the canvas or three.js makes the canvas

Example 1: fullscreen, We make the canvas

"use strict";  const  renderer = new THREE.WebGLRenderer({canvas: document.querySelector("canvas")});  // There's no reason to set the aspect here because we're going // to set it every frame anyway so we'll set it to 2 since 2 // is the the aspect for the canvas default size (300w/150h = 2) const  camera = new THREE.PerspectiveCamera(70, 2, 1, 1000); camera.position.z = 400;  const scene = new THREE.Scene(); const geometry = new THREE.BoxGeometry(200, 200, 200); const material = new THREE.MeshPhongMaterial({   color: 0x555555,   specular: 0xffffff,   shininess: 50,   shading: THREE.SmoothShading });  const mesh = new THREE.Mesh(geometry, material); scene.add(mesh);  const light1 = new THREE.PointLight(0xff80C0, 2, 0); light1.position.set(200, 100, 300); scene.add(light1);  function resizeCanvasToDisplaySize() {   const canvas = renderer.domElement;   const width = canvas.clientWidth;   const height = canvas.clientHeight;   if (canvas.width !== width ||canvas.height !== height) {     // you must pass false here or three.js sadly fights the browser     renderer.setSize(width, height, false);     camera.aspect = width / height;     camera.updateProjectionMatrix();      // set render target sizes here   } }  function animate(time) {   time *= 0.001;  // seconds    resizeCanvasToDisplaySize();    mesh.rotation.x = time * 0.5;   mesh.rotation.y = time * 1;    renderer.render(scene, camera);   requestAnimationFrame(animate); }  requestAnimationFrame(animate);
body { margin: 0; } canvas { width: 100vw; height: 100vh; display: block; }
<canvas></canvas> <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/85/three.min.js"></script>

Example 2: fullscreen canvas, three.js makes the canvas

"use strict";  const renderer = new THREE.WebGLRenderer(); document.body.appendChild(renderer.domElement);  // There's no reason to set the aspect here because we're going // to set it every frame anyway so we'll set it to 2 since 2 // is the the aspect for the canvas default size (300w/150h = 2) const  camera = new THREE.PerspectiveCamera(70, 2, 1, 1000); camera.position.z = 400;  const scene = new THREE.Scene(); const geometry = new THREE.BoxGeometry(200, 200, 200); const material = new THREE.MeshPhongMaterial({   color: 0x555555,   specular: 0xffffff,   shininess: 50,   shading: THREE.SmoothShading });  const mesh = new THREE.Mesh(geometry, material); scene.add(mesh);  const light1 = new THREE.PointLight(0xff80C0, 2, 0); light1.position.set(200, 100, 300); scene.add(light1);  function resizeCanvasToDisplaySize() {   const canvas = renderer.domElement;   const width = canvas.clientWidth;   const height = canvas.clientHeight;   if (canvas.width !== width ||canvas.height !== height) {     // you must pass false here or three.js sadly fights the browser     renderer.setSize(width, height, false);     camera.aspect = width / height;     camera.updateProjectionMatrix();      // set render target sizes here   } }  function animate(time) {   time *= 0.001;  // seconds    resizeCanvasToDisplaySize();    mesh.rotation.x = time * 0.5;   mesh.rotation.y = time * 1;    renderer.render(scene, camera);   requestAnimationFrame(animate); }  requestAnimationFrame(animate);
body { margin: 0; } canvas { width: 100vw; height: 100vh; display: block; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/85/three.min.js"></script>

Example 3: inline canvas

"use strict";  const  renderer = new THREE.WebGLRenderer({canvas: document.querySelector(".diagram canvas")});  // There's no reason to set the aspect here because we're going // to set it every frame anyway so we'll set it to 2 since 2 // is the the aspect for the canvas default size (300w/150h = 2) const  camera = new THREE.PerspectiveCamera(70, 2, 1, 1000); camera.position.z = 400;  const scene = new THREE.Scene(); const geometry = new THREE.BoxGeometry(200, 200, 200); const material = new THREE.MeshPhongMaterial({   color: 0x555555,   specular: 0xffffff,   shininess: 50,   shading: THREE.SmoothShading });  const mesh = new THREE.Mesh(geometry, material); scene.add(mesh);  const light1 = new THREE.PointLight(0xff80C0, 2, 0); light1.position.set(200, 100, 300); scene.add(light1);  function resizeCanvasToDisplaySize() {   const canvas = renderer.domElement;   const width = canvas.clientWidth;   const height = canvas.clientHeight;   if (canvas.width !== width ||canvas.height !== height) {     // you must pass false here or three.js sadly fights the browser     renderer.setSize(width, height, false);     camera.aspect = width / height;     camera.updateProjectionMatrix();      // set render target sizes here   } }  function animate(time) {   time *= 0.001;  // seconds    resizeCanvasToDisplaySize();    mesh.rotation.x = time * 0.5;   mesh.rotation.y = time * 1;    renderer.render(scene, camera);   requestAnimationFrame(animate); }  requestAnimationFrame(animate);
body { font-size: x-large; } .diagram { width: 150px; height: 150px; float: left; margin: 1em; } canvas { width: 100%; height: 100%; }
<p> Pretend this is a diagram in a physics lesson and it's inline. Notice we didn't have to change the code to handle this case. <span class="diagram"><canvas></canvas></span> The same code that handles fullscreen handles this case as well. The only difference is the CSS and how we look up the canvas. Otherwise it just works. We didn't have to change the code because we cooperated with the browser instead of fighting it. </p> <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/85/three.min.js"></script>

Example 4: 50% width canvas (like a live editor)

"use strict";  const  renderer = new THREE.WebGLRenderer({canvas: document.querySelector("canvas")});  // There's no reason to set the aspect here because we're going // to set it every frame anyway so we'll set it to 2 since 2 // is the the aspect for the canvas default size (300w/150h = 2) const  camera = new THREE.PerspectiveCamera(70, 2, 1, 1000); camera.position.z = 400;  const scene = new THREE.Scene(); const geometry = new THREE.BoxGeometry(200, 200, 200); const material = new THREE.MeshPhongMaterial({   color: 0x555555,   specular: 0xffffff,   shininess: 50,   shading: THREE.SmoothShading });  const mesh = new THREE.Mesh(geometry, material); scene.add(mesh);  const light1 = new THREE.PointLight(0xff80C0, 2, 0); light1.position.set(200, 100, 300); scene.add(light1);  function resizeCanvasToDisplaySize() {   const canvas = renderer.domElement;   const width = canvas.clientWidth;   const height = canvas.clientHeight;   if (canvas.width !== width ||canvas.height !== height) {     // you must pass false here or three.js sadly fights the browser     renderer.setSize(width, height, false);     camera.aspect = width / height;     camera.updateProjectionMatrix();      // set render target sizes here   } }  function animate(time) {   time *= 0.001;  // seconds    resizeCanvasToDisplaySize();    mesh.rotation.x = time * 0.5;   mesh.rotation.y = time * 1;    renderer.render(scene, camera);   requestAnimationFrame(animate); }  requestAnimationFrame(animate);
html {   box-sizing: border-box; } *, *:before, *:after {   box-sizing: inherit; } body { margin: 0; } .outer { } .frame {    display: flex;   width: 100vw;   height: 100vh; } .frame>* {   flex: 1 1 50%; } #editor {   font-family: monospace;   padding: .5em;   background: #444;   color: white; } canvas {    width: 100%;   height: 100%; }
<div class="frame">   <div id="result">     <canvas></canvas>   </div>   <div id="editor">   explaintion of example on left or the code for it would go here   </div> </div> <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/85/three.min.js"></script>

notice window.innerWidth and window.innerHeight are never referenced in the code above and yet it works for all cases.


Update: 2021, there is no "best" way, there are just multiple ways with various tradeoffs.

Some people seem to be concerned about perf as if checking if 2 values don't match 2 other values has any measurable performance impact in any program that needs performance 🙄

In any case there's the older answer I posted above which just checks the size of the container. People have suggested using window.addEventListener('resize', ...) but failed to notice that does not handle the case where the canvas changes size but the window itself does not (example 4 above).

In any case, in 2021 we can use ResizeObserver instead of window.addEventListener('resize', ...) for those that believe checking 2 variables per frame is a perf overhead. ResizeObserver will fire when the canvas changes size which can happen, even when the window itself doesn't change size.

function resizeCanvasToDisplaySize() {   const canvas = renderer.domElement;   // look up the size the canvas is being displayed   const width = canvas.clientWidth;   const height = canvas.clientHeight;    // you must pass false here or three.js sadly fights the browser   renderer.setSize(width, height, false);   camera.aspect = width / height;   camera.updateProjectionMatrix();    // update any render target sizes here }  const resizeObserver = new ResizeObserver(resizeCanvasToDisplaySize); resizeObserver.observe(canvas, {box: 'content-box'}); 

Example 1: fullscreen, We make the canvas

body { margin: 0; } canvas { width: 100vw; height: 100vh; display: block; }
<canvas></canvas> <script type="module"> import * as THREE from 'https://threejsfundamentals.org/threejs/resources/threejs/r132/build/three.module.js';  const  renderer = new THREE.WebGLRenderer({canvas: document.querySelector("canvas")});  // There's no reason to set the aspect here because we're going // to set it every frame anyway so we'll set it to 2 since 2 // is the the aspect for the canvas default size (300w/150h = 2) const  camera = new THREE.PerspectiveCamera(70, 2, 1, 1000); camera.position.z = 400;  const scene = new THREE.Scene(); const geometry = new THREE.BoxGeometry(200, 200, 200); const material = new THREE.MeshPhongMaterial({   color: 0x555555,   specular: 0xffffff,   shininess: 50,   shading: THREE.SmoothShading });  const mesh = new THREE.Mesh(geometry, material); scene.add(mesh);  const light1 = new THREE.PointLight(0xff80C0, 2, 0); light1.position.set(200, 100, 300); scene.add(light1);  function resizeCanvasToDisplaySize() {   const canvas = renderer.domElement;   const width = canvas.clientWidth;   const height = canvas.clientHeight;    // you must pass false here or three.js sadly fights the browser   renderer.setSize(width, height, false);   camera.aspect = width / height;   camera.updateProjectionMatrix();    // set render target sizes here }  function animate(time) {   time *= 0.001;  // seconds    mesh.rotation.x = time * 0.5;   mesh.rotation.y = time * 1;    renderer.render(scene, camera);   requestAnimationFrame(animate); }  requestAnimationFrame(animate);  const resizeObserver = new ResizeObserver(resizeCanvasToDisplaySize); resizeObserver.observe(renderer.domElement, {box: 'content-box'}); </script>

Example 4: 50% width canvas (like a live editor)

html {   box-sizing: border-box; } *, *:before, *:after {   box-sizing: inherit; } body { margin: 0; } .outer { } .frame {    display: flex;   width: 100vw;   height: 100vh; } .frame>* {   flex: 1 1 50%; } #ui {   position: absolute;   right: 0;   bottom: 0;   padding: 0.5em; } #editor {   font-family: monospace;   padding: .5em;   background: #444;   color: white; } canvas {    width: 100%;   height: 100%; }
<div class="frame">   <div id="result">     <canvas></canvas>   </div>   <div id="editor">     <p>no window resize</p>     <p>For example level editor might have the level     displayed on the left and a control panel on the right.</p>     <p>Click the buttons below. The only thing changing is CSS. The window size is not changing so using <code>window.addEventListener('resize', ...)</code> would fail</p>     <div id="ui">       <button type="button" data-size="25%">small</button>       <button type="button" data-size="50%">medium</button>       <button type="button" data-size="75%">large</button>     </div>   </div> </div> <script type="module"> import * as THREE from 'https://threejsfundamentals.org/threejs/resources/threejs/r132/build/three.module.js';  const  renderer = new THREE.WebGLRenderer({canvas: document.querySelector("canvas")});  // There's no reason to set the aspect here because we're going // to set it every frame anyway so we'll set it to 2 since 2 // is the the aspect for the canvas default size (300w/150h = 2) const  camera = new THREE.PerspectiveCamera(70, 2, 1, 1000); camera.position.z = 400;  const scene = new THREE.Scene(); const geometry = new THREE.BoxGeometry(200, 200, 200); const material = new THREE.MeshPhongMaterial({   color: 0x555555,   specular: 0xffffff,   shininess: 50,   shading: THREE.SmoothShading });  const mesh = new THREE.Mesh(geometry, material); scene.add(mesh);  const light1 = new THREE.PointLight(0xff80C0, 2, 0); light1.position.set(200, 100, 300); scene.add(light1);  function resizeCanvasToDisplaySize() {   const canvas = renderer.domElement;   const width = canvas.clientWidth;   const height = canvas.clientHeight;    // you must pass false here or three.js sadly fights the browser   renderer.setSize(width, height, false);   camera.aspect = width / height;   camera.updateProjectionMatrix();    // set render target sizes here }  function animate(time) {   time *= 0.001;  // seconds    mesh.rotation.x = time * 0.5;   mesh.rotation.y = time * 1;    renderer.render(scene, camera);   requestAnimationFrame(animate); }  requestAnimationFrame(animate);  const editorElem = document.querySelector("#editor"); document.querySelectorAll('button').forEach(elem => {   elem.addEventListener('click', () => {     editorElem.style.flexBasis = elem.dataset.size;   }); });   const resizeObserver = new ResizeObserver(resizeCanvasToDisplaySize); resizeObserver.observe(renderer.domElement, {box: 'content-box'}); </script>

Why not use ResizeObserver?

  1. it's not available in old browsers (like IE) if you still need to support it

  2. it's not guaranteed to update as quickly.

    In other words with rAF and checking every frame, ideally your content will stay the correct size at all times since the browser is asking you to prepare your content. Conversely, ResizeObserver just says it will eventually give you an event. That might come 5-10 frames after your content actually needed to be resized.

On more thing is handling devicePixelRatio. That is too big a topic for this answer but you can see more here

like image 188
gman Avatar answered Sep 21 '22 02:09

gman


Well,that's not difficult.Set your render's size will work.

container = document.getElementById('container'); renderer.setSize($(container).width(), $(container).height()); container.appendChild(renderer.domElement); 
like image 37
aboutqx Avatar answered Sep 23 '22 02:09

aboutqx