threejs封装加载 .glb 格式模型,修改贴图

之前写项目时,模型默认加载出来的样例图,项目中可旋转
在这里插入图片描述

组件

<!--
 * @Autor: 
 * @Version: 1.0.0
 * @Date: 2022-11-10 16:23:45
 * @LastEditors: DESKTOP-KT9D4U6
 * @LastEditTime: 2022-12-20 11:03:02
 * @Description: Supermarket management system
 * @Author: desktop-mhum5pv
 * threejs 模型搭建
-->
<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 = (row: { classifyName: string }) => {
	// updateSticker 方法两个参数
	// 第一个参数,为对应 上面绑定的模型通道名称,比如: 床板的名称 dizuo
	// 第二个参数没有可以不传,有的传材质 url 和 本地图片都可以
	Three.value.updateSticker(row.classifyName);
};

// 更换颜色
const changeColor = () => {
	// updateColor 方法两个参数
	// 参数一:参数传入颜色值
	// 参数二:传入对应通道的 label
	Three.value.updateColor("#FF0000", "床板");
};

onBeforeUnmount(() => {
	// 清除three
	threeExample.clear();
});
</script>
<style lang="scss" scoped>
#tddis {
    width: 100vw;
    height: 100vh;
    overflow: hidden;
    box-sizing: border-box;
}
#canvas {
    width: 100vw;
	height: 100vh;
	box-sizing: border-box;
	background-color: rgba(0, 0, 0, 0.1) !important;
}

.maskLoading {
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    display: grid;
    position: fixed;
    place-items: center;
    background: rgba(0,0,0,0.6);
}
</style>

点击跳转测试模型下载地址

被子(被套)贴图

在这里插入图片描述

床板、床头垫

在这里插入图片描述

床单

在这里插入图片描述

枕头1和枕头2(这张图是随便上传的,之前的枕头贴图找不到了,暂时用这个也是可以的)

在这里插入图片描述

有问题请反馈下,进行修改

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 8
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

前端小袁

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值