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

news2025/1/12 0:52:28

准备

首先我们需要两个模型,一个是场景模型,另一个是人物模型。
人物模型我这里用的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>


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/679870.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C++ STL vector容器用法

文章目录 1 vector初始化方法2 vector容器迭代器3 data()函数4 emplace_back()和push_back()的区别5 insert()函数6 vector删除元素参考 1 vector初始化方法 方式1&#xff1a; std::vector<double> values;//创建空的vcetor values.reserve(20); //设置容器的内存分配…

【实战】 JWT、用户认证与异步请求(1) —— React17+React Hook+TS4 最佳实践,仿 Jira 企业级项目(四)

文章目录 一、项目起航&#xff1a;项目初始化与配置二、React 与 Hook 应用&#xff1a;实现项目列表三、TS 应用&#xff1a;JS神助攻 - 强类型四、JWT、用户认证与异步请求1.login2.middleware of json-server3.jira-dev-tool&#xff08;imooc-jira-tool&#xff09;安装问…

《流浪地球 2》的硬核黑科技

电影中&#xff0c;由刘德华饰演的量子计算机工程师图恒宇有一个惊心动魄的情节。为了同步启动全球地球发动机&#xff0c;需要重启互联网&#xff0c;避免地壳破碎和地质灾害。而重启互联网的关键则是要启动“根服务器”。电影中没有具体交代是什么根服务器&#xff0c;但是当…

Ubuntu18.04屏幕分辨率问题

本篇博客最早发布于实验室公共博客&#xff0c;但已无人维护&#xff0c;现迁移至个人博客 起因 本来昨天还好好的&#xff0c;过了一夜&#xff0c;就变了&#xff0c;像极了咳咳(自行脑补) redwallbot-2小车上固定的屏幕&#xff0c;屏幕分辨率本来应该是1920x1080的&#…

DevOps系列文章之 linux安装ftp

第一步 1、用root 进入系统 2、使用命令 rpm -qa|grep vsftpd 查看系统是否安装了ftp&#xff0c;若安装了vsftp&#xff0c;使用这个命令会在屏幕上显示vsftpd的版本 3、使用命令rpm -e vsftpd 即可卸载ftp 4、再使用rpm -qa|grep vsftpd 查看系统是否已删除ftp&#xff0…

【小技巧】vscode 在 JS 文件中补全 HTML标签

文章目录 vscode中有很多插件可以支持 HTML 标签自动补全&#xff0c;在.vue和.html文件中都没有问题&#xff0c;但是在使用react时&#xff0c;HTML标签是写在js或者是ts文件中&#xff0c;插件就不起作用了 解决方案&#xff1a; 在设置中插入这段设置代码 "emmet.i…

uniapp 微信小程序sourcemap映射

uniapp 微信小程序sourcemap映射 错误捕获 由于微信小程序中没有window对象&#xff0c;不能通过window.onerror和window.onunhandledRejection方法进行全局的监听。不过我们也可以使用以下几种方法。 使用try…catch 将可能出现的错误的代码使用try...catch包裹 try{cont…

【C++】一文读懂C++中的异常处理机制

文章目录 C 中的异常处理机制1.1 什么是异常&#xff1f;1.2 调用abort()1.3 返回错误码1.4 异常机制1.5 将对象用作异常类型1.6 异常规范和C111.7 栈解退1.7.1 return和throw的区别1.7.2 什么是栈解退 1.8 其他异常特性1.9 excepyion类1.9.1 stdexcept异常类1.9.2 bad_alloc异…

329款超有设计感的英文字体合集

一组超有设计感的英文字体合集&#xff0c;总共329个字库包含多种字体风格&#xff1a;手写字体、签名字体、复古字体、笔刷字体、漫画字体等无衬线字体。适用于签名、文具、标志、排版引言、杂志或书籍封面。素材获取&#xff1a;取括号内容&#xff0c;&#xff08;scwan&…

【 openGauss数据库】--运维指南01

【 openGauss数据库】--运维指南01 &#x1f53b; 一、 openGauss数据库运维指南&#x1f530; 1.1 启停openGauss&#x1f530; 1.2 查看openGauss数据库状态 &#x1f53b; 二、 维护检查项&#x1f530; 2.1 检查实例状态&#x1f530; 2.2 检查锁信息&#x1f530; 2.3 统计…

个人向非企业,基于目前主流图床的选购指南

1. 为什么需要搭建自己的图床 最近研究了一下国内外比较主流的图床与对象存储&#xff0c;因为个人写作更加偏向于使用Markdown&#xff0c;而国内很多平台如掘金&#xff0c;简书&#xff0c;csdn等等网站都做了相关的防盗链&#xff0c;即使是我为作者本人&#xff0c;想取用…

C语言里面那些你必须知道的常用关键字(详细讲解)

前言 哈喽&#xff0c;各位铁汁们好啊&#xff01;✨今天来给大家带来的是C语言中我们常用的关键字静态static的详细讲解和typedef 、#define定义常量和宏。   既然是详解想必大家必定是想学一些平常学不到的东西吧&#xff01;这里博主给大家详细讲解static修饰的变量在内存…

2023最全的Java架构师面试120题解析(MySQL/Redis/架构/高并发等)

最全架构师题目将包含如下技术范围&#xff1a; 1.Java基础和高级: 集合框架: List&#xff1a;ArrayList、LinkedList&#xff1b;Set&#xff1a;HashSet、TreeSet Map:TreeMap/ConcurrentHashMap&#xff1b;Queue:ConcurrentLinkedQueue等 泛型、反射、并发编程、JVM、A…

基于Smb协议实现网络文件传输(Golang)

在前面章节已经展示了一些关于SMB的基本介绍&#xff0c;以及对应SMB相关操作的Java实现&#xff0c;这一章主要是前一章的补充&#xff0c;使用Golang来对 SMB共享文件夹进行操作。如果没有阅读过上一章节的同学&#xff0c;请跳转到 基于Smb协议实现网络文件传输&#xff0c;…

Axure教程—折叠面手风琴效果

上文中介绍了用Axure制作折叠面板的基础制作&#xff0c;这次介绍折叠面板手机风琴效果 效果 预览地址&#xff1a;https://e18rf6.axshare.com 功能 点击标题展开内容&#xff0c;点击另一标题&#xff0c;其展开的内容折叠 制作 拖入四个动态面板&#xff0c;分别命名为1、…

PHP流程控制与文件包含:基础与关键要点

目录 PHP流程控制 顺序结构&#xff1a; 分支结构&#xff1a; Switch分支&#xff1a; PHP循环结构 for循环 while循环 do-while循环 while和do-while的区别&#xff1a; 循环控制 流程控制代替语法 PHP文件包含 PHP文件包含的作用 PHP文件包含的四种形式 PHP文…

【spring cloud学习】3、Eureka Server注册中心

Eureka本身是Netflix开源的一款注册中心产品&#xff0c;并且Spring Cloud提供了相应的集成封装。选择Eureka作为注册中心实例来讲解是出于以下原因&#xff1a; &#xff08;1&#xff09;Eureka在业界的应用十分广泛&#xff0c;整个框架经受住了Netflix严酷生产环境的考验。…

Qt中的信号和信号槽(一)

目录 1. 信号和槽概述 信号和槽的关系 2. 标准信号槽使用 标准信号/槽 示例&#xff1a; 3. 自定义信号槽使用 自定义信号 自定义槽 示例&#xff1a; 1. 信号和槽概述 信号和槽是一种事件驱动的通信机制&#xff0c;广泛应用于Qt框架的事件处理、GUI编程、网络通信等…

如何在教育与科研领域使用ChatGPT

ChatGPT提示是您给予ChatGPT的一系列指示&#xff0c;以便它能够按需生成结果。由于ChatGPT是一种会话型人工智能&#xff0c;因此它需要明确的指示才能生成准确的结果。 ChatGPT提示的结构通常是以指令格式呈现的。它看起来像是您在与AI交流&#xff0c;给予它执行特定任务的…

基于树莓派4B的OpenCV安装与简单应用(真速通版)

前言&#xff1a;本文为手把手教学树莓派4B的OpenCV安装与简单应用&#xff08;真速通版本&#xff09;&#xff0c;树莓派4B最为目前最新款的树莓派家族一员深受创客和开发者喜爱。树莓派4B作为一款搭载 Cortex-A72 系列芯片的板载电脑&#xff0c;其不仅可以作为简单的 MCU 进…