把《枫桥夜泊》拆成四条时间轴:一首诗如何变成三维场景

月落江天:开场镜头里的枫桥、江面与压在天际线上的月亮

封面:1.6 秒处的开场。月亮已经压得很低,石拱桥和枫林只是江岸上的剪影,月光在江面拉出一条断续的银蓝色碎影。

模型:DeepSeek-flash4.1;harness:Claude Code;能力来源:一组对齐 three.js r186 的 threejs-skills。

这篇文章复盘的是:一份把《枫桥夜泊》拆成空间、时间与机制的提示词,如何落成一个可漫游的三维场景,并导出成 40 秒、60 fps 的短片。下文把输入的那份提示词称为「世界说明书」,把产出称为「场景 v1」。

引言:二十八个字,四十秒

《枫桥夜泊》只有二十八个字。它写了月、乌、霜、江、枫、渔火、姑苏城、寒山寺、钟声、客船。如果只是把这些名词做成模型,一件件摆进画面,得到的是一张会动的明信片,不是这首诗。

诗里真正难画的是动词和时间:月「落」,乌「啼」,霜「满天」,渔火「对」愁眠,钟声「到」客船。落是一个过程,到是一段延迟。把诗变成场景,关键不是把名词堆齐,而是为每个动作找到一个可以被渲染的机制。

这份世界说明书一共十一节,从色彩、建模、动态、镜头、字幕,一直写到导出规格。它不像一份「元素清单」,更像一份世界的说明书:先规定这个夜晚的光、色、尺度和方位,再规定什么东西在什么时候动。

先说结论:这首诗最适合被拆成四条时间轴——月落的轴、风吹叶落的轴、寺院显形的轴、钟声抵达的轴。四条轴对齐在同一个 40 秒里,诗就成了镜头。

一、诗里能动的,是动词

把四句诗拆开,会看到两种东西:布景和机制。

诗句 布景(名词) 机制(动词 / 时间) 主要实现
月落乌啼霜满天 月、乌、霜、天 落、啼、满天 sky.js、birds.js、mist.js
江枫渔火对愁眠 枫、渔火、客船 并置、静默、摇曳 trees.js、fishing.js、boat.js
姑苏城外寒山寺 城、寺、山 城外(距离)、显现 terrain.js、temple.js、雾
夜半钟声到客船 钟、船 到(传播延迟) temple.js、water.js、audio.js

名词决定场景的体积,动词决定动画的预算。提示词里最值钱的句子,往往不是「要有枫树」,而是「部分落叶随风飘落,偶尔落在江面」——它把一棵树从一个静态模型,变成了一个持续发生的系统。

这份提示词还有一个很清楚的取舍:它反复写「不要」。不要做成暴雪,不要科幻雷达波,不要霓虹圆环,不要平面插画代替三维场景,不要让船悬空或穿模。这些禁止项不是修辞,它们是在提前排除模型在信息不足时最容易选择的默认解。

二、先定世界基准,再谈物件

场景 v1 的所有坐标都收在一份 src/config.js 里。这不是为了代码整洁,而是为了让「江」成为整个世界的基准。

坐标系沿用仓库约定:X 轴向东,Z 轴向南,Y 轴向上,1 个世界单位约等于 1 米。河道沿 X 轴展开,z = 0 是江心;南岸是近景的枫桥、枫林、码头与客船,北岸抬升为寒山寺所在的山坡。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export const WORLD = {
waterLevel: 0.35,
landLevel: 1.35,
bankTop: 1.6,
riverHalfWidth: 34,
riverLength: 520,
templeBase: 9.2,
};

export const LAYOUT = {
bridge: { x: -42, z: 0, width: 7.2, span: 74, rise: 11.5 },
boat: { x: 14, z: 23.4, heading: -0.16 },
temple: { x: 42, z: -118, y: WORLD.templeBase },
bell: { x: 47, z: -126, y: 20.5 },
moonDir: new THREE.Vector3(-0.55, 0.08, -0.83).normalize(),
};

水面高度、陆地高度、河宽、寺院台基,是四个共享的水平面。桥、船、树、寺、渔火都从这几个常量上长出来,而不是各自记一套魔法数字。客船到寺院在 Z 方向上相差 141.4 个单位;这个距离不是随手写的,它同时决定了第 16–26 秒镜头隔着多远的江面去「看」寺院,以及雾要浓到什么程度,才能让寺院「若隐若现」。

月亮的方向也是常量:(-0.55, 0.08, -0.83),仰角大约 4.5°。它压在西北天际线附近,所以才有「落」的余地。提示词里那句「月亮位于远山与天际线附近,呈现月落状态」,最后落成一个随时间缓慢下沉的方向函数:

1
2
3
4
5
6
export function moonDirectionAt(t) {
const drop = THREE.MathUtils.clamp(t / 40, 0, 1);
const az = -0.55 - drop * 0.01;
const elevation = 0.078 - drop * 0.03;
return new THREE.Vector3(az, elevation, -1).normalize();
}

40 秒里,仰角从约 4.5° 降到约 2.7°。变化很小,小到单看一帧几乎察觉不到;但把它交给时间轴,月亮就真的在落。诗里的「落」不是一个状态,是一个斜率。

三、四句诗,四种机制

1. 月落乌啼霜满天

这一句有三个动作,分别落在三个模块里。

天空是一整块 shader。月盘、月海、边缘亮环、月晕、贴近地平线的薄云和稀疏星光都在同一个片元着色器里计算。星光用 step(0.9981, star) 筛出来——只保留约千分之二的网格,正好是「少量星光,不要过度密集」的分量。

乌鸦是低多边形剪影:身体、头、喙、尾,两片可以上下扇动的翅膀。三只乌鸦各自沿一条 CatmullRomCurve3 飞行,用 getPointAt 取位置、用切线定朝向,转弯时按曲率侧倾,翅膀按正弦节奏扇动。它们只在各自的时间窗里出现,所以画面里始终只有一两只,不会变成一群。

「霜满天」最容易被做错。提示词专门写了「不应把画面做成暴雪或大面积飘雪」。最终的方案是 520 个极细的霜粒,加上贴着水面缓慢流动的雾片;它们不是天气系统,只是一层几乎看不清的冷凝感。观众不会注意到霜粒,但会觉得空气是冷的。

这就是「意境」在工程里的样子:不是把比喻做大,而是把它做小,小到刚好能被感觉到。

2. 江枫渔火对愁眠

枫树是递归生成的。主干分出枝,枝再分叉,直到末梢;叶簇和带枫叶剪影的叶片卡片全部用 InstancedMesh。真正让树「活」起来的是一段注入到材质里的顶点风场:每个顶点按世界坐标和时间获得一个很小的位移,整片树冠因此以不同的相位轻轻摇动。

但只有整片摇动还不够。提示词还要「少量枫叶从树枝上脱落,飘落江面」。于是另有 120 片独立模拟的落叶:每片有自己的速度、旋转和相位,受重力和侧风影响;落到 waterLevel + 0.015 时停止下落,转为漂浮,漂 8–12 秒再重新生成。

这里有一个很实用的分层:大面积的叶子交给 GPU 的顶点着色器,少量需要「叙事」的叶子交给 CPU 逐片模拟。 如果 120 片叶子也走 shader,它们会整齐得像一场雨;如果整棵树都走 CPU,2K / 60 fps 的预算立刻不够。

渔火是另一层。江面上有五艘渔船,分布在远近不同的位置,每盏灯有自己的闪烁相位。它们通过 water.setLantern(index, position, color, power) 注册到水面 shader,最多 10 盏。船上的灯是暖橙色 #FFAD58,月光是银蓝色 #B7C9DE;这一冷一暖的对比,就是「对愁眠」里那个「对」字的视觉支点。

客船停在 (14, 23.4)。船体由逐站截面放样,木舱、竹篷、桅杆、收帆、缆绳和船头灯笼各自成组;船身随水流横摇、升沉、轻微偏航,舱内暖光与窗棂后的诗人剪影保持安静。提示词说人物「不需要复杂的人物动画」,这个克制是对的——诗人一动,画面就从「夜泊」变成了「夜航」。

江枫渔火:枫林、渔船灯火与石桥

图 1:9 秒处的江岸。枫树的暗红树冠压在画面上方,渔船灯火在江面拉出暖色倒影;冷的月光和暖的渔火在同一个水面 shader 里共存。

3. 姑苏城外寒山寺

寒山寺在 z = -118,客船在 z = 23.4,中间隔着 141 个单位的江面。提示词要求「建筑轮廓能够被观众辨认,但不应过于突出」「寺院与客船之间存在明确的空间距离」。场景没有靠把寺院做小来实现「远」,而是叠了三层:

  1. 距离:寺院真的在江对岸的山坡上,台基高度 9.2,从客船看过去需要仰视。
  2. 雾:FogExp2,密度 0.0029,越远越接近雾的颜色 #27313F。
  3. 景深:BokehPass 的聚焦距离随相机与注视点更新,远处的屋檐自然变软。

寺院的建筑本身共用一套屋面生成器。山门、主殿、偏殿、钟楼、五层塔的屋顶都由同一个函数生成,用 t 参数控制屋脊高度、坡面曲率和四角起翘;改变尺寸与起翘量,就能得到不同等级的建筑。这样做的价值不在省代码,而在风格一致:所有屋檐的弧线来自同一个曲面族,远看才不会像拼贴。

雾中寒山寺:对岸的寺院与钟楼

图 2:24 秒处,寒山寺从雾里显形。寺院被距离、雾和景深三层推开,轮廓可辨,但不抢客船。

4. 夜半钟声到客船

这是全诗唯一一句明确写了「传播」的诗。前三句都是视觉,第四句突然变成听觉;而且「到」字说明钟声不是同时在所有地方响,它需要时间。

场景把这件事写成了绝对时间:

  • STRIKE_TIME = 28.6 秒,撞木在 25.6 秒开始向后拉开,28.2 秒开始荡向古钟,28.6 秒撞上。
  • 钟体在撞击后按指数衰减摆动:sin(strike * 4.1) * 0.105 * exp(-strike * 0.85)。
  • 钟体本身还有极细的震动:sin(strike * 38) * 0.012 * pulse,同时暖光按 exp(-strike * 1.7) 脉冲一次。
  • 31.1 秒和 32.4 秒,江面在客船附近出现两组同心涟漪——这是钟声「到」的时刻,距离撞钟晚了 2.5 秒和 3.8 秒。
1
2
3
4
5
6
7
8
9
10
11
12
const strike = time - STRIKE_TIME;

// 钟摆:先自由微摆,撞后叠加一段指数衰减
let bellAngle = Math.sin(time * 0.55) * 0.008;
if (strike > 0) {
bellAngle += Math.sin(strike * 4.1) * 0.105 * Math.exp(-strike * 0.85);
}
tower.bellGroup.rotation.z = bellAngle;

// 钟体被撞后的细微震动
const pulse = strike > 0 ? Math.exp(-strike * 1.7) * Math.min(1, strike * 8) : 0;
tower.bellMesh.position.x = Math.sin(strike * 38) * 0.012 * pulse;

声波的视觉化是最容易过火的地方。提示词写得很明确:「使用透明环形波纹、空气扰动等视觉方式」「不要使用科幻雷达波或夸张的霓虹圆环」。最终的实现是四道 220×220 的平面,加法混合,半径在 7 秒内从 0.06 扩到 0.76;透明度只有 0.045 - i * 0.006,波前还被噪声打散成不连续的弧。换句话说,它几乎看不见——只在寺院前的雾里留下一圈很淡的扰动。

江面的涟漪则通过 uniform 写进水面 shader:最多 6 组 (x, z, 起始时间, 强度),每组的径向斜率叠加到基础波场上。钟声抵达时,涟漪从客船附近向外扩散,和月光、渔火共用同一套法线。

钟楼与古钟:撞钟前的瞬间

图 3:34 秒处的钟楼。古钟悬在二层,撞木已经荡开;钟体的青铜色和寺院的暖光只在局部出现。

声波越过江面:四道极淡的环形波前

图 4:30.4 秒,钟声正在越过江面。波前被噪声打散,透明度极低,只够让雾和江面轻轻动一下。

声音本身由 Web Audio 实时合成:钟声是几组带轻微拍频的正弦分音,乌鸦是滤波后的噪声脉冲,水声和风声是噪声加低频振荡;混响发送量 0.36,让钟声在江面上留一条尾巴。成片音轨则用 Python 离线渲染,和画面在同一张时间表上对齐。

四、水面:四句诗叠在同一个 shader 里

江面是整篇文章里技术密度最高的部分。它用 Reflector 做真实的平面镜像——天空、寺院、枫树、灯火都会被反射——再在镜像之上叠一层自定义 GLSL。

这一层 shader 同时负责五件事:

  1. 波纹:四组不同频率、不同速度的行波叠加成斜率,再用两层低频噪声打破规则感。
  2. 反射扰动:用波斜率偏移反射纹理的 UV,让倒影随水波扭动;再取一次偏移更大的采样做混合,避免倒影太干净。
  3. 月光碎影:沿「相机 → 月亮」的方位拉出一条细长的银色路径,路径内部再用高光项和噪声切成断续的碎片。
  4. 渔火倒影:把每盏灯沿水面镜像,取「相机 → 镜像点」的线段,在江面上形成一条被噪声打断的暖色光带。
  5. 钟声涟漪与贴岸泡沫:涟漪作为径向斜率叠加;靠近岸线 |z| > 31 时压暗水面,形成一条暗色水线。

月光那一段值得单独看:

1
2
3
4
5
6
7
8
vec2 toPoint = normalize(p - cameraPosition.xz + vec2(0.0001));
vec2 moonH = normalize(uMoonDir.xz);
float angleDiff = acos(clamp(dot(toPoint, moonH), -1.0, 1.0));
float path = exp(-angleDiff * angleDiff * 170.0);

vec3 halfDir = normalize(normalize(uMoonDir) + viewDir);
float sparkle = pow(max(dot(normal, halfDir), 0.0), 58.0);
col += uMoonColor * path * (0.008 + sparkle * 0.5) * shimmer * micro;

它没有一张「月光贴图」。月光之所以在江面上,是因为相机、月亮方位和水面法线在每一帧重新算了一次。镜头一动,碎影跟着动;月亮下沉,路径跟着偏。渔火倒影同理。倒影不是画上去的,是算出来的。

这也是提示词里那句「通过 Shader 实现水面反射和扰动」真正的价值:它要求的不是某个 API,而是一种建模方式——把水面当成一个光学系统,而不是一张会动的背景。

客船与诗人:舱窗后的剪影

图 5:16 秒处的客船。船体、木舱、竹篷和缆绳都是程序化生成;舱内暖光只够照亮窗棂和诗人的轮廓。

五、40 秒是一条函数

影片时间线在 src/film.js。它只做一件事:把任意时刻 t 映射成相机位置、注视点、字幕透明度和世界状态。

相机不是用关键帧动画软件做的,而是两组关键点加 Catmull-Rom 插值:

1
2
3
4
5
6
7
const KEYS = [
{ t: 0.0, position: V(106, 68, 138), target: V(-8, 26, -42) },
{ t: 4.0, position: V(78, 45, 110), target: V(-9, 20, -34) },
{ t: 7.0, position: V(44, 22, 78), target: V(-16, 12, -18) },
// ……
{ t: 34.6, position: V(46, 17.0, -64), target: V(54, 17, -126) },
];

每个关键点同时给出位置和注视点,两个都做 Catmull-Rom 插值。这样相机既经过每个机位,又保持速度连续,不会在关键点之间突然拐弯。35 秒之后换用另一组 FINALE_KEYS:镜头回到客船,再拉远到全景。

35 秒处有一次刻意的黑场。钟楼在江对岸,客船在南岸;如果让相机直接横跨 141 个单位的江面,观众会看到一段没有叙事的长距离平移。黑场遮住这次切镜,比硬切更自然,也比强行飞过去更诚实。

时间 镜头 字幕 主要机制
0–7 s 高空俯视,月落江天 月落乌啼霜满天 月亮下沉、乌鸦飞行、霜粒与雾
7–16 s 下降掠过枫林与渔火,靠近客船 江枫渔火对愁眠 风场、落叶、渔火倒影、船体横摇
16–26 s 从客船望向对岸,寺院显形 姑苏城外寒山寺 距离、雾、景深、屋面生成器
26–35 s 推进钟楼,撞钟,声波越江 夜半钟声到客船 撞木、钟摆、声波环、江面涟漪
35–40 s 黑场切回客船,拉远,全诗淡出 全诗 第二组相机曲线、字幕淡出

字幕不单独做动画状态机,而是几个 fade(t, inStart, inEnd, outStart, outEnd) 窗口。applyOverlay(t) 每帧根据绝对时间设置透明度和位移;拖动时间轴时,字幕会直接落到正确的位置,不会「补动画」。

这套结构还有一个直接好处:可重复导出。 在 ?film=1&capture=1 下,页面关闭实时循环,由 window.__capture.renderAt(t) 以固定 1/60 秒步长推进;deterministic.js 用固定种子覆盖 Math.random,树叶、乌鸦、雾和渔火的随机相位每次导出都一致。2400 帧捕获了 349 秒,平均约 6.9 fps;中断后可以加 --resume 从已有帧继续。

如果所有状态都从 t 推导,那么播放、暂停、拖动时间轴、重导出就是同一个函数的不同调用方式。反过来,只要有一处动画依赖「上一帧的状态」,时间轴一拖就会漂移,导出也无法复现。

诗意收尾:客船、月影与完整诗篇

图 6:38 秒处的收尾。镜头回到客船,远处寒山寺隐入夜雾,诗名、作者与四句诗依次浮出。

六、三个看得见的坑

第一,阴影贴图不能在首帧之前关掉。

项目一度把 renderer.shadowMap.autoUpdate 设为 false,想省掉每帧更新阴影的开销。结果是首帧没有生成阴影贴图,所有 MeshStandardMaterial 在采样 sampler2DShadow 时失败,整批网格直接不绘制。画面不是「阴影不对」,而是「东西不见了」。现在保持 autoUpdate = true。这条教训很具体:性能优化必须在首帧就验证,不能只看后面的帧率数字。

第二,可重复导出不是默认能力。

树叶、乌鸦、雾和渔火都用了随机数。如果不固定种子,每次导出的帧序列都不一样:第 1200 帧的某片叶子可能在这里,也可能在那里。deterministic.js 在捕获模式下覆盖随机源,导出脚本从 0 秒重放,才让「同一份代码导出同一段视频」成立。

第三,2K / 60 fps 的预算决定了雾的做法。

提示词要求「避免过度使用高成本实时体积渲染」。场景里的雾因此是贴水的横向雾片、寺院前的雾团和极细的霜粒,而不是体积雾;阴影是单主方向光的 2048² PCF;叶簇、叶片、栏杆柱、芦苇、山树全部实例化;像素比上限 1.6。这些选择不显眼,但它们决定了 2400 帧能不能在 349 秒内跑完。

七、导出:2400 帧与两版配乐

成片规格是 ffprobe 实测出来的,不是估计:

项目 实测值
分辨率 / 帧率 2560 × 1440 / 60 fps
时长 / 总帧数 40.000 s / 2400
视频编码 H.264 High Profile Level 5.1,BT.709
视频码率 / 文件大小 39.2 Mbps / 约 188 MiB
音频 AAC LC 192 kbps,48 kHz 立体声
响度 -18.0 LUFS,True Peak -8.9 dBFS
捕获耗时 349 s(约 6.9 fps)

配乐有两个版本。主版本由 scripts/compose-score.py 用 numpy / scipy 程序化合成:水声、风声、鸦鸣、两次寺钟都按时间线写入,不依赖任何外部采样。另一版用 HeyGen 曲库里的古琴 / 箫曲替换音乐层,同时保留画内的水风、鸦鸣和寺钟,画面流直接复制,不做二次编码。

这里的分工也很清楚:画面负责「看见」,声音负责「到达」。 16.5 秒的远钟和 28.6 秒的撞钟不是背景音乐,它们是第四句诗的主语。

八、这份提示词为什么有效

回看这份世界说明书,真正起作用的不是篇幅,而是六种写法。

1. 先写世界基准,再写物件。
提示词第一节不是「要有哪些建筑」,而是天空、山、江、雾、月光的关系。场景因此先有了一条江和两岸的高程,再往上长桥、树、船、寺。

2. 用 hex 和数值代替形容词。
「深蓝黑 #101A2C」「月光银蓝 #B7C9DE」「渔火暖橙 #FFAD58」——这些值直接变成 COLORS 常量,也直接约束了后处理、灯光和水面 shader。形容词只能表达偏好,数值可以验收。

3. 每句诗绑定一个镜头、一个字幕窗口、一个机制。
0–7 秒对应月落与乌啼,7–16 秒对应枫与渔火,16–26 秒对应寺院显形,26–35 秒对应钟声抵达。时间轴不是后期配的,它在提示词里就已经和诗句对齐。

4. 写禁止项。
「不要做成暴雪」「不要科幻雷达波」「不要平面插画代替三维场景」「不允许悬空或穿模」。模型在信息不足时会选最省力的默认值;禁止项就是提前堵住那些默认值。

5. 给可验收的标准。
40 秒、60 fps、2560×1440 或 3840×2160、四句诗对应四个镜头、声波与钟声同步。这些标准可以在浏览器、ffprobe 和截图里逐条检查,而不是「感觉对不对」。

6. 让所有状态由绝对时间推导。
这条提示词没有直接写出「绝对时间」四个字,但它要求时间轴拖动、自动演示、重播和逐帧导出。满足这些要求的唯一稳妥方式,就是让相机、字幕、钟摆、涟漪都成为 t 的函数。

还有一条元规则:让 skill 管 API,让提示词管世界。 提示词不去复述 Three.js 的用法,而是把篇幅留给「这个夜晚应该长什么样、什么时候发生什么」。

九、边界:场景能画到哪里

场景 v1 是风格化还原,不是唐代建筑与船型的考据。石拱桥的拱圈、客船的船体、寺院的屋面,都是从真实形制里抽出来的几何特征,不是实测模型。木材、石材、瓦片、树皮、枫叶的纹理全部由 Canvas 程序化生成;它们可控、可复现,但细节有上限。

比建模更远的边界是「意境」。构建可以通过,脚本可以全绿,ffprobe 可以逐项对上,截图可以非空;但「清冷、孤寂、悠远」最终仍然要人眼看。本文用到的七张配图只做了尺寸与非空校验,没有做人眼审美判断——那一步得留给读者。

还有一层:诗的意义不在几何里。场景能提供的是一个可以走进去的舞台,它不能替代诗,也不应该替代诗。

十、复现

场景是一个独立的 Vite + Three.js 项目,位于 material/枫桥夜泊/。环境要求 Node.js ≥ 20.19,以及支持 WebGL 2 的桌面浏览器。

1
2
3
4
5
6
cd material/枫桥夜泊

npm install
npm run dev # http://localhost:5173
npm run build # 产物在 dist/,base 为 "./"
npm run preview

交互方式:

操作 效果
拖拽 / 滚轮 / 右键拖拽 旋转、缩放、平移;首次操作会暂停影片并进入自由探索
空格 播放 / 暂停
1 – 6 全景、枫桥、江枫、客船、寒山寺、江面
R 重置视角
底部时间轴 拖到任意镜头;松开后保持暂停
雾气 / 字幕 / 声音 独立开关;声音默认关闭,需点击开启
“观看影片” 进入 ?film=1 的纯净 40 秒影片页

导出流程:

1
2
3
4
5
6
7
8
npm run dev -- --port 5188 --strictPort

# 另开终端
npm run export:film -- --url http://127.0.0.1:5188/ --frames deliverables/frames
npm run encode:film -- --frames deliverables/frames \
--out deliverables/fengqiao-yebo-2k60-silent.mp4
npm run score
npm run stills:film -- --url http://127.0.0.1:5188/ --out deliverables/previews

源码与完整交付记录在 open-wuxu/unreal-factory。material/枫桥夜泊/deliverables/REPORT.md 记录了成片规格、资源许可、验收清单与两版配乐的混音参数。

结语:先找到那句诗的「机制」

「绘制」这个词有点误导。我们画的不是一张图,也不是四张配图,而是一组会随时间发生的关系:月亮下沉,树冠摇动,落叶落水,寺院从雾里显形,钟声越过江面,最后抵达客船。

二十八个字里,真正需要工程化的不是名词,而是时间。月落是 40 秒的下沉,钟声是 2.5 秒的抵达,枫叶是 120 个实例的循环。诗不在模型里,在这些时间里。

下次让模型画一首诗,先别急着列名词。先问自己:哪一句里有时间?哪个动作需要一个系统?把那个系统写进第一节。

引用与致谢

  • 提示词原文:prompt/枫桥夜泊.md,与场景源码同在 open-wuxu/unreal-factory。
  • threejs-skills:cloudai-x/threejs-skills,13 个按主题拆分的 Three.js skill,对齐 three.js r186。
  • Three.js:MIT 许可。
  • 场景源码、截图与成片:open-wuxu/unreal-factory 的 material/枫桥夜泊/。文中七张图片取自该项目的 deliverables/previews/。
  • 模型与 harness:DeepSeek-flash4.1 在 Claude Code 中运行。

感谢阅读。