问题
公司需要我调研一下 VRAR 展示小车,我就想起 ThreeJS 了,在网上找了一下 Demo 和教程
Demo
https://threejs.org/examples/#webgl_materials_car
看到这个 Demo 我立刻知道这是我想要的了
开发过程中碰到的关键点
模型
模型可以在 https://sketchfab.com 下载,选择 glTF 格式
模型编辑器
可以用 Blender 编辑模型,但是我不会用,所以就不介绍了
模型加载
const loader = new GLTFLoader() //引入模型的 loader 实例
const gltf = await loadFile('src/assets/3d/2022_rolls-royce_phantom_extended_series_ii/scene.gltf')
const model = gltf.scene;
scene.add(model)
模型颜色设置
这个是设置模型颜色的代码,scene.traverse
方法可以遍历所有的模型,child.isMesh
判断是否是模型,child.name
判断模型的名称,child.material.color.set
设置模型颜色
const setCarColor = (index) => {
const currentColor = new Color(colorAry[index])
scene.traverse(child => {
if (child.isMesh) {
console.log(child.name)
if (child.name == 'Object_6') {
child.material.color.set(currentColor)
}
}
})
}
有趣的是我设置其中一个模型颜色的时候,其他模型的颜色也会改变,后来发现是因为这些模型的材质是共用的。如果想单独修改这个模型的颜色,可以用 child.material = child.material.clone()
克隆一个材质,然后再设置颜色
const setCarColor = (index) => {
const currentColor = new Color(colorAry[index])
scene.traverse(child => {
if (child.isMesh) {
console.log(child.name)
if (child.name == 'Object_6') {
child.material = child.material.clone()
child.material.color.set(currentColor)
}
}
})
}
模型加载进度
一般来说模型很大,最好弄个进度条
const loadFile = (url) => {
return new Promise(((resolve, reject) => {
loader.load(url,
(gltf) => {
resolve(gltf)
}, ({ loaded, total }) => {
let 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 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()
}
环境贴图
车转动的时候,需要车身有反光的效果,所以需要设置环境贴图
scene.environment = new RGBELoader().load('src/assets/textures/equirectangular/venice_sunset_1k.hdr');
scene.environment.mapping = THREE.EquirectangularReflectionMapping;
renderer.toneMapping = THREE.LinearToneMapping;
renderer.toneMappingExposure = 1;
灯光
车看上去黑不溜秋的是因为没有光,所以需要设置灯光
const setLight = () => {
directionalLight = new THREE.DirectionalLight( 0xffffff, 0.5 );
directionalLight.position.set( 0, 1, 0 );
scene.add( directionalLight );
hemisphereLight = new THREE.HemisphereLight( 0xffffff, 0x000000, 0.4 );
scene.add( hemisphereLight );
}
源码
人狠话不多直接上源码
<script setup>
import { onMounted, reactive, ref, toRefs } from 'vue'
import {
Color,
Scene,
WebGLRenderer,
} from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
import * as THREE from 'three';
import { RGBELoader } from 'three/addons/loaders/RGBELoader.js';
//车身颜色数组
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: 510,
y: 128,
z: 0,
}// 相机的默认坐标
const map = reactive(defaultMap)//把相机坐标设置成可观察对象
const { x, y, z } = toRefs(map)//输出坐标给模板使用
let scene, camera, renderer, controls, floor, dhelper, hHelper, directionalLight, hemisphereLight, grid // 定义所有 three 实例变量
let isLoading = ref(true) //是否显示 loading 这个 load 模型监听的进度
let loadingWidth = ref(0)// loading 的进度
//创建灯光
const setLight = () => {
}
// 创建场景
const setScene = () => {
scene = new Scene()
// 设置背景颜色为白色
scene.background = new Color(0xffffff);
scene.environment = new RGBELoader().load('src/assets/textures/equirectangular/venice_sunset_1k.hdr');
scene.environment.mapping = THREE.EquirectangularReflectionMapping;
renderer = new WebGLRenderer({
antialias: true,
})
renderer.setSize(innerWidth, innerHeight)
renderer.toneMapping = THREE.LinearToneMapping;
renderer.toneMappingExposure = 1;
document.querySelector('.boxs').appendChild(renderer.domElement)
renderer.shadowMapEnabled = true
}
// 创建相机
const setCamera = () => {
camera = new THREE.PerspectiveCamera( 40, window.innerWidth / window.innerHeight, 0.1, 100 );
camera.position.set( - 6.5, 2, 4 );
}
// 设置模型控制
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) {
console.log(child.name)
if (child.name == 'Object_6') {
child.material.color.set(currentColor)
}
}
})
}
const loadFile = (url) => {
return new Promise(((resolve, reject) => {
loader.load(url,
(gltf) => {
resolve(gltf)
}, ({ loaded, total }) => {
let 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()
const gltf = await loadFile('src/assets/3d/2022_rolls-royce_phantom_extended_series_ii/scene.gltf')
const model = gltf.scene;
scene.add(model)
loop()
}
//用 vue 钩子函数调用
onMounted(init)
</script>
<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" :style="{ backgroundColor: item }"></div>
</div>
</div>
</div>
</template>
<style scoped>
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>
资源
- glTF Viewer glTF 查看器
- gltfeditor glTF 编辑器
- 3dviewer 3D 查看器
- Sketchfab 在这下载模型
- Blender 模型编辑器
参考
感谢
开发过程中碰到了一些问题,和群友 undefined 讨论很多,感谢他的帮助