昆虫世界

September 6, 2026 · View on GitHub

简体中文 · English

CI License: MIT LINUX DO

可交互的 3D 昆虫图鉴 —— 旋转、缩放、点击标注点,认识 63 种昆虫的身体构造、生活史与生态角色。

线上:https://insect-world.pages.dev · English: https://insect-world.pages.dev/en/

羽化 —— 帝王蝶从蛹里出来,翅从皱缩的一小团撑开成全尺寸

每一只虫都是代码实时生成的,仓库里没有一个模型文件。

63 个物种覆盖 14 个目:鞘翅目 28、双翅目 5、鳞翅目 5、膜翅目 5、半翅目 4、直翅目 4,蜻蜓目、螳螂目、脉翅目、蜚蠊目各 2,䗛目、革翅目、广翅目、毛翅目各 1。

右边这只帝王蝶正在羽化:翅从皱缩的一小团撑开成全尺寸,同时从下垂抬回静止姿态。 它没有另做一套动画资产 —— 画成虫的那套代码,同样画得出卵、幼虫和蛹。 这是参数化几何做得到、而扫描来的模型做不到的事,十三个物种有完整的生活史阶段模型。

长期目标 60 种 / 14 目已达成,此后第 7 轮补上了三种日常昆虫(家蝇、淡色库蚊、德国小蠊)——读者指出图鉴里甲虫占了近半、身边最常见的虫却一种没有,这是兑现「后续会陆续添加」的公开承诺。物种史与打磨史见 docs/roadmap.mddocs/polish-plan.md,模型审计方法见 docs/model-audit-notes.md


图鉴总览 —— 63 种 / 14 个目

双主题视觉,同一个馆的日与夜:默认**「标本台」(冷灰绿档案衬板 + 青铜铭牌 + 标本标签的印刷墨色),一键切换「博物馆之夜」**(展厅黑做底、黄铜做骨,标本靠 聚光灯锥与光池从暗场里托出——金属鞘翅、薄膜虹彩、萤火虫的尾灯都在深底上起飞)。

浅色「标本台」(默认)暗色「博物馆之夜」
标本台博物馆之夜

⚠️ 关于讲解文本:63 种昆虫的总述、关键数据、生活史与冷知识由 AI 撰写, 尚未经昆虫学文献或专业人士逐条核校。当作「看着好玩、顺便认个形态」的读物, 别当作可引用的资料;发现错处欢迎开 issue。

跑起来

npm install
npm run dev          # 主站      http://localhost:5178
                     # 模型调试台 http://localhost:5178/preview.html
npm test             # 4000 多个测试
npm run build        # tsc --noEmit + vite build
npm run deploy       # 构建并发布到 Cloudflare Pages(需先 npx wrangler login)

三个辅助脚本(都不进依赖,用到时临时装):

bash scripts/make-og.sh          # 重生成分享卡 public/og.png 与图标三件套(需 ImageMagick)
node scripts/shots.mjs           # 重拍 README 截图(无头 Chromium,用法见文件头注释)
node scripts/perf-firstframe.mjs # 首帧分段计时(配合 ?perf=1 的打点通道,见 docs/perf-notes.md)

部署在 Cloudflare Pages,静态托管,无后端。public/_headers 配了缓存策略: 产物文件名带内容哈希,按一年不可变缓存;HTML 每次回源校验,保证发新版立刻生效。

根路径 / 另外挂了一个 Cloudflare Pages Function(functions/index.ts):按 Accept-Language 把没有中文语言标签的访客 302 到 /en/(爬虫与已有语言选择 cookie 的访客不动,响应带 Vary: Accept-Language)。functions/ 目录在仓库根、 不在 dist/ 里,但不需要改上面这条 deploy 命令——wrangler pages deploy 的 functions 目录默认是「执行命令那一刻的当前工作目录 + functions」,跟被部署 的目录参数(这里是 dist)无关(读的是 wrangler 4.x 源码里 deploy2()functionsDirectory = customFunctionsDirectory || path.join(process.cwd(), "functions"), 不是猜的)。npm run deploy 就是在仓库根跑的,会自动发现并打包进部署。本地可以用 npx wrangler pages dev dist 起一份服务,用不同 Accept-Language/Cookiecurl 根路径来复验这条链路。

怎么用

左栏点选,或用 逐只翻。拖动转动虫体,滚轮拉近,点彩色圆点看部位说明。

左侧工具条:聚焦把镜头凑到当前标注的部位上,剖切沿矢状面切开,分层让外骨骼半透明,对比在展台底部浮出对照条。

右栏的读它的图鉴详解是分步讲解,每翻一步镜头会自己移到讲到的部位上;小测是真能作答的,判对错、给解释、统计得分。

分步讲解 —— 讲到哪一步,镜头就移到哪个部位

对比条 —— 展台底部的内联对照

它们是活的

站定的虫在做极小幅度的姿势微调(约 1.7°,六条腿按着生位置错开相位)。 本来就会悬停的八只——蜻蜓、豆娘、蜜蜂、熊蜂、食蚜蝇、草蛉、天蛾、大蚊—— 在原地振翅;蜻蜓目的前后翅反相扑动,那是它们能悬停的原因,也是一眼认出 蜻蜓的动态特征。

拍翅频率是压过的,而且是明摆着的取舍:蜜蜂真实 230Hz、食蚜蝇 200Hz, 而 60fps 屏幕的奈奎斯特上限是 30Hz —— 照实数驱动的结果不是「快」是混叠, 翅膀会看起来慢慢倒着扇。用对数压到 4~12Hz,保住物种之间的相对次序; 真实频率留在代码的数据表里,压缩函数写在旁边,不藏。

没做完整步态,理由也写在这儿:展台是转台、虫子居中,腿在迈而身体不位移, 读出来不是「走路」是踏步机。悬停没有这个问题——悬停本来就是原地不动的 真实行为。

一生

底部的生活史卡片点开,展台会跟着弹窗逐步换成卵、幼虫、蛹的立体标本。 目前做了十三种,35 个阶段模型

完全变态招牌
独角仙乳白的蛴螬 → 已长出角雏形的蛹 → 黑亮的成虫,反差最大的一条
帝王蝶三色横带的毛虫、金斑绿蛹(垂蛹,尾钩倒挂)
柞蚕蛾蚕宝宝 + 纵向半剖的茧,能看见褐色的蛹躺在里面
西方蜜蜂全程在六角巢房里;蛹的复眼先于躯干显色
中华黄萤卵、幼虫、蛹全都发光;幼虫的叠瓦状背板
神农洁蜣螂卵与幼虫都在育儿粪梨里;与独角仙的蛴螬形态刻意分开
七星瓢虫一簇竖立的卵 → 石板蓝黑带橙斑与疣突的幼虫 → 裸蛹尾端还挂着蜕下的幼虫皮
日本弓背蚁全程在巢里;茧的侧壁开一扇窗,看得见里面蜷着的蛹
星天牛卵嵌在树皮的人字刻槽里;幼虫笔直、前胸背板宽大如盾;蛹的长触角沿体侧盘了一圈
不完全变态(对照)招牌
黑蚱蝉知了猴:开掘前足 + 背上一对翅芽
碧伟蜓水虿:可折叠的面罩状下唇 + 四片翅芽
中华大刀螳卵鞘挂在枝上,背脊一条孵化带;若虫是缩小版成虫 + 短翅芽
东亚飞蝗卵囊竖在土里,上段是泡沫塞;蝻的翅芽是倒转的(前后位置与成虫相反)

若虫背上那对(或四片)翅芽就是「翅还停在芽的阶段」的可视证据 —— 完全变态与 不完全变态的区别,正是中小学讲昆虫的第一课,也是这个功能存在的理由。

独角仙的一生 —— 土里的卵、白色 C 形蛴螬、带角雏形的褐蛹、成虫

四格都是同一个相机、同一个取景框裁出来的,所以「卵是一小撮、蛴螬是一大条」的体型差是模型自己的,不是排版凑的。这张图由 scripts/make-lifecycle-strip.mjs 从线上同一份代码渲出来。

阶段之间不做连续形变:毛虫变蝴蝶在生物学上本来就不是连续形变(组织解离 + 成虫盘发育),硬 morph 出来好看但错。唯一做成连续的是羽化展翅——因为它 本来就是连续的:刚出蛹的翅是一小团皱缩的软组织,靠血淋巴撑开再硬化。

(本页顶部那只正在羽化的帝王蝶就是这一段。)

它是实录,不是逐帧手调:scripts/make-emerge-gif.mjs 打开线上的生活史、翻到蛹、再翻一步,把浏览器合成器推出来的帧收下来。曲线是 1-(1-u)³——先快后慢,血淋巴一开始压力最大;翅在撑开的同时从下垂逐渐抬回静止姿态。

一个必须先讲清楚的决定:模型是代码生成的

参考站每个器官是一份约 3.2 MB 的 GLB 资产(/models/heart.glb 等),属于采购或扫描来的素材。昆虫没有对应的现成资产,所以这里换了条路:每个物种的几何体由 TypeScript 在浏览器里实时生成,一行外部模型文件都没有。

这不只是妥协。昆虫的形态比内脏规律得多 —— 体分头/胸/腹三段,胸部生三对足、两对翅,附肢是分节的锥管,翅面由放射状翅脉支撑。这些结构参数化以后,加一个物种只需要写一份尺寸与配色的描述,而不是再买一个模型。代价是写实度不如三维扫描,换来的是零资产依赖、任意可扩展,以及每处形态都能在代码里追溯到形态学依据。

单个物种 1.3 万3.6 万三角面(最小是淡色库蚊 13,308,最大是柞蚕蛾 35,732),在浏览器里构建耗时 3090 毫秒 —— 但第一只要 230~730 毫秒,因为程序化表面贴图是全库共享的、由它一次性生成(实测与首帧分段账见 docs/perf-notes.md)。生产构建首屏 JS gzip 约 424 KB(three 主导的 vendor 340 KB + 主包 84 KB),桌面后期管线 103 KB 为懒加载独立 chunk,手机不下载,物种代码按需分包,点到谁才下载谁。

借来的与自写的

学自参考站的是信息架构:三栏工作台(图鉴列表 / 3D 展台 / 详情面板)、模型上的彩色 标注点、底部扩展卡片、顶栏五个入口,以及剖切与对比这两件功能的设想 —— 这一层不藏着。 具体实现全部为自写。

自己定的是一条交互原则:每个控件都得有真实响应,不留只有样子的按钮。逐项落到:

控件这里的行为
顶栏导航与搜索五个入口都有激活态;搜索实时过滤下拉,总览按目分组
工具条 Isolate / Layers / Zoom / Reset镜头聚焦部位 / 外骨骼半透明 / 拉近一档 / 复位
剖切与对比裁剪平面真的切开模型;展台底部浮出对比条,另可换对照物种
Quiz 小测真能作答:判对错、给解释、统计得分
讲解弹窗分 3~4 步,每步把 3D 镜头移到讲到的部位

视觉的来历,说清楚:v1 的配色与版式是照着参考站实测取值复刻的(奶油纸底 #f7f0e7 + 珊瑚 #eb7c6b + Cormorant Garamond,讲解弹窗连宽度圆角都照抄)。 仓库转公开时这些借来的数值已全部替换为自研的两套主题:暗色「博物馆之夜」 (#131110 展厅黑 + #b08d57 黄铜)与浅色「标本台」(#e9ebe4 档案衬板 + #7d6128 青铜 + 砖红/靛蓝/苔绿/赭黄四色标签墨),排印换成 Playfair Display + Noto Serif SC,弹窗尺寸也换成自己的一套。

结构

src/
  three/
    builders/
      kit.ts              建模工具箱 —— 所有物种的地基
      surface.ts          程序生成表面微观贴图(刻点/纵沟/微颗粒,运行时 CanvasTexture)
      eyes.ts             复眼六边形小眼面法线贴图
      venation.ts         参数化翅脉网(纵脉+渐密横脉围出真翅室)
      <id>.ts             每个物种一个文件,导出 build<Name>(): InsectModel
      stages/<id>-<阶段>.ts  卵/幼虫/蛹/若虫,一个文件一个阶段
    registry.ts           按 id 动态加载物种模块(Vite 代码分割 + LRU 显存回收)
    InsectCanvas.tsx      3D 展台:工作室光照、按需渲染、触角微动、背面圆点淡出
    PostFX.tsx            桌面后期(N8AO + Bloom + 链尾 ACES),懒加载独立 chunk
    stages.ts             生活史阶段的注册表(与物种注册表分开,懒加载边界设在这里)
    motion/               动作层:纯函数,只写关节 rotation、不碰几何
      types.ts              Motion 契约、进出场的幅度权重、真实拍翅频率→屏幕频率的压缩
      idle.ts               静息微动(全体)
      hover.ts              悬停振翅(8 只)与各自的真实拍翅频率
      emerge.ts             羽化展翅(唯一一段连续形变)
  components/
    TopBar / LibraryPanel / Stage / DetailPanel / BottomCards   三栏工作台
    Discovery.tsx         讲解弹窗(讲解 / 动态演示 / 小测 / 栖境 / 生活史 五个变体)
    CompareBar.tsx        展台底部的内联对比条
    Gallery.tsx           按目分组的全部物种总览
    InsectGlyph.tsx       63 个手写 SVG 剪影
  data/
    types.ts              数据契约
    insects.zh.ts / insects.en.ts   63 种昆虫的图鉴数据(中英各一份)
    guides.zh.ts / guides.en.ts     63 种的分步讲解与测验(中英各一份)
  preview.tsx             模型调试台(/preview.html)

kit.ts 提供什么

类别API
放样核心loft spindle segmentedAbdomen(默认节间凹槽)segmentedAbdomenMembranes(节间软膜环)
附肢leg legPair antenna antennaPair
wing wingPair wingGeometry wingVeins
头部器官compoundEye compoundEyePair ocelli mandibles rostrum
材质chitin(surface 刻点/纵沟/绒面、translucent 半透)elytra(iridescent 虹彩)membrane(薄膜虹彩)
收尾finalize boundingRadius mirrorZ

loft 是全部几何的地基:给一串椭圆截面,沿路径放样成封闭实体。它对退化输入(重合点、零半径、竖直路径)有专门的测试,因为这些情况一旦产生 NaN,整个模型会静默变成空白。

物种文件只依赖 kit(与 surface/eyes/venation 三个工具模块),彼此不依赖 —— 63 个物种是七轮多 agent 并行写出来的。

坐标与数据的约定

模型局部坐标:+X 向前(头部方向),+Y 向上(背方),+Z 向右,单位 1 = 1 厘米真实体长。

finalize() 把模型居中并同步平移锚点,返回 { group, anchors, radius }radius 供相机自动取景 —— 竹节虫 10 cm 和瓢虫 0.7 cm 差着 20 倍,靠它归一化。

anchors 的 key 是建模层与数据层之间唯一的耦合点:insects.*.ts 里每个 hotspot.anchorguides.*.ts 里每个 LessonStep.anchor,都必须能在对应物种的 anchors 里找到,否则那个标注点会静默消失 —— 页面不报错、两边各自的测试也都是绿的。three/__tests__/integration.test.ts 专门钉住了这层接缝。

加一个物种要做什么

注册表用 import.meta.glob('./builders/*.ts') 扫描目录,文件名直接当 id,导出的 buildXxx()build 前缀找。所以放一个新文件进去就自动注册,不必回来改任何现有代码。

但内容还是要一份份写,四处的 id 必须一致:

写什么放哪分量
3D 几何生成代码src/three/builders/<id>.ts200–400 行,最费事
图鉴数据(学名、6 条关键数据、5–6 个标注点、生态、冷知识…)src/data/insects.zh.ts + insects.en.ts各一条记录
讲解与测验(3–4 步 + 2 道题)src/data/guides.zh.ts + `guides.en.ts$各一条记录
24 \times 24 剪影图标$src/components/InsectGlyph.tsx`一个小组件

漏了会被测试拦下:数据里有记录却没有 builder 文件 → 集成测试失败;某个 anchor 在模型上找不到 → 接缝测试失败。

踩过的坑

几个只有实际跑起来才会撞上、且都不会被类型检查或 NaN 检查抓到的问题:

<Environment> 会挂起整棵子树。 drei 的 Environment 即使只用内联 Lightformer(不加载外部 HDR)也可能 suspend。它和模型、灯光共处一个 Suspense 边界时,表现是:canvas 尺寸正常、WebGL 上下文正常、控制台无报错、画面全空。必须给它自己的 <Suspense>

内联回调会造成无限重载。 onLoaded 这类回调若是父组件每次渲染新建的闭包,又进了加载 effect 的依赖数组,就会形成「加载完 → 通知父组件 → 父组件重渲染 → 新回调 → 重新加载」的死循环,同样表现为 3D 区永远空白。回调要放进 ref 再用。

legPair 曾经不是镜像。 原写法是「把 base.z 取负,再翻 scale.z」。但 leg() 内部算出的腿节方向 z 分量恒为正、不随 base.z 变号,于是左腿的基节被翻回右侧,整条腿从右侧根部斜穿过身体中线。三个物种作者独立报告了这个现象。几何完全合法、没有 NaN,只是长错了地方 —— 这类问题只能靠专门的对称性断言抓住(__tests__/mirror.test.ts)。

wing()spread 语义与直觉相反,且文档自己也错过一回。 实测是 180 = 向本侧完全展开、90 = 沿体轴向前、0 = 横穿身体伸向对侧。早期文档写成「0 = 侧展」,而当时的测试只比对了展开跨度 —— 0 与 180 的跨度一模一样、只是方向相反,于是错误文档被绿灯测试背书,先后坑了两位物种作者。现在 mirror.test.ts 连方向一起钉住了,别顺手「修正」实现。

3D 只在浏览器窗口「可见」时才会渲染,截图因此一度做不出来。 隐藏标签页没有 requestAnimationFrame,r3f 的 Canvas 量不到容器尺寸就永远不挂载子树——症状是 canvas 在、无报错、加载转圈永不消失,而 DOM 外壳照常渲染,于是截图工具拍到的 是一张「正在生成…」。判据一行:document.visibilityState !== 'visible' 或 600ms 内 rAF 计数为 0,就别再排查渲染代码了。出图改走无头 Chromium(scripts/shots.mjs), 那里 visibilityState 恒为 visible,顺带让 README 截图变成可复现的一条命令。

elytra() 的清漆层调太高会整片过曝。 原值 0.85 配 Environment 的面光源,正对光的角度会把固有色和隆起的体积感一起吃掉 —— 深栗褐的独角仙从正面看像两个白球。压到 0.55 才对。

wingVeins() 的翅脉半径是硬编码的绝对值。 在 3 单位以上的大翅上细到看不见,帝王蝶因此一度整片糊成均匀褐色。大翅要写按翅宽缩放的局部翅脉。

标注点必须挂进旋转的那个 group 里。 anchors 是模型的局部坐标。如果把 <Html> 热点当作兄弟节点摆在场景里,它们会被当成世界坐标固定在空中 —— 静止时看着完全正常、位置分毫不差,一开自动旋转就露馅:虫在转,点不动。修法是让热点成为旋转 group 的 children。

连带还有一处:自转角度是累加的,所以「聚焦到某个部位」时不能直接拿局部锚点当目标,必须先套上 group 的世界矩阵,否则自动旋转开着时镜头会对到空处去。聚焦期间也顺手停掉自转 —— 镜头锁死在一点而虫还在转,那个部位会自己溜走。

测试全绿不等于长得像。 几何合法性、包围盒比例、面数预算都能自动验,但「这只虫看起来像不像萤火虫」只能用眼睛看。/preview.html 就是为此存在的:单物种放大、线框、网格地面、面数与锚点统计。萤火虫的前胸盾片一度被做成两个大椭球(看着像两只虫黏在一起),而所有测试都是绿的。

测试

npm test

50 多个文件、4000 多个测试。下表是一次快照,示意各层的分量(逐项数字随迭代漂移,以 npm test 实际输出为准):

文件数量管什么
data/__tests__/guides.test.ts1074讲解与测验的结构、字数、anchor 逐物种校验
data/__tests__/insects.test.ts952图鉴数据契约、anchor 白名单、trivia 不得复述 summary
data/__tests__/parity.test.ts632中英两版逐字段对齐(学名一致、条目不缺不多)
data/__tests__/lengths.en.test.ts252英文文案长度闸门 —— 并行翻译的验收线
components/__tests__/glyph.test.tsx19263 个剪影的结构与坐标越界
`three/tests/integration.test.ts$192建模层 \times 数据层的接缝
$tests/layers.test.ts`68三层齐备性 + 展示文案字符白名单
three/__tests__/anchors-have-geometry.test.ts64每个 anchor 落点上真有几何体
builders/__tests__/kit.test.ts30放样地基与退化输入
builders/__tests__/*.test.ts(其余 31 个)602各物种形态断言、表面材质落位、翅脉/节间膜/触角钩子普查
builders/__tests__/mirror.test.ts12成对附肢对称性、wingspread 语义与方向
其余零散文件219搜索、滚动、页脚、对比度、免责声明等

形态类断言写的时候有个自检标准:把代码改回出问题的版本,这条断言会失败吗? 不会就等于没写。

「后足腿节最粗处 ≥ 腿节长的 1/3」这类边界值断言尤其容易写成恰好擦边通过而实际无效 —— 蝗虫那条最初就是 0.333 压线过,看着是绿的其实什么也没管住。后来改成量取真实网格的最大半径,外加一条「两端不许收细到峰值的 28% 以下」,才真正拦得住。

但这条自检标准仍有它管不到的地方,而且是最贵的一块:断言量的是数字,人看的是长相,两者可以毫无关系。 兰花螳螂的花瓣状腿节「宽度 ≥ 厚度的 3.5 倍」测出来是 5.75,绿的;渲染出来却是几片侧立的薄板, 整只虫像一只苍白的虾 —— 因为决定朝向的那个四元数只约束了长度轴,绕它的滚转是随意的, 扁平面正好侧对着镜头,而宽厚比这个数字丝毫不受影响。 同理,白蚁兵蚁的两颚在世界坐标里分得很开,但默认机位的视线方向恰好把这个分离压扁, 屏幕上糊成一根独角。

能钉住这类问题的断言,必须量到用户真正看见的那个量: 兰花螳螂改成断言花瓣的三个维度里有两个远大于第三个(一块板只有一个); 白蚁兵蚁改成把两颚顶点投影到默认机位的成像平面上,沿颚长切 20 段, 断言其中有连续一长段两者的投影包围盒不相交。 写形态断言前值得先问一句:这个数字,和我要看的那件事是同一件事吗?

这一类「测试查不出长相」的完整审计方法 —— 出图规格、扫背面剔除的剪影差集、 定位像素归属的 ID 图 —— 沉淀在 docs/model-audit-notes.md

已知不足

  • 写实度上限仍低于三维扫描 —— 表面微观(刻点/沟纹/绒面/蜂窝复眼)已由程序贴图补足,但没有手绘贴图与真实绒毛物理;换来的是零资产文件与全库风格统一。
  • 观察笔记只存在浏览器本地(localStorage),换设备或清缓存就没了; 头像菜单里的「复制为 Markdown」是目前唯一的带走方式。
  • 剖切与分层是通用实现(裁剪平面 / 降低外骨骼不透明度),没有按物种定制解剖层次。
  • 生活史只做了八种,其余 55 种的「生活史」卡片仍只有文字。羽化展翅也只对 有翅骨架的物种成立 —— 甲虫的鞘翅是逐只自写的,这段动画对它们空转。
  • 卵是最难的一个阶段,因为它本身没有结构。做得成立的几颗靠的都是可指认的 表面结构(帝王蝶的辐辏纵棱)或真实的语境(蜂巢六角房、育儿粪梨、 水草茎里的产卵刻痕);没有这两样的那几颗只能算过线。
  • 拍翅频率是压过的,见上文「它们是活的」。相对次序是真的,绝对值不是; 真实频率与压缩函数都在代码里写明,别拿屏幕上数出来的次数当数据。
  • 部分物种的近缘种列表是按常见中文名写的,未逐条核对分类学文献。
  • 英文版(/en/)的全部内容是 AI 从中文翻译的,未经母语者校订,也未请昆虫学者 复核英文术语与常用名。学名(拉丁二名法)两版一致,那是唯一可靠的对照锚点。

致谢与许可

信息架构学自 Anatomy Atelier(源码在 thebuggeddev/anatomy)—— 三栏工作台、模型上的 彩色标注点、底部扩展卡片、引导讲解的位置,以及一部分交互手感,都来自它。本项目不含它的 任何代码,全部为自写实现。感谢它把这个交互形态做了出来。

本仓库的 3D 建模代码、图鉴文案、讲解与测验内容、两套视觉主题及全部实现均为原创, 以 MIT 许可发布,见 LICENSE

⚠️ 再说一次:图鉴文案由 AI 撰写,未经专业核校,不适合作为学术或教学引用来源。