ThreeJS案例一——在场景中添加视频,使用人物动作以及用键盘控制在场景中行走的动画

准备

首先我们需要两个模型,一个是场景模型,另一个是人物模型。
人物模型我这里用的Threejs官网中的给的模型,名称是Xbot.glb
请添加图片描述

当然人物模型也可以自己去这个网站下载sketchfab,下载后给模型添加动画mixamo
下载模型动画

  1. 先让入你的模型

请添加图片描述

  1. 选择正确的模型文件格式

请添加图片描述

这里注意一下用Blander软件给模型添加动画的两种方式,具体写法的区别后面会说到

方式一:把每个单独的动画拆分出来
方式二:将所用到的动画统一放在一个时间戳中

加载场景

<!-- author: Mr.J -->
<!-- date: 2023-04-12 11:43:45 -->
<!-- description: Vue3+JS代码块模板 -->
<template>
  <div class="container" ref="container">
  </div>
</template>

<script setup>
import * as THREE from "three";
// 轨道
import {
      
       OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import {
      
       GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
import {
      
       ref, reactive, onMounted } from "vue";
// 三个必备的参数
let scene,
  camera,
  renderer,
  controls,

onMounted(() => {
      
      
  // 外层需要获取到dom元素以及浏览器宽高,来对画布设置长宽
  // clientWidth等同于container.value.clientWidth
  let container = document.querySelector(".container");
  const {
      
       clientWidth, clientHeight } = container;
  console.log(clientHeight);

  init();
  animate();
  // 首先需要获取场景,这里公共方法放在init函数中
  function init() {
      
      
    scene = new THREE.Scene();
    // 给相机设置一个背景
    scene.background = new THREE.Color(0.2, 0.2, 0.2);
    // 透视投影相机PerspectiveCamera
    // 支持的参数:fov, aspect, near, far
    camera = new THREE.PerspectiveCamera(
      75,
      clientWidth / clientHeight,
      0.01,
      100
    );
    // 相机坐标
    camera.position.set(10, 10, 10);
    // 相机观察目标
    camera.lookAt(scene.position);
    // 渲染器
    renderer = new THREE.WebGLRenderer();
    // 渲染多大的地方
    renderer.setSize(clientWidth, clientHeight);
    container.appendChild(renderer.domElement);
    controls = new OrbitControls(camera, renderer.domElement);
    // 环境光
    const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
    scene.add(ambientLight);

    // 方向光
    const directionLight = new THREE.DirectionalLight(0xffffff, 0.2);
    scene.add(directionLight);

    addBox();
  }

  function addBox() {
      
      
    new GLTFLoader().load(
      new URL(`../assets/changjing.glb`, import.meta.url).href,
      (gltf) => {
      
      
        scene.add(gltf.scene);
  }
  
  function animate() {
      
      
    requestAnimationFrame(animate);
    renderer.render(scene, camera);
    if (mixer) {
      
      
      mixer.update(clock.getDelta());
    }
  }
});
</script>

<style>
.container {
      
      
  width: 100%;
  height: 100vh;
  position: relative;
  z-index: 1;
}
</style>


场景加载完后再放入人物模型:

    new GLTFLoader().load(
      new URL(`../assets/Xbot.glb`, import.meta.url).href,
      (gltf) => {
    
    
        playerMesh = gltf.scene;
        scene.add(playerMesh);
        // 模型的位置
        playerMesh.position.set(13, 0.18, 0);
        // 模型初始面朝哪里的位置
        playerMesh.rotateY(-Math.PI / 2);
        // 镜头给到模型
        playerMesh.add(camera);
        // 相机初始位置
        camera.position.set(0, 2, -3);
        // 相机的位置在人物的后方,这样可以形成第三方视角
        camera.lookAt(new THREE.Vector3(0, 0, 1));
        // 给人物背后添加一个点光源,用来照亮万物
        const pointLight = new THREE.PointLight(0xffffff, 0.8);
        // 光源加载场景中
        scene.add(pointLight);
        // 在人物场景中添加这个点光源
        playerMesh.add(pointLight);
        // 设置点光源初始位置
        pointLight.position.set(0, 1.5, -2);
        console.log(gltf.animations);
      }
    );

这里需要将控制器给取消,并且将初始镜头删除,把镜头给到人物模型
到这里模型就全部引入完成

给场景模型中放入视频

        gltf.scene.traverse((child) => {
    
    
          console.log("name:", child.name);
          if (child.name == "电影幕布" || child.name == "曲面展屏" || child.name == "立方体" ) {
    
    
            const video = document.createElement("video");
            video.src = new URL(
              `../assets/4a9d0b86dedea8b4cd31ac59f44e841f.mp4`,
              import.meta.url
            ).href;
            video.muted = true;
            video.autoplay = "autoplay";
            video.loop = true;
            video.play();
            const videoTexture = new THREE.VideoTexture(video);
            const videoMaterial = new THREE.MeshBasicMaterial({
    
    
              map: videoTexture,
            });
            child.material = videoMaterial;
          }
          if (child.name == "2023"  || child.name == "支架") {
    
    
            const video = document.createElement("video");
            video.src = new URL(
              `../assets/c36c0c2d80c4084a519f608d969ae686.mp4`,
              import.meta.url
            ).href;
            video.muted = true;
            video.autoplay = "autoplay";
            video.loop = true;
            video.play();
            const videoTexture = new THREE.VideoTexture(video);
            const videoMaterial = new THREE.MeshBasicMaterial({
    
    
              map: videoTexture,
            });
            child.material = videoMaterial;
          }
        });

注意:视频无法显示的原因,可能是添加材质的问题导致视频无法正常展示,我们这里只要设置uv就可以了
请添加图片描述

请添加图片描述

请添加图片描述

关于视频出现倒过来的问题

uv模式下全选模型旋转合适的角度即可

人物行走效果

前面我们已经把镜头给到了人物模型中,接下来就可以用键盘控制人物进行前进。
这里说一下上面提到的的两种动画使用方式

1. 将所有的动画放在一个时间戳中设置动画AnimationMixer

如果用同一个时间线来加载动画,可以用到动画混合器AnimationMixer

  // 剪切人物动作
  playerMixer = new THREE.AnimationMixer(gltf.scene);

  const clipIdle = THREE.AnimationUtils.subclip(gltf.animations[0],'idle',0,30);
  actionIdle = playerMixer.clipAction(clipIdle);
  // actionWalk.play();

  const clipWalk = THREE.AnimationUtils.subclip(gltf.animations[0],'walk',31,281);
  actionWalk = playerMixer.clipAction(clipWalk);

  // 默认站立
  actionIdle.play();

只获取前30帧为站立动画,后面的为站行走动画

2. 将每个动画单独存储成一个独立的动画元素

如果用单独的动画名称,直接获取所有的animations动画名称

 animations = gltf.animations;
 console.log(animations)

请添加图片描述

定义一个全局变量用来加载动画效果

mixer = startAnimation(
  playerMesh, // 就是gltf.scene
  animations, // 动画数组
  "idle" // animationName,这里是"idle"(站立)
);

思路:默认的动作是需要一个站立,用键盘控制时需要让模型自带的动画让模型动起来
这里就需要用到js中的键盘事件keydownkeyup

封装动画函数


 function startAnimation(skinnedMesh, animations, animationName) {
    
    
    const m_mixer = new THREE.AnimationMixer(skinnedMesh);
    const clip = THREE.AnimationClip.findByName(animations, animationName);
    if (clip) {
    
    
      const action = m_mixer.clipAction(clip);
      action.play();
    }
    return m_mixer;
  }
  let isWalk = false;
  window.addEventListener("keydown", (e) => {
    
    
    // 前进
    if (e.key == "w") {
    
    
      playerMesh.translateZ(0.1);
      if (!isWalk) {
    
    
        console.log(e.key);
        isWalk = true;

        mixer = startAnimation(
          playerMesh,
          animations,
          "walk" // animationName,这里是"Run"
        );
      }
    }
  });
    window.addEventListener("keyup", (e) => {
    
    
    console.log(e.key);
    if (e.key == "w"  ) {
    
    
      isWalk = false;
      mixer = startAnimation(
        playerMesh,
        animations,
        "idle" // animationName,这里是"Run"
      );
 
    }
  });

isWalk是用来控制长按事件在没松开之前只会触发一次,否则按住w会一直重复触发行走动画
在动画函数中加一个clock函数,其中clock.getDelta()方法获得两帧的时间间隔,此方法可以直接更新混合器相关的时间

  let clock = new THREE.Clock();
  function animate() {
    
    
    requestAnimationFrame(animate);
    renderer.render(scene, camera);
    if (mixer) {
    
    
      mixer.update(clock.getDelta());
    }
  }

通过鼠标旋转镜头

  window.addEventListener("mousemove", (e) => {
    
    
    if (prePos) {
    
    
      playerMesh.rotateY((prePos - e.clientX) * 0.01);
    }
    prePos = e.clientX;
  });

实现效果:
请添加图片描述

完整代码:

/*
 * @Author: Southern Wind
 * @Date: 2023-06-24 
 * @Last Modified by: Mr.Jia
 * @Last Modified time: 2023-06-24 16:30:24
 */

<template>
  <div class="container" ref="container">
  </div>
</template>

<script setup>
import * as THREE from "three";
// 轨道控制器
import {
      
       OrbitControls } from "three/examples/jsm/controls/OrbitControls";
// GLTF加载
import {
      
       GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
import {
      
       ref, reactive, onMounted } from "vue";
// 全局变量
let scene, camera, renderer, playerMesh, prePos, mixer, animations;

onMounted(() => {
      
      
  // 外层需要获取到dom元素以及浏览器宽高,来对画布设置长宽
  // clientWidth等同于container.value.clientWidth
  let container = document.querySelector(".container");
  const {
      
       clientWidth, clientHeight } = container;
  console.log(clientHeight);

  init();
  animate();
  // 首先需要获取场景,这里公共方法放在init函数中
  function init() {
      
      
    scene = new THREE.Scene();
    // 给相机设置一个背景
    scene.background = new THREE.Color(0.2, 0.2, 0.2);
    // 透视投影相机PerspectiveCamera
    // 支持的参数:fov, aspect, near, far
    camera = new THREE.PerspectiveCamera(
      75,
      clientWidth / clientHeight,
      0.01,
      100
    );
    // 相机坐标
    // camera.position.set(10, 10, 10);
    // 相机观察目标
    camera.lookAt(scene.position);
    // 渲染器
    renderer = new THREE.WebGLRenderer();
    // 渲染多大的地方
    renderer.setSize(clientWidth, clientHeight);
    container.appendChild(renderer.domElement);
    // controls = new OrbitControls(camera, renderer.domElement);
    // 环境光
    const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
    scene.add(ambientLight);

    // 方向光
    const directionLight = new THREE.DirectionalLight(0xffffff, 0.2);
    scene.add(directionLight);
    addBox();
  }

  function addBox() {
      
      
    new GLTFLoader().load(
      new URL(`../assets/changjing.glb`, import.meta.url).href,
      (gltf) => {
      
      
        scene.add(gltf.scene);
        gltf.scene.traverse((child) => {
      
      
          console.log("name:", child.name);
          if (
            child.name == "电影幕布" ||
            child.name == "曲面展屏" ||
            child.name == "立方体"
          ) {
      
      
            const video = document.createElement("video");
            video.src = new URL(
              `../assets/4a9d0b86dedea8b4cd31ac59f44e841f.mp4`,
              import.meta.url
            ).href;
            video.muted = true;
            video.autoplay = "autoplay";
            video.loop = true;
            video.play();
            const videoTexture = new THREE.VideoTexture(video);
            const videoMaterial = new THREE.MeshBasicMaterial({
      
      
              map: videoTexture,
            });
            child.material = videoMaterial;
          }
          if (child.name == "2023" || child.name == "支架") {
      
      
            const video = document.createElement("video");
            video.src = new URL(
              `../assets/c36c0c2d80c4084a519f608d969ae686.mp4`,
              import.meta.url
            ).href;
            video.muted = true;
            video.autoplay = "autoplay";
            video.loop = true;
            video.play();
            const videoTexture = new THREE.VideoTexture(video);
            const videoMaterial = new THREE.MeshBasicMaterial({
      
      
              map: videoTexture,
            });
            child.material = videoMaterial;
          }
        });
      }
    );
    new GLTFLoader().load(
      new URL(`../assets/Xbot.glb`, import.meta.url).href,
      (gltf) => {
      
      
        playerMesh = gltf.scene;
        scene.add(playerMesh);
        playerMesh.position.set(13, 0.18, 0);
        playerMesh.rotateY(-Math.PI / 2);
        playerMesh.add(camera);
        camera.position.set(0, 2, -3);
        camera.lookAt(new THREE.Vector3(0, 0, 1));
        const pointLight = new THREE.PointLight(0xffffff, 0.8);
        scene.add(pointLight);
        playerMesh.add(pointLight);
        pointLight.position.set(0, 1.5, -2);
        console.log(gltf.animations);
        animations = gltf.animations;

        mixer = startAnimation(
          playerMesh,
          animations,
          "idle" // animationName,这里是"Run"
        );
      }
    );
  }
  let isWalk = false;
  window.addEventListener("keydown", (e) => {
      
      
    // 前进
    if (e.key == "w") {
      
      
      playerMesh.translateZ(0.1);
      if (!isWalk) {
      
      
        console.log(e.key);
        isWalk = true;

        mixer = startAnimation(
          playerMesh,
          animations,
          "walk" // animationName,这里是"Run"
        );
      }
    }
  });
  window.addEventListener("keydown", (e) => {
      
      
    // 后退
    if (e.key == "s") {
      
      
      playerMesh.translateZ(-0.1);

      if (!isWalk) {
      
      
        console.log(e.key);
        isWalk = true;

        mixer = startAnimation(
          playerMesh,
          animations,
          "walk" // animationName,这里是"Run"
        );
      }
    }
  });
  window.addEventListener("keydown", (e) => {
      
      
    // 左
    if (e.key == "a") {
      
      
      playerMesh.translateX(0.1);
      if (!isWalk) {
      
      
        console.log(e.key);
        isWalk = true;

        mixer = startAnimation(
          playerMesh,
          animations,
          "walk" // animationName,这里是"Run"
        );
      }
    }
  });
  window.addEventListener("keydown", (e) => {
      
      
    // 右
    if (e.key == "d") {
      
      
      playerMesh.translateX(-0.1);
      playerMesh.rotateY(-Math.PI / 32);
      if (!isWalk) {
      
      
        console.log(e.key);
        isWalk = true;

        mixer = startAnimation(
          playerMesh,
          animations,
          "walk" // animationName,这里是"Run"
        );
      }
    }
  });
  let clock = new THREE.Clock();
  function startAnimation(skinnedMesh, animations, animationName) {
      
      
    const m_mixer = new THREE.AnimationMixer(skinnedMesh);
    const clip = THREE.AnimationClip.findByName(animations, animationName);
    if (clip) {
      
      
      const action = m_mixer.clipAction(clip);
      action.play();
    }
    return m_mixer;
  }
  window.addEventListener("mousemove", (e) => {
      
      
    if (prePos) {
      
      
      playerMesh.rotateY((prePos - e.clientX) * 0.01);
    }
    prePos = e.clientX;
  });
  window.addEventListener("keyup", (e) => {
      
      
    console.log(e.key);
    if (e.key == "w" || e.key == "s" || e.key == "d" || e.key == "a") {
      
      
      isWalk = false;
      mixer = startAnimation(
        playerMesh,
        animations,
        "idle" // animationName,这里是"Run"
      );
    }
  });
  function animate() {
      
      
    requestAnimationFrame(animate);
    renderer.render(scene, camera);
    if (mixer) {
      
      
      mixer.update(clock.getDelta());
    }
  }
});
</script>

<style>
.container {
      
      
  width: 100%;
  height: 100vh;
  position: relative;
  z-index: 1;
}
</style>


猜你喜欢

转载自blog.csdn.net/nanchen_J/article/details/131363416
今日推荐