你知道 three.js 吗

1,238 阅读4分钟

这段时间,开发的项目从数据表格,跨越到 echarts 图表,两种不同的展现形式,都可以直观的分析数据之间的相互关系,但是 echarts 图表看起来会更形象一点,并且页面展现也很好看。当时想如果在页面中添加一些3D的图形,会不会变得不同,比如 github 首页的地球展示,怀揣一些好奇心,尝试着先看看这些图形及动效是怎么做的,是通过什么实现的?

image.png

这时 threejs 出现在眼前,three.js 是 JavaScript 编写的 WebGL 第三方库,可以用它创建各种三维场景,其中包括了摄影机、光影、材质等各种对象。直接对 WebGL 中的原生 Api 进行研究,还是有一定难度的,会涉及图形处理等。对于传统 js 的开发人员,threejs 中涉及的一些概念也是很新颖的,比如什么是几何体、材质、场景、相机、渲染器、控制器等。

实际上创建 3D 图形的过程可以想象成拍照,在一个大场景中,首先准备一个相机,设置相机参数,调整窗口的大小或远近,让物体尽可能呈现在视野中,再添加光源,使物体产生不同的阴影效果。

27dda476f912f3d3e41f85cac02051ab.png

场景

这时必须创建的,接下来的物体、坐标系等需要调用 add 方法添加到场景中去。

import * as THREE from "three";

this.scene = new THREE.Scene();

相机

相机就像人的眼睛,在视角内的物体,都可以被捕捉到,这里有近端面和远端面的配置,就好比能人能看到最近的位置以及最远的位置。

// width、height为画布宽高
setCamera() {
    this.camera = new THREE.PerspectiveCamera(
      75, // 相机视角
      this.width / this.height, // 长宽比例
      0.1, // 近端面位置
      1000 // 远端面位置
    );
    // 设置相机位置
    this.camera.position.set(0, 0, 10);
  }

物体

添加一个球体,可以内置对象通过 SphereGeometry 实例直接生成。

setSphere() {
    const geometry = new THREE.SphereGeometry(1, 20, 20);
    const material = new THREE.MeshBasicMaterial();
    const sphere = new THREE.Mesh(geometry, material);
    this.scene.add(sphere);
}

添加完这三个要素,要在页面上呈现 3D 物体,则需要关键一步,通过 WebGLRenderer 函数进行渲染处理,将生成的canvas,及 domElement 属性,添加到创建的 dom 容器中,this.container。

setRender() {
    this.renderer = new THREE.WebGLRenderer({
      antialias: true, // 抗锯齿
    });
    // 设置渲染的尺寸大小
    this.renderer.setSize(this.width, this.height);
    // 投射阴影
    this.renderer.shadowMap.enabled = true;
    this.container?.appendChild(this.renderer.domElement);
}

image.png

控制器

为了更方便的查看物体,及判断物体坐标位置,我们还需要添加辅助坐标系及轨道控制器,实现鼠标拖动360度旋转查看,如上图,坐标系中红色代表 X 轴. 绿色代表 Y 轴. 蓝色代表 Z 轴。

这里我们先了解一下三维坐标系,分为左手坐标系和右手坐标系。

image.png

WebGL 和 Three.js 使用的坐标系是右手坐标系,即右手伸开,拇指为 X 轴,食指为 Y 轴,手心为 Z 轴。

import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";

setControls() {
    // 创建轨道控制器
    this.controls = new OrbitControls(this.camera, this.renderer.domElement);
    // 设置控制器阻尼,让控制器更有真实效果,必须在动画循环里调用.update()。
    this.controls.enableDamping = true;

    // 添加坐标轴辅助器
    const axesHelper = new THREE.AxesHelper(5);
    this.scene.add(axesHelper);
}

每次旋转或放大操作都需要重新渲染,因此需要重复函数调用不断的更新物体,用到 requestAnimationFrame 函数。

animate() {
    this.controls.update();
    this.renderer.render(this.scene, this.camera);
    // 渲染下一帧的时候就会调用render函数
    requestAnimationFrame(this.animate.bind(this));
}

2022-12-18 16.52.27.gif

平行光和点光源

threejs 还可以模拟物体在光照下的阴影效果,但是需要满足几点条件:

  • 材质满足能够对光照有反应,例如使用 MeshStandardMaterial 创建物体材料,而不是MeshBasicMaterial。
    const material = new THREE.MeshBasicMaterial();
  • 渲染器开启阴影计算
    this.renderer.shadowMap.enabled = true;
  • 光照投射阴影
  • 球投射阴影
  • 平面接受阴影

首先添加光源,两者选择一个即可

  • 平行光
setLight() {
    const light = new THREE.AmbientLight(0xffffff, 0.5);
    this.scene.add(light);
    // 平行光投射阴影
    const directLight = new THREE.DirectionalLight(0xffffff, 0.5);
    directLight.position.set(10, 10, 10);
    directLight.castShadow = true; // 投射阴影
    directLight.shadow.radius = 5;
    this.scene.add(directLight);
}
  • 点光源
setLight() {
    // 点光源投射阴影
    const pointLight = new THREE.PointLight(0xffffff, 1);
    const smallPoint = new THREE.Mesh(
      new THREE.SphereGeometry(0.2, 20, 20),
      new THREE.MeshStandardMaterial()
    );
    smallPoint.position.set(2, 2, 2);
    smallPoint.add(pointLight);
    this.scene.add(smallPoint);
}

有平行光和物体,还需新增一个面板接受阴影,便于观察阴影效果

setPlane() {
    const geometry = new THREE.PlaneGeometry(10, 10);
    const material = new THREE.MeshStandardMaterial();
    const plane = new THREE.Mesh(geometry, material);
    // 修改面板位置
    plane.position.set(0, -1, 0);
    plane.rotation.x = -Math.PI / 2;
    // 接收阴影
    plane.receiveShadow = true;
    this.scene.add(plane);
}

我们看下:

2022-12-18 17.27.45.gif

总结

去学习和使用 threejs 的过程还是挺有趣的,可以了解一下 WebGL 的知识及坐标系等,接下来可以继续把地球再创建出来,给它添加贴图、纹理等,使其更加细腻。