Prepare
First we need two models, one is the scene model and the other is the character model.
The character model I use here is the model given in the Threejs official website, and the name is Xbot.glb
.
Of course, you can also go to this website to download sketchfab for character models . After downloading, add animation to the model using mixamo
to download the model animation.
- Enter your model first
- Choose the right model file format
Here, pay attention to the two ways to add animation to the model using Blander software. The difference in specific writing methods will be discussed later.
Method 1: Split each individual animation.
Method 2: Unify all animations used in a timestamp.
Load scene
<!-- 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>
After the scene is loaded, add the character model:
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);
}
);
Here you need to cancel the controller, delete the initial lens, and give the lens to the character model.
At this point, the model is all introduced.
Add video to the scene model
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;
}
});
Note: The reason why the video cannot be displayed may be that the problem of adding materials prevents the video from being displayed normally. We only need to set the uv here.
Regarding the problem of the video appearing upside down
In uv mode, select all models and rotate them to the appropriate angle.
Character walking effect
We have already given the camera to the character model before, and then we can use the keyboard to control the character to move forward.
Let’s talk about the two ways to use animation mentioned above.
1. Put all animations in one timestamp and animate themAnimationMixer
If you use the same timeline to load animations, you can use the animation mixerAnimationMixer
// 剪切人物动作
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();
Only the first 30 frames are obtained as standing animation, and the following ones are standing and walking animation.
2. Store each animation as an independent animation element
If you use separate animation names, get all animations
animation names directly
animations = gltf.animations;
console.log(animations)
Define a global variable to load animation effects
mixer = startAnimation(
playerMesh, // 就是gltf.scene
animations, // 动画数组
"idle" // animationName,这里是"idle"(站立)
);
Idea: The default action requires a standing person. When using keyboard control, you need to use the animation that comes with the model to make the model move. Here you need to use keyboard events andkeydown
keyup
encapsulated animation functions in js.
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
It is used to control that the long press event will only be triggered once before being released. Otherwise, the w
walking animation will be repeatedly triggered by pressing and holding
. Add a clock function to the animation function, in which clock.getDelta()
the method obtains the time interval between two frames. This method can directly update the mixture. device related time
let clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
if (mixer) {
mixer.update(clock.getDelta());
}
}
Rotate camera with mouse
window.addEventListener("mousemove", (e) => {
if (prePos) {
playerMesh.rotateY((prePos - e.clientX) * 0.01);
}
prePos = e.clientX;
});
Realization effect:
Complete code:
/*
* @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>