关于 shader-glsl
这是一项用于编写 GLSL GPU 片段着色器的技能,适用于生成视觉效果、图像转场和后期处理。内容包括着色器技巧、Three.js 配置、性能建议,以及渲染和检查可复现帧的流程。
主要功能
- 01生成动画渐变、噪声和图案
- 02绘制 SDF 形状与抗锯齿边缘
- 03制作溶解、位移和故障转场
- 04配置 Three.js ShaderMaterial
- 05移动设备与着色器性能建议
适用场景
- 01制作动画背景
- 02构建图像转场
- 03添加后期处理效果
- 04在固定时间检查着色器帧
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ iart-ai/ webgl-animation-skills --skill shader-glsl安装后在 Claude Code 中输入/shader-glsl确认已加载。
附带文件README.mdreferences/glsl-cookbook.md
详细介绍
着色器 / GLSL
编写 GPU 片元着色器,用于生成动态效果、渐变、转场和后期处理。片元着色器会并行地为每个像素运行一次,是实现全屏生成式动态效果的最高性能方式。
适用场景
- 动态渐变、噪声、等离子或极光背景。
- 图像转场:溶解、位移、故障、波纹、擦除。
- 扭曲、色差、生成式图案、SDF 形状。
- 对渲染场景执行后期处理。
片元着色器骨架
每个片元着色器都会计算一个像素的颜色。先将坐标归一化并校正宽高比,再构建颜色。
precision highp float;
uniform float u_time;
uniform vec2 u_resolution;
uniform vec2 u_mouse;
void main() {
vec2 uv = gl_FragCoord.xy / u_resolution.xy; // 0..1
vec2 p = uv * 2.0 - 1.0; // -1..1, centered
p.x *= u_resolution.x / u_resolution.y; // aspect-correct
vec3 col = 0.5 + 0.5 * cos(u_time + p.xyx + vec3(0.0, 2.0, 4.0));
gl_FragColor = vec4(col, 1.0);
}
上面的余弦调色板代码(Inigo Quilez 调色板)是快速实现出色动态渐变的最佳方式:a + b*cos(2π*(c*t + d)),其中 a,b,c,d 是可调的 vec3。
核心构件
smoothstep + mix 是最常用的函数。smoothstep(e0, e1, x) 会生成从 0 到 1 的平滑渐变;mix(a, b, t) 会进行线性混合。按一个像素的宽度对边缘进行抗锯齿处理:
float px = fwidth(d); // screen-space derivative
float mask = smoothstep(px, -px, d); // crisp AA edge from SDF distance d
哈希 + 值噪声(无需纹理):
float hash(vec2 p){ return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }
float noise(vec2 p){
vec2 i = floor(p), f = fract(p);
vec2 u = f * f * (3.0 - 2.0 * f); // smooth interpolation
return mix(mix(hash(i), hash(i + vec2(1,0)), u.x),
mix(hash(i + vec2(0,1)), hash(i + vec2(1,1)), u.x), u.y);
}
float fbm(vec2 p){ // fractal noise, organic
float v = 0.0, a = 0.5;
for (int i = 0; i < 5; i++){ v += a * noise(p); p *= 2.0; a *= 0.5; }
return v;
}
完整的值噪声、单纯形噪声和 fbm 变体见 references/glsl-cookbook.md。
SDF 形状可生成与分辨率无关的清晰几何图形。距离值在内部为负,在外部为正:
float sdCircle(vec2 p, float r){ return length(p) - r; }
float sdBox(vec2 p, vec2 b){ vec2 d = abs(p) - b; return length(max(d,0.0)) + min(max(d.x,d.y),0.0); }
// render: float m = smoothstep(fwidth(d), -fwidth(d), d);
域扭曲可实现流动、大理石纹理效果——将噪声输入另一个噪声:
vec2 q = vec2(fbm(p), fbm(p + vec2(5.2, 1.3)));
float n = fbm(p + 4.0 * q + u_time * 0.1);
图像转场
采样两个纹理,并根据进度 uniform u_progress(0→1)逐像素混合。
溶解 / 噪声擦除——通过对噪声进行阈值处理来显示图像:
float n = noise(uv * 20.0);
float edge = smoothstep(u_progress - 0.05, u_progress, n);
gl_FragColor = mix(texture2D(tex0, uv), texture2D(tex1, uv), 1.0 - edge);
位移——采样前使用位移贴图偏移 UV:
float disp = texture2D(dispTex, uv).r;
vec2 d0 = uv + vec2(disp * u_progress * 0.3, 0.0);
vec2 d1 = uv - vec2(disp * (1.0 - u_progress) * 0.3, 0.0);
gl_FragColor = mix(texture2D(tex0, d0), texture2D(tex1, d1), u_progress);
故障——按时间偏移行,并拆分 RGB 通道(色差):
float row = floor(uv.y * 20.0);
float shift = (hash(vec2(row, floor(u_time * 12.0))) - 0.5) * 0.1 * u_glitch;
vec2 g = uv + vec2(shift, 0.0);
vec3 c;
c.r = texture2D(tex, g + vec2(0.005, 0.0)).r; // channel offset
c.g = texture2D(tex, g).g;
c.b = texture2D(tex, g - vec2(0.005, 0.0)).b;
gl_FragColor = vec4(c, 1.0);
Three.js ShaderMaterial 接线
import * as THREE from 'three';
const uniforms = {
u_time: { value: 0 },
u_resolution: { value: new THREE.Vector2(innerWidth, innerHeight) },
u_mouse: { value: new THREE.Vector2(0, 0) },
};
const material = new THREE.ShaderMaterial({
uniforms,
vertexShader: `void main(){ gl_Position = vec4(position, 1.0); }`,
fragmentShader: FRAG_SRC, // your GLSL string
});
// Full-screen triangle/quad: a plane that covers clip space.
const mesh = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), material);
const scene = new THREE.Scene(); scene.add(mesh);
const camera = new THREE.Camera(); // no projection needed for clip-space quad
const renderer = new THREE.WebGLRenderer();
renderer.setSize(innerWidth, innerHeight);
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
document.body.appendChild(renderer.domElement);
const clock = new THREE.Clock();
renderer.setAnimationLoop(() => {
uniforms.u_time.value = clock.getElapsedTime();
renderer.render(scene, camera);
});
addEventListener('resize', () => {
renderer.setSize(innerWidth, innerHeight);
uniforms.u_resolution.value.set(innerWidth, innerHeight);
});
对于使用纯相机的全屏处理通道,请编写顶点着色器以直接输出 position,并跳过投影(如上所示)。对于应用于实际几何体的着色器,请通过 varying vec2 vUv; void main(){ vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0); } 从顶点着色器传递 vUv。
移动设备 / 性能
- 在不需要 highp 时,在移动设备上声明
precision mediump float;;某些效果(大坐标、深层 fbm)需要highp。 - 限制像素比:
renderer.setPixelRatio(Math.min(devicePixelRatio, 2))。对于开销较大的着色器,可先渲染到低分辨率目标,再放大。 - GLSL ES 中的循环必须使用常量边界——不能使用动态循环次数。将 fbm 倍频程数保持在 ≤ 5–6。
- 避免在高频路径中使用
if/ 分支;优先使用mix/step/smoothstep。尽量减少texture2D调用;尽可能避免依赖纹理读取。 - WebGL2 / GLSL ES 3.00 支持
texelFetch、整数运算和textureLod;声明#version 300 es,并使用in/out/fragColor。
交付与验证(独立 HTML)
打包的辅助脚本(
scripts/):scripts/seek-shot.sh anim.html 0 1.5 3会冻结?t=N测试框架,并为每个时刻截图;scripts/contact-sheet.sh sheet.png frame-*.png会将截图拼成一张图,便于一眼检查。参见scripts/README.md。
对于自包含的着色器(渐变/噪声背景、转场、生成式循环),交付物应是一个可直接在浏览器中打开的 HTML 文件——通过 importmap 从 CDN 加载 Three.js、一个全屏四边形、一个渲染循环,无需构建步骤。着色器适合用单个文件实现;一个文件就能完成任务时,不要使用打包器。
输出要求:
- 一个
.html:importmap 将three固定到 CDN;GLSL 字符串、ShaderMaterial、全屏四边形和渲染循环都放在同一个内联<script type="module">中。 - 着色器是 uniforms 的纯函数——所有效果都由
u_time(转场还包括u_progress)驱动。所有动画都通过一个可固定的 uniform 控制。 - 着色器中的随机性已由确定性的
hash(uv)提供——无需逐帧设定随机种子;只要不要在u_time之外使用墙上时钟即可。
跳转/冻结测试框架——在固定时间渲染一帧以供截图。 ?t=N 会将 u_time(以及可选的 u_progress)设为 N,渲染一帧后停止循环,生成确定性的静帧。
<script type="module">
// ... uniforms, material, full-screen quad, renderer ...
const t = new URLSearchParams(location.search).get("t");
function frame(time) {
uniforms.u_time.value = time;
uniforms.u_progress && (uniforms.u_progress.value = Math.min(time, 1)); // transitions
renderer.render(scene, camera);
}
if (t !== null) {
frame(parseFloat(t)); // one fixed frame, no loop
window.__ready = true;
} else {
const clock = new THREE.Clock();
renderer.setAnimationLoop(() => frame(clock.getElapsedTime()));
}
</script>
验证流程——渲染 → 冻结 → 截图 → 检查:在三个时刻打开页面——开始、中间、结束(?t=0、?t=<mid>、?t=<end>;转场则使用 u_progress 0/0.5/1)——分别截图,并检查还原度(是否符合需求)和瑕疵:画布全黑/空白 = 着色器编译或解析错误(查看控制台中的 GLSL 日志)、色带、NaN 溢出(pow/log 对负数运算产生的白色/杂乱像素)、转场缺少纹理(CDN/资源返回 404)。WebGL 需要 GPU 上下文;无头模式下的 Playwright/Chromium 会提供该上下文(swiftshader)。
npx playwright screenshot --wait-for-timeout=600 "file://$PWD/shader.html?t=2.0" frame-mid.png
完成前:
- 画布能正常渲染——不是全黑/空白,没有着色器编译或控制台错误,也没有 CDN 404。
?t=N能冻结可复现的帧(相同的 N → 相同的像素;u_time是唯一的时钟)。- 已对开始/中间/结束时刻(或进度 0/0.5/1)截图——符合需求,没有色带/NaN/黑屏。
- 如果嵌入 SPA,应正确释放资源且无泄漏(
material.dispose()、geometry.dispose()、renderer.dispose(),并停止循环)。 - 遵循
prefers-reduced-motion——冻结u_time,或在动画仅用于装饰时降低动画速度。
快速参考
| 目标 | 基元 |
|---|---|
| 动态渐变 | 余弦调色板 a + b*cos(...) |
| 有机纹理 | fbm(uv * scale + time) |
| 清晰形状 | SDF + smoothstep(fwidth(d), -fwidth(d), d) |
| 流体/大理石纹 | 域扭曲:噪声输入噪声 |
| 显现转场 | 将阈值噪声与 u_progress 比较 |
| 故障效果 | 行哈希偏移 + RGB 通道偏移 |
| 抗锯齿边缘 | 使用 fwidth(d) 获取屏幕空间宽度 |
参考文件
references/glsl-cookbook.md— 完整的值噪声和单纯形噪声 + fbm 实现、IQ 余弦调色板配方、包含布尔运算和圆角的完整 SDF 形状库、域扭曲、作为完整着色器实现的三种图像转场(溶解/位移/故障效果)、Three.js uniform/纹理接线、GLSL ES 3.00 迁移,以及移动端精度注意事项。