5、Three.js响应式设计

348 阅读3分钟

1-canvas 的响应式布局——保持画布尺寸和画板尺寸一致

canvas 的尺寸有两种:

  • 一种是width、height属性,一般称其为画布尺寸,即图形绘制的地方。画布尺寸(像素尺寸),即canvas画布在高度和宽度上有多少个像素,默认值分别为300px、150px
/* canvas的画布尺寸-图形绘制区域*/
<canvas id="canvas" width="300" height="300">
  • 另一种是css样式里的width、height属性,可通过内联样式、内部样式表或外部样式表设置。一般称其为画板尺寸,用于渲染绘制完成的图形。默认值为空。
/*CSS样式中的画板尺寸*/
<canvas id="canvas" style="width:300px; height:300px;">
<style>
/*CSS样式中的画板尺寸*/
    #canvas {
       width:300px;
       height:300px;
    }
</style>

在web前端,dom元素的响应式布局一般是通过css 实现的。

而canvas 则并非如此,canvas 的响应式布局需要考虑其像素尺寸。

  • 如果设置了画布尺寸,未设置画板尺寸,或者两者都未设置,那么画板尺寸随画布尺寸改变。
  • 但若设置了画板尺寸,而未设置画布尺寸,或者分别设置了,那么画板尺寸将不再随画布尺寸而改变。

如果canvas画布尺寸画板尺寸设置的不一样时,就会产生一个问题,渲染时画布要通过缩放来使其与画板尺寸一样,那么画布上已经绘制好的图形也会随之缩放,随之导致变形失真。

接下来,让canvas 画布自适应浏览器的窗口的尺寸,来实现canvas 的响应式布局。

1.1 示例:一个立方体

1.gif

<template>
  <div id="threeBox"></div>
</template>

<script>
import * as THREE from 'three'
export default {
  data() {
    return {
      
    }
  },
  mounted() {
    this.init();
  },
  methods: {
    init() {
      const scene = new THREE.Scene();
      const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );

      const renderer = new THREE.WebGLRenderer();
      renderer.setSize( window.innerWidth, window.innerHeight );
      document.querySelector("#threeBox").appendChild( renderer.domElement );

      const geometry = new THREE.BoxGeometry();
      const material = new THREE.MeshNormalMaterial();
      const cube = new THREE.Mesh(geometry, material);
      scene.add(cube);

      camera.position.z = 5;
      animate()
      function animate() {
          requestAnimationFrame( animate );

          cube.rotation.x += 0.01;
          cube.rotation.y += 0.01;

          renderer.render( scene, camera );
      }


    }
  }
}
</script>

1.2 先取消renderer 的尺寸设置。

//renderer.setSize(innerWidth, innerHeight);
// renderer.setSize( window.innerWidth, window.innerHeight );

1.3 用css 设置canvas 画布及其父元素的尺寸,使其充满窗口。

html {
 height: 100%;
}
body {
 margin: 0;
 overflow: hidden;
 height: 100%;
}
#threeBox, canvas{
 width: 100%;
 height: 100%;
}

效果如下:

1.gif

由上图可见,立方体的边界出现了锯齿,这就是位图被css拉伸后失真导致的,默认canvas 画布的尺寸只有300*150

因此,我们需要用canvas 画布的像素尺寸自适应窗口

1.4 让canvas 像素尺寸随css 尺寸同步更新

建立一个让canvas 像素尺寸随css 尺寸同步更新的方法

resizeRendererToDisplaySize(renderer);

// 将渲染尺寸设置为其显示的尺寸,返回画布像素尺寸是否等于其显示(css)尺寸的布尔值
function resizeRendererToDisplaySize(renderer) {
  const { width, height, clientWidth, clientHeight } = renderer.domElement;
  const needResize = width !== clientWidth || height !== clientHeight;
  if (needResize) {
    renderer.setSize(clientWidth, clientHeight, false);
  }
  return needResize;
}

renderer.setSize(w,h,bool) 是重置渲染尺寸的方法,在此方法里会根据w,h参数重置canvas 画布的尺寸。

setSize() 方法中的bool 参数很重要,会用于判断是否设置canvas 画布的css 尺寸。

1.5. 同步调整 相机视口的宽高比

当canvas 画布的尺寸变化了,相机视口的宽高比也需要同步调整。这样我们拖拽浏览器的边界,缩放浏览器的时候,就可以看到canvas 画布自适应浏览器的尺寸了。

function animate() {
  requestAnimationFrame(animate);
  if (resizeRendererToDisplaySize(renderer)) {
    const { clientWidth, clientHeight } = renderer.domElement;
    camera.aspect = clientWidth / clientHeight;
    camera.updateProjectionMatrix();
  }
  cubes.forEach((cube) => {
    cube.rotation.x += 0.01;
    cube.rotation.y += 0.01;
  });
  renderer.render(scene, camera);
}
  • camera.aspect 属性是相机视口的宽高比
  • 当相机视口的宽高比变了,相机的透视投影矩阵也会随之改变,因此我们需要使用camera.updateProjectionMatrix() 方法更新透视投影矩阵。
  • 之所以不把更新相机视口宽高比的方法一起放进resizeRendererToDisplaySize()里,这是为了降低resizeRendererToDisplaySize() 方法和相机的耦合度。

1.6 修改后效果

1.gif

<template>
  <div id="threeBox"></div>
</template>

<script>
/* eslint-disable */
import * as THREE from 'three'
export default {
  data() {
    return {
      
    }
  },
  mounted() {
    this.init();
  },
  methods: {
    init() {
      const scene = new THREE.Scene();
      const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );

      const renderer = new THREE.WebGLRenderer();
      // renderer.setSize( window.innerWidth, window.innerHeight );
      document.querySelector("#threeBox").appendChild( renderer.domElement );

      const geometry = new THREE.BoxGeometry();
      const material = new THREE.MeshNormalMaterial();
      const cube = new THREE.Mesh(geometry, material);
      scene.add(cube);

      

      resizeRendererToDisplaySize(renderer);

      // 将渲染尺寸设置为其显示的尺寸,返回画布像素尺寸是否等于其显示(css)尺寸的布尔值
      function resizeRendererToDisplaySize(renderer) {
       const { width, height, clientWidth, clientHeight } = renderer.domElement;
       const needResize = width !== clientWidth || height !== clientHeight;
       if (needResize) {
         renderer.setSize(clientWidth, clientHeight, false);
        }
       return needResize;
      }

      camera.position.z = 5;
      
      animate()
      function animate() {
         requestAnimationFrame(animate);
         if (resizeRendererToDisplaySize(renderer)) {
           const { clientWidth, clientHeight } = renderer.domElement;
           camera.aspect = clientWidth / clientHeight;
           camera.updateProjectionMatrix();
          }
          cube.rotation.x += 0.01;
          cube.rotation.y += 0.01;
          renderer.render(scene, camera);
      }


    },
    
  }
}
</script>
<style lang="scss">
html {
 height: 100%;
}
body {
 margin: 0;
 overflow: hidden;
 height: 100%;
}
#threeBox, canvas{
 width: 100%;
 height: 100%;
}
</style>