Threejs canvas 基于容器的大小

Threejs canvas size based on container

如何根据容器计算 canvas 大小?为了避免滚动。

如果我根据 window 设置大小,canvas 就太大了。

好吧,那不是 difficult.Set 你的渲染尺寸会起作用。

container = document.getElementById('container');
renderer.setSize($(container).width(), $(container).height());
container.appendChild(renderer.domElement);

可以说,调整 three.js 大小的最佳方法是对其进行编码,因此它只接受 canvas 由 CSS 设置的任何大小。这样,无论您如何使用 canvas,您的代码都可以正常工作,无需针对不同情况进行更改。

首先,在设置初始宽高比时,没有理由设置它,因为我们要设置它以响应 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);

然后我们需要一些代码来调整 canvas 的大小以匹配其显示大小

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
  }
}

渲染前在你的渲染循环中调用它

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);

这里有 3 个例子,这 3 个例子之间的唯一区别是 CSS 以及我们是否使 canvas 或 three.js 使 canvas

示例 1:全屏,我们制作 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>

示例 2:全屏 canvas、three.js 使 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>

示例 3:内联 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>

示例 4:50% 宽度 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);
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>

注意 window.innerWidthwindow.innerHeight 从未在上面的代码中引用,但它适用于所有情况。


更新:2021 年,没有“最好”的方法,只有多种权衡取舍的方法。

有些人似乎担心 perf,好像检查 2 个值是否与其他 2 个值不匹配在任何需要性能的程序中都有任何可衡量的性能影响

无论如何,我在上面发布的旧答案只是检查容器的大小。人们建议使用 window.addEventListener('resize', ...) 但没有注意到不处理 canvas 更改大小但 window 本身不更改大小的情况(上面的示例 4)。

无论如何,在 2021 年,我们可以使用 ResizeObserver 而不是 window.addEventListener('resize', ...),因为那些认为每帧检查 2 个变量是性能开销的人。 ResizeObserver 会在 canvas 改变大小时触发,这可能会发生,即使 window 本身没有改变大小。

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'});

示例 1:全屏,我们制作 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>

示例 4:50% 宽度 canvas(就像实时编辑器一样)

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>

为什么不使用 ResizeObserver

  1. 如果您仍然需要支持它,它在旧浏览器(如 IE)中不可用

  2. 不保证更新得那么快。

    换句话说,使用 rAF 并检查每一帧,理想情况下您的内容将始终保持正确的大小,因为浏览器会要求您准备内容。相反,ResizeObserver 只是说它最终会给你一个事件。这可能会在您的内容实际需要调整大小后出现 5-10 帧。

更多的是处理 devicePixelRatio。对于这个答案来说这个话题太大了,但你可以 see more here

我认为调整 canvas 大小的最佳方法不是上面接受的答案。 每个动画帧@gman 都会运行函数resizeCanvasToDisplaySize,做多次计算。

我认为最好的方法是创建一个 window 事件列表 'resize' 和一个布尔值来说明用户是否调整大小。在动画帧中,您可以检查用户是否调整了大小。如果他调整大小,你可以调整动画帧中的canvas。

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()
}

也许我忽略了这里的重点 - 但为什么现有建议涉及从动画循环中调整 canvas 的大小?

您希望动画循环尽可能少,因为理想情况下它会每秒重复 30 次以上,并且应该尽可能高效地优化到 运行 - 提供最大 fps到最慢的系统 运行ning 它。

我认为从 resize 事件侦听器中调用 resize 函数没有什么坏处——就像 Meindert Stijfhals 在下面建议的那样。

像这样:

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
}

如果您设置了某种仅在更改时渲染,您可以在调整大小函数的末尾调用渲染函数。否则下次渲染循环触发时,它应该只使用新设置渲染。

当然@gman 提供的答案是完整的答案。然而,为了概括可能的情况,人们必须假设这两种可能性,这就是答案截然不同的原因。

第一种可能性是当容器是全局(头)对象时:window,或者可能是<body><frameset>,在这种情况下,最方便的方法是使用 window.addEventListener('resize', onResize()) { ... }.

第二种可能性,是最初提出的一种可能性:Three.js WebGL 输出的容器何时应该响应。在这种情况下,无论容器是 canvas、一个部分还是 div,处理调整大小的最佳方法是将容器定位在 DOM 中,并使用 renderer.setsize() 方法通过将容器的 clientWidthclientHeight 传递给它,然后在渲染循环函数中调用 onResize() 函数,@gman 提到了它的完整实现。在这种特殊情况下,不需要使用 addEventListener.

这是允许我处理 canvas 渲染大小的代码:

首先,场景在 <div id="your_scene"> 中渲染,您可以选择尺寸:

<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%;
}

然后,你抓取它的尺寸:

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

然后,您(在我的例子中)将渲染器背景设置为透明,设置像素比率,然后将容器的大小应用到渲染器。 Three.js 通过将渲染器 <canvas> 元素附加到 your_scene 容器来工作,所以这就是为什么这里的最后一步是 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);

这应该足以让某人在他们的现场进行取证。做最多工作的部分是 setSize 调用,但您需要有正确的尺寸,此外,您的相机必须指向正确的位置。

如果您的代码与我的相似,但仍然无法正常工作,请查看您的相机视角并确保您的场景对象确实在视图中。

我不确定从上面的评论或 API 文档中是否对每个人来说都是显而易见的,但以防万一其他人遇到我遇到的问题:请注意渲染器的 setSize() 方法在设置 canvas 的大小时采用 CSS 像素,而不是实际设备像素。这意味着在高 DPI 屏幕上,它创建的实际像素数似乎基于 width * window.devicePixelRatioheight * window.devicePixelRatio。这很重要,例如,当您尝试做我所做的事情时:根据来自 three.js 的帧捕获特定大小的 GIF。我最终做了以下事情:

camera.aspect = gifWidth / gifHeight;
camera.updateProjectionMatrix();
var scale = window.devicePixelRatio;
renderer.setSize( gifWidth / scale, gifHeight / scale, true );

这可能不是处理它的理想方式,但它对我有用。