「冰墩墩」代碼,開源了!
在下方公眾號后臺回復:JGNB,可獲取杰哥原創(chuàng)的 PDF 手冊。
大家好,我是杰哥。
北京冬奧會成功帶火了本次吉祥物冰墩墩。憨厚可愛的熊貓形象,讓冰墩墩的實體公仔、鑰匙扣、胸針都被一搶而空,眾多網友呼吁現(xiàn)在真的是「一墩難求」!
為了圓大家「人手一墩」的夢想,國內一位程序員 dragonir,用前端 + 建模的技術自己實現(xiàn)了一個冰墩墩,并將代碼開源到了 Github 上。
原文地址:
https://segmentfault.com/a/1190000041363089
背景
迎冬奧,一起向未來!本文使用Three.js + React技術棧,實現(xiàn)冬日和奧運元素,制作了一個充滿趣味和紀念意義的冬奧主題3D頁面。本文涉及到的知識點主要包括:TorusGeometry圓環(huán)面、MeshLambertMaterial非光澤表面材質、MeshDepthMaterial深度網格材質、custromMaterial自定義材質、Points粒子、PointsMaterial點材質等。
效果
實現(xiàn)效果如以下 ?? 動圖所示,頁面主要由2022冬奧會吉祥物冰墩墩、奧運五環(huán)、舞動的旗幟 ??、樹木 ?? 以及下雪效果 ?? 等組成。按住鼠標左鍵移動可以改為相機位置,獲得不同視圖。

?? 在線預覽:https://dragonir.github.io/3d/#/olympic (部署在
GitHub,加載速度可能會有點慢 ??)
實現(xiàn)
引入資源
首先引入開發(fā)頁面所需要的庫和外部資源,OrbitControls用于鏡頭軌道控制、TWEEN用于補間動畫實現(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';
//?...
頁面DOM結構
頁面DOM結構非常簡單,只有渲染3D元素的#container容器和顯示加載進度的.olympic_loading元素。
<div>
??<div?id="container">div>
??{this.state.loadingProcess?===?100???''?:?(
????<div?className="olympic_loading">
??????<div?className="box">{this.state.loadingProcess}?%div>
????div>
??)}
div>
場景初始化
初始化渲染容器、場景、相機。
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用于產生陰影,調節(jié)頁面亮度、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);
加載進度管理
使用THREE.LoadingManager管理頁面模型加載進度,在它的回調函數(shù)中執(zhí)行一些與加載進度相關的方法。本例中的頁面加載進度就是在onProgress中完成的,當頁面加載進度為100%時,執(zhí)行TWEEN鏡頭補間動畫。
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)?});
????//?鏡頭補間動畫
????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構建模型,然后導出glb格式加載創(chuàng)建的。當然也可以只使用 Three.js自帶平面網格加凹凸貼圖也可以實現(xiàn)類似的效果。使用Blender自建模型的優(yōu)點在于可以自由可視化地調整地面的起伏效果。
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)在添加可愛的冬奧會吉祥物熊貓冰墩墩 ??,冰墩墩同樣是使用glb格式模型加載的。
它的原始模型來源于:

從這個網站免費現(xiàn)在模型后,原模型是使用3D max建的我發(fā)現(xiàn)并不能直接用在網頁中,需要在Blender中轉換模型格式,還需要調整調整模型的貼圖法線,才能還原渲染圖效果。
原模型:

冰墩墩貼圖:

轉換成Blender支持的模型,并在Blender中調整模型貼圖法線、并添加貼圖:

導出glb格式:

?? 在 Blender 中給模型添加貼圖教程傳送門:https://jingyan.baidu.com/article/363872ecf6367f2f4ba16f95.html
仔細觀察冰墩墩??可以發(fā)現(xiàn),它的外面有一層透明塑料或玻璃質感外殼,這個效果可以通過修改模型的透明度、金屬度、粗糙度等材質參數(shù)實現(xiàn),最后就可以渲染出如 ??banner圖所示的那種效果,具體如以下代碼所示。
loader.load(bingdundunModel,?mesh?=>?{
??mesh.scene.traverse(child?=>?{
????if?(child.isMesh)?{
??????//?內部
??????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)建奧運五環(huán)
奧運五環(huán)由基礎幾何模型圓環(huán)面TorusGeometry來實現(xiàn),創(chuàng)建五個圓環(huán)面,并調整它們的材質顏色和位置來構成藍黑紅黃綠順序的五環(huán)結構。五環(huán)材質使用的是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一個用于生成圓環(huán)幾何體的類。
構造函數(shù):
TorusGeometry(radius:?Float,?tube:?Float,?radialSegments:?Integer,?tubularSegments:?Integer,?arc:?Float)
radius:圓環(huán)的半徑,從圓環(huán)的中心到管道(橫截面)的中心。默認值是1。tube:管道的半徑,默認值為0.4。radialSegments:圓環(huán)的分段數(shù),默認值為8。tubularSegments:管道的分段數(shù),默認值為6。arc:圓環(huán)的圓心角(單位是弧度),默認值為Math.PI * 2。
?? MeshLambertMaterial 非光澤表面材質
一種非光澤表面的材質,沒有鏡面高光。該材質使用基于非物理的Lambertian模型來計算反射率。這可以很好地模擬一些表面(例如未經處理的木材或石材),但不能模擬具有鏡面高光的光澤表面(例如涂漆木材)。
構造函數(shù):
MeshLambertMaterial(parameters?:?Object)
parameters:(可選)用于定義材質外觀的對象,具有一個或多個屬性。材質的任何屬性都可以從此處傳入。
創(chuàng)建旗幟
旗面模型是從sketchfab下載的,還需要一個旗桿,可以在Blender中添加了一個柱狀立方體,并調整好合適的長寬高和旗面結合起來。原本想把國旗貼圖添加到旗幟模型上,但為了避免使用錯誤,造成敏感問題,于是使用 北京2022冬奧會 旗幟貼圖了 ??。

旗面貼圖:
旗面添加了動畫,需要在代碼中執(zhí)行動畫幀播放。
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);
??//?動畫
??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ù)太多會降低頁面性能,造成卡頓。本文中使用兩個如下圖 ?? 所示的兩個交叉的面來作為樹的基座,這樣的話樹只有兩個面數(shù),使用這個技巧可以和大程度上優(yōu)化頁面性能,而且樹 ?? 的樣子看起來也是有 3D 感的。

材質貼圖:

為了使樹只在貼圖透明部分透明、其他地方不透明,并且可以產生樹狀陰影而不是長方體陰影,需要給樹模型添加如下MeshPhysicalMaterial、MeshDepthMaterial兩種材質,兩種材質使用同樣的紋理貼圖,其中 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);
??//?克隆另兩棵樹
??let?tree2?=?mesh.scene.clone();
??tree2.position.set(10,?-8,?-15);
??tree2.scale.set(18,?18,?18);
??scene.add(tree2)
??//?...
});
實現(xiàn)效果也可以從 ?? 上面 Banner 圖中可以看到,為了畫面更好看,我取消了樹的陰影顯示。
?? 在 3D 功能開發(fā)中,一些不重要的裝飾模型都可以采取這種策略來優(yōu)化。
?? MeshDepthMaterial 深度網格材質
一種按深度繪制幾何體的材質。深度基于相機遠近平面,白色最近,黑色最遠。
構造函數(shù):
MeshDepthMaterial(parameters:?Object)
parameters:(可選)用于定義材質外觀的對象,具有一個或多個屬性。材質的任何屬性都可以從此處傳入。
特殊屬性:
.depthPacking[Constant]:depth packing的編碼。默認為BasicDepthPacking。.displacementMap[Texture]:位移貼圖會影響網格頂點的位置,與僅影響材質的光照和陰影的其他貼圖不同,移位的頂點可以投射陰影,阻擋其他對象,以及充當真實的幾何體。.displacementScale[Float]:位移貼圖對網格的影響程度(黑色是無位移,白色是最大位移)。如果沒有設置位移貼圖,則不會應用此值。默認值為 1。.displacementBias[Float]:位移貼圖在網格頂點上的偏移量。如果沒有設置位移貼圖,則不會應用此值。默認值為0。
?? custromMaterial 自定義材質
給網格添加custromMaterial自定義材質屬性,可以實現(xiàn)透明外圍 png 圖片貼圖的內容區(qū)域陰影。
創(chuàng)建雪花
創(chuàng)建雪花 ??,就要用到粒子知識。THREE.Points是用來創(chuàng)建點的類,也用來批量管理粒子。本例中創(chuàng)建了1500個雪花粒子,并為它們設置了限定三維空間的隨機坐標及橫向和豎向的隨機移動速度。
//?雪花貼圖
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中,雨 ???、雪 ??、云 ??、星辰 ? 等生活中常見的粒子都可以使用Points來模擬實現(xiàn)。
構造函數(shù):
new?THREE.Points(geometry,?material);
構造函數(shù)可以接受兩個參數(shù),一個幾何體和一個材質,幾何體參數(shù)用來制定粒子的位置坐標,材質參數(shù)用來格式化粒子;
可以基于簡單幾何體對象如
BoxGeometry、SphereGeometry等作為粒子系統(tǒng)的參數(shù);一般來講,需要自己指定頂點來確定粒子的位置。
?? PointsMaterial 點材質
通過THREE.PointsMaterial可以設置粒子的屬性參數(shù),是Points使用的默認材質。
構造函數(shù):
PointsMaterial(parameters?:?Object)
parameters:(可選)用于定義材質外觀的對象,具有一個或多個屬性。材質的任何屬性都可以從此處傳入。
?? 材質屬性 .blending
材質的.blending屬性主要控制紋理融合的疊加方式,.blending屬性的值包括:
THREE.NormalBlending?:默認值THREE.AdditiveBlending:加法融合模式THREE.SubtractiveBlending:減法融合模式THREE.MultiplyBlending:乘法融合模式THREE.CustomBlending:自定義融合模式,與.blendSrc, .blendDst 或 .blendEquation屬性組合使用
?? 材質屬性 .sizeAttenuation
粒子的大小是否會被相機深度衰減,默認為true(僅限透視相機)。
?? Three.js 向量
幾維向量就有幾個分量,二維向量Vector2有x和y兩個分量,三維向量Vector3有x、y、z三個分量,四維向量Vector4有x、y、z、w四個分量。
相關API:
Vector2:二維向量Vector3:三維向量Vector4:四維向量
鏡頭控制、縮放適配、動畫
controls?=?new?OrbitControls(camera,?renderer.domElement);
controls.target.set(0,?0,?0);
controls.enableDamping?=?true;
//?禁用平移
controls.enablePan?=?false;
//?禁用縮放
controls.enableZoom?=?false;
//?垂直旋轉角度限制
controls.minPolarAngle?=?1.4;
controls.maxPolarAngle?=?1.8;
//?水平旋轉角度限制
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();
??//?旗幟動畫更新
??mixer?&&?mixer.update(new?THREE.Clock().getDelta());
??//?鏡頭動畫
??TWEEN?&&?TWEEN.update();
??//?五環(huán)自轉
??fiveCyclesGroup?&&?(fiveCyclesGroup.rotation.y?+=?.01);
??//?頂點變動之后需要更新,否則無法實現(xiàn)雨滴特效
??points.geometry.verticesNeedUpdate?=?true;
??//?雪花動畫更新
??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;
??});
}
?? 完整代碼:
https://github.com/dragonir/3d/tree/master/src/containers/Olympic
總結
?? 本文中主要包含的新知識點包括:
TorusGeometry圓環(huán)面MeshLambertMaterial非光澤表面材質MeshDepthMaterial深度網格材質custromMaterial自定義材質Points粒子PointsMaterial點材質材質屬性
.blending、.sizeAttenuationThree.js向量
進一步優(yōu)化的空間:
添加更多的交互功能、界面樣式進一步優(yōu)化;
吉祥物冰墩墩添加骨骼動畫,并可以通過鼠標和鍵盤控制其移動和交互。
真的是太硬核了!dragonir 同學寫的那么詳細,看來他是真的想教會我們。
點擊下方「閱讀原文」直接查看網頁,3D 可視化方向的同學,也可以看 github 源碼來學習。
推薦閱讀

