组件
<template>
<div id="modelCreation">
<slot>slot>
div>
template>
<script setup lang="ts">
/**
* 组件使用
* 1、安装依赖:npm install three -D
* 2、使用的页面引用组件
* 3、引入加载的模型,浏览器会打印通道数据
* 4、通过定义好的类、函数修改贴图
*/
/**
* 参数说明
* @param el dom节点
* @param list 模型渲染贴纸
* @param modelUrl 模型地址
* @param speed 旋转速度
* @param size 模型大小
* @param vector 模型 x,y,z分量
* @param position 模型 x,y,z位置
* @param autoRotate true 自定旋转 false 禁止旋转
*/
import * as THREE from "three";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
const emit = defineEmits(["load"]);
let array: any = [];
let speed: number = 1;
let scene: any = null;
let camera: any = null;
let controls: any = null;
let renderer: any = null;
let stopAnimationType: boolean = false;
let oldId: number | null = null, id: number | null = null;
// 定义接收参数的类型
interface InitType {
el: string;
list: any[];
size: number;
speed: number;
modelUrl: string;
vector: object[];
position: object[];
autoRotate: boolean;
}
// 初始化模型
class Init {
list: object[];
gltfLoader: any;
model: InitType;
autoRotate: boolean = true;
canvas: HTMLElement | null = null;
constructor(data: InitType) {
this.clear();
// 参数存储
this.model = data;
// 开启动画
stopAnimationType = true;
// 设置是否自动动画
this.model.autoRotate === undefined || (() => this.autoRotate = this.model.autoRotate)();
// 速度判断
if (this.model.speed) speed = this.model.speed;
// 获取模型数据
this.model.list ? this.list = this.model.list : this.list = [];
// 获取当前渲染的dom元素
if (!this.model.el) throw new Error("请填写渲染的dom元素");
// 获取dom元素
this.canvas = document.querySelector(this.model.el) as HTMLElement;
// 相机设置
camera = new THREE.PerspectiveCamera(
this.model.size ? this.model.size : 28,
this.canvas.clientWidth / this.canvas.clientHeight,
0.01,
100
);
// 相机初始偏移角度
camera.position.set(
this.model.position && this.model.position.length && this.model.position[0] ? this.model.position[0] : -4,
this.model.position && this.model.position.length && this.model.position[1] ? this.model.position[1] : 9,
this.model.position && this.model.position.length && this.model.position[2] ? this.model.position[2] : 9
);
// 设置Vector3 x,y,z 三个分量,进行缩放
camera.lookAt(new THREE.Vector3(
this.model.vector && this.model.vector.length && this.model.vector[0] ? this.model.vector[0] : 1,
this.model.vector && this.model.vector.length && this.model.vector[0] ? this.model.vector[0] : 45,
this.model.vector && this.model.vector.length && this.model.vector[0] ? this.model.vector[0] : 20
));
// 创建场景对象Scene
scene = new THREE.Scene();
// 创建模型容器
this.gltfLoader = new GLTFLoader();
if (!this.model.modelUrl) {
throw new Error("请添加模型地址");
};
// 调用模型,加载模型
this.modeLoad();
animate();
};
public modeLoad() {
// 加载模型
this.gltfLoader.load(
this.model.modelUrl,
(gltf: any) => {
console.log(
`%c 模型通道 ↓↓↓,通过 name 判断`,
`color: #FFF;
height: 60px;
display: block;
font-size: 20px;
line-height: 60px;
padding-left: 50px;
padding-right: 80px;
background:#ff720df2;
border: 2px solid #FFF;
`
);
console.table(gltf.scene.children);
array = [];
this.list.forEach((item: any) => {
item.loadBear = gltf.scene.children.find((child: any) => {
return item.name == child.name;
});
array.push(item);
updateSticker(item.label, item.url);
});
nextTick(() => {
scene.add(gltf.scene);
});
},
(xhr: any) => {
if (xhr.loaded / xhr.total > 0) {
emit("load", Number((Number(xhr.loaded / xhr.total) * 100).toFixed(2)));
};
// console.log(xhr.loaded / xhr.total, "当前进度:" + (xhr.loaded / xhr.total) * 100 + "%");
},
(error: any) => {
console.log(error);
}
);
renderer = new THREE.WebGLRenderer({
canvas: this.canvas,
antialias: true,
});
renderer.setClearColor(0xfeffff, 0);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(this.canvas?.clientWidth, this.canvas?.clientHeight);
renderer.outputEncoding = THREE.sRGBEncoding;
controls = new OrbitControls(camera, renderer.domElement);
controls.maxPolarAngle = 1;
controls.minAzimuthAngle = 0;
controls.enablePan = true;
controls.autoRotate = this.autoRotate;
let obj: any = {
list: this.list,
model: this.model,
canvas: this.canvas,
}, that = this;
for (let key in obj) {
Object.defineProperty(that, key, {
get: function () {
return obj[key];
},
set: function (value) {
throw new Error("当前属性:'" + key + "'的值, 无法进行修改");
}
});
};
};
// 清空threejs中实例
public clear() {
if (scene) {
scene.traverse((child: any) => {
if (child.material) {
child.material.dispose();
}
if (child.geometry) {
child.geometry.dispose();
}
child = null;
});
};
if (renderer) {
renderer.forceContextLoss();
renderer.dispose();
renderer.domElement = null;
renderer = null;
};
if (scene) {
scene.clear();
scene = null;
};
camera = null;
stopAnimationType = false;
controls = null;
};
};
// 动画加载
const animate = () => {
if (stopAnimationType) {
controls.autoRotateSpeed = speed;
controls.update();
renderer.render(scene, camera);
id = requestAnimationFrame(animate);
clear(oldId as number);
oldId = id;
};
};
// 清除每次requestAnimationFrame产生的内存
const clear = (oldId: number) => {
if (oldId) {
cancelAnimationFrame(oldId);
};
};
// 更换材质
const updateSticker = (name: string, url: string = "") => {
array.forEach(async (item: any) => {
if (item.label == name && url) {
let getsheet = new THREE.TextureLoader().load(url);
getsheet.flipY = false;
getsheet.encoding = THREE.sRGBEncoding;
item.loadBear.material = await new THREE.MeshBasicMaterial({
map: getsheet,
});
}
// url 不存在时,但是有颜色的配置,就将颜色渲染到模型上去
else if (item.label == name && !url && item.color) {
let getsheet = new THREE.TextureLoader().load(url);
getsheet.flipY = false;
getsheet.encoding = THREE.sRGBEncoding;
item.loadBear.material = await new THREE.MeshBasicMaterial({
color: item.color
});
};
});
};
// 更换颜色
const updateColor = (color: string) => {
array.forEach(async (item: any) => {
if (item.label == "床板" || item.label == "床头垫") {
item.loadBear.material.color.set(color);
};
});
};
defineExpose({ Init, updateSticker, updateColor });
</script>
组件调用
<template>
<div id="tddis">
<modelCreation ref="Three" @load="load">
<div>
<canvas id="canvas">canvas>
div>
modelCreation>
<view class="maskLoading" v-if="data.maskLoading">
<van-loading color="#0094ff" size="60px" text-size="30px" vertical>模型已加载{{data.time}}···van-loading>
view>
div>
template>
<script setup lang="ts">
// 初始化类
let threeExample: any = null;
// 初始化组件实例
const Three: any = ref(null);
// 加载数据
const data: any = reactive({
time:null,
maskLoading: false,
});
onMounted(() => {
threeExample = new Three.value.Init({
el: "#canvas",
autoRotate: true, // 是否旋转模型
modelUrl: "./sijiantao3.glb", // 模型地址
list: [
{ label: "床板", name: "dizuo", url: "./chuangban.png" },
{ label: "床头垫", name: "kaobeiR", url: "./chuangtoudian.png" },
{ label: "被套", name: "Group22783", url: "", color: "#7b7573" }, // 如果没有模型贴图,可以添加颜色,如果有贴图,颜色不生效
{ label: "床单", name: "chuangli", url: "" },
{ label: "枕头1", name: "Plane003", url: "" },
{ label: "枕头2", name: "Plane003001", url: "" },
],
});
});
// 模型加载中,返回加载进度
const load = (val: number) => {
data.maskLoading = true;
data.time = val;
if(val == 100) {
data.maskLoading = false;
}
};
// 更换材质
const replaceTheMaterial = () => {
// updateSticker 方法两个参数
// 第一个参数,为对应 上面绑定的模型通道名称,比如: 床板的名称 dizuo
// 第二个参数没有可以不传,有的传材质 url 和 本地图片都可以
Three.value.updateSticker(row.classifyName);
};
// 更换颜色
const changeColor = () => {
// updateColor 方法两个参数
// 参数一:参数传入颜色值
// 参数二:传入对应通道的 label
Three.value.updateColor("#FF0000", "床板");
};
onBeforeUnmount(() => {
// 清除three
threeExample.clear();
});
</script>
点击跳转测试模型下载地址
被子(被套)贴图
床板、床头垫
床单
枕头1和枕头2(这张图是随便上传的,之前的枕头贴图找不到了,暂时用这个也是可以的)
有问题请反馈下,进行修改