.lobj 格式設計文件

Binary Triangle Mesh Vertex Labels

為什麼需要 .lobj?

AI 應用中,3D 網格模型的語意標註越來越重要—— 牙齒分割、器官辨識、場景理解,都需要在網格的每個頂點上記錄分類標籤(label)。 然而現有主流格式都沒有為此設計,於是有了 .lobj。

現有格式的缺口

.stl

結構最簡單,但頂點大量重複,無法與標籤一一對應。

.obj

有獨立頂點清單,結構良好,但無標籤欄位。

.ply

可自訂屬性,但定義繁瑣,互通性差。

設計要求

如何設計

為何選 .obj 作為基礎?

.obj 天生將頂點與面分開儲存,只需附加一個等長的標籤陣列, 即可對應每個頂點的語意,無需改動面的結構。

# 原始 .obj 結構
v  x  y  z       ← 頂點清單
f  i  j  k       ← 面清單(引用頂點索引)

# .lobj 只需多一份
label  n         ← 標籤清單(與頂點一一對應)

為何用二進制?

  • 解析速度快:可直接做記憶體映射,無需逐字元解析
  • 體積小:float32 (4 bytes) 遠比 ASCII 數字字串緊湊

標籤型別:int8

  • 標籤數量通常 127 以內,int8 已足夠
  • 選用有號數而非 uint8,保留 -1 作為「未標記(unlabeled)」的慣例值

二進制檔案結構

HEADER  ·  28 bytes(7 × int32)
counts[0]  int32  頂點數 cntv  必要
counts[1]  int32  法向量數     0 或 cntv
counts[2]  int32  顏色數       0 或 cntv
counts[3]  int32  標籤數       0 或 cntv
counts[4]  int32  紋理座標數   0 或 M
counts[5]  int32  三角形數 cntt 必要
counts[6]  int32  紋理ID數     0 或 cntt
DATA CHUNKS  ·  依 header 順序緊接
vertices   float32  cntv × 3  必要
normals    float32  若 > 0
colors     float32  若 > 0
labels     int8     若 > 0
tex2d      float32  若 > 0
triangles  uint32   cntt × 3  必要
texId      uint32   若 > 0
💡 實務上,vertices、triangles、labels 是絕大多數場景唯一用到的三個欄位,其餘均為選填。

設計回顧

.lobj 最初設計時,以盡量相容原始 .obj 為目標, 因此保留了法向量、顏色、紋理座標等多個選填欄位。

實際使用後發現:絕大多數場景只需要 vertices、triangles、labels, 其他欄位的使用率極低。

這反映了「通用性 vs. 精簡性」的設計取捨。 對於只需核心三欄位的使用者,忽略其他欄位即可,格式本身不強制填寫。

程式碼(JavaScript)

import { isValidLobj, parseLobjBinary } from '@cadcam/lobj';

// 從 File 物件載入(例如 input[type=file] 或 drag-drop)
async function loadFile(file) {
    try {
        const buf = await file.arrayBuffer();
        if (isValidLobj(buf)) {
            const lobjdata = parseLobjBinary(buf);
        }
    } catch (e) {
        alert('解析失敗: ' + e.message);
    }
}

// 拖曳 drag-drop 方式
function add_event_drop_lobj(container) {
    container.addEventListener('dragover', e => e.preventDefault());
    container.addEventListener('drop', e => {
        e.preventDefault();
        const file = e.dataTransfer.files[0];
        if (file) loadFile(file);
    });
}

// input[type=file] 方式
document.getElementById('fileInput').addEventListener('change', e => {
    const file = e.target.files[0];
    if (file) loadFile(file);
});

// 從伺服器 fetch 載入
async function fetch_from_url(url) {
    const resp = await fetch(url);
    if (!resp.ok) throw new Error(`Failed to load ${url}: ${resp.status}`);
    const buf = await resp.arrayBuffer();
    if (isValidLobj(buf)) return parseLobjBinary(buf);
    throw new Error(`Invalid .lobj file: ${url}`);
}