🪐 行星科技概念官网!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" 是网页设计中的一个术语,通常指页面顶部的大型横幅区域。 对于开发者而言,它可以更直观地理解为:用户在访问网站瞬间所感受到的视觉冲击,或促使他们停留在网站的关键视觉因素。

相信大家偶尔也会刷到一些以星球为主题的官网,看起来既梦幻又酷炫。

这些网站往往拥有天马行空的页面布局,美丽的星球在画面中静静流动,营造出一种未来科技与宇宙幻想交织的氛围。

于是,我让 GPT 🧠 通过文生图生成了一张原型设计稿:

Page 原型图

并尝试将其复原,于是得到了以下结果

Page 静态预览

Page 动图

由于平台图片体积限制, 动画画质存在大幅抽帧和压缩,还请各位可以在 PC 端自行体验

PC端在线预览地址(需要魔法): isgalaxias.vercel.app/

DeBug 在线调试界面: isgalaxias.vercel.app/#debug

Github 仓库地址: github.com/hexianWeb/i...

转发贴环节

关注我的掘友最熟悉的环节了,那么让我来介绍本次项目,它被 Threejs Journey 课程作者 Bruno Simon 转发

(其实项目 8 月底就写完了,但是我疯狂鸽子,非常抱歉!)

3.场景搭建 🧱

由于本专栏主要聚焦于 Three.js,因此本文不会详细讲解从 0 到 1 的完整页面实现过程,而是重点介绍与 Three.js 相关的实现部分。

首先,让我们分析一下当前场景(Scene)中的主要元素:

  • 最外层的 星云图背景
  • 散布在空间中的 星点中心星环
  • 漂浮在太空中的 三颗行星

3.1 星云图背景 🌌

这一部分的实现非常简单:只需将加载好的星云贴图设置为场景的 background 即可。

在此也顺便推荐一个优秀的 3D 贴图资源站 ------ Solar System Scope。 本项目中使用的所有行星与星云贴图,均来自该网站。 这里找到星云图

但注意这是一个根据 CC Attribution 4.0 许可证提供各种行星纹理的网站,因此需要提供相应的版权信息。如果您将网站上线,请务必在页面上的某个位置显示这些版权信息。

随后,在 Three.js 中加载贴图,并将其赋值为场景背景。 同时可以通过 backgroundIntensity 调整背景亮度,使整体层次更柔和。

js 复制代码
this.scene.background = this.resources.items.spaceTexture
this.scene.backgroundIntensity = 0.25

3.2 星星 与 星环✨

这一部分构成了整个 Hero Section 的核心视觉焦点。我们通过 Points 系统创建了一个由大量粒子组成的螺旋星系,其中包含星星和中心星环。

乍一看,当前的实现方式似乎可以分为两个部分,"中心的粒子圆环和外层的粒子群",以及周围缓慢旋转的粒子群对吗?但是仔细看"中心的粒子圆环和外层的粒子群",似乎并没有那么强的割裂感,反而更像是粒子圆环带动了整个粒子群一同旋转,对吧?这是怎么做到的?

其实这只是一个非常巧妙的视觉错觉,有点像小时候玩的万花筒。如下图

当我们放开控制器、稍微调整视角时,就会发现这个结构实际上只是一个简单的圆柱体。 通过透视营造出"浩瀚星海"的假象。

于是,现在你大概已经能想到几种实现方式了:

  • 利用MeshSurfaceSampler在多个圆柱体表面使用对几何体平面进行采样,以构建粒子位置;
  • 或者在构建 BufferGeometry 时,利用三角函数约束顶点分布,使所有粒子落在圆环轨迹上。
  • 下面我来分享我在项目中采用的具体思路。
3.2.1 实现思路 🌠

可以概括为以下几个步骤:

  1. 生成螺旋分布的粒子位置:基于极坐标系统,沿着多个分支(branches)生成螺旋分布的粒子
  2. 应用圆环约束:使用数学函数将粒子约束在环形区域内,形成星环效果
  3. 添加随机扰动:为每个粒子添加随机偏移,营造自然的星系形态
  4. 着色器动画:在顶点着色器中实现旋转动画,让星系"流动"起来
3.2.2粒子位置生成 💫

setGalaxy() 方法中,我们为每个粒子生成位置数据:

js 复制代码
// 生成在环形区域内的半径
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 圆环约束算法 🌌

为了让粒子呈现出星环的效果,我们使用了圆环约束算法

js 复制代码
// 计算距离圆环中心的归一化距离(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轴添加了随机偏移:

js 复制代码
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%

当然也可以把这个效果去掉,当前效果会变为下图:

或许你有更大胆的创意?在中心部分放上一些特殊的星空元素?比如黑洞,或者一只巨大的"外星之眼" 👁️让整个场景更具神秘感。

3.2.5 粒子圆环带动粒子群 🌀

到目前为止,我们已经定义了粒子在空间中的分布关系,也确定了它们在星环结构中的约束逻辑。接下来要做的,就是让这些粒子 动起来 ------ 让星环旋转,从而带动整个星系流动。

动画实现思路

在这里我们通过 顶点着色器(vertex shader) 来实现粒子的动态旋转。相比在 JavaScript 层更新所有粒子坐标,使用 GPU 端的位移计算可以大幅降低性能消耗,并让动画更流畅。

核心逻辑是基于 粒子到中心的距离衰减旋转速度

  • 离中心越近 → 旋转越快
  • 离中心越远 → 旋转越慢

这样能让整个星环在旋转时呈现一种"内圈快、外圈慢"的自然涡旋感。

顶点着色器动画逻辑
glsl 复制代码
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); 用于控制旋转的衰减速率。

如果旋转速度变化过大,可以通过以下方式做平滑限制:

glsl 复制代码
distanceToCenter = clamp(distanceToCenter, 0.0, 10.0);

让距离过近或过远的粒子保持在合理的旋转速率范围内。

最终呈现出的效果,就是一个不断旋转、充满空间层次感的 银河涡旋

3.3 星球 🌍

这一部分是整个页面中最具表现力的视觉元素 ------ 三颗漂浮在太空中的星球。

而对于如何在 Threejs里面显示一颗星球,我想已经有太多技术文章来描述这一过程了。这里我也推荐你去看 Threejs jouneryEarth Shader 章节。(这里 B 站链接,但是有条件还是建议补补票,作者会在每个圣诞节给 discount )

这里我主要描述当前页面的星球实现与其余教程的不同点。

星球渲染包含几个关键部分:

  1. 星球主体:使用各向异性过滤提升纹理质量。使用置换贴图增加地形细节,法线贴图增强表面凹凸感
  2. 光照系统:自定义环境光 + 点光源,支持衰减、漫反射和镜面反射
3.3.1 星球主体 🪐

放大星球可以看到表面存在明显的地形起伏,这通常会让人想到 MeshStandardMaterialbumpMapdisplacementMap。 但在本项目中,我并没有直接使用标准物理材质,而是采用了 ShaderMaterial,并在顶点着色器中根据 法线贴图 自行实现位移和光照控制。 这么做的原因是为了后续自定义光照系统做铺垫。

当然条条大路通罗马,你也可以通过

方案一MeshStandardMaterial+ 置换/法线贴图 + 点光源(快速简洁)

方案二 :使用 THREE-CustomShaderMaterial 在标准光照的基础上扩展自定义 Shader

这里仅提供我的实现方案

首先我们先创建一个顶点足够多的球体,再简单的创建ShaderMaterial以及应用baseColor到球体上后有以下几点需要注意

  1. 因为后面置换贴图在顶点着色器中改变顶点位置,形成地形起伏,需要通过操控顶点位置来实现这种凹凸地形,所以我们需要分配足够多的顶点给后续vertexShader假如没有分配足够多的顶点则会导致地形呈现"方块感":
js 复制代码
// 创建星球几何体(增加细分以支持置换)
this.geometry = new THREE.IcosahedronGeometry(this.radius, 64, 64)

较少顶点情况:

在顶点着色器中,我们使用置换贴图来改变顶点的位置,形成地形起伏:

glsl 复制代码
// 从置换贴图的红色通道读取高度值
float displacement = texture2D(uDisplacementMap, uv).r;	// uniform 传入 网站上下载好的贴图即可

// 沿法线方向偏移顶点位置
vec3 displacedPosition = position + normal * displacement * uDisplacementScale;

uDisplacementScale 控制置换强度,数值越大,地形起伏越明显。

现在你应该拥有了一个凹凸不平的星球

3.3.2 光照系统

片元着色器负责计算最终的颜色,包含以下几个关键步骤:

1. 法线贴图处理

我们实现了完整的 TBN(切线-副切线-法线)矩阵计算,将法线贴图从切线空间转换到世界空间:

glsl 复制代码
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. 环境光计算

环境光提供基础照明,让阴影区域也有可见度:

glsl 复制代码
vec3 ambient = uAmbientLight * textureColor * uAmbientLightIntensity;

3. 点光源计算

点光源使用兰伯特漫反射模型,并包含距离衰减:

glsl 复制代码
// 计算光源方向
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. 镜面反射

我们还实现了简单的镜面高光,模拟金属表面的反射:

glsl 复制代码
// 菲涅尔效应
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;

最终颜色由环境光、漫反射和镜面反射组合而成:

glsl 复制代码
vec3 finalColor = ambient + diffuse + specularColor * 0.3;

效果如下:

这是一套很经典的着色模型(Blinn-Phong Reflectance Model ),当然你也可以从我之前推荐的 games101 的第 7 节 第39 分开始看到相关内容。

3.3.3 各向异性过滤

放大后中央模糊?别急!这是采样角度导致的走样问题 📉

这看齐来是真的挺丑的,接下来我们需要设定一个特殊的texture属性值来解决它。------Anisotropic Filtering 各向异性过滤

听起来有点熟悉对不对?

很多人在做比较大的场景时,会将地板贴图的各向异性过滤值调高,因为这样可以让"让贴图在远处或倾斜的角度下依然保持清晰,不会变糊"。

但为什么会出现这种走样的情况呢?

原因在于当三维表面与摄像机夹角较大时,屏幕上一个像素在纹理空间中的采样区域会被透视投影拉伸,形成一个长条或椭圆形的采样足迹。传统过滤算法假设采样区域为正方形,因此在这种情况下容易出现模糊或走样。 (这里同样也推荐您去看 games101 图形学入门课程中对于走样和采样相关章节)

这里我做简单解释,可能不太准确,最好还是自己系统学习:

首先我们要明确一点,当 GPU 渲染模型时,它会计算屏幕上每个像素的颜色。 要得到颜色,就需要"采样"纹理(texture):

bash 复制代码
屏幕像素 → 给定一个像素在三维表面上的位置 → 通过 UV 映射 → 找到对应贴图坐标 → 从贴图图像中取出颜色值。

当表面与摄像机有角度、或者表面离得太远时会出现一个屏幕像素对应到贴图上多个像素点(过采样)。

若是最理想的情况下,就是贴图当前与摄像机为正交视图(假设现在一个格子就是一个实际像素点)

此时每个屏幕像素都对应贴图中 等面积的区域(正交视角,且物体远)此时无论是点采样,还是双线性过滤,三线性过滤。各个区域显示的内容应该都是"一致"的,采样逻辑一致,在正交情况下得到的采样结果就会一致,画面就会一致。

当视角逐渐倾斜时,情况便发生了变化。可以看到,贴图在透视关系下被压缩成一个梯形,而屏幕上的某一个像素点此时对应到纹理空间中的"长条"区域。这意味着在同样的屏幕分辨率下,纹理需要被更密集地采样才能保持清晰。然而,传统的双线性或三线性过滤只会在纹理的相邻像素间进行平均,它并不能感知"方向性"的拉伸。

于是,当这种拉伸主要沿着某一个方向(例如 V 轴)发生时,原有过滤会出现"模糊一片或锯齿拖影"的情况。

各向异性过滤(AF)正是为了解决这种在倾斜角度观察纹理时的模糊问题 而提出的。 各向异性过滤(Anisotropic Filtering)会估算像素在纹理空间中的椭圆采样范围,并在主要拉伸方向上进行多次采样(通常 2 至 16 次),再将结果加权平均。这种方式能在倾斜角度下保持纹理细节清晰。

现在让我们将贴图信息应用上 AF

js 复制代码
// 设置主纹理 & 各项异性过滤
const texture = this.resources.items[this.textureName] // 从 Solar System Scope 下载的贴图
texture.generateMipmaps = true	// 默认为 true 是否为纹理生成 mipmap 用于后续三线性插值
texture.minFilter = THREE.LinearMipMapLinearFilter // 三线性插值
texture.colorSpace = THREE.SRGBColorSpace	// 色彩空间
texture.anisotropy = 8	// 设定各向异性值 越大越耗性能!!

当然,如果你想知道当前最大可支持各向异性值是多少。可以通过console.log('Max AF:', renderer.capabilities.getMaxAnisotropy()); 来获取该值可设定的最大值是多少。

但目前来说 8 对我们够用。

现在在看星球最后的问题也解决了!

当然,我无法通过一篇文章来讲清楚所有的内容,包括 为什么要用TBN?大气层是怎么实现的?场景辉光镜头怎么实现?一切还请观众老爷们去看看源码。

4.最终成果 🎉

细节调整完毕后,网站终于大功告成!🚀

5.最后的一些话 🗣

人们常说,AI 时代将取代那些碌碌无为的人。 但我更愿意相信,AI 的到来,是在解放那些富有创造力的灵魂------ 让他们得以更轻易地将脑海中的灵感化为现实 💫。

至于 Web3D,这个概念似乎一直徘徊在热潮之外, 既未消逝,也未真正崛起。 然而,随着 AI 的出现技术门槛的逐渐降低,它或许终将迎来属于自己的时刻 🚀。

我已经厌倦了那些平平无奇、千篇一律的网页。 我希望未来的网络世界能拥有------ 更多的色彩 🎨, 更多的想象空间 🌌, 更多被 AI 点燃的创造之光 ✨。

而我愿意,为那些仍然相信创作力的人,铺出一条属于未来的路。 🌈

6.专栏

本专栏的愿景

本专栏的愿景是通过分享 Three.js 的中高级应用和实战技巧,帮助开发者更好地将 3D 技术应用到实际项目中,打造令人印象深刻的 Hero Section。我们希望通过本专栏的内容,能够激发开发者的创造力,推动 Web3D 技术的普及和应用。

加入社区,共同成长

如果您对 Threejs 这个 3D 图像框架很感兴趣,或者您也深信未来国内会涌现越来越多 3D 设计风格的网站,欢迎加入 ice 图形学社区 。这里是国内 Web 图形学最全的知识库,致力于打造一个全新的图形学生态体系!您可以在认证达人里找到我这个 Threejs 爱好者和其他大佬。

此外,如果您很喜欢 Threejs 又在烦恼其原生开发的繁琐,那么我诚邀您尝试 TresjsTvTjs , 他们都是基于 VueThreejs 框架。 TvTjs 也为您提供了大量的可使用案例,并且拥有较为活跃的开发社区,在这里你能碰到志同道合的朋友一起做开源!

7.往期回顾

🎮 前端也能造城市?源码公开:那个被外网 2.7 万人围观的 Three.js 小游戏

😲我又写出了被 Three.js 官推转发的项目?!🥳🥳(源码分享)

😮😮😮 我写出了被 Threejs 官推转发的项目🚀✨?!

相关推荐
前端拿破轮8 小时前
ReactNative从入门到性能优化(一)
前端·react native·客户端
码界奇点8 小时前
Java Web学习 第1篇前端基石HTML 入门与核心概念解析
java·前端·学习·xhtml
云枫晖8 小时前
Webpack系列-开发环境
前端·webpack
Rverdoser8 小时前
制作网站的价格一般由什么组成
前端·git·github
拉不动的猪8 小时前
深入理解 JavaScript 中的静态属性、原型属性与实例属性
前端·javascript·面试
linda26188 小时前
链接形式与跳转逻辑总览
前端·javascript
怪可爱的地球人8 小时前
骨架屏
前端
用户677847150628 小时前
前端将html导出为word文件
前端
前端付豪8 小时前
如何使用 Vuex 设计你的数据流
前端·javascript·vue.js