我正在参加中秋创意投稿大赛,详情请看:中秋创意投稿大赛
前言
本文以实现一个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
这个基类,add
是Object3D
类的实例方法,可以在介绍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
— 摄像机距视锥体远端面的距离
在了解这些参数之前,我们要去弄明白摄像机视锥体,用一张图来说明视锥体中以下名词的概念。
- 摄像机视锥体垂直视野角度是图中的α。
- 摄像机视锥体近端面是图中的near plane。
- 摄像机视锥体远端面是图中的far plane。
- 摄像机视锥体长宽比表示输出图像的宽和高之比。
在两张图说明fov
、aspect
、near
、far
参数的含义。
只有离相机的距离大于near
值,小于far
值,且在相机的可视角度fov
之内区域,才能被相机拍摄到,把这个区域称为视景体,正如透视图中,灰色的部分是视景体,是可能被渲染的物体所在的区域。
fov
是视景体竖直方向上的张角(是角度制而非弧度制),如侧视图所示。
aspect
等于width / height
,是相机水平方向和竖直方向长度的比值,通常设为 canvas 的横纵比例。
near
和far
分别是相机到视景体最近、最远的距离,均为正值,且far
应大于near
。
在aspect
、near
、far
不变的前提下,fov
越大,在视景体中的物体会越小,这是物体相对于整个视景体的大小就变小了,看起来物体就显得变小了。实际上物体的实际大小并未改变。
在相机位置不变的前提下,near
越大,在视景体中的物体会越小,符合透视相机“近大远小”的特点。
下面来使用PerspectiveCamera
类来创建一个透视相机。
this.camera = new PerspectiveCamera(75, innerWidth / innerHeight, 1, 1000);
其中innerWidth
和innerHeight
是浏览器的可视窗口的宽和高,表示通过相机看的画面铺满整个浏览器页面。
创建完相机,还要给相机设置位置,通过PerspectiveCamera
类实例化对象的position
属性来设置,该属性是继承Object3D
基类的,且该属性是Vector3
类的实例化对象,其有个set
实例方法来设置x、y、z的坐标。在three.js中采用的是右手坐标系。
// this.camera.position.set(x, y, z);
this.camera.position.set(0, 0, 100);
给相机设置位置也就是让物体能在相机产生的视景体中展示出来。为了让物体不变形,一般只调整z坐标的值。z值只能在near
和far
之间,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。
其中widthSegments
和heightSegments
的含义如下图所示,其值越大,创建的球体越圆。这是因为在图形底层的实现中,并没有曲线的概念,曲线都是由多个折线近似构成的。对于球体而言,当这两个值较大的时候,形成的多面体就可以近似看做是球体了。
phiStart
、phiLength
、thetaStart
、thetaLength
的单位是度(°),一般用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
类(欧拉角)其属性x
、y
、z
用来控制物体绕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>