three - 模型

40 阅读3分钟

GLTFLoader

import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';

const loader = new GLTFLoader();

const mesh = new THREE.Group();

loader.load("./Horse.gltf", function (gltf) {
    mesh.add(gltf.scene);
})

export default mesh;

其实 gltf 模型文件一共有三种形式:

  • 只有 .gltf:用到的纹理图片以 base64 内嵌,顶点信息也是
  • .gltf + .bin + .png/.jpg:.gltf 描述结构,用到的顶点信息等放在 .bin 里、纹理图片是单独的 jpg、png 文件
  • .glb:用二进制的方式把所有资源到打包到一个文件里,体积更小

模型转换 gltf-pipeline

模型压缩 - draco

npx gltf-pipeline -i ./public/Michelle.glb -o ./public/Michelle2.glb -d

压缩后的模型加载需要使用 DRACOLoader

const mesh = new THREE.Group();
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath( 'https://www.gstatic.com/draco/versioned/decoders/1.5.6/' );
loader.setDRACOLoader(dracoLoader);

包围盒

包围盒(Bounding Box)是一个完全包含模型所有几何体的最小轴对齐立方体(AABB, Axis-Aligned Bounding Box)。它由两个关键点定义:

  • min:包围盒在 x、y、z 三个方向上的最小坐标
  • max:包围盒在 x、y、z 三个方向上的最大坐标
import * as THREE from 'three'; 
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'; 
const loader = new GLTFLoader(); 
loader.load('model.glb', (gltf) => { const model = gltf.scene; scene.add(model);
// 计算包围盒 
const box = new THREE.Box3().setFromObject(model); 
const helper = new THREE.Box3Helper(box, 0xff0000); 
// 红色线框 
scene.add(helper); 
// 打印信息 
console.log('包围盒中心:', box.getCenter(new THREE.Vector3())); 
console.log('包围盒尺寸:', box.getSize(new THREE.Vector3())); });

常用API

  • Box3Helper:可视化 Box3 包围盒
  • setFromObject:和 expandByObject 一样,计算对象和子对象的包围盒
  • getSize:计算包围盒大小,不用自己计算
  • expandByScalar:扩展包围盒
  • intersectsBox:检测包围盒是否相交,可用来做碰撞检测
  • intersect:计算相交部分大小
  • union:计算并集大小
  • getCenter:获取包围盒中心位置坐标

正投影相机和阴影

  • 透视投影相机是近大远小效果,而正投影相机是远近一样大。
  • 正投影相机确实用的比较少,但在设置平行光阴影的时候会用到。
  • 6 种灯光里只有点光源、聚光灯、平行光可以产生阴影,需要在 renderer 开启阴影 shadowMap.enabled,在灯光处开启阴影 castShadow,在产生阴影的物体设置阴影 castShadow,在接收阴影的物体设置 receiveShadow。
  • 之后还要设置阴影相机的大小,平行光的阴影相机是正投影相机,点光源和聚光灯的是透视投影相机。
  • 阴影相机的可视范围覆盖住要产生阴影的物体即可。

OrbitControls

  • 自动圆周运动 controls.autoRotate = true;
  • 开启惯性 controls.enableDamping = true;
  • 可以开启 rotate、zoom、pan
  • 可以限制 rotate 的范围 maxPolarAngle

射线和选中物体

  • 确定了起点 origin 和方向 direction 就可以生成一条射线 Ray,用它的 intersectTriangle 方法可以判断是否和三角形相交。
  • 一般我们会用 Raycaster 来生成射线,用 intersectObjects 方法判断是否和网格模型相交。
  • 点击的实现原理就是基于射线,把 offsetX、offsetY 的网页坐标转换为 -1 到 1 的标准屏幕坐标,然后用传入 camera,用 Raycaster 的 setFromCamera 方法生成一条射线。
  • 这样就会从相机的位置到你点击位置对应的三维空间的位置生成一条射线,射线穿过的物体就是被点击的。
renderer.domElement.addEventListener("click", (e) => {
  // 这两行将鼠标点击的屏幕坐标(像素值)转换为Three.js使用的归一化设备坐标(NDC),范围在-1到1之间。注意y坐标需要翻转,因为屏幕坐标系和WebGL坐标系的Y轴方向相反。
  // NDC 是“标准化的屏幕坐标”,中心为 (0, 0),四个角分别是 (±1, ±1)
  const y = -((e.offsetY / height) * 2 - 1);
  const x = (e.offsetX / width) * 2 - 1;
  // 创建一条从相机出发穿过点击点的射线
  const rayCaster = new THREE.Raycaster();
  rayCaster.setFromCamera(new THREE.Vector2(x, y), camera);
  const arrowHelper = new THREE.ArrowHelper(
    rayCaster.ray.direction,
    rayCaster.ray.origin,
    3000
  );
  scene.add(arrowHelper);

  // 获取射线和盒子的交点信息
  const intersections = rayCaster.intersectObjects(mesh.children);

  // 相交的盒子 颜色 改成橙色
  intersections.forEach((item) => {
    item.object.material.color = new THREE.Color("orange");
  });
});