前言
课件地址
课堂目标
- 理解图形树的概念
- 掌握图形树的用法
知识点
- 图形树
- 坐标轴辅助对象
- 栅格辅助对象
- dat.gui 调试工具
1-图形树的概念
图形树是three.js 的核心内容之一。
图形树的本质就是多坐标系的嵌套,之前我们在WebGL里说过世界坐标系和本地坐标系的概念,若大家理解了这个概念,也就理解了图形树。
图形树中的每个节点,都有一个独立的坐标系,因此每个节点中的图形就有一个本地坐标位。若想知道这个图形在整个Scene 场景中的位置,那就需要将其本地坐标位转换为世界坐标位。
我们之前在WebGL里说相关知识的时候,举过一个“宇宙>太阳>地球>月球”的例子,接下来咱们继续那它说事。
2-宇宙示例
上图中,黄球是太阳,篮球是地球,灰球是月球。
地球绕太阳公转,月球绕地球旋转。
从月球的角度来看,它是在地球的“局部空间”中旋转的,它只考虑绕它在地球的本地坐标系内的旋转。尽管从太阳的角度来看,它相对于太阳的运动轨迹是一条螺旋形曲线。
这就好像生活在地球上的人不必考虑地球自身的自转,也不必考虑地球绕太阳的公转,大家想怎么走就怎么走,不需要去想地球的移动或旋转。
然而,即使你在地球上坐着不动,你仍然以大约1000英里/小时的速度随地球自转,以大约67000英里/小时的速度围绕太阳公转。
接下来我们就用代码模拟一下太阳、地球和月球间的运动关系。
2-1-绘制太阳、地球和月球
1.利用咱们上一章封装的Stage对象搭建一个场景。
const stage = new Stage();
const { scene, renderer,camera } = stage;
2.设置相机的视点位、目标点和上方向,使其变成俯视状态。
camera.position.set(0, 20, 0)
camera.up.set(0, 0, -1)
camera.lookAt(0, 0, 0)
默认相机的上方向是y方向的,但当相机俯视的时候,y方向就不合适了,所以我将上方向设置为了-z方向。这就相当于我低下头俯视裁剪空间。
设置完了上方向,别忘了用lookAt() 方法设置相机的目标点,这个方法除了可以设置目标点,还可以更新相机的视图矩阵。
3.向场景中添加一个太阳。
// 太阳、地球和月亮都共用一个球体
const radius = 1;
const widthSegments = 6;
const heightSegments = 6;
const sphereGeometry = new SphereGeometry(radius, widthSegments, heightSegments);
// 太阳
const sunMaterial = new MeshPhongMaterial({emissive: 0xFFFF00});
const sunMesh = new Mesh(sphereGeometry, sunMaterial);
scene.add(sunMesh);
//需要旋转的对象集合
const objects:Object3D[] = [sunMesh]
MeshPhongMaterial 材质中的emissive 是一个自发光属性,这样它在没有光源的前提下,也能可见。
4.在场景中心添加一个点光源。
const color = 0xFFFFFF;
const intensity = 3;
const light = new PointLight(color, intensity);
scene.add(light);
5.在渲染之前,遍历objects 中的物体,使其转起来。
stage.beforeRender = (time = 0) => {
time *= 0.001
objects.forEach((obj) => {
obj.rotation.y = time
})
}
效果如下:
6.向场景中添加一个地球。
const earthMaterial = new MeshPhongMaterial({
color: 0x2233ff,
emissive: 0x112244,
})
const earthMesh = new Mesh(sphereGeometry, earthMaterial)
earthMesh.scale.set(0.5, 0.5, 0.5)
earthMesh.position.x = 5
scene.add(earthMesh)
objects.push(earthMesh)
现在我把地球添加到了Scene 场景里,那它会和太阳各转各的。
接下来,咱们改一下上面的代码,把地球放太阳系里,使其绕太阳旋转。
7.利用Group对象建立一个太阳坐标系,将太阳和地球都置入其中。
// 太阳坐标系
const solarSystem = new Group()
scene.add(solarSystem)
objects.push(solarSystem)
// 太阳
const sunMaterial = new MeshPhongMaterial({ emissive: 0xffff00 })
const sunMesh = new Mesh(sphereGeometry, sunMaterial)
solarSystem.add(sunMesh)
// 地球
const earthMaterial = new MeshPhongMaterial({
color: 0x2233ff,
emissive: 0x112244,
})
const earthMesh = new Mesh(sphereGeometry, earthMaterial)
earthMesh.scale.set(0.5, 0.5, 0.5)
earthMesh.position.x = 5
solarSystem.add(earthMesh)
现在,太阳坐标系的原点与太阳对象的中心点是一致的,旋转太阳坐标系的时候会带动太阳的自转。与此同时,还会让地球绕太阳公转。
8.用同样的原理,在地球外面再包裹一个地球坐标系,并建立月球坐标系和月球。
// 太阳坐标系
const solarSystem = new Group()
scene.add(solarSystem)
objects.push(solarSystem)
// 地球坐标系
const earthSystem = new Group()
earthSystem.position.x = 5
solarSystem.add(earthSystem)
objects.push(earthSystem)
// 月球坐标系
const moonSystem = new Group()
moonSystem.position.x = 2
earthSystem.add(moonSystem)
objects.push(moonSystem)
// 太阳
const sunMaterial = new MeshPhongMaterial({ emissive: 0xff9600 })
const sunMesh = new Mesh(sphereGeometry, sunMaterial)
solarSystem.add(sunMesh)
// 地球
const earthMaterial = new MeshPhongMaterial({
color: 0x00acec,
emissive: 0x00acec,
})
const earthMesh = new Mesh(sphereGeometry, earthMaterial)
earthMesh.scale.set(0.5, 0.5, 0.5)
earthSystem.add(earthMesh)
// 月球
const moonMaterial = new MeshPhongMaterial({
color: 0x999999,
emissive: 0x999999,
})
const moonMesh = new Mesh(sphereGeometry, moonMaterial)
moonMesh.scale.set(0.2, 0.2, 0.2)
moonSystem.add(moonMesh)
效果如下:
我们在上面的代码里,针对太阳、地球和月球建立了3个坐标系对象,这个坐标系对象是无法直接显示的,不过我们可以通过three.js 里的坐标轴辅助对象和栅格辅助对象将其显示出来。
2-2-添加辅助对象
1.建立AxesGridHelper类,用于为坐标系添加坐标轴和栅格。
import { AxesHelper, GridHelper, LineBasicMaterial, Object3D } from "three"
export default class AxesGridHelper {
grid: GridHelper
axes: AxesHelper
_visible: boolean = true
constructor(obj: Object3D, size = 2) {
const axes = new AxesHelper()
const axesMat = axes.material as LineBasicMaterial
axesMat.depthTest = false
obj.add(axes)
const grid = new GridHelper(size)
const gridMat = grid.material as LineBasicMaterial
gridMat.depthTest = false
obj.add(grid)
this.grid = grid
this.axes = axes
this.visible = this._visible
}
get visible() {
return this._visible
}
set visible(v) {
this._visible = v
this.grid.visible = v
this.axes.visible = v
}
}
2.遍历objects,以其中的坐标系为参数实例化AxisGridHelper。
objects.forEach((obj) => {
new AxesGridHelper(obj)
})
效果如下:
接下来我们还可以通过GUI工具控制辅助对象是否显示。
2-3-调试项目
1.下载dat.gui
npm i dat.gui @types/dat.gui --save
2.引入GUI工具
import { GUI } from "dat.gui"
3.实例化GUI工具
const gui = new GUI({ autoPlace: false })
autoPlace:是否将GUI的DOM 元素添加到body中,默认为true。
我这里将autoPlace设置false,是为了将GUI的DOM 元素添加到canvas 包裹器里。
const Universe: React.FC = (): JSX.Element => {
const divRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const { current } = divRef
if (current) {
current.innerHTML = ""
current.append(renderer.domElement)
current.append(gui.domElement)
stage.animate()
}
}, [])
return <div ref={divRef} className="canvasWrapper"></div>
}
4.声明一个实例化AxisGridHelper 对象的方法,并将辅助对象添加到gui 中。
function makeAxesGrid(obj: Object3D, label: string) {
const helper = new AxesGridHelper(obj)
gui.add(helper, "visible").name(label)
}
- obj 坐标系对象
- label 控制器的标签名
5.基于3个坐标系,建立三个辅助对象和控制器。
makeAxesGrid(solarSystem, "solarSystem")
makeAxesGrid(earthSystem, "earthSystem")
makeAxesGrid(moonSystem, "moonSystem")
最终效果如下: