












最近,GitHub上有一個很火的項目,一個基於 3D 模型的細胞學習平臺。該平臺的目標是幫助用戶通過 3D 模型學習細胞的基本概念和功能。
代碼地址為:LearningCell
項目代碼很短很少,非常簡單,隨著3d高斯潑濺效果的不斷完善,3D-WEB也快迎來了新的應用和突破。
項目的技術棧如下:
.
├── .github/workflows/deploy.yml # GitHub Pages 自動部署
├── README.md
├── app/ # Vite 前端工程
│ ├── public/
│ │ ├── draco/ # 自帶的 Draco 解碼器
│ │ ├── images/ # 細胞縮略圖(已壓縮)
│ │ └── models/ # 5 個 .glb 模型
│ ├── src/
│ │ ├── components/ # UI 組件(側欄、3D 查看器、信息面板等)
│ │ ├── data/models.ts # 5 個生物概念的數據
│ │ ├── hooks/useModel.ts # 加載狀態訂閱 hook
│ │ ├── lib/modelLoader.ts # 流式下載 + Draco 解析 + 緩存
│ │ ├── App.tsx
│ │ └── ...
│ └── package.json
└── (根目錄其它是源文件備份,例如未壓縮的 PNG 與 .draco.glb 原始資源)
1,入口代碼
在App.tsx中,我們使用useEffect來加載默認模型和預加載其它模型。
useEffect(() => {
let cancelled = false;
const firstEntry = loadModel(activeModel.modelUrl, {
fileSize: activeModel.fileSize,
});
let started = false;
const queueOthers = () => {
if (cancelled || started) return;
started = true;
const queue = MODELS.filter((m) => m.id !== activeModel.id);
let i = 0;
const next = () => {
if (cancelled || i >= queue.length) return;
const m = queue[i++];
preloadModel(m.modelUrl, { fileSize: m.fileSize });
const entry = getLoadEntry(m.modelUrl);
entry?.promise.finally(() => {
if (cancelled) return;
setTimeout(next, 120);
});
};
next();
};
}
2,模型加載代碼
在modelLoader.ts中,我們定義了loadModel函數,用於加載3D模型。
首先是從URL中獲取模型的文件大小,然後使用fetchWithProgress函數來流式下載模型。但是在本項目中是不需要用到下載,而是直接加載模型;
加載器直接基於three.js的GLTFLoader和DRACOLoader來加載模型。
import { GLTFLoader, type GLTF } from 'three/examples/jsm/loaders/GLTFLoader.js';
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js';
詳細的實現函數如下
export function loadModel(url: string, options: LoadOptions): LoadEntry {
const existing = cache.get(url);
if (existing) return existing;
const entry: LoadEntry = {
status: 'downloading',
progress: 0,
listeners: new Set(),
promise: Promise.resolve() as unknown as Promise<GLTF>,
};
cache.set(url, entry);
notifyCache();
entry.promise = (async () => {
try {
const buffer = await fetchWithProgress(url, options.fileSize, (loaded, total) => {
entry.status = 'downloading';
entry.progress = Math.min(0.95, (loaded / Math.max(1, total)) * 0.95);
notifyEntry(entry);
});
entry.buffer = buffer;
entry.status = 'parsing';
entry.progress = 0.97;
notifyEntry(entry);
const gltf = await parseGLTF(buffer, '');
entry.gltf = gltf;
entry.status = 'done';
entry.progress = 1;
notifyEntry(entry);
return gltf;
} catch (error) {
entry.status = 'error';
entry.error = error;
notifyEntry(entry);
throw error;
}
})();
return entry;
}
3,模型渲染代碼
具體的實現是在ModelScene.tsx中,我們使用useModel hook來訂閱模型加載狀態,當模型加載完成後,我們將模型添加到場景中。
/**
* 將 GLTF.scene 居中、縮放到合適大小後渲染。
* 通過 useFrame 實現可控的自動旋轉。
*/
export function ModelScene({
gltf,
autoRotate,
initialRotationY = 0,
displayScale = 1,
}: Props) {
const groupRef = useRef<THREE.Group>(null);
const { centeredScene, scale } = useMemo(() => {
const cloned = cloneScene(gltf); // 調用工具函數克隆場景,確保每個組件實例擁有獨立的模型副本
const box = new THREE.Box3().setFromObject(cloned); // 計算模型的包圍盒,獲取模型的空間範圍
const size = new THREE.Vector3();
box.getSize(size);
const center = new THREE.Vector3();
box.getCenter(center); // 通過將模型位置減去包圍盒中心點座標,實現幾何中心對齊原點
cloned.position.x -= center.x;
cloned.position.y -= center.y;
cloned.position.z -= center.z;
const maxDim = Math.max(size.x, size.y, size.z) || 1;
const targetSize = 2.0;
return {
centeredScene: cloned,
scale: (targetSize / maxDim) * displayScale,
};
}, [gltf, displayScale]);
// 切換模型時重置旋轉到默認角度 (當切換模型或修改初始旋轉角度時,重置模型到指定的初始姿態。)
useEffect(() => {
if (groupRef.current) {
groupRef.current.rotation.set(0, initialRotationY, 0);
}
}, [initialRotationY, gltf]);
useFrame((_, delta) => { // 每一幀更新模型的旋轉角度,實現自動旋轉效果。
if (autoRotate && groupRef.current) {
groupRef.current.rotation.y += delta * 0.25; // 每一幀增加0.25弧度的旋轉角度,實現自動旋轉效果。
}
});
return (
<group ref={groupRef} scale={scale} rotation={[0, initialRotationY, 0]}>
<primitive object={centeredScene} />
</group>
);
}
此內容由慣性聚合(RSS閱讀器)自動聚合整理,僅供閱讀參考。 原文來自 — 版權歸原作者所有。