Threejs画布大小基于容器

Posted

技术标签:

【中文标题】Threejs画布大小基于容器【英文标题】:Threejs canvas size based on container 【发布时间】:2015-07-05 05:48:01 【问题描述】:

如何根据容器计算画布大小?避免滚动。

如果我根据窗口设置大小,画布太大了。

【问题讨论】:

【参考方案1】:

嗯,这并不难。设置渲染的大小就可以了。

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

【讨论】:

这是否处理视网膜显示、DPI 缩放等? 向下滚动以获得更好的答案。 好答案,我看不到 jQuery 的用途【参考方案2】:

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

首先在设置初始纵横比时没有理由设置它,因为我们要设置它以响应画布的大小不同,所以设置两次只是浪费代码

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

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

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 个示例,这三个示例之间的唯一区别是 CSS 以及是我们制作 canvas 还是 three.js 制作了 canvas

示例 1:全屏,我们制作画布

"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:全屏画布,three.js制作画布

"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; 
&lt;script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/85/three.min.js"&gt;&lt;/script&gt;

示例 3:内联画布

"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% 宽度的画布(如实时编辑器)

"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 年,没有“最佳”方式,只有多种方式需要权衡取舍。

有些人似乎关心性能,好像检查 2 个值是否不匹配 2 个其他值对任何需要性能的程序有任何可衡量的性能影响?

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

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

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:全屏,我们制作画布

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% 宽度的画布(如实时编辑器)

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

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

    不保证会尽快更新。

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

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

【讨论】:

在第二个示例中,为什么自动生成的画布元素具有 CSS 宽度/高度?自动画布具有宽度和高度的内联样式,因此您不会覆盖它。 自动生成的画布没有样式。仅当您调用 renderer.setSize 并且最后不传递 false 时才应用样式。这可以说是与浏览器作斗争。 CSS 的全部意义在于让 CSS 选择大小。你的画布可以设置为 50%、50px、50em、50rem、50vw。它可以在 flexbox、表格、段落、网格框中。您应该使用 CSS 选择大小,查找它的大小。您不应该与浏览器对抗并通过 javascript 强制其大小 这不是“最好的方法”,因为在渲染循环上运行resizeCanvasToDisplaySize 是对性能的巨大浪费。最好和最安全的方法是在 window.addEventListener('resize', resizeCanvasToDisplaySize) 中运行它,这样它只会在布局更改而不是每一帧时渲染。 您对“大规模”的定义非常有趣【参考方案3】:

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

我认为最好的方法是创建一个窗口事件列表器“调整大小”和一个布尔值,表示用户是否调整大小。在动画帧中,您可以检查用户是否调整了大小。如果他调整了大小,您可以调整动画帧中画布的大小。

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

【讨论】:

问题是关于更一般的“容器”而不是窗口。你会如何调整你的答案来解决这个问题? @MikeGoodstadt 一种选择是查看 Resize Observer(带有适用于旧浏览器的 polyfill),它可以监控元素的大小变化。这对我来说仍然不满意,因为我的画布容器根据抽屉的动画改变了大小。调整大小观察者似乎只在调整大小的开始和结束时触发,所以我的画布调整大小函数只在那时触发,而不是在容器动画期间触发。最终,我使用 JS 手动为我的容器设置动画,连同画布大小,使用 requestAnimationFrame 在基本函数中同步。 即使这个答案也会检查每个循环中的resized 变量。您可以在 addEventListener 中调用 resize 函数【参考方案4】:

也许我在这里漏掉了重点 - 但是为什么现有的建议涉及从动画循环中调整画布的大小?

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

我认为从 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

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

【讨论】:

【参考方案5】:

@gman 提供的答案当然是完整的答案。但是,要回顾可能的情况,必须假设两种可能性,这就是答案大相径庭的原因。

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

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

【讨论】:

【参考方案6】:

下面的代码让我能够掌握画布渲染大小:

首先,场景在&lt;div id="your_scene"&gt; 内渲染,您可以选择尺寸:

<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 通过将 your_scene 容器与渲染器 &lt;canvas&gt; 元素附加来工作,这就是为什么这里的最后一步是 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 调用,但您需要有正确的尺寸,此外,您的相机必须指向正确的位置。

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

【讨论】:

【参考方案7】:

我不确定从上面的 cmets 或 API 文档中是否对每个人都很明显,但以防万一其他人遇到我遇到的问题:请注意,渲染器的 setSize() 方法在设置画布的大小,而不是实际的设备像素。这意味着在高 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 );

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

【讨论】:

以上是关于Threejs画布大小基于容器的主要内容,如果未能解决你的问题,请参考以下文章

在具有动画滚动功能的threeJS画布中,如何防止向上滚动超出页面顶部?

ThreeJS canvas画布自适应的原理

ThreeJS渲染器更改父级大小

如何将 Threejs 连接到 React?

如何使用 babylonjs 或 threejs 预览 3D 中的 2D 平面对象? [关闭]

ThreeJS零基础入门-三种坐标系位置数据的转换