国风 3D 网页渲染。当用户要"水墨风 3D""中国风网页特效""Three.js 水墨""国风 H5""青绿山水 3D""非真实感渲染 NPR""古风 WebGL"时使用。提供可运行的中式渲染 shader 与实现方法,不是通用 Three.js 教程。
Scanned 9/3/2026
Install to Claude Code
npx -y skills add sanhuang520-ship-it/awesome-chinese-ai-tools --skill guofeng-threejs --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Guofeng Threejs?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/sanhuang520-ship-it-guofeng-threejs)More formats (shields.io, HTML) on the badges page.
---
name: guofeng-threejs
description: 国风 3D 网页渲染。当用户要"水墨风 3D""中国风网页特效""Three.js 水墨""国风 H5""青绿山水 3D""非真实感渲染 NPR""古风 WebGL"时使用。提供可运行的中式渲染 shader 与实现方法,不是通用 Three.js 教程。
metadata:
author: sanhuang520-ship-it
category: development
tags: development, design, animation, chinese
---
# 国风 Three.js 渲染
**定位说明**:通用 Three.js 教程已经很多(英文社区有大量高质量资源)。
这个技能只做一件他们不做的事——**中式美学的实时渲染**。
## 什么时候用
中国风官网 / 文旅 H5 / 品牌活动页 / 水墨风交互 / 国风游戏原型 / 非真实感渲染(NPR)。
**不适用**:通用 3D 建模、物理仿真、写实渲染——那些去看 Three.js 官方文档更好。
## 先问清楚
1. **载体**(PC 官网 / 手机 H5 / 微信内嵌)—— 决定性能预算
2. **要哪种中式质感**(水墨 / 青绿山水 / 敦煌壁画 / 剪纸皮影)
3. **有没有模型**(有 glTF?还是用几何体?)
4. **动效需求**(自动旋转 / 跟随鼠标 / 滚动驱动)
> ⚠️ **移动端警告**:Three.js + 自定义 shader 在中低端安卓上会明显发热掉帧。
> H5 项目务必先确认目标机型,必要时降级为静态图。
## 水墨渲染的三个核心
水墨效果不是加个滤镜,是**三件事的组合**:
### 1️⃣ 墨分五色 —— 色阶量化
真实光照是连续的,水墨是**分层**的。把明暗量化成 5 阶:
```glsl
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 压深
毛笔在轮廓处停留更久,墨更浓。用视线与法线夹角模拟:
```glsl
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️⃣ 笔触扰动 —— 噪声打破机械感
纯数学量化的边界太整齐,不像手绘。**在量化前扰动光照值**:
```glsl
float n = noise(vP * 5.5 + uTime * 0.06);
lam += (n - 0.5) * 0.16; // 扰动幅度别超过 0.2
```
再叠一层细噪声当宣纸颗粒:
```glsl
col *= 0.97 + 0.03 * noise(vP * 40.0);
```
## 青绿山水:和工笔只差一个参数
这个方案**没有引入任何新技术**——描边用的是和 `gongbi-demo.html` 完全同一套反向外壳,
连代码都一样。唯一的差别是 `uLine` 传进去的是**暖金**而不是墨黑。
但气质完全不同。demo 里的「墨线对照」档就是为了证明这件事:
同样的山、同样的青绿配色,只换线色,「金碧山水」的贵气立刻消失。
**「金碧山水」这个名字,就是从那条金线来的。**
### 石青石绿要按海拔映射,不是按明暗
这是青绿山水的固定语法:**山脚石绿、山顶石青**。不是随机分色,也不是用明暗分:
```glsl
// 顶点着色器里把世界坐标传出来
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` 大部分时间挤在中段,
青绿两色根本分不开,看着就是一片墨绿。
### 金不只在线上,也在山脊
除了描边,受光的山脊再敷一层金粉,「金碧」才立得住:
```glsl
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️⃣ 多倍频噪声,不是单层
剥落是自然破坏,天然多尺度。单层噪声一眼假,要叠:
```glsl
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️⃣ 剥落 = 露出下面的地仗
不是把颜色调暗,是**颜料层没了、露出白垩地仗**。所以是往地仗色混,不是往黑里混:
```glsl
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 相乘):
```glsl
float c = fbm(vP * 18.0);
float crack = smoothstep(0.495, 0.5, c) * smoothstep(0.515, 0.505, c);
```
### 3️⃣ 铅丹变黑:一个真实的美术史细节
敦煌壁画里人物"脸是黑的",**不是当年就那么画的**——唐代用的铅丹(红色)
氧化后变成深褐近黑。所以这个效果要满足两个条件才对:
- **只作用在红色区**(其他矿物颜料没这个问题)
- **斑块状**,不是整体压暗
```glsl
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` 那条全场为零的。
反向外壳的做法:
```js
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)`
皮影是一张平的皮子,正面背面一个色,没有受光面和背光面。所以片元着色器里
**刻意不引入光照项**:
```glsl
// 水墨: float lam = max(dot(N, uLight), 0.0); → 再量化
// 皮影: 直接就是纸的本色,颜色不随朝向变化
vec3 col = uPaper;
```
### 2️⃣ 背光透射 —— rim 往亮里走,不往暗里走
同样是 rim,方向完全相反:水墨用它压出浓墨,皮影用它表现灯光从薄边透出来。
```glsl
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”时,**先给结论,限制读取范围**:
1. 先用本文件已有的三项技法、性能要点和 Demo 路径回答。
2. 如需核对实现,只搜索 `demo.html` 中与问题直接相关的行;不要整文件输出,也不要读取 `intro-demo.html`,除非用户问开场动画。
3. 最多核对 3 类证据:Three.js 版本、shader 关键字、性能保护。每类只摘必要行号与结论。
4. 不启动浏览器、不运行 Demo,就明确写“静态源码审查,未做运行时验证”。
静态审查应控制在:**技术结论 → 移动端风险 → Demo 路径 → 未验证项**,不要因为仓库里已有源码就展开成逐行代码审计。
#### 用户给出字数上限时
字数上限是硬门槛,不是大致篇幅。先在内部压缩并计数字符,再只输出成品;不要把过程说明、长绝对路径或额外寒暄计入最终答案。中文短审查优先使用下面的紧凑结构:
```text
结论:静态思路可行,未运行。
技法:①…;②…;③…。
风险:①…;②…;③…。
路径:`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,原因写在那里) |
## 性能要点
1. **shader 里的 noise 很贵**。手机端把 `noise(vP * 40.0)` 的纸纹改成贴图采样。
2. **draw call 越少越好**。同材质的物体用 `InstancedMesh`。
3. **像素比要限制**:`renderer.setPixelRatio(Math.min(devicePixelRatio, 2))`,否则高分屏直接卡死。
4. **不用后处理就别引 EffectComposer**,能在材质里做的就在材质里做。
⚠️ 但这条不是绝对的——**"材质里做"也可能更贵**。做工笔勒线时实测:
反向外壳虽然免了后处理管线,却让 draw call 和三角面**正好翻倍**(3→6,25,248→50,496),
而且成本随物体数量线性增长;Sobel 后处理是一次全屏 pass,与场景复杂度无关。
**物体少用外壳,物体多/面数高时后处理反而便宜**,别照搬结论,按场景算一次。
(详见「工笔:勒线才是骨架」一节)
5. 移动端建议 **60fps 掉到 30fps 就该降级**——加个 FPS 监测自动降质量。
## 边界
1. **不做通用 Three.js 教学**。基础用法请看 [官方文档](https://threejs.org/docs/),这里只讲中式渲染。
2. **不保证跨设备一致**。GLSL 在不同 GPU/驱动上有精度差异,重要项目要真机测。
3. **不模仿具体画家风格**。可以做"水墨感",不做"齐白石风格"——风格模仿有争议。
4. **不确定的图形学细节要说明**。涉及特定 GPU 兼容性、WebGPU 迁移等,建议查最新文档。
## 相关
- `guochao-visual-cn` —— 国潮视觉(AI 出图用)
- `chinese-web-themes` —— 中式网页主题(2D 排版用)
- 三者关系:**出图** → **排版** → **3D 呈现**
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!