读完本文,你将掌握在 Three.js 中用着色器程序化生成手绘涂鸦风格的完整方案——无需预先绘制大量手绘贴图,通过两步渲染管线即可让任意 3D 场景呈现一致的手绘感。适合正在探索非写实渲染风格的 WebGL 开发者。
核心思路:两步渲染管线
传统做法需要大量手绘贴图,而 Three.js 的实现路径是把风格化与场景内容解耦:
- Pass 1 — 数据缓冲:将场景渲染到一个自定义的缓冲纹理中,只存储作画所需的原始信息(明暗、墨色 ID、法线),不做任何风格化处理。
- 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 等游戏中已有大量成功应用,验证了程序化涂鸦风格的商业可行性。