在线试玩 → www.noahland.cn/rainy-store/ 浏览器直接打开即可(推荐 Chrome/Edge + 独显),鼠标拖动环绕视角。

雨天的深夜,街角亮着灯的便利店——这是我基于店长测试Agent产生的渲染项目修改而来:把一个 Three.js r184 的卡通风格单文件场景,重构成 PBR 写实管线,同时尝试保持:成品必须仍是单个 HTML 文件,零外部依赖,双击就能开。

从卡通到写实,差距在哪
店长测试产生的原场景(baseline)是 MeshToonMaterial 四阶卡通渐变 + 描边后处理,可爱:
| 项 | baseline | 目标 |
|---|---|---|
| 材质 | MeshToonMaterial + 顶点色 | 全 PBR(albedo/roughness/normal/ORM) |
| 阴影 | 完全没有 | 烘焙阴影贴图 |
| 后处理 | Outline + Bloom | SSAO + SSGI + SSR + SSS + Bloom |
| 纹理 | 3 张 2048 Canvas 图集 | 全部程序化生成,禁止手绘位图 |

改造前的卡通风格 baseline。图集资源全部复用,我仅修改了整条渲染管线。
几条关键技术决策
吃「静态场景」的红利。 光源位置 100% 静态,全场只有路灯闪烁强度和信号灯颜色两处动态 → 阴影贴图只烘焙一次,renderer.shadowMap.autoUpdate = false,之后每帧零成本。几何也基本静态 → SSGI 走低频更新 + 时域累积 + 重投影,不在每帧全量 raymarch。这两条是在 4060 上跑得动 SSGI 的前提。
所有画质 Pass 一律半分辨率。 SSAO / SSGI / SSR / SSS 全部 ≤ 0.5× 渲染,配套空间降噪;SSGI 另配时域累积。每帧全场景渲染次数硬约束 ≤ 2 次(GBuffer prepass + 主渲染)。
纹理零手绘。 没有额外的纯手绘材质贴图,优先级是「复用现有图集 → 公式程序化生成 → 参考公开技术实现」。Roughness/Metal/AO 由公式生成 ORM 图烘焙内联,法线贴图由高度函数 Sobel 求导生成,一切能离线烘焙的全离线。
自适应降级而不是砍画质。 沿用帧时长统计,超预算时按「关 SSGI 时域累积 → 降 renderScale → 砍雨粒子数」的顺序降;首帧黑屏时逐级降级(关 MSAA → 关 Bloom → 关 SSGI)再展示画面。
反射的分面处理。 路面和人行道分开各渲一张反射 RT,反射查询点按反射面高度修正,再配逐面几何剔除——根治了抬高面倒影错位和矮墙底面镜面错位这类经典 bug。
性能红线
| 项 | 值 |
|---|---|
| 显卡 | RTX 4060 Laptop |
| 帧率 | 30–60 FPS(低于 30 不达标) |
| 主渲染分辨率 | renderScale 0.75(可调) |
| 网络请求 | 0 |
场景本身不大——13 个 Mesh 承载整条街,Draw Call 个位数——压力全在屏幕空间管线上。真正难的不是把效果做出来,难的是在 30 FPS 的预算里把效果做出来。
为什么是单文件
这是项目最有性格的约束:禁止 CDN、禁止 fetch、所有纹理运行时 Canvas 生成或构建期内联为 data URI,最终产物 file:// 双击即开。这样就构造出了一个可以随手发给朋友双击打开的雨夜。
后续
- 代码整理后若店长同意,我会开源到 GitHub,届时补上链接
- 待办清单:SSS 改进、雨滴打在玻璃上的折射、低配模式更多档位
如果你打开 demo 觉得卡,?q=low 可以切低配。我自己手机反正是没抗住。