three.js图形树

1,124 阅读5分钟

前言

课件地址

github.com/buglas/thre…

课堂目标

  • 理解图形树的概念
  • 掌握图形树的用法

知识点

  • 图形树
  • 坐标轴辅助对象
  • 栅格辅助对象
  • dat.gui 调试工具

1-图形树的概念

图形树是three.js 的核心内容之一。

图形树的本质就是多坐标系的嵌套,之前我们在WebGL里说过世界坐标系和本地坐标系的概念,若大家理解了这个概念,也就理解了图形树。

img

图形树中的每个节点,都有一个独立的坐标系,因此每个节点中的图形就有一个本地坐标位。若想知道这个图形在整个Scene 场景中的位置,那就需要将其本地坐标位转换为世界坐标位。

我们之前在WebGL里说相关知识的时候,举过一个“宇宙>太阳>地球>月球”的例子,接下来咱们继续那它说事。

2-宇宙示例

1

上图中,黄球是太阳,篮球是地球,灰球是月球。

地球绕太阳公转,月球绕地球旋转。

从月球的角度来看,它是在地球的“局部空间”中旋转的,它只考虑绕它在地球的本地坐标系内的旋转。尽管从太阳的角度来看,它相对于太阳的运动轨迹是一条螺旋形曲线。

这就好像生活在地球上的人不必考虑地球自身的自转,也不必考虑地球绕太阳的公转,大家想怎么走就怎么走,不需要去想地球的移动或旋转。

然而,即使你在地球上坐着不动,你仍然以大约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
    })
}

效果如下:

1

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)

效果如下:

1

我们在上面的代码里,针对太阳、地球和月球建立了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)
})

效果如下:

image-20220523085141600

接下来我们还可以通过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")

最终效果如下:

image-20220523102258840

参考链接:threejs.org/manual/#en/…