如何在Three.js中导入3D模型(GLTF)|用“我的模型”替换盒子

这是一个会话,你导入在Blender或 glbxz.com 官网中准备的下载3D模型,并用盒子替换它们。 世界(基础)保持不变,但只有放置的部分会改变。



在此之前:别每次都写同样的准备

在进入正题之前,让我先绕个弯。

到目前为止,我一直把场景、摄像机、渲染器、光线和地板放在main.js顶部。 因为内容短暂,所以还算可以接受,但随着我有更多东西要做,每次都写同样的20行准备工作感觉很浪费。

此外,之前的最小代码有个弱点,就是调整窗口大小会导致失真(你试试就能看到)。 我一直保持沉默,因为这会妨碍我开始,但我希望能尽快解决。

所以,让基础base.js。

// src/base.js ── シリーズ共通の土台
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import { RoomEnvironment } from "three/examples/jsm/environments/RoomEnvironment.js";

export function createWorld({ environment = true, floor = true } = {}) {
  // --- 3要素(A2でやったところ) ---
  const scene = new THREE.Scene();
  scene.background = new THREE.Color(0x15171c);

  const camera = new THREE.PerspectiveCamera(75, innerWidth / innerHeight, 0.1, 1000);
  camera.position.set(0, 1, 3); // 少し上から見下ろすと、床の影が見える

  const renderer = new THREE.WebGLRenderer({ antialias: true });
  renderer.setSize(innerWidth, innerHeight);
  renderer.setPixelRatio(Math.min(devicePixelRatio, 2)); // 高解像度でも重くしすぎない
  renderer.shadowMap.enabled = true;
  document.body.appendChild(renderer.domElement);

  // --- マウスで視点を回せるように ---
  const controls = new OrbitControls(camera, renderer.domElement);
  controls.enableDamping = true;

  // --- ライト2灯(A3でやったところ) ---
  const ambient = new THREE.AmbientLight(0xffffff, 1.5);
  const directional = new THREE.DirectionalLight(0xffffff, 2);
  directional.position.set(2, 3, 4);
  directional.castShadow = true;
  directional.shadow.mapSize.set(1024, 1024);
  scene.add(ambient, directional);

  // --- 環境マップ(映り込み) ---
  if (environment) {
    const pmrem = new THREE.PMREMGenerator(renderer);
    scene.environment = pmrem.fromScene(new RoomEnvironment()).texture;
  }

  // --- 影を受ける床 ---
  if (floor) {
    const ground = new THREE.Mesh(
      new THREE.PlaneGeometry(20, 20),
      new THREE.MeshStandardMaterial({ color: 0x2a2d34, roughness: 1 })
    );
    ground.rotation.x = -Math.PI / 2;
    ground.position.y = -1;
    ground.receiveShadow = true;
    scene.add(ground);
  }

  // --- リサイズへの追従(毎回書くのが面倒だったやつ) ---
  addEventListener("resize", () => {
    camera.aspect = innerWidth / innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(innerWidth, innerHeight);
  });

  // --- アニメーションループ ---
  const clock = new THREE.Clock();
  const run = (update) => {
    renderer.setAnimationLoop(() => {
      controls.update();
      update?.(clock.getElapsedTime()); // 経過秒を渡す
      renderer.render(scene, camera);
    });
  };

  return { scene, camera, renderer, controls, run };
}

没有一个新故事。 我只是把我在 A1 到 A3 里做的事情总结成函数,添加了调整大小支持和 OrbitControls。

这意味着只有在该会话中放置的物品。main.js

import * as THREE from "three";
import { createWorld } from "./base.js";

const { scene, run } = createWorld();

// ここから下が、その回のテーマ
const cube = new THREE.Mesh(
  new THREE.BoxGeometry(),
  new THREE.MeshStandardMaterial({ color: 0x4a90d9, roughness: 0.3, metalness: 0.6 })
);
cube.castShadow = true;
scene.add(cube);

run((time) => {
  cube.rotation.y = time * 0.5;
});

这次,我们唯一要做的就是用装载过的模型替换这个零件。cube

你可以把GLTF看作是“3D世界的jpg”。

3D模型有无数种文件格式,但如果你在网上使用,GLTF是唯一的选择。.obj.fbx.stl

该格式由Khronos Group(与WebGL同属一个组织)开发,专为网页分发设计,也是阅读最直接Three.js的方式。 而且需要携带的信息量非常大。

GLTF能带来的信息目录
网格 形状(几何)
材料 颜色、粗糙度、金属度(A3的PBR值已包含)
纹理 附图
场景结构 亲子关系。 手臂悬挂在身体上,诸如此类。
动画 骨骼运动、位置和旋转的变化
摄像灯 由制作软件设置的设置

换句话说,“你可以把整个视觉环境融合在一起。” 多亏了这个,加载时看起来就是。

扩展有两种类型,我就简单地整理一下。

形式目录使用点
.gltf JSON 主文件 + 用于图片和二进制文件的独立文件 当你想看看里面是什么,或者只换掉纹理时
.glb 一个将所有内容合并到单一文件的二进制文件 基本上,就是这个。 加载请求只需一次

如果你不确定。 由于文件分布较少,路径错误也更少。.glb

装载GLTFLoader

模型会被放置到(在Vite中,内容直接送到路线,你可以在那里查阅)。public/public//robot.glb

import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";

const loader = new GLTFLoader();

loader.load(
  "/robot.glb",
  (gltf) => {
    scene.add(gltf.scene); // 読み込み完了
  },
  (e) => console.log(`${(e.loaded / e.total * 100).toFixed(0)}% 読み込み中`),
  (err) => console.error("読み込み失敗", err)
);

有两个关键点。

  1. GLTFLoader 不是主要的三个,但可以参考示例/JSM...... Three.js的附加功能已包含在此。 还是同一个地方OrbitControls
  2. 加载是异步的...... 他们会立即回来,完成后会再回电。 因此,“对加载模型做某件事”的过程总是写在第二个参数中load()

回调中有一个对象,用于总结文件内容。gltf

财产目录
gltf.scene 模型车身(集团)。 我几乎只用这个
gltf.animations AnimationClip 布局。 有动漫,就有内容。
gltf.cameras 制作软件设置的摄像机
gltf.asset 如导出的软件名称和版本等信息

顺便说一句,这不是Mesh,而是Group。 可以把它想象成一个“容器”,里面悬挂着多个网格。 这一事实后来会在暗处发挥作用。gltf.scene

“加载但什么也看不到”的三个主要原因

也许一开始你会被困在这里。 我也被困住了。 就是没有错误提示,但屏幕完全是黑色的。

主要有三个主要原因。

(1)完全不同尺度的尺寸

3D模型的单元因软件和创作者而异,有的像巨大的墙壁一样矗立在摄像机前,有的则太小看不见。

治疗时建议不要去动固定镜头,而是先测量后再调整。scale

const box = new THREE.Box3().setFromObject(model); // モデルを囲む箱を測る
const size = box.getSize(new THREE.Vector3());
const center = box.getCenter(new THREE.Vector3());

const scale = 1.6 / size.y;        // 高さを1.6くらいに揃える
model.scale.setScalar(scale);

Box3是一种计算“包围物体最小方格”的工具。 只需除以高度(),任何模型的尺寸都会匹配。 每次更换型号时,你都不用再去找号码,所以值得记住这一点。size.y

(2)起源并非你想象的那样

模型的起点(0,0,0)是在身体的脚部还是中心,取决于创作者。 如果你把一个模型以中心作为原点放置,它会沉入地板。y = 0

这也可以通过我之前测量的那个来解决。box

model.position.x = -center.x * scale;
model.position.z = -center.z * scale;
model.position.y = -1 - box.min.y * scale; // 床(y=-1)に足を着ける

box.min.y它位于模型的最底部,所以你应该与地板高度匹配。

(3)纯粹黑暗

这是上次的最后一次召回。 GLTF材料基本上相当厚重,没有光线时会变成漆黑一片。MeshStandardMaterial

base.js如果你用它,灯光和环境贴图都包含在内,所以没问题,但如果你创建了一个场景,模型最终呈现出黑色剪影,首先要怀疑光照。

阴影用于覆盖所有网格。traverse

前面提到的“哈集团”在这里发挥了作用。gltf.scene

model.castShadow = true; // ❌ これでは影は出ない

即使你设置了一个组,实际绘制还是由内层网格完成的,所以什么都不会发生。 你需要描摹所有内容,一个一个地竖起来。castShadow

model.traverse((obj) => {
  if (obj.isMesh) {
    obj.castShadow = true;
    obj.receiveShadow = true;
  }
});

traverse()是一种让你和你的后代能够将它传递给所有人的方法。 由于3D模型是由躯干、头部、手臂等网格组成的集合,这就是建模时的基本形式。

顺便说一句,你也可以用它来批量更换材料。 你可以通过把所有导入的模型转换成线框图来调试结构,这很方便记住。traverse

播放动画

如果模型有动画,单纯加载动画是无法实现的。 你需要把它连接到播放设备()。AnimationMixer

let mixer = null;

loader.load("/robot.glb", (gltf) => {
  const model = gltf.scene;
  scene.add(model);

  if (gltf.animations.length > 0) {
    mixer = new THREE.AnimationMixer(model);
    mixer.clipAction(gltf.animations[0]).play(); // 最初のクリップを再生
  }
});

事情就是这样运作的。

  • 动画剪辑 ... 动作单位如“走路”或“挥手”。 排列成数组。gltf.animations
  • 动画混合器 ... 专门用于该型号的播放设备。 每个模型做一个
  • 动画动作...... 用它制作的播放控制器。 ......还有其他办法。clipAction()play()stop()fadeIn()

除非你对每一帧调用 mixer.update(),否则时间不会进展。 这里有一个重要的点需要注意。

let prev = 0;

run((time) => {
  const delta = time - prev; // 前フレームからの差を出す
  prev = time;

  mixer?.update(delta);
});

mixer.update()我们想要的是差额(delta),而不是总经过的时间。 设计目的是通过“从起点开始计时秒”,所以你可以像上面所示自己做出决定。base.jsrun()

如果你不小心把总值传过了,动画会瞬间快进结束(我一开始就是这么做的)。

压缩较重模型

3D模型的大小可以轻易达到几十MB不等。 即使是本地的瞬间,在移动网络上也可能致命。

减轻重量有两种方法,这两种方式都是GLTF的标准扩展。

压缩压缩什么添加装载侧
德拉科 形状(网格) DRACOLoader
KTX2 / Basis 纹理图像 KTX2Loader

除非解码器连接,否则无法读取Draco压缩模型。 话虽如此,新增内容几乎只有两行。

import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader.js";

const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath("https://www.gstatic.com/draco/versioned/decoders/1.5.7/");

const loader = new GLTFLoader();
loader.setDRACOLoader(dracoLoader);

压缩本身是出口端的工作。 在Blender的导出设置里检查Draco,或者通过像gltf-transform这样的CLI操作很容易。 几MB长到几百KB的情况很常见,所以在发布前玩一遍会让人安心。

posted @ 2026-09-18 10:01  吃饭不忘  阅读(6)  评论(0)    收藏  举报