Threejs canvas size based on container

前端 未结 6 422
借酒劲吻你
借酒劲吻你 2021-02-01 20:58

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.

相关标签:
6条回答
  • 2021-02-01 21:07

    Of course the answer provided by @gman is the full-fledged answer. However, to recap the possible scenarios, one has to assume the two possibilities, which is the reason why the answers differ drastically.

    The first possibility is when the container is the global(head) object: window, or maybe <body> or <frameset>, in which case, the most convenient method would be using window.addEventListener('resize', onResize()) { ... }.

    The second possibility, is the one which has been originally asked: when the container of the Three.js WebGL output should be responsive. In that case, whether the container is canvas, a section or a div, the best way to handle the resizing is to target the container in DOM, and use the renderer.setsize() method by passing it the container's clientWidth and clientHeight, and then calling onResize() function in the render loop function, which its complete implementation is aforementioned by @gman. In this particular case, there is no need to use addEventListener.

    0 讨论(0)
  • 2021-02-01 21:08

    Here's the code that allowed me to get a handle on my canvas rendering size:

    First, the scene is rendered inside a <div id="your_scene"> which has the dimensions of your choice:

    <div class="relative h-512 w-512">
        <div id="your_scene"></div>
    </div>
    

    CSS:

    .relative {
        position: relative;
    }
    
    h-512 {
        height: 51.2rem;
    }
    
    w-512 {
        width: 51.2rem;
    }
    
    #your_scene {
        overflow: hidden;
        position: absolute;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
    }
    

    Then, you grab the dimensions of it:

    this.mount = document.querySelector('#your_scene');
    this.width = document.querySelector('#your_scene').offsetWidth;
    this.height = document.querySelector('#your_scene').offsetHeight;
    

    Then, you (in my case) set the renderer background to transparent, set pixel ratio, and then apply the size of the container to the renderer. Three.js works by appending the your_scene container with the renderer <canvas> element, so that's why the final step here is to appendChild:

    this.renderer = new THREE.WebGLRenderer({ alpha: true });
    this.renderer.setPixelRatio(window.devicePixelRatio);
    this.renderer.setSize(this.width, this.height);
    this.mount.appendChild(this.renderer.domElement);
    

    That should hopefully be enough to allow someone to get forensic on their scene. The part doing the most work is the setSize call, but you need to have the correct dimensions, and additionally, your camera must be pointed in the correct spot.

    If you have your code similar to mine, but it still doesn't work, look at your camera perspective and make sure your scene objects are actually in view.

    0 讨论(0)
  • 2021-02-01 21:15

    Maybe I'm missing the point here - but why do the existing suggestions involve resizing the canvas from within the animation loop?

    You'd want your animation loop to do as little as possible, as it will ideally be repeated 30+ times a second, and should be optimized to run as efficiently as possible - affording the maximum fps to the slowest system running it.

    I think there's no harm in calling the resize function from within the resize event listener - much like Meindert Stijfhals suggested below.

    Something like this:

    var container = renderer.domElement.parentElement;
    container.addEventListener('resize', onContainerResize);
    
    function onContainerResize() {
        var box = container.getBoundingClientRect();
        renderer.setSize(box.width, box.height);
    
        camera.aspect = box.width/box.height
        camera.updateProjectionMatrix()
        // optional animate/renderloop call put here for render-on-changes
    }
    

    If you have some kind of render-only-on-changes set up, you can call the render function at the end of the resize function. Otherwise the next time the render loop does fire it should just render with the new settings.

    0 讨论(0)
  • 2021-02-01 21:17

    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);
    
    0 讨论(0)
  • 2021-02-01 21:21

    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.

    0 讨论(0)
  • 2021-02-01 21:27

    I think that the best way to resize the canvas is not the accepted answer above. Every animationframe @gman will run the function resizeCanvasToDisplaySize, doing multiple calculations.

    I think that the best way is to create a window event lister 'resize' and a boolean saying wether the user resized or not. In the animation frame you can check wether the user resized. If he resized, you can resize the canvas in the animationframe.

    let resized = false
    
    // resize event listener
    window.addEventListener('resize', function() {
        resized = true
    })
    
    function animate(time) {
        time *= 0.001
    
        if (resized) resize()
    
        // rotate the cube
        cube.rotation.x += 0.01
        cube.rotation.y += 0.01
    
        // render the view
        renderer.render(scene, camera)
    
        // animate
        requestAnimationFrame(animate)
    }
    
    function resize() {
        resized = false
    
        // update the size
        renderer.setSize(window.innerWidth, window.innerHeight)
    
        // update the camera
        const canvas = renderer.domElement
        camera.aspect = canvas.clientWidth/canvas.clientHeight
        camera.updateProjectionMatrix()
    }
    
    0 讨论(0)
提交回复
热议问题