切换动画不同动作(.weight)Threejs (15)
一、CSS2DRenderer(HTML标签)

引入扩展库CSS2DRenderer.js
model.js
javascript
// 引入CSS2模型对象CSS2DObject
import { CSS2DObject } from 'three/addons/renderers/CSS2DRenderer.js';
CSS2模型对象CSS2DObject
model.js
javascript
const div = document.getElementById('tag');
// HTML元素转化为threejs的CSS2模型对象
const tag = new CSS2DObject(div);
javascript
tag.position.set(50,0,50);
index.js
javascript
scene.add(tag);

CSS2渲染器CSS2DRenderer
javascript
// 引入CSS2渲染器CSS2DRenderer
import { CSS2DRenderer } from 'three/addons/renderers/CSS2DRenderer.js';
// 创建一个CSS2渲染器CSS2DRenderer
const css2Renderer = new CSS2DRenderer();
CSS2Renderer.render()渲染HTML标签
javascript
// 用法和webgl渲染器渲染方法类似
css2Renderer.render(scene, camera);
// renderer.render(scene, camera);
CSS2Renderer.setSize()
javascript
// width, height:canvas画布宽高度
css2Renderer.setSize(width, height);
渲染结果CSS2Renderer.domElement
javascript
document.body.appendChild(css2Renderer.domElement);

CSS2Renderer.domElement重新定位
javascript
css2Renderer.domElement.style.position = 'absolute';
css2Renderer.domElement.style.top = '0px';

二、HTML标签遮挡Canvas画布事件
.style.pointerEvents
javascript
css2Renderer.domElement.style.pointerEvents = 'none';

CSS属性z-index
css2Renderer.domElement在下,threejs canvas画布在上,标签被canvas画布遮挡,看不到标签。
javascript
renderer.domElement.style.zIndex = 1;
css2Renderer.domElement.style.zIndex = -1;

css2Renderer.domElement在上,threejs canvas画布在下,可以看到标签
javascript
renderer.domElement.style.zIndex = -1;
css2Renderer.domElement.style.zIndex = 1;

三、 Canvas尺寸变化(HTML标签)
javascript
// 画布跟随窗口变化
window.onresize = function () {
const width = window.innerWidth;
const height = window.innerHeight;
// cnavas画布宽高度重新设置
renderer.setSize(width,height);
// 相机参数重新设置
camera.aspect = width / height;
camera.updateProjectionMatrix();
};
javascript
//相机
const width = window.innerWidth;
const height = window.innerHeight;
// 画布跟随窗口变化
window.onresize = function () {
renderer.setSize(window.innerWidth, window.innerHeight);
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
};


Canvas全屏尺寸变化,CSS2渲染器设置
javascript
css2Renderer.setSize(window.innerWidth, window.innerHeight);

canvas局部布局,CSS2渲染器设置

码中web页面右下角div元素是用来插入canvas画布的HTML元素。
javascript
#tag {
padding: 10px;
color: #ffffff;
background: rgba(25, 25, 25, 0.5);
border-radius: 5px;
width: 65px;
}
javascript
<div id="webgl" style="position: absolute;top: 60px;left: 200px">
<div id="tag">
标签内容
</div>
</div>
CSS2渲染器输出的标签
javascript
// 引入CSS2渲染器CSS2DRenderer
import { CSS2DRenderer } from "three/addons/renderers/CSS2DRenderer.js";
// 创建一个CSS2渲染器CSS2DRenderer
const css2Renderer = new CSS2DRenderer();
// width, height:canvas画布宽高度
css2Renderer.setSize(width, height);
document.getElementById("webgl").appendChild(css2Renderer.domElement);
css2Renderer.domElement.style.position = "absolute";
css2Renderer.domElement.style.top = "0px";
css2Renderer.domElement.style.pointerEvents = "none";
javascript
// 渲染循环
function render() {
css2Renderer.render(scene, camera);
renderer.render(scene, camera);
requestAnimationFrame(render);
}
javascript
// 画布跟随窗口变化
window.onresize = function () {
const width = window.innerWidth - 200; //canvas画布高度
const height = window.innerHeight - 60; //canvas画布宽度
renderer.setSize(width, height);
css2Renderer.setSize(width, height);
camera.aspect = width / height;
camera.updateProjectionMatrix();
// 其它CSS代码,与threejs无关
document.getElementById("left").style.height = height + "px";
};
model.js
javascript
const div = document.getElementById("tag");
// HTML元素转化为threejs的CSS2模型对象
const tag = new CSS2DObject(div);
tag.position.set(50, 0, 50);
model.add(tag);

四、标签位置不同设置方式
javascript
import * as THREE from "three";
// 引入CSS2模型对象CSS2DObject
import { CSS2DObject } from "three/addons/renderers/CSS2DRenderer.js";
const group = new THREE.Group();
const geometry = new THREE.BoxGeometry(50, 50, 50);
const material = new THREE.MeshLambertMaterial({
color: 0xffffff,
});
const mesh = new THREE.Mesh(geometry, material);
mesh.position.set(50, 0, 50);
// mesh设置一个父对象meshGroup
const meshGroup = new THREE.Group();
meshGroup.add(mesh);
// mesh位置受到父对象局部坐标.positionn影响
meshGroup.position.x = -100;
const div = document.getElementById("tag");
// HTML元素转化为threejs的CSS2模型对象
const tag = new CSS2DObject(div);
tag.position.set(50, 0, 50);
group.add(meshGrouph, tag);
export default group;

.getWorldPosition()方法计算世界坐标
javascript
const worldPosition = new THREE.Vector3();
// 获取mesh的世界坐标(meshGroup.position和mesh.position累加结果)
mesh.getWorldPosition(worldPosition);
// mesh世界坐标复制给tag
tag.position.copy(worldPosition);

CSS2模型对象作为Mesh子对象
javascript
const group = new THREE.Group();
//标签tag作为mesh子对象,默认受到父对象位置影响
mesh.add(tag);
// group.add(meshGroup, tag);
group.add(meshGroup);
标注模型几何体的某个顶点
javascript
const pos = geometry.attributes.position;
// 获取几何体顶点1的xyz坐标,设置标签局部坐标.position属性
tag.position.set(pos.getX(0),pos.getY(0),pos.getZ(0));

标注圆锥顶部(了解局部坐标系原点)
javascript
const geometry = new THREE.ConeGeometry(25, 80);
const material = new THREE.MeshBasicMaterial({
color: 0xffffff,
});
const mesh = new THREE.Mesh(geometry, material);
mesh.position.set(50, 0, 50);
// 可视化模型的局部坐标系
const axesHelper = new THREE.AxesHelper(100);
mesh.add(axesHelper);

标签默认是标注在模型的局部坐标系坐标原点。

javascript
//y轴正方向,平移高度一半
geometry.translate(0, 40, 0);
//圆锥mesh局部坐标系原点在自己底部时候,标签需要向上偏移圆锥自身高度
tag.position.y += 80;

沿着y方向平移-40,改变圆锥几何体顶点坐标,圆锥mesh的局部坐标系坐标原点此刻位于圆锥顶部,这样标签刚好标注在顶部。

五、标签位置(标注工厂设备)

threejs获取工厂设备,查看局部坐标系
javascript
const obj = gltf.scene.getObjectByName('设备B');
// 可视化工厂设备obj的局部坐标系
const axesHelper = new THREE.AxesHelper(30);
obj.add(axesHelper);
CSS2模型对象标注工厂设备
javascript
loader.load("../工厂.glb", function (gltf) {
const tag = new CSS2DObject(div);
// const obj = gltf.scene.getObjectByName('设备A');
const obj = gltf.scene.getObjectByName('大货车1');
//标签tag作为obj子对象,默认标注在工厂设备obj的局部坐标系坐标原点
obj.add(tag);
})

建模软件创建空对象(控制标签位置)
在三维建模软件中,任何你想标注的位置,创建一个空对象(空的模型对象,没有任何模型顶点数据,只是一个空对象)。
不同三维建模软件中,创建空对象方式不同,不过思路是相同。


javascript
// 单独.glb文件
loader.load("../../工厂.glb", function (gltf) {
model.add(gltf.scene);
const div = document.getElementById("tag");
const tag = new CSS2DObject(div);
// const obj = gltf.scene.getObjectByName("大货车1");
const obj = gltf.scene.getObjectByName("空物体");
// 可视化工厂设备obj的局部坐标系
const axesHelper = new THREE.AxesHelper(30);
obj.add(axesHelper);
obj.add(tag);
});

六、标签指示线或箭头指向标注点
CSS2渲染器渲染HTML标签的位置特征
HTML元素标签自身的几何中心与标注点重合。
工厂设备标签CSS代码
javascript
<!-- CSS布局方式写法很多,不一定和课程一致 -->
<div id="tag">
<!-- position:relative;约束子元素绝对定位参照点 -->
<div style="position:relative;width:400px;height:322px;color: #fff;">
<!-- 图片绝对定位100%填充父元素,作为标签的背景 -->
<img src="./信息背景.png" alt="" style="width:100%;position: absolute;left: 0px;top: 0px;">
<!-- 名称、存储量、设备状态、等信息叠加到背景图上即可 -->
<div style="position:absolute;left:48px;top:36px;font-size:16px;">
<div style="font-size:20px;font-weight: 400;">
<span>设备A</span>
</div>
<div style="margin-top: 30px;">
<span style="font-weight: 400;margin-left: 80px;font-size: 40px;color: #00ffff;">276559 L</span>
</div>
<div style="margin-top: 20px;">
<span style="color: #ccc;font-weight: 300;">管理</span><span
style="font-weight: 400;margin-left: 30px;">郭老师</span>
</div>
<div style="margin-top: 10px;">
<span style="color: #ccc;font-weight: 300;">工号</span><span
style="font-weight: 400;margin-left: 30px;">webgl3d.cn</span>
</div>
</div>
<div style="position:absolute;left:285px;top:35px;">
<span style="color: #ffff00;">异常</span>
</div>
</div>
</div>

工厂标签的HTML元素适当平移
javascript
div.style.top = '-161px'; //平移-161px,指示线端点和标注点重合

HTML标签渲染前隐藏

javascript
style="display: none;"


七、鼠标选中模型弹出标签(工厂)
新建文件tag.js
javascript
// 引入CSS2模型对象CSS2DObject
import {
CSS2DObject
} from 'three/addons/renderers/CSS2DRenderer.js';
const div = document.getElementById('tag');
div.style.top = '-161px'; //指示线端点和标注点重合
// HTML元素转化为threejs的CSS2模型对象
const tag = new CSS2DObject(div);
export default tag;
在射线代码基础上,添加标签代码
javascript
if (intersects.length > 0) {
// 通过.ancestors属性判断那个模型对象被选中了
outlinePass.selectedObjects = [intersects[0].object.ancestors];
//tag会标注在intersects[0].object.ancestors模型的局部坐标系原点位置
intersects[0].object.ancestors.add(tag);
}
由于局部坐标在中心

工厂模型添加一个空对象,用来标记需要标注的位置。
javascript
if (intersects.length > 0) {
// 通过.ancestors属性判断那个模型对象被选中了
outlinePass.selectedObjects = [intersects[0].object.ancestors];
// 获取模型对象对应的标注点
// console.log('intersects[0].object.ancestors.name',intersects[0].object.ancestors.name);
const obj = model.getObjectByName(intersects[0].object.ancestors.name+'标注');
//tag会标注在空对象obj对应的位置
obj.add(tag);
}

没有选中模型,不显示标签和发光描边

javascript
// 存储当前选中的对象
let chooseObj = null;
javascript
// 射线交叉计算拾取模型
const intersects = raycaster.intersectObjects(cunchu.children);
if (intersects.length > 0) {
// 通过.ancestors属性判断那个模型对象被选中了
outlinePass.selectedObjects = [intersects[0].object.ancestors];
const obj = model.getObjectByName(
intersects[0].object.ancestors.name + "标注"
);
obj.add(tag);
chooseObj = obj;
} else {
if (chooseObj) {
//把原来选中模型对应的标签和发光描边隐藏
outlinePass.selectedObjects = []; //无发光描边
chooseObj.remove(tag); //从场景移除
chooseObj = null; // 重置选中对象
}
}

修改标签内容
javascript
// 获取设备名称标签
const span = document.getElementById("name");
addEventListener("click", function (event) {
...
// 射线交叉计算拾取模型
const intersects = raycaster.intersectObjects(cunchu.children);
if (intersects.length > 0) {
// 通过.ancestors属性判断那个模型对象被选中了
outlinePass.selectedObjects = [intersects[0].object.ancestors];
const obj = model.getObjectByName(
intersects[0].object.ancestors.name + "标注"
);
obj.add(tag);
chooseObj = obj;
span.innerHTML = intersects[0].object.ancestors.name;
} else {
if (chooseObj) {
//把原来选中模型对应的标签和发光描边隐藏
outlinePass.selectedObjects = []; //无发光描边
chooseObj.remove(tag); //从场景移除
chooseObj = null; // 重置选中对象
}
}
});

八、单击按钮关闭HTML标签
HTML标签增加一个关闭按钮
javascript
<style>
#close:hover {
cursor: pointer;
}
</style>
<div style="position:absolute;left:350px;top:20px;">
<img id="close" src="./关闭.png" width="32">
</div>

单击按钮关闭HTML标签
javascript
// 鼠标单击按钮,关闭HTML标签
document.getElementById('close').addEventListener('click',function(){
if (chooseObj) {//把原来选中模型对应的标签和发光描边隐藏
outlinePass.selectedObjects = []; //无发光描边
chooseObj.remove(tag); //从场景移除
}
})

单击关闭按钮无效情况

HTML标签设置了属性.style.pointerEvents = 'none'
javascript
<img id="close" src="./关闭.png" style="pointer-events: auto;">
or
javascript
document.getElementById('close').style.pointerEvents = 'auto';

九、CSS3DRenderer渲染HTML标签
CSS3渲染的标签会跟着场景相机同步缩放,而CSS2渲染的标签默认保持自身像素值。

设置CSS3渲染器代码
javascript
import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
// 引入CSS2渲染器CSS2DRenderer
// import { CSS2DRenderer } from "three/addons/renderers/CSS2DRenderer.js";
// 引入CSS3渲染器CSS3DRenderer
import { CSS3DRenderer } from "three/addons/renderers/CSS3DRenderer.js";
import model from "./model.js"; //模型对象
//场景
const scene = new THREE.Scene();
scene.add(model); //模型对象添加到场景中
//辅助观察的坐标系
const axesHelper = new THREE.AxesHelper(100);
scene.add(axesHelper);
//光源设置
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(100, 60, 50);
scene.add(directionalLight);
const ambient = new THREE.AmbientLight(0xffffff, 0.4);
scene.add(ambient);
//相机
const width = window.innerWidth;
const height = window.innerHeight;
// const width = 600;
// const height = 300;
const camera = new THREE.PerspectiveCamera(30, width / height, 1, 3000);
camera.position.set(292, 223, 185);
camera.lookAt(0, 0, 0);
// WebGL渲染器设置
const renderer = new THREE.WebGLRenderer({
antialias: true, //开启优化锯齿
});
renderer.setPixelRatio(window.devicePixelRatio); //防止输出模糊
renderer.setSize(width, height);
document.body.appendChild(renderer.domElement);
// 创建一个CSS2渲染器CSS2DRenderer
// const css2Renderer = new CSS2DRenderer();
// width, height:canvas画布宽高度
// css2Renderer.setSize(width, height);
// document.body.appendChild(css2Renderer.domElement);
// css2Renderer.domElement.style.position = "absolute";
// css2Renderer.domElement.style.top = "0px";
// css2Renderer.domElement.style.pointerEvents = "none";
// 创建一个CSS3渲染器CSS3DRenderer
const css3Renderer = new CSS3DRenderer();
css3Renderer.setSize(width, height);
// HTML标签<div id="tag"></div>外面父元素叠加到canvas画布上且重合
css3Renderer.domElement.style.position = "absolute";
css3Renderer.domElement.style.top = "0px";
//设置.pointerEvents=none,解决HTML元素标签对threejs canvas画布鼠标事件的遮挡
css3Renderer.domElement.style.pointerEvents = "none";
document.body.appendChild(css3Renderer.domElement);
// 渲染循环
function render() {
// 用法和webgl渲染器渲染方法类似
// css2Renderer.render(scene, camera);
css3Renderer.render(scene, camera);
renderer.render(scene, camera);
requestAnimationFrame(render);
}
render();
const controls = new OrbitControls(camera, renderer.domElement);
// 画布跟随窗口变化
window.onresize = function () {
css3Renderer.setSize(width, height);
renderer.setSize(window.innerWidth, window.innerHeight);
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
};
CSS3对象模型CSS3DObject
javascript
import * as THREE from "three";
// 引入CSS2模型对象CSS2DObject
// import { CSS2DObject } from "three/addons/renderers/CSS2DRenderer.js";
// 引入CSS3模型对象CSS3DObject
import { CSS3DObject } from 'three/addons/renderers/CSS3DRenderer.js';
const geometry = new THREE.ConeGeometry(25, 80);
const material = new THREE.MeshBasicMaterial({
color: 0xffffff,
transparent: true,
opacity: 0.5,
});
const mesh = new THREE.Mesh(geometry, material);
mesh.position.set(50, 0, 50);
// 可视化模型的局部坐标系
const axesHelper = new THREE.AxesHelper(100);
mesh.add(axesHelper);
// mesh设置一个父对象meshGroup
const meshGroup = new THREE.Group();
meshGroup.add(mesh);
// mesh位置受到父对象局部坐标.positionn影响
meshGroup.position.x = -100;
// const div = document.getElementById("tag");
// // HTML元素转化为threejs的CSS2模型对象
// const tag = new CSS2DObject(div);
// //y轴正方向,平移高度一半
// geometry.translate(0, 40, 0);
// //圆锥mesh局部坐标系原点在自己底部时候,标签需要向上偏移圆锥自身高度
// tag.position.y += 80;
const div = document.getElementById("tag");
// HTML元素转化为threejs的CSS3模型对象
const tag = new CSS3DObject(div);
//标签tag作为mesh子对象,默认标注在模型局部坐标系坐标原点
mesh.add(tag);
// 相对父对象局部坐标原点偏移80,刚好标注在圆锥
tag.position.y += 80;
const group = new THREE.Group();
//标签tag作为mesh子对象,默认受到父对象位置影响
mesh.add(tag);
// group.add(meshGroup, tag);
group.add(meshGroup);
export default group;

禁止CSS3DObject标签对应HTMl元素背面显示

javascript
style="backface-visibility: hidden;"

CSS3精灵模型CSS3DSprite
javascript
// 引入CSS3精灵模型对象CSS3DSprite
import { CSS3DSprite } from 'three/addons/renderers/CSS3DRenderer.js';
const div = document.getElementById('tag');
// HTML元素转化为threejs的CSS3精灵模型`CSS3DSprite`
const tag = new CSS3DSprite(div);
//标签tag作为mesh子对象,默认标注在模型局部坐标系坐标原点
mesh.add(tag);
// 相对父对象局部坐标原点偏移80,刚好标注在圆锥
tag.position.y += 80;

标签局部遮挡鼠标事件

javascript
css3Renderer.domElement.style.pointerEvents = "none";
虽然css3Renderer.domElement不遮挡canvas画布的鼠标事件,但是<div id="tag"></div>遮挡canvas画布的鼠标事件
model.js
javascript
div.style.pointerEvents = 'none';

十、CSS3批量标注多个标签
CSS3渲染器基本代码
tag.js
javascript
// 引入CSS2模型对象CSS2DObject
import { CSS3DSprite } from "three/examples/jsm/renderers/CSS3DSprite.js";
const div = document.getElementById("tag");
// HTML元素转化为threejs的CSS3对象
// const tag = new CSS3DObject(div);
const tag = new CSS3DSprite(div);
div.style.pointerEvents = "none"; //避免标签遮挡canvas鼠标事件
// obj是建模软件中创建的一个空对象
const obj = gltf.scene.getObjectByName("设备A标注");
//tag会标注在空对象obj对应的位置
obj.add(tag);
export default tag;
标签HTML、CSS代码
javascript
<style>
#tag {
width: 70px;
height: 40px;
line-height: 32px;
text-align: center;
color: #fff;
font-size: 16px;
background-image: url(./标签箭头背景.png);
background-repeat: no-repeat;
background-size: 100% 100%;
}
</style>
<div id="tag">设备A</div>
javascript
//需要批量标注的标签数据arr
const arr = ['设备A','设备B','停车场'];
for (let i = 0; i < arr.length; i++) {
// 注意是多个标签,需要克隆复制一份
const div = document.getElementById('tag').cloneNode();
div.innerHTML = arr[i];//标签数据填写
// HTML元素转化为threejs的CSS3对象
// const tag = new CSS3DObject(div);
const tag = new CSS3DSprite(div);
div.style.pointerEvents = 'none'; //避免标签遮挡canvas鼠标事件
// obj是建模软件中创建的一个空对象
const obj = gltf.scene.getObjectByName(arr[i]+'标注');
//tag会标注在空对象obj对应的位置
obj.add(tag);
tag.scale.set(0.1,0.1,1);//适当缩放模型标签
tag.position.y = 40/2*0.1;//标签底部箭头和空对象标注点重合:偏移高度像素值一半*缩放比例
}

十一、精灵模型Sprite作为标签
精灵模型标签
javascript
const texLoader = new THREE.TextureLoader();
const texture = texLoader.load("./警告.png");
const spriteMaterial = new THREE.SpriteMaterial({
map: texture,
});
const sprite = new THREE.Sprite(spriteMaterial);
sprite.scale.set(5, 5, 1);
sprite.position.y = 5 / 2; //标签底部箭头和空对象标注点重合
// obj是建模软件中创建的一个空对象
const obj = gltf.scene.getObjectByName("设备A标注");
//tag会标注在空对象obj对应的位置
obj.add(sprite);

创建sprite.js
javascript
import * as THREE from "three";
function createSprite(obj, state) {
const texLoader = new THREE.TextureLoader();
let texture = null;
if (state == "警告") {
texture = texLoader.load("./警告.png");
} else {
texture = texLoader.load("./提示.png");
}
const spriteMaterial = new THREE.SpriteMaterial({
map: texture,
});
const sprite = new THREE.Sprite(spriteMaterial);
sprite.scale.set(5, 5, 1);
sprite.position.y = 5 / 2; //标签底部箭头和空对象标注点重合
// obj是建模软件中创建的一个空对象
const obj = gltf.scene.getObjectByName("设备A标注");
//tag会标注在空对象obj对应的位置
obj.add(sprite);
}
export default createSprite;

十二、Sprite标签(Canvas作为贴图)
创建canvas.js
javascript
// 生成一个canvas对象,标注文字为参数name
function createCanvas(name) {
/**
* 创建一个canvas对象,绘制几何图案或添加文字
*/
const canvas = document.createElement("canvas");
const arr = name.split(""); //分割为单独字符串
let num = 0;
const reg = /[\u4e00-\u9fa5]/;
for (let i = 0; i < arr.length; i++) {
if (reg.test(arr[i])) {
//判断是不是汉字
num += 1;
} else {
num += 0.5; //英文字母或数字累加0.5
}
}
// 根据字符串符号类型和数量、文字font-size大小来设置canvas画布宽高度
const h = 80; //根据渲染像素大小设置,过大性能差,过小不清晰
const w = h + num * 32;
canvas.width = w;
canvas.height = h;
const h1 = h * 0.8;
const c = canvas.getContext("2d");
// 定义轮廓颜色,黑色半透明
c.fillStyle = "rgba(0,0,0,0.5)";
// 绘制半圆+矩形轮廓
const R = h1 / 2;
c.arc(R, R, R, -Math.PI / 2, Math.PI / 2, true); //顺时针半圆
c.arc(w - R, R, R, Math.PI / 2, -Math.PI / 2, true); //顺时针半圆
c.fill();
// 绘制箭头
c.beginPath();
const h2 = h - h1;
c.moveTo(w / 2 - h2 * 0.6, h1);
c.lineTo(w / 2 + h2 * 0.6, h1);
c.lineTo(w / 2, h);
c.fill();
// 文字
c.beginPath();
c.translate(w / 2, h1 / 2);
c.fillStyle = "#ffffff"; //文本填充颜色
c.font = "normal 32px 宋体"; //字体样式设置
c.textBaseline = "middle"; //文本与fillText定义的纵坐标
c.textAlign = "center"; //文本居中(以fillText定义的横坐标)
c.fillText(name, 0, 0);
return canvas;
}
export default createCanvas;
model.js
javascript
// 引入Three.js
import * as THREE from "three";
// 引入gltf模型加载库GLTFLoader.js
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
import createCanvas from "./canvas.js";
const loader = new GLTFLoader(); //创建一个GLTF加载器
const model = new THREE.Group(); //声明一个组对象,用来添加加载成功的三维场景
// 单独.glb文件
loader.load("../../工厂.glb", function (gltf) {
model.add(gltf.scene);
const canvas = createCanvas("设备A");
// canvas画布作为CanvasTexture的参数创建一个纹理对象
// 本质上你可以理解为CanvasTexture读取参数canvas画布上的像素值
const texture = new THREE.CanvasTexture(canvas);
const spriteMaterial = new THREE.SpriteMaterial({
map: texture,
});
const sprite = new THREE.Sprite(spriteMaterial);
const y = 4; //精灵y方向尺寸
// sprite宽高比和canvas画布保持一致
const x = (canvas.width / canvas.height) * y; //精灵x方向尺寸
sprite.scale.set(x, y, 1); // 控制精灵大小
sprite.position.y = y / 2; //标签底部箭头和空对象标注点重合
const obj = gltf.scene.getObjectByName("设备A标注"); // obj是建模软件中创建的一个空对象
obj.add(sprite); //tag会标注在空对象obj对应的位置
});
export default model;

cavnas精灵标签封装(标注多个)
Canvas包含外部图片
要等图像加载完成再执行THREE.CanvasTexture(canvas)
Threejs (16)
一、关键帧动画
创建关键帧动画AnimationClip
模型命名
javascript
// 给需要设置关键帧动画的模型命名
mesh1.name = "Box";
KeyframeTrack设置关键帧数据
javascript
const times = [0, 3, 6]; //时间轴上,设置三个时刻0、3、6秒
// times中三个不同时间点,物体分别对应values中的三个xyz坐标
const values = [0, 0, 0, 100, 0, 0, 0, 0, 100];
// 0~3秒,物体从(0,0,0)逐渐移动到(100,0,0),3~6秒逐渐从(100,0,0)移动到(0,0,100)
const posKF = new THREE.KeyframeTrack("Box.position", times, values);
// 从2秒到5秒,物体从红色逐渐变化为蓝色
const colorKF = new THREE.KeyframeTrack(
"Box.material.color",
[2, 5],
[1, 0, 0, 0, 0, 1]
);
创建关键帧动画AnimationClip
javascript
// 1.3 AnimationClip表示一个关键帧动画,可以基于关键帧数据产生动画效果
// 创建一个clip关键帧动画对象,命名"test",动画持续时间6s
// AnimationClip包含的所有关键帧数据都放到参数3数组中即可
const clip = new THREE.AnimationClip("test",6,[posKF, colorKF]);
model.js
javascript
// 给需要设置关键帧动画的模型命名
mesh.name = "Box";
const times = [0, 3, 6]; //时间轴上,设置三个时刻0、3、6秒
// times中三个不同时间点,物体分别对应values中的三个xyz坐标
const values = [0, 0, 0, 100, 0, 0, 0, 0, 100];
// 0~3秒,物体从(0,0,0)逐渐移动到(100,0,0),3~6秒逐渐从(100,0,0)移动到(0,0,100)
const posKF = new THREE.KeyframeTrack('Box.position', times, values);
// 从2秒到5秒,物体从红色逐渐变化为蓝色
const colorKF = new THREE.KeyframeTrack('Box.material.color', [2, 5], [1, 0, 0, 0, 0, 1]);
// 1.3 基于关键帧数据,创建一个clip关键帧动画对象,命名"test",持续时间6秒。
const clip = new THREE.AnimationClip("test", 6, [posKF, colorKF]);
AnimationMixer播放关键帧动画AnimationClip
javascript
//包含关键帧动画的模型对象作为AnimationMixer的参数创建一个播放器mixer
const mixer = new THREE.AnimationMixer(mesh1);
//AnimationMixer的`.clipAction()`返回一个AnimationAction对象
const clipAction = mixer.clipAction(clip);
//.play()控制动画播放,默认循环播放
clipAction.play();
mixer.update()更新播放器AnimationMixer时间
javascript
function loop() {
requestAnimationFrame(loop);
}
loop();

二、动画播放(暂停、倍速、循环)
动画动作对象AnimationAction

AnimationAction的循环属性.loop
javascript
//不循环播放
clipAction.loop = THREE.LoopOnce;

AnimationAction的.clampWhenFinished属性
javascript
// 物体状态停留在动画结束的时候
clipAction.clampWhenFinished = true;

停止结束动画.stop()
javascript
<div class="pos">
<div id="stop" class="bu">停止</div>
<div id="play" class="bu" style="margin-left: 10px;">播放</div>
</div>
model.js
javascript
document.getElementById('stop').addEventListener('click',function(){
clipAction.stop();//动画停止结束,回到开始状态
})
document.getElementById('play').addEventListener('click',function(){
clipAction.play();//播放动画
})

是否暂停播放.paused
javascript
<div id="bu" class="bu">暂停</div>
javascript
const bu = document.getElementById('bu');
bu.addEventListener('click',function(){
// AnimationAction.paused默认值false,设置为true,可以临时暂停动画
if (clipAction.paused) {//暂停状态
clipAction.paused = false;//切换为播放状态
bu.innerHTML='暂停';// 如果改变为播放状态,按钮文字设置为"暂停"
} else {//播放状态
clipAction.paused = true;//切换为暂停状态
bu.innerHTML='继续';// 如果改变为暂停状态,按钮文字设置为"继续"
}
})

倍速播放.timeScale
javascript
clipAction.timeScale = 1;//默认
clipAction.timeScale = 2;//2倍速

拖动条调整播放速度
javascript
const gui = new GUI(); //创建GUI对象
// 0~6倍速之间调节
gui.add(clipAction, 'timeScale', 0, 6);

三、动画播放(拖动任意时间状态)
控制动画播放特定时间段
javascript
//AnimationAction设置开始播放时间:从1秒时刻对应动画开始播放
clipAction.time = 1;
//AnimationClip设置播放结束时间:到5秒时刻对应的动画状态停止
clip.duration = 5;

查看时间轴上任意时间动画状态
javascript
//在暂停情况下,设置.time属性,把动画定位在任意时刻
clipAction.paused = true;


拖动条拖动显示动画任意时刻模型状态
javascript
//在暂停情况下,设置.time属性,把动画定位在任意时刻
clipAction.paused = true;
拖动条拖动显示动画任意时刻模型状态
javascript
import {GUI} from 'three/addons/libs/lil-gui.module.min.js';
const gui = new GUI(); //创建GUI对象
gui.add(clipAction, 'time', 0, 6).step(0.1);;

动画下一步状态
下一步按钮
javascript
<div id="bu" class="bu">下一步</div>
javascript
const bu = document.getElementById('bu');
bu.addEventListener('click', function () {
clipAction.time += 0.1;
})

四、解析外部模型关键帧动画
查看gltf模型动画数据
javascript
console.log('控制台查看gltf对象结构', gltf);

播放AnimationClip动画
javascript
//包含关键帧动画的模型作为参数创建一个播放器
const mixer = new THREE.AnimationMixer(gltf.scene);
// 获取gltf.animations[0]的第一个clip动画对象
const clipAction = mixer.clipAction(gltf.animations[0]); //创建动画clipAction对象
clipAction.play(); //播放动画
// 如果想播放动画,需要周期性执行`mixer.update()`更新AnimationMixer时间数据
const clock = new THREE.Clock();
function loop() {
requestAnimationFrame(loop);
//clock.getDelta()方法获得loop()两次执行时间间隔
const frameT = clock.getDelta();
// 更新播放器相关的时间
mixer.update(frameT);
}
loop();

动画是否循环播放
人走路、跑步美术美术一般设置很短时间运动,如果你想一直看到运动动作,不用设置非循环。
javascript
//不循环播放
clipAction.loop = THREE.LoopOnce;
// 物体状态停留在动画结束的时候
clipAction.clampWhenFinished = true

五、机械虚拟装配案例(播放)
按钮控制虚拟装配播放、暂停
javascript
const bu = document.getElementById('bu');
bu.addEventListener('click',function(){
// AnimationAction.paused默认值false,设置为true,可以临时暂停动画
if (clipAction.paused) {//暂停状态
clipAction.paused = false;//切换为播放状态
bu.innerHTML='暂停';// 如果改变为播放状态,按钮文字设置为"暂停"
} else {//播放状态
clipAction.paused = true;//切换为暂停状态
bu.innerHTML='播放';// 如果改变为暂停状态,按钮文字设置为"播放"
}
})
动画播放结束,按钮样式恢复到播放
javascript
clipAction.loop = THREE.LoopOnce;
// 动画播放完成事件
mixer.addEventListener("finished", function () {
bu.innerHTML = "播放"; //播放完成,按钮显示为"播放"
clipAction.reset(); //重新开始新的动画播放
clipAction.paused = true; //切换为暂停状态
});
拖动条控制播放倍速
javascript
import {GUI} from 'three/addons/libs/lil-gui.module.min.js';
const gui = new GUI(); //创建GUI对象
// 0~2倍速之间调节
gui.add(clipAction, 'timeScale', 0, 2).step(0.1).name('倍速');

六、虚拟装配(任意时间定位)
属性.duration获取动画默认的执行时间
javascript
const duration = clip.duration;//默认持续时间

拖动条查看任意时间动画状态
javascript
// 拖动条查看动画任何时刻模型状态
gui.add(clipAction,'time',0,duration).step(0.1).name('拖动');

拖动条与播放按钮功能组合

javascript
// 拖动条查看动画任何时刻模型状态
gui
.add(clipAction, "time", 0, duration)
.step(0.1)
.name("拖动")
.onChange(function () {
//如果动画处于播放状态会影响拖动条时间定位
if (!clipAction.paused) {
clipAction.paused = true; //切换为暂停状态
bu.innerHTML = "播放"; //修改按钮样式
}
});

七、变形动画原理
.morphAttributes设置几何体变形目标顶点数据
javascript
//几何体两组顶点一一对应,位置不同,然后通过权重系数,可以控制模型形状在两组顶点之间变化
const geometry = new THREE.BoxGeometry(50, 50, 50);
// 为geometry提供变形目标的顶点数据(注意和原始geometry顶点数量一致)
const target1 = new THREE.BoxGeometry(50, 200, 50).attributes.position;//变高
const target2 = new THREE.BoxGeometry(10, 50, 10).attributes.position;//变细
// 几何体顶点变形目标数据,可以设置1组或多组
geometry.morphAttributes.position = [target1, target2];
const mesh = new THREE.Mesh(geometry, material);
.morphTargetInfluences权重系数控制变形程度
javascript
//权重0:物体形状对应geometry.attributes.position表示形状
mesh.morphTargetInfluences[0] = 0.0;
//权重1:物体形状对应target1表示形状
mesh.morphTargetInfluences[0] = 1.0;
//权重0.5:物体形状对应geometry和target1变形中间状态
mesh.morphTargetInfluences[0] = 0.5;

多个变形目标综合影响模型形状
javascript
// 两个变形目标同时影响模型形状
mesh.morphTargetInfluences[1] = 0.5;
mesh.morphTargetInfluences[0] = 0.5;

GUI控制变形权重系数.morphTargetInfluences
javascript
import {GUI} from 'three/addons/libs/lil-gui.module.min.js';
const gui = new GUI();
// GUI拖动条可视化改变变形目标权重系数
const obj = {
t1: 0,
t2: 0,
}
gui.add(obj, 't1', 0, 1).name('变形目标1').onChange(function (v) {
// 变形目标1对物体形状影响权重
mesh.morphTargetInfluences[0] = v;
});
gui.add(obj, 't2', 0, 1).name('变形目标2').onChange(function (v) {
// 变形目标2对物体形状影响权重
mesh.morphTargetInfluences[1] = v;
});

生成变形动画
javascript
// 创建变形动画权重系数的关键帧数据
mesh.name = "Box";//关键帧动画控制的模型对象命名
// 设置变形目标1对应权重随着时间的变化
const KF1 = new THREE.KeyframeTrack('Box.morphTargetInfluences[0]', [0, 5], [0, 1]);
// 设置变形目标2对应权重随着时间的变化
const KF2 = new THREE.KeyframeTrack('Box.morphTargetInfluences[1]', [5, 10], [0, 1]);
// 创建一个剪辑clip对象
const clip = new THREE.AnimationClip("t", 10, [KF1, KF2]);

八、变形动画

查看模型几何体变形相关信息
javascript
// 访问网格模型
const mesh = gltf.scene.children[0];
// 获取所有变形目标的顶点数据
const tArr = mesh.geometry.morphAttributes.position;
console.log("所有变形目标", tArr);
console.log("所有权重", mesh.morphTargetInfluences);

UI界面定制
javascript
import { GUI } from "three/addons/libs/lil-gui.module.min.js";
const gui = new GUI();
批量设置所有变形目标的拖动条
javascript
loader.load("../../猴头.glb", function (gltf) {
console.log("控制台查看gltf对象结构", gltf);
model.add(gltf.scene);
// 访问网格模型
const mesh = gltf.scene.children[0];
// 获取所有变形目标的顶点数据
const tArr = mesh.geometry.morphAttributes.position;
console.log("所有变形目标", tArr);
console.log("所有权重", mesh.morphTargetInfluences);
// 每个变形目标对应的含义(注意和变形目标对应起来)
const nameArr = ["耳朵变尖", "头顶变高", "眉毛变高"];
// GUI拖动条可视化改变变形目标权重系数
const obj = {};
for (let i = 0; i < tArr.length; i++) {
obj["t" + i] = 0; //obj批量定义一个属性表示变性目标的权重系数
// // 批量设置要改变的obj属性,对应name名字,和对应权重
gui
.add(obj, "t" + i, 0, 1)
.name(nameArr[i])
.onChange(function (v) {
mesh.morphTargetInfluences[i] = v;
});
}
});

外部模型变形数据生成动画
javascript
// 创建变形动画权重系数的关键帧数据
mesh.name = "per"; //关键帧动画控制的模型对象命名
// 设置变形目标1对应权重随着时间的变化
const KF1 = new THREE.KeyframeTrack(
"per.morphTargetInfluences[0]",
[0, 5],
[0, 1]
);
// 生成关键帧动画
const clip = new THREE.AnimationClip("t", 5, [KF1]);
//包含关键帧动画的模型作为参数创建一个播放器
const mixer = new THREE.AnimationMixer(gltf.scene);
const clipAction = mixer.clipAction(clip);
clipAction.play();
const clock = new THREE.Clock();
function loop() {
requestAnimationFrame(loop);
const frameT = clock.getDelta();
// 更新播放器相关的时间
mixer.update(frameT);
}
loop();

九、骨骼关节Bone
骨骼关节Bone树结构
javascript
const Bone1 = new THREE.Bone(); //关节1,用来作为根关节
const Bone2 = new THREE.Bone(); //关节2
const Bone3 = new THREE.Bone(); //关节3
// 设置关节父子关系 多个骨头关节构成一个树结构
Bone1.add(Bone2);
Bone2.add(Bone3);
设置关节模型的位置和姿态角度
javascript
//根关节Bone1默认位置是(0,0,0)
Bone2.position.y = 60; //Bone2相对父对象Bone1位置
Bone3.position.y = 30; //Bone3相对父对象Bone2位置
//平移Bone1,Bone2、Bone3跟着平移
Bone1.position.set(50, 0, 50);
// 骨骼关节旋转
Bone1.rotateX(Math.PI / 6);
Bone2.rotateX(Math.PI / 6);
javascript
// 骨骼关节可以和普通网格模型一样作为其他模型子对象,添加到场景中
const group = new THREE.Group();
group.add(Bone1);
// SkeletonHelper会可视化参数模型对象所包含的所有骨骼关节
const skeletonHelper = new THREE.SkeletonHelper(group);
group.add(skeletonHelper);

拖动条控制骨骼关节旋转
javascript
import {GUI} from 'three/addons/libs/lil-gui.module.min.js';
const gui = new GUI();
gui.add(Bone1.rotation, 'x', 0, Math.PI / 3).name('关节1');
gui.add(Bone2.rotation, 'x', 0, Math.PI / 3).name('关节2');

十、查看外部模型骨骼动画

可视化外部模型骨骼关节
javascript
import * as THREE from "three";
// 引入gltf模型加载库GLTFLoader.js
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
const loader = new GLTFLoader();
const model = new THREE.Group();
loader.load("../../骨骼动画.glb", function (gltf) {
console.log("控制台查看gltf对象结构", gltf);
model.add(gltf.scene);
// 骨骼辅助显示
const skeletonHelper = new THREE.SkeletonHelper(gltf.scene);
model.add(skeletonHelper);
});
export default model;

根据骨骼名称读取骨骼关节
javascript
// 根据骨骼关节名字获取骨关节Bone
// 在三维软件中,骨骼关节层层展开,可以看到下面三个骨骼关节
const bone1 = gltf.scene.getObjectByName('Bone1'); //关节1
const bone2 = gltf.scene.getObjectByName('Bone2'); //关节2
const bone3 = gltf.scene.getObjectByName('Bone3'); //关节3
代码测试骨骼关节Bone带动模型表面变化
javascript
bone2.rotation.x = Math.PI / 6; //关节2旋转
bone3.rotation.x = Math.PI / 6; //关节3旋转
查看骨骼网格模型SkinnedMesh
javascript
// 根据节点名字获取某个骨骼网格模型
const SkinnedMesh = gltf.scene.getObjectByName("");
console.log("骨骼网格模型", SkinnedMesh);

访问骨骼网格模型的骨架SkinnedMesh.skeleton
javascript
console.log('骨架', SkinnedMesh.skeleton);
骨架的骨骼关节属性.skeleton.bones
javascript
console.log('骨架所有关节', SkinnedMesh.skeleton.bones);
console.log('根关节', SkinnedMesh.skeleton.bones[0]);
播放骨骼网格模型的关键帧动画
javascript
import * as THREE from "three";
// 引入gltf模型加载库GLTFLoader.js
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
const loader = new GLTFLoader();
const model = new THREE.Group();
loader.load("../../骨骼动画.glb", function (gltf) {
console.log("控制台查看gltf对象结构", gltf);
model.add(gltf.scene);
// 骨骼辅助显示
// const skeletonHelper = new THREE.SkeletonHelper(gltf.scene);
// model.add(skeletonHelper);
// 根据骨骼关节名字获取骨关节Bone
// 在三维软件中,骨骼关节层层展开,可以看到下面三个骨骼关节
// const bone1 = gltf.scene.getObjectByName("mixamorig:Hips");
// const bone2 = gltf.scene.getObjectByName("Ctrl_Hand_IK_Left"); //关节2
// const bone3 = gltf.scene.getObjectByName("Ctrl_ArmPole_IK_Right"); //关节3
// console.log("关节1", bone1);
// console.log("关节2", bone2);
// console.log("关节3", bone3);
// bone1.rotation.x = Math.PI / 6; //关节1旋转
// bone2.rotation.x = Math.PI / 6; //关节2旋转
// bone3.rotation.x = Math.PI / 6; //关节3旋转
// 根据节点名字获取某个骨骼网格模型
// const SkinnedMesh = gltf.scene.getObjectByName("mixamorig:Head");
// console.log("骨骼网格模型", SkinnedMesh);
// console.log("骨架", SkinnedMesh.skeleton);
//包含关键帧动画的模型作为参数创建一个播放器
const mixer = new THREE.AnimationMixer(gltf.scene);
// gltf.animations[0]休息
// gltf.animations[1]休息
// gltf.animations[2]走路
// gltf.animations[3]跑
const clipAction = mixer.clipAction(gltf.animations[3]);
clipAction.play(); //播放动画
// 如果想播放动画,需要周期性执行`mixer.update()`更新AnimationMixer时间数据
const clock = new THREE.Clock();
function loop() {
requestAnimationFrame(loop);
//clock.getDelta()方法获得loop()两次执行时间间隔
const frameT = clock.getDelta();
// 更新播放器相关的时间
mixer.update(frameT);
}
loop();
});
export default model;

十一、骨骼动画不同动作切换
切换动画不同动作(.play()和.stop())
javascript
#pos {
position: absolute;
top: 75%;
left: 50%;
color: aliceblue;
}
javascript
<div id="pos">
<div id="Idle1"
class="bu1">休息1</div>
<div id="Idle2"
class="bu2">休息2</div>
<div id="Run"
class="bu"
style="margin-left: 10px;">跑步</div>
<div id="Walk"
class="bu"
style="margin-left: 10px;">走路</div>
</div>
点击按钮,按钮对应的动作对象AnimationAction,执行.play()方法开始动画执行,原来执行中的动画动作对象,执行.stop()方法终止执行。
javascript
import * as THREE from "three";
// 引入gltf模型加载库GLTFLoader.js
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
const loader = new GLTFLoader();
const model = new THREE.Group();
loader.load("../../骨骼动画.glb", function (gltf) {
model.add(gltf.scene);
//包含关键帧动画的模型作为参数创建一个播放器
const mixer = new THREE.AnimationMixer(gltf.scene);
// gltf.animations[0]休息
// gltf.animations[1]休息
// gltf.animations[2]走路
// gltf.animations[3]跑
const IdleAction = mixer.clipAction(gltf.animations[0]);
const IdleAction1 = mixer.clipAction(gltf.animations[1]);
const RunAction = mixer.clipAction(gltf.animations[3]);
const WalkAction = mixer.clipAction(gltf.animations[2]);
IdleAction.play();
let ActionState = IdleAction; //当前处于播放状态的动画动作对象
// 通过UI按钮控制,切换动画运动状态
document.getElementById("Idle1").addEventListener("click", function () {
ActionState.stop(); //播放状态动画终止
IdleAction.play();
ActionState = IdleAction;
});
document.getElementById("Idle2").addEventListener("click", function () {
ActionState.stop();
IdleAction1.play();
ActionState = IdleAction1;
});
document.getElementById("Run").addEventListener("click", function () {
ActionState.stop(); //播放状态动画终止
RunAction.play();
ActionState = RunAction;
});
document.getElementById("Walk").addEventListener("click", function () {
ActionState.stop(); //播放状态动画终止
WalkAction.play();
ActionState = WalkAction;
});
// 如果想播放动画,需要周期性执行`mixer.update()`更新AnimationMixer时间数据
const clock = new THREE.Clock();
function loop() {
requestAnimationFrame(loop);
//clock.getDelta()方法获得loop()两次执行时间间隔
const frameT = clock.getDelta();
// 更新播放器相关的时间
mixer.update(frameT);
}
loop();
});
export default model;

AnimationAction的权重属性.weight
javascript
IdleAction.play();
RunAction.play();
WalkAction.play();

javascript
// 跑步和走路动画对人影响程度为0,人处于休闲状态
IdleAction.weight = 1.0;
RunAction.weight = 0.0;
WalkAction.weight = 0.0;

切换动画不同动作(.weight)
javascript
IdleAction.play();
IdleAction1.play();
RunAction.play();
WalkAction.play();
// 跑步和走路动画对人影响程度为0,人处于休闲状态
IdleAction.weight = 1.0;
IdleAction1.weight = 0.0;
RunAction.weight = 0.0;
WalkAction.weight = 0.0;
let ActionState = IdleAction; //当前处于播放状态的动画动作对象
// 通过UI按钮控制,切换动画运动状态
document.getElementById("Idle1").addEventListener("click", function () {
ActionState.weight = 0.0; //播放状态动画权重设置为0
IdleAction.weight = 1.0;
ActionState = IdleAction;
});
document.getElementById("Idle2").addEventListener("click", function () {
ActionState.weight = 0.0; //播放状态动画权重设置为0
IdleAction1.weight = 1.0;
ActionState = IdleAction;
});
document.getElementById("Run").addEventListener("click", function () {
ActionState.weight = 0.0; //播放状态动画权重设置为0
RunAction.weight = 1.0;
ActionState = RunAction;
});
document.getElementById("Walk").addEventListener("click", function () {
ActionState.weight = 0.0; //播放状态动画权重设置为0
WalkAction.weight = 1.0;
ActionState = WalkAction;
});
