用最簡(jiǎn)單方式打造Three.js 3D汽車(chē)展示廳在上一篇文章簡(jiǎn)單粗略的描述了開(kāi)發(fā)3D汽車(chē)展廳,筆者再寫(xiě)一篇比較詳細(xì)的教程
前言
在上一篇文章簡(jiǎn)單粗略的描述了開(kāi)發(fā)3D汽車(chē)展廳,筆者想再寫(xiě)一篇比較詳細(xì)的教程。對(duì)于筆者來(lái)說(shuō)Three.js說(shuō)難不難,說(shuō)簡(jiǎn)單也不簡(jiǎn)單。說(shuō)簡(jiǎn)單因?yàn)樗?jiǎn)化了對(duì)三維知識(shí)的理解,簡(jiǎn)化了很多操作。說(shuō)難是因?yàn)閍pi很多,要用熟也不是一朝半夕的時(shí)間。筆者在這給大家介紹一下以最簡(jiǎn)單方式打造一個(gè)3D汽車(chē)展示廳。這個(gè)3D汽車(chē)展廳實(shí)現(xiàn)出來(lái)也不算完整,主要想讓同學(xué)們找找感覺(jué),找些成就感,有感覺(jué)自己也有學(xué)下去的動(dòng)力。^_^
簡(jiǎn)單粗略了解三維
在2D里只有兩個(gè)坐標(biāo),分別是X軸,和Y軸。在3D就多了一個(gè)Z軸。相信剛學(xué)3D的同學(xué)對(duì)X軸和Y軸都比較熟悉,Z軸是比較陌生,筆者建議大家可以上 three編輯器的網(wǎng)站嘗試創(chuàng)建一些幾何物體,找找對(duì)3D理解。
完整效果
需要了解這幾個(gè)概念
筆者用舞臺(tái)表演來(lái)比如:
場(chǎng)景 Sence 相當(dāng)于一個(gè)舞臺(tái),在這里是布置場(chǎng)景物品和表演者表演的地方 相機(jī) Carma 相當(dāng)于觀眾的眼睛去觀看 幾何體 Geometry 相當(dāng)于舞臺(tái)的表演者 燈光 light 相當(dāng)于舞臺(tái)燈光照射 控制 Controls 相當(dāng)于這出舞臺(tái)劇的總導(dǎo)演既然知道這幾個(gè)概念,我們就根據(jù)這幾大概念以函數(shù)形式區(qū)分,就很好理解。在這個(gè)three程序我分別創(chuàng)建了: setScene、setCarma、 loadfile、setLight、setControls分別對(duì)應(yīng)以上幾個(gè)概念
創(chuàng)建場(chǎng)景
首先我們還是用vue3的setup方式編寫(xiě),npm安裝three包, 引入 Scene,WebGLRenderer 兩個(gè)對(duì)象,創(chuàng)建兩個(gè)變量 scene、renderer并賦值,這樣就簡(jiǎn)單搭建了一個(gè)場(chǎng)景,場(chǎng)景背景默認(rèn)是黑色。創(chuàng)建一個(gè)init初始化函數(shù),并在onMounted調(diào)用
<script setup> import {onMounted} from 'vue' import { Scene,WebGLRenderer,PerspectiveCamera} from 'three' let scene,renderer //創(chuàng)建場(chǎng)景 const setScene = ()=>{ scene = new Scene() renderer = new WebGLRenderer() renderer.setSize(innerWidth, innerHeight) document.querySelector('.boxs').appendChild(renderer.domElement) } //初始化所有函數(shù) const init = () => { setScene() } //用vue鉤子函數(shù)調(diào)用 onMounted(init) </script>
創(chuàng)建相機(jī)
有了場(chǎng)景就要加相機(jī),相機(jī)相當(dāng)于人的眼睛去觀察幾何物體,引入PerspectiveCamera, 參數(shù)有4個(gè),具體可以看看官網(wǎng)文檔。然后通過(guò)實(shí)例方法position.set設(shè)置相機(jī)坐標(biāo)
<script setup> import { Scene,WebGLRenderer,PerspectiveCamera} from 'three' let scene,renderer,camera //相機(jī)的默認(rèn)坐標(biāo) const defaultMap = { x: 510, y: 128, z: 0, } //創(chuàng)建場(chǎng)景 const setScene = ()=>{ scene = new Scene() renderer = new WebGLRenderer() renderer.setSize(innerWidth, innerHeight) document.querySelector('.boxs').appendChild(renderer.domElement) } //創(chuàng)建相機(jī) const setCamera = () => { const {x, y, z} = defaultMap camera = new PerspectiveCamera(60, innerWidth / innerHeight, 1, 1000) camera.position.set(x, y, z) } //初始化所有函數(shù) const init = () => { setScene() setCamera() } //用vue鉤子函數(shù)調(diào)用 onMounted(init) </script>
引入特斯拉模型
在three我們除了可以通過(guò)api創(chuàng)建幾何物體,還可以引入第三方3d模型,具體可以上 sketchfab ,國(guó)外一個(gè)3d模型下載網(wǎng)站,里面有很多免費(fèi)的模型下載,這次用特斯拉汽車(chē)模型為例,下載一個(gè)gltf格式的3D模型。引入GLTFLoader 創(chuàng)建一個(gè)loadfile函數(shù)并通過(guò)Promise返回模型數(shù)據(jù),在init函數(shù)加上async調(diào)用loadfile得到返回模型數(shù)據(jù)并添加到場(chǎng)景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) //相機(jī)的默認(rèn)坐標(biāo) const defaultMap = { x: 510, y: 128, z: 0, } //創(chuàng)建場(chǎng)景 const setScene = ()=>{ scene = new Scene() renderer = new WebGLRenderer() renderer.setSize(innerWidth, innerHeight) document.querySelector('.boxs').appendChild(renderer.domElement) } //創(chuàng)建相機(jī) const setCamera = () => { const {x, y, z} = defaultMap camera = new PerspectiveCamera(60, innerWidth / innerHeight, 1, 1000) camera.position.set(x, y, z) } //通過(guò)Promise處理一下loadfile函數(shù) 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) } ) })) } //初始化所有函數(shù) const init = async() => { const gltf =await loadFile('src/assets/3d/tesla_2018_model_3/scene.gltf') setScene() setCamera() scene.add(gltf.scene) } //用vue鉤子函數(shù)調(diào)用 onMounted(init)
創(chuàng)建燈光
汽車(chē)模型還看不見(jiàn),所以我們要給它設(shè)置燈光,引入DirectionalLight,DirectionalLightHelper,HemisphereLight,HemisphereLightHelper,并設(shè)置燈光的參數(shù),使模型可見(jiàn),并有些反射光面,陰影的效果,然后也在init函數(shù)調(diào)用 setLight,再增加loop函數(shù),使場(chǎng)景、照相機(jī)、模型不停循環(huán)調(diào)用。然后車(chē)模型就能看見(jiàn)啦,看到車(chē)出現(xiàn)的那一刻,好像自己的新買(mǎi)的一樣,^_^
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) //相機(jī)的默認(rèn)坐標(biāo) const defaultMap = { x: 510, y: 128, z: 0, } //創(chuàng)建場(chǎng)景 const setScene = ()=>{ scene = new Scene() renderer = new WebGLRenderer() renderer.setSize(innerWidth, innerHeight) document.querySelector('.boxs').appendChild(renderer.domElement) } //創(chuàng)建相機(jī) const setCamera = () => { const {x, y, z} = defaultMap camera = new PerspectiveCamera(60, innerWidth / innerHeight, 1, 1000) camera.position.set(x, y, z) } //通過(guò)Promise處理一下loadfile函數(shù) 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) } ) })) } // 設(shè)置燈光 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) } //初始化所有函數(shù) const init = async() => { const gltf = await loadFile('src/assets/3d/tesla_2018_model_3/scene.gltf') setScene() setCamera() setLight() scene.add(gltf.scene) loop() } //使場(chǎng)景、照相機(jī)、模型不停調(diào)用 const loop = () => { requestAnimationFrame(loop) renderer.render(scene, camera) } //用vue鉤子函數(shù)調(diào)用 onMounted(init)
控制模型
想用鼠標(biāo)自由旋轉(zhuǎn),或者自動(dòng)旋轉(zhuǎn),就要引用 OrbitControls對(duì)象,創(chuàng)建setControls函數(shù)也是在init調(diào)用,通過(guò)綁定change還可以監(jiān)聽(tīng)坐標(biāo)變化,另外也要在loop函數(shù)增加 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) //相機(jī)的默認(rèn)坐標(biāo) const defaultMap = { x: 510, y: 128, z: 0, } //創(chuàng)建場(chǎng)景 const setScene = ()=>{ scene = new Scene() renderer = new WebGLRenderer() renderer.setSize(innerWidth, innerHeight) document.querySelector('.boxs').appendChild(renderer.domElement) } //創(chuàng)建相機(jī) const setCamera = () => { const {x, y, z} = defaultMap camera = new PerspectiveCamera(60, innerWidth / innerHeight, 1, 1000) camera.position.set(x, y, z) } //通過(guò)Promise處理一下loadfile函數(shù) 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) } ) })) } // 設(shè)置燈光 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) } // 設(shè)置模型控制 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) } //初始化所有函數(shù) const init = async() => { const gltf =await loadFile('src/assets/3d/tesla_2018_model_3/scene.gltf') setScene() setCamera() setLight() setControls() scene.add(gltf.scene) } //使場(chǎng)景、照相機(jī)、模型不停調(diào)用和更新位置數(shù)據(jù) const loop = () => { requestAnimationFrame(loop) renderer.render(scene, camera) controls.update() } //用vue鉤子函數(shù)調(diào)用 onMounted(init)
改變車(chē)身顏色
到這里基礎(chǔ)的已經(jīng)搭建好了,接下來(lái)我們?cè)偌右粋€(gè)功能改變汽車(chē)車(chē)身顏色,也是展廳展示一個(gè)比較基礎(chǔ)的功能.創(chuàng)建一個(gè)setColor 這里說(shuō)一下實(shí)例scene 有一個(gè)traverse函數(shù),它回調(diào)了所有模型的子模型信息,只要我們找到對(duì)應(yīng)name屬性,就可以更改顏色,和增加貼圖等等, 因?yàn)閷?duì)模型結(jié)構(gòu)不怎熟悉,所以根據(jù)名字來(lái)猜了一下 找到door_前序的名字大概應(yīng)該就車(chē)身的套件。當(dāng)然如果細(xì)分到我只想改引擎蓋的顏色就要找出引擎蓋套件。當(dāng)然要很熟悉這個(gè)模型結(jié)構(gòu)了
//設(shè)置車(chē)身顏色 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控制,包括設(shè)置車(chē)身顏色、是否自動(dòng)轉(zhuǎn)動(dòng)等等。大家運(yùn)行以下代碼的時(shí)候記得用vite打包工具創(chuàng)建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">轉(zhuǎn)動(dòng)車(chē)</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' //車(chē)身顏色數(shù)組 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)"] // 車(chē)身顏色數(shù)組 const loader = new GLTFLoader() //引入模型的loader實(shí)例 const defaultMap = { x: 510, y: 128, z: 0, }// 相機(jī)的默認(rèn)坐標(biāo) const map = reactive(defaultMap)//把相機(jī)坐標(biāo)設(shè)置成可觀察對(duì)象 const {x, y, z} = toRefs(map)//輸出坐標(biāo)給模板使用 let scene, camera, renderer, controls, floor, dhelper, hHelper, directionalLight, hemisphereLight // 定義所有three實(shí)例變量 let isLoading = ref(true) //是否顯示loading 這個(gè)load模型監(jiān)聽(tīng)的進(jìn)度 let loadingWidth = ref(0)// loading的進(jìn)度 //創(chuàng)建燈光 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) } // 創(chuàng)建場(chǎng)景 const setScene = () => { scene = new Scene() renderer = new WebGLRenderer() renderer.setSize(innerWidth, innerHeight) document.querySelector('.boxs').appendChild(renderer.domElement) } // 創(chuàng)建相機(jī) const setCamera = () => { const {x, y, z} = defaultMap camera = new PerspectiveCamera(60, innerWidth / innerHeight, 1, 1000) camera.position.set(x, y, z) } // 設(shè)置模型控制 const setControls = () => { controls = new OrbitControls(camera, renderer.domElement) controls.maxPolarAngle = 0.9 * Math.PI / 2 controls.enableZoom = true controls.addEventListener('change', render) } //返回坐標(biāo)信息 const render = () => { map.x = Number.parseInt(camera.position.x) map.y = Number.parseInt(camera.position.y) map.z = Number.parseInt(camera.position.z) } // 循環(huán)場(chǎng)景 、相機(jī)、 位置更新 const loop = () => { requestAnimationFrame(loop) renderer.render(scene, camera) controls.update() } //是否自動(dòng)轉(zhuǎn)動(dòng) const isAutoFun = () => { controls.autoRotate = true } //停止轉(zhuǎn)動(dòng) const stop = () => { controls.autoRotate = false } //設(shè)置車(chē)身顏色 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) } ) })) } //初始化所有函數(shù) 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鉤子函數(shù)調(diào)用 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>
最后
在這個(gè)3D汽車(chē)展示廳筆者只是簡(jiǎn)單創(chuàng)建了一些基礎(chǔ)功能,還有很多功能可以增加,比如創(chuàng)建背景、地板、一些陰影、定點(diǎn)顯示車(chē)套件位置信息等等。掌握套路,這些功能實(shí)現(xiàn)也不難。另外筆者要沖二級(jí)啦,希望大家如果喜歡能給小弟點(diǎn)個(gè)贊,謝謝啦 ^_^
相關(guān)知識(shí)
創(chuàng)維汽車(chē)
“安全=豪華”?沃爾沃汽車(chē)寫(xiě)出了一個(gè)等式
Meta標(biāo)簽name=“description”描述標(biāo)簽的寫(xiě)法及用法
汽車(chē)黑科技研究:車(chē)載健康的五個(gè)發(fā)展階段
關(guān)注車(chē)內(nèi)健康,倡導(dǎo)健康出行 中國(guó)汽車(chē)健康指數(shù)引領(lǐng)汽車(chē)行業(yè)加速發(fā)展
小班健康詳案教案及教學(xué)反思《小汽車(chē)嘟嘟嘟》
健康出行新篇章:人居環(huán)境與健康汽車(chē)高質(zhì)量發(fā)展論壇成功舉辦
交通運(yùn)輸部辦公廳關(guān)于開(kāi)展汽車(chē)維修電子健康檔案系統(tǒng)建設(shè)工作的通知
開(kāi)展親子教育活動(dòng)方案(3篇)
2024動(dòng)感單車(chē)趨勢(shì):如何讓你的汽車(chē)生活更動(dòng)感?
網(wǎng)址: 用最簡(jiǎn)單方式打造Three.js 3D汽車(chē)展示廳在上一篇文章簡(jiǎn)單粗略的描述了開(kāi)發(fā)3D汽車(chē)展廳,筆者再寫(xiě)一篇比較詳細(xì)的教程 http://www.u1s5d6.cn/newsview536863.html
推薦資訊
- 1發(fā)朋友圈對(duì)老公徹底失望的心情 12775
- 2BMI體重指數(shù)計(jì)算公式是什么 11235
- 3補(bǔ)腎吃什么 補(bǔ)腎最佳食物推薦 11199
- 4性生活姿勢(shì)有哪些 盤(pán)點(diǎn)夫妻性 10428
- 5BMI正常值范圍一般是多少? 10137
- 6在線基礎(chǔ)代謝率(BMR)計(jì)算 9652
- 7一邊做飯一邊躁狂怎么辦 9138
- 8從出汗看健康 出汗透露你的健 9063
- 9早上怎么喝水最健康? 8613
- 10五大原因危害女性健康 如何保 7828
- 今日水素:陽(yáng)臺(tái)菜園,居家種出健康芽菜指南
- 運(yùn)動(dòng)女孩的休閑穿搭
- 《居家健康監(jiān)測(cè)證明》可在“隨申辦”在線開(kāi)
- 【便民提示】在線開(kāi)具“居家健康監(jiān)測(cè)證明”
- 健身休閑館如何經(jīng)營(yíng)管理
- 這個(gè)集運(yùn)動(dòng)、休閑、時(shí)尚為一體的運(yùn)動(dòng)品牌進(jìn)
- 知名運(yùn)動(dòng)休閑服裝品牌
- 江北新區(qū)兩案例入選省級(jí)居家社區(qū)養(yǎng)老服務(wù)典
- 康健園·戰(zhàn)“疫”|慢性病患者如何做好居家
- 北京啟明康健休閑健身中心 (北京市豐臺(tái)區(qū)