不用 Unity,Three.js 也能做出这种质量的能量护盾特效

游戏里的能量护盾是一类辨识度很高的视觉效果:半透明球形、表面覆盖六边形格子,被攻击时撞击点会向外扩散发光波纹,几次连击之后颜色从蓝变红,最终整个护盾溶解消失。这类效果在实时渲染里实现起来并不简单,涉及多个着色器技术的组合。

flow-shield-effect 是开发者 Christian Ortiz 开源的一个 Three.js 项目,用纯自定义 GLSL 着色器(没有借助外部 shader 库)实现了上述护盾效果的完整版本,并附带一个可以实时调参的交互式开发环境。项目定位为「生产质量」——着色器按功能拆分为独立文件,参数通过 GUI 全部暴露,可以直接作为工程参考。

在线演示地址:https://flow-shield-effect.vercel.app (opens in a new tab)

效果分为四个层次

六边形网格层

护盾表面的六边形网格是通过三平面投影(Tri-planar Projection)绘制的。三平面投影是一种将纹理映射到任意形状上的技术,不依赖模型的 UV 坐标,适合球体、有机形体等难以展平的形状。在此基础上,着色器还实现了单元格闪烁和底部衰减——越靠近护盾底部,格子越淡,增加体积感。

表面动态层

菲涅尔(Fresnel)效果让护盾边缘更亮、中心更暗,模拟真实世界中透明介质的光学特性。在静止状态下,护盾表面叠加了流体噪声动画,产生持续的能量扰动感;随着生命值下降,着色器会在蓝色和红色之间插值,颜色变化本身也是护盾状态的视觉反馈。

命中系统

这是效果里交互性最强的部分。着色器内置了一个环形缓冲区,最多同时追踪 6 次撞击。每次撞击发生时,护盾会在撞击点向外扩散发光环,同时该位置的六边形格子高亮显示,并触发生命值扣减。多次快速连击时,多个扩散波纹可以并存,互不干扰。

出现/消失动画

基于噪声的溶解(Dissolve)动画控制护盾的材质化和非材质化过程——护盾出现时从噪声边缘逐渐扩展成形,消失时反向溶解,边缘保留发光效果。动画进度通过一个参数手动控制,方便接入外部逻辑驱动。

后处理层面,项目集成了泛光(Bloom)和胶片噪点(Film Grain),前者增强发光区域的扩散感,后者给画面加一层细微质感。

几个核心技术点

六边形怎么贴到球面上

通常给模型贴纹理依赖 UV 展开,但球体的 UV 在两极会严重变形。这个项目的做法是:按法线最大分量把球面分成 6 个面,每个面单独做平面投影,在面与面的接缝处用 smoothstep 把六边形淡出,避免撕裂感:

float hexPattern(vec2 p) {
  p *= uHexScale;
  const vec2 s = vec2(1.0, 1.7320508); // sqrt(3),六边形的几何比例
  vec4 hC = floor(vec4(p, p - vec2(0.5, 1.0)) / s.xyxy) + 0.5;
  vec4 h  = vec4(p - hC.xy * s, p - (hC.zw + 0.5) * s);
  vec2 cell = (dot(h.xy, h.xy) < dot(h.zw, h.zw)) ? h.xy : h.zw;
  cell = abs(cell);
  float d = max(dot(cell, s * 0.5), cell.x);
  return smoothstep(0.5 - uEdgeWidth, 0.5, d);
}
 
// 接缝处让六边形消隐,避免投影切换时出现硬边
vec3 absN = abs(normalize(vObjPos));
float dominance = max(absN.x, max(absN.y, absN.z));
float hexFade = smoothstep(0.65, 0.85, dominance);

边缘发光 + 流体噪声

Fresnel 边缘光只需一行:视线与法线点积越小(越平行于表面)说明越是边缘,亮度越高。流体扰动用两层不同方向的 Simplex 噪声叠加,采样坐标随时间位移产生流动感:

// Fresnel:边缘比中心亮
float fresnel = pow(1.0 - dot(vNormal, vViewDir), uFresnelPower) * uFresnelStrength;
 
// 双层流动噪声,方向不同避免周期感
float t   = uTime * uFlowSpeed;
float fn1 = snoise(vObjPos * uFlowScale + vec3(t, t * 0.6, t * 0.4));
float fn2 = snoise(vObjPos * uFlowScale * 2.1 + vec3(-t * 0.5, t * 0.9, t * 0.3));
float flowNoise = (fn1 * 0.6 + fn2 * 0.4) * 0.5 + 0.5;

命中波纹怎么同时存在多个

JS 侧点击时把撞击点坐标和时间写入长度为 6 的环形缓冲区,超过 6 个就覆盖最旧的。Fragment Shader 每帧遍历全部 6 条记录,用 acos(dot(...)) 算球面测地距离,时间驱动波纹半径膨胀,两层衰减(时间 + 半径)让波纹自然消退:

for (int i = 0; i < MAX_HITS; i++) {
  float elapsed  = uTime - uHitTime[i];
  float isActive = step(0.0, elapsed) * step(elapsed, uHitDuration);
 
  // 球面大圆距离(比欧氏距离更准确)
  float dist = acos(clamp(dot(normPos, normalize(uHitPos[i])), -1.0, 1.0));
 
  // 波纹半径随时间膨胀,加噪声扰动让边缘不规则
  float ringR    = min(elapsed * uHitRingSpeed, uHitMaxRadius);
  float noiseD   = snoise(normPos * 5.0 + vec3(elapsed * 2.0)) * 0.05;
  float ring     = smoothstep(uHitRingWidth, 0.0, abs(dist + noiseD - ringR));
 
  // 时间衰减 × 半径衰减,波纹扩散到边缘后自然消失
  float fade       = 1.0 - smoothstep(uHitDuration * 0.5, uHitDuration, elapsed);
  float radialFade = 1.0 - smoothstep(uHitMaxRadius * 0.75, uHitMaxRadius, ringR);
  ringContrib += ring * fade * radialFade * isActive;
}

JS 侧对应代码,每次点击写入缓冲区并扣血:

const handleClick = (e: ThreeEvent<MouseEvent>) => {
  const localPoint = e.object.worldToLocal(e.point.clone()); // 转到对象空间
  const idx = hitIdxRef.current % MAX_HITS;                  // 环形覆盖
  hitIdxRef.current++;
  u.uHitPos.value[idx].copy(localPoint);
  u.uHitTime.value[idx] = timeRef.current;
  lifeRef.current = Math.max(0, lifeRef.current - hitDamage / 100);
};

溶解动画的边缘发光

用 3D Simplex 噪声采样对象空间位置,与阈值 uReveal 比较,低于阈值直接丢弃片元(discard)。边缘发光用两次 smoothstep 卡出一个窄带,护盾消失时这条发光边缘会在表面扫过:

float noise      = snoise(vObjPos * uNoiseScale) * 0.5 + 0.5;
float revealMask = smoothstep(uReveal - uNoiseEdgeWidth, uReveal, noise);
if (revealMask < 0.001) discard; // 丢弃低于阈值的片元
 
// 用两段 smoothstep 卡出边缘发光窄带
float innerFade  = mix(0.98, 0.15, uNoiseEdgeSmoothness);
float edgeLow    = smoothstep(uReveal - uNoiseEdgeWidth,
                              uReveal - uNoiseEdgeWidth * innerFade, noise);
float edgeHigh   = smoothstep(uReveal - uNoiseEdgeWidth * 0.15, uReveal, noise);
float revealEdge = edgeLow * (1.0 - edgeHigh); // 相减得到发光环

开发环境与使用方式

项目本身是一个完整的 Next.js 应用,内置了一个 Unity 风格的 3D 开发环境,方便直接在浏览器里调试效果。

克隆并启动:

git clone https://github.com/cortiz2894/flow-shield-effect.git
cd flow-shield-effect
pnpm install
pnpm dev

访问 http://localhost:3000 (opens in a new tab) 即可进入开发环境。

开发环境提供的控制项:

功能说明
实时参数调节通过 Leva GUI 调整所有着色器参数,修改即时生效
轨道相机鼠标拖拽旋转视角,滚轮缩放
光照控制实时调整场景光源方向和强度
HDRI 环境可切换环境贴图,影响反射和环境光
GLB 模型导入支持导入自己的 3D 模型替换默认球体

项目内置两个预设场景:Default(浮动护盾球)和 Droideka(星球大战蜘蛛机器人模型),可快速切换查看效果差异。

代码组织方式

作者将着色器代码拆分为独立文件,而不是写成一个大 GLSL 文件:着色器逻辑、控制参数和常量分属不同文件。对于想学习或修改其中某个具体效果(比如只看命中系统的实现,或只参考溶解动画的写法)的开发者来说,定位代码的成本相对较低。

技术栈完整列表:

版本用途
Three.js0.1823D 渲染核心
React Three Fiber-Three.js 的 React 封装
Drei-R3F 常用工具集
Next.js16.1应用框架
Leva-实时参数调节 GUI
TypeScript-类型支持

GLSL 部分全部为手写,没有依赖 glsl-noise 等外部着色器工具库。

写在最后

flow-shield-effect 适合以下几类使用场景:作为游戏 UI 或科幻题材 Web 项目的视觉效果参考;学习 Fresnel、流体噪声、溶解动画等着色器技术的具体实现;或者作为 React Three Fiber 项目的完整工程结构参考。

它本身不是一个可以 npm install 直接引入的库,而是一个完整的演示工程,使用方式是克隆后按需提取或改写其中的着色器代码。项目目前版本为 v1.01(2026-03-03 更新),处于活跃维护状态。

GitHub 地址:https://github.com/cortiz2894/flow-shield-effect (opens in a new tab)