Three.js - 实现一辆坦克沿路径行驶(十二)

8,135 阅读5分钟

「这是我参与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)
      }

image.png

绘制坦克

  • 绘制多个几何体组合一个物体时,我们移动坦克的时候是所有的几何体都要移动。这里我们就要分析这个物体,那些部位是要一起变化的。把需要一起变化的几何体都放入局部空间中(子节点的变化都是在父节点变化后基础上变化的)。
  1. 创建坦克局部空间,几何体都放入这个局部空间。在全局空间中移动这个局部空间,就是移动整个坦克。
  const tank = new THREE.Object3D();
  scene.add(tank);
  1. 创建轮胎和底盘。
      // 创建底盘
      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
      })

image.png

  1. 添加局部相机到底盘上。它父节点是底盘,所以它的位移和旋转都是在底盘局部空间变化的。
      // 底盘局部相机
      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)

image.png

  1. 绘制坦克头
      // 坦克头
      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)

image.png

绘制目标

  • 目标是另一个在全局场景中的物体。绘制目标后我们把炮干指向目标。
// 目标
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)

image.png

绘制移动路径

      // 绘制移动路径
      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)

image.png

加入动画

  • 在循环渲染函数中,改变物体的位置使轮胎滚动就实现了坦克的移动。
  • 先把相机放入数组中,根据循环次数获取对应下标的相机渲染切换场景。也可以手动切换渲染相机。
  1. 移动坦克
    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)
        ...
    }

1.gif

  1. 切换场景。
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)
    ...
}

1.gif

总结

three.js中我们要灵活的使用Object3D等物体类节点,通过它将物体的移动、旋转、缩放,同步到子节点。就比如本节,如果不使用Object3D,那么我们需要计算每一个我几合体的全局坐标位移,这需要一些复杂的数学来实现,而使用了Object3D就减少了这一系列麻烦的计算。