体验地址:https://themonolithproject.net/
原文发布于 2025 年 11 月 29 日要做一个包含 13 个完全不同场景、还得保持统一美术风格的 WebGL 大项目,最核心的问题其实是:怎么才能不写 13 遍几乎一模一样的材质和特效代码?答案是我们基于 React Three Fiber 搞了四套高度可组合的系统:1.延迟渲染 + 描边2.可组合材质系统3.可组合粒子系统4.场景切换系统下面把整个技术思路原原本本讲清楚。先说背景和美术
项目发起人 Kehan 直接找到我,他已经让插画师 Lin 画好了一批概念图。那种手绘感极强的笔触和配色,一看就让人上头。我们迅速拉了个小团队:Fabian 写 shader、Nando 负责创意、Henry 做 3D、Daisy 制片,后期 HappyShip 还来救场。整个项目期间,大家每天都在疯狂扔参考图,我的项目书签夹到现在还躺着 50+ 条链接。
1. 延迟渲染 + 彩色描边
美术风格里最醒目的就是彩色描边。我们调研了当时主流的三种做法:1.基于深度+法线做边缘检测(后处理)2.倒置外壳(Inverse Hull)3.Material ID 索性直接存颜色最后选了第一种。倒置外壳一拉近相机描边粗细就变,Material ID 又搞不定粒子云。深度+法线是最稳的。法线 G-Buffer
用 Three.js 的 WebGLRenderTarget count 开多个 G-Buffer。法线那张用了八面体编码,把 vec3 塞进 RG16F 里,能省一半显存,代价就是编码解码多几行代码。描边颜色 G-Buffer
本来可以用一个 4-8 色的查找表把颜色也压到几个 bit,但为了调起来方便,我们直接用了完整的 RGB。最终描边
准备好 G-Buffer 后,跑一遍卷积滤波检测边缘,再把颜色 G-Buffer 的颜色填上去。过程中参考了 Maxime Heckel 的 Moebius Style 和 Visual Tech Art 的 Outline 实现,站了巨人的肩膀。坑Three.js 一旦开了 count > 1,所有原生 Material(MeshBasicMaterial 之类)默认就不出片了。必须给每个 location 写点东西,哪怕是把自己原样输出:
layout(location =1) out vec4 gNormal;
voidmain() {
gNormal = gNormal; // 就是占个坑
}2. 可组合材质系统13 个场景,几百个物体,每个物体材质都不一样,但很多效果其实是重复的:渐变、流动贴图、风吹、描边、明暗调整……于是我们把“一段 shader 功能 + 它需要的 uniform + 它需要的 JS 逻辑”封装成一个 React 组件,想用哪个就挂哪个。核心是GBufferMaterial,本质是个带一堆插入点的 ShaderMaterial:
uniform float uTime;
/// insert
voidmain() {
vec2 st = vUv;
/// insert
}
最简单的模块长这样(颜色模块):
exportconstMaterialModuleColor =forwardRef(({ color, blend =''}, ref) => {
const_color =useColor(color);
const{ material } =useMaterialModule({
name:'MaterialModuleColor',
uniforms: { uColor: { value: _color } },
fragmentShader: {
setup:`uniform vec3 uColor;`,
main: `pc_fragColor.rgb ${blend}= uColor;`,
},
});
useEffect(() => material.uniforms.uColor.value = _color, [_color]);
useImperativeHandle(ref, () => _color);
return>;<br />});
用起来就是给 Monolith 本身挂一堆模块:
<MaterialModuleGradient color1="#71b3dd"color2="#acc9ad"mixFunc="st.y"/>
<MaterialModuleAnimatedGradient speed={0.4} blend="*"/>
<MaterialModuleBrightness amount={2} />
<MaterialModuleMap map={tDetails} blend="*"oneMinus={true} />
<MaterialModuleFlowMapColor color="#444444"blend="+"/>
{/* ...还有风、扭曲顶点之类的模块 */}
所有模块在整个项目里到处复用,改一个地方全站生效,爽得不行。3. 可组合粒子系统粒子系统也照葫芦画瓢,核心是ParticleSystem组件,用 ping-pong render target 自己算 position/velocity/rotation/life。同样留了插入点给模块用:
if(needsReset) {/// insert }
/// insert
nextPosition += currVelocity * uDelta;
/// insert 发射器模块(Emission):●EmissionPlane、EmissionSphere●最强的是 EmissionShape,直接传一个 geometry,用 MeshSurfaceSampler 在表面采样行为模块:●VelocityAddDirection / OverTime / Noise●PositionAddMouse(鼠标推拉粒子)●PositionSetSpline(强制走样条曲线,行星环就是这么做的)行星环例子:
<ParticleSystem maxParticles={amount} rate={amount/200} prewarm looping>
<EmissionSphere radius={2} />
<RotationSetRandom speed={1.5} />
<GBufferMaterial depthWrite={false} transparent>
<MaterialModuleFlowMapColor blend="+"/>
注意:粒子材质直接用的前面那套可组合材质模块,所以鼠标经过的流动效果跟 Monolith 本体是完全同一个模块。4. 场景切换系统13 个场景,切换花样还特别多:向上擦除、径向模糊、zoom blur、球形展开、mask 蒙版……我们统一做法:先把 A、B 两个场景都用延迟渲染打出来(带 depth/normal),然后丢给一个 fullscreen triangle 去混。写了四套过渡材质:●MaterialTransitionMix(最万能,靠一张灰度混图 + progress)●MaterialTransitionZoom(zoom blur)●MaterialTransitionRadialPosition(径向)●MaterialTransitionRaymarched(球形展开)混图可以是:●运行时实时生成的矩形(向上擦除)●把目标场景用特殊 mask 模式再渲染一次得到的黑白图(tablet → fall)延迟渲染本身也被做成了可组合的模块,最后长这样:
<DeferredChromaticAberration maxDistortion={0.03} />
<DeferredLighting ambient={1} />
<DeferredOutline depthThreshold={0.1} normalThreshold={0.1} />
{/* 沙漠那段大气最明显 */}
最后说说感受这几套系统最大的价值就是:把过去那种几百行巨型 shader 文件、复制粘贴一堆 uniform 的日子彻底干掉了。现在想加一个新效果,只需要写一个 50 行的小模块,挂到需要的地方就行。调试的时候改一个模块,全站实时热重载,效率高到飞起。更重要的是,整个代码结构变得特别清晰,谁接手都能一眼看懂“这个渐变是哪个模块”“这个风是哪个模块”,再也不用在几千行 GLSL 里找魔法数字。如果以后再做一个风格化大项目,我大概率还会沿用这套玩法,只不过现在有 TSL 了,估计能做得更优雅。以上,就是《The Monolith》能按时上线、还保持高质量的真正原因。Ethan Chiu2025.11.29
https://tympanus.net/codrops/2025/11/29/building-the-monolith-composable-rendering-systems-for-a-13-scene-webgl-epic/
.cls-1{fill:#001e36;}.cls-2{fill:#31a8ff;}
.cls-1{fill:#001e36;}.cls-2{fill:#31a8ff;}
.cls-1{fill:#001e36;}.cls-2{fill:#31a8ff;}
.cls-1{fill:#001e36;}.cls-2{fill:#31a8ff;}
.cls-1{fill:#001e36;}.cls-2{fill:#31a8ff;}
.cls-1{fill:#001e36;}.cls-2{fill:#31a8ff;}
.cls-1{fill:#001e36;}.cls-2{fill:#31a8ff;}
.cls-1{fill:#001e36;}.cls-2{fill:#31a8ff;}
.cls-1{fill:#001e36;}.cls-2{fill:#31a8ff;}
.cls-1{fill:#001e36;}.cls-2{fill:#31a8ff;}
.cls-1{fill:#001e36;}.cls-2{fill:#31a8ff;}
