透视错觉动画 是做什么的?
风格概述
一架水平放置的相机使用超长焦镜头(从400米外约2°视角)将景深场景压扁成一张平面绘画。然后进行推拉变焦:镜头焦距变宽,同时相机向前移动,使主体大小保持不变,但背后的空间被拉伸打开。相机还会旋转到转角视角。每个转角视角初始是扁平的,然后展开为3D。在视角扁平时,物体在深度方向悄悄滑动,无人察觉,因此当深度恢复时,一排物体变成了斜线。整个场景以干净的黑白线稿风格绘制,使用平涂二分着色、排线阴影和手绘天空,并完美循环。
快速开始
mkdir -p ~/Movies/edits/<date>-<name>/stills && cd $_
printf '{"type":"module"}' > package.json && npm i [email protected] [email protected]
cp -r ~/simcraft/skills/video/perspective-illusion/templates/* .
cp scramble_night.html scene.html # 或选择最接近的模板(见下文),然后编辑
node render.mjs scene.html x --stills=0.3,1.5,3,6,8.5 # 每个镜头的静帧,保存至 stills/
node render_fast.mjs scene.html preview.mp4 --preview # 整段循环,半尺寸,约1.5分钟
node render_fast.mjs scene.html final.mp4 # 全质量,10秒约需30分钟
python3 ~/simcraft/skills/video/vox-explainer/scripts/motion_check.py final.mp4
python3 ~/simcraft/skills/video/perspective-illusion/scripts/blink_map.py ffmpeg final.mp4 blink.png
在静帧和预览上修复问题。仅在预览通过批准后渲染全质量。若 ffmpeg 不在 PATH 中,请设置 $FFMPEG。
模板
从最接近新场景的模板开始。保留其相机支架、时间线、后期通道和渲染钩子,仅替换场景部分(即 // ---------- kit 和 // ---------- cameras 之间的所有内容)。
| 模板 | 场景 | 渲染器 |
|---|---|---|
street.html, street_plus.html | 白天的街道,包含电线杆、圆锥筒、人行横道和红色天空。street_plus 增加了细节和转角视角。 | render_smooth.mjs |
market.html, market_real.html | 超市过道,绘制风格和真实风格。 | render_smooth.mjs (market.html: render.mjs) |
scramble_night.html | 夜晚大雨中的大型路口,有行走的人群、潮湿反射和广告屏幕。它包含了所有加速优化。 | render_fast.mjs |
对于新场景,scramble_night.html 是引擎的最佳基础,因为它是唯一包含快速渲染器钩子的模板。删除其夜间附加功能(见 reference.md)可获得白天效果。
第1步:构建一个能欺骗眼睛的场景
任何地点只要具备以下要素即可:
- 一条长轴。 沿 -z 轴布置场景(街道、过道、站台、走廊),相机沿着它看。沿此轴的深度是长焦隐藏的部分。
- 不同深度上的重复物体。 电线杆、灯柱、货架、柱子、标志牌、树木、人。在长焦镜头下它们会叠成一个平面。排列整齐的电线杆融合为一根,远处立面上的标志堆叠成一堵墙。
- 可交换物体。 3到6个相似物体(圆锥筒、人、垃圾桶、椅子、盒子),其排列在“一行”和“对角线”之间切换。在扁平视角中它们也可以被绘制得更大(
cs),以便在压扁时比例正确。 - 以米为单位的真实世界比例。 镜头和对焦平面只有在真实比例下才有意义:一个0.75米的圆锥筒,1.7米的人,3.2米的一层楼。
- 在转角视角可见的细节。 每个转角视角都会揭示建筑侧面、物体背面和地面。填充它们:侧窗、排水管、电缆、路缘石、汽车下方的接触阴影。扁平视角隐藏粗糙,转角视角则会暴露。
- 使每个细节成为一个表面的一部分(纹理、堆叠的套管),或者保持不同表面之间至少0.4米距离。从400米外看,相距几厘米的表面会闪烁。
第2步:编排相机运动
相机从不倾斜。它保持水平,使垂直线条垂直,如同建筑师的图纸。每个镜头都是一个普通对象:
{ x, y, zf, W, fov, H, el, yaw, lay, cs }
| 键 | 含义 |
|---|---|
x, y | 相机的侧向位置和高度(米) |
zf, W | 对焦平面,以及该平面上的画幅宽度(米)。在改变 fov 时保持 W 不变就是推拉变焦。 |
fov | 水平镜头角度。约2°是扁平,25°至90°是景深。 |
H | 镜头移位:地平线的位置(-1底部,0中部,1顶部)。 |
el | 通过抬高相机并移动镜头来实现俯视,绝不倾斜。 |
yaw | 围绕焦点旋转以获得转角视角。 |
lay, cs | 可交换物体的布局(0行,1对角线)及其扁平视角的缩放。 |
有效的镜头词汇表:
| 镜头 | 设置 |
|---|---|
| 扁平 | fov 约2,H 0。整个场景读作一张立面图。 |
| 虫眼视角 | 膝盖高度,fov 90,地平线压低(H 约-0.7)。 |
| 高视角 | 相机在中等焦距下置于高处,地平线在画面之上。 |
| 转角视角 | yaw ±18至35°,el 9至50°。它成对出现:先是长焦版本(fov 4),然后同一个视野的广角版本(fov 25至45)。 |
| 对角线低视角 | 中等焦距,展示可交换物体的深度。 |
循环结构。 扁平,然后一个景深视图,接着一个转角视角(扁平到3D),回到扁平(在此处交换物体),一个高对角线,另一个转角视角,一个低视角,再回到扁平。循环结束于它开始时的镜头。
时间。 所有参数通过一个单调三次(Fritsch-Carlson)曲线在关键帧时间过渡,因此速度从不跳跃。在 log 空间中插值 W 和 fov。添加微小的“漂移”镜头(W × 0.96)使停顿有呼吸感而非静止。10秒感觉流畅,而7秒感觉仓促。要改变时长,缩放关键帧时间而不是重写它们。
在扁平时作弊。 只在扁平停顿期间改变 lay。如果在物体移动时相机也在深度方向上移动,技巧就会暴露。
第一个转角视角应该朝主体对象(一辆车、一个标志)没有覆盖的一侧转,这样揭示时会展示新内容。
第3步:外观
页面渲染四个通道:颜色、法线带深度、纯色(无光照)和仅阳光的阴影遮罩。最终着色器将它们组合起来。
- 墨线 在深度(二阶差分)、法线或纯色变化处生成。近处加重线条。地面标记使用细灰色线条。在单个像素覆盖数米或远处纹理变成噪点的地方,颜色线条渐淡。
- 二分着色。 亮面和暗面,无渐变。将遮罩光照设置为
2*PI,因为 three.js 将 Lambert 除以 pi,否则一切都会变成暗面。 - 仅投射阴影使用排线,排线锚定在世界空间(而非屏幕空间),并通过
fwidth淡化。 - 根据视线方向绘制天空,而非屏幕位置,以便云朵在相机移动时保持不变。保持云朵边缘柔和。
- 调色板。 6到10种纯色加上一种强调色(红色天空、黄色标志)。要匹配参考图,使用 k-means 从其帧中提取颜色。
- 灯光。 一个低角度硬质太阳,使主要主体保持照明并在地面上投下长阴影。来自高侧的太阳会使整个场景陷入一个大阴影中。
关于夜间变体(月光、亮窗、湿润反射、辉光、雨、调色)、人群行走或广告屏幕和标志,见 reference.md。
要从 scramble_night.html 获得白天的外观:
- 将月亮换成温暖的太阳,提高环境光。
- 删除点光源循环。
- 从后期着色器中移除湿地面块、雾线和夜间调色。
- 移除雨,并将窗户的
emissiveIntensity设为0。 - 绘制一个白天的
skyAt。
第4步:细节调整(决定作品优劣)
初版总是看起来很空旷。优秀的版本来自精心设计的调整轮次,每轮都在静帧上检查后再进行下一轮。
- 形体。 真实比例的主要体积、长轴、可交换物体和相机时间线。检查扁平镜头是否真的读起来扁平,以及每个移动是否都有揭示。
- 结构。 真实场所应有的一切,基于现实世界的逻辑建造。对街道而言,包括路缘石、排水沟、人孔、带支架和箱体的电线杆、双面标志、围栏、垃圾桶、自动售货机、自行车架、架空电线、窗框、窗台和空调外机。查看真实场所的照片,在建造前列出所有元素。
- 磨损和故事。 使其“有人居住”的小物件:贴纸、胶带、代码绘制的文字海报、水渍、靠着的自行车、晾晒的衣物、猫、咖啡杯。在相机停留的每个表面上至少添加3个。
- 转角视角审查。 对每个转角视角,渲染宽幅静帧并填充任何看起来空旷的地方。通常是建筑侧面、标志背面、屋顶(水箱、栏杆、天线)和物体之间的地面。
- 光和影。 确保每个物体都投射出可读的阴影,在停放物下方添加接触阴影,并确保每个主要形体的阴影侧都进入了二分着色步骤。仅在一个视角中出现的阴影是对相机移动的奖励。
- 生活(可选)。 匀速行走的人、火车或汽车、屏幕、鸟、旗帜。所有东西循环整数倍。
- 润色。 调色板和谐、线宽、天空、暗角和调色。然后对最终版本进行闪烁检查。
密度规则:从扁平的远焦镜头看,画面应该从边缘到边缘都感觉充实,像一张密集的插图,没有大片的空白墙壁或地面区域,除非是刻意让眼睛休息的地方。
质量标准
在全渲染之前,在每个关键时间(以及移动的中间点)检查静帧,对照以下列表。修复任何不通过项,然后再次检查。
- 扁平镜头读起来像一张平面画。没有任何东西透露深度。
- 每个移动都以一个揭示结束:隐藏的对角线、侧街、原本相隔很远的物体。
- 转角视角从扁平开始然后打开,第一个转角远离主体对象。
- 线条清晰均匀,无双线、无辉光面上的墨水、无远处纹理上的噪点。
- 二分着色在每个主要形体上清晰可见,排线仅出现在投射阴影中。
- 没有任何物体读起来像裸露的几何体。每个盒子都有装饰、接缝、底座或标签。
- 文字可读且拼写正确,使用真实字体,品牌为虚构。
- 天空与场景的光照方向和颜色匹配。
- 调色板有限且和谐,有一个明确的强调色。
- 循环接缝不可见(第0帧等于最后一帧),且无任何物体突然出现或消失。
- 预览无卡顿。在最终版本上,
blink_map计数低,且闪烁的部分是预设的运动(雨、屏幕)。
在审查静帧时,像插画师一样看它们:说出每帧最弱的三个地方,修正它们,然后重复。不要满足于“它能用”。
第5步:渲染和检查
render_fast.mjs在 GPU 上渲染每一帧,作为抖动子渲染的平均值:4次用于抗锯齿,在快速移动时最多32次用于无间隙运动模糊,大小由window.probe点移动的距离决定。使用--preview获得半尺寸30fps预览。--from、--to和--fps也可用。render_smooth.mjs对没有renderMany的页面提供相同质量,但在 ffmpeg 中平均,因此慢数倍。- 加载时按材质合并静态网格(
BufferGeometryUtils.mergeGeometries)。一个场景从7,654次绘制调用减少到530,大约快了7倍。保持任何移动的物体不合并。 - 检查最终版本,使用
motion_check.py(无冻结)和blink_map.py,它统计在几乎静止的帧中闪烁的像素数。在改动前后对比计数。预览默认会有闪烁,因此仅对最终版本判断闪烁。
页面接口契约
任何场景页面只要提供以下钩子,即可与渲染器配合工作:
| 钩子 | 功能 |
|---|---|
#frame | 被截屏的画布。 |
window.READY | 在资源和字体加载完成后设置。 |
window.render(ms) | 在时间 ms 绘制帧。 |
window.probe(ms) | 返回几个世界点在屏幕上的位置,用于确定运动模糊的大小。 |
window.setJitter(x, y) | 镜头亚像素偏移量(像素),通过 projectionMatrix.elements[8] 和 [9] 设置。 |
window.renderMany([[ms, jx, jy], ...]) | 在一次调用中平均多次渲染(render_fast 需要)。 |
?scale=.5 | 以半尺寸渲染。 |
所有动画必须是 ms 的纯函数,并按循环长度周期性变化。对于步进、爬行和雨,使用每循环整数倍周期。
闪烁和故障
| 原因 | 修复 |
|---|---|
| 屏幕空间中的任何内容(抖动、排线、噪点、雾效按相机距离) | 将其锚定到世界空间或视线方向。 |
| 相距几厘米的表面 | 合并成一个表面,或保持至少0.4米距离。 |
| 重叠的共面平面(道路标记、地板块) | 将标记绘制到一个平面上,并使各块边缘对边缘相接。 |
| 亚像素细节(栏杆、网格、电缆、雨) | 每帧抖动抗锯齿,并按每像素米数淡化密集线条。 |
| 每帧一次渲染 | 绝对不要在最终渲染中这样做。 |
| 云朵或反射上的硬边 | 柔化它们,并在屏幕边缘淡化效果。 |
| 循环包裹或突然出现的运动物体 | 在循环中最快相机移动时包裹它们,通过 window.probe 找到。 |
陷阱
- 单行代码中的
//注释会吞掉该行剩余部分(如scene.add、灯光)。请使用/* */。 - 添加到场景根节点的辅助工具(如
plane())忽略当前组,可能落到其他物体上方。应在组内构建。 - 将自发光面(屏幕、标志面板)排除在纯色和阴影通道之外,否则每个字形都会被上墨并变暗。
ffmpeg -stream_loop ... -c copy制作的循环在某些播放器中会提前停止。请重新编码。- 对于 X(推特),32:9 并排展示太宽无法上传。请垂直堆叠。
- 仅使用原创角色和虚构品牌。模仿参考技法的同时,绝不抄袭其作品。