iT邦幫忙

2026 iThome 鐵人賽

DAY 24
0
AI Engineering

《矽墟》:我把一部科幻小說當成軟體專案來管系列 第 24

Day 24|把一張三視圖拆成 9 個元件,先分類再實作

  • 分享至 

  • xImage
  •  

模組四|從 2D 到 3D(Day 20–25)

昨天寫完契約——chenger-sculpt-spec.json 裡的 qualityContract,一份規定「哪些地方不准變、哪些地方准簡化」的驗收條款——今天實作。

要用 Three.js 內建的基本幾何體,把一個角色堆出來。問題是:看著一張三視圖,你要從哪裡開始?

問題:「用球體做頭」是廢話

如果你問怎麼用基本幾何體做角色,答案通常是「頭用球體、身體用圓柱、裙子用圓錐」。

這在只有三個部位的時候有用。碰到實際的角色設計就不夠了。這個角色身上有:雙丸子頭、呆毛、廣袖和服、黑金層疊袖口、繩結腰帶、掛飾流蘇、百褶裙、厚底靴、以及一支連著鏈條和繩索的鐵錨。

問題不在「哪個部位用哪個幾何體」,在於你根本不知道要切成幾塊。

切太粗,做不出特徵;切太細,617 行程式碼會變成一團無法維護的東西。(這 617 行量的是 chenger.html<script type="module"> 區塊的內容行,整檔連同 HTML 與 CSS 是 865 行。)

怎麼做:加一個分類步驟

我先拿卡片比一下這是哪一類,手就自己去抽對應的那支工具

我在「看圖」和「寫程式」之間插了一步:把每個部位標上拓撲類型。

chenger-sculpt-spec.jsoncomponents 陣列,每個元件三個欄位:

{
  "id": "skirt",
  "topologyClass": "pleated-cone",
  "primitiveStrategy": "CylinderGeometry open cone, radial cos ripple on vertices, flatShading",
  "localFeatures": ["hanging charm discs + tassels (instanced)", "brown underlayer"]
}
  • topologyClass:這東西本質上是什麼形狀家族
  • primitiveStrategy:用哪些 primitive、怎麼組
  • localFeatures:這個部位的細節清單

關鍵是第一欄。先分類,實作就幾乎是機械的。

九個元件與它們的拓撲類型

id topologyClass primitiveStrategy(節錄)
head sphere SphereGeometry scaled
hair clustered-volumes sphere cap + 瀏海 lobes + 2 個丸子(torus+spheres)+ 呆毛 TubeGeometry + 側髮 cones
face applique 壓扁的 sphere 當眼(白+琥珀虹膜+瞳+高光)、薄 box 當眉、torus 弧當嘴
torso-kimono lathe LatheGeometry 白上衣、黑 V 領 box 條、繩項鍊 torus 弧 + cone 流蘇
sleeves flared-cones LatheGeometry 廣袖、黑袖口 cylinder + 金環 tori、sphere 手
skirt pleated-cone CylinderGeometry 開口錐、頂點徑向 cos 漣漪、flatShading
rope-belt torus+knot TorusGeometry 腰帶、結用 spheres、cone 流蘇
legs-boots stacked-cylinders 皮膚 cylinder、白襪 cylinder、靴 cylinder + 黑厚底 box、橘鞋帶條
anchor compound 環 torus + 桿 cylinder + 橫木 box + 弧臂 torus + 擠出的爪 + InstancedMesh 鏈環 + CatmullRomCurve3 繩管

九種拓撲類型,九種不同的處理方式。

為什麼分類這一步有價值

因為分類把「這看起來像什麼」轉成「這用哪套方法做」。

看著頭髮想「該怎麼做頭髮」是沒有出口的問題。但如果先判定它是 clustered-volumes(一團一團的體積),那方法就出來了:不要試圖做出髮絲,做出幾團體積,然後在上面放配件。

同樣的,applique(貼花)這個分類告訴我:臉不是一個立體結構,是貼在球面上的平面元素。 眼睛是壓扁的球,眉毛是薄板,嘴是一段圓環弧。不用做眼窩、不用做鼻梁。

分類是一個決定,而決定一旦做出來,實作就沒有選擇焦慮了。

最值得單獨講的一個:百褶裙

我只搖了一下曲柄,那整排折面自己就出來了,一折都沒扳過

CylinderGeometry open cone, radial cos ripple on vertices, flatShading

拆開來:

  1. CylinderGeometry 做一個上窄下寬、沒有頂底面的開口錐
  2. 對它的頂點做一個沿圓周方向的 cos 波動:把半徑寫成 r + amplitude * cos(n * θ)
  3. flatShading

第三步是關鍵。flatShading 讓每個面用單一法線著色,於是平滑的餘弦波在視覺上變成一道一道的折面——那正是百褶的樣子。

沒有做任何一個褶子。 褶子是「一個數學函數 + 一個渲染設定」共同產生的視覺結果。

這是這整套方法的縮影:不要模仿外觀,要找出產生那個外觀的機制。

另一個:鐵錨的鏈條

chain of instanced torus links + CatmullRomCurve3 rope tube to belt

鏈條是一堆一模一樣的環——這是 InstancedMesh 的教科書案例。一份幾何、一份材質、N 個變換矩陣。

繩子則是 CatmullRomCurve3:給幾個控制點,讓曲線自己穿過它們,然後沿曲線掃出管狀幾何。

繩子從腰帶連到錨。如果錨的位置要調整,我只要改控制點,繩子會自己重新彎。繩子不是一個模型,是一個關係。

實際用了哪些幾何體

617 行 JavaScript 裡的實際呼叫次數:

SphereGeometry      16      ConeGeometry         6
TorusGeometry       12      CatmullRomCurve3     4
CylinderGeometry    12      TubeGeometry         3
BoxGeometry          7      LatheGeometry        2
                            CircleGeometry       1
                            ExtrudeGeometry      1
                            InstancedMesh        1
Group 14

(數的是原始碼裡 new THREE.XXX( 的出現次數。網格本身沒有列進來:程式裡的 THREE.Mesh 幾乎都經過一個 mesh(geo, mat) 小工廠產生,直接數建構子會嚴重低估,數工廠呼叫又會漏掉迴圈裡展開的那些,所以這個數字我量不準,就不寫。)

沒有任何一個是外部載入的模型。 全部是 Three.js 內建的參數化幾何。

Group 有 14 個——那是 9 個元件加上一些子群組。分組不只是整理,它決定了哪些東西會一起移動。

一個額外的層:runtime

規格檔最後還有一段,是我覺得最容易被忽略但最有價值的:

"runtime": {
  "pivots": ["root", "anchorGroup"],
  "sockets": ["belt-knot", "anchor-ring"],
  "nodes": ["head", "hair", "torso", "skirt", "anchor", "ropeTether"],
  "variantTargets": ["hair", "clothMain", "clothDark", "gold"]
}

這一段跟外觀無關,講的是這個模型可以被程式怎麼操作:

  • pivots:哪些點可以當旋轉軸
  • sockets:哪些位置可以掛東西上去
  • nodes:哪些部位可以被單獨取用
  • variantTargets:哪些材質可以被換色(就是 Day 22 講的那四組 themed

這一段是「它是一個可控物件」跟「它是一個好看的擺件」的差別。

image-to-3D 給的是一整塊網格,它沒有 socket、沒有可單獨取用的 node、沒有 variantTarget。要有,你得自己去切。

而這條路線的模型是用程式碼堆出來的,所以它天生就是結構化的。每個部位本來就是一個變數。

代價

代價一:拆解是人工的,而且要花時間。

九個元件、九種拓撲判定、每個的 primitiveStrategy,這些都是我看著三視圖一個一個想出來的。

一個角色大概要半天。 20 個角色就是 10 天。

這正是 Day 22 提到的「可量產性」在這條路線上的版本,它也不好量產,只是瓶頸從 GPU 換成了人。

代價二:忠實度就是不高。

契約寫 0.72——那是 targetFidelity 這個欄位,意思是「跟三視圖有七成像就算過關」。實際看起來就是 0.72 的樣子:認得出是這個角色,但它不是那張圖。

這條路線贏的不是像,是可控。 如果你的需求是「要像」,這條路線從一開始就不該考慮。

代價三:617 行的可讀性靠規格檔撐著。

程式碼本身是一長串幾何體的建構與擺位。單獨讀那 617 行,你很難看出結構。

規格檔是它的目錄。 兩個檔案要一起讀才有意義,而這代表它們有 Day 12 講過的那個風險:沒有機制保證規格檔跟程式碼同步。

我改程式碼的時候,不一定會回去改 JSON。這是這個做法目前最脆弱的地方。

帶走什麼

一、在「觀察」和「實作」之間插一個「分類」。

看著一個複雜的東西直接想「怎麼做」,會卡住。先問「這在結構上屬於哪一類」,答案出來之後,做法通常就跟著出來了。

分類的價值不在於分得多準,在於它把一個開放問題變成一個有限選項的問題。

這件事在寫程式上也一樣:看到一段需求直接想怎麼實作會卡;先判定「這是一個狀態機/一個管線/一個查表」,實作方式就收斂了。

二、不要模仿外觀,找出產生外觀的機制。

百褶裙不是做出很多褶子,是 cosflatShading

問句:這個視覺效果,是由什麼規則產生的? 找到規則之後,你用十行程式碼得到的東西,會比手工堆一百個褶子更好調、更好改、也更小。

三、規格裡要有一段「這東西之後怎麼被操作」。

大部分規格只描述成品長什麼樣。加一段描述它可以被程式怎麼動:哪裡是軸、哪裡可以掛東西、哪些部分可以被單獨換掉。

這一段決定了它是一個資產還是一個零件。資產只能整個換掉,零件可以被組合。

四、拆解粒度就是你的維護粒度。

九個元件,代表我之後能單獨改的最小單位是「元件」。我可以只換頭髮的顏色、只調鐵錨的位置。

如果我當初拆成三塊,那我就只能整塊改。如果拆成三十塊,程式碼會變得沒人看得懂。

拆解的時候要想的不是「怎麼做出來」,是「之後我會想單獨改哪些東西」。


明天 Day 25,模組四結算:兩條路線的完整對照,以及為什麼那顆花了一次 GPU 生成的模型,最後一行都沒被用到。 我會附上實際的 grep 結果。


上一篇
Day 23|動手之前先寫「像到什麼程度算合格」,而且目標值要小於 1
下一篇
Day 25|4B 參數的 SOTA 模型生出來的東西,我一行都沒用
系列文
《矽墟》:我把一部科幻小說當成軟體專案來管28
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言