「这是我参与2022首次更文挑战的第12天,活动详情查看:2022首次更文挑战」
简介
本节主要讲解,通过几何体组合成一个坦克形状的物体。让这个物体按照我们规定的线路移动,并且保持头部向前。添加多个相机,根据相机切换展示不同场景。
搭建基础
- 引入组件,实例化渲染器。
// 官网地址
import * as THREE from 'https://threejsfundamentals.org/threejs/resources/threejs/r132/build/three.module.js'
import { OrbitControls } from 'https://threejsfundamentals.org/threejs/resources/threejs/r132/examples/jsm/controls/OrbitControls.js'
const canvas = document.querySelector('#c2d')
// 渲染器
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true })
复制代码
- 实例化全局相机。本节会切换场景,创建相机实例化公用方法。
/**
* 创建相机公用方法
* */
function makeCamera(fov = 40) {
const aspect = 2 // the canvas default
const zNear = 0.1
const zFar = 1000
return new THREE.PerspectiveCamera(fov, aspect, zNear, zFar)
}
const camera = makeCamera()
// .multiplyScalar() 矩阵的每个元素乘以参数。
camera.position.set(8, 4, 10).multiplyScalar(3)
// 朝向
camera.lookAt(0, 0, 0)
// 控制相机
const controls = new OrbitControls(camera, canvas)
controls.update()
复制代码
- 实例化场景
// 场景
const scene = new THREE.Scene()
复制代码
- 初始化灯光
{
// 方向光
const light = new THREE.DirectionalLight(0xffffff, 1)
light.position.set(0, 20, 0)
scene.add(light)
}
{
// 方向光
const light = new THREE.DirectionalLight(0xffffff, 1)
light.position.set(1, 2, 4)
scene.add(light)
}
复制代码
- 渲染函数
function render(time) {
time *= 0.001
// 加载渲染器
renderer.render(scene, camera)
// 开始动画
requestAnimationFrame(render)
}
// 开始渲染
requestAnimationFrame(render)
复制代码
绘制物体
绘制地面
- 创建一个平面,旋转一下。
{
// 平面几何
const groundGeometry = new THREE.PlaneGeometry(50, 50)
const groundMaterial = new THREE.MeshPhongMaterial({ color: 0xcc8866 })
const groundMesh = new THREE.Mesh(groundGeometry, groundMaterial)
groundMesh.rotation.x = Math.PI * -0.5
scene.add(groundMesh)
}
复制代码
绘制坦克
- 绘制多个几何体组合一个物体时,我们移动坦克的时候是所有的几何体都要移动。这里我们就要分析这个物体,那些部位是要一起变化的。把需要一起变化的几何体都放入局部空间中(子节点的变化都是在父节点变化后基础上变化的)。
- 创建坦克局部空间,几何体都放入这个局部空间。在全局空间中移动这个局部空间,就是移动整个坦克。
const tank = new THREE.Object3D();
scene.add(tank);
复制代码
- 创建轮胎和底盘。
// 创建底盘
const carWidth = 4
const carHeight = 1
const carLength = 8
// 几何体
const bodyGeometry = new THREE.BoxGeometry(carWidth, carHeight, carLength)
const bodyMaterial = new THREE.MeshPhongMaterial({ color: 0x6688aa })
const bodyMesh = new THREE.Mesh(bodyGeometry, bodyMaterial)
bodyMesh.position.y = 1.4
tank.add(bodyMesh)
const wheelRadius = 1
const wheelThickness = 0.5
const wheelSegments = 36
// 圆柱体
const wheelGeometry = new THREE.CylinderGeometry(
wheelRadius, // 圆柱顶部圆的半径
wheelRadius, // 圆柱底部圆的半径
wheelThickness, // 高度
wheelSegments // X轴分成多少段
)
const wheelMaterial = new THREE.MeshPhongMaterial({ color: 0x888888 })
// 根据底盘 定位轮胎位置
const wheelPositions = [
[-carWidth / 2 - wheelThickness / 2, -carHeight / 2, carLength / 3],
[carWidth / 2 + wheelThickness / 2, -carHeight / 2, carLength / 3],
[-carWidth / 2 - wheelThickness / 2, -carHeight / 2, 0],
[carWidth / 2 + wheelThickness / 2, -carHeight / 2, 0],
[-carWidth / 2 - wheelThickness / 2, -carHeight / 2, -carLength / 3],
[carWidth / 2 + wheelThickness / 2, -carHeight / 2, -carLength / 3]
]
const wheelMeshes = wheelPositions.map((position) => {
const mesh = new THREE.Mesh(wheelGeometry, wheelMaterial)
mesh.position.set(...position)
mesh.rotation.z = Math.PI * 0.5
bodyMesh.add(mesh)
return mesh
})
复制代码
- 添加局部相机到底盘上。它父节点是底盘,所以它的位移和旋转都是在底盘局部空间变化的。
// 底盘局部相机
const tankCameraFov = 75
const tankCamera = makeCamera(tankCameraFov)
tankCamera.position.y = 3
tankCamera.position.z = -6
tankCamera.rotation.y = Math.PI
bodyMesh.add(tankCamera)
复制代码
// 暂时修改 渲染 底盘局部相机
// renderer.render(scene, camera)
renderer.render(scene, tankCamera)
复制代码
- 绘制坦克头
// 坦克头
const domeRadius = 2
const domeWidthSubdivisions = 12
const domeHeightSubdivisions = 12
const domePhiStart = 0
const domePhiEnd = Math.PI * 2
const domeThetaStart = 0
const domeThetaEnd = Math.PI * 0.5
const domeGeometry = new THREE.SphereGeometry(
domeRadius,
domeWidthSubdivisions,
domeHeightSubdivisions,
domePhiStart,
domePhiEnd,
domeThetaStart,
domeThetaEnd
)
const domeMesh = new THREE.Mesh(domeGeometry, bodyMaterial)
bodyMesh.add(domeMesh)
domeMesh.position.y = 0.5
// 炮干
const turretWidth = 0.5
const turretHeight = 0.5
const turretLength = 5
const turretGeometry = new THREE.BoxGeometry(turretWidth, turretHeight, turretLength)
const turretMesh = new THREE.Mesh(turretGeometry, bodyMaterial)
const turretPivot = new THREE.Object3D()
turretPivot.position.y = 0.5
turretMesh.position.z = turretLength * 0.5
turretPivot.add(turretMesh)
bodyMesh.add(turretPivot)
复制代码
绘制目标
- 目标是另一个在全局场景中的物体。绘制目标后我们把炮干指向目标。
// 目标
const targetGeometry = new THREE.SphereGeometry(0.5, 36, 36)
const targetMaterial = new THREE.MeshPhongMaterial({ color: 0x00ff00, flatShading: true })
const targetMesh = new THREE.Mesh(targetGeometry, targetMaterial)
const targetElevation = new THREE.Object3D()
const targetBob = new THREE.Object3D()
scene.add(targetElevation)
targetElevation.position.z = carLength * 2
targetElevation.position.y = 8
targetElevation.add(targetBob)
targetBob.add(targetMesh)
// 获取目标全局坐标
const targetPosition = new THREE.Vector3()
targetMesh.getWorldPosition(targetPosition)
// 炮台瞄准目标
turretPivot.lookAt(targetPosition)
// 目标上的相机
const targetCamera = makeCamera()
targetCamera.position.y = 1
targetCamera.position.z = -2
targetCamera.rotation.y = Math.PI
targetBob.add(targetCamera)
复制代码
绘制移动路径
- 使用
.SplineCurve()
创建一个平滑的二维样条曲线。
// 绘制移动路径
const curve = new THREE.SplineCurve([
new THREE.Vector2(-10, 20),
new THREE.Vector2(-5, 5),
new THREE.Vector2(0, 0),
new THREE.Vector2(5, -5),
new THREE.Vector2(10, 0),
new THREE.Vector2(5, 10),
new THREE.Vector2(-5, 10),
new THREE.Vector2(-10, -10),
new THREE.Vector2(-15, -8),
new THREE.Vector2(-10, 20)
])
const points = curve.getPoints(50)
const geometry = new THREE.BufferGeometry().setFromPoints(points)
const material = new THREE.LineBasicMaterial({ color: 0xff0000 })
const splineObject = new THREE.Line(geometry, material)
splineObject.rotation.x = Math.PI * 0.5
splineObject.position.y = 0.05
scene.add(splineObject)
复制代码
加入动画
- 在循环渲染函数中,改变物体的位置使轮胎滚动就实现了坦克的移动。
- 先把相机放入数组中,根据循环次数获取对应下标的相机渲染切换场景。也可以手动切换渲染相机。
- 移动坦克
const targetPosition2 = new THREE.Vector3()
const tankPosition = new THREE.Vector2()
const tankTarget = new THREE.Vector2()
function render(time) {
...
// 上下移动目标
targetBob.position.y = Math.sin(time * 2) * 4
targetMaterial.emissive.setHSL((time * 10) % 1, 1, 0.25)
targetMaterial.color.setHSL((time * 10) % 1, 1, 0.25)
// 获取目标全局坐标
targetMesh.getWorldPosition(targetPosition2)
// 炮台瞄准目标
turretPivot.lookAt(targetPosition2)
// 根据路线移动坦克
const tankTime = time * 0.05
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)
...
}
复制代码
- 切换场景。
const cameras = [
{ cam: camera, desc: '全局相机' },
{ cam: targetCamera, desc: '目标上的相机' },
{ cam: tankCamera, desc: '底盘 局部相机' }
]
function render(time) {
...
// 切换相机
const camera1 = cameras[time % cameras.length | 0]
// 获取坦克的 全局坐标
tank.getWorldPosition(targetPosition2)
// 看向坦克
targetCamera.lookAt(targetPosition2)
// 加载渲染器 tankCamera targetCamera
renderer.render(scene, camera1.cam)
...
}
复制代码
总结
在three.js
中我们要灵活的使用Object3D
等物体类节点,通过它将物体的移动、旋转、缩放,同步到子节点。就比如本节,如果不使用Object3D
,那么我们需要计算每一个我几合体的全局坐标位移,这需要一些复杂的数学来实现,而使用了Object3D
就减少了这一系列麻烦的计算。
扫描二维码关注公众号,回复:
13675727 查看本文章