🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付

🪐 行星科技概念官網!Hero Section 回歸!(Three.js ✨)

🪐 行星科技概念官網!Hero Section 回歸!(Three.js ✨)

0.好久不見 👋

新老觀眾老爺們!我鴿子王何賢又雙叒叕回歸啦!😆

真的是好久不見!記得上一次發文章還是在上一次。一轉眼就兩個月沒更新了。

這期間,我憑藉之前的 9 篇文章成功晉級到創作等級 LV.5 🎉(會不會是掘金史上最快傳說?)獲得了 1K+ 的粉絲。

衷心感謝大家一直以來的支持 ❤️。

不知不覺,今年都快結束了。從最初的「周更博主」到「月更博主」,再到如今的「半年更博主」😂,每次被催更都覺得有點心虛。

不行!這次絕不能再鴿了!🕊️

重新自我介紹一下——我是一名在 Three.js 領域裡摸爬滾打的初級玩家。在這裡,我會分享自己的所見、所聞與所想。


1.前置條件 ⚙️

歡迎閱讀本篇文章!在深入探討 Three.jsShader (GLSL) 的進階內容之前,請確保您已經具備以下基礎知識:

  1. Three.js 基礎
    您需要熟悉 Three.js 的基本概念與使用方法,包括場景(Scene)、相機(Camera)、渲染器(Renderer)、幾何體(Geometry)、材質(Material)和網格(Mesh)等核心組件。
    如果您對這些內容還不熟悉,建議先學習 Three.js 的入門教程。我比較推薦外網知名博主 Bruno Simon 的課程 threejs-journey(B 站上有免費版,但如果條件允許,建議支持正版課程)。
    當然,如果您希望我分享自己的學習路徑,可以在評論區留言。人數夠多的話,我會著手撰寫一篇系統的學習路線文章。

  2. Shader 語法
    本文將涉及 GLSL(OpenGL Shading Language)的編寫,因此您需要了解 GLSL 的基本語法,包括頂點著色器(Vertex Shader)與片元著色器(Fragment Shader)的結構,以及如何在 Three.js 中使用自定義著色器。


2.Hero Section 概覽 🌌

“Hero Section” 是網頁設計中的一個術語,通常指頁面頂部的大型橫幅區域。
對於開發者而言,它可以更直觀地理解為:用戶在訪問網站瞬間所感受到的視覺衝擊,或促使他們停留在網站的關鍵視覺因素。

相信大家偶爾也會刷到一些以星球為主題的官網,看起來既夢幻又酷炫。

hero

這些網站往往擁有天馬行空的頁面佈局,美麗的星球在畫面中靜靜流動,營造出一種未來科技與宇宙幻想交織的氛圍。

於是,我讓 GPT 🧠 通過文生圖生成了一張原型設計稿:

Page 原型圖

prototype

並嘗試將其復原,於是得到了以下結果

Page 靜態預覽

static

Page 動圖

由於平台圖片體積限制, 動畫畫質存在大幅抽幀和壓縮,還請各位可以在 PC 端自行體驗

animation

PC端在線預覽地址(需要魔法): isgalaxias.vercel.app/

DeBug 在線調試界面: isgalaxias.vercel.app/#debug

Github 倉庫地址: github.com/hexianWeb/i…

轉發貼環節

關注我的掘友最熟悉的環節了,那麼讓我來介紹本次項目,它被 Threejs Journey 課程作者 Bruno Simon 轉發

retweet

(其實項目 8 月底就寫完了,但是我瘋狂鴿子,非常抱歉!)

3.場景搭建 🧱

由於本專欄主要聚焦於 Three.js,因此本文不會詳細講解從 0 到 1 的完整頁面實現過程,而是重點介紹與 Three.js 相關的實現部分。

首先,讓我們分析一下當前場景(Scene)中的主要元素:

  • 最外層的 星雲圖背景
  • 散佈在空間中的 星點中心星環
  • 漂浮在太空中的 三顆行星

3.1 星雲圖背景 🌌

這一部分的實現非常簡單:只需將加載好的星雲貼圖設置為場景的 background 即可。

在此也順便推薦一個優秀的 3D 貼圖资源站 —— Solar System Scope
本項目中使用的所有行星與星雲貼圖,均來自該網站。
這裡找到星雲圖

background

但注意這是一個根據 CC Attribution 4.0 許可證提供各種行星紋理的網站,因此需要提供相應的版權信息。如果您將網站上線,請務必在頁面上的某個位置顯示這些版權信息。

隨後,在 Three.js 中加載貼圖,並將其賦值為場景背景。
同時可以通過 backgroundIntensity 調整背景亮度,使整體層次更柔和。

this.scene.background = this.resources.items.spaceTexture
this.scene.backgroundIntensity = 0.25

background

3.2 星星 與 星環✨

這一部分構成了整個 Hero Section 的核心視覺焦點。我們通過 Points 系統創建了包含星星和中心星環的大量粒子的螺旋星系。

galaxy

乍一看,當前的實現方式似乎可以分為兩個部分,“中心的粒子圓環和外層的粒子群”,以及周圍緩慢旋轉的粒子群對嗎?但是仔細看“中心的粒子圓環和外層的粒子群”,似乎並沒有那麼強的割裂感,反而更像是粒子圓環帶動了整個粒子群一同旋轉,對吧?這是怎麼做到的?

其實這只是個非常巧妙的視覺錯覺,有點像小時候玩的萬花筒。如下圖

illusion

當我們放開控制器、稍微調整視角時,就會發現這個結構實際上只是個簡單的圓柱體。
通過透視營造出“浩瀚星海”的假象。

於是,現在你大概已經能想到幾種實現方式了:

  • 利用MeshSurfaceSampler在多個圓柱體表面使用對幾何體平面進行採樣,以構建粒子位置;
  • 或者在構建 BufferGeometry 時,利用三角函數約束頂點分佈,使所有粒子落在圓環軌跡上。

下面我來分享我在項目中採用的具體思路。

3.2.1 實現思路 🌠

可以概括為以下幾個步驟:

  1. 生成螺旋分佈的粒子位置:基於極坐標系統,沿著多個分支(branches)生成螺旋分佈的粒子
  2. 應用圓環約束:使用數學函數將粒子約束在環形區域內,形成星環效果
  3. 添加隨機擾動:為每個粒子添加隨機偏移,營造自然的星系形態
  4. 著色器動畫:在頂點著色器中實現旋轉動畫,讓星系"流動"起來
3.2.2粒子位置生成 💫

setGalaxy() 方法中,我們為每個粒子生成位置數據:

// 生成在環形區域內的半徑
const minRadius = this.parameters.innerRadius * this.parameters.radius
const maxRadius = this.parameters.radius
const radius = minRadius + Math.random() * (maxRadius - minRadius)

// 計算螺旋角度和分支角度
const spinAngle = radius * this.parameters.spin
const branchAngle = (i % this.parameters.branches) / this.parameters.branches * Math.PI * 2

// 生成基礎位置
const x = Math.cos(branchAngle + spinAngle) * radius
const z = Math.sin(branchAngle + spinAngle) * radius

這裡的關鍵是:

  • spinAngle:根據半徑和旋轉參數 spin 計算螺旋角度,離中心越遠,旋轉角度越大
  • branchAngle:將粒子均勻分佈到多個分支上,形成星系的旋臂結構
3.2.3 圓環約束算法 🌌

為了讓粒子呈現出星環的效果,我們使用了圓環約束算法

// 計算距離圓環中心的歸一化距離(0-1)
const ringCenter = (minRadius + maxRadius) * 0.5
const ringWidth = maxRadius - minRadius
const distanceToRingCenter = Math.abs(radius - ringCenter) / (ringWidth * 0.5)

// 使用帽形函數(反向拋物線)來約束隨機擾動
const ringConstraint = (1.0 - distanceToRingCenter ** this.parameters.ringFalloff) * this.parameters.constraintStrength
const effectiveRandomness = this.parameters.randomness * ringConstraint

這個算法的核心思想是:

  • 距離圓環中心越近的粒子,隨機擾動越大(約束強度高)
  • 距離邊緣越近的粒子,隨機擾動越小(約束強度低)
  • 通過 ringFalloff 參數控制衰減曲線,constraintStrength 控制整體約束強度

這樣就能形成一條清晰的星環帶,而不是均勻分佈的粒子。

3.2.4隨機擾動 🌀

為了增加星系的自然感,我們為每個粒子的Y軸添加了隨機偏移:

const randomY = effectiveRandomness * radius * (Math.random() < 0.5 ? 1 : -0.4) * Math.random() ** this.parameters.randomnessPower * 20

這裡可以看到我對於擾動約束存在兩種細分情況 Math.random() < 0.5 ? 1 : -0.4
可以理解為粒子 Y軸正向偏移時隨機偏移效果為 100%,負向偏移時隨機效果只有 40%

當然也可以把這個效果去掉,當前效果會變為下圖:

without

或許你有更大膽的創意?在中心部分放上一些特殊的星空元素?比如黑洞,或者一隻巨大的“外星之眼” 👁️讓整個場景更具神秘感。

3.2.5 粒子圓環帶動粒子群 🌀

到目前為止,我們已經定義了粒子在空間中的分佈關係,也確定了它們在星環結構中的約束邏輯。接下來要做的,就是讓這些粒子 動起來 —— 讓星環旋轉,從而帶動整個星系流動。

動畫實現思路

在這裡我們通過 頂點著色器(vertex shader) 來實現粒子的動態旋轉。相比在 JavaScript 層更新所有粒子坐標,使用 GPU 端的位移計算可以大幅降低性能消耗,並讓動畫更流暢。

核心邏輯是基於 粒子到中心的距離衰減旋轉速度

  • 離中心越近 → 旋轉越快
  • 離中心越遠 → 旋轉越慢

這樣能讓整個星環在旋轉時呈現一種“內圈快、外圈慢”的自然渦旋感。

頂點著色器動畫邏輯
float angle = atan(modelPosition.x, modelPosition.z);
float distanceToCenter = length(modelPosition.y);
float offset = (1.0 / distanceToCenter) * uTime * 0.2;
angle += offset;

// 更新位置
modelPosition.x = cos(angle);
modelPosition.z = sin(angle);

計算到中心的距離
distanceToCenter = length(modelPosition.y); 用於控制旋轉的衰減速率。

如果旋轉速度變化過大,可以通過以下方式做平滑限制:

distanceToCenter = clamp(distanceToCenter, 0.0, 10.0);

讓距離過近或過遠的粒子保持在合理的旋轉速率範圍內。

最終呈現出的效果,就是一個不斷旋轉、充滿空間層次感的 銀河渦旋

spiral

3.3 星球 🌍

這一部分是整個頁面中最具表現力的視覺元素 —— 三顆漂浮在太空中的星球。

planet

而對於如何在 Threejs裡面顯示一顆星球,我想已經有太多技術文章來描述這一過程了。這裡我也推薦你去看 Threejs jouneryEarth Shader 章節。(這裡 B 站鏈接,但是有條件還是建議補補票,作者會在每個聖誕節給 discount )

這裡我主要描述當前頁面的星球實現與其餘教程的不同點。

星球渲染包含幾個關鍵部分:

  1. 星球主體:使用各向異性過濾提升紋理質量。使用置換貼圖增加地形細節,法線貼圖增強表面凹凸感
  2. 光照系統:自定義環境光 + 點光源,支持衰減、漫反射和鏡面反射
3.3.1 星球主體 🪐

放大星球可以看到表面存在明顯的地形起伏,這通常會讓人想到 MeshStandardMaterialbumpMapdisplacementMap
但在本項目中,我並沒有直接使用標準物理材質,而是採用了 ShaderMaterial,並在頂點著色器中根據 法線貼圖 自行實現位移和光照控制。
這麼做的原因是為了後續自定義光照系統做鋪墊。

當然條條大路通羅馬,你也可以通過

這裡僅提供我的實現方案

sphere

首先我們先創建一個頂點足夠多的球體,再簡單的創建ShaderMaterial以及應用baseColor到球體上後有以下幾點需要注意

  1. 因為後面置換貼圖在頂點著色器中改變頂點位置,形成地形起伏,需要通過操控頂點位置來實現這種凹凸地形,所以我們需要分配足夠多的頂點給後續vertexShader假如沒有分配足夠多的頂點則會導致地形呈現“方塊感”:
// 創建星球幾何體(增加細分以支持置換)
this.geometry = new THREE.IcosahedronGeometry(this.radius, 64, 64)

較少頂點情況:

low-vertex

在頂點著色器中,我們使用置換貼圖來改變頂點的位置,形成地形起伏:

float displacement = texture2D(uDisplacementMap, uv).r; // uniform 傳入 網站上下載好的貼圖即可

// 沿法線方向偏移頂點位置
vec3 displacedPosition = position + normal * displacement * uDisplacementScale;

uDisplacementScale 控制置換強度,數值越大,地形起伏越明顯。

現在你應該擁有了一個凹凸不平的星球

displacement

3.3.2 光照系統

片元著色器負責計算最終的顏色,包含以下幾個關鍵步驟:

1. 法線貼圖處理
我們實現了完整的 TBN(切線-副切線-法線)矩陣計算,將法線貼圖從切線空間轉換到世界空間:

vec3 perturbNormal(vec3 normal, vec3 position, vec2 uv, sampler2D normalMap, float normalScale) {
  // 獲取法線貼圖值並轉換到 -1 到 1 範圍
  vec3 normalMapColor = texture2D(normalMap, uv).rgb;
  vec3 normalMapNormal = normalize(normalMapColor * 2.0 - 1.0);

  // 通過螢幕空間導數計算切線和副切線
  vec3 q1 = dFdx(position);
  vec3 q2 = dFdy(position);
  vec2 st1 = dFdx(uv);
  vec2 st2 = dFdy(uv);

  vec3 tangent = normalize(q1 * st2.t - q2 * st1.t);
  vec3 bitangent = normalize(-q1 * st2.s + q2 * st1.s);

  // 構建 TBN 矩陣
  mat3 tbn = mat3(tangent, bitangent, normal);

  // 混合原始法線和擾動後的法線
  vec3 perturbedNormal = normalize(mix(normal, tbn * normalMapNormal, normalScale));

  return perturbedNormal;
}

2. 環境光計算
環境光提供基礎照明,讓陰影區域也有可見度:

vec3 ambient = uAmbientLight * textureColor * uAmbientLightIntensity;

3. 點光源計算
點光源使用蘭伯特漫反射模型,並包含距離衰減:

// 計算光源方向
vec3 lightDirection = uPointLightPosition - vPosition;
float distance = length(lightDirection);
lightDirection = normalize(lightDirection);

// 距離平方衰減(避免近距離過度曝光)
float attenuation = 1.0 / (1.0 + 0.09 * distance + 0.032 * distance * distance);

// 蘭伯特漫反射
float lightIntensity = max(dot(normal, lightDirection), 0.0);
vec3 diffuse = uPointLightColor * textureColor * lightIntensity * uPointLightIntensity * attenuation;

4. 鏡面反射
我們還實現了簡單的鏡面高光,模擬金屬表面的反射:

// 菲涅爾效應
vec3 viewDirection = normalize(cameraPosition - vPosition);
float fresnel = pow(1.0 - max(dot(normal, viewDirection), 0.0), 2.0);

// 金屬度和粗糙度影響
vec3 metallic = mix(textureColor, vec3(1.0), uMetalness);
float roughnessFactor = 1.0 - uRoughness;

// 鏡面反射
vec3 reflectDirection = reflect(-lightDirection, normal);
float specular = pow(max(dot(viewDirection, reflectDirection), 0.0), 4.0 * roughnessFactor);
vec3 specularColor = uPointLightColor * specular * metallic * fresnel * attenuation;

最終顏色由環境光、漫反射和鏡面反射組合而成:

vec3 finalColor = ambient + diffuse + specularColor * 0.3;

效果如下:

final

這是一套很經典的著色模型(Blinn-Phong Reflectance Model),當然你也可以從我之前推薦的 games101 的第 7 節 第39 分開始看到相關內容。

3.3.3 各向異性過濾

放大後中央模糊?別急!這是採樣角度導致的走樣問題 📉

sampling

這看齊來是真的挺醜的,接下來我們需要設定一個特殊的texture屬性值來解決它。——Anisotropic Filtering各向異性過濾

聽起來有點熟悉對不對?
很多人在做比較大的場景時,會將地板貼圖的各向異性過濾值調高,因為這樣可以讓"讓貼圖在遠處或傾斜的角度下依然保持清晰,不會變糊"。

anisotropic

但為什麼會出現這種走樣的情況呢?
原因在於當三維表面與攝像機夾角較大時,螢幕上壹個像素在紋理空間中的採樣區域會被透視投影拉伸,形成壹個長條或椭圓形的採樣足跡。傳統過濾算法假設採樣區域為正方形,因此在這種情況下容易出現模糊或走樣。(這裡同樣也推薦您去看 games101 圖形學入門課程中對於走樣和採樣相關章節)


原文出處:https://juejin.cn/post/7568136081301930047


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝22   💬4   ❤️6
681
🥈
我愛JS
📝1   💬4   ❤️2
45
🥉
酷豪
1
#5
1
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付