Vue+Threejs实现人物奔跑场景

0 阅读3分钟

实现效果说明

  1. 调用人物模型内置跑步骨骼动画,循环原地播放跑动动作。
  2. 巨型草坪平面持续向后平移,视觉上实现人物向前无限奔跑。
  3. 场景添加线性白雾,远处草地逐步雾化,消除平面硬边界,提升空间纵深感。
  4. 完整的窗口自适应、光照阴影、纹理平铺、内存释放全套工程化代码。

演示动态图效果如下:

run.gif

完整代码

1. template 结构:

<template>
  <div class="three-container" ref="content" id="three"></div>
</template>

2. script 代码:

<script>
import * as THREE from 'three'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'

export default {
  beforeDestroy() {
    // 组件销毁前释放ThreeJS资源的方法,防止内存泄漏
    this.leaveDestory()
  },
  mounted() {
    this.$nextTick(() => {
      // 初始化方法
      this.init()
      // 监听浏览器窗口尺寸变化,自适应画布大小
      window.onresize = () => {
        this.resize()
      }
    })
  },
  data() {
    return {
      isLoading: false
    }
  },
  methods: {
    // 资源销毁函数:销毁动画帧、几何体、材质、渲染器等,避免页面反复挂载造成内存溢出
    leaveDestory() {
      // 取消动画帧请求,停止渲染循环
      if (window.myMar) {
        cancelAnimationFrame(window.myMar)
        window.myMar = null
      }
      if (window.scene) {
	// 遍历场景所有网格,释放几何体、材质显存资源
        window.scene.traverse(function (v) {
            if (v.type === 'Mesh') {
                if (v.geometry && v.geometry.dispose) {
                    v.geometry.dispose()
                }
                if (v.material && v.material.dispose) {
                    v.material.dispose()
                }
            }
        })
        if (window.scene.children.length > 0) {
          window.scene.remove(window.scene.children[0])
        }
        window.renderer.dispose()  // 释放渲染器资源
        window.renderer.forceContextLoss()  // 强制丢失WebGL上下文,彻底释放显存
        window.renderer.domElement = null
        window.renderer = null
        window.camera = null
        window.clock = null
        if (window.mixer) {
          this.stopAnimation()
        }
        window.gltf = null
        window.model = null
        window.ground = null
        window.onresize = null
        window.scene.clear()
        window.scene = null
      }
    },
    // 停止人物跑步动画的方法
    stopAnimation() {
      if (window.mixer) {
        window.animationAction.stop()  // 停止动画片段播放
        window.mixer = null
        window.animationAction = null
        window.ground.position.z = 0  // 重置地面位置
      }
    },
    // 启动模型自带跑步动画的方法
    startAnimation() {
      // 创建动画混合器,绑定人物模型,用来解析模型内置骨骼动画
      window.mixer = new THREE.AnimationMixer(window.model)
      // 获取模型第一个动画片段(跑步动画)
      window.animationAction = window.mixer.clipAction(window.gltf.animations[0])
      // 开启循环播放跑步动画
      window.animationAction.play()
      // 启动渲染循环的方法
      this.animate()
    },
    addGround() {
      let textureLoader = new THREE.TextureLoader()
      textureLoader.load('images/grasslight-big.jpg', (texture) => {
        // 创建超大平面几何体,长宽16000,预留足够移动空间
        const gg = new THREE.PlaneGeometry(16000, 16000)
        const gm = new THREE.MeshPhongMaterial({ color: 0xffffff, map: texture })
        window.ground = new THREE.Mesh(gg, gm)
        // 平面绕x轴旋转-90度,从竖直状态放平作为地面
        ground.rotation.x = -Math.PI / 2
        // 纹理横向、纵向重复64次,实现细密草地效果
        ground.material.map.repeat.set(64, 64)
        // 设置纹理超出范围后重复平铺
        ground.material.map.wrapS = THREE.RepeatWrapping
        ground.material.map.wrapT = THREE.RepeatWrapping
        // 设置纹理色彩空间,保证颜色显示正常
        ground.material.map.colorSpace = THREE.SRGBColorSpace
        // 地面接收阴影
        ground.receiveShadow = true
        window.scene.add(window.ground)
      })
    },
    resize() {
      let width = this.$refs.content.offsetWidth
      let height = this.$refs.content.offsetHeight
      window.camera.aspect = width / height
      // 更新相机投影矩阵
      window.camera.updateProjectionMatrix()
      window.renderer.setSize(width, height)
    },
    async init() {
      let width = this.$refs.content.offsetWidth
      let height = this.$refs.content.offsetHeight
      // 创建三维场景容器
      window.scene = new THREE.Scene()
      // 创建透视相机,45度视场角
      window.camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 1000)
      // 初始化WebGL渲染器,开启抗锯齿
      window.renderer = new THREE.WebGLRenderer({ antialias: true })
      // 适配设备像素比,解决高清屏模糊
      window.renderer.setPixelRatio(window.devicePixelRatio)
      // 画布背景设为白色
      window.renderer.setClearColor(0xffffff, 1.0)
      window.renderer.setSize(width, height)
      // 开启阴影
      window.renderer.shadowMapEnabled = true
			
      // 设置相机位置,看向场景原点
      window.camera.position.x = -72.93
      window.camera.position.y = 126.06
      window.camera.position.z = 323.85
      window.camera.lookAt(window.scene.position)
			
      // 添加半球环境光,柔和照亮整体场景
      let hemiLight = new THREE.HemisphereLight(0xffffff, 0xffffff, 0.3)
      hemiLight.position.set(0, 500, 0)
      window.scene.add(hemiLight)
			
      // 添加聚光灯,产生阴影,增强立体感
      let spotLight = new THREE.SpotLight(0xffffff)
      spotLight.position.set(-40, 650, -10)
      spotLight.castShadow = true
      window.scene.add(spotLight)
			
      // 计时器,用来获取帧间隔、驱动骨骼动画更新
      window.clock = new THREE.Clock()
      // 创建草地地面的方法
      this.addGround()
      // 添加场景白雾,近实远虚,提升画面真实感,对应效果图雾气效果
      window.scene.fog = new THREE.Fog(0xffffff, 280, 1050)
      // 将渲染画布挂载到DOM容器中
      document.getElementById('three').appendChild(window.renderer.domElement)
      // 调用加载跑步人物模型的方法
      this.addModel()
    },
    // 加载跑步人物模型的方法
    addModel() {
      this.isLoading = true
      let loader = new GLTFLoader()
      loader.load('static/models/workman.gltf', (gltf) => {
        window.gltf = gltf
        window.model = gltf.scene
        // 缩放模型尺寸
        window.model.scale.set(0.55, 0.55, 0.55)
        // 调整模型y坐标,贴合地面
        window.model.position.y = -2
        window.scene.add(window.model)
        window.renderer.render(window.scene, window.camera)
        this.isLoading = false
        // 模型加载完毕启动跑步动画
        this.startAnimation();
      })
    },
    animate() {
      window.myMar = requestAnimationFrame(this.animate)
      // 更新骨骼动画状态
      if (window.mixer) {
        const delta = window.clock.getDelta()
        window.mixer.update(delta)
      }
      // 地面循环移动:z坐标持续减小向后偏移;到达阈值后重置位置,实现无限滚动
      if (window.ground.position.z <= -500) {
        window.ground.position.z = 0
      } else {
        window.ground.position.z = window.ground.position.z - 5
      }
      window.renderer.render(window.scene, window.camera)
    }
  }
}
</script>

3. css样式代码:

.three-container {
    width: 100%;
    height: 100%;
    position: relative;
    overflow: hidden;
}

三大核心实现原理拆解

1. 人物原地跑步:GLTF 骨骼动画驱动

  • 使用 GLTFLoader 加载模型,通过 THREE.AnimationMixer 动画混合器解析模型内置动画片段。
  • clipAction() 获取跑步动作,调用 play() 开启循环播放,人物坐标全程固定,视觉上原地跑动。

2. 无限滚动草地:相对运动视觉欺骗

核心思路:人物不动,地面持续向后移动,利用人眼视觉错觉模拟向前奔跑。

  • 创建超大尺寸平面作为地面,避免短距离移动出现边界。
  • 渲染循环中每帧修改地面 position.z 向后偏移。
  • 当地面偏移量达到阈值 -500 时,直接重置位置为 0,无缝循环,实现无限跑道。
  • 草地纹理使用 RepeatWrapping 重复平铺,保证大面积地面纹理不拉伸。

3. 场景雾化景深:线性雾增强真实感

通过 THREE.Fog 给场景添加线性白雾:

  • 参数说明:new THREE.Fog(雾颜色, 近裁剪距离, 远裁剪距离)
  • 距离小于 280:完全无雾,清晰显示近处草地。
  • 距离 280 ~ 1050:雾气逐渐变浓。
  • 距离超过 1050:完全被白雾覆盖,遮挡远处边界,弱化平面生硬边界,提升空间层次感。

模型说明:  本文演示所用的 3D 模型来源于网络,版权归原作者所有。本文仅做 Three.js 前端技术学习演示,并未对模型本身做任何修改。