记一次:Blender GLB 加载成功,博客首页却坏了一半
上一篇写完为什么不再让 AI 直接生成赛博朋克家具以后,我真的把整个房间搬进 Blender,导出了一个完整 GLB。
模型加载成功,颜色也有,第一眼看着挺像那么回事。
我以为这事差不多结束了。结果点了一下工作区才发现:显示器没内容,文章终端不能点,书架和服务器没反应,热点、镜头、自由探索也全变了。
说白了,房间还在,博客没了。

模型显示出来了,功能却没了
最早的首页房间全部由 Three.js 程序化生成。
桌子、床、书架、服务器是几何体,三联屏显示 Git commit、部署状态和最近文章,书架上的书对应博客合集,服务器抽屉可以拉出来,中央终端会轮播文章。
这些东西散在不同组件里,但最后都由顶层 Banner 编排。
换成 GLB 时,我一开始把问题想简单了,大概就是:
tsx
<Canvas>
<Suspense fallback={null}>
<primitive object={scene} />
</Suspense>
</Canvas>
这段代码没有错。
错的是我把“加载模型”当成了“恢复场景”。
GLB 只带回了几何、材质和节点层级。原来的真实数据、CanvasTexture、射线交互、相机动画、热点 UI、雨滴和昼夜主题都不会自己长回来。
我把一个应用错当成了一个模型
排查旧代码以后,我才把边界重新理清楚。
Blender 负责这些东西:
- 房间和家具的几何结构
- 材质槽和基础 PBR 参数
- 原点、尺寸和朝向
- 稳定的节点名称
- GLB 导出
Three.js 继续负责:
- 真实博客数据
- 显示器和文章终端内容
- 书架、服务器和 LED 状态
- 热点、镜头和自由探索
- 昼夜灯光、雨滴和后处理
- HTML 覆盖层和文章跳转
现在的结构更像这样:
模型只是资产层,不是整个 3D 应用。
这个结论挺朴素,但我绕了一圈才真正接受。
先把 GLB 节点变成契约
完整房间没有拆成几十个 GLB。
我保留了一个整体场景,只要求需要交互的对象拥有稳定名称:
text
monitor_left_screen
monitor_center_screen
monitor_right_screen
article_terminal_screen
book_unit_01
book_unit_02
...
server_drawer_01
server_drawer_02
...
server_led_01_01
server_led_01_02
...
模型加载后,代码先校验这些节点。
开发环境发现节点缺失就直接打印错误,不允许某个功能悄悄消失。
ts
const missingNodes = REQUIRED_CONTENT_NODES.filter(
(name) => !scene.getObjectByName(name),
);
if (missingNodes.length > 0) {
console.error('[LoadedSceneBanner] GLB node contract mismatch', {
missingNodes,
});
}
这样 Blender 和 Three.js 之间就不再靠“大家记得这个名字”维持关系,而是有了一份真正能检查的资产契约。
三联屏最坑:文字没歪,比例还是炸了
显示器是这次最折腾的地方。
我继续复用了原来的 CanvasTexture。左屏显示提交记录,中屏显示部署状态,右屏显示最近文章。
第一次贴上去以后,文字方向不对。我调整纹理方向,看起来水平了,但内容仍然巨大,标题和列表都被裁掉。
一开始我以为是 Canvas 分辨率不对。
后来才发现,真凶是 Blender 里的屏幕模型。
屏幕不是一个简单平面,而是带倒角的 Box。它的正面只占整张 UV 图集的一部分。直接把 CanvasTexture 替换到 Box 材质上,Three.js 采样到的不是完整 0–1 UV,自然会把局部内容放大。
单纯旋转纹理解决不了这个问题。
最后我保留 Blender 屏幕作为外壳,在正面动态加了一层标准 Plane:
ts
object.geometry.computeBoundingBox();
const bounds = object.geometry.boundingBox;
if (!bounds) return;
const size = bounds.getSize(new THREE.Vector3());
const geometry = new THREE.PlaneGeometry(
size.x * 0.94,
size.y * 0.92,
);
const material = new THREE.MeshBasicMaterial({
map: texture,
toneMapped: false,
side: THREE.DoubleSide,
});
const overlay = new THREE.Mesh(geometry, material);
overlay.position.set(
0,
0,
bounds.max.z + Math.max(size.z * 0.08, 0.002),
);
object.add(overlay);
这个 Plane 天生使用完整 UV。
Blender 负责屏幕外壳、边框和倒角,运行时 Plane 负责动态内容。两个职责拆开以后,三联屏和文章终端的标题、摘要、列表终于完整落在屏幕内。
这也是整次改造里最适合单独记下来的坑。
一个完整 GLB 也能做细粒度交互
之前我担心整个房间导成一个 GLB,以后就没法交互。
实际不是这样。
只要节点没有被合并,射线命中子 Mesh 后可以一路向上寻找交互根节点:
ts
function findInteractiveNode(object: THREE.Object3D) {
let current: THREE.Object3D | null = object;
while (current) {
if (/^(book_unit_|server_drawer_)/.test(current.name)) {
return current.name;
}
current = current.parent;
}
return null;
}
书本 hover 时向外抽出,点击后显示真实合集卡片。
服务器抽屉 hover 和点击时滑出,旁边的 LED 根据部署状态变成青色、粉色或琥珀色。正在部署的机器继续做呼吸动画。
文章终端的运行时 Plane 也能保留点击事件,跳转到当前轮播文章。
所以“一个场景 GLB”和“每件家具一个 GLB”不是交互能力上的二选一。
拆不拆,应该看缓存、独立加载和资产复用,而不是因为害怕射线检测不到。
夜间不是把整个房间涂黑
交互恢复以后,夜间模式又出了问题。
Blender 原始材质本来就偏深,再叠加暗背景、Bloom 和 Vignette,房间里除了灯带什么都看不见。
我之前总觉得赛博朋克就应该黑一点。现在看,黑到家具都认不出来,只剩几根青色和粉色线,那不叫氛围,叫没打灯。
这次我没有简单提高环境光,而是一起调整了几件事:
- 给夜间 PBR 材质增加语义色映射,避免接近纯黑
- 增加环境光、半球光和方向补光
- 给工作区与生活区补少量青色、粉色区域光
- 收敛 Bloom,避免发光条吞掉暗部
- 降低 Vignette,不再把四周压成黑块
- 使用 sRGB 输出和 ACES Filmic Tone Mapping
ts
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = variant === 'night' ? 1.62 : 1.05;
日间和夜间仍然是同一个 GLB,只替换材质语义、灯光和后处理。

我把静态降级删了
这次还有一个和旧文章结论相反的决定。
之前为了处理持续高负载,我做过性能分档、低 DPR 和静态 Banner 降级。那套策略在当时有意义,因为程序化场景一直跑着大量动画和后处理。
换成当前 GLB 后,实际加载和运行已经不会明显卡顿,但肉眼能看到锯齿。
如果继续为了一个已经不存在的性能问题降低质量,就有点本末倒置了。
所以我删除了首页静态场景降级,Canvas 保持:
tsx
<Canvas
dpr={[1, 3]}
gl={{
antialias: true,
alpha: false,
powerPreference: 'high-performance',
}}
>
后处理也启用了 8 倍多重采样:
tsx
<EffectComposer multisampling={8}>
<Bloom />
<Vignette />
</EffectComposer>
prefers-reduced-motion 仍然有效,但它只控制雨滴、视差和镜头动画,不再降低静态画面的清晰度。
性能优化不是一个只能不断做减法的过程。
场景结构变了,旧策略也应该重新验证。
昼夜预览也要能重现
改日间预览图时,我还遇到一个很现实的问题:原来的 PNG 根本没有进 Git。
所以没法 git restore。
最后我补了一段 Blender 渲染脚本。它先从当前 Blend 文件渲染夜间,再只在内存里替换日间材质、世界光和补光,渲染日间图片,但不把临时日间状态保存回源文件。
现在这两张图不再是“某次手动调出来的结果”,而是可以重新生成的产物。
这件事不复杂,但比在文件名后面不断加 final-v2-new 靠谱多了。
最后
上一篇文章里我写,自己真正需要的不是一个模型,而是一套可以被程序理解的资产。
这次算是把这句话真的做完了。
Blender 解决了程序化家具像积木的问题,但它不会替我保存博客逻辑。Three.js 也不是把 GLB 显示出来就算完成,它还要把数据、交互和页面重新绑回模型。
GLB 是资产,不是应用。
现在这个房间仍然不算精细,但显示器有真实内容,书架能打开合集,服务器会显示部署状态,终端可以进入文章,昼夜也还是同一个空间。
这才比较像一个能继续住下去的博客首页。
站长