如何根据容器计算画布大小以避免滚动条出现?
如果我基于窗口设置大小,画布会太大。
可以说,调整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个示例,这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>
"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:内联画布
"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>
"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.innerWidth
和window.innerHeight
,但对于所有情况都有效。
更新:2021年,没有“最佳”方法,只有多种具有不同权衡的方法。
有些人似乎担心性能问题,好像检查2个值是否与另外2个值不匹配会对需要性能的任何程序产生可测量的性能影响。
无论如何,还有我之前发布的旧答案,它只检查容器的大小。有人建议使用window.addEventListener('resize', ...)
,但没有注意到它不能处理画布尺寸发生变化而窗口本身未发生变化的情况(例如上面的示例4)。
无论如何,在2021年,我们可以使用ResizeObserver
代替window.addEventListener('resize', ...)
,对于那些认为每帧检查2个变量是性能开销的人来说。 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),ResizeObserver
无法使用。
它不能保证更新速度。
换句话说,通过rAF和每帧检查,理想情况下,你的内容将始终保持正确的大小,因为浏览器要求你准备好你的内容。相比之下,ResizeObserver
只是表示最终会给你一个事件。这可能会在你的内容实际上需要调整大小的5-10帧之后才出现。
还有一件事是处理devicePixelRatio。这是一个太大的话题,超出了本答案的范围,但你可以在这里了解更多信息
那很简单,设置一下渲染的大小就可以了。
container = document.getElementById('container');
renderer.setSize($(container).width(), $(container).height());
container.appendChild(renderer.domElement);
我认为最好的调整画布大小的方法不是上面接受的答案。每个animationframe @gman都会运行函数resizeCanvasToDisplaySize,进行多次计算。
我认为最好的方法是创建一个窗口事件监听器“resize”和一个布尔值,表示用户是否已经调整大小。在动画帧中,可以检查用户是否调整了大小。如果他调整了大小,您可以在动画帧中调整画布大小。
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()
}
resized
变量,你仍然可以在addEventListener中调用resize
函数。 - Ömer Feyyaz Selçukvar 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提供的答案是完整的答案。但是,为了总结可能的情况,必须假设两种可能性,这就是答案差异巨大的原因。
第一种可能性是容器是全局(head)对象:window
,或者可能是<body>
或<frameset>
,在这种情况下,最方便的方法是使用window.addEventListener('resize', onResize()) { ... }
。
第二种可能性是最初提出的问题:Three.js WebGL输出的容器应该具有响应性。在这种情况下,无论容器是canvas、section还是div,处理调整大小的最佳方法是针对DOM中的容器,并通过传递容器的clientWidth
和clientHeight
来使用renderer.setsize()
方法,然后在渲染循环函数中调用onResize()
函数,其完整实现由@gman提供。在这种特殊情况下,不需要使用addEventListener
。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>minimal threejs</title>
<style>
* {
margin: 0;
padding: 0;
}
#canvas-wrapper {
background-color: grey;
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
display: flex;
justify-content: center;
align-items: center;
}
#main-canvas {
border: 5px solid red;
max-width: calc(100vh / 3 * 4);
width: 100%;
image-rendering: pixelated;
}
</style>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.151.0/build/three.module.js"
}
}
</script>
</head>
<body>
<div id="canvas-wrapper">
<canvas id="main-canvas"></canvas>
</div>
<script type="module">
import * as THREE from 'three';
const main_canvas = document.getElementById("main-canvas");
const n = 4
let cwidth = 640 / n;
let cheight = 480 / n;
main_canvas.setAttribute("width", cwidth + "px");
main_canvas.setAttribute("height", cheight + "px");
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, main_canvas.clientWidth / main_canvas.clientHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({
antialias: true,
canvas: main_canvas,
});
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
camera.position.z = 5;
function animate() {
requestAnimationFrame(animate);
cube.rotation.x += 0.01;
cube.rotation.y += 0.01;
renderer.render(scene, camera);
}
animate();
</script>
</body>
</html>
<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;
your_scene
容器附加到渲染器<canvas>
元素上来工作,因此这里的最后一步是使用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()
方法在设置画布大小时采用CSS像素,而不是实际设备像素。这意味着在高DPI屏幕上,它创建的实际像素数量似乎基于width * window.devicePixelRatio
和height * window.devicePixelRatio
。例如,在您尝试像我一样基于three.js帧捕获特定大小的GIF时,这很重要。最终我做了以下操作:
camera.aspect = gifWidth / gifHeight;
camera.updateProjectionMatrix();
var scale = window.devicePixelRatio;
renderer.setSize( gifWidth / scale, gifHeight / scale, true );
这可能不是处理它的理想方式,但对我来说有效。
resizeCanvasToDisplaySize
会浪费大量性能。最好和最安全的方法是在window.addEventListener('resize', resizeCanvasToDisplaySize)
中运行它,这样只有在布局更改时才会进行渲染,而不是每一帧都进行渲染。 - TOPKATcanvas.width !== width
应该改为canvas.width !== width*window.devicePixelRatio
吗? - Alec Jacobson