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

效果如上圖,頁(yè)面由包含我的個(gè)人信息的獎(jiǎng)牌、1000+ Followers 模型構(gòu)成
在線預(yù)覽:https://dragonir.github.io/3d/#/segmentfault
實(shí)現(xiàn)
引入資源
首先引入開(kāi)發(fā)功能所需的庫(kù),其中 FBXLoader 用于加在 1000+ 字體模型、OrbitControls 鏡頭軌道控制、TWEEN 用于生成補(bǔ)間動(dòng)畫、Stats 用于開(kāi)發(fā)時(shí)性能查看。
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";場(chǎng)景初始化
這部分內(nèi)容主要用于初始化場(chǎng)景和參數(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);
// 場(chǎng)景
scene = new THREE.Scene();
// 給場(chǎng)景設(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á)到更好的視覺(jué)效果,為OrbitControls設(shè)置了縮放禁用、平移禁用和減小默認(rèn)旋轉(zhuǎn)速度
光照效果
為了模擬真實(shí)的物理場(chǎng)景,本示例中使用了 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 庫(kù)提供了一些列光源,而且沒(méi)種光源都有特定的行為和用途。這些光源包括:
| 光源名稱 | 描述 |
|---|---|
| AmbientLight 環(huán)境光 | 這是一種基礎(chǔ)光源,它的顏色會(huì)添加到整個(gè)場(chǎng)景和所有對(duì)象的當(dāng)前顏色上 |
| PointLight 點(diǎn)光源 | 空間中的一點(diǎn),朝所有的方向發(fā)射光線 |
| SpotLight 聚光燈光源 | 這種光源有聚光的效果,類似臺(tái)燈、天花板上的吊燈,或者手電筒 |
| DirectionLight 平行光 | 也稱為無(wú)限光。從這種光源發(fā)出的光線可以看著平行的。例如,太陽(yáng)光 |
| HemishpereLight 半球光 | 這是一種特殊光源,可以用來(lái)創(chuàng)建更加自然的室外光線,模擬放光面和光線微弱的天空 |
| AreaLight 面光源 | 使用這種光源可以指定散發(fā)光線的平面,而不是空間中的一個(gè)點(diǎn) |
| LensFlare 鏡頭眩光 | 這不是一種光源,但是通過(guò) LensFlare 可以為場(chǎng)景中的光源添加眩光效果 |
THREE.DirectionLight 平行光
THREE.DirectionLight 可以看作是距離很遠(yuǎn)的光,它發(fā)出的所有光線都是相互平行的。平行光的一個(gè)范例就是太陽(yáng)光。被平行光照亮的整個(gè)區(qū)域接受到的光強(qiáng)是一樣的。
構(gòu)造函數(shù):
new THREE.DirectionLight(color);
屬性說(shuō)明:
- position:光源在場(chǎng)景中的位置。
target:目標(biāo)。它的指向很重要。使用target屬性,你可以將光源指向場(chǎng)景中的特定對(duì)象或位置。此屬性需要一個(gè)THREE.Object3D對(duì)象。intensity:光源照射的強(qiáng)度,默認(rèn)值:1。castShadow:投影,如果設(shè)置為true,這個(gè)光源就會(huì)生成陰影。onlyShadow:僅陰影,如果此屬性設(shè)置為true,則該光源只生成陰影,而不會(huì)在場(chǎng)景中添加任何光照。shadow.camera.near:投影近點(diǎn),表示距離光源的哪一個(gè)位置開(kāi)始生成陰影。shadow.camera.far:投影遠(yuǎn)點(diǎn),表示到距離光源的哪一個(gè)位置可以生成陰影。shadow.camera.left:投影左邊界。shadow.camera.right:投影右邊界。shadow.camera.top:投影上邊界。shadow.camera.bottom:投影下邊界。shadow.map.width和shadow.map.height:陰影映射寬度和陰影映射高度。決定了有多少像素用來(lái)生成陰影。當(dāng)陰影具有鋸齒狀邊緣或看起來(lái)不光滑時(shí),可以增加這個(gè)值。在場(chǎng)景渲染之后無(wú)法更改。兩者的默認(rèn)值均為:512。
THREE.HemisphereLight 半球光光源
使用半球光光源,可以創(chuàng)建出更加貼近自然的光照效果。
構(gòu)造函數(shù):
new THREE.HeimsphereLight(groundColor, color, intensity);
屬性說(shuō)明:
groundColor:從地面發(fā)出的光線顏色。Color:從天空發(fā)出的光線顏色。intensity:光線照射的強(qiáng)度。
THREE.AmbientLight 環(huán)境光
在創(chuàng)建 THREE.AmbientLight 時(shí),顏色會(huì)應(yīng)用到全局。該光源并沒(méi)有特別的來(lái)源方向,并且不會(huì)產(chǎn)生陰影。
構(gòu)造函數(shù):
new THREE.AmbientLight(color);
使用建議:
- 通常不能將
THREE.AmbientLight作為場(chǎng)景中唯一的光源,因?yàn)樗鼤?huì)將場(chǎng)景中的所有物體渲染為相同的顏色。 - 使用其他光源,如
THREE.SpotLight或THREE.DirectionLight的同時(shí)使用它,目的是弱化陰影或給場(chǎng)景添加一些額外顏色。 - 由于
THREE.AmbientLight光源不需要指定位置并且會(huì)應(yīng)用到全局,所以只需要指定個(gè)顏色,然后將它添加到場(chǎng)景中即可。
添加網(wǎng)格和地面
添加網(wǎng)格是為了方便開(kāi)發(fā),可以調(diào)整模型的合適的相對(duì)位置,本例中保留網(wǎng)格的目的是為了頁(yè)面更有 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)建獎(jiǎng)牌
由于時(shí)間關(guān)系,本示例獎(jiǎng)牌模型直接使用 Three.js 自帶的基礎(chǔ)立方體模型 THREE.BoxGeometry 來(lái)實(shí)現(xiàn),你也可以使用其他立方體如球體、圓珠等,甚至可以使用 Blender 等專業(yè)建模軟件創(chuàng)建自己喜歡的獎(jiǎng)牌形狀。(ps:個(gè)人覺(jué)得立方體也挺好看的 ??)
獎(jiǎng)牌UI素材生成
獎(jiǎng)牌上下面和側(cè)面貼圖制作:
為了生成的獎(jiǎng)牌有黃金質(zhì)感,本例中使用下圖該材質(zhì)貼圖,來(lái)生成亮瞎眼的24K純金效果 。

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

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

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

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

使用上面生成的素材,現(xiàn)在進(jìn)行獎(jiǎng)牌模型的構(gòu)建。正面和背面使用個(gè)人信息材質(zhì),其他面使用金屬材質(zhì)。然后遍歷對(duì)所有面調(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];
// ?? 立方體長(zhǎng)寬高比例需要和貼圖的大小比例一致,厚度可以隨便定
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 張效果圖依次對(duì)應(yīng)的是:
- 圖1:創(chuàng)建沒(méi)有貼圖的 BoxGeometry,只是一個(gè)白色的立方體。
- 圖2:立方體添加 材質(zhì)貼圖,此時(shí)沒(méi)有凹凸效果。
- 圖3:立方體添加 法相貼圖,此時(shí)產(chǎn)生凹凸效果。
- 圖4:調(diào)節(jié)立方體材質(zhì)的 金屬度、粗糙程度 和 反射率,更具有真實(shí)感。
Three.js 中的貼圖
貼圖類型
map:材質(zhì)貼圖normalMap:法線貼圖bumpMap:凹凸貼圖envMap:環(huán)境貼圖specularMap:高光貼圖lightMap:光照貼圖
貼圖原理
通過(guò)紋理貼圖加載器 TextureLoader() 去新創(chuàng)建一個(gè)貼圖對(duì)象出來(lái),然后再去調(diào)用里面的 load() 方法去加載一張圖片,這樣就會(huì)返回一個(gè)紋理對(duì)象,紋理對(duì)象可以作為模型材質(zhì)顏色貼圖 map 屬性的值,材質(zhì)的顏色貼圖屬性 map 設(shè)置后,模型會(huì)從紋理貼圖上采集像素值。
MeshPhysicalMaterial 物理材質(zhì)
MeshPhysicalMaterial 類是 PBR 物理材質(zhì),可以更好的模擬光照計(jì)算,相比較高光網(wǎng)格材質(zhì)MeshPhongMaterial 渲染效果更逼真。
如果你想展示一個(gè)產(chǎn)品,為了更逼真的渲染效果最好選擇該材質(zhì),如果游戲?yàn)榱烁玫娘@示效果可以選擇 PBR 材質(zhì) MeshPhysicalMaterial,而不是高光材質(zhì) MeshPhongMaterial。
特殊屬性
.metalness金屬度屬性:表示材質(zhì)像金屬的程度。非金屬材料,如木材或石材,使用 0.0,金屬使用 1.0,中間沒(méi)有(通常). 默認(rèn) 0.5. 0.0 到 1.0 之間的值可用于生銹的金屬外觀。如果還提供了粗糙度貼圖.metalnessMap,則兩個(gè)值都相乘。.roughness粗糙度屬性:表示材質(zhì)的粗糙程度. 0.0 表示平滑的鏡面反射,1.0 表示完全漫反射. 默認(rèn) 0.5. 如果還提供粗糙度貼圖.roughnessMap,則兩個(gè)值相乘..metalnessMap金屬度貼圖:紋理的藍(lán)色通道用于改變材料的金屬度..roughnessMap粗糙度貼圖:紋理的綠色通道用于改變材料的粗糙度。
注意使用物理材質(zhì)的時(shí)候,一般需要設(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) });
// 加載鏡頭移動(dòng)補(bǔ)間動(dòng)畫
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ǔ)間動(dòng)畫
相機(jī)移動(dòng)實(shí)現(xiàn)漫游等動(dòng)畫,頁(yè)面打開(kāi)時(shí),模型加載完畢從大變小的動(dòng)畫就是通過(guò) TWEEN 實(shí)現(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();
}動(dòng)畫更新
最后不要忘了要在 requestAnimationFrame 中更新場(chǎng)景、軌道控制器、TWEEN、以及模型的自轉(zhuǎn)等。
// 監(jiān)聽(tīng)頁(yè)面縮放,更新相機(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();
// 獎(jiǎng)牌模型自轉(zhuǎn)
box && (box.rotation.y += .04);
}禮花動(dòng)畫
最后,通過(guò) box-shadow 和簡(jiǎn)單的 CSS 動(dòng)畫,給頁(yè)面添加禮花綻放效果,營(yíng)造歡慶氛圍!
<div className="firework_1"></div> <div className="firework_2"></div> <!-- ... --> <div className="firework_10"></div>
樣式動(dòng)畫:
[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;
}
}實(shí)現(xiàn)效果:

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

