油畫風格影片
你用程式碼作畫,就像畫家用畫筆作畫一樣:先決定畫面(構圖、明暗、色調),然後從後往前鋪上真實的筆觸,檢視結果,再調整。 引擎將這些筆觸轉換為幀;沒有任何東西是渲染效果濾鏡 — 每一個標記都是你放置的筆觸。
這個方法取自四場繪畫示範(過程、松樹、油畫樹林、日落雲彩)以及對 13 幅公共領域大師風景畫的測量。提煉出的心得在 references/ 中;可運作的程式碼在 engine/ 和 examples/ 中。
設定(每個專案一次)
cp -R /path/to/painterly-video/engine ./painter-engine
cd painter-engine
npm ci
node bin/paint.mjs still ../path/to/scene.js --t 8 # → PNG 位於場景旁(stills/<scene>/)
請將 /path/to/painterly-video 替換為包含此 SKILL.md 的資料夾(例如 ~/.claude/skills/painterly-video)。需要 Node ≥ 22.12、具有 WebGPU 的 Google Chrome(或設定 CHROME_PATH)、ffmpeg 和 ffprobe。參考研究還需要 Python 與 tools/requirements.txt。無需建置步驟或渲染 API 金鑰;渲染在本機硬體上執行。開始一個新場景時,請複製 examples/ 中最接近的檔案(sunset-river.js 天空/雲/水 · lone-pine.js 垂直構圖、單一主體 · lake-path.js 樹木/縱深/路徑 · sunset-study.js 帶有背光邊緣的暗雲塊戲劇性天空(示範結構研究) · mountain-hamlet.js 山、小屋、岩石、行走人物、煙霧 · brush-chart.js 筆刷與混色)。
工作流程 — 請勿跳過檢視步驟
- 簡報。 主題、情緒、時間、格式(16:9、9:16、1:1 — 任意
--size)、時長,以及時間分配方式:逐步揭示、活畫作、或兩者兼具(預設:約 60% 揭示、40% 活畫作)。 - 若有參考,則研究參考(
tools/study.py,參見references/composition.md)。它會回報明暗塊、色調、彩度、色板與焦點 — 請模仿其結構,而非畫面。 - 先構圖,再上色。 將構圖以註解寫在場景檔案的頂端:地平線高度(絕不能在中間)、焦點(放在三分線上,最強明暗對比與最高彩度處)、引導線、框架元素,以及明暗計畫 — 每個大形狀一個明暗值(0–1),分成三個不相等的大塊。
references/composition.md。 - 從顏料取色,置於明暗計畫上。
tone(mix({ ultramarine: 1, transYellowOxide: 1 }), 0.42)。除焦點強調外,其餘地方保持低彩度(大師畫作:OKLab 平均彩度 0.02–0.06,高彩度區域約低於 8%)。 - 從後往前繪製,使用主題繪製器(
paintSky、paintCumulus、paintHills、paintMountains、paintWater、paintTreeline、paintPine、paintFir、paintTree、paintGrass、paintRocks、paintHouse、paintGlow、paintMoon、walker、boat、smoke…)以及原始layer.scatter/stroke/path處理其他部分。每個階段一個p.phase('name'),圖層由遠到近建立。references/api.md。 - 檢視、批評、調整 — 至少兩輪。 渲染一張靜態圖,打開 PNG,像判斷油畫一樣判斷它:瞇眼看(三個明暗塊是否清晰?)、焦點是否在計畫位置、是否有任何東西等距/等大/等飽和、是否有筆觸看起來機械化?在程式碼中修正原因。對自己的靜態圖執行
study.py並與計畫比較。這個循環就是這項技能的核心;第一次渲染永遠不會正確。 - 時間。
p.timelapse()用於揭示,然後是動態:sway、drift、shimmer、p.wind、p.camera(按圖層深度的視差)、p.dynamic用於移動中的物體。references/animation.md。 - 接觸表,然後渲染。
still --n 12 --sheet→ 檢查時間軸的每個階段;然後render→qa。references/pipeline.md。 - 如實報告:哪些是自動檢查的、哪些是你親自檢視的、哪些你沒有做。
場景骨架
import { Painting, mix, tone, mixOk, skyColor, paintSky, ridge, paintHills, paintGrass, key, ease } from 'painter';
export const meta = { duration: 16, fps: 30 };
export default function ({ aspect, seed }) { // aspect = 渲染的寬度 / 高度
const p = new Painting({ aspect, seed, ground: '#c9b89a' }); // 有色畫布,絕不能用白色
p.duration = 16;
const A = aspect; // 場景單位:x 0…A,y 0(頂部)… 1(底部)
const skyTop = tone(mix({ ultramarine: 2, titaniumWhite: 2 }), 0.72), skyLow = tone(mix({ cadmiumOrange: 1, titaniumWhite: 3 }), 0.88);
const hillCol = tone(mix({ ultramarine: 1, burntUmber: 1 }), 0.3);
const hill = ridge({ y: 0.68, amp: 0.05, freq: 2, seed });
p.phase('sky');
paintSky(p.layer('sky', { depth: 0 }), { color: skyColor({ top: 0, bottom: 0.7, stops: [[0, skyTop], [1, skyLow]] }), bottom: 0.72 });
p.phase('land');
paintHills(p.layer('hill', { depth: 0.6 }), { top: hill, color: (x, y, k) => hillCol, size: 0.03 });
paintGrass(p.layer('grass', { depth: 1 }), { bounds: [0, 0.8, A, 1.05], dark: hillCol, light: tone(hillCol, 0.5), blade: 0.05 });
p.timelapse({ start: 0.3, end: 10 }); // 畫面於 10 秒完成
p.camera = (t) => ({ zoom: key(t, [[10, 1], [16, 1.07]], ease.inOut) });
p.wind = (t) => ({ strength: key(t, [[0, 0], [10, 0.15], [12, 1]]) });
return p;
}
使其看起來像油畫的規則(簡短列表)
- 明暗做主要工作,色彩得功勞。 計畫三個不等的明暗塊;對每張靜態圖瞇眼測試。
- 底色調,絕不用白色。 選擇底色來和諧畫面(暖赭色襯托綠色,淺奶油色襯托日落)。
- 先遠後近。 遠 = 較亮、較冷、較灰、較柔、較薄顏料、較小筆觸。近 = 最暗的暗部、最高對比與彩度、最厚顏料、最大筆觸。使用
atmosphere(color, d, air)。 - 暗部先畫且薄,亮部後畫且厚。 陰影與遠處使用
thick0.1–0.3,高光使用 0.8–1;高光要小且少。 - 亮襯暗,暗襯亮。 每個色塊都用與下方相差一個小明暗值(±0.05)的筆觸打破;
p.valueAt(x, y)/p.sample(x, y)可以告訴你下面的值。 - 筆觸順應形體:環繞雲塊、沿著山坡、水平於水面、向上於草葉、向下於樹幹。
- 混合綠色並使其沉穩(
earth(mix({ ultramarine: 1, cadmiumYellow: 1 }), 0.3));純彩度只留給一個重點。 - 讓一切變化:大小、間距、高度、間隔。相等 = 機械化。奇數群組、重疊形狀、一個主導。
- 消失與清晰邊緣:形體轉向或後退處柔化(
soft0.5+),焦點處清晰。 - 時間的純函數。 場景由
seed決定;每一幀只取決於t。絕不使用Math.random()或真實時鐘時間 — 使用layer.rand/rng(seed)。
常出錯的地方(從建置過程中學到)
- 白色吞噬混色。 光譜混合器按亮度加權顏料;1:9 的顏色:白色混色幾乎是白色。先用少量或不用白色混合色相,再用
tone(c, value)放置。用chroma(c, 0.5–0.8)削減刺眼的結果。 - 雲像球加棍 → 扁平基底上的許多小葉狀(
cumulusShape葉數 40–60),僅朝向太陽一側亮色,圓潤柔和的點畫,適度的thick。 - 松樹像棕櫚樹 → 針葉是圓形簇(短扇形筆觸爆發)位於枝條上,其間有天空空隙,下方暗,上方亮;枝條低垂且高舉。
- 樹木有傘輻狀枝條/黑色樹幹 → 枝條末端必須在樹冠內;樹幹暗部明暗值約 0.22,非黑色,並隨距離增加空氣感(
atmosphere)。 - 整個區域使用飽和顏色(橙色地面、綠松色天空) → 使其沉穩;將純色作為少量小筆觸散佈。
- 路徑看起來像河流 → 暖淺灰色,筆觸沿行進方向,冷紫色僅用於陰影。
- 水由短劃組成看起來像數位效果 → 水是長水平拖筆,彼此融合,僅在靠近觀者處有少許漣漪(
paintWater的作用);不要手動添加一排排劃線。 - 海洋看起來像玻璃鏡面 →
paintWater({ chop: 1 })用於波背與白浪;湖泊與河流保持chop: 0。 - 燈/窗光看起來像平板一塊 → 在暗色環境上用
paintGlow,然後加上小而熱的核心;一筆長柔筆觸不是發光。 - 光線看起來像白色尖刺 → 少、寬、非常柔、低不透明度的光束(
paintRays預設值)。 - 畫布紋理在淺薄顏料上變成網格 → 保持
look.weave≤ 0.2。 - 山看起來像鱗片牆或斑馬條紋 → 用
peaks()建立天際線,保持遠距(薄顏料、size≈ 0.02、窄明暗範圍),讓paintMountains將其分成少數幾個大的受光與陰影平面。 - 平坦帶狀不是雲層/山丘/樹線 → 變化頂緣、內部數值並留下空隙。
- 場景因模糊的瀏覽器錯誤而失敗 → CLI 先進行語法檢查;執行時錯誤會印出行號。區域變數遮蔽顏色名稱(
leaf、sky、hill)通常是c.map is not a function的原因。 - 讀取過時的靜態圖:打開 PNG 前務必檢查命令輸出是否有錯誤。
- WebGPU 與 WebCodecs 僅存在於安全來源 — CLI 使用
http://127.0.0.1;絕不要透過file://開啟場景。
參考檔案
references/painting-lessons.md— 每場示範的教學內容及其如何映射到引擎呼叫。references/composition.md— 場景構圖、明暗計畫、大師畫作的數據、study.py。references/api.md— Painting / layer / brush / colour / subject-painter 參考,撰寫新的主題繪製器。references/animation.md— 揭示節奏、活畫作動態、相機與視差、動態圖層、煮沸效果、音訊。references/pipeline.md— CLI、渲染設定、效能、品質檢查、疑難排解、交付成果。