img2threejs 實測:同一張鍵盤照片,ChatGPT 5.6 和 Opus 5 各跑完八個 Pass

看到 hoainho/img2threejs 時,我原本以為它是把圖片丟進模型,直接吐出一份 3D mesh。實際裝起來跑過才發現,它做的是另一件事:先讓 agent 看參考圖、拆出物件結構,再用 Three.js primitive、材質和程式化幾何重建物件。

最後拿到的不是 .glb 或 .obj,而是一份 JSON 規格與可進版控的 TypeScript。這個方向很適合網站上的互動物件、遊戲道具原型和需要繼續改程式的 WebGL 場景,但它不是一鍵式圖片轉 3D。

這次我拿日常使用的 FILCO NINJA 來測試。我想實際觀察 img2threejs 如何處理大量重複按鍵、不同寬度的鍵帽、功能鍵分區與線材,因此讓它一路跑完八個 pass——而且跑了兩輪:先用 ChatGPT 5.6,再用完全相同的條件換 Opus 5 跑一次。

本次用來測試 img2threejs 的 FILCO NINJA 鍵盤參考圖

NOTE — 八個 pass 跑完,不等於照片級 3D 複製

兩輪的 spec 都通過一般驗證和 --strict-quality,八個 pass 也都留下 render、comparison 與 review 紀錄,流程狀態跑到 complete(八個階段全部走完、每階段都有審查紀錄)。本文的「成品」是這輪流程完成後可互動、可繼續修改的即時網頁模型,不是 photogrammetry(用大量實拍照片反推出真實立體形狀的技術),也不是從單張照片還原出的精準產品 mesh。

工具內建那道「照片 vs. render 像素級比對」兩輪都沒有通過。它叫 Tier 1,用程式硬比兩張圖的輪廓有多重疊;之所以是「第一關」,是因為它最便宜,過了才輪到後面更貴的視覺審查——這次兩輪都卡在第一關。

這項結果我會和 agent 的視覺 review 分開列出,因為兩者結論不同:agent 看畫面覺得可以,程式比對輪廓覺得不行。 我不用「八個 pass 跑完」掩蓋輪廓與取景仍有差距。

先給趕時間的人:三句話結論

TIP — 不想讀完整篇的話,這裡就是全文結論
  1. 它產出的是可讀、可改、可進版控的 Three.js 程式碼(TypeScript)+一份 JSON 規格,不是 .glb / .obj 模型檔。要拿去 Blender 雕或 3D 列印的話,這支不適合。
  2. 成品是「一眼認得出是這個東西、可以旋轉、可以點按鍵」的網頁 3D 物件,不是照片級複製。 想像產品頁上那顆可以轉的小物件,不是電商實拍的替代品。兩輪都沒過工具自己的像素級比對。
  3. 你不需要懂 3D 就能開始跑。 上手成本是一句中文提示詞加一張圖;真正的成本在後面——工具的自動材質判斷會錯,需要人去覆寫。第二輪那次我把它判錯的四種材質整理成一張表,錯在哪、錯的方向是什麼都記在裡面。

img2threejs 實際產生什麼

它用 agent 做視覺判斷,再由 Python 腳本檢查規格與流程:

參考圖
  → 圖片適用性檢查
  → Pre-Spec Assessment
  → ObjectSculptSpec
  → 規格驗證
  → Three.js TypeScript factory
  → 瀏覽器 render
  → 參考圖與 render 對照
  → agent 決定繼續或修正

八個 pass 各自負責什麼

一般物件的完整 build 順序是(這張表後面兩輪都會用到):

Pass 這個階段在做什麼
blockout 先用大形狀抓出物件輪廓、尺寸比例和主要量體,暫時不處理材質。
structural-pass 拆出物件的零件階層、重複結構與接合關係,確認把手、支架等部件接在正確位置。
form-refinement 修細外型,包括曲線、斜面、倒角(把尖銳的邊修成小斜面或圓弧)、厚度、錐度(一頭粗一頭細)、不對稱與局部幾何。
material-pass 對齊各部位的顏色、粗糙度(霧面還是鏡面)、金屬度(是不是金屬)、凹凸感與材質變化。
surface-pass 加入刮痕、污漬、磨損、顆粒、AO(縫隙與夾角自然變暗的效果)、normal/bump(用貼圖假造凹凸,不真的增加幾何)等表面細節。
lighting-pass 設定主光、補光、輪廓光與環境光,讓形狀和材質在畫面上看得清楚。
interaction-pass 補上 pivot、socket、collider 等節點,讓模型之後能旋轉、動畫、碰撞或拆解。
optimization-pass 在外觀確認後控制三角形、draw call、instancing、LOD 與 FPS,降低執行成本。

每個階段只處理一類問題。輪廓不對就不該急著補材質,零件接合還沒處理好,也不該先花時間做刮痕。這種 gate 設計比「請 agent 一次把模型寫完」更容易找出是哪一步開始走偏。

產生的 factory 回傳 THREE.Group,並在 root.userData.sculptRuntime 放入節點、socket、collider 等 runtime 資訊。也就是說,輸出不只是畫面上的一坨幾何,後續仍能用程式控制零件。

安裝:Codex 與 Claude Code

流程和輸出大概是這樣。接下來是怎麼把它裝進來——這一步比我預期的短,兩個環境都只有一段指令。

Codex 可以用內建的 skill installer 直接從 GitHub 安裝:

python3 ~/.codex/skills/.system/skill-installer/scripts/install-skill-from-github.py \
  --repo hoainho/img2threejs \
  --path . \
  --name img2threejs

安裝位置是 ~/.codex/skills/img2threejs。開新的 Codex 任務後,就能讓 agent 讀取這個 skill 的完整工作流程。

官方 README 也提供 Claude Code 的安裝方式,本文第二輪就跑在這裡:

git clone https://github.com/hoainho/img2threejs.git \
  ~/.claude/skills/img2threejs

兩輪的共同條件

兩輪是同一個實驗跑兩次,所以先把「固定了什麼、只變了什麼」講清楚,後面的差異才有意義。

鍵盤照片只有一張,是普通的網頁尺寸產品照。兩輪的基本圖片檢查與 reference admission 都通過。

第一次使用不需要先懂 assessment、spec 或 blockout。GitHub README 給 Claude Code 的基礎提示詞只有一句:

/img2threejs Rebuild this object as a Three.js model, keep the proportions, angles, and colours.

附上參考圖後,改成一般說法即可。兩輪用的都是這一句,逐字相同:

請使用 img2threejs skill,把這個物件重建成 Three.js 模型,
保留原圖的比例、角度和顏色。

後面的圖片檢查、spec、分階段生成與 render 對照,都交給 skill 處理。你只需要描述想重建哪個物件,不必先學會內部術語。

INFO — 固定什麼、變什麼

兩輪相同:同一張 keyboard-reference.png、上面那句中文提示詞,以及同一組 img2threejs、Python、Node.js、Three.js 與 Vite 版本(實測版本記在下方,不是專案宣告的最低需求)。

兩輪不同:執行的 agent。第一輪是 ChatGPT 5.6、推理強度 xhigh;第二輪是 Claude Code 的 Opus 5(1M context)session。

所以後面看到的差別可以歸到 agent 身上,不是環境。

實測版本:img2threejs 1.3.0、Python 3.14.3、Node.js 24.4.0、Three.js 0.178.0、Vite 7.3.6。兩輪的輸出都是 TypeScript,也都跑過 tsc 檢查;第二輪的 TypeScript 是 5.9.3,另外裝了 @types/three。

還有一件事值得先講清楚:這些檢查底下的 Python 腳本全部只做「量測、檢查、記錄」,沒有一支會判斷「圖裡是什麼」或「做得像不像」——那兩件事從頭到尾都是 agent 在做。腳本的作用是讓 agent 沒辦法自己說「我覺得可以了」就往下走。

第一輪:ChatGPT 5.6

起手式與 spec

starter spec 一開始確實過不了 strict-quality。這次我補完材質 local override、每個 component 的 colorMaterialRecipe、detail inventory、物件專屬 feature target、燈光描述與 reference PBR 證據後,鍵盤的 strict 驗證才變成通過、而且沒有留下 warning。

四種材質的 PBR 取樣證據都過了工具的信心門檻。這代表 crop 可供後續材質實作參考,不代表從單張照片反推出真實物理材質。

spec 定案之後,orchestrate_passes.py 才會解鎖第一個 pass。下面是這輪八個階段的實際 render。

八個 pass

1. blockout:只留下低矮長方形底座,先看整體比例。

鍵盤第 1 個 pass:blockout

2. structural-pass:加入六排按鍵、功能鍵、導覽區、方向鍵與後方線材。

鍵盤第 2 個 pass:structural-pass

3. form-refinement:底座和鍵帽改成圓角幾何,鍵列加入高度差。

鍵盤第 3 個 pass:form-refinement

4. material-pass:分開底座、deck、鍵帽與橡膠線材的 PBR 反應。

鍵盤第 4 個 pass:material-pass

5. surface-pass:補上鍵帽文字、前緣接縫與 logo。

鍵盤第 5 個 pass:surface-pass

6. lighting-pass:以柔和主光、補光、輪廓光和接觸陰影拉開黑色層次。

鍵盤第 6 個 pass:lighting-pass

7. interaction-pass:替按鍵加上可按壓 action、鍵盤 collider 與線材 socket。

鍵盤第 7 個 pass:interaction-pass

8. optimization-pass:依鍵帽寬度分組 instancing,保留外觀並減少重複物件。

鍵盤第 8 個 pass:optimization-pass 最終成品

最終 reference/render 對照:

鍵盤參考圖與 img2threejs 最終成品對照

我也補拍參考視角、俯視與側視。multi-angle 檢查回傳 degenerate: false,證明它不是只在單一角度成立的平面假 3D:

鍵盤最終成品的參考、俯視與側視

直接轉轉看

第一輪 ChatGPT 5.6 xhigh 的成品:拖曳旋轉、滾輪縮放,點一下鍵帽會下壓。

這輪的結果與限制

這輪的 spec 把鍵盤拆成一棵相當細的元件樹,四種材質,八個 pass 每一階段都留了 review。agent 視覺 review 的最終分數在可接受範圍。成品已經有可辨識的 TKL 分區、不同寬度鍵帽、文字、前緣、線材與按鍵互動;它仍不是原廠 CAD,鍵帽側面 sculpt、字型和隱藏底部都是近似。

最終 generator factory 是把 spec 逐一展開成 component 的寫法,篇幅相當長,我另外調整了幾何、互動與 instancing。tsc --noEmit 和 Vite production build 通過,瀏覽器裡沒有 runtime error。

真正沒有跟著人工 review 一起「通過」的是 deterministic 的檢查。Tier 1 做的事是把 render 的剪影疊到照片的剪影上,看兩者重疊多少——這輪的重疊率離門檻還很遠。純數學檢查的 divine_eye.py 也直接退回(它綜合輪廓、比例、感知雜湊、明暗等訊號,不呼叫任何模型)。主要差異來自照片與 render 的取景、尺寸和實際輪廓,因此本文把它稱為「八個 pass 完成的即時網頁成品」,不稱為照片級還原。

第一輪就停在這裡:八個 pass 跑完了,deterministic 的門檻沒過。當下我沒辦法判斷這是 skill 的上限,還是這一個 agent 的上限——這兩件事的差別很大,值不值得繼續投時間也差很多。所以我把同樣的條件再跑一次,只換掉執行的 agent。

第二輪:Opus 5,同一張圖、同一句提示詞

這輪的照片、提示詞、img2threejs 版本和 Node/Three.js/Vite 都和第一輪一樣,執行的是 Claude Code 的 Opus 5(1M context)session。共同條件都寫在前面〈兩輪的共同條件〉,所以下面看到的差別可以歸到 agent 身上。

起手式與 spec

Stage 1/2 一樣通過,但腳本的材質分類全錯。

參考圖 check_reference_admission.py 回傳 ADMITTED。複雜度我評為 complex,detail inventory 把圖切成網格逐區掃過,找出的細節條目超過工具要求的下限。四種材質的 PBR 取樣證據也都過了信心門檻。

真正值得記下來的是這裡:analyze_texture.py 對四種材質的 finish 分類全部判錯,而且錯得很有方向性。

材質 腳本判定 照它的判定做會變成 我改成
外殼 case-satin-black worn-composite(磨損複合材) 完全不反光的粗糙塑膠——但前緣的小斜面上明明有一道明顯的高光帶 半亮面塑膠,加一層很薄的清漆
鍵帽 keycap-matte-black painted-metal(烤漆金屬) 霧面鍵帽變成一片油光——它把鍵帽上的白色字元誤認成反光亮點了 純霧面,清漆歸零
底板 deck-black gem-metal(寶石/金屬) 塑膠底板反射出周圍環境——它抓到的裁切圖是鍵縫的格狀圖案,讀錯了 非金屬
線材 cable-rubber-black brushed-steel(拉絲不鏽鋼) USB 線變成一條金屬管——橡膠是非導體,也沒有拉絲金屬那種方向性反光 非金屬,取消方向性反光

四種材質全錯,而且錯的方向一致:它傾向把黑色霧面的東西判成金屬或亮面。 如果你打算跑黑色、深色或霧面的物件,這一步預設要當成「需要人工覆寫」,不是「跑完就好」。

extract_part_color_recipe.py 也把鍵帽判成 stone、信心很低,即使我下了 --material-class-hint plastic 也沒被採納。最後我只留下它抽出來的顏色值,材質分類和粗糙度自己填,並把「腳本原本判成什麼、為什麼不採用」一起寫進那個零件的配方註解裡。

TIP — 腳本的「證據」可用,腳本的「判斷」不可信

這正好對上 SKILL.md 那句 Never let a script *score* visuals — that is the agent's job。腳本抽出來的像素證據(albedo cluster、palette、粗糙度基準)是可用的;它們對「這是什麼材質」的判斷則不可信。

我把四次覆寫連同理由寫進 materials[].agentOverride,讓後面看 spec 的人知道哪些設定是機器給的、哪些是我改的。

每一種材質都要從照片上裁一小塊當證據,其中兩張沒有通過 admission 的檢查,理由都跟「這張圖能不能代表一個零件」有關:

  • 底板那張裁到的是鍵與鍵之間的縫隙,畫面是一格一格的網狀而不是一整塊連續表面,工具判定它不像單一零件(mask coherence 不足)。
  • 線材那張是因為線在原圖裡只有幾個像素寬,我把中心線重新取樣成一小張長條圖,結果整張都是線材、沒有背景可以對照(foreground coverage 滿載)。

這兩張我只拿來當顏色與粗糙度的參考,沒有拿去當輪廓來源。

spec 通過 strict-quality,但留一個 warning。

最後的 spec 拆成 macro/meso/micro 三層的元件樹、四種材質、三組重複結構系統,以及一份物件專屬的重點特徵清單。--strict-quality 通過,但留下一條 warning:suitability is pass but risks are present; confirm they are acceptable。

這條 warning 沒有辦法「回填確認」——validator 只要 suitability 是 pass 且 risks 非空就會發。要清掉它只能把 risks 刪掉,那等於把「單視角、四種黑會塌成一團、字型無法還原、取景會拖垮輪廓比對、分類器判錯」這五項風險藏起來。我選擇留著 warning。

過 strict-quality 的路上被擋了四次,每次都是真的缺東西:

  1. primitive 必須落在一份允許清單裡,我原本寫的 rounded-box、dished-box、tube-along-curve 都不合法。
  2. 每個 component 都要 colorMaterialRecipe,顏色還得是 rgba() 字串。
  3. referencePbr.maps 要是物件,不能是字串陣列。
  4. unknownsToResolveBeforeImplementation 不能留未解項目。

第四項我沒有走捷徑。 最偷懶的做法是把未解項目直接清空,validator 就閉嘴了;我改成新增 resolvedUnknowns,逐條寫下「接受為推測,信心壓低」,讓後面看 spec 的人知道哪些地方是猜的。

spec 過關之後才輪到 pass。下面同樣是八個階段的畫面,其中兩步我被擋回來重做,也一併寫在對應的 pass 下面。

八個 pass

1. blockout:底座量體、整體腳印比例與打字傾角。

Opus 5 鍵盤第 1 個 pass:blockout

這個 pass 我被 gate 擋了一次。第一次把傾角放在 form-refinement,結果 blockout 的 critical feature case-silhouette 分數低於門檻,append_review.py 直接拒絕寫入 continue。傾角本來就屬於「主要量體」,我把它移到 blockout 才過關。這是 gate 在做該做的事。

2. structural-pass:全部鍵帽、六排、導覽鍵區、倒 T 方向鍵與線材。

Opus 5 鍵盤第 2 個 pass:structural-pass

鍵位由一張 layout table 驅動,每顆鍵的位置與寬度都由表格指定——從一般字母鍵到空白鍵,各種不同寬度都在表裡。這一步讓工具的比例檢查與尺寸檢查雙雙進到門檻內。

3. form-refinement:圓角外框、前緣 chamfer、鍵帽碟形凹面與 R1–R4 高度階差。

Opus 5 鍵盤第 3 個 pass:form-refinement

工具的輪廓比對幾乎沒有反應。這一步改的是輪廓裡面的東西,deterministic 的輪廓指標對碟形、倒角和高度階差基本上是盲的。

4. material-pass:四種材質各自獨立的 albedo/roughness/normal/AO。

Opus 5 鍵盤第 4 個 pass:material-pass

四個通道都用各自的 seed 生成,沒有把 albedo 拿去當 roughness 或 normal 用。這個 pass 的畫面明顯比參考圖暗,因為參考打光還沒建。

5. surface-pass:鍵帽文字、FILCO logo 與鍵縫暗化。

Opus 5 鍵盤第 5 個 pass:surface-pass

所有鍵帽的字其實是印在同一張大圖上(legend atlas),一顆鍵取其中一格。字印在鍵帽頂面左上角、數字排的 shift 符號在數字上方,和參考圖一致。字型是 canvas 畫的 condensed sans,不是原廠字面。

6. lighting-pass:主光、補光、輪廓光、ACES tone mapping 與接觸陰影。

Opus 5 鍵盤第 6 個 pass:lighting-pass

這是第一個「四種黑分得開」的 pass。線材的投影我特意關掉——參考圖的背景被打到純白,照片裡看不到線材陰影,加上去會是照片不支持的假細節。

7. interaction-pass:每顆鍵一個轉軸與碰撞體,外殼與線材也各有自己的節點。

Opus 5 鍵盤第 7 個 pass:interaction-pass

這張是壓著 G 拍的(?press=g),下壓行程有實際作用。

8. optimization-pass:所有鍵帽合併成少數幾個 geometry,外觀零變化。

Opus 5 鍵盤第 8 個 pass:optimization-pass 最終成品

這個 pass 我第一版寫錯,而且錯得很典型:鍵帽形狀幾乎一樣,直覺上該用 InstancedMesh(用同一份幾何複製很多份,只換位置,省效能),按鍵寬分組。但這招的前提是「這些複製品完全一樣」——而每顆鍵帽印的字都不一樣。 共用幾何就等於共用取圖座標,結果所有鍵帽全部去取整張 atlas,畫面變成一片亂碼。

改成「每個 row/cluster 合併成一個 geometry」之後,每顆鍵的取圖座標和每排的傾角都保住了,按鍵動作改用記錄下來的頂點範圍位移實作。key field 的 draw call(GPU 每一次「畫一批東西」的呼叫,數量直接影響效能)從「每顆鍵一次」降到「每排一次」。

最終 reference/render 對照:

Opus 5 版本的鍵盤參考圖與最終成品對照

參考視角、俯視與側視。diagnose_render_multi_angle.py 回傳 degenerate: false,轉到側面也沒有塌成薄片:

Opus 5 版本鍵盤成品的參考、俯視與側視

俯視那張是我最滿意的一張——鍵盤的分區、鍵寬和文字逐鍵對得上參考圖。

直接轉轉看

上面都是截圖,但這個 skill 的重點就是「輸出是活的程式」,所以我把這輪的成品直接跑在這頁上:

拖曳旋轉、滾輪縮放,上排可以切換八個 pass,點一下鍵帽會下壓。

值得注意的是按鍵下壓不是我另外做的動畫。interaction-pass 產出的模型本來就在 root.userData.sculptRuntime 裡放好了每顆鍵的轉軸、碰撞體與可觸發的動作,這個頁面只是把滑鼠事件接上去而已。這也是為什麼前面說「輸出不只是畫面上的一坨幾何」。

切到「1 量體」再一路點到「8 優化」,會比看八張靜態圖更清楚每一個 pass 到底加了什麼。

這輪的結果與限制

這輪的元件樹比第一輪精簡,但同樣是四種材質、八個 pass 都留了 review,agent 視覺 review 的最終分數略高於第一輪。runtime 端每顆鍵都有自己的轉軸、碰撞體與可觸發的動作。

程式分成四個檔(layout table、材質貼圖產生器、模型 factory、review harness),起點是 generate_threejs_factory.py 產出的那份 factory,我在上面繼續改。tsc --noEmit 與 Vite production build 都通過,瀏覽器裡沒有 runtime error。

輪廓比對的重疊率明顯比第一輪高,divine_eye.py 也從「直接退回」變成「有機會但要再看」。仍然沒有到門檻——原因和下一節一起講。

兩輪對照與我的判斷

兩輪到這裡都跑完了。同一張照片、同一句提示詞、同一版 skill,換一個 agent,差別放在一起比較清楚。

項目 ChatGPT 5.6 Opus 5
元件拆解 拆得較細 較精簡
材質處理 四種材質分開 四種材質分開
strict-quality 通過,沒有 warning 通過,留一條 warning(我刻意不清掉風險)
逐階段 review 八個 pass 都有紀錄 八個 pass 都有紀錄
agent 視覺評分 兩輪接近 略高
像素級輪廓比對 未通過 未通過,但輪廓明顯更接近
純數學綜合判決 直接退回 有機會但要再看
多視角檢查 不是假 3D 不是假 3D
程式規模 逐一展開,較長 抽成表格驅動,短很多
可維護性 改鍵位要動很多處 改一行表格

一句話結論:兩輪都跑得完,Opus 5 這輪的輪廓明顯更接近照片。

程式長短不代表誰寫得比較好。第一輪是 spec 直接展開成逐一 component 的寫法;第二輪短很多,是因為我把所有鍵帽抽成一張 layout table 加一個 geometry 產生器。兩種都能跑,但前者改鍵位要動很多地方,後者改一行表格就好。

WARNING — 兩輪都沒有通過 Tier 1

Opus 5 這輪的輪廓重疊率比第一輪好很多,純數學判決也從「直接退回」變成「有機會但要再看」,但都還沒摸到 pipeline 的硬門檻,兩輪都沒過。

卡住的地方很明確:我把 render 的剪影疊到照片上看,render 的輪廓一開始整體比照片小一圈,修掉之後剩下的差異是「照片的前後景深比 render 大、render 的左右兩端比照片寬」。那是真實相機的鏡頭特性,用虛擬相機的透視逼不出來。我試過換更廣的視角加近距離,重疊率反而掉下去。

一個附帶觀察:顏色和整體明暗其實對得很好,差的是邊緣。也就是說這次沒過關不是材質沒做對,是形狀與取景沒對上。

所以兩輪同樣都不叫照片級還原,是「八個 pass 完成、可互動、可繼續改程式的即時網頁模型」。

我會把它用在哪裡

產品頁上的旋轉小物件、遊戲道具 blockout、互動式教學圖,會是我願意繼續使用 img2threejs 的情境。輸出可以看 diff、改尺寸、換材質,也能把零件接到互動事件;若目標是可雕刻、可列印或高擬真的 mesh,我會改用真正的 image-to-3D 模型。

兩輪都不只停在 blockout。鍵盤各自完成八個 pass、strict-quality、逐階段 review、多視角檢查與最終 browser render。它的重複按鍵與清楚分區很適合程序化零件組裝;如果要再往產品級逼近,我會補拍側視與底部照片,繼續修正鍵帽輪廓、字型與底座細節。

參考資料

聊聊你的想法

有不同的做法,或遇到相似的問題?歡迎一起討論。

Email 不會公開,其他讀者看不到;僅用於回覆通知,可隨時取消。

免登入 · 送出後直接公開

留言與討論

正在載入留言…