手绘涂鸦风格不求人:Three.js 着色器程序化渲染指南

Smart AI Code2026-09-27阅读约 6 分钟

读完本文,你将掌握在 Three.js 中用着色器程序化生成手绘涂鸦风格的完整方案——无需预先绘制大量手绘贴图,通过两步渲染管线即可让任意 3D 场景呈现一致的手绘感。适合正在探索非写实渲染风格的 WebGL 开发者。

核心思路:两步渲染管线

传统做法需要大量手绘贴图,而 Three.js 的实现路径是把风格化与场景内容解耦:

  1. Pass 1 — 数据缓冲:将场景渲染到一个自定义的缓冲纹理中,只存储作画所需的原始信息(明暗、墨色 ID、法线),不做任何风格化处理。
  2. Pass 2 — 全屏后处理:用一个独立的正交相机场景,逐像素决定每个位置应该画成纸色、墨线还是轮廓线。

这样做的好处是:无论场景中的模型如何运动、相机如何变换,最终呈现的手绘风格始终保持一致。

来源:cnblogs.com 项目实现说明

Pass 1:数据缓冲

数据缓冲使用自定义 ShaderMaterial,片元输出一个 RGBA 半浮点向量,各通道含义如下:

  • R:明暗值(shade),由半球光 dot(n, L) * 0.5 + 0.5 计算
  • G:墨色 ID(inkId),区分不同颜色的笔触
  • B/A:法线的 x 和 y 分量(n.x, n.y)

关键配置:

// 深度纹理必须使用 FloatType,否则逆深度轮廓检测会出现噪点
// 渲染目标使用 HalfFloatType + NearestFilter,不生成 mipmap

如果你希望整片区域涂纯色(如背景填充),可以将 shade 设为 -1,后处理阶段会识别并直接输出纯色。

来源:cnblogs.com 数据缓冲实现说明

Pass 2:全屏后处理 — 五层笔触

后处理阶段按顺序叠加五层效果:

1. 手抖采样

模拟手绘的不稳定性,用值噪声偏移采样 UV。关键点是整幅笔触需要同步微抖,而不是每个笔画独立随机,否则会失去整体感。

// 值噪声偏移 UV,整幅画面统一微抖
vec2 wobOffset = noise(uv + time) * jitterStrength;

来源:cnblogs.com 手抖采样实现;alanzucconi.com 顶点位移方案可作为替代路径

2. 轮廓检测:逆深度 Laplacian

这是整个效果最关键的算法。原理是:任意平面在屏幕空间中的 1/z 是仿射函数,二阶差分恒等于 0,因此平面内部不会产生响应。只有在深度不连续处(即物体轮廓),逆深度的二阶差分才非零。

float lap = abs(1.0/linDepth(zl) + 1.0/linDepth(zr) - 2.0*iw)
           + abs(1.0/linDepth(zu) + 1.0/linDepth(zd) - 2.0*iw);
float edge = smoothstep(0.07, 0.30, lap / (iw + 1e-7));

除以 1/d 归一化后,任意距离的轮廓强度一致,不会近处强、远处弱。法线突变处也会补一条轮廓,避免贴在一起的物体之间出现断层。

来源:cnblogs.com 轮廓检测原理说明

3. 阴影线(排线)

阴影线需要"钉"在表面上,随物体转动不漂移。实现方式是从深度重建世界坐标和法线,然后做表面锚定的条纹投影:

  • 用法线最朝外的轴决定投影面,避免条纹拉伸
  • 间距按 2 的幂分级(LOD),远处自动变粗,防止摩尔纹
  • 三个方向(45° / -45° / 15°)叠加,密度随明暗递减
float stripes(vec2 p, vec2 dir, float spacing, float width) {
  float t = dot(p, vec2(-dir.y, dir.x));
  float f = abs(fract(t / spacing) - 0.5) * spacing;
  float soft = width * 0.6;
  return 1.0 - smoothstep(width*0.5 - soft, width*0.5 + soft, f);
}

来源:cnblogs.com 阴影线实现

4. 纸面效果

纸纹使用双层值噪声叠加:低频大块模拟纸张纹理的不均匀,高频细颗粒模拟纸面微小粗糙感。横线由屏幕 y 坐标取模生成,间距为画布高度的 1/13.5,颜色为淡蓝 (0.58, 0.70, 0.92)。红色边距可按需保留或删除。

来源:cnblogs.com 纸面效果实现

5. 合成顺序

paper → mix(墨色, hatch * 0.72) → mix(轮廓墨色 * 0.92, edge * ew)

其中 ew = 0.75 + 0.35 * vnoise 让轮廓线边缘毛糙一点,更接近手绘感。

常见坑与调参速查

必须注意的配置

问题原因解决方案
轮廓出现噪点深度纹理非 FloatType改用 FloatType
轮廓与颜色错位深度与颜色采样用了不同 wob 偏移使用同一偏移量
黑屏空场景未渲染确保即使空场景也渲一帧
轮廓线过粗采样半径过大调整 o = 1.15 * sc

来源:cnblogs.com 常见坑说明

关键参数速查

参数作用调大效果
wob * 2.0 * sc * px手抖幅度更"手抖"
o = 1.15 * sc轮廓采样半径线更粗
smoothstep(0.07, 0.30, ...)轮廓灵敏度更易勾边
0.016(LOD 基准)阴影线间距线更稀
w = sp * 0.17笔宽排线更粗
0.64 / 0.42 / 0.24三方向明暗阈值更早出现更多方向线
hatch * 0.72墨色浓度更黑
grain / rule * 0.5纸纹强度 / 横线透明度纸更糙 / 线更明显
uPaper / uLineSpacing纸色 / 横线间距改纸色 / 改密度

来源:cnblogs.com 调参速查表

Three.js 版本注意事项

  • r147 及更早:默认 Linear 输出,示例中的颜色值是手调亮的
  • r152 及更新:texture2D 需改为 texture,outputEncoding 改名需同步更新
  • 换用深色系时,建议对颜色做 pow(col, 1/2.2) 或启用 sRGBEncoding

来源:cnblogs.com 版本迁移说明

验证与参考

  • 在线体验:https://zsj23333.github.io/doodleBreak/
  • GitHub 源码:https://github.com/zsj23333/doodleBreak

来源:alanzucconi.com 指出,该风格在 GoNNER、Baba is You 等游戏中已有大量成功应用,验证了程序化涂鸦风格的商业可行性。