惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Recent Announcements
Recent Announcements
雷峰网
雷峰网
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Hugging Face - Blog
Hugging Face - Blog
博客园 - 司徒正美
人人都是产品经理
人人都是产品经理
博客园 - 【当耐特】
量子位
有赞技术团队
有赞技术团队
博客园 - 三生石上(FineUI控件)
博客园 - Franky
M
MIT News - Artificial intelligence
U
Unit 42
Last Week in AI
Last Week in AI
酷 壳 – CoolShell
酷 壳 – CoolShell
The Cloudflare Blog
J
Java Code Geeks
V
Visual Studio Blog
Engineering at Meta
Engineering at Meta
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
MyScale Blog
MyScale Blog
T
Tailwind CSS Blog
T
The Blog of Author Tim Ferriss
V
V2EX

博客园 - zhang-yd

今日开源[第57期]beautiful-water源码解读 今日开源[第56期]human-atlas源码解读 今日开源[第55期]model-x-studio源码解读 今日开源[第54期]Spark(@sparkjsdev/spark)源码解读 今日开源[第53期]PlayCanvas Engine(playcanvas/engine)源码解读 今日开源[第52期]SPEEDBALL GI WebGPU Showcase(speedball-gi)源码解读 今日开源[第51期]Pi Agent Harness(pi)源码解读 今日开源[第50期]World Monitor(worldmonitor)游戏项目解读 今日开源[第49期]竹知了(zhuzhiliao)小玩具项目解读 今日开源[第48期]Operation Ironhold 游戏项目解读 今日开源[第47期]Academic Research Skills for Claude Code(ARS)项目skill解读 今日开源[第46期]AI-Research-SKILLs项目skill解读 今日开源[第45期]nature-research-skills项目skill解读 今日开源[第44期]findskills项目skill解读 今日开源[第43期]last30days-skill 今日开源[第42期]Cangjie Skill 今日开源[第41期]MoneyPrinterTurbo 今日开源[第39期]img2threejs 今日开源[第39期]Kronos 今日开源[第38期]Open Code Review (OCR) 今日开源[第37期]Openship 今日开源[第36期]croc 今日开源[第35期] Code-Review-Graph 今日开源[第34期] 《深入理解 AI Agent:设计原理与工程实践》 今日开源[第33期] Home Assistant Core 今日开源[第32期] Vibe-Trading 今日开源[第31期]RuView 今日开源[第30期]Chrome DevTools MCP 今日开源[第29期]RomM (ROM Manager) 今日开源[第28期]Page Agent
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>
  );
}