透視錯覺
風格說明(一段話)
一台水平放置的相機,使用極長焦鏡頭(約 2° 視角,距離 400 公尺),將深遠的場景壓縮成看似平面畫作。接著進行推拉變焦:鏡頭廣角端打開,同時相機向前移動,使主體大小不變,但背景空間逐漸展開。相機也會旋轉到角落視角,每個角落視角起初是平面的,然後再打開成 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)中畫得更大,這樣在壓扁時比例看起來會正確。 - 以公尺為單位的真實世界比例。 鏡頭和焦平面只有在真實比例下才有意義: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)曲線在關鍵時間點之間運行,因此速度永遠不會跳躍。在對數空間中內插 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進行 30 fps 的半尺寸渲染。也可使用--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 (Twitter),32:9 的並排畫面太寬而無法上傳。請改為垂直堆疊。
- 僅使用原創角色和虛構品牌。模仿參考資料的技術,而非其藝術作品。