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

news/2024/11/29 13:50:53/

准备

首先我们需要两个模型,一个是场景模型,另一个是人物模型。
人物模型我这里用的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.clientWidthlet 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, farcamera = 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.sceneanimations, // 动画数组"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.clientWidthlet 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, farcamera = 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.ppmy.cn/news/551046.html

相关文章

【实战】 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;安装问…

MySQL生产环境高可用架构详解

一、MySQL高可用集群介绍 1、数据库主从架构与分库分表 随着现在互联网的应用越来越大&#xff0c;数据库会频繁的成为整个应用的性能瓶颈。而 我们经常使用的MySQL数据库&#xff0c;也会不断面临数据量太大、数据访问太频繁、数据 读写速度太快等一系列的问题。所以&#xf…

GO学习之入门语法

GO系列 1、GO学习之Hello World 2、GO学习之入门语法 文章目录 GO系列前言一&#xff1a;基础数据类型1.1 基本类型1.2 引用类型 二&#xff1a;基本操作2.1 if else2.2 for / range2.3 switch2.4 goto2.5 break / continue 三&#xff1a;总结 前言 最近新老大找我聊天&…

第三方库介绍——mosquitto

文章目录 概述程序&#xff08;指令&#xff09;说明安装服务端与客户端服务端指令配置配置文件&#xff1a;mosquitto.conf认证配置&#xff1a;pwfile权限配置&#xff1a;aclfile启动服务器&#xff0c;选择配置文件&#xff1a;mosquitto.conf 测试发布指令&#xff1a;订阅…

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

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

基于屏幕像素抖动的PCF

PCF无非就是把周围的像素加吧加吧, 然后取个平均值. 结果的平滑程度, 跟Kernel的大小有直接关系. 下面来对这个描过边的锯齿茶壶PCF一把: 2x2: 3x3: 4x4: 当然, Kernel越大, 效果越好. 但大到一定程度效果就不明显了, 而且还要考虑性能问题, 毕竟多次的纹理采样很慢. 其实呢, …

Godot屏幕抖动效果原理与实现

要用Godot实现屏幕/相机抖动效果&#xff0c;调查了网上的一些实现方案&#xff0c;效果都很不满意&#xff0c;于是自己实现了一个。 原理 从渲染过程看&#xff0c;实际发生抖动的是场景相机。 抖动过程对应物理学上的振动&#xff08;物理量在某个定值附近反复变化&#x…

游戏界面缩放后屏幕抖动的问题

最近解决了一个游戏界面缩放后屏幕抖动的问题&#xff0c;拿来与大家分享一下。 我们公司的游戏在界面缩放到75%、50%、40%、25%后会出现明显的画面抖动&#xff0c;最后近过同事们的协助和努力之后&#xff0c;明白了DDraw缩放的规律&#xff0c;大致上的过程应该是&#xff…