前端工程师的 3D 第一课:一个模型如何进入网页

我是一个有前端开发经验、但几乎没有 3D 建模经验的学习者。我的目标是逐步做出能在网页里展示的货架和商品模型。开始之前,我首先需要回答:模型在哪里制作,以什么格式交付,又由谁把它画到屏幕上?

这篇记录的是起点:使用现成的第三方模型,完成从 glTF 资产到网页显示的一次验证。它还不是从 Blender 建模、导出到 Web 展示的完整闭环,也不涉及角色建模或动画制作。

一、先分清:制作工具、交付格式、渲染引擎

如果只用三句话向外行解释我选择的这条路线:

  1. Blender 像制作车间,用来创建和编辑模型,并把工作工程保存为 .blend
  2. glTF / GLB 像标准交付包,把模型的形状、材质等运行时数据交给其他软件使用。
  3. Three.js 像网页里的展示系统,通过加载器读取交付包,再根据相机和渲染设置把模型画出来。
角色 解决的问题 不要混淆的地方
Blender 模型怎么制作、编辑和导出 .blend 是工程保存格式,不是本文要交付给网页的格式
glTF / GLB 3D 资产如何存储、传输和跨工具使用 它是格式,不是建模软件,也不会自己渲染
Three.js 如何在网页中组织和渲染 3D 场景 它是 JavaScript 3D 库,不是只能读取某一种模型的播放器

我最初把这三者理解成"缺一不可",这个说法太绝对了。模型可以由其他建模软件制作,也可以通过代码生成;Three.js 还能借助不同加载器读取其他格式。这里重要的是理解"制作 → 交付 → 展示"的职责分工,而不是认为三个具体名字无法替换。

.blend.gltf.glb 到底有什么区别?

格式 可以怎样理解 常见内容或组织方式
.blend Blender 的可编辑工程 场景、对象、网格、材质、修改器等工作数据
.gltf glTF 的 JSON 文件 描述场景与资源关系,常搭配外部 .bin 和贴图,也可以内嵌数据
.glb glTF 的二进制容器 通常把 JSON、二进制数据和贴图打包在一个文件里,方便分发

.blend 不能直接交给本文使用的 GLTFLoader,因为它不是 glTF 数据;Three.js 也没有内置的 .blend 加载器。正确流程是在 Blender 中保存工程,再导出 glTF / GLB,不是修改文件扩展名。

导出也不意味着把 Blender 的全部能力原样搬进网页:目标格式不支持的修改器、材质节点等内容,需要按导出器规则转换、应用或烘焙。

还有两个容易混淆的点:GLB 不等于压缩后的 glTF,打包和压缩是两件事;虽然 GLB 常用于单文件交付,但格式仍允许引用外部资源,是否真正自包含要看具体文件。

二、把资产管线画对

我最初画图时,把 Blender 指向 glTF 的箭头标成了 .blend。这样容易让人误以为 .blend 就是直接传给网页的文件。更准确的图应该把"保存"和"导出"分开:

text 复制代码
Blender 中制作模型
    ├── 保存工程 ──→ model.blend(保留编辑能力)
    └── 导出资产 ──→ model.gltf + model.bin + 贴图
                    或通常自包含的 model.glb
                              ↓
                  资产校验(按需进行压缩)
                              ↓
                  Web 服务提供资产访问地址
                              ↓
                  GLTFLoader 加载并解析
                              ↓
                  把模型对象加入 Three.js 场景
                              ↓
                  相机 + 渲染器 → 网页画面

压缩与校验是后续要补齐的环节,本次没有做。我的实际起点是下载已有的 glTF 资产,然后验证图中从 Web 加载到渲染的部分。

三、Network 面板里,哪些才是模型资源?

我打开 Three.js 的 glTF 示例,在 Network 面板里记下了 HTML、JavaScript、贴图、model-index.json、GLB 和一些 blob: 地址。当时只是记下文件名,并没有真正分清它们的职责。

回头整理时,我把这些记录分成三类:

类别 资源 如何理解
页面与界面 HTML、CSS、页面脚本、字体 负责页面布局、交互和文字,不等于模型资产
引擎与工具 Three.js、GLTFLoaderOrbitControls 及辅助模块 分别提供渲染能力、模型解析和视角控制;它们也不是模型数据
资产与索引 .gltf / .glb、可能存在的 .bin、贴图、模型索引 需要进一步查看内容和引用关系,不能仅靠扩展名猜用途

三个需要纠正的判断

第一,model-index.json 不是 glTF 模型本体。 我看到的文件来自 Khronos 示例资产库的模型索引,其中列出模型名称、缩略图路径及不同格式版本的文件名。它更像目录;真正的资产要沿着其中的路径继续找。

第二,贴图不是 UV 坐标。 贴图通常是一张供材质采样的图片;UV 是网格上的二维坐标,告诉渲染器"模型表面的这个位置,应该取图片的哪个位置"。UV 数据通常存放在模型的顶点属性中,而不是一张独立的"UV 图片"。PNG 也可能是缩略图、颜色贴图或其他用途,需要结合模型引用判断。

第三,blob: 不是一种 3D 文件格式。 它是浏览器为 Blob 等对象创建的对象 URL。例如,加载器可以为 GLB 内嵌的图片创建这样的地址;其他页面功能也可能使用它。看到 blob: 不代表又从服务器下载了一个同名文件,更不能直接断定它就是贴图,需要查看 Initiator 或创建它的代码。

对照自己的 Demo,资源关系就具体了

我的 Demo 使用的是拆分式 glTF,不是 GLB:

text 复制代码
stage0/
├── index.html
├── main.js
└── public/
    ├── scene.gltf
    ├── scene.bin
    ├── license.txt
    └── textures/
        └── surfaceShader1_baseColor.png

scene.gltf 的实际内容可以确认:

  • buffers[0].uri 指向 scene.bin,其中存放顶点属性、索引等二进制数据。
  • images[0].uri 指向 textures/surfaceShader1_baseColor.png;材质通过纹理引用把它作为基础颜色贴图使用。
  • 网格的 TEXCOORD_0 是第一组 UV 坐标属性,与这张颜色图片不是同一回事。

因此,本例的资产依赖链是 scene.gltf → scene.bin + 颜色贴图

四、第一次把模型加载到网页

用最少的工具验证加载过程

这次我使用普通 HTML 和 JavaScript,通过 CDN 加载 Three.js,没有引入 React 或构建工具,便于直接观察资源路径和加载过程。

演示文件位于 stage0/index.html 使用 import map,把模块名称映射到具体 URL:

html 复制代码
<script type="importmap">
  {
    "imports": {
      "three": "https://cdn.jsdelivr.net/npm/three@v0.149.0/build/three.module.js",
      "three/addons/": "https://cdn.jsdelivr.net/npm/three@v0.149.0/examples/jsm/"
    }
  }
</script>
<script type="module" src="./main.js"></script>

这里记录的是本次实际使用的 Three.js 0.149.0 ,不是当前最新版本的推荐。核心库与 addons 应使用相同版本;教程 URL 中的 <version> 是占位符,需要替换,不能原样复制。此方案还依赖浏览器支持 import map,并且能访问对应 CDN。

已有演示通过 Live Server 访问。也可以在安装 Node.js 的环境中,从项目根目录运行:

bash 复制代码
npx --yes serve . --listen 3000

然后访问 http://localhost:3000/stage0/,不要直接双击 HTML 用 file:// 打开。这里的 public 只是普通目录,不会自动从 URL 中省略。

加载器负责解析,场景负责容纳,渲染器负责绘制

Scene 用来组织对象,Camera 决定从哪里看,Renderer 根据这些信息输出画面。创建了它们,并不等于已经把模型画出来。

下面是加载部分的核心逻辑摘录,不是可单独运行的完整程序;省略了场景、相机、渲染器的初始化和后文的缩放处理:

js 复制代码
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";

const loader = new GLTFLoader();
loader.load(
  "./public/scene.gltf",
  (gltf) => {
    scene.add(gltf.scene);
  },
  undefined,
  (error) => console.error("模型加载失败:", error),
);

GLTFLoader 既能加载 .gltf,也能加载 .glb。本例中,它会根据 scene.gltf 的相对路径继续加载二进制数据和图片,不需要手动逐个请求。

五、画面看不到模型:我踩过的两个坑

坑一:模型还没加载完,就只渲染了一次

模型加载是异步的。如果在调用 loader.load() 后立刻执行一次 renderer.render(scene, camera),这一帧可能还没有模型。之后即使加载回调把模型加入场景,画面也不会自动刷新。

我的 Demo 使用持续渲染循环,因此模型加入后,后续帧就能把它画出来。核心逻辑可以简化成下面这样,其中 controls 是已经创建好的 OrbitControls 实例:

js 复制代码
function animate() {
  requestAnimationFrame(animate);
  controls.update();
  renderer.render(scene, camera);
}

animate();

但这不意味着"加载模型必须写动画循环"。如果只是静态展示,也可以在加载成功并加入场景后渲染一次,在相机或画面变化时再按需渲染。真正的原则是:场景更新后,要有新的渲染发生。

坑二:模型的尺寸和中心,与默认相机位置不匹配

当时的调试记录是:模型包围盒约为 102 × 199 × 53 个场景单位,中心偏在 y ≈ 91;相机却放在 z = 5,朝向原点。这些是初次排查时的近似记录,不是重新测量的数据,也不能直接当作模型的真实物理尺寸。

这种情况下,模型即使加载成功,也可能大部分落在视野之外,或者相机离它过近。问题不是"第三方模型一定太大",而是资产的尺度、位置与当前取景不匹配。

我实际采用的处理不是移动相机,而是把模型的最长边缩放到 3 个场景单位,再把包围盒中心移到原点。下面这段代码放在加载成功回调中,modelgltf.scene,处理后再执行 scene.add(model)

js 复制代码
// 1. 计算模型包围盒,拿到尺寸
const box = new THREE.Box3().setFromObject(model);
const size = box.getSize(new THREE.Vector3());

// 2. 按最长边缩放到目标大小(原模型将近 200 单位,太大了)
const maxDim = Math.max(size.x, size.y, size.z);
const targetSize = 3;
model.scale.setScalar(targetSize / maxDim);

// 3. 缩放后重新算中心,把模型平移到原点(否则它偏向 +Y 一大截)
const scaledBox = new THREE.Box3().setFromObject(model);
const center = scaledBox.getCenter(new THREE.Vector3());
model.position.sub(center);

这段简化处理适用于本例中包围盒非空、最大尺寸大于零、根节点初始变换为单位变换,且尚未加入带变换父节点的模型。它不是适配任意模型的通用算法。

对预览来说,"统一缩放、居中"很方便;但以后要把真实尺寸的杯子摆到货架上,就不能把每件商品都缩放到同样大小。资产的单位、尺寸和原点应该有一致约定,取景则可以通过调整相机完成。取景还与相机视角、画布宽高比和裁剪范围有关,并非改一个缩放值就一定适用于所有情况。

完成这些处理后,已有运行截图显示了带颜色的人物模型。当前代码也加入了 OrbitControls,用于旋转视角和缩放观察;这些结果证明了首次加载实践有产出,但不代表已经完成性能或跨设备验证。

六、为什么没有灯,模型也能显示颜色?

这个模型的材质声明了 KHR_materials_unlit 扩展。我最初把它记成"自发光",这里必须纠正:unlit 是不受场景光照影响的着色方式,不等同于 emissive(自发光)。

  • Unlit:主要使用基础颜色、颜色贴图等信息显示表面,不计算普通 PBR 材质那样的光照反射。因此本例没有添加灯光,也能显示颜色。
  • Emissive:材质的自发光颜色贡献,是另一种概念。在普通 Three.js 实时渲染中,设置它也不意味着物体会自动像灯一样照亮周围物体。
  • 普通 PBR 材质:需要考虑照明条件。如果既没有灯光或环境照明,也没有自发光贡献,通常会呈现黑色或难以辨认;不能简单概括为"没有显式添加灯,就一定全黑"。

所以,看见颜色不等于已经掌握材质和打光。这个模型帮助我隔离了照明问题,先验证加载流程;普通 PBR 模型的照明效果还需要另做练习。

七、模型能下载,不代表可以任意使用

本次学习使用的是 Sketchfab 上的第三方作品,并非我自己建模,也不是 CC0 资产:

本文学习演示基于 Matilda,作者为 nicolekeane,采用 CC BY-NC-SA 4.0 许可。本文演示代码对模型的处理是运行时缩放和居中,不涉及重新建模或导出资产。

下一篇:走进 Blender,练习视图操作、物体模式与编辑模式,先把一个简单物体做出来。

参考资料

相关推荐
颜进强1 小时前
04 · NestJS 依赖注入:你在 `@Module` 写的 providers,和构造参数里那个类型,是怎么"对上"的?
前端·后端·ai编程
肆仲冬1 小时前
不用框架,用 TypeScript 从零搭一个 Agent 框架
前端
林语琛1 小时前
我写的 switch…break 被 Babel 偷偷吞了
前端·javascript·babel
烈风逍遥1 小时前
第六篇:RAG 知识库构建与检索全链路
前端·人工智能·后端
爱丶不疚1 小时前
什么是 Jev 决策模型?它适合干什么?
前端·agent
江畔柳前堤2 小时前
字节跳动·大模型应用知识手册
前端·人工智能·深度学习·opencv·目标检测·重构·transformer
LEE2 小时前
前端转型全栈 03:接口失败也返回 200,OpenAPI 契约与错误码怎么定
前端·后端·全栈
我命由我123453 小时前
CSS - CSS 媒体查询 orientation
前端·javascript·css·html·css3·html5·js
lichenyang4533 小时前
让 VK 小程序调用 HarmonyOS 原生能力:一次跨端 Bridge SDK 的设计与实践
前端