「冰墩墩」代碼,開源了!手動實現(xiàn)"冰墩墩自由"。

轉載自:GitHubDaily | 作者:dragonir
??七月干貨組整理的《名企AI面試100題》《名企AI面試100篇》《機器學習十大算法系列》《2021年最新大廠AI面試題 Q4版》文末免費送!
??七月干貨組整理的《名企AI面試100題》《名企AI面試100篇》《機器學習十大算法系列》《2021年最新大廠AI面試題 Q4版》文末免費送!
隨著前兩天冬奧會序幕的正式拉開,也成功帶火了本次吉祥物冰墩墩。憨厚可愛的熊貓形象,讓冰墩墩的實體公仔、鑰匙扣都被一搶而空,眾多網(wǎng)友呼吁現(xiàn)在真的是「一墩難求」!
原文地址:https://segmentfault.com/a/1190000041363089
背景
本文使用 Three.js + React 技術棧,實現(xiàn)冬日和奧運元素,制作了一個充滿趣味和紀念意義的冬奧主題 3D 頁面。
本文涉及到的知識點主要包括:TorusGeometry 圓環(huán)面、MeshLambertMaterial 非光澤表面材質、MeshDepthMaterial 深度網(wǎng)格材質、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 自帶平面網(wǎng)格加凹凸貼圖也可以實現(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 格式模型加載的。
它的原始模型來源于這里:
從這個網(wǎng)站免費現(xiàn)在模型后,原模型是使用 3D max 建的我發(fā)現(xiàn)并不能直接用在網(wǎng)頁中,需要在 Blender 中轉換模型格式,還需要調整調整模型的貼圖法線,才能還原渲染圖效果。
原模型:

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

導出 glb 格式:

仔細觀察冰墩墩 可以發(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 中添加了一個柱狀立方體,并調整好合適的長寬高和旗面結合起來。

旗面貼圖:

旗面添加了動畫,需要在代碼中執(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 深度網(wǎng)格材質
一種按深度繪制幾何體的材質。深度基于相機遠近平面,白色最近,黑色最遠。
構造函數(shù):
MeshDepthMaterial(parameters: Object)
parameters:(可選)用于定義材質外觀的對象,具有一個或多個屬性。材質的任何屬性都可以從此處傳入。
特殊屬性:
.depthPacking[Constant]:depth packing的編碼。默認為BasicDepthPacking。.displacementMap[Texture]:位移貼圖會影響網(wǎng)格頂點的位置,與僅影響材質的光照和陰影的其他貼圖不同,移位的頂點可以投射陰影,阻擋其他對象,以及充當真實的幾何體。.displacementScale[Float]:位移貼圖對網(wǎng)格的影響程度(黑色是無位移,白色是最大位移)。如果沒有設置位移貼圖,則不會應用此值。默認值為1。.displacementBias[Float]:位移貼圖在網(wǎng)格頂點上的偏移量。如果沒有設置位移貼圖,則不會應用此值。默認值為0。
custromMaterial 自定義材質
給網(wǎng)格添加 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深度網(wǎng)格材質custromMaterial自定義材質Points粒子PointsMaterial點材質材質屬性 .blending、.sizeAttenuationThree.js向量
進一步優(yōu)化的空間:
添加更多的交互功能、界面樣式進一步優(yōu)化; 吉祥物冰墩墩添加骨骼動畫,并可以通過鼠標和鍵盤控制其移動和交互。
— 免費福利 —
進大廠是大部分程序員的夢想,而進大廠的門檻也是比較高的。刷題,也成為面試前的必備環(huán)節(jié)。
七妹給大家準備了“武功秘籍”,七月在線干貨組繼19年出的兩本書《名企AI面試100題》和《名企AI面試100篇》后,又整理出《機器學習十大算法系列》、《2021年最新大廠AI面試題 Q4版》兩本圖書,不少同學通過學習拿到拿到dream offer。
為了讓更多AI人受益,七仔現(xiàn)把電子版免費送給大家,希望對你的求職有所幫助。如果點贊和點在看的人數(shù)較多,我會后續(xù)整理資料并分享答案給大家。
↓ ↓ ↓以下4本書,電子版,直接送 ↓ ↓ ↓ 私我回復“088”領取!


