Three.js教程
Shader
charged-blast-vfx-threejs-shader
charged-blast-vfx-threejs-shader

不用 Unity Three.js 也能做出洛克人能量炮发射

洛克人(Mega Man)里那个经典动作:按住射击键,角色手部开始聚集能量,光球越来越亮,松手的瞬间一道带着光尾的能量炮打出去——这套特效被 Creative Developer Christian Ortiz 用 Three.js 和自定义 GLSL Shader 在浏览器里完整复刻了出来,并开源到了 GitHub。

项目名叫 Charge Shot VFX,包含从充能到发射的完整特效链:充能光球、环绕粒子、旋转光环、弹道、枪口闪光、运动拖尾、描边——每一个视觉层都有独立的 Shader 实现。附带一个实时调参的 3D Playground,可以对每个参数现场调整、实时预览效果。

特效组成

整套效果拆开来,有七个独立的视觉层,每一层都有单独的 Shader 负责。

充能光球(Charge Orb)

光球由三个叠加的 Shader 层构成,都挂在同一个几何体上,材质设为加法混合(Additive Blending),叠得越多越亮。

最外层是边缘泛光,核心是 Fresnel 计算——法线与视线的点积越小(越接近球的边缘)亮度越高,再乘以充能进度 uProgress,光晕随蓄力过程线性增强:

float f    = 1.0 - abs(dot(normalize(vNormal), vec3(0.0, 0.0, 1.0)));
float glow = pow(clamp(1.0 - f, 0.0, 1.0), uFalloffPower);
float alpha = glow * uEmission * uProgress;

最内层是流动噪声,用 domain-warped FBM(先用一层噪声扰动采样坐标,再对扰动后的坐标算 FBM)生成有机感的表面扰动,UV 随时间偏移产生流动效果,并用一个环形遮罩限制在球体轮廓区域内:

float cloud = smoothstep(0.3, 0.75, nv);
float mask  = smoothstep(0.05, 0.4, f) * smoothstep(1.0, 0.35, f);
float alpha = cloud * mask * uFlowIntensity * uProgress;

三层叠加后,光球边缘有 Fresnel 泛光,表面有噪声流动,整体亮度随充能进度 0→1 变化。

环绕粒子

粒子用 Instanced Mesh 实现——最多 80 个粒子作为一批实例化的广告牌四边形,共享一次 draw call。粒子的透明度(alpha)编码进 instanceColor.r,值是 sin(t / maxLife * PI),从 0 升到 1 再回到 0,出现和消失自然平滑:

float d      = length(vUv - 0.5) * 2.0;
float circle = 1.0 - smoothstep(0.5, 1.0, d);
float alpha  = circle * vAlpha; // vAlpha = sin(t/maxLife * PI)

粒子还有向光球中心的径向拉力,结合三轴 sin 扰动产生螺旋聚拢的轨迹。触发发射时,不是直接清除粒子,而是把剩余生命设为接近最大值,让它们自然淡出,形成能量释放时向外散开的视觉感。

弹道

蓄力弹的颜色带用正弦波驱动三个颜色(A → C → B)循环滚动,颜色沿弹轴方向随时间向前移动,视觉上像能量在内部流动:

float t        = (uTipY - vPosY) / max(0.001, uTipY - uTailY);
float scrolled = fract(t - uTime * uGradientSpeed);
float wave     = sin(scrolled * PI);
float blend    = pow(max(0.0, wave), max(0.01, 1.0 / uBandWidth));
vec3  col      = mix(mix(uColorA, uColorC, t1), uColorB, t2);
col += uFresnelColor * fresnel * uFresnelEmission;

拖尾用法线与视线的点积控制边缘透明度衰减,避免圆柱截面的硬截断感。普通弹只有基础颜色加 Fresnel,Shader 简化很多,视觉上和蓄力弹差别明显。

枪口闪光(Muzzle Flash)

闪光在一个平面几何体上纯靠 Shader 绘制。极坐标计算 UV 到圆心的距离挖出圆孔(uInnerRadius 由 GSAP 动画从 0 驱动到 1),射线条通过角度取模后做 smoothstep 生成等分射线:

float angle   = atan(dir.y, dir.x);
float sector  = fract(angle / 6.28318 * uNumRays);
float rayMask = 1.0 - smoothstep(uRayWidth - soft, uRayWidth + soft, edge);
float inner   = smoothstep(uInnerRadius - uEdgeSoftness, uInnerRadius, d);
float alpha   = outer * inner * rayMask * uEmission;

旋转和淡出是两条独立的 GSAP 时间线,闪光视觉上消失后转动还在继续,等完全静止时效果更自然。

描边

给带骨骼动画的角色加描边有个经典坑:直接把网格放大,描边会卡在 T-Pose 不跟动画走。这里用 onBeforeCompile 解决——在 Three.js 编译 Shader 时注入代码,在骨骼蒙皮计算 #include <skinning_vertex> 之后才做法线外扩,所以顶点位置已经是动画变形后的结果:

shader.vertexShader = shader.vertexShader.replace(
  '#include <skinning_vertex>',
  `#include <skinning_vertex>
   transformed += normal * uOutlineWidth;`
);

材质设为 BackSide(只渲染背面),正面被原始网格遮住,外轮廓部分露出来形成描边。

后期处理

叠加了亮度阈值 Bloom(发光区域经多级模糊后叠回原图向外晕散)和胶片颗粒,让整体画面有游戏和电影之间的质感,而不是普通的 3D 渲染感。

调参 Playground

所有 Shader 参数都通过 Leva(为 React Three Fiber 设计的 GUI 库)暴露到界面上,可以实时拖动调整。比起单纯看代码,这种方式能直接感受每个参数对视觉结果的影响——先调出感觉,再对照 Shader 实现,理解会快很多。

如何运行

需要 Node.js 18 及以上,推荐用 pnpm:

git clone https://github.com/cortiz2894/charged-blast-vfx.git
cd charged-blast-vfx
pnpm install
pnpm dev

启动后访问 http://localhost:3000 即可。按住空格键充能,短按发普通弹,按住超过 0.5 秒松开发蓄力炮。也可以直接访问线上 Demo:charged-blast-vfx.vercel.app

写在最后

这个项目的价值主要在两个方向:一是作为视觉参考,如果你在做游戏风格的 Web 交互,这套特效的完成度足够高,可以直接借鉴思路;二是作为 Shader 学习材料,Fresnel 效果、FBM 噪声、Instanced Mesh 粒子、蒙皮描边这几个技术点,在这份代码里都有完整的 GLSL 实现,可以逐个拆开来研究。

https://github.com/cortiz2894/charged-blast-vfx (opens in a new tab)