如何用three.js实现一个3D月亮

3,774 阅读7分钟

我正在参加中秋创意投稿大赛,详情请看:中秋创意投稿大赛

前言

本文以实现一个3D月亮为例子,入门three.js,掘金中已经有很多文章描述如何实现一个3D月亮,但是都没有详细介绍实现一个3D月亮过程所用到方法及其参数的作用和影响,本文将详细介绍。

引入three.js

执行命令 npm install three --save 安装,安装成功后。

在页面中用 import {//...} from "three" 从 three.js 引入所需要的方法。

import {
  Scene,
  WebGLRenderer,
} from "three";

创建一个场景

简单的来说就是创建一个容器,物体在其中展示,使用Scene类来创建。

this.scene = new Scene();

Scene提供一个实例方法add,往场景中添加内容。

this.scene.add()

而这个实例方法,你在three.js的官方文档中介绍Scene类的地方会找不到,这是因为Scene类继承Object3D这个基类,addObject3D类的实例方法,可以在介绍Object3D类的地方找到add方法。

创建一个渲染器

渲染器的作用是把添加到场景中的内容渲染出来,使用WebGLRenderer类来创建。

this.renderer = new WebGLRenderer();
document.querySelector("#planet").appendChild(this.renderer.domElement);

WebGLRenderer类的构造函数接收一个配置对象,当然也可以不传,将会采用最合理值来配置,渲染器会输出一个 canvas ,可以通过setSize实例方法来设置 canvas 的长和宽。

this.renderer.setSize(innerWidth, innerHeight);

渲染器创键完成,要通过domElement属性获取渲染器生成 canvas 的 DOM 对象,然后添加 document 中。

document.querySelector("#planet").appendChild(this.renderer.domElement);

创建一个相机

相机是three.js中一个很关键的要素,不同的相机呈现出来的3D效果各不相同。three中的相机分为两种。一种是正交相机和透视相机。在正交相机中,无论物体距离相机距离远或者近,呈现到用户眼睛中的物体的大小都是保持不变的。在透视相机中,符合我们用眼睛观察事物的特点,近大远小。

在实现一个3D月亮中,我们选择透视相机,月亮实际很大,地球离月亮,看月亮很小,很符合透视相机的效果。

透视相机使用PerspectiveCamera类来创建,PerspectiveCamera类的构造函数接收以下几个参数,这些参数控制着物体的展示效果。

PerspectiveCamera( fov : Number, aspect : Number, near : Number, far : Number )

  • fov — 摄像机视锥体垂直视野角度也称可视角度
  • aspect — 摄像机视锥体长宽比
  • near — 摄像机距视锥体近端面的距离
  • far — 摄像机距视锥体远端面的距离

在了解这些参数之前,我们要去弄明白摄像机视锥体,用一张图来说明视锥体中以下名词的概念。

20200319140013738.png

  • 摄像机视锥体垂直视野角度是图中的α。
  • 摄像机视锥体近端面是图中的near plane。
  • 摄像机视锥体远端面是图中的far plane。
  • 摄像机视锥体长宽比表示输出图像的宽和高之比。

在两张图说明fovaspectnearfar参数的含义。

1.png

2.png

只有离相机的距离大于near值,小于far值,且在相机的可视角度fov之内区域,才能被相机拍摄到,把这个区域称为视景体,正如透视图中,灰色的部分是视景体,是可能被渲染的物体所在的区域。

fov是视景体竖直方向上的张角(是角度制而非弧度制),如侧视图所示。

aspect等于width / height,是相机水平方向和竖直方向长度的比值,通常设为 canvas 的横纵比例。

nearfar分别是相机到视景体最近、最远的距离,均为正值,且far应大于near

aspectnearfar不变的前提下,fov越大,在视景体中的物体会越小,这是物体相对于整个视景体的大小就变小了,看起来物体就显得变小了。实际上物体的实际大小并未改变。

3.png

在相机位置不变的前提下,near越大,在视景体中的物体会越小,符合透视相机“近大远小”的特点。

下面来使用PerspectiveCamera类来创建一个透视相机。

this.camera = new PerspectiveCamera(75, innerWidth / innerHeight, 1, 1000);

其中innerWidthinnerHeight是浏览器的可视窗口的宽和高,表示通过相机看的画面铺满整个浏览器页面。

创建完相机,还要给相机设置位置,通过PerspectiveCamera类实例化对象的position属性来设置,该属性是继承Object3D基类的,且该属性是Vector3类的实例化对象,其有个set实例方法来设置x、y、z的坐标。在three.js中采用的是右手坐标系。

image.png

// this.camera.position.set(x, y, z);
this.camera.position.set(0, 0, 100);

给相机设置位置也就是让物体能在相机产生的视景体中展示出来。为了让物体不变形,一般只调整z坐标的值。z值只能在nearfar之间,z值越小物体越大。

给相机设置完位置后,还要一个非常重要的操作,让相机永远朝向场景。

this.camera.lookAt(this.scene.position);

创建一个球体

以上把一些基本设置都弄好后,来创建一个球体当作月亮,在创建前,把代码整合一下。

this.scene = new Scene();
this.renderer = new WebGLRenderer();
this.renderer.setSize(innerWidth, innerHeight);
document.querySelector("#planet").appendChild(this.renderer.domElement);
this.camera = new PerspectiveCamera(75, innerWidth / innerHeight, 1, 1000);
this.camera.position.set(0, 0, 100);
this.camera.lookAt(this.scene.position);

球体是一种几何体,three.js提供一些列创建几何体的类,可以使用SphereGeometry类来创建一个球体。

SphereGeometry(radius : Float, widthSegments : Integer, heightSegments : Integer, phiStart : Float, phiLength : Float, thetaStart : Float, thetaLength : Float

  • radius — 球体半径,默认为1。
  • widthSegments — 水平分段数(沿着经线分段),最小值为3,默认值为8。
  • heightSegments — 垂直分段数(沿着纬线分段),最小值为2,默认值为6。
  • phiStart — 指定水平(经线)起始角度,默认值为0。
  • phiLength — 指定水平(经线)扫描角度的大小,默认值为 Math.PI * 2。
  • thetaStart — 指定垂直(纬线)起始角度,默认值为0。
  • thetaLength — 指定垂直(纬线)扫描角度大小,默认值为 Math.PI。

其中widthSegmentsheightSegments的含义如下图所示,其值越大,创建的球体越圆。这是因为在图形底层的实现中,并没有曲线的概念,曲线都是由多个折线近似构成的。对于球体而言,当这两个值较大的时候,形成的多面体就可以近似看做是球体了。

image.png

phiStartphiLengththetaStartthetaLength的单位是度(°),一般用Math.PI来赋值,Math.PI等于180°,那么90°就用Math.PI/2表示,这些参数可以实现一个切片球体,如半球体。

例如创建一个左半球new SphereGeometry(6,32,32,Math.PI,Math.PI)

例如创建一个右半球new SphereGeometry(6,32,32,0,Math.PI)

例如创建一个上半球new SphereGeometry(6,32,32,0,Math.PI*2,0,Math.PI/2)

然而写下

const geometry = new SphereGeometry(30,32,32);
this.scene.add(geometry);

会发现所预计的球体并没有出现。这是因为还未给球体上色,当然看不见了。

给球体添加颜色

在three.js中用材质来给几何体上色,先用MeshBasicMaterial类创建一种不受光照的影响的材质,换个话来说,这种材质是常亮的。

const material = new MeshBasicMaterial({ color: 0xffff00});

创建完了材质(颜色)怎么给球体上色呢,要用到网格Mesh,相当用一张网把这些有颜色的材质贴在球体上。

Mesh( geometry : Geometry, material : Material )

  • Geometry 几何体

  • Material 材质

this.moon = new Mesh(geometry, material);

球体上色完成后,将球体添加到场景中。

this.scene.add(moon);

运行后,会很尴尬,预想的月亮还是没有出现在屏幕上。这是因为相机还没把球体渲染到场景中,要执行以下代码。

this.renderer.render(this.scene, this.camera);

用相机渲染一个场景后,屏幕才会出现月亮。

让月亮动起来

使用OrbitControls类来创建一个控制器来实现,先引入

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

OrbitControls( object : Camera, domElement : HTMLDOMElement )

  • Camera 相机

  • domElement 要控制的DOM对象,一般是渲染器生成的DOM对象

new OrbitControls(this.camera, this.renderer.domElement);

有了控制器后,让月亮动起来还要用定时器定时让相机把月亮渲染到场景中。这里的定时器选择requestAnimationFrame,其采用系统时间间隔,可以保持最佳绘制效率。

loop() {
    requestAnimationFrame(this.loop);
    this.renderer.render(this.scene, this.camera);
    this.camera.lookAt(this.scene.position);
},

此时可以用鼠标控制月亮旋转。那怎么让月亮自转起来。

让月亮自转

three.js基类Object3D中有个属性rotation可以控制物体旋转角度,Mesh类也是继承基类Object3D

属性rotation是个Euler类(欧拉角)其属性xyz用来控制物体绕X轴、Y轴、Z轴旋转角度。

我们让月亮绕Y轴转动,只要在loop函数中加一段代码就行。

loop() {
    this.moon.rotation.y += 0.01;
    requestAnimationFrame(this.loop);
    this.renderer.render(this.scene, this.camera);
    this.camera.lookAt(this.scene.position);
},

最后

这里是用Vue2.0开发,先把之前代码整理一下。

<template>
  <div id="planet"></div>
</template>
<script>
import {
  Scene,
  WebGLRenderer,
  PerspectiveCamera,
  SphereGeometry,
  MeshBasicMaterial,
  Mesh,
} from "three";
export default {
  data() {
    return {
      scene: null,
      renderer: null,
      camera: null,
    };
  },
  mounted() {
    this.scene = new Scene();
    this.renderer = new WebGLRenderer();
    this.renderer.setSize(innerWidth, innerHeight);
    document.querySelector("#planet").appendChild(this.renderer.domElement);
    this.camera = new PerspectiveCamera(75, innerWidth / innerHeight, 1, 1000);
    this.camera.position.set(0, 0, 100);
    this.camera.lookAt(this.scene.position);
    const geometry = new SphereGeometry(30, 32, 32);
    const material = new MeshBasicMaterial({
      color: 0xe4ff03
    });
    this.moon = new Mesh(geometry, material);
    this.scene.add(moon);
    new OrbitControls(this.camera, this.renderer.domElement);
    this.loop();
  },
  methods:{
    loop() {
      this.moon.rotation.y += 0.01;
      requestAnimationFrame(this.loop);
      this.renderer.render(this.scene, this.camera);
      this.camera.lookAt(this.scene.position);
    },
  }
};
</script>