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-blackworn-composite(磨損複合材)完全不反光的粗糙塑膠——但前緣的小斜面上明明有一道明顯的高光帶半亮面塑膠,加一層很薄的清漆
鍵帽 keycap-matte-blackpainted-metal(烤漆金屬)霧面鍵帽變成一片油光——它把鍵帽上的白色字元誤認成反光亮點了純霧面,清漆歸零
底板 deck-blackgem-metal(寶石/金屬)塑膠底板反射出周圍環境——它抓到的裁切圖是鍵縫的格狀圖案,讀錯了非金屬
線材 cable-rubber-blackbrushed-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 只要 suitabilitypassrisks 非空就會發。要清掉它只能把 risks 刪掉,那等於把「單視角、四種黑會塌成一團、字型無法還原、取景會拖垮輪廓比對、分類器判錯」這五項風險藏起來。我選擇留著 warning。

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

  1. primitive 必須落在一份允許清單裡,我原本寫的 rounded-boxdished-boxtube-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.6Opus 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。它的重複按鍵與清楚分區很適合程序化零件組裝;如果要再往產品級逼近,我會補拍側視與底部照片,繼續修正鍵帽輪廓、字型與底座細節。

參考資料

留言