冰墩墩代碼3D建模,由論壇用戶(hù)原創(chuàng)制作分享的一個(gè)冰墩墩3D建模代碼,Three.js環(huán)境下實(shí)現(xiàn),加入了東奧主題的背景,冰墩墩3D建?梢詥为(dú)調(diào)出來(lái)使用,用戶(hù)可以根據(jù)需求來(lái)選擇不同的材質(zhì),如果有3D打印機(jī)設(shè)備還可以直接導(dǎo)入制作冰墩墩3D實(shí)物模型,讓您輕松擁有一款屬于自己的冰墩墩。本界面提供冰墩墩代碼資源鏈接,點(diǎn)擊后可直達(dá)官方界面,歡迎有需要的朋友們前往使用。
冰墩墩代碼作者介紹
迎冬奧,一起向未來(lái)!2022冬奧會(huì)馬上就要開(kāi)始了,本文使用 Three.js + React 技術(shù)棧,實(shí)現(xiàn)冬日和奧運(yùn)元素,制作了一個(gè)充滿(mǎn)趣味和紀(jì)念意義的冬奧主題 3D 頁(yè)面。本文涉及到的知識(shí)點(diǎn)主要包括:TorusGeometry 圓環(huán)面、MeshLambertMaterial 非光澤表面材質(zhì)、MeshDepthMaterial 深度網(wǎng)格材質(zhì)、custromMaterial 自定義材質(zhì)、Points 粒子、PointsMaterial 點(diǎn)材質(zhì)等。
相關(guān)新聞
程序員大佬 dragonir 用Three.js + React 技術(shù)棧,實(shí)現(xiàn)冬日和奧運(yùn)元素,制作了一個(gè)充滿(mǎn)趣味和紀(jì)念意義的冬奧主題 3D 頁(yè)面。
冰墩墩代碼說(shuō)明
引入資源
首先引入開(kāi)發(fā)頁(yè)面所需要的庫(kù)和外部資源,OrbitControls 用于鏡頭軌道控制、TWEEN 用于補(bǔ)間動(dòng)畫(huà)實(shí)現(xiàn)、GLTFLoader 用于加載 glb 或 gltf 格式的 3D 模型、以及一些其他模型、貼圖等資源。
import React from 'react';import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";import { TWEEN } from "three/examples/jsm/libs/tween.module.min.js";import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";import bingdundunModel from './models/bingdundun.glb';// ...
頁(yè)面DOM結(jié)構(gòu)
頁(yè)面 DOM 結(jié)構(gòu)非常簡(jiǎn)單,只有渲染 3D 元素的 #container 容器和顯示加載進(jìn)度的 .olympic_loading元素。
<div> <div id="container"></div> {this.state.loadingProcess === 100 ? '' : ( <div className="olympic_loading"> <div className="box">{this.state.loadingProcess} %</div> </div> )}</div>
場(chǎng)景初始化
初始化渲染容器、場(chǎng)景、相機(jī)。關(guān)于這部分內(nèi)容的詳細(xì)知識(shí)點(diǎn),可以查閱我往期的文章,本文中不再贅述。
container = document.getElementById('container');renderer = new THREE.WebGLRenderer({ antialias: true });renderer.setPixelRatio(window.devicePixelRatio);renderer.setSize(window.innerWidth, window.innerHeight);renderer.shadowMap.enabled = true;container.appendChild(renderer.domElement);scene = new THREE.Scene();scene.background = new THREE.TextureLoader().load(skyTexture);camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);camera.position.set(0, 30, 100);camera.lookAt(new THREE.Vector3(0, 0, 0));
添加光源
本示例中主要添加了兩種光源:DirectionalLight 用于產(chǎn)生陰影,調(diào)節(jié)頁(yè)面亮度、AmbientLight 用于渲染環(huán)境氛圍。
// 直射光const light = new THREE.DirectionalLight(0xffffff, 1);light.intensity = 1;light.position.set(16, 16, 8);light.castShadow = true;light.shadow.mapSize.width = 512 * 12;light.shadow.mapSize.height = 512 * 12;light.shadow.camera.top = 40;light.shadow.camera.bottom = -40;light.shadow.camera.left = -40;light.shadow.camera.right = 40;scene.add(light);// 環(huán)境光const ambientLight = new THREE.AmbientLight(0xcfffff);ambientLight.intensity = 1;scene.add(ambientLight);
加載進(jìn)度管理
使用 THREE.LoadingManager 管理頁(yè)面模型加載進(jìn)度,在它的回調(diào)函數(shù)中執(zhí)行一些與加載進(jìn)度相關(guān)的方法。本例中的頁(yè)面加載進(jìn)度就是在 onProgress 中完成的,當(dāng)頁(yè)面加載進(jìn)度為 100% 時(shí),執(zhí)行 TWEEN 鏡頭補(bǔ)間動(dòng)畫(huà)。
const manager = new THREE.LoadingManager();manager.onStart = (url, loaded, total) => {};manager.onLoad = () => { console.log('Loading complete!')};manager.onProgress = (url, loaded, total) => { if (Math.floor(loaded / total * 100) === 100) { this.setState({ loadingProcess: Math.floor(loaded / total * 100) }); // 鏡頭補(bǔ)間動(dòng)畫(huà) Animations.animateCamera(camera, controls, { x: 0, y: -1, z: 20 }, { x: 0, y: 0, z: 0 }, 3600, () => {}); } else { this.setState({ loadingProcess: Math.floor(loaded / total * 100) }); }};
創(chuàng)建地面
本示例中凹凸起伏的地面是使用 Blender 構(gòu)建模型,然后導(dǎo)出 glb 格式加載創(chuàng)建的。當(dāng)然也可以只使用 Three.js 自帶平面網(wǎng)格加凹凸貼圖也可以實(shí)現(xiàn)類(lèi)似的效果。使用 Blender 自建模型的優(yōu)點(diǎn)在于可以自由可視化地調(diào)整地面的起伏效果。
var loader = new THREE.GLTFLoader(manager);loader.load(landModel, function (mesh) { mesh.scene.traverse(function (child) { if (child.isMesh) { child.material.metalness = .1; child.material.roughness = .8; // 地面 if (child.name === 'Mesh_2') { child.material.metalness = .5; child.receiveShadow = true; } }); mesh.scene.rotation.y = Math.PI / 4; mesh.scene.position.set(15, -20, 0); mesh.scene.scale.set(.9, .9, .9); land = mesh.scene; scene.add(land);});
創(chuàng)建冬奧吉祥物冰墩墩
現(xiàn)在添加可愛(ài)的冬奧會(huì)吉祥物熊貓冰墩墩 🐼,冰墩墩同樣是使用 glb 格式模型加載的。它的原始模型來(lái)源于這里,從這個(gè)網(wǎng)站免費(fèi)現(xiàn)在模型后,原模型是使用 3D max 建的我發(fā)現(xiàn)并不能直接用在網(wǎng)頁(yè)中,需要在 Blender 中轉(zhuǎn)換模型格式,還需要調(diào)整調(diào)整模型的貼圖法線,才能還原渲染圖效果。
原模型:
冰墩墩貼圖:
轉(zhuǎn)換成Blender支持的模型,并在Blender中調(diào)整模型貼圖法線、并添加貼圖:
導(dǎo)出glb格式:
📖 在 Blender 中給模型添加貼圖教程傳送門(mén):在Blender中怎么給模型貼圖
仔細(xì)觀察冰墩墩 🐼可以發(fā)現(xiàn),它的外面有一層透明塑料或玻璃質(zhì)感外殼,這個(gè)效果可以通過(guò)修改模型的透明度、金屬度、粗糙度等材質(zhì)參數(shù)實(shí)現(xiàn),最后就可以渲染出如 👆 banner圖 所示的那種效果,具體如以下代碼所示。
loader.load(bingdundunModel, mesh => { mesh.scene.traverse(child => { if (child.isMesh) { // 內(nèi)部 if (child.name === 'oldtiger001') { child.material.metalness = .5 child.material.roughness = .8 } // 半透明外殼 if (child.name === 'oldtiger002') { child.material.transparent = true; child.material.opacity = .5 child.material.metalness = .2 child.material.roughness = 0 child.material.refractionRatio = 1 child.castShadow = true; } } }); mesh.scene.rotation.y = Math.PI / 24; mesh.scene.position.set(-8, -12, 0); mesh.scene.scale.set(24, 24, 24); scene.add(mesh.scene);});
創(chuàng)建奧運(yùn)五環(huán)
奧運(yùn)五環(huán)由基礎(chǔ)幾何模型圓環(huán)面 TorusGeometry 來(lái)實(shí)現(xiàn),創(chuàng)建五個(gè)圓環(huán)面,并調(diào)整它們的材質(zhì)顏色和位置來(lái)構(gòu)成藍(lán)黑紅黃綠順序的五環(huán)結(jié)構(gòu)。五環(huán)材質(zhì)使用的是 MeshLambertMaterial。
const fiveCycles = [ { key: 'cycle_0', color: 0x0885c2, position: { x: -250, y: 0, z: 0 }}, { key: 'cycle_1', color: 0x000000, position: { x: -10, y: 0, z: 5 }}, { key: 'cycle_2', color: 0xed334e, position: { x: 230, y: 0, z: 0 }}, { key: 'cycle_3', color: 0xfbb132, position: { x: -125, y: -100, z: -5 }}, { key: 'cycle_4', color: 0x1c8b3c, position: { x: 115, y: -100, z: 10 }}];fiveCycles.map(item => { let cycleMesh = new THREE.Mesh(new THREE.TorusGeometry(100, 10, 10, 50), new THREE.MeshLambertMaterial({ color: new THREE.Color(item.color), side: THREE.DoubleSide })); cycleMesh.castShadow = true; cycleMesh.position.set(item.position.x, item.position.y, item.position.z); meshes.push(cycleMesh); fiveCyclesGroup.add(cycleMesh);});fiveCyclesGroup.scale.set(.036, .036, .036);fiveCyclesGroup.position.set(0, 10, -8);scene.add(fiveCyclesGroup);
💡 TorusGeometry 圓環(huán)面
TorusGeometry 一個(gè)用于生成圓環(huán)幾何體的類(lèi)。
構(gòu)造函數(shù):
TorusGeometry(radius: Float, tube: Float, radialSegments: Integer, tubularSegments: Integer, arc: Float)
radius:圓環(huán)的半徑,從圓環(huán)的中心到管道(橫截面)的中心。默認(rèn)值是 1。
tube:管道的半徑,默認(rèn)值為 0.4。
radialSegments:圓環(huán)的分段數(shù),默認(rèn)值為 8。
tubularSegments:管道的分段數(shù),默認(rèn)值為 6。
arc:圓環(huán)的圓心角(單位是弧度),默認(rèn)值為 Math.PI * 2。
💡 MeshLambertMaterial 非光澤表面材質(zhì)
一種非光澤表面的材質(zhì),沒(méi)有鏡面高光。該材質(zhì)使用基于非物理的 Lambertian 模型來(lái)計(jì)算反射率。這可以很好地模擬一些表面(例如未經(jīng)處理的木材或石材),但不能模擬具有鏡面高光的光澤表面(例如涂漆木材)。
構(gòu)造函數(shù):
MeshLambertMaterial(parameters : Object)
parameters:(可選)用于定義材質(zhì)外觀的對(duì)象,具有一個(gè)或多個(gè)屬性。材質(zhì)的任何屬性都可以從此處傳入。
創(chuàng)建旗幟
旗面模型是從sketchfab下載的,還需要一個(gè)旗桿,可以在 Blender中添加了一個(gè)柱狀立方體,并調(diào)整好合適的長(zhǎng)寬高和旗面結(jié)合起來(lái)。原本想把國(guó)旗貼圖添加到旗幟模型上,但為了避免使用錯(cuò)誤,造成敏感問(wèn)題,于是使用 北京2022冬奧會(huì) 旗幟貼圖了 😂。
旗面貼圖:
旗面添加了動(dòng)畫(huà),需要在代碼中執(zhí)行動(dòng)畫(huà)幀播放。
loader.load(flagModel, mesh => { mesh.scene.traverse(child => { if (child.isMesh) { child.castShadow = true; // 旗幟 if (child.name === 'mesh_0001') { child.material.metalness = .1; child.material.roughness = .1; child.material.map = new THREE.TextureLoader().load(flagTexture); } // 旗桿 if (child.name === '柱體') { child.material.metalness = .6; child.material.roughness = 0; child.material.refractionRatio = 1; child.material.color = new THREE.Color(0xeeeeee); } } }); mesh.scene.rotation.y = Math.PI / 24; mesh.scene.position.set(2, -7, -1); mesh.scene.scale.set(4, 4, 4); // 動(dòng)畫(huà) let meshAnimation = mesh.animations[0]; mixer = new THREE.AnimationMixer(mesh.scene); let animationClip = meshAnimation; let clipAction = mixer.clipAction(animationClip).play(); animationClip = clipAction.getClip(); scene.add(mesh.scene);});
創(chuàng)建樹(shù)木
為了充實(shí)畫(huà)面,營(yíng)造冬日氛圍,于是就添加了幾棵松樹(shù) 🌲 作為裝飾。添加松樹(shù)的時(shí)候用到一個(gè)技巧非常重要:我們知道因?yàn)闃?shù)的模型非常復(fù)雜,有非常多的面數(shù),面數(shù)太多會(huì)降低頁(yè)面性能,造成卡頓。本文中使用兩個(gè)如下圖 👇 所示的兩個(gè)交叉的面來(lái)作為樹(shù)的基座,這樣的話樹(shù)只有兩個(gè)面數(shù),使用這個(gè)技巧可以和大程度上優(yōu)化頁(yè)面性能,而且樹(shù) 🌲 的樣子看起來(lái)也是有 3D 感的。
材質(zhì)貼圖:
為了使樹(shù)只在貼圖透明部分透明、其他地方不透明,并且可以產(chǎn)生樹(shù)狀陰影而不是長(zhǎng)方體陰影,需要給樹(shù)模型添加如下 MeshPhysicalMaterial、MeshDepthMaterial 兩種材質(zhì),兩種材質(zhì)使用同樣的紋理貼圖,其中 MeshDepthMaterial 添加到模型的 custromMaterial 屬性上。
let treeMaterial = new THREE.MeshPhysicalMaterial({ map: new THREE.TextureLoader().load(treeTexture), transparent: true, side: THREE.DoubleSide, metalness: .2, roughness: .8, depthTest: true, depthWrite: false, skinning: false, fog: false, reflectivity: 0.1, refractionRatio: 0,});let treeCustomDepthMaterial = new THREE.MeshDepthMaterial({ depthPacking: THREE.RGBADepthPacking, map: new THREE.TextureLoader().load(treeTexture), alphaTest: 0.5});loader.load(treeModel, mesh => { mesh.scene.traverse(child =>{ if (child.isMesh) { child.material = treeMaterial; child.custromMaterial = treeCustomDepthMaterial; } }); mesh.scene.position.set(14, -9, 0); mesh.scene.scale.set(16, 16, 16); scene.add(mesh.scene); // 克隆另兩棵樹(shù) let tree2 = mesh.scene.clone(); tree2.position.set(10, -8, -15); tree2.scale.set(18, 18, 18); scene.add(tree2) // ...});
實(shí)現(xiàn)效果也可以從 👆 上面 Banner 圖中可以看到,為了畫(huà)面更好看,我取消了樹(shù)的陰影顯示。
📌 在 3D 功能開(kāi)發(fā)中,一些不重要的裝飾模型都可以采取這種策略來(lái)優(yōu)化。
💡 MeshDepthMaterial 深度網(wǎng)格材質(zhì)
一種按深度繪制幾何體的材質(zhì)。深度基于相機(jī)遠(yuǎn)近平面,白色最近,黑色最遠(yuǎn)。
構(gòu)造函數(shù):
MeshDepthMaterial(parameters: Object)
parameters:(可選)用于定義材質(zhì)外觀的對(duì)象,具有一個(gè)或多個(gè)屬性。材質(zhì)的任何屬性都可以從此處傳入。
特殊屬性:
.depthPacking[Constant]:depth packing 的編碼。默認(rèn)為 BasicDepthPacking。
.displacementMap[Texture]:位移貼圖會(huì)影響網(wǎng)格頂點(diǎn)的位置,與僅影響材質(zhì)的光照和陰影的其他貼圖不同,移位的頂點(diǎn)可以投射陰影,阻擋其他對(duì)象,以及充當(dāng)真實(shí)的幾何體。
.displacementScale[Float]:位移貼圖對(duì)網(wǎng)格的影響程度(黑色是無(wú)位移,白色是最大位移)。如果沒(méi)有設(shè)置位移貼圖,則不會(huì)應(yīng)用此值。默認(rèn)值為 1。
.displacementBias[Float]:位移貼圖在網(wǎng)格頂點(diǎn)上的偏移量。如果沒(méi)有設(shè)置位移貼圖,則不會(huì)應(yīng)用此值。默認(rèn)值為 0。
💡 custromMaterial 自定義材質(zhì)
給網(wǎng)格添加 custromMaterial 自定義材質(zhì)屬性,可以實(shí)現(xiàn)透明外圍 png 圖片貼圖的內(nèi)容區(qū)域陰影。
創(chuàng)建雪花
創(chuàng)建雪花 ❄️,就要用到粒子知識(shí)。THREE.Points 是用來(lái)創(chuàng)建點(diǎn)的類(lèi),也用來(lái)批量管理粒子。本例中創(chuàng)建了 1500 個(gè)雪花粒子,并為它們?cè)O(shè)置了限定三維空間的隨機(jī)坐標(biāo)及橫向和豎向的隨機(jī)移動(dòng)速度。
// 雪花貼圖let texture = new THREE.TextureLoader().load(snowTexture);let geometry = new THREE.Geometry();let range = 100;let pointsMaterial = new THREE.PointsMaterial({ size: 1, transparent: true, opacity: 0.8, map: texture, // 背景融合 blending: THREE.AdditiveBlending, // 景深衰弱 sizeAttenuation: true, depthTest: false});for (let i = 0; i < 1500; i++) { let vertice = new THREE.Vector3(Math.random() * range - range / 2, Math.random() * range * 1.5, Math.random() * range - range / 2); // 縱向移速 vertice.velocityY = 0.1 + Math.random() / 3; // 橫向移速 vertice.velocityX = (Math.random() - 0.5) / 3; // 加入到幾何 geometry.vertices.push(vertice);}geometry.center();points = new THREE.Points(geometry, pointsMaterial);points.position.y = -30;scene.add(points);
💡 Points 粒子
Three.js 中,雨 🌧️、雪 ❄️、云 ☁️、星辰 ✨ 等生活中常見(jiàn)的粒子都可以使用 Points 來(lái)模擬實(shí)現(xiàn)。
構(gòu)造函數(shù):
new THREE.Points(geometry, material);
構(gòu)造函數(shù)可以接受兩個(gè)參數(shù),一個(gè)幾何體和一個(gè)材質(zhì),幾何體參數(shù)用來(lái)制定粒子的位置坐標(biāo),材質(zhì)參數(shù)用來(lái)格式化粒子;
可以基于簡(jiǎn)單幾何體對(duì)象如 BoxGeometry、SphereGeometry等作為粒子系統(tǒng)的參數(shù);
一般來(lái)講,需要自己指定頂點(diǎn)來(lái)確定粒子的位置。
💡 PointsMaterial 點(diǎn)材質(zhì)
通過(guò) THREE.PointsMaterial 可以設(shè)置粒子的屬性參數(shù),是 Points 使用的默認(rèn)材質(zhì)。
構(gòu)造函數(shù):
PointsMaterial(parameters : Object)
parameters:(可選)用于定義材質(zhì)外觀的對(duì)象,具有一個(gè)或多個(gè)屬性。材質(zhì)的任何屬性都可以從此處傳入。
💡 材質(zhì)屬性 .blending
材質(zhì)的.blending 屬性主要控制紋理融合的疊加方式,.blending 屬性的值包括:
THREE.NormalBlending�:默認(rèn)值
THREE.AdditiveBlending:加法融合模式
THREE.SubtractiveBlending:減法融合模式
THREE.MultiplyBlending:乘法融合模式
THREE.CustomBlending:自定義融合模式,與 .blendSrc, .blendDst 或 .blendEquation 屬性組合使用
💡 材質(zhì)屬性 .sizeAttenuation
粒子的大小是否會(huì)被相機(jī)深度衰減,默認(rèn)為 true(僅限透視相機(jī))。
💡 Three.js 向量
幾維向量就有幾個(gè)分量,二維向量 Vector2 有 x 和 y 兩個(gè)分量,三維向量 Vector3 有x、y、z 三個(gè)分量,四維向量 Vector4 有 x、y、z、w 四個(gè)分量。
相關(guān)API:
Vector2:二維向量
Vector3:三維向量
Vector4:四維向量
鏡頭控制、縮放適配、動(dòng)畫(huà)
controls = new OrbitControls(camera, renderer.domElement);controls.target.set(0, 0, 0);controls.enableDamping = true;// 禁用平移controls.enablePan = false;// 禁用縮放controls.enableZoom = false;// 垂直旋轉(zhuǎn)角度限制controls.minPolarAngle = 1.4;controls.maxPolarAngle = 1.8;// 水平旋轉(zhuǎn)角度限制controls.minAzimuthAngle = -.6;controls.maxAzimuthAngle = .6;window.addEventListener('resize', () => { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight);}, false);function animate() { requestAnimationFrame(animate); renderer.render(scene, camera); controls && controls.update(); // 旗幟動(dòng)畫(huà)更新 mixer && mixer.update(new THREE.Clock().getDelta()); // 鏡頭動(dòng)畫(huà) TWEEN && TWEEN.update(); // 五環(huán)自轉(zhuǎn) fiveCyclesGroup && (fiveCyclesGroup.rotation.y += .01); // 頂點(diǎn)變動(dòng)之后需要更新,否則無(wú)法實(shí)現(xiàn)雨滴特效 points.geometry.verticesNeedUpdate = true; // 雪花動(dòng)畫(huà)更新 let vertices = points.geometry.vertices; vertices.forEach(function (v) { v.y = v.y - (v.velocityY); v.x = v.x - (v.velocityX); if (v.y <= 0) v.y = 60; if (v.x <= -20 || v.x >= 20) v.velocityX = v.velocityX * -1; });}