跳到主要内容

汽车 3D 展示厅

预览

素材

3D 模型

安装

yarn add three

实现

Vue2.0 实现

:::details 点击查看代码

<template>
<div id="container">
<div class="maskLoading" v-if="isLoading">
<div class="loading">
<div :style="{ width: loadingWidth + '%' }"></div>
</div>
<div style="padding-left: 10px;">{{ parseInt(loadingWidth) }}%</div>
</div>
<div class="mask">
<p>x : {{ map.x }} y:{{ map.y }} z :{{ map.z }}</p>
<button @click="isAutoFun">转动车</button>
<button @click="stop">停止</button>
<div class="flex">
<div
@click="setCarColor(index)"
v-for="(item, index) in colorAry"
:key='index'
:style="{ backgroundColor: item }"
></div>
</div>
</div>
</div>
</template>

<script>
import {
Color,
DirectionalLight,
DirectionalLightHelper,
HemisphereLight,
HemisphereLightHelper,
PerspectiveCamera,
Scene,
WebGLRenderer,
} from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
export default {
components: {},
data() {
return {
width: "",
height: 500,
scene: null,
floor: null,
camera: null,
dhelper: null,
hHelper: null,
directionalLight: null,
hemisphereLight: null,
controls: null,
renderer: null,
loader: null,
isLoading: true,
loadingWidth: 0,
defaultMap: {
x: 0,
y: 0,
z: -6,
},
map: {
x: 0,
y: 0,
z: -6,
},
colorAry: [
"rgb(216, 27, 67)",
"rgb(142, 36, 170)",
"rgb(81, 45, 168)",
"rgb(48, 63, 159)",
"rgb(30, 136, 229)",
"rgb(0, 137, 123)",
"rgb(67, 160, 71)",
"rgb(251, 192, 45)",
"rgb(245, 124, 0)",
"rgb(230, 74, 25)",
"rgb(233, 30, 78)",
"rgb(156, 39, 176)",
"rgb(0, 0, 0)",
],
};
},
watch: {},
mounted() {
this.width = this.$el.clientWidth;
this.loader = new GLTFLoader();
this.init();
},
beforeDestroy() {
this.scene = null;
this.renderer=null;
this.camera = null;
this.controls = null;
this.floor = null;
this.dhelper = null;
this.hHelper = null;
this.directionalLight = null;
this.hemisphereLight = null;
this.loader = null;
},
methods: {
//创建场景
setScene() {
this.scene = new Scene();
this.renderer = new WebGLRenderer();
this.renderer.setSize(this.width, this.height);
this.$el.appendChild(renderer.domElement);
},
//创建灯光
setLight() {
this.directionalLight = new DirectionalLight(0xffffff, 1);
this.directionalLight.position.set(50, 50, -20);
this.dhelper = new DirectionalLightHelper(
this.directionalLight,
5,
0xff0000
);
this.hemisphereLight = new HemisphereLight(0xffffff, 0xffffff, 0.6);
this.hemisphereLight.position.set(0, 8, 0);
this.hHelper = new HemisphereLightHelper(this.hemisphereLight, 5);
this.scene.add(this.directionalLight);
this.scene.add(this.hemisphereLight);
},
// 创建场景
setScene() {
this.scene = new Scene();
this.renderer = new WebGLRenderer();
this.renderer.setSize(this.width, this.height);
this.$el.appendChild(this.renderer.domElement);
},
// 创建相机
setCamera() {
const { x, y, z } = this.defaultMap;
this.camera = new PerspectiveCamera(
60,
this.width / this.height,
1,
1000
);
this.camera.position.set(x, y, z);
},
// 设置模型控制
setControls() {
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
this.controls.maxPolarAngle = (0.9 * Math.PI) / 2;
this.controls.enableZoom = true;
this.controls.addEventListener("change", this.render);
},
//返回坐标信息
render() {
this.map.x = Number.parseInt(this.camera.position.x);
this.map.y = Number.parseInt(this.camera.position.y);
this.map.z = Number.parseInt(this.camera.position.z);
},
// 循环场景 、相机、 位置更新
loop() {
requestAnimationFrame(this.loop);
this.renderer.render(this.scene, this.camera);
this.controls.update();
},
//是否自动转动
isAutoFun() {
this.controls.autoRotate = true;
},
//停止转动
stop() {
this.controls.autoRotate = false;
},
//设置车身颜色
setCarColor(index) {
const currentColor = new Color(this.colorAry[index]);
this.scene.traverse((child) => {
if (child.isMesh) {
if (child.name.includes("Roof_Body")) {
child.material.color.set(currentColor);
}
}
});
},
loadFile(url) {
return new Promise((resolve, reject) => {
this.loader.load(
url,
(gltf) => {
resolve(gltf);
},
({ loaded, total }) => {
let load = Math.abs(loaded / total * 100);
this.loadingWidth = load;
if (load >= 100) {
setTimeout(() => {
this.isLoading = false;
}, 1000);
}
console.log((loaded / total * 100) + '% loaded')
},
(err) => {
reject(err);
}
);
});
},
//初始化所有函数
async init() {
this.setScene();
this.setCamera();
this.setLight();
this.setControls();
// 模型文件放到 public 目录下
const gltf = await this.loadFile("/images/model/scene.gltf");
this.scene.add(gltf.scene);
this.loop();
},
},
};
</script>

<style scoped>
#container {
position: relative;
}
.maskLoading {
height:500px;
background: #000;
position: absolute;
display: flex;
justify-content: center;
align-items: center;
top: 0;
left: 0;
bottom: 0;
right: 0;
z-index: 1111111;
color: #fff;
}

.maskLoading .loading {
width: 400px;
height: 20px;
border: 1px solid #fff;
background: #000;
overflow: hidden;
border-radius: 10px;
}

.maskLoading .loading div {
background: #fff;
height: 20px;
width: 0;
transition-duration: 500ms;
transition-timing-function: ease-in;
}

.mask {
color: #fff;
position: absolute;
bottom: 24px;
left: 48px;
width: 100%;
}

.flex {
display: flex;
flex-wrap: wrap;
padding: 20px;
padding-left: 0;
}

.flex div {
width: 10px;
height: 10px;
margin: 5px;
cursor: pointer;
}
</style>

:::

Vue3.0 实现

:::details 点击查看代码

<template>
<div class="boxs">
<div class="maskLoading" v-if="isLoading">
<div class="loading">
<div :style="{width: loadingWidth + '%'}"></div>
</div>
<div style="padding-left: 10px;">{{ parseInt(loadingWidth) }}%</div>
</div>
<div class="mask">
<p>x : {{ x }} y:{{ y }} z :{{ z }}</p>
<button @click="isAutoFun">转动车</button>
<button @click="stop">停止</button>
<div class="flex">
<div
@click="setCarColor(index)"
v-for="(item, index) in colorAry"
:key="index"
:style="{backgroundColor: item}"
></div>
</div>
</div>
</div>
</template>

<script lang="ts" setup>
import {onMounted, reactive, ref, toRefs} from 'vue';
import {
Color,
DirectionalLight,
DirectionalLightHelper,
HemisphereLight,
HemisphereLightHelper,
PerspectiveCamera,
Scene,
WebGLRenderer,
} from 'three';
import {OrbitControls} from 'three/examples/jsm/controls/OrbitControls.js';
import {GLTFLoader} from 'three/examples/jsm/loaders/GLTFLoader';
//车身颜色数组
const colorAry = [
'rgb(216, 27, 67)',
'rgb(142, 36, 170)',
'rgb(81, 45, 168)',
'rgb(48, 63, 159)',
'rgb(30, 136, 229)',
'rgb(0, 137, 123)',
'rgb(67, 160, 71)',
'rgb(251, 192, 45)',
'rgb(245, 124, 0)',
'rgb(230, 74, 25)',
'rgb(233, 30, 78)',
'rgb(156, 39, 176)',
'rgb(0, 0, 0)',
]; // 车身颜色数组
const loader = new GLTFLoader(); //引入模型的loader实例
const defaultMap = {
x: 0,
y: 0,
z: -6,
}; // 相机的默认坐标
const map = reactive(defaultMap); //把相机坐标设置成可观察对象
const {x, y, z} = toRefs(map); //输出坐标给模板使用
let scene,
camera,
renderer,
controls,
floor,
dhelper,
hHelper,
directionalLight,
hemisphereLight; // 定义所有three实例变量
const isLoading = ref(true); //是否显示loading 这个load模型监听的进度
const loadingWidth = ref(0); // loading的进度

//创建灯光
const setLight = () => {
directionalLight = new DirectionalLight(0xffffff, 1);
directionalLight.position.set(50, 50, -20);
dhelper = new DirectionalLightHelper(directionalLight, 5, 0xff0000);
hemisphereLight = new HemisphereLight(0xffffff, 0xffffff, 0.6);
hemisphereLight.position.set(0, 8, 0);
hHelper = new HemisphereLightHelper(hemisphereLight, 5);
scene.add(directionalLight);
scene.add(hemisphereLight);
};

// 创建场景
const setScene = () => {
scene = new Scene();
renderer = new WebGLRenderer();
renderer.setSize(innerWidth, innerHeight);
document.querySelector('.boxs').appendChild(renderer.domElement);
};

// 创建相机
const setCamera = () => {
const {x, y, z} = defaultMap;
camera = new PerspectiveCamera(60, innerWidth / innerHeight, 1, 1000);
camera.position.set(x, y, z);
};

// 设置模型控制
const setControls = () => {
controls = new OrbitControls(camera, renderer.domElement);
controls.maxPolarAngle = (0.9 * Math.PI) / 2;
controls.enableZoom = true;
controls.addEventListener('change', render);
};

//返回坐标信息
const render = () => {
map.x = Number.parseInt(camera.position.x);
map.y = Number.parseInt(camera.position.y);
map.z = Number.parseInt(camera.position.z);
};

// 循环场景 、相机、 位置更新
const loop = () => {
requestAnimationFrame(loop);
renderer.render(scene, camera);
controls.update();
};

//是否自动转动
const isAutoFun = () => {
controls.autoRotate = true;
};
//停止转动
const stop = () => {
controls.autoRotate = false;
};

//设置车身颜色
const setCarColor = (index) => {
const currentColor = new Color(colorAry[index]);
scene.traverse((child) => {
if (child.isMesh) {
if (child.name.includes('Roof_Body')) {
child.material.color.set(currentColor);
}
}
});
};

const loadFile = (url) => {
return new Promise((resolve, reject) => {
loader.load(
url,
(gltf) => {
resolve(gltf);
},
({loaded, total}) => {
const load = Math.abs((loaded / total) * 100);
loadingWidth.value = load;
if (load >= 100) {
setTimeout(() => {
isLoading.value = false;
}, 1000);
}
console.log((loaded / total) * 100 + '% loaded');
},
(err) => {
reject(err);
},
);
});
};

//初始化所有函数
const init = async () => {
setScene();
setCamera();
setLight();
setControls();
// 模型文件放到 public 目录下
const gltf = await loadFile('/model/scene.gltf');
console.log(gltf);
scene.add(gltf.scene);
loop();
};
//用vue钩子函数调用
onMounted(init);
</script>

<style>
body {
margin: 0;
}

.maskLoading {
background: #000;
position: fixed;
display: flex;
justify-content: center;
align-items: center;
top: 0;
left: 0;
bottom: 0;
right: 0;
z-index: 1111111;
color: #fff;
}

.maskLoading .loading {
width: 400px;
height: 20px;
border: 1px solid #fff;
background: #000;
overflow: hidden;
border-radius: 10px;
}

.maskLoading .loading div {
background: #fff;
height: 20px;
width: 0;
transition-duration: 500ms;
transition-timing-function: ease-in;
}

canvas {
width: 100%;
height: 100%;
margin: auto;
}

.mask {
color: #fff;
position: absolute;
bottom: 0;
left: 0;
width: 100%;
}

.flex {
display: flex;
flex-wrap: wrap;
padding: 20px;
}

.flex div {
width: 10px;
height: 10px;
margin: 5px;
cursor: pointer;
}
</style>

:::

React 实现