前言
在上一篇文章简单粗略的描述了开发3D汽车展厅,笔者想再写一篇比较详细的教程。对于笔者来说Three.js说难不难,说简单也不简单。说简单因为他简化了对三维知识的理解,简化了很多操作。说难是因为api很多,要用熟也不是一朝半夕的时间。笔者在这给大家介绍一下以最简单方式打造一个3D汽车展示厅。这个3D汽车展厅实现出来也不算完整,主要想让同学们找找感觉,找些成就感,有感觉自己也有学下去的动力。_
简单粗略了解三维
在2D里只有两个坐标,分别是X轴,和Y轴。在3D就多了一个Z轴。相信刚学3D的同学对X轴和Y轴都比较熟悉,Z轴是比较陌生,笔者建议大家可以上 three编辑器的网站尝试创建一些几何物体,找找对3D理解。
完整效果
需要了解这几个概念
笔者用舞台表演来比如:
- 场景
Sence
相当于在一个舞台,在这里是布置场景物品和表演者表演的地方 - 相机
Carma
相当于观众的眼睛去观看 - 几何体
Geometry
相当于舞台的表演者 - 灯光
light
相当于舞台灯光照射 - 控制
Controls
相当于这出舞台剧的总导演
既然知道这几个概念,我们就根据这几大概念以函数形式区分,就很好理解。在这个three程序我分别创建了:
setScene
、setCarma
、 loadfile
、setLight
、setControls
分别对应以上几个概念
创建场景
首先我们还是用vue3的setup方式编写,npm安装three包, 引入 Scene
,WebGLRenderer
两个对象,创建两个变量 scene
、renderer
并赋值,这样就简单搭建了一个场景,场景背景默认是黑色。创建一个init
初始化函数,并在onMounted
调用
<script setup>
import {onMounted} from 'vue'
import { Scene,WebGLRenderer,PerspectiveCamera} from 'three'
let scene,renderer
//创建场景
const setScene = ()=>{
scene = new Scene()
renderer = new WebGLRenderer()
renderer.setSize(innerWidth, innerHeight)
document.querySelector('.boxs').appendChild(renderer.domElement)
}
//初始化所有函数
const init = () => {
setScene()
}
//用vue钩子函数调用
onMounted(init)
</script>
创建相机
有了场景就要加相机,相机相当于人的眼睛去观察几何物体,引入PerspectiveCamera
,
参数有4个,具体可以看看官网文档。然后通过实例方法position.set
设置相机坐标
<script setup>
import { Scene,WebGLRenderer,PerspectiveCamera} from 'three'
let scene,renderer,camera
//相机的默认坐标
const defaultMap = {
x: 510,
y: 128,
z: 0,
}
//创建场景
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 init = () => {
setScene()
setCamera()
}
//用vue钩子函数调用
onMounted(init)
</script>
引入特斯拉模型
在three我们除了可以通过api创建几何物体,还可以引入第三方3d模型,具体可以上 sketchfab ,国外一个3d模型下载网站,里面有很多免费的模型下载,这次用特斯拉汽车模型为例,下载一个
gltf
格式的3D模型。引入GLTFLoader
创建一个loadfile
函数并通过Promise返回模型数据,在init
函数加上async
调用loadfile
得到返回模型数据并添加到场景scene
import {GLTFLoader} from 'three/examples/jsm/loaders/GLTFLoader'
import { Scene,WebGLRenderer,PerspectiveCamera} from 'three'
let scene,renderer,camera,directionalLight,dhelper
let isLoading = ref(true)
let loadingWidth = ref(0)
//相机的默认坐标
const defaultMap = {
x: 510,
y: 128,
z: 0,
}
//创建场景
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)
}
//通过Promise处理一下loadfile函数
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() => {
const gltf =await loadFile('src/assets/3d/tesla_2018_model_3/scene.gltf')
setScene()
setCamera()
scene.add(gltf.scene)
}
//用vue钩子函数调用
onMounted(init)
创建灯光
汽车模型还看不见,所以我们要给它设置灯光,引入DirectionalLight
,DirectionalLightHelper
,HemisphereLight
,HemisphereLightHelper
,并设置灯光的参数,使模型可见,并有些反射光面,阴影的效果,然后也在init
函数调用 setLight
,再增加loop
函数,使场景、照相机、模型不停循环调用。然后车模型就能看见啦,看到车出现的那一刻,好像自己的新买的一样,_
import {GLTFLoader} from 'three/examples/jsm/loaders/GLTFLoader'
import { Scene,WebGLRenderer,PerspectiveCamera, DirectionalLight,
DirectionalLightHelper,
HemisphereLight,
HemisphereLightHelper} from 'three'
let scene,renderer,camera,directionalLight,hemisphereLight,dhelper,hHelper
let isLoading = ref(true)
let loadingWidth = ref(0)
//相机的默认坐标
const defaultMap = {
x: 510,
y: 128,
z: 0,
}
//创建场景
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)
}
//通过Promise处理一下loadfile函数
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 setLight = () => {
directionalLight = new DirectionalLight(0xffffff, 0.5)
directionalLight.position.set(-4, 8, 4)
dhelper = new DirectionalLightHelper(directionalLight, 5, 0xff0000)
hemisphereLight = new HemisphereLight(0xffffff, 0xffffff, 0.4)
hemisphereLight.position.set(0, 8, 0)
hHelper = new HemisphereLightHelper(hemisphereLight, 5)
scene.add(directionalLight)
scene.add(hemisphereLight)
}
//初始化所有函数
const init = async() => {
const gltf = await loadFile('src/assets/3d/tesla_2018_model_3/scene.gltf')
setScene()
setCamera()
setLight()
scene.add(gltf.scene)
loop()
}
//使场景、照相机、模型不停调用
const loop = () => {
requestAnimationFrame(loop)
renderer.render(scene, camera)
}
//用vue钩子函数调用
onMounted(init)
控制模型
想用鼠标自由旋转,或者自动旋转,就要引用 OrbitControls
对象,创建setControls
函数也是在init
调用,通过绑定change
还可以监听坐标变化,另外也要在loop
函数增加
controls.update()
才可以更新位置变化
import { Scene,WebGLRenderer,PerspectiveCamera} from 'three'
import {GLTFLoader} from 'three/examples/jsm/loaders/GLTFLoader'
import {OrbitControls} from 'three/examples/jsm/controls/OrbitControls.js'
let scene,renderer,camera,directionalLight,hemisphereLight,dhelper,hHelper,controls
let isLoading = ref(true)
let loadingWidth = ref(0)
//相机的默认坐标
const defaultMap = {
x: 510,
y: 128,
z: 0,
}
//创建场景
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)
}
//通过Promise处理一下loadfile函数
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 setLight = () => {
directionalLight = new DirectionalLight(0xffffff, 0.5)
directionalLight.position.set(-4, 8, 4)
dhelper = new DirectionalLightHelper(directionalLight, 5, 0xff0000)
hemisphereLight = new HemisphereLight(0xffffff, 0xffffff, 0.4)
hemisphereLight.position.set(0, 8, 0)
hHelper = new HemisphereLightHelper(hemisphereLight, 5)
scene.add(directionalLight)
scene.add(hemisphereLight)
}
// 设置模型控制
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 init = async() => {
const gltf =await loadFile('src/assets/3d/tesla_2018_model_3/scene.gltf')
setScene()
setCamera()
setLight()
setControls()
scene.add(gltf.scene)
}
//使场景、照相机、模型不停调用和更新位置数据
const loop = () => {
requestAnimationFrame(loop)
renderer.render(scene, camera)
controls.update()
}
//用vue钩子函数调用
onMounted(init)
改变车身颜色
到这里基础的已经搭建好了,接下来我们再加一个功能改变汽车车身颜色,也是展厅展示一个比较基础的功能.创建一个setColor
这里说一下实例scene
有一个traverse
函数,它回调了所有模型的子模型信息,只要我们找到对应name属性,就可以更改颜色,和增加贴图等等,
因为对模型结构不怎熟悉,所以根据名字来猜了一下 找到door_
前序的名字大概应该就车身的套件。当然如果细分到我只想改引擎盖的颜色就要找出引擎盖套件。当然要很熟悉这个模型结构了
//设置车身颜色
const setCarColor = (index) => {
const currentColor = new Color(colorAry[index])
scene.traverse(child => {
if (child.isMesh) {
console.log(child.name)
if (child.name.includes('door_')) {
child.material.color.set(currentColor)
}
}
})
}
上完整代码
其他操作都是交给vue控制,包括设置车身颜色、是否自动转动等等。大家运行以下代码的时候记得用vite
打包工具创建vue3模板
<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>
<script 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: 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 // 定义所有three实例变量
let isLoading = ref(true) //是否显示loading 这个load模型监听的进度
let loadingWidth = ref(0)// loading的进度
//创建灯光
const setLight = () => {
directionalLight = new DirectionalLight(0xffffff, 0.5)
directionalLight.position.set(-4, 8, 4)
dhelper = new DirectionalLightHelper(directionalLight, 5, 0xff0000)
hemisphereLight = new HemisphereLight(0xffffff, 0xffffff, 0.4)
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) {
console.log(child.name)
if (child.name.includes('door_')) {
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/tesla_2018_model_3/scene.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>
最后
在这个3D汽车展示厅笔者只是简单创建了一些基础功能,还有很多功能可以增加,比如创建背景、地板、一些阴影、定点显示车套件位置信息等等。掌握套路,这些功能实现也不难。希望大家如果喜欢能给小弟点个赞,谢谢啦 _