- 图形树
- 坐标轴辅助对象
- 栅格辅助对象
- dat.gui 调试工具
1 图形树的概念
图形树是three.js 的核心内容之一。
图形树的本质就是多坐标系的嵌套,在WebGL里有世界坐标系和本地坐标系的概念,理解了这个概念,也就理解了图形树。
1-1 世界坐标和本地坐标
- 一个应用程序可能包含成百上千个单独的对象,我们必须把它们放到一个公共的场景里。
- 公共的场景就是世界坐标
- 相对于物体来说,物体本身的坐标就是本地坐标
- 相机默认在世界坐标系的原点
1-1-1 世界坐标
- 比如我们在threejs编辑器中,添加两个盒子,这两个盒子在场景中都有自己的position
- 场景就是世界坐标
- 两个盒子的位置是世界坐标系下的位置
1-1-2 本地坐标
- 还是上面那张图,我们每个盒子都有自己的坐标系,自己的坐标系就是本地坐标系。
1-2 three.js图形树
图形树中的每个节点,都有一个独立的坐标系,因此每个节点中的图形就有一个本地坐标位。若想知道这个图形在整个Scene 场景中的位置,那就需要将其本地坐标位转换为世界坐标位。
一个“宇宙>太阳>地球>月球”的例子。
2 宇宙示例——太阳坐标系和地球坐标系
上图中,黄球是太阳,篮球是地球,灰球是月球。
地球绕太阳公转,月球绕地球旋转。
从月球的角度来看,它是在地球的“局部空间”中旋转的,它只考虑绕它在地球的本地坐标系内的旋转。尽管从太阳的角度来看,它相对于太阳的运动轨迹是一条螺旋形曲线。
这就好像生活在地球上的人不必考虑地球自身的自转,也不必考虑地球绕太阳的公转,大家想怎么走就怎么走,不需要去想地球的移动或旋转。
然而,即使你在地球上坐着不动,你仍然以大约1000英里/小时的速度随地球自转,以大约67000英里/小时的速度围绕太阳公转。
接下来用代码模拟一下太阳、地球和月球间的运动关系。
2-1-绘制太阳、地球和月球
- 利用之前封装(渲染器、场景、相机、轨道控制器和响应式布局)的Stage对象搭建一个场景。
const stage = new Stage();
const { scene, renderer,camera } = stage;
- src/components/Stage.js
import { PerspectiveCamera, Scene, WebGLRenderer } from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
export default class Stage {
// 渲染器
// renderer: WebGLRenderer;
// // 场景
// scene: Scene;
// // 相机
// camera: PerspectiveCamera;
// // 轨道控制器
// controls: OrbitControls;
// // 渲染之前
// beforeRender = (time: number = 0) => {};
// 初始化场景
constructor(x= 0, y=0, z=12) {
this.scene = new Scene();
this.renderer = new WebGLRenderer({ antialias: true });
const { clientWidth, clientHeight } = this.renderer.domElement;
this.renderer.setSize(clientWidth * devicePixelRatio, clientHeight * devicePixelRatio, false);
this.camera = new PerspectiveCamera(45, clientWidth / clientHeight, 0.1, 1000);
this.camera.position.set(x, y, z);
this.camera.lookAt(0, 0, 0);
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
this.beforeRender = (time) => {console.log(time);}
}
// 响应式布局
responsive() {
const { renderer, camera } = this;
if (this.resizeRendererToDisplaySize(renderer)) {
const { clientWidth, clientHeight } = renderer.domElement;
camera.aspect = clientWidth / clientHeight;
camera.updateProjectionMatrix();
}
}
// 重置渲染尺寸
resizeRendererToDisplaySize(renderer) {
const { width, height, clientWidth, clientHeight } = renderer.domElement;
const [w, h] = [clientWidth * devicePixelRatio, clientHeight * devicePixelRatio];
const needResize = width !== w || height !== h;
if (needResize) {
renderer.setSize(w, h, false);
}
return needResize;
}
// 连续渲染
animate(time = 0) {
this.responsive();
this.beforeRender(time);
this.renderer.render(this.scene, this.camera);
requestAnimationFrame((time) => {
this.animate(time);
});
}
}
- 设置相机的视点位、目标点和上方向,使其变成俯视状态。
camera.position.set(0, 20, 0)
camera.up.set(0, 0, -1)
camera.lookAt(0, 0, 0)
- 默认相机的上方向是y方向的,但当相机俯视的时候,y方向就不合适了,所以将上方向设置为了-z方向。这就相当于低下头俯视裁剪空间。
- 设置完了上方向,别忘了用
lookAt()
方法设置相机的目标点,这个方法除了可以设置目标点,还可以更新相机的视图矩阵。
- 向场景中添加一个太阳。
// 太阳、地球和月亮都共用一个球体
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= [sunMesh]
MeshPhongMaterial 材质中的emissive 是一个自发光属性,这样它在没有光源的前提下,也能可见。
- 在场景中心添加一个点光源。
const color = 0xFFFFFF;
const intensity = 3;
const light = new PointLight(color, intensity);
scene.add(light);
- 在渲染之前,遍历objects 中的物体,使其转起来。
stage.beforeRender = (time = 0) => {
time *= 0.001
objects.forEach((obj) => {
obj.rotation.y = time
})
}
效果如下:
<template>
<div class="canvasWrapper"></div>
</template>
<script> /* eslint-disable */ import { Mesh, MeshPhongMaterial, SphereGeometry, PointLight } from 'three' // import * as THREE from 'three' import Stage from '@/components/Stage' export default { data() { return {} }, mounted() { this.init() }, methods: { init() { // 利用封装(渲染器、场景、相机、轨道控制器和响应式布局)的Stage对象搭建一个场景 const stage = new Stage() const { scene, renderer, camera } = stage // 设置相机的视点位、目标点和上方向,使其变成俯视状态 camera.position.set(0, 20, 0) // 将上方向设置为了-z方向 camera.up.set(0, 0, -1) // 设置相机的目标点 camera.lookAt(0, 0, 0) // 太阳、地球和月亮都共用一个球体 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 = [sunMesh] const color = 0xffffff const intensity = 3 const light = new PointLight(color, intensity) scene.add(light) stage.beforeRender = (time = 0) => { time *= 0.001 objects.forEach((obj) => { obj.rotation.y = time console.log(obj) }) } const current = document.querySelector('.canvasWrapper') if (current) { current.innerHTML = '' current.append(renderer.domElement) } // 渲染 stage.animate() } } } </script>
<style> .canvasWrapper { width: 100%; height: 100%; } .canvasWrapper canvas { width: 100% !important; height: 100%; } </style>
- 向场景中添加一个地球。
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 场景里,它和太阳各转各的。
接下来,改一下上面的代码,把地球放太阳系里,使其绕太阳旋转。
- 利用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)
console.log('太阳坐标系', solarSystem)
现在,太阳坐标系的原点与太阳对象的中心点是一致的,旋转太阳坐标系的时候会带动太阳的自转。与此同时,还会让地球绕太阳公转。
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)
console.log('太阳坐标系', solarSystem) // 包含太阳对象sunMesh和地球坐标系earthSystem
console.log('地球坐标系', earthSystem) // 地球对象earthMesh和月球坐标系moonSystem
console.log('月球坐标系', moonSystem) // 月球对象moonMesh
效果如下:
在上面的代码里,针对太阳、地球和月球建立了3个坐标系对象,这个坐标系对象是无法直接显示的,不过我们可以通过three.js 里的坐标轴辅助对象和栅格辅助对象将其显示出来。
2-2 添加辅助对象——为坐标系添加坐标轴和栅格
1.建立AxesGridHelper类,用于为坐标系添加坐标轴和栅格。
// src\components\AxesGridHelper.js
import { AxesHelper, GridHelper } from 'three'
export default class AxesGridHelper {
// grid: GridHelper
// axes: AxesHelper
// _visible: boolean = true
constructor(obj, size = 2, _visible = false) {
const axes = new AxesHelper()
const axesMat = axes.material
axesMat.depthTest = false
obj.add(axes)
const grid = new GridHelper(size)
const gridMat = grid.material
gridMat.depthTest = false
obj.add(grid)
this.grid = grid
this.axes = axes
// _visible默认值false
this._visible = _visible
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。
// import AxesGridHelper from '@/components/AxesGridHelper'
objects.forEach((obj) => {
new AxesGridHelper(obj)
})
效果如下:
接下来通过GUI工具控制辅助对象是否显示。
2-3 控制坐标系辅助对象是否显示——dat.gui
1.下载dat.gui
npm i dat.gui @types/dat.gui --save --registry=https://registry.npm.taobao.org
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 current = document.querySelector('.canvasWrapper')
if (current) {
current.innerHTML = ''
current.append(renderer.domElement)
current.append(gui.domElement)
// 渲染
stage.animate()
}
4.声明一个实例化AxisGridHelper 对象的方法,并将辅助对象添加到gui 中。
function makeAxesGrid(obj, label) {
const helper = new AxesGridHelper(obj)
gui.add(helper, "visible").name(label).onChange((e) => {
console.log('是否显示' + label, e)
// helper.visible = e
})
}
- obj 坐标系对象
- label 控制器的标签名
- 基于3个坐标系,建立三个辅助对象和控制器。
makeAxesGrid(solarSystem, "solarSystem")
makeAxesGrid(earthSystem, "earthSystem")
makeAxesGrid(moonSystem, "moonSystem")
最终效果如下:
<template>
<div class="canvasWrapper"></div>
</template>
<script> /* eslint-disable */ import { Mesh, MeshPhongMaterial, Group, SphereGeometry, PointLight } from 'three' import * as THREE from 'three' import Stage from '@/components/Stage' import { GUI } from 'dat.gui' import AxesGridHelper from '@/components/AxesGridHelper' export default { data() { return {} }, mounted() { this.init() }, methods: { init() { //dat.gui 控制坐标系辅助对象是否显示 const gui = new GUI({ autoPlace: false }) // 实例化AxisGridHelper对象, 并将辅助对象添加到gui 中 function makeAxesGrid(obj, label) { const helper = new AxesGridHelper(obj) gui .add(helper, 'visible') .name(label) .onChange((e) => { // 监听单选项改变事件 console.log('是否显示' + label, e) // helper.visible = e }) } // 利用封装(渲染器、场景、相机、轨道控制器和响应式布局)的Stage对象搭建一个场景 const stage = new Stage() const { scene, renderer, camera } = stage // 设置相机的视点位、目标点和上方向,使其变成俯视状态 camera.position.set(0, 20, 0) // 将上方向设置为了-z方向 camera.up.set(0, 0, -1) // 设置相机的目标点 camera.lookAt(0, 0, 0) // 太阳、地球和月亮都共用一个球体 const radius = 1 const widthSegments = 6 const heightSegments = 6 const sphereGeometry = new SphereGeometry( radius, widthSegments, heightSegments ) //需要旋转的对象集合 const objects = [] // 太阳坐标系 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) console.log('太阳坐标系', solarSystem) console.log('地球坐标系', earthSystem) console.log('月球坐标系', moonSystem) makeAxesGrid(solarSystem, 'solarSystem') makeAxesGrid(earthSystem, 'earthSystem') makeAxesGrid(moonSystem, 'moonSystem') // 在场景中心添加一个点光源 const color = 0xffffff const intensity = 3 const light = new PointLight(color, intensity) scene.add(light) // 在渲染之前,遍历objects 中的物体,使其转起来 stage.beforeRender = (time = 0) => { time *= 0.001 objects.forEach((obj) => { obj.rotation.y = time //console.log(obj) }) } // 以其中的太阳、地球、月球坐标系为参数实例化AxisGridHelper objects.forEach((obj) => { new AxesGridHelper(obj) }) const current = document.querySelector('.canvasWrapper') if (current) { current.innerHTML = '' current.append(gui.domElement) // 将GUI的DOM 元素添加到canvas 包裹器里 current.append(renderer.domElement) // 渲染 stage.animate() } } } } </script>
<style> .canvasWrapper { width: 100%; height: 100%; } .canvasWrapper canvas { width: 100% !important; height: 100%; } </style>
3-坦克案例——实现路径跟随和相机切换
巩固对图形树的理解,以及如何实现路径跟随和相机切换。
3-1- 分析案例的图形树
当前这个场景,其内部有许多图形嵌套,而且很多节点都是虚拟的Group对象。
Group对象就是方便模型管理和变换的。
这个场景的图形树中还嵌套着多个相机,里面有许多打镜头的套路。
3-2-代码实现
1.利用之前封装的Stage 对象快速搭建场景。
const stage = new Stage()
const { scene, renderer } = stage
//背景色
renderer.setClearColor(0xaaaaaa)
//开启投影
renderer.shadowMap.enabled = true
2.建立一个静止不动的主相机,把stage的相机变成这个主相机。
const camera = makeCamera()
camera.position.set(16, 8, 20)
camera.lookAt(0, 0, 0)
stage.camera = camera
- makeCamera() 是一个自定义的实例化透视相机的方法。
function makeCamera(fov = 40) {
const aspect = 2
const zNear = 0.1
const zFar = 1000
return new PerspectiveCamera(fov, aspect, zNear, zFar)
}
3.建立两道光。
// 平行光1
const light = new DirectionalLight(0xffffff, 1)
light.position.set(0, 20, 0)
scene.add(light)
light.castShadow = true
light.shadow.mapSize.width = 2048
light.shadow.mapSize.height = 2048
const d = 50
light.shadow.camera.left = -d
light.shadow.camera.right = d
light.shadow.camera.top = d
light.shadow.camera.bottom = -d
light.shadow.camera.near = 1
light.shadow.camera.far = 50
// 平行光2
const light2 = new DirectionalLight(0xffffff, 1)
light2.position.set(1, 2, 4)
scene.add(light2)
4.地面
const groundGeometry = new PlaneGeometry(50, 50)
const groundMaterial = new MeshPhongMaterial({ color: 0xcc8866 })
const groundMesh = new Mesh(groundGeometry, groundMaterial)
groundMesh.rotation.x = Math.PI * -0.5
groundMesh.receiveShadow = true
scene.add(groundMesh)
5.建立坦克Group,在其中放一个随坦克而动的相机。
// 坦克
const tank = new Group()
scene.add(tank)
// 坦克相机
const carRadius = 1
const bodyCenterY = (carRadius * 3) / 2
const tankCameraFov = 75
const tankCamera = makeCamera(tankCameraFov)
tankCamera.position.y = 5
tankCamera.position.z = -10
tankCamera.lookAt(0, bodyCenterY, 0)
tank.add(tankCamera)
6.建立坦克车身,放坦克Group里,这就是一个球。
const bodyGeometry = new SphereGeometry(carRadius)
const bodyMaterial = new MeshPhongMaterial({ color: 0x6688aa })
const bodyMesh = new Mesh(bodyGeometry, bodyMaterial)
bodyMesh.position.y = bodyCenterY
bodyMesh.castShadow = true
tank.add(bodyMesh)
7.建立两个车轱辘,放坦克Group里。
const wheelRadius = 0.6
const wheelThickness = 0.5
const wheelSegments = 8
const wheelGeometry = new CylinderGeometry(
wheelRadius,
wheelRadius,
wheelThickness,
wheelSegments
)
const wheelMaterial = new MeshPhongMaterial({ color: 0x888888 })
const cx = carRadius + wheelThickness / 2
const wheelMeshes = [-cx, cx].map((x) => {
const mesh = new Mesh(wheelGeometry, wheelMaterial)
mesh.rotation.z = Math.PI * 0.5
mesh.position.set(x, wheelRadius, 0)
mesh.castShadow = true
tank.add(mesh)
return mesh
})
8.建立炮筒Group,把炮筒模型和炮筒相机放进去,炮筒相机会随炮筒变换。
// 炮筒
const barrel = new Group()
barrel.position.y = bodyCenterY + 0.3
tank.add(barrel)
// 炮筒模型
const barrelSize = 0.3
const barrelLength = 5
const barrelGeometry = new BoxGeometry(barrelSize, barrelSize, barrelLength)
const barrelMesh = new Mesh(barrelGeometry, bodyMaterial)
barrelMesh.position.z = barrelLength / 2
barrelMesh.castShadow = true
barrel.add(barrelMesh)
// 炮管相机
const barrelCamera = makeCamera()
barrelCamera.position.y = 1.4
barrel.add(barrelCamera)
9.建立目标点Group,把目标点模型和目标相机放进去。
// 目标-负责目标的整体高度
const target = new Group()
target.position.z = 2
target.position.y = 4
scene.add(target)
// 浮动节点-负责目标的上下浮动
const targetBob = new Group()
target.add(targetBob)
// 目标模型
const targetGeometry = new SphereGeometry(0.5, 6, 3)
const targetMaterial = new MeshPhongMaterial({
color: 0x00ff00,
flatShading: true,
})
const targetMesh = new Mesh(targetGeometry, targetMaterial)
targetMesh.castShadow = true
targetBob.add(targetMesh)
// 目标相机
const targetCamera = makeCamera()
targetCamera.position.y = 1
targetCamera.position.z = -2
targetCamera.rotation.y = Math.PI
// 目标相机旋转轴-带动目标相机的旋转
const targetCameraPivot = new Group()
targetBob.add(targetCameraPivot)
targetCameraPivot.add(targetCamera)
之后在连续渲染的时候,目标点会通过targetBob 对象上下浮动。
targetBob.position.y = Math.sin(time * 2) * 2
在连续渲染方法里,目标相机会通过targetCameraPivot 对象进行旋转,看向坦克。
// 获取目标点的世界位
targetMesh.getWorldPosition(targetPosition)
// 炮筒指向目标点
barrel.lookAt(targetPosition)
重点解释一下,为什么不直接让目标相机targetCamera看向坦克,而是让一个包裹了相机的targetCameraPivot 看向坦克。
首先,想要的效果是让目标相机站在目标点后面看向坦克,这样既能看见一部分目标点,也能看见坦克,效果如下:
这样就需要把相机视点放在目标点后面:
const targetCamera = makeCamera()
//设置目标点位置
targetCamera.position.y = 1
targetCamera.position.z = -2
//让相机视线朝向z轴正方向,默认相机视线朝向-z
targetCamera.rotation.y = Math.PI
来个俯视的示意图:
接下来,我们若直接让相机看向坦克,那目标点就会移出相机视口:
所以就需要在相机外面再包裹一个Group对象,用Group对象的旋转,带动相机绕目标点旋转:
10.建立一条路径,之后会让坦克沿此路径移动。
//坦克移动路径
const curve = new SplineCurve([
new Vector2(-6, 5),
new Vector2(-6, -4),
new Vector2(8, 0),
new Vector2(-6, 12),
new Vector2(-6, 5),
])
const points = curve.getPoints(50)
const geometry = new BufferGeometry().setFromPoints(points)
const material = new LineBasicMaterial({ color: 0xff0000 })
const splineObject = new Line(geometry, material)
splineObject.rotation.x = Math.PI * 0.5
splineObject.position.y = 0.05
scene.add(splineObject)
11.提前声明好三个向量。
// 坦克位置
const tankPosition = new Vector2()
// 坦克朝向
const tankTarget = new Vector2()
// 目标位
const targetPosition = new Vector3()
- tankPosition和tankTarget之后会从curve路径中获取。
- targetPosition 是暂存目标对象的世界位的。
12.用GUI切换相机。
const gui = new GUI({ autoPlace: false })
const cameras= new Map([
["camera", camera],
["barrelCamera", barrelCamera],
["targetCamera", targetCamera],
["tankCamera", tankCamera],
])
const curCamera = { name: "camera" }
gui.add(curCamera, "name", [...cameras.keys()]).onChange((key: string) => {
const {
domElement: { clientWidth, clientHeight },
} = renderer
const cam = cameras.get(key)
if (cam) {
stage.camera = cam
stage.camera.aspect = clientWidth / clientHeight
stage.camera.updateProjectionMatrix()
}
})
13.在连续渲染的时候,让坦克、目标对象和相机动起来。
// 渲染之前
stage.beforeRender = (time = 0) => {
time *= 0.001
// 坦克移动插值
const tankTime = time * 0.1
// 坦克位置
curve.getPointAt(tankTime % 1, tankPosition)
// 坦克朝向
curve.getPointAt((tankTime + 0.01) % 1, tankTarget)
// 设置坦克位置
tank.position.set(tankPosition.x, 0, tankPosition.y)
// 设置坦克朝向
tank.lookAt(tankTarget.x, 0, tankTarget.y)
// 车轱辘的滚动
wheelMeshes.forEach((obj) => {
obj.rotation.x = time * 3
})
// 目标对象的上下浮动
targetBob.position.y = Math.sin(time * 2) * 2
// 获取目标点的世界位
targetMesh.getWorldPosition(targetPosition)
// 炮筒指向目标点
barrel.lookAt(targetPosition)
if (curCamera.name === "barrelCamera") {
// 炮筒相机看向目标点
barrelCamera.lookAt(targetPosition)
} else if (curCamera.name === "targetCamera") {
// 目标相机看向坦克
tank.getWorldPosition(targetPosition)
targetCameraPivot.lookAt(targetPosition)
}
}
上面的坦克在做移动的时候,是基于一个取值范围在[0,1]间的插值 ,用curve 的getPointAt() 方法获取的点位。
3-3 效果展示
<template>
<div class="canvasWrapper"></div>
</template>
<script>
/* eslint-disable */
import {
Mesh,
MeshPhongMaterial,
LineBasicMaterial,
Group,
PerspectiveCamera,
PlaneGeometry,
SphereGeometry,
BoxGeometry,
CylinderGeometry,
DirectionalLight,
SplineCurve,
Vector2,
Vector3,
Line,
BufferGeometry
} from 'three'
// import * as THREE from 'three'
// Stage.js文件 上边示例里有
import Stage from '@/components/Stage'
import { GUI } from 'dat.gui'
// import AxesGridHelper from '@/components/AxesGridHelper'
export default {
data() {
return {}
},
mounted() {
this.init()
},
methods: {
init() {
// 利用封装(渲染器、场景、相机、轨道控制器和响应式布局)的Stage对象搭建一个场景
const stage = new Stage()
const { scene, renderer } = stage
//背景色
renderer.setClearColor(0xaaaaaa)
//开启投影
renderer.shadowMap.enabled = true
// 建立一个静止不动的主相机
const camera = makeCamera()
camera.position.set(16, 8, 20)
camera.lookAt(0, 0, 0)
stage.camera = camera
// 一个自定义的实例化透视相机的方法
function makeCamera(fov = 40) {
const aspect = 2
const zNear = 0.1
const zFar = 1000
return new PerspectiveCamera(fov, aspect, zNear, zFar)
}
// 平行光1
const light = new DirectionalLight(0xffffff, 1)
light.position.set(0, 20, 0)
scene.add(light)
light.castShadow = true
light.shadow.mapSize.width = 2048
light.shadow.mapSize.height = 2048
const d = 50
light.shadow.camera.left = -d
light.shadow.camera.right = d
light.shadow.camera.top = d
light.shadow.camera.bottom = -d
light.shadow.camera.near = 1
light.shadow.camera.far = 50
// 平行光2
const light2 = new DirectionalLight(0xffffff, 1)
light2.position.set(1, 2, 4)
scene.add(light2)
// 地面
const groundGeometry = new PlaneGeometry(50, 50)
const groundMaterial = new MeshPhongMaterial({ color: 0xcc8866 })
const groundMesh = new Mesh(groundGeometry, groundMaterial)
groundMesh.rotation.x = Math.PI * -0.5
groundMesh.receiveShadow = true
scene.add(groundMesh)
// 坦克
const tank = new Group()
scene.add(tank)
// 坦克相机
const carRadius = 1
const bodyCenterY = (carRadius * 3) / 2
const tankCameraFov = 75
const tankCamera = makeCamera(tankCameraFov)
tankCamera.position.y = 5
tankCamera.position.z = -10
tankCamera.lookAt(0, bodyCenterY, 0)
tank.add(tankCamera)
// 坦克车身
const bodyGeometry = new SphereGeometry(carRadius)
const bodyMaterial = new MeshPhongMaterial({ color: 0x6688aa })
const bodyMesh = new Mesh(bodyGeometry, bodyMaterial)
bodyMesh.position.y = bodyCenterY
bodyMesh.castShadow = true
tank.add(bodyMesh)
// 两个车轱辘
const wheelRadius = 0.6
const wheelThickness = 0.5
const wheelSegments = 8
const wheelGeometry = new CylinderGeometry(
wheelRadius,
wheelRadius,
wheelThickness,
wheelSegments
)
const wheelMaterial = new MeshPhongMaterial({ color: 0x888888 })
const cx = carRadius + wheelThickness / 2
const wheelMeshes = [-cx, cx].map((x) => {
const mesh = new Mesh(wheelGeometry, wheelMaterial)
mesh.rotation.z = Math.PI * 0.5
mesh.position.set(x, wheelRadius, 0)
mesh.castShadow = true
tank.add(mesh)
return mesh
})
// 炮筒
const barrel = new Group()
barrel.position.y = bodyCenterY + 0.3
tank.add(barrel)
// 炮筒模型
const barrelSize = 0.3
const barrelLength = 5
const barrelGeometry = new BoxGeometry(
barrelSize,
barrelSize,
barrelLength
)
const barrelMesh = new Mesh(barrelGeometry, bodyMaterial)
barrelMesh.position.z = barrelLength / 2
barrelMesh.castShadow = true
barrel.add(barrelMesh)
// 炮管相机
const barrelCamera = makeCamera()
barrelCamera.position.y = 1.4
barrel.add(barrelCamera)
// 目标-负责目标的整体高度
const target = new Group()
target.position.z = 2
target.position.y = 4
scene.add(target)
// 浮动节点-负责目标的上下浮动
const targetBob = new Group()
target.add(targetBob)
// 目标模型
const targetGeometry = new SphereGeometry(0.5, 6, 3)
const targetMaterial = new MeshPhongMaterial({
color: 0x00ff00,
flatShading: true
})
const targetMesh = new Mesh(targetGeometry, targetMaterial)
targetMesh.castShadow = true
targetBob.add(targetMesh)
// 目标相机
const targetCamera = makeCamera()
targetCamera.position.y = 1
targetCamera.position.z = -2
targetCamera.rotation.y = Math.PI
// 目标相机旋转轴-带动目标相机的旋转
const targetCameraPivot = new Group()
targetBob.add(targetCameraPivot)
targetCameraPivot.add(targetCamera)
// 建立一条路径,之后会让坦克沿此路径移动
const curve = new SplineCurve([
new Vector2(-6, 5),
new Vector2(-6, -4),
new Vector2(8, 0),
new Vector2(-6, 12),
new Vector2(-6, 5)
])
const points = curve.getPoints(50)
const geometry = new BufferGeometry().setFromPoints(points)
const material = new LineBasicMaterial({ color: 0xff0000 })
const splineObject = new Line(geometry, material)
splineObject.rotation.x = Math.PI * 0.5
splineObject.position.y = 0.05
scene.add(splineObject)
// 坦克位置
const tankPosition = new Vector2()
// 坦克朝向
const tankTarget = new Vector2()
// 目标位
const targetPosition = new Vector3()
// 用dat.gui切换相机
const gui = new GUI({ autoPlace: false })
const cameras = new Map([
['camera', camera],
['barrelCamera', barrelCamera],
['targetCamera', targetCamera],
['tankCamera', tankCamera]
])
const curCamera = { name: 'camera' }
gui.add(curCamera, 'name', [...cameras.keys()]).onChange((key) => {
const {
domElement: { clientWidth, clientHeight }
} = renderer
const cam = cameras.get(key)
if (cam) {
stage.camera = cam
stage.camera.aspect = clientWidth / clientHeight
stage.camera.updateProjectionMatrix()
}
})
// 渲染之前
stage.beforeRender = (time = 0) => {
time *= 0.001
// 坦克移动插值
const tankTime = time * 0.1
// 坦克位置
curve.getPointAt(tankTime % 1, tankPosition)
// 坦克朝向
curve.getPointAt((tankTime + 0.01) % 1, tankTarget)
// 设置坦克位置
tank.position.set(tankPosition.x, 0, tankPosition.y)
// 设置坦克朝向
tank.lookAt(tankTarget.x, 0, tankTarget.y)
// 车轱辘的滚动
wheelMeshes.forEach((obj) => {
obj.rotation.x = time * 3
})
// 目标对象的上下浮动
targetBob.position.y = Math.sin(time * 2) * 2
// 获取目标点的世界位
targetMesh.getWorldPosition(targetPosition)
// 炮筒指向目标点
barrel.lookAt(targetPosition)
if (curCamera.name === 'barrelCamera') {
// 炮筒相机看向目标点
barrelCamera.lookAt(targetPosition)
} else if (curCamera.name === 'targetCamera') {
// 目标相机看向坦克
tank.getWorldPosition(targetPosition)
targetCameraPivot.lookAt(targetPosition)
}
}
const current = document.querySelector('.canvasWrapper')
if (current) {
current.innerHTML = ''
current.append(gui.domElement)
current.append(renderer.domElement)
// 渲染
stage.animate()
}
}
}
}
</script>
<style>
.canvasWrapper {
width: 100%;
height: 100%;
}
.canvasWrapper canvas {
width: 100% !important;
height: 100%;
}
</style>
今天的文章7. Three.js图形树、宇宙案例和坦克案例分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/14504.html