Three.js加载 .obj文件 和 .gltf文件

文章目录

  • [加载 .OBJ 文件](#加载 .OBJ 文件)
    • [1. OBJ 是什么](#1. OBJ 是什么)
    • [2. 如何引入 OBJLoader](#2. 如何引入 OBJLoader)
    • [3. 基础加载 OBJ](#3. 基础加载 OBJ)
    • [4. 只加载 OBJ 可能没有正确材质](#4. 只加载 OBJ 可能没有正确材质)
    • [5. 如何引入 MTLLoader](#5. 如何引入 MTLLoader)
    • [6. 正确流程:先加载 MTL,再加载 OBJ](#6. 正确流程:先加载 MTL,再加载 OBJ)
    • [7. 设置资源路径](#7. 设置资源路径)
    • [8. 材质可能需要双面显示](#8. 材质可能需要双面显示)
    • [9. 加载进来的 root 是什么](#9. 加载进来的 root 是什么)
    • [10. 可以手动替换材质](#10. 可以手动替换材质)
    • [11. OBJ 和 glTF 的区别](#11. OBJ 和 glTF 的区别)
  • [加载 .gltf 文件](#加载 .gltf 文件)
    • [1. glTF 是什么](#1. glTF 是什么)
    • [2. glTF优点](#2. glTF优点)
    • [3. 如何引入 GLTFLoader](#3. 如何引入 GLTFLoader)
    • [4. 基础加载写法](#4. 基础加载写法)
    • [5. OBJ 加载和 glTF 加载的对比](#5. OBJ 加载和 glTF 加载的对比)
    • [6. .gltf 和 .glb 的区别](#6. .gltf 和 .glb 的区别)
    • [8. 查看 glTF 的场景图](#8. 查看 glTF 的场景图)
    • [9. 通过名字查找模型中的节点](#9. 通过名字查找模型中的节点)
    • [10. 让汽车沿路径运动](#10. 让汽车沿路径运动)
    • [11. 开启阴影](#11. 开启阴影)
    • [12. GLTFLoader 的完整常用模板](#12. GLTFLoader 的完整常用模板)
    • [14. 总结](#14. 总结)

加载 .OBJ 文件

1. OBJ 是什么

.obj 是一种比较老、但很常见的 3D 模型格式。

它主要保存模型的几何信息,比如:

  • 顶点位置
  • 顶点法线
  • UV 坐标
  • 面信息
  • 对象分组
  • 材质名称引用

.obj 通常不完整保存材质细节,所以经常会配套一个 .mtl 文件。

简单理解:

javascript 复制代码
.obj = 模型形状 / 顶点 / 面 / UV / 法线
.mtl = 材质 / 贴图 / 颜色参数

2. 如何引入 OBJLoader

在 Three.js 项目中,可以这样引入:

javascript 复制代码
import { OBJLoader } from 'three/addons/loaders/OBJLoader.js';

如果项目使用 three/examples/jsm 写法,也可以这样:

javascript 复制代码
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js';

3. 基础加载 OBJ

最基础的加载方式:

javascript 复制代码
const objLoader = new OBJLoader();

objLoader.load('resources/models/windmill/windmill.obj', (root) => {
  scene.add(root);
});

这里的 root 通常是一个 Group,里面包含多个 Mesh

可以理解成:

javascript 复制代码
OBJLoader 读取 windmill.obj
解析出模型节点
把模型根对象 root 加入 scene

4. 只加载 OBJ 可能没有正确材质

  • .obj 主要负责几何体
  • .mtl 才保存材质和贴图引用
  • .obj 文件里只是通过 mtllibusemtl 引用材质

OBJ 文件里可能有类似内容:

javascript 复制代码
mtllib windmill.mtl
usemtl windmill

意思是:

javascript 复制代码
这个 OBJ 要使用 windmill.mtl 里的 windmill 材质

但如果不加载 .mtl,Three.js 就不知道具体材质和贴图该怎么设置。

5. 如何引入 MTLLoader

为了加载 .mtl 材质文件,需要再引入 MTLLoader

javascript 复制代码
import { MTLLoader } from 'three/addons/loaders/MTLLoader.js';

当前项目里也可以这样写:

javascript 复制代码
import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader.js';

6. 正确流程:先加载 MTL,再加载 OBJ

重点流程是:

javascript 复制代码
const mtlLoader = new MTLLoader();

mtlLoader.load('resources/models/windmill/windmill.mtl', (mtl) => {
  mtl.preload();

  const objLoader = new OBJLoader();
  objLoader.setMaterials(mtl);

  objLoader.load('resources/models/windmill/windmill.obj', (root) => {
    scene.add(root);
  });
});

加载顺序很重要:

javascript 复制代码
1. 用 MTLLoader 加载 windmill.mtl
2. 调用 mtl.preload()//preload 会把 MTL 里声明的材质实例化,OBJLoader 才能按材质名匹配。
3. 创建 OBJLoader
4. objLoader.setMaterials(mtl)
5. 再用 OBJLoader 加载 windmill.obj
6. scene.add(root)

其中:

javascript 复制代码
mtl.preload()

会提前准备 .mtl 里声明的材质。

javascript 复制代码
objLoader.setMaterials(mtl)

会告诉 OBJLoader

javascript 复制代码
加载 OBJ 时,如果遇到 usemtl,就去这个 MTL 材质库里找对应材质

7. 设置资源路径

如果 OBJ、MTL、贴图在同一个目录,通常可以设置路径:

javascript 复制代码
const modelPath = 'resources/models/windmill/';

const mtlLoader = new MTLLoader();
mtlLoader.setPath(modelPath);
mtlLoader.setResourcePath(modelPath);

const objLoader = new OBJLoader();
objLoader.setPath(modelPath);

然后加载时只写文件名:

javascript 复制代码
mtlLoader.load('windmill.mtl', (mtl) => {
  mtl.preload();

  objLoader.setMaterials(mtl);

  objLoader.load('windmill.obj', (root) => {
    scene.add(root);
  });
});

这里:

javascript 复制代码
setPath()

用于设置 .mtl.obj 文件所在目录。

javascript 复制代码
setResourcePath()

用于设置材质里贴图资源的查找目录。

8. 材质可能需要双面显示

材质默认只渲染正面。

对于薄片模型,比如:

  • 风车叶片
  • 树叶
  • 纸片
  • 布片

经常需要设置双面材质:

javascript 复制代码
material.side = THREE.DoubleSide;

如果要给所有材质都设置双面:

javascript 复制代码
for (const material of Object.values(mtl.materials)) {
  material.side = THREE.DoubleSide;
}

但要注意:双面渲染比单面渲染更费性能。

9. 加载进来的 root 是什么

OBJLoader 加载出来的结果通常是:

javascript 复制代码
THREE.Group

里面可能有多个子对象:

javascript 复制代码
root
  ├─ Mesh
  ├─ Mesh
  └─ Mesh

所以如果要统一处理模型里的所有网格,通常用:

javascript 复制代码
root.traverse((child) => {
  if (child.isMesh) {
    child.castShadow = true;
    child.receiveShadow = true;
  }
});

10. 可以手动替换材质

如果 .mtl 解析结果不理想,也可以手动创建材质,然后给模型替换。

比如:

javascript 复制代码
const materials = {
  Material: new THREE.MeshPhongMaterial({ color: 'red' }),
  windmill: new THREE.MeshPhongMaterial({ color: 'white' }),
};

root.traverse((node) => {
  const material = materials[node.material?.name];

  if (material) {
    node.material = material;
  }
});

这样可以绕过 .mtl 的不兼容问题,完全由代码控制材质效果。

11. OBJ 和 glTF 的区别

OBJ 的特点:

javascript 复制代码
OBJ = 老格式,简单通用,主要保存几何
MTL = 附属材质文件
贴图 = 外部图片文件

glTF / GLB 的特点:

javascript 复制代码
glTF / GLB = 更适合实时渲染和网页传输
可以包含材质、贴图、动画、场景层级
通常更容易直接在 Three.js 中使用

加载 .gltf 文件

相比 OBJ,glTF / GLB 更适合在网页和实时 3D 应用中加载模型。它不仅能保存模型几何,还能保存材质、贴图、层级结构、动画等信息,所以在 Three.js 里通常比 OBJ 更省心。

1. glTF 是什么

glTF 全称是 GL Transmission Format,可以理解成"面向 3D 图形传输和渲染的格式"。

3D 格式大概分成几类:

  • 3D 编辑器格式:比如 .blend.max.mb
  • 交换格式:比如 .obj.dae.fbx
  • 应用程序格式:比如某些游戏内部格式
  • 传输格式:比如 .gltf / .glb

glTF 更像是专门为网页和实时渲染准备的"传输格式"。

2. glTF优点

OBJ 比较老,主要保存几何数据,材质和贴图要靠 .mtl 配合,而且经常需要手动修路径、法线贴图、双面材质等问题。

glTF 的优点是:

  • 更适合传输
  • 通常体积更小
  • 更适合直接渲染
  • 可以包含场景层级
  • 可以包含材质
  • 可以包含贴图
  • 可以包含动画
  • 更接近 Three.js 需要的数据结构

文章里说,加载 glTF 经常是:

加载后纹理、材质等就直接工作了。

3. 如何引入 GLTFLoader

在 Three.js 项目中,需要从 examples/addons 里引入 GLTFLoader

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

如果你的项目使用的是当前仓库这种写法,也可以这样:

javascript 复制代码
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';

这两种本质上都是引入 Three.js 提供的 glTF 加载器,具体路径取决于项目依赖和构建配置。

4. 基础加载写法

核心代码是:

javascript 复制代码
const gltfLoader = new GLTFLoader();
const url = 'resources/models/cartoon_lowpoly_small_city_free_pack/scene.gltf';

gltfLoader.load(url, (gltf) => {
  const root = gltf.scene;
  scene.add(root);
});

这里的 gltf 对象里常见内容有:

javascript 复制代码
gltf.scene      // 默认场景,通常直接 scene.add(gltf.scene)
gltf.scenes     // glTF 文件里的所有场景
gltf.animations // 动画剪辑数组
gltf.cameras    // 文件里带的相机
gltf.asset      // glTF 文件元信息

最常用的是:

javascript 复制代码
scene.add(gltf.scene);

5. OBJ 加载和 glTF 加载的对比

OBJ 通常要这样:

javascript 复制代码
const mtlLoader = new MTLLoader();

mtlLoader.load('windmill.mtl', (mtl) => {
  mtl.preload();

  const objLoader = new OBJLoader();
  objLoader.setMaterials(mtl);

  objLoader.load('windmill.obj', (root) => {
    scene.add(root);
  });
});

而 glTF 通常更简单:

javascript 复制代码
const gltfLoader = new GLTFLoader();

gltfLoader.load('scene.gltf', (gltf) => {
  scene.add(gltf.scene);
});

区别是:

  • OBJ:几何、材质、贴图经常分开处理
  • glTF:模型、材质、贴图、层级结构通常都在同一个资源体系里

6. .gltf 和 .glb 的区别

文章主要讲 .gltf,但实际开发中也常见 .glb

简单理解:

javascript 复制代码
.gltf = JSON 主文件 + 可能外带 .bin + 贴图
.glb  = 二进制单文件,通常把模型、材质、贴图都打包进去

比如:

javascript 复制代码
model.gltf
model.bin
texture.png

或者:

javascript 复制代码
model.glb

实际项目中,.glb 更方便部署,因为通常只有一个文件。

8. 查看 glTF 的场景图

glTF 文件通常带有完整场景层级,比如城市、道路、汽车、建筑等分组。

dumpObject 函数,把场景图打印到控制台:

javascript 复制代码
function dumpObject(obj, lines = [], isLast = true, prefix = '') {
  const localPrefix = isLast ? '└─' : '├─';

  lines.push(
    `${prefix}${prefix ? localPrefix : ''}${obj.name || '*no-name*'} [${obj.type}]`
  );

  const newPrefix = prefix + (isLast ? '  ' : '│ ');
  const lastNdx = obj.children.length - 1;

  obj.children.forEach((child, ndx) => {
    const isLast = ndx === lastNdx;
    dumpObject(child, lines, isLast, newPrefix);
  });

  return lines;
}

加载后调用:

javascript 复制代码
gltfLoader.load('scene.gltf', (gltf) => {
  const root = gltf.scene;
  scene.add(root);

  console.log(dumpObject(root).join('\n'));
});

这样可以看到模型里面有哪些节点,比如:

javascript 复制代码
Scene
  └─RootNode
    ├─Cars
    ├─Buildings
    └─Roads

这对于后续查找某个对象很有用。

9. 通过名字查找模型中的节点

比如模型中所有汽车都在名为 Cars 的父节点下面,于是可以这样找:

javascript 复制代码
const cars = root.getObjectByName('Cars');

然后可以操作它的子节点:

javascript 复制代码
for (const car of cars.children) {
  car.rotation.y = time;
}

10. 让汽车沿路径运动

可以让模型中的车子按模型中的道路运行

  1. 从 Blender 导出道路路径点。
  2. 用这些点创建 CatmullRomCurve3 曲线。
  3. 每帧根据时间计算汽车在曲线上的位置。
  4. lookAt 让汽车朝向前方。

示意代码:

javascript 复制代码
const curve = new THREE.CatmullRomCurve3(points, true);

const carPosition = new THREE.Vector3();
const carTarget = new THREE.Vector3();

function render(time) {
  time *= 0.001;

  const u = time * 0.01;

  curve.getPointAt(u % 1, carPosition);
  curve.getPointAt((u + 0.01) % 1, carTarget);

  car.position.copy(carPosition);
  car.lookAt(carTarget);

  renderer.render(scene, camera);
  requestAnimationFrame(render);
}

这部分说明:glTF 加载出来的场景不是只能"看",也可以继续操作里面的节点做动画和交互。

11. 开启阴影

如果想让 glTF 场景有阴影,需要:

  1. renderer 开启阴影
  2. 灯光开启投射阴影
  3. 模型节点开启投射/接收阴影
  4. 调整阴影相机范围

示意代码:

javascript 复制代码
renderer.shadowMap.enabled = true;

灯光:

javascript 复制代码
const light = new THREE.DirectionalLight(0xffffff, 1);
light.castShadow = true;
scene.add(light);

模型:

javascript 复制代码
root.traverse((obj) => {
  if (obj.castShadow !== undefined) {
    obj.castShadow = true;
    obj.receiveShadow = true;
  }
});

如果不设置这些,即使模型和灯光都加载了,也不会自动出现阴影。

12. GLTFLoader 的完整常用模板

实际项目中可以写成这样:

javascript 复制代码
import * as THREE from 'three';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';

const scene = new THREE.Scene();

const loader = new GLTFLoader();

loader.load(
  '/assets/models/city/scene.gltf',
  (gltf) => {
    const root = gltf.scene;
    scene.add(root);

    root.traverse((obj) => {
      if (obj.isMesh) {
        obj.castShadow = true;
        obj.receiveShadow = true;
      }
    });
  },
  (xhr) => {
    console.log((xhr.loaded / xhr.total) * 100 + '% loaded');
  },
  (error) => {
    console.error('GLTF 加载失败', error);
  },
);

如果是 .glb,写法一样,只是 URL 换成 .glb

javascript 复制代码
loader.load('/assets/models/robot.glb', (gltf) => {
  scene.add(gltf.scene);
});

14. 总结

这篇文章的核心不是只教一行 GLTFLoader.load(),而是讲完整思路:

  • glTF 是更适合网页实时渲染的 3D 传输格式
  • 相比 OBJ,glTF 更容易保留材质、贴图、层级和动画
  • 在 Three.js 中用 GLTFLoader 加载
  • 加载结果通常通过 gltf.scene 加入场景
  • 可以打印 scene graph 查看模型内部结构
  • 可以通过 getObjectByName() 找到特定节点
  • 加载后仍然可能需要自动取景、修正朝向、处理缩放
  • 如果要动画,资源本身的层级和命名很重要
  • 阴影需要 renderer、灯光、模型节点一起开启

一句话总结:

OBJ 更像"把模型几何导进来",glTF 更像"把一个准备好给实时渲染使用的 3D 场景导进来"。

相关推荐
wjs20241 小时前
Font Awesome 性别图标
开发语言
SmartBrain1 小时前
AI全栈开发(SDD):慢病管理系统工程级设计
java·大数据·开发语言·人工智能·架构·aigc
lsx2024061 小时前
选择(Selectable)
开发语言
漠效1 小时前
随机代理‌IP访问脚本
开发语言·python
梦想CAD控件1 小时前
网页端对DWG图纸进行预览与批注(CAD轻量化)
java·前端·javascript
SilentSamsara1 小时前
元类与 __init_subclass__:类是如何被“创建“出来的
开发语言·python·青少年编程
小a杰.1 小时前
Ascend C算子开发实战 - 从零开始写算子
c语言·开发语言
雪度娃娃2 小时前
Asio异步读写——连接的安全回收问题
开发语言·c++·安全·php
baivfhpwxf20232 小时前
c# 中对像之间频繁的转换会慢吗?
开发语言·c#