慣性聚合 高效追蹤和閱讀你感興趣的部落格、新聞、科技資訊
閱讀原文 在慣性聚合中打開

推薦訂閱源

博客园 - 司徒正美
V
V2EX
T
Tailwind CSS Blog
有赞技术团队
有赞技术团队
aimingoo的专栏
aimingoo的专栏
Apple Machine Learning Research
Apple Machine Learning Research
IT之家
IT之家
Blog — PlanetScale
Blog — PlanetScale
A
About on SuperTechFans
月光博客
月光博客
T
The Blog of Author Tim Ferriss
宝玉的分享
宝玉的分享
Martin Fowler
Martin Fowler
博客园 - 聂微东
The GitHub Blog
The GitHub Blog
V
Visual Studio Blog
WordPress大学
WordPress大学
酷 壳 – CoolShell
酷 壳 – CoolShell
Engineering at Meta
Engineering at Meta
GbyAI
GbyAI

博客园 - zhang-yd

今日开源[第12期]LiteParse 今日开源[第11期]OmniVoice-Studio 今日开源[第10期]ds4(DwarfStar) 今日开源[第9期]graphify 今日开源[第8期]open-notebook 今日开源[第7期]spec-kit 今日开源[第6期]Production Agentic RAG Course 今日开源[第5期]Headroom 今日开源[第4期]OpenTalking 今日开源[第3期]train-llm-from-scratch 今日开源[第2期]Project N.O.M.A.D. 今日开源[第1期]MoneyPrinterTurbo 论文解读-《It Takes a Graph to Know a Graph Rewiring for Homophily with a Reference Graph》 论文解读-《Mitigating Over-Squashing in Graph Neural Networks by Spectrum-Preserving Sparsification》 论文解读-《Make Heterophily Graphs Better Fit GNN A Graph Rewiring Approach》 论文解读-《Temporal Graph Rewiring with Expander Graphs 》 论文解读-《Understanding Oversquashing in GNNs through the Lens of Effective Resistance》 论文解读-《Homophily-oriented Heterogeneous Graph Rewiring》 论文-Deep appearance modeling: A survey 代码阅读笔记-nanoclaw 代码阅读笔记-OpenManus 论文解读-《An Empirical Evaluation of Rewiring Approaches in Graph Neural Networks》 论文解读-《Probabilistic Graph Rewiring via Virtual Nodes》 论文解读-《Probabilistically Rewired Message-Passing Neural Networks》 论文解读-《Joint Graph Rewiring and Feature Denoising via Spectral Resonance》 代码阅读笔记-nanobot 论文解读-《Oversquashing in GNNs through the lens of information contraction and graph expansion》 论文解读-《GNNs Getting ComFy Community and Feature Similarity Guided Rewiring》 - zhang-yd 论文解读-《PANDA Expanded Width-Aware Message Passing Beyond Rewiring》 代码阅读笔记-AiPyApp 论文解读-《Deep Graph Contrastive Representation Learning》 论文解读-《Community-Invariant Graph Contrastive Learning》 论文解读-《DiffWire Inductive Graph Rewiring via the Lovász Bound》 论文解读-《The Effectiveness of Curvature-Based Rewiring and the Role of Hyperparameters in GNNs Revisited》 论文解读-《Over-Squashing in GNNs and Causal Inference of Rewiring Strategies》 论文解读-《Uncertainty-Aware Graph Structure Learning》
LearningCell代碼解讀
zhang-yd · 2026-05-24 · via 博客园 - zhang-yd

LearningCell代碼解讀

項目介紹

最近,GitHub上有一個很火的項目,一個基於 3D 模型的細胞學習平臺。該平臺的目標是幫助用戶通過 3D 模型學習細胞的基本概念和功能。

代碼地址為:LearningCell

項目代碼很短很少,非常簡單,隨著3d高斯潑濺效果的不斷完善,3D-WEB也快迎來了新的應用和突破。

項目的技術棧如下:

  • 前端:React + TypeScript + Vite
  • 3D 模型:GLTF 格式
  • 3D 模型加載:Draco 壓縮

目錄結構

.
├── .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>
  );
}