前言
在上一篇文章简单粗略的描述了开发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} = defaultMapcamera = 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,dhelperlet 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} = defaultMapcamera = 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 = loadif (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,hHelperlet 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} = defaultMapcamera = 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 = loadif (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,controlslet 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} = defaultMapcamera = 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 = loadif (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 / 2controls.enableZoom = truecontrols.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} = defaultMapcamera = 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 / 2controls.enableZoom = truecontrols.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 = loadif (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汽车展示厅笔者只是简单创建了一些基础功能,还有很多功能可以增加,比如创建背景、地板、一些阴影、定点显示车套件位置信息等等。掌握套路,这些功能实现也不难。如果喜欢就给小弟点个赞,谢谢啦 _