使用Three.js制作一個3D獎牌頁面
背景
本文使用 React + Three.js 技術(shù)棧,實現(xiàn)粉絲突破1000的3D紀(jì)念頁面,包含的主要知識點(diǎn)包括:Three.js 提供的光源、DirectionLight 平行光、HemisphereLight 半球光源、AmbientLight 環(huán)境光、獎牌素材生成、貼圖知識、MeshPhysicalMaterial 物理材質(zhì)、TWEEN 鏡頭補(bǔ)間動畫、CSS 禮花動畫等。
效果

效果如上圖,頁面由包含我的個人信息的獎牌、1000+ Followers 模型構(gòu)成
在線預(yù)覽:https://dragonir.github.io/3d/#/segmentfault
實現(xiàn)
引入資源
首先引入開發(fā)功能所需的庫,其中 FBXLoader 用于加在 1000+ 字體模型、OrbitControls 鏡頭軌道控制、TWEEN 用于生成補(bǔ)間動畫、Stats 用于開發(fā)時性能查看。
import * as THREE from "three";
import { FBXLoader } from "three/examples/jsm/loaders/FBXLoader";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import { TWEEN } from "three/examples/jsm/libs/tween.module.min.js";
import Stats from "three/examples/jsm/libs/stats.module";場景初始化
這部分內(nèi)容主要用于初始化場景和參數(shù),詳細(xì)講解可點(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;
renderer.shadowMap.needsUpdate = true;
container.appendChild(renderer.domElement);
// 場景
scene = new THREE.Scene();
// 給場景設(shè)置好看的背景
scene.background = new THREE.TextureLoader().load(backgroundTexture);
// 攝像機(jī)
camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 0, 0);
camera.lookAt(new THREE.Vector3(0, 0, 0));
// 控制器
controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(0, 0, 0);
controls.enableDamping = true;
controls.enableZoom = false;
controls.enablePan = false;
controls.rotateSpeed = .2;為了達(dá)到更好的視覺效果,為OrbitControls設(shè)置了縮放禁用、平移禁用和減小默認(rèn)旋轉(zhuǎn)速度
光照效果
為了模擬真實的物理場景,本示例中使用了 3種 光源。
// 直射光
const cubeGeometry = new THREE.BoxGeometry(0.001, 0.001, 0.001);
const cubeMaterial = new THREE.MeshLambertMaterial({ color: 0xffffff });
const cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
cube.position.set(0, 0, 0);
light = new THREE.DirectionalLight(0xffffff, 1);
light.intensity = 1;
light.position.set(18, 20, 60);
light.castShadow = true;
light.target = cube;
light.shadow.mapSize.width = 512 * 12;
light.shadow.mapSize.height = 512 * 12;
light.shadow.camera.top = 80;
light.shadow.camera.bottom = -80;
light.shadow.camera.left = -80;
light.shadow.camera.right = 80;
scene.add(light);
// 半球光
const ambientLight = new THREE.AmbientLight(0xffffff);
ambientLight.intensity = .8;
scene.add(ambientLight);
// 環(huán)境光
const hemisphereLight = new THREE.HemisphereLight(0xffffff, 0xfffc00);
hemisphereLight.intensity = .3;
scene.add(hemisphereLight);Three.js 提供的光源
Three.js 庫提供了一些列光源,而且沒種光源都有特定的行為和用途。這些光源包括:
| 光源名稱 | 描述 |
|---|---|
| AmbientLight 環(huán)境光 | 這是一種基礎(chǔ)光源,它的顏色會添加到整個場景和所有對象的當(dāng)前顏色上 |
| PointLight 點(diǎn)光源 | 空間中的一點(diǎn),朝所有的方向發(fā)射光線 |
| SpotLight 聚光燈光源 | 這種光源有聚光的效果,類似臺燈、天花板上的吊燈,或者手電筒 |
| DirectionLight 平行光 | 也稱為無限光。從這種光源發(fā)出的光線可以看著平行的。例如,太陽光 |
| HemishpereLight 半球光 | 這是一種特殊光源,可以用來創(chuàng)建更加自然的室外光線,模擬放光面和光線微弱的天空 |
| AreaLight 面光源 | 使用這種光源可以指定散發(fā)光線的平面,而不是空間中的一個點(diǎn) |
| LensFlare 鏡頭眩光 | 這不是一種光源,但是通過 LensFlare 可以為場景中的光源添加眩光效果 |
THREE.DirectionLight 平行光
THREE.DirectionLight 可以看作是距離很遠(yuǎn)的光,它發(fā)出的所有光線都是相互平行的。平行光的一個范例就是太陽光。被平行光照亮的整個區(qū)域接受到的光強(qiáng)是一樣的。
構(gòu)造函數(shù):
new THREE.DirectionLight(color);
屬性說明:
- position:光源在場景中的位置。
target:目標(biāo)。它的指向很重要。使用target屬性,你可以將光源指向場景中的特定對象或位置。此屬性需要一個THREE.Object3D對象。intensity:光源照射的強(qiáng)度,默認(rèn)值:1。castShadow:投影,如果設(shè)置為true,這個光源就會生成陰影。onlyShadow:僅陰影,如果此屬性設(shè)置為true,則該光源只生成陰影,而不會在場景中添加任何光照。shadow.camera.near:投影近點(diǎn),表示距離光源的哪一個位置開始生成陰影。shadow.camera.far:投影遠(yuǎn)點(diǎn),表示到距離光源的哪一個位置可以生成陰影。shadow.camera.left:投影左邊界。shadow.camera.right:投影右邊界。shadow.camera.top:投影上邊界。shadow.camera.bottom:投影下邊界。shadow.map.width和shadow.map.height:陰影映射寬度和陰影映射高度。決定了有多少像素用來生成陰影。當(dāng)陰影具有鋸齒狀邊緣或看起來不光滑時,可以增加這個值。在場景渲染之后無法更改。兩者的默認(rèn)值均為:512。
THREE.HemisphereLight 半球光光源
使用半球光光源,可以創(chuàng)建出更加貼近自然的光照效果。
構(gòu)造函數(shù):
new THREE.HeimsphereLight(groundColor, color, intensity);
屬性說明:
groundColor:從地面發(fā)出的光線顏色。Color:從天空發(fā)出的光線顏色。intensity:光線照射的強(qiáng)度。
THREE.AmbientLight 環(huán)境光
在創(chuàng)建 THREE.AmbientLight 時,顏色會應(yīng)用到全局。該光源并沒有特別的來源方向,并且不會產(chǎn)生陰影。
構(gòu)造函數(shù):
new THREE.AmbientLight(color);
使用建議:
- 通常不能將
THREE.AmbientLight作為場景中唯一的光源,因為它會將場景中的所有物體渲染為相同的顏色。 - 使用其他光源,如
THREE.SpotLight或THREE.DirectionLight的同時使用它,目的是弱化陰影或給場景添加一些額外顏色。 - 由于
THREE.AmbientLight光源不需要指定位置并且會應(yīng)用到全局,所以只需要指定個顏色,然后將它添加到場景中即可。
添加網(wǎng)格和地面
添加網(wǎng)格是為了方便開發(fā),可以調(diào)整模型的合適的相對位置,本例中保留網(wǎng)格的目的是為了頁面更有 3D景深效果。透明材質(zhì)的地面是為了顯示模型的陰影。
// 網(wǎng)格
const grid = new THREE.GridHelper(200, 200, 0xffffff, 0xffffff);
grid.position.set(0, -30, -50);
grid.material.transparent = true;
grid.material.opacity = 0.1;
scene.add(grid);
// 創(chuàng)建地面,透明材質(zhì)顯示陰影
var planeGeometry = new THREE.PlaneGeometry(200, 200);
var planeMaterial = new THREE.ShadowMaterial({ opacity: .5 });
var plane = new THREE.Mesh(planeGeometry, planeMaterial);
plane.rotation.x = -0.5 * Math.PI;
plane.position.set(0, -30, -50);
plane.receiveShadow = true;
scene.add(plane);創(chuàng)建獎牌
由于時間關(guān)系,本示例獎牌模型直接使用 Three.js 自帶的基礎(chǔ)立方體模型 THREE.BoxGeometry 來實現(xiàn),你也可以使用其他立方體如球體、圓珠等,甚至可以使用 Blender 等專業(yè)建模軟件創(chuàng)建自己喜歡的獎牌形狀。(ps:個人覺得立方體也挺好看的 ??)
獎牌UI素材生成
獎牌上下面和側(cè)面貼圖制作:
為了生成的獎牌有黃金質(zhì)感,本例中使用下圖該材質(zhì)貼圖,來生成亮瞎眼的24K純金效果 。

獎牌正面和背面貼圖制作:
獎牌的正面和背面使用的貼圖是 SegmentFault 個人中心頁的截圖,為了更具有金屬效果,我用上面金屬材質(zhì)貼圖給它添加了一個帶有圓角的邊框。
Photoshop 生成圓角金屬邊框具體方法:截圖上面添加金屬圖層 -> 使用框選工具框選需要刪除的內(nèi)容 -> 點(diǎn)擊選擇 -> 點(diǎn)擊修改 -> 點(diǎn)擊平滑 -> 輸入合適的圓角大小 -> 刪除選區(qū) -> 合并圖層 -> 完成并導(dǎo)出圖片。

最終的正反面的材質(zhì)貼圖如下圖所示,為了顯示更清晰,我在 Photoshop 中同時修改了圖片的對比度 和 飽和度,并加了 SegmentFault 的 Logo 在上面。

獎牌正面和背面的法相貼圖制作:
為了生成凹凸質(zhì)感,就需要為模型添加法相貼圖。使用上面已經(jīng)生成的正面和背面的材質(zhì)貼圖,就可以使用在線工具自動生成法相貼圖。生成時可以根據(jù)需要,通過調(diào)整 Strength、Level、Blur 等參數(shù)進(jìn)行樣式微調(diào),并且能夠?qū)崟r預(yù)覽。調(diào)整好后點(diǎn)擊 Download 下載即可。

貼圖在線制作工具傳送門:NormalMap-Online
通過多次調(diào)節(jié)優(yōu)化,最終使用的法相貼圖如下圖所示。

使用上面生成的素材,現(xiàn)在進(jìn)行獎牌模型的構(gòu)建。正面和背面使用個人信息材質(zhì),其他面使用金屬材質(zhì)。然后遍歷對所有面調(diào)整金屬度和粗糙度樣式。
let segmentMap = new THREE.MeshPhysicalMaterial({map: new THREE.TextureLoader().load(segmentTexture), normalMap: new THREE.TextureLoader().load(normalMapTexture) });
let metalMap = new THREE.MeshPhysicalMaterial({map: new THREE.TextureLoader().load(metalTexture)});
// 創(chuàng)建紋理數(shù)組
const boxMaps = [metalMap, metalMap, metalMap, metalMap, segmentMap, segmentMap];
// ?? 立方體長寬高比例需要和貼圖的大小比例一致,厚度可以隨便定
box = new THREE.Mesh(new THREE.BoxGeometry(297, 456, 12), boxMaps);
box.material.map(item => {
// 材質(zhì)樣式調(diào)整
item.metalness = .5;
item.roughness = .4;
item.refractionRatio = 1;
return item;
});
box.scale.set(0.085, 0.085, 0.085);
box.position.set(-22, 2, 0);
box.castShadow = true;
meshes.push(box);
scene.add(box);
上面 4 張效果圖依次對應(yīng)的是:
- 圖1:創(chuàng)建沒有貼圖的 BoxGeometry,只是一個白色的立方體。
- 圖2:立方體添加 材質(zhì)貼圖,此時沒有凹凸效果。
- 圖3:立方體添加 法相貼圖,此時產(chǎn)生凹凸效果。
- 圖4:調(diào)節(jié)立方體材質(zhì)的 金屬度、粗糙程度 和 反射率,更具有真實感。
Three.js 中的貼圖
貼圖類型
map:材質(zhì)貼圖normalMap:法線貼圖bumpMap:凹凸貼圖envMap:環(huán)境貼圖specularMap:高光貼圖lightMap:光照貼圖
貼圖原理
通過紋理貼圖加載器 TextureLoader() 去新創(chuàng)建一個貼圖對象出來,然后再去調(diào)用里面的 load() 方法去加載一張圖片,這樣就會返回一個紋理對象,紋理對象可以作為模型材質(zhì)顏色貼圖 map 屬性的值,材質(zhì)的顏色貼圖屬性 map 設(shè)置后,模型會從紋理貼圖上采集像素值。
MeshPhysicalMaterial 物理材質(zhì)
MeshPhysicalMaterial 類是 PBR 物理材質(zhì),可以更好的模擬光照計算,相比較高光網(wǎng)格材質(zhì)MeshPhongMaterial 渲染效果更逼真。
如果你想展示一個產(chǎn)品,為了更逼真的渲染效果最好選擇該材質(zhì),如果游戲為了更好的顯示效果可以選擇 PBR 材質(zhì) MeshPhysicalMaterial,而不是高光材質(zhì) MeshPhongMaterial。
特殊屬性
.metalness金屬度屬性:表示材質(zhì)像金屬的程度。非金屬材料,如木材或石材,使用 0.0,金屬使用 1.0,中間沒有(通常). 默認(rèn) 0.5. 0.0 到 1.0 之間的值可用于生銹的金屬外觀。如果還提供了粗糙度貼圖.metalnessMap,則兩個值都相乘。.roughness粗糙度屬性:表示材質(zhì)的粗糙程度. 0.0 表示平滑的鏡面反射,1.0 表示完全漫反射. 默認(rèn) 0.5. 如果還提供粗糙度貼圖.roughnessMap,則兩個值相乘..metalnessMap金屬度貼圖:紋理的藍(lán)色通道用于改變材料的金屬度..roughnessMap粗糙度貼圖:紋理的綠色通道用于改變材料的粗糙度。
注意使用物理材質(zhì)的時候,一般需要設(shè)置環(huán)境貼圖 .envMap。
加載1000+文字模型
1000+ 字樣的模型使用 THREE.LoadingManager 和 FBXLoader 加載。詳細(xì)使用方法也不再本文中贅述,可參考文章末尾鏈接查看我的其他文章,里面有詳細(xì)描述。
const manager = new THREE.LoadingManager();
manager.onProgress = async(url, loaded, total) => {
if (Math.floor(loaded / total * 100) === 100) {
// 設(shè)置加載進(jìn)度
_this.setState({ loadingProcess: Math.floor(loaded / total * 100) });
// 加載鏡頭移動補(bǔ)間動畫
Animations.animateCamera(camera, controls, { x: 0, y: 4, z: 60 }, { x: 0, y: 0, z: 0 }, 3600, () => {});
} else {
_this.setState({ loadingProcess: Math.floor(loaded / total * 100) });
}
};
const fbxLoader = new FBXLoader(manager);
fbxLoader.load(textModel, mesh => {
mesh.traverse(child => {
if (child.isMesh) {
// 生成陰影
child.castShadow = true;
// 樣式調(diào)整
child.material.metalness = 1;
child.material.roughness = .2;
meshes.push(mesh);
}
});
mesh.position.set(16, -4, 0);
mesh.rotation.x = Math.PI / 2
mesh.scale.set(.08, .08, .08);
scene.add(mesh);
});
補(bǔ)間動畫
相機(jī)移動實現(xiàn)漫游等動畫,頁面打開時,模型加載完畢從大變小的動畫就是通過 TWEEN 實現(xiàn)的。
animateCamera: (camera, controls, newP, newT, time = 2000, callBack) => {
var tween = new TWEEN.Tween({
x1: camera.position.x, // 相機(jī)x
y1: camera.position.y, // 相機(jī)y
z1: camera.position.z, // 相機(jī)z
x2: controls.target.x, // 控制點(diǎn)的中心點(diǎn)x
y2: controls.target.y, // 控制點(diǎn)的中心點(diǎn)y
z2: controls.target.z, // 控制點(diǎn)的中心點(diǎn)z
});
tween.to({
x1: newP.x,
y1: newP.y,
z1: newP.z,
x2: newT.x,
y2: newT.y,
z2: newT.z,
}, time);
tween.onUpdate(function (object) {
camera.position.x = object.x1;
camera.position.y = object.y1;
camera.position.z = object.z1;
controls.target.x = object.x2;
controls.target.y = object.y2;
controls.target.z = object.z2;
controls.update();
});
tween.onComplete(function () {
controls.enabled = true;
callBack();
});
tween.easing(TWEEN.Easing.Cubic.InOut);
tween.start();
}動畫更新
最后不要忘了要在 requestAnimationFrame 中更新場景、軌道控制器、TWEEN、以及模型的自轉(zhuǎn)等。
// 監(jiān)聽頁面縮放,更新相機(jī)和渲染
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
stats && stats.update();
controls && controls.update();
TWEEN && TWEEN.update();
// 獎牌模型自轉(zhuǎn)
box && (box.rotation.y += .04);
}禮花動畫
最后,通過 box-shadow 和簡單的 CSS 動畫,給頁面添加禮花綻放效果,營造歡慶氛圍!
<div className="firework_1"></div> <div className="firework_2"></div> <!-- ... --> <div className="firework_10"></div>
樣式動畫:
[class^=firework_] {
position: absolute;
width: 0.1rem;
height: 0.1rem;
border-radius: 50%;
transform: scale(8)
}
.firework_1 {
animation: firework_lg 2s both infinite;
animation-delay: 0.3s;
top: 5%;
left: 5%;
}
@keyframes firework_lg {
0%, 100% {
opacity: 0;
}
10%, 70% {
opacity: 1;
}
100% {
box-shadow: -0.9rem 0rem 0 #fff, 0.9rem 0rem 0 #fff, 0rem -0.9rem 0 #fff, 0rem 0.9rem 0 #fff, 0.63rem -0.63rem 0 #fff, 0.63rem 0.63rem 0 #fff, -0.63rem -0.63rem 0 #fff, -0.63rem 0.63rem 0 #fff;
}
}實現(xiàn)效果:

總結(jié)
本文中主要涉及到的知識點(diǎn)包括:
Three.js提供的光源THREE.DirectionLight平行光THREE.HemisphereLight半球光光源THREE.AmbientLight環(huán)境光- 獎牌
UI素材生成 Three.js中的貼圖MeshPhysicalMaterial物理材質(zhì)TWEEN鏡頭補(bǔ)間動畫CSS禮花動畫
以上就是使用Three.js制作一個3D獎牌頁面的詳細(xì)內(nèi)容,更多關(guān)于Three.js 3D獎牌的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
js實時監(jiān)控文本框輸入字?jǐn)?shù)的實例代碼
下面小編就為大家分享一篇實時監(jiān)控文本框輸入字?jǐn)?shù)的實例代碼,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-01-01
Javascript實現(xiàn)顏色rgb與16進(jìn)制轉(zhuǎn)換的方法
這篇文章主要介紹了Javascript實現(xiàn)顏色rgb與16進(jìn)制轉(zhuǎn)換的方法,實例分析了顏色值轉(zhuǎn)換的常用技巧與使用方法,非常具有實用價值,需要的朋友可以參考下2015-04-04
JavaScript實現(xiàn)設(shè)置默認(rèn)日期范圍為最近40天的方法分析
這篇文章主要介紹了JavaScript實現(xiàn)設(shè)置默認(rèn)日期范圍為最近40天的方法,結(jié)合實例形式分析了javascript結(jié)合HTML5 date元素進(jìn)行時間運(yùn)算相關(guān)操作技巧,需要的朋友可以參考下2017-07-07
微信小程序?qū)崿F(xiàn)選擇內(nèi)容顯示對應(yīng)內(nèi)容
這篇文章主要為大家詳細(xì)介紹了微信小程序?qū)崿F(xiàn)選擇內(nèi)容顯示對應(yīng)內(nèi)容,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-07-07
關(guān)于JavaScript中name的意義沖突示例介紹
這篇文章主要介紹了關(guān)于JavaScript中name的意義沖突,需要的朋友可以參考下2014-05-05
基于javascript數(shù)組實現(xiàn)圖片輪播
這篇文章主要為大家詳細(xì)介紹了基于javascript數(shù)組實現(xiàn)圖片輪播的相關(guān)資料,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-05-05

