准备
首先我们需要两个模型,一个是场景模型,另一个是人物模型。
人物模型我这里用的Threejs官网中的给的模型,名称是Xbot.glb
。
当然人物模型也可以自己去这个网站下载sketchfab,下载后给模型添加动画mixamo
下载模型动画
- 先让入你的模型
- 选择正确的模型文件格式
这里注意一下用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中的键盘事件keydown
、keyup
封装动画函数
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>