不用 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.js | 0.182 | 3D 渲染核心 |
| React Three Fiber | - | Three.js 的 React 封装 |
| Drei | - | R3F 常用工具集 |
| Next.js | 16.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)