原理:场景先渲进"数据缓冲",再用全屏后处理把每个像素"画"成圆珠笔/铅笔笔触。

github:https://github.com/zsj23333/doodleBreak 在线体验:纸上打砖块 · Doodle Breakout
一、两步管线
pass 1 数据缓冲 pass 2 全屏后处理
场景 ──► RGBA半浮点(r=明暗 g=墨色ID ba=法线xy) ──► 每像素画成: 轮廓+阴影线+纸纹
+ Float深度纹理
- 数据缓冲:自定义 ShaderMaterial,只存"作画所需信息",无任何风格化。
- 后处理:全屏 quad(独立场景 + 正交相机),逐像素决定画成纸色、墨线还是轮廓。
- 好处:风格化与场景内容解耦,任意模型/相机运动都有一致手绘感。
二、数据缓冲 pass
- 片元输出 vec4(shade, inkId, n.x, n.y),shade = dot(n, L)*0.5+0.5 半球光;fill 模式输出 shade=-1(后处理整片涂纯色)。
- 深度纹理必须 FloatType;RT 用 HalfFloatType + NearestFilter、无 mipmap。
- 同色物体共享材质实例,减少 draw call。
三、后处理五层笔触
1. 手抖采样:值噪声偏移采样 UV,整幅笔触同步微抖。
2. 轮廓 = 逆深度 Laplacian(关键):
任意平面 ax+by+cz=d 的 1/z 在屏幕空间是仿射函数 → 二阶差分恒 0。平面内部无响应,只有穿过真实深度不连续(轮廓)时非零。除以 1/d 归一化,任意距离轮廓强度一致。
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)); // 尺度不变
法线突变也补一条轮廓(贴在一起的物体间);轮廓颜色取最近深度物体的墨色。
3. 阴影线 = 表面锚定 + LOD + 三方向排线:
- 从深度重建世界坐标和法线 → 条纹"钉"在表面上,随物体转动不漂移。
- 法线最朝外的轴决定投影面,避免条纹拉伸。
- 间距按 sp = 0.5 * exp2(floor(log2(0.016*d/0.5))) 做 2 的幂分级,远处自动变粗防摩尔纹。
- 三方向(45°/-45°/15°)叠加,密度随 shade 递减:smoothstep(0.64→0.5)、(0.42→0.32)、(0.24→0.14),最暗处涂黑。
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);
}
4. 纸面:双层值噪声(低频大块+高频细颗粒)做纸纹;横线 = 屏幕 y 取模,间距 画布高/13.5,淡蓝 (0.58,0.70,0.92)。红边距可选(不需要就删)。
5. 合成:paper → mix(墨色, hatch*0.72) → mix(轮廓墨色*0.92, edge*ew),ew = 0.75+0.35*vnoise 让轮廓线毛糙一点。
渲染器:render(scene) 到 RT → render(postScene, ortho) 到屏幕。后处理 quad 必须单独场景,否则主场景被重复渲染。
四、调参速查表
| 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 | 纸色 / 横线间距 | 改纸色 / 改密度 |


