用 Three.js 实现手绘涂鸦风格游戏

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

github:https://github.com/zsj23333/doodleBreak

在线体验:https://zsj23333.github.io/doodleBreak/

一、两步管线

复制代码
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=d1/z 在屏幕空间是仿射函数 → 二阶差分恒 0。平面内部无响应,只有穿过真实深度不连续(轮廓)时非零 。除以 1/d 归一化,任意距离轮廓强度一致。

glsl 复制代码
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),最暗处涂黑。
glsl 复制代码
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 纸色 / 横线间距 改纸色 / 改密度

五、常见坑

  1. r147 默认 Linear 输出,示例色是手调亮的;换深色系需自行 pow(col, 1/2.2) 或开 sRGBEncoding
  2. r152+ 升级注意 texture2D → textureoutputEncoding 改名。
  3. 深度纹理必须 FloatType,否则逆深度轮廓出噪点。
  4. RT 用 NearestFilter 无 mipmap;深度与颜色采样必须用同一 wob 偏移,否则轮廓错位。
  5. 空场景也要渲一帧,否则黑屏。