国风 Three.js 渲染
定位说明:通用 Three.js 教程已经很多(英文社区有大量高质量资源)。 这个技能只做一件他们不做的事——中式美学的实时渲染。
什么时候用
中国风官网 / 文旅 H5 / 品牌活动页 / 水墨风交互 / 国风游戏原型 / 非真实感渲染(NPR)。
不适用:通用 3D 建模、物理仿真、写实渲染——那些去看 Three.js 官方文档更好。
先问清楚
- 载体(PC 官网 / 手机 H5 / 微信内嵌)—— 决定性能预算
- 要哪种中式质感(水墨 / 青绿山水 / 敦煌壁画 / 剪纸皮影)
- 有没有模型(有 glTF?还是用几何体?)
- 动效需求(自动旋转 / 跟随鼠标 / 滚动驱动)
⚠️ 移动端警告:Three.js + 自定义 shader 在中低端安卓上会明显发热掉帧。 H5 项目务必先确认目标机型,必要时降级为静态图。
水墨渲染的三个核心
水墨效果不是加个滤镜,是三件事的组合:
1️⃣ 墨分五色 —— 色阶量化
真实光照是连续的,水墨是分层的。把明暗量化成 5 阶:
float lam = max(dot(N, normalize(uLight)), 0.0);
float steps = 5.0;
float q = floor(clamp(lam, 0.0, 1.0) * steps) / (steps - 1.0);
vec3 col = mix(uInk, uPaper, q); // 浓墨 → 纸白
阶数太多(>8)失去水墨感,太少(<3)像色块。5 阶是甜点。
2️⃣ 边缘积墨 —— rim 压深
毛笔在轮廓处停留更久,墨更浓。用视线与法线夹角模拟:
float rim = 1.0 - max(dot(N, normalize(vV)), 0.0);
float edge = smoothstep(0.45, 0.95, rim);
col = mix(col, uInk, edge * 0.85);
3️⃣ 笔触扰动 —— 噪声打破机械感
纯数学量化的边界太整齐,不像手绘。在量化前扰动光照值:
float n = noise(vP * 5.5 + uTime * 0.06);
lam += (n - 0.5) * 0.16; // 扰动幅度别超过 0.2
再叠一层细噪声当宣纸颗粒:
col *= 0.97 + 0.03 * noise(vP * 40.0);
青绿山水:和工笔只差一个参数
这个方案没有引入任何新技术——描边用的是和 gongbi-demo.html 完全同一套反向外壳,
连代码都一样。唯一的差别是 uLine 传进去的是暖金而不是墨黑。
但气质完全不同。demo 里的「墨线对照」档就是为了证明这件事: 同样的山、同样的青绿配色,只换线色,「金碧山水」的贵气立刻消失。 「金碧山水」这个名字,就是从那条金线来的。
石青石绿要按海拔映射,不是按明暗
这是青绿山水的固定语法:山脚石绿、山顶石青。不是随机分色,也不是用明暗分:
// 顶点着色器里把世界坐标传出来
vW = (modelMatrix * vec4(position, 1.0)).xyz;
// 片元着色器:按 y 取海拔,归一化范围要贴合实际山高
float alt = clamp((vW.y + 1.45) / 3.05, 0.0, 1.0);
alt = smoothstep(0.08, 0.92, alt);
vec3 base = mix(uLow, uHigh, alt); // uLow=石绿, uHigh=石青
⚠️ 归一化范围一定要贴合实际模型高度。初版系数没对上,alt 大部分时间挤在中段,
青绿两色根本分不开,看着就是一片墨绿。
金不只在线上,也在山脊
除了描边,受光的山脊再敷一层金粉,「金碧」才立得住:
float ridge = smoothstep(0.72, 0.98, lam) * smoothstep(0.35, 0.8, alt);
col = mix(col, uLine, ridge * uGold * 0.42);
uGold = 0 时这条完全不生效,正好用来做墨线对照档。
又栽了一次的坑
暗部下限压太狠。初版 mix(base * 0.55, base, q),矿物色的饱和度全被吃掉,
整片山糊成墨绿——和敦煌那次是同一个错误。改成 0.78 才对。
青绿山水(尤其《千里江山图》)以鲜艳著称,把它画暗就丢了根本特征。
水面也一样:初版 11×6 的平面四条硬边全在画面里,像一块灰绿托盘; 放大到 46×34 让边缘出画,再把不透明度压到 0.13 —— 青绿山水的水多是 留白加一点淡色,让绢底透上来才对。
敦煌壁画:让它好看的是破坏,不是配色
前三个方案(水墨、工笔、剪纸)里噪声都是配角——用来打破机械感的微扰。 敦煌反过来:噪声是主角,幅度大一个量级。
一句话概括这个方案:敦煌壁画好看,很大程度上是因为它旧了。
把做旧关掉,剩下的只是三色平涂,一点特征都没有。demo 里的「初绘」档就是这个对照——
同一套 shader、同样的配色,只把 uAge 从 1.0 降到 0.15,敦煌感立刻消失。
1️⃣ 多倍频噪声,不是单层
剥落是自然破坏,天然多尺度。单层噪声一眼假,要叠:
float fbm(vec3 p){
float s = 0.0, a = 0.5;
for (int i = 0; i < 4; i++) { s += a * noise(p); p *= 2.03; a *= 0.5; }
return s;
}
然后按尺度分工:大块剥落 fbm(vP*3.4)、中等起甲 fbm(vP*11.0)、颗粒 noise(vP*46.0)。
2️⃣ 剥落 = 露出下面的地仗
不是把颜色调暗,是颜料层没了、露出白垩地仗。所以是往地仗色混,不是往黑里混:
float loss = smoothstep(0.52, 0.34, flake * 0.65 + mottle * 0.35);
col = mix(col, uPlaster, clamp(loss * uAge, 0.0, 0.92));
龟裂用噪声的等值线取细缝(两个反向 smoothstep 相乘):
float c = fbm(vP * 18.0);
float crack = smoothstep(0.495, 0.5, c) * smoothstep(0.515, 0.505, c);
3️⃣ 铅丹变黑:一个真实的美术史细节
敦煌壁画里人物"脸是黑的",不是当年就那么画的——唐代用的铅丹(红色) 氧化后变成深褐近黑。所以这个效果要满足两个条件才对:
- 只作用在红色区(其他矿物颜料没这个问题)
- 斑块状,不是整体压暗
float oxid = smoothstep(0.55, 0.85, fbm(vP * 5.2));
float isRed = step(region, 0.40);
col = mix(col, vec3(0.13, 0.10, 0.09), oxid * isRed * uAge * 0.75);
踩过的坑
- 色区别用纯 fbm 划分,那会得到"迷彩"。真壁画是分层分块设色的,
一栏一栏往上排。改用
vP.y方向的色带做主结构、噪声只负责把边界啃毛。 - 前景和背景用同一套 shader 会糊成一片。给背景墙单独一个
uRecede压暗并去饱和,图与底才分得开。 - 背景墙用平面就够了。先用半径 2.6 的圆柱,它最前端跑到
z=+1.4把前面的浮雕全挡住;改缓弧又因为thetaStart朝向不好控变成一块悬空小板。 壁画本来就画在平墙上,PlaneGeometry没这些坑。 - 暗部下限别压太狠。壁画是平涂,本来就没多少明暗;压到 0.62 整张图发闷, 矿物色的饱和度全看不出来了。
工笔:勒线才是骨架
工笔和水墨的差别不在"画得细",在于它是线主导的。所以除了把色阶从 5 阶提到 9 阶、 把墨换成矿物色之外,真正的难点是那条线怎么画。
描边的三条路,两条走不通
按 SKILL.md 的性能建议「能在材质里做的就在材质里做」,先试了材质内的两种做法:
| 做法 | 结果 |
|---|---|
fwidth(N) 检测法线突变画结构折边 |
❌ 光滑有机形体上基本无效。这类形体没有硬转折,length(fwidth(N)) 全场趋近 0,调阈值救不回来 |
rim(1 - dot(N,V))压出轮廓暗边 |
❌ 能压出边,但那是渐变带不是线,宽度随曲率变化,没有勒线该有的等宽感 |
反向外壳(沿法线外扩 + side: BackSide) |
✅ 真正等宽的线,且不需要后处理 |
调试这类"看不见的效果"有个省事办法:把各个分量分别输出到 r/g 通道, 一眼就能看出哪个在工作、哪个是 0。我就是这么发现
fwidth那条全场为零的。
反向外壳的做法:
const outlineMaterial = new THREE.ShaderMaterial({
uniforms: { uLine, uThickness: { value: 0.018 } },
side: THREE.BackSide, // 只渲染背面
vertexShader: `
uniform float uThickness;
void main() {
vec3 p = position + normalize(normal) * uThickness; // 沿法线外扩
gl_Position = projectionMatrix * modelViewMatrix * vec4(p, 1.0);
}`,
fragmentShader: `uniform vec3 uLine; void main(){ gl_FragColor = vec4(uLine, 1.0); }`,
});
// 每个本体挂一个外壳作为子对象,跟着一起变换
mesh.add(new THREE.Mesh(geo, outlineMaterial));
代价要说清楚
反向外壳虽然不引 EffectComposer,但它不免费:每个物体多渲染一遍, demo 里实测 draw call 3 → 6、三角面 25,248 → 50,496,正好翻倍。
所以「材质内做」和「后处理」的取舍不是一句"别引 EffectComposer"能概括的:
- 物体少(本 demo 3 个):反向外壳更划算,省掉一整套后处理管线
- 物体多 / 面数高:外壳的成本随场景线性增长,而 Sobel 后处理是一次全屏 pass, 与场景复杂度无关——这时候后处理反而更便宜
- 只有 Sobel 读深度/法线缓冲才能画内部结构线,外壳只能画外轮廓
另外两个细节
- 勒线必须明显深于最重的一阶颜色。初版把线色设得和
deep很接近, 而 rim 线恰好落在最暗处——深色画在深色上,等于没画。工笔的线本来就是墨线,直接用近黑最稳。 - 扰动要比水墨小一个量级。水墨靠噪声打破机械感,工笔靠线立骨架, 噪声一大线就毛了。这里用的是水墨的 1/4(0.04 vs 0.16)。
剪纸皮影:把水墨那套反过来
水墨方案的三步是「量化明暗 → 边缘压深 → 噪声扰动」。剪纸皮影每一步都要反着做, 这也是为什么它值得单独实现一遍——能验证这套思路不是只会「色调分离」一招。
1️⃣ 完全平面化 —— 不写 dot(N, L)
皮影是一张平的皮子,正面背面一个色,没有受光面和背光面。所以片元着色器里 刻意不引入光照项:
// 水墨: float lam = max(dot(N, uLight), 0.0); → 再量化
// 皮影: 直接就是纸的本色,颜色不随朝向变化
vec3 col = uPaper;
2️⃣ 背光透射 —— rim 往亮里走,不往暗里走
同样是 rim,方向完全相反:水墨用它压出浓墨,皮影用它表现灯光从薄边透出来。
float rim = 1.0 - max(dot(N, normalize(vV)), 0.0);
float bleed = smoothstep(0.05, 0.75, rim);
col = mix(col, uGlow, bleed * 0.95); // 水墨这里是 mix(col, uInk, ...)
阈值要放低、范围要拉宽,否则只有镂空边缘发光、外轮廓是死的。
3️⃣ 信息量在「镂空」不在「体积」
剪纸没有明暗层次可用,全靠轮廓和镂空说话,所以几何体要用 Shape + holes
而不是现成的球/环:
- 瓣尖必须尖:
pow(abs(cos(n*t/2)), 1.9)把峰压尖、谷压宽。用圆润的 正弦花瓣会像齿轮,不像刀剪的。 - 放射长条槽比圆孔像得多:
Path.absellipse(..., rotation)让长轴对准圆心。 - 多层要错开:三层同心同大小时,前面那张会把后面全挡住,「层」根本看不出来。 尺寸拉开 + 初始角度错开 + 轻微偏移才有叠透效果。
灯光与幕布
皮影的光源在幕布后面,画面是「暗幕布上一盏灯」,不是满屏发光。 灯晕范围和强度都要收着用,否则背景过曝会把剪影吞掉。
配色方案
复用中国传统色,三套起步:
| 风格 | 纸色 | 浓墨 | 中间调 |
|---|---|---|---|
| 水墨 | #F7F5F0 |
#2E2A26 |
#5A5550 |
| 青绿 | #F2F5F2 |
#14322B |
#2F6B5E |
| 朱砂 | #FBF7F4 |
#3A1512 |
#B23A2E |
关键:背景色要和纸色一致,否则物体像贴在画上而不是画在纸上。
完整可运行示例
demo.html —— 水墨(Three.js r170 + importmap,单文件无需构建):
- 实时水墨 shader(上面三个技法全在里面)
- 三套配色可切换(水墨 / 青绿 / 朱砂)
- 实测:2 draw call / 14496 三角面 / WebGL 无错误
qinglv-demo.html —— 青绿山水(2026-08-22 新增):
- 色阶量化 + 石青石绿按海拔映射 + 金色描边
- 三档可切换:千里江山 / 金碧(金更重)/ 墨线对照
- 「墨线对照」档是这个 demo 的价值所在:完全相同的几何体与青绿配色, 只把线色从暖金换成墨黑,「金碧」的贵气立刻消失。 一个参数就能验证表里那句「edge 用暖金而非墨黑」不是随口说的
dunhuang-demo.html —— 敦煌壁画(2026-08-22 新增):
- 三色限定(土红/石青/石绿)+ 多倍频噪声做剥落、龟裂、铅丹变黑
- 三档老化可切换:盛唐 / 北魏(更重)/ 初绘(几乎不破坏)
- 「初绘」那档是这个 demo 最有说服力的地方:同一套 shader,只把
uAge从 1.0 降到 0.15,画面立刻变成鲜艳平涂,一点"敦煌感"都没有
gongbi-demo.html —— 工笔(2026-08-20 新增):
- 9 阶量化 + 矿物色(石青/石绿/朱砂)+ 反向外壳墨线
- 界面上实时显示 draw call / 三角面 / fps,性能代价看得见不靠嘴说
- 实测:描边让 draw call 和三角面正好翻倍(3→6 call,25,248→50,496 面)
papercut-demo.html —— 剪纸皮影(2026-08-20 新增):
- 和水墨正好相反的一套做法,见下面「剪纸皮影」一节
- 三层镂空叠透 + 幕布灯晕,三套配色可切换(皮影 / 窗花 / 敦煌)
- 实测:headless Chrome 真 GPU 渲染无 WebGL 错误;1400×950 与 390×844 两种尺寸都完整成像(相机距离按宽高比自适应,早期版本在竖屏会裁掉一圈)
在线预览:https://sanhuang520-ship-it.github.io/awesome-chinese-ai-tools/themes/ink3d.html
只做方案审查时
用户明确说“不修改、不运行,只做技术方案或检查现成 Demo”时,先给结论,限制读取范围:
- 先用本文件已有的三项技法、性能要点和 Demo 路径回答。
- 如需核对实现,只搜索
demo.html中与问题直接相关的行;不要整文件输出,也不要读取intro-demo.html,除非用户问开场动画。 - 最多核对 3 类证据:Three.js 版本、shader 关键字、性能保护。每类只摘必要行号与结论。
- 不启动浏览器、不运行 Demo,就明确写“静态源码审查,未做运行时验证”。
静态审查应控制在:技术结论 → 移动端风险 → Demo 路径 → 未验证项,不要因为仓库里已有源码就展开成逐行代码审计。
用户给出字数上限时
字数上限是硬门槛,不是大致篇幅。先在内部压缩并计数字符,再只输出成品;不要把过程说明、长绝对路径或额外寒暄计入最终答案。中文短审查优先使用下面的紧凑结构:
结论:静态思路可行,未运行。
技法:①…;②…;③…。
风险:①…;②…;③…。
路径:`skills/guofeng-threejs/demo.html`;`skills/guofeng-threejs/intro-demo.html`。
未实测:FPS/功耗/内存、目标机型与 WebView、降级和上下文恢复。
- 用户要求 300 字内时,最终响应按 Unicode 字符计数必须
<= 300;拿不准就压到约 220–260 字留余量。 - 路径用仓库相对路径,不泄露临时目录或用户本机路径。
- 用户禁止运行时,只做
rg/ 定点读取等静态核对;不要启动浏览器、服务器、构建或 Demo。
其他中式风格(表内 4 种已全部实现)
| 风格 | 关键技法 |
|---|---|
| 青绿山水 | ✅ 已实现,见 qinglv-demo.html 与下节 |
| 敦煌壁画 | ✅ 已实现,见 dunhuang-demo.html 与下节 |
| 剪纸皮影 | ✅ 已实现,见 papercut-demo.html 与下节 |
| 工笔 | ✅ 已实现,见 gongbi-demo.html 与下节(描边没用 Sobel,原因写在那里) |
性能要点
- shader 里的 noise 很贵。手机端把
noise(vP * 40.0)的纸纹改成贴图采样。 - draw call 越少越好。同材质的物体用
InstancedMesh。 - 像素比要限制:
renderer.setPixelRatio(Math.min(devicePixelRatio, 2)),否则高分屏直接卡死。 - 不用后处理就别引 EffectComposer,能在材质里做的就在材质里做。 ⚠️ 但这条不是绝对的——"材质里做"也可能更贵。做工笔勒线时实测: 反向外壳虽然免了后处理管线,却让 draw call 和三角面正好翻倍(3→6,25,248→50,496), 而且成本随物体数量线性增长;Sobel 后处理是一次全屏 pass,与场景复杂度无关。 物体少用外壳,物体多/面数高时后处理反而便宜,别照搬结论,按场景算一次。 (详见「工笔:勒线才是骨架」一节)
- 移动端建议 60fps 掉到 30fps 就该降级——加个 FPS 监测自动降质量。
边界
- 不做通用 Three.js 教学。基础用法请看 官方文档,这里只讲中式渲染。
- 不保证跨设备一致。GLSL 在不同 GPU/驱动上有精度差异,重要项目要真机测。
- 不模仿具体画家风格。可以做"水墨感",不做"齐白石风格"——风格模仿有争议。
- 不确定的图形学细节要说明。涉及特定 GPU 兼容性、WebGPU 迁移等,建议查最新文档。
相关
guochao-visual-cn—— 国潮视觉(AI 出图用)chinese-web-themes—— 中式网页主题(2D 排版用)- 三者关系:出图 → 排版 → 3D 呈现