Vue3 - Three-js 商城可視化實戰
作者:前端了liaoliao
https://juejin.cn/post/7137192060045492231
實戰目的
根據不同的產品配合接口展示相應的描述。根據選擇的場景及其物品實現可視化的產品展示效果。效果展示
支持不同位置展示不同描述:配合數據配置渲染不同楨的效果
根據選中的產品,切換相應產品效果
根據選中場景,切換相應的場景
實現思路
封裝一個 Three 的函數,支持設置相機、場景、渲染函數,添加模型解析器,添加物品,整合渲染效果,添加事件監聽,完善模型動畫展示
具體實現
使用 vite 搭建一個項目,後安裝 Three 支持,進行具體實現
準備 vue 項目
- 使用 Vite + Vue[1] 搭建
# npm 6.x
npm init vite@latest my-vue-app --template vue
# npm 7+, 需要額外的雙橫線:
npm init vite@latest my-vue-app -- --template vue
# yarn
yarn create vite my-vue-app --template vue
# pnpm
pnpm create vite my-vue-app -- --template vue
- 根據自己的環境選擇自己的搭建代碼
npm init vite@latest my-vue-app -- --template vue
- 根據提示創建項目
- 確認項目正常訪問
安裝 Three
npm install --save three
刪除無用代碼,添加渲染節點
增加一個場景展示的 div,用於渲染 3D 信息
Three 實戰
加載場景及控制器
初始化場景 HDR 圖片
// 初始化場景
initScene() {
this.scene = new THREE.Scene();
this.setEnvMap("001");
}
// 場景設置
setEnvMap(hdr) {
new RGBELoader().setPath("./hdr/").load(`${hdr}.hdr`, (texture) => {
texture.mapping = THREE.EquirectangularRefractionMapping;
this.scene.background = texture;
this.scene.environment = texture;
});
}
確定相機位置
initCamera() {
this.camera = new THREE.PerspectiveCamera(
45, // 角度
window.innerWidth / window.innerHeight, // 比例
0.25, // 近
200 // 遠
);
// 相機位置
this.camera.position.set(-1.8, 0.6, 2.7);
}
渲染:根據相機位置和場景圖渲染初始畫面
render() {
this.renderer.render(this.scene, this.camera);
}
動畫:渲染初始畫面
animate() {
this.renderer.setAnimationLoop(this.render.bind(this));
}
此時,這個頁面只能展示出部分的靜態畫面,要想通過鼠標控制相機的位置,則需要增加控制器
引入控制器
// 控制器
initControls() {
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
}
加入控制器後,則可以通過鼠標的滑動控制相機的角度
增加產品模型
引入模型解析器
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
添加模型到場景裏
setModel(modelName) {
const loader = new GLTFLoader().setPath("/gltf/");
loader.load(modelName, (gltf) => {
this.model = gltf.scene.children[0];
this.scene.add(gltf.scene);
});
}
// 添加模型
async addMesh() {
let res = await this.setModel("bag2.glb");
}
模型已經加入到場景裏,但是模型不在場景中間🤔️,模型比較亮,真實物品看不清楚
打印一下模型解析後的數據,我們可以看到模型有自己的相機場景動畫等信息,我們可以把當前相應的設置調整成模型的設置
模型調整
- 調整相機爲模型相機
setModel(modelName) {
...
// 修改相機爲模型相機
this.camera = gltf.cameras[0];
...
}
調整後模型位置在畫面中間
- 調整場景其他配置
// 設置模型
setModel(modelName) {
return new Promise((resolve, reject) => {
const loader = new GLTFLoader().setPath("./gltf/");
loader.load(modelName, (gltf) => {
console.log(gltf);
this.model && this.model.removeFromParent();
this.model = gltf.scene.children[0];
if (modelName === "bag2.glb" && !this.dish) {
this.dish = gltf.scene.children[5];
// 修改相機爲模型相機
this.camera = gltf.cameras[0];
// 調用動畫
this.mixer = new THREE.AnimationMixer(gltf.scene.children[1]);
this.animateAction = this.mixer.clipAction(gltf.animations[0]);
// 設置動畫播放時長
this.animateAction.setDuration(20).setLoop(THREE.LoopOnce);
// 設置播放後停止
this.animateAction.clampWhenFinished = true;
// 設置燈光
this.spotlight1 = gltf.scene.children[2].children[0];
this.spotlight1.intensity = 1;
this.spotlight2 = gltf.scene.children[3].children[0];
this.spotlight2.intensity = 1;
this.spotlight3 = gltf.scene.children[4].children[0];
this.spotlight3.intensity = 1;
// this.scene.add(this.dish);
}
this.scene.add(gltf.scene);
resolve(`${this.modelName}模型添加成功`);
});
});
}
調整參數後的產品展示效果
- 定時器和滾輪監聽動畫
// 添加定時器
render() {
var delta = this.clock.getDelta();
this.mixer && this.mixer.update(delta);
this.renderer.render(this.scene, this.camera);
}
// 監聽滾輪事件
window.addEventListener("mousewheel", this.onMouseWheel.bind(this));
// 監聽滾輪事件
onMouseWheel(e) {
let timeScale = e.deltaY > 0 ? 1 : -1;
this.animateAction.setEffectiveTimeScale(timeScale);
this.animateAction.paused = false;
this.animateAction.play();
if (this.timeoutId) {
clearTimeout(this.timeoutId);
}
this.timeoutId = setTimeout(() => {
this.animateAction.halt(0.3);
}, 300);
}
場景和產品模型都添加成功,結合動畫,可以進行產品的預覽
添加窗口監聽事件
調整頁面窗口時,保證場景的全屏展示
// 監聽場景大小改變,調整渲染尺寸
window.addEventListener("resize", this.onWindowResize.bind(this));
// 監聽尺寸
onWindowResize() {
this.camera.aspect = window.innerWidth / window.innerHeight;
this.camera.updateProjectionMatrix();
this.renderer.setSize(window.innerWidth, window.innerHeight);
}
優化加載
模型加載成功後在展示
constructor(selector, onFinish) {
this.onFinish = onFinish;
}
// 添加物品增加回調函數
async addMesh() {
let res = await this.setModel("bag2.glb");
this.onFinish(res);
}
增加商品的介紹
根據 duration 和 time 計算當前處於哪部門節點
window.addEventListener("mousewheel", (e) => {
let duration = data.base3d.animateAction._clip.duration;
let time = data.base3d.animateAction.time;
let index = Math.floor((time / duration) * 4);
data.descIndex = index;
});
增加選擇場景和產品
創建數據增加操作的 dom
<template>
<div class="loading" v-show="data.isLoading">
<Loading :progress="data.progress"></Loading>
</div>
<div class="product" id="product" v-show="!data.isLoading">
<div
class="desc"
:class="{ active: data.descIndex == i }"
v-if="data.products[data.pIndex]"
v-for="(desc, i) in data.products[data.pIndex].desc"
>
<h1 class="title">{{ desc.title }}</h1>
<p class="content">{{ desc.content }}</p>
</div>
</div>
<div class="prod-list">
<h1><SketchOutlined></SketchOutlined>產品推薦</h1>
<div class="products">
<div
class="prod-item"
:class="{ active: pI == data.pIndex }"
v-for="(prod, pI) in data.products"
@click="changeModel(prod, pI)"
>
<div class="prod-title">
{{ prod.title }}
</div>
<div class="img">
<img :src="prod.imgsrc" :alt="prod.title" />
</div>
</div>
</div>
</div>
<div class="scene" id="scene" v-show="!data.isLoading"></div>
<div class="scene-list">
<h3><RadarChartOutlined></RadarChartOutlined> 切換使用場景</h3>
<div class="scenes">
<div
class="scene-item"
v-for="(scene, index) in data.scenes"
@click="changeHdr(scene, index)"
>
<img
:class="{ active: index == data.sceneIndex }"
:src="`./hdr/${scene}.jpg`"
:alt="scene"
/>
</div>
</div>
</div>
</template>
<script setup>
import Base3D from "../utils/base3d";
import { reactive, onMounted } from "vue";
const infoList = [
{
id: 7589,
title: "GUCCI 古馳新款女包",
imgsrc: "./imgs/bag.png",
price: 17899,
modelPath: "./gltf/",
modelName: "bag2.glb",
desc: [
{
title: "與一款全新的郵差包設計。",
content: "該系列手袋同時推出摩登廓形的水桶包款式",
},
{
title: "向60年前古馳的經典手袋致敬。",
content: "Gucci 1955馬銜扣系列手袋延續經典手袋線條與造型",
},
{
title: "手袋結構設計精巧",
content: "搭配可調節長度的肩帶,肩背或斜挎皆宜。",
},
{
title: "GUCCI 1955馬銜扣系列手袋",
content:
"標誌性的馬銜扣設計源於馬術運動,由金屬雙環和一條銜鏈組合而成。",
},
],
},
{
id: 7590,
title: "Macbook Pro",
imgsrc: "./imgs/macpro.jpg",
price: 25899,
modelPath: "./gltf/",
modelName: "Macbookpro2.glb",
desc: [
{
title: "超高速M1 Pro和M1 Max芯片",
content: "帶來顛覆性表現和驚人續航",
},
{
title: "炫目的Liquid視網膜XDR顯示屏",
content: "Macbookpro各類強大端口也都整裝就位",
},
{
title: "戰力更猛,耐力也更強!",
content: "無論是剪輯8K視頻、編譯代碼都能隨時隨地輕鬆搞定",
},
{
title: "Pro到MAX,霸氣不封頂",
content: "圖形處理器速度最高提升至4倍,機器學習性能提升至5倍",
},
],
},
{
id: 7591,
title: "水晶涼鞋女細跟",
imgsrc: "./imgs/womenshoes.jpg",
price: 17899,
modelPath: "./gltf/",
modelName: "shoes.glb",
desc: [
{ title: "白變女神季", content: "性感潮品、優雅輕淑範!" },
{ title: "舒適、煥新", content: "手感光滑、富有彈性、舒適一整天" },
{ title: "個性、魅力", content: "水晶搭配金屬,凸顯柔美氣質" },
{ title: "全透、高端水晶", content: "每一處的細節,都很到位!" },
],
},
];
const hdr = ["000", "001", "002", "003", "004", "005"];
const data = reactive({
products: [],
isLoading: true,
scenes: [],
pIndex: 0,
sceneIndex: 0,
base3d: {},
descIndex: 0,
progress: 0,
});
function LoadingFinish() {
data.isLoading = false;
}
onMounted(() => {
data.products = infoList;
data.scenes = hdr;
data.base3d = new Base3D("#scene", LoadingFinish);
data.base3d.onProgress((e) => {
let progressNum = e.loaded / e.total;
progressNum = progressNum.toFixed(2) * 100;
data.progress = progressNum;
// console.log(progressNum);
});
});
window.addEventListener("mousewheel", (e) => {
console.log("🚀.animateAction", data.base3d.animateAction);
let duration = data.base3d.animateAction._clip.duration;
let time = data.base3d.animateAction.time;
let index = Math.floor((time / duration) * 4);
data.descIndex = index;
});
</script>
<style scoped>
.desc {
position: fixed;
z-index: 100000;
background-color: rgba(255, 255, 255, 0.5);
width: 600px;
top: 100px;
left: 50%;
margin-left: -300px;
transition: all 0.5s;
transform: translate(-100vw, 0);
padding: 15px;
}
.desc.active {
transform: translate(0, 0);
}
.prod-list,
.scene-list {
display: block;
position: fixed;
top: 0;
z-index: 999;
width: auto;
}
h1 {
font-size: 20px;
font-weight: 900;
padding: 10px 25px 0;
}
.prod-list {
left: 0;
}
.scene-list {
right: 0;
}
.products {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.prod-item {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 250px;
background-color: rgba(255, 255, 255, 0.5);
border-radius: 20px;
overflow: hidden;
margin: 10px 0;
box-shadow: 2px 2px 5px #666;
transition: all 0.3s;
}
.prod-item img {
width: 190px;
}
.prod-title {
padding: 0 20px;
}
.scene-item {
padding: 6px 0;
}
.scene-item img {
width: 250px;
border-radius: 10px;
box-shadow: 2px 2px 10px #666;
transition: all 0.3s;
}
img.active {
box-shadow: 2px 2px 5px #666, 0px 0px 10px red;
}
img:hover {
transform: translate(0px, -5px);
box-shadow: 2px 2px 5px #666, 0px 0px 10px orangered;
}
</style>
增加操作事件
function changeModel(prod, pI) {
data.pIndex = pI;
data.base3d.setModel(prod.modelName);
}
function changeHdr(scene, index) {
data.sceneIndex = index;
data.base3d.setEnvMap(scene);
}
大功告成
支持不同位置展示不同描述:配合數據配置渲染不同楨的效果
根據選中的產品,切換相應產品效果
根據選中場景,切換相應的場景
總結
-
通過類的方式創建的方法,能夠很好的保存了創建過程中 3D 模型的所具備的屬性和功能,在實例化後,可以很便捷的獲取到相應的屬性
-
在創建場景 / 模型時,可以根據要突出的效果調整相應的參數,我們可以認真觀察創建出來的實例對象中包含的屬性和方法,方便我們渲染使用
參考包 / 支持
-
Three.js[2]
-
本項目參考視頻 [3]
-
源碼 [4]
參考資料
[1]
vite 中文網: https://vitejs.cn/guide/#scaffolding-your-first-vite-project
[2]
Three.js: http://www.webgl3d.cn/Three.js/
[3]
threejs 打造沉浸式商城 2022 全新 Vue3 企業項目實戰: https://www.bilibili.com/video/BV15T4y1175F/?p=21&vd_source=797532e4fa3575d6c48e18321f8de472
[4]
源碼: https://gitee.com/yueliangliaoliao/vue-three
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/7J8QTE-psJpSRgXnuB1Z0g