记一次:Blender GLB 加载成功,博客首页却坏了一半

2026年07月11日0 次阅读0 人喜欢
BlenderThree.jsReact Three FiberglTFWebGL3D赛博朋克前端踩坑记录小破站建设

上一篇写完为什么不再让 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 覆盖层和文章跳转

现在的结构更像这样:

flowchart TD A["HomePageClient"] --> B["Homepage3DBanner"] B --> C["LoadedSceneBanner"] C --> D["Blender GLB:几何和基础材质"] C --> E["运行时绑定层:数据和交互"] C --> F["场景编排层:相机、灯光、HUD"] E --> G["三联屏 / 文章终端"] E --> H["书架 / 服务器 / LED"]

模型只是资产层,不是整个 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 是资产,不是应用。

现在这个房间仍然不算精细,但显示器有真实内容,书架能打开合集,服务器会显示部署状态,终端可以进入文章,昼夜也还是同一个空间。

这才比较像一个能继续住下去的博客首页。

站长

加载评论中...