
HexaZen - EP06:積木選擇器與縮圖生成
HexaZen 有 30 多種積木,光靠文字名稱選擇太無趣了,這裡需要每個積木的 3D 預覽縮圖。
但 30 多張圖片手動截圖太累了,而且每次微調積木就要重做。...(›´ω`‹ )
來做個自動縮圖產生器吧!( ´ ▽ ` )ノ
多執行緒縮圖生成器
核心概念是建立多個隱藏的 canvas,每個 canvas 都有獨立的 Babylon.js 引擎,像產線一樣平行生成縮圖。
決定並行數
首先根據裝置性能決定要啟動幾個「產線」:
content\aquarium\hexazen\composables\use-thumbnail-generator.ts
function getOptimalConcurrency(): number {
if (typeof window === 'undefined' || !window.navigator) {
return 2
}
const cores = navigator.hardwareConcurrency || 4
const memory = (navigator as any).deviceMemory || 4
let optimal = 2
if (cores >= 8 && memory >= 8) {
optimal = 6
}
else if (cores >= 4 && memory >= 4) {
optimal = 4
}
else {
optimal = 2
}
return Math.min(optimal, 6)
}8 核 + 8GB 記憶體以上開 6 個,4 核開 4 個,其他開 2 個。上限 6 個避免吃太多資源。
建立隱藏的產線
每個產線都是一個完整的 Babylon.js 場景:
function createGenerator() {
const queue = new PQueue({ concurrency: 1 })
const canvas = document.createElement('canvas')
canvas.style.visibility = 'hidden'
canvas.width = 128
canvas.height = 128
document.body.appendChild(canvas)
const engine = new Engine(canvas, true)
const scene = new Scene(engine)
scene.clearColor = new Color4(0, 0, 0, 0) // 透明背景
const camera = new ArcRotateCamera(
'camera',
Math.PI / 4,
Math.PI / 4,
5,
Vector3.Zero(),
scene,
)
camera.useFramingBehavior = true
if (camera.framingBehavior) {
camera.framingBehavior.mode = FramingBehavior.FitFrustumSidesMode
camera.framingBehavior.radiusScale = 1
camera.framingBehavior.framingTime = 0
}
const light = new HemisphericLight('light', new Vector3(0, 1, 0), scene)
// ...
}128×128 的小 canvas,透明背景,FramingBehavior 會自動將模型置中並調整鏡頭距離。
縮圖生成流程
function generateThumbnail(
blockType: BlockType,
options?: { signal?: AbortSignal },
) {
return queue.add(
async () => {
const blockData = blockDefinitions[blockType]
// 載入所有部件
const tasks = blockData.content.partList.map(async (part) => {
const modelPath = `${blockData.content.rootFolderName}/${part.path}`
const model = await LoadAssetContainerAsync(modelPath, scene)
const root = model.meshes[0]
if (root) {
root.position.copyFrom(Vector3.FromArray(part.position))
root.rotationQuaternion?.copyFrom(
Quaternion.FromArray(part.rotationQuaternion),
)
root.scaling.copyFrom(Vector3.FromArray(part.scaling))
}
return model
})
const containerList = await Promise.all(tasks)
// 清除上一個模型,載入新模型
clearLastContainer()
lastContainerList = containerList
containerList.forEach((c) => c.addAllToScene())
// 自動調整鏡頭框住所有 mesh
const meshes = containerList
.flatMap((c) => c.meshes)
.filter((m) => m.getTotalVertices() > 0)
camera.framingBehavior?.zoomOnMeshesHierarchy(meshes, true)
// 渲染一幀並截圖
engine.runRenderLoop(() => scene.render())
await scene.whenReadyAsync(true)
camera.beta = Math.PI / 4
camera.alpha = Math.PI / 4
const base64 = await Tools.CreateScreenshotAsync(engine, camera, {
width: 128,
height: 128,
})
engine.stopRenderLoop()
const res = await fetch(base64)
return await res.blob()
},
{ signal },
)
}流程為:載入模型 → 自動框住 → 渲染 → 截圖 → 轉 Blob。
這裡使用 p-queue 套件解決 queue 實作,每個產線一次只處理一個任務(concurrency: 1),但多個產線可以同時工作。
Round-Robin 分配
任務以輪詢方式分配給產線:
function generateThumbnail(blockType: BlockType) {
const generator = generatorList[index]
index = (index + 1) % concurrency
return generator.generateThumbnail(blockType)
}IndexedDB 快取
生成縮圖很耗時,不能每次開頁面都重新產生。用 IndexedDB 快取起來:
content\aquarium\hexazen\domains\block\block-picker.vue
const blockThumbnailList = computedAsync(async () => {
const tasks = blockTypeList.map(async (blockType) => {
const key = `hexazen-${version}-block-thumbnail-${blockType}`
// 先查快取
const cache = await get(key)
if (cache) {
return {
type: blockType,
thumbnail: URL.createObjectURL(cache),
}
}
// 快取沒有,即時生成
const imgBlob = await generateThumbnail(blockType)
await set(key, imgBlob)
return {
type: blockType,
thumbnail: URL.createObjectURL(imgBlob),
}
})
return Promise.all(tasks)
}, [])快取的 key 包含 version,版本更新時自動失效重新生成。
離開頁面時也要記得清理 ObjectURL:
onBeforeUnmount(() => {
blockThumbnailList.value.forEach((item) => {
URL.revokeObjectURL(item.thumbnail)
})
})積木選擇器 UI
有了縮圖,UI 就簡單了。使用輪播元件搭配倒角邊框風格:
<template>
<div class="chamfer-4 p-1 bg-gray-200">
<div class="bg-white chamfer-3.5 p-3">
<u-carousel v-slot="{ item }" :items="blockThumbnailList" drag-free>
<div
class="size-22 chamfer-3 p-0.5 bg-gray-100
cursor-pointer select-none"
@click="handleClick(item.type)"
>
<img
:src="item.thumbnail"
class="border-none! bg-white chamfer-2.5"
>
</div>
</u-carousel>
</div>
</div>
</template>drag-free 讓使用者可以自由拖拽瀏覽,手機上也很順暢。◝(≧∀≦)◟
createSharedComposable 單例化
縮圖生成器在整個應用中只需要一個實例,使用 VueUse 的 createSharedComposable:
export const useThumbnailGenerator = createSharedComposable(
_useThumbnailGenerator,
)不管幾個元件呼叫 useThumbnailGenerator(),都會拿到同一個實例,共享同一批產線。
總結 🐟
以上程式碼可以在此取得
- 多執行緒無頭 Babylon.js 場景自動產生 3D 縮圖
- 根據裝置性能動態調整並行數
- IndexedDB 快取避免重複生成
createSharedComposable確保全域單例- 輪播 UI 支援拖拽瀏覽
下一章讓我們來看分享編碼與 URL 壓縮的實作吧。♪( ◜ω◝و(و
TIP
因為篇幅問題,某些部分簡單帶過。
若想看到更詳細的解釋,請不吝留言或寫信給我喔!(*´∀`)~♥