前端Threejs入门(五)

29 阅读16分钟

前言

小编今天学了几节,分享给大家。也可以回顾一下往节。

14-Three.js UV映射与应用指南

📚 核心知识点

graph TD
    A[UV Mapping] --> B[基本概念]
    A --> C[UV坐标设置]
    A --> D[映射方式]
    A --> E[应用场景]
    B --> F[二维纹理坐标系]
    B --> G[0-1范围]
    C --> H[几何体属性设置]
    C --> I[手动修改UV]
    D --> J[平面映射]
    D --> K[立方体映射]
    D --> L[球形映射]
    E --> M[纹理贴图]
    E --> N[光照贴图]
    E --> O[动画效果]

🧠 知识详解

1. UV基础概念

  • 二维纹理坐标系(U水平,V垂直)
  • 范围:[0,1]区间(可超出)
  • 顶点与纹理的对应关系

2. UV坐标设置方式

geometry.attributes.uv = new THREE.BufferAttribute(uvArray, 2)

3. 常见映射方式

类型特点适用场景
平面映射投影到平面墙面、地面
立方体映射6面投影盒子、建筑
球形映射球面环绕行星、球体

4. 主要应用场景

  • 纹理贴图控制
  • 光照贴图烘焙
  • 动态纹理动画
  • 表面细节控制

💻 完整代码实现

// main.js
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

// 初始化场景
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth/window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

// 添加控制器
const controls = new OrbitControls(camera, renderer.domElement);
camera.position.z = 5;

// 创建自定义几何体(平面)
const geometry = new THREE.PlaneGeometry(4, 4);
const texLoader = new THREE.TextureLoader();

// 设置自定义UV坐标
const uvs = new Float32Array([
  0,0,  // 顶点0
  2,0,  // 顶点1
  2,2,  // 顶点2
  0,2   // 顶点3
]);
geometry.setAttribute('uv', new THREE.BufferAttribute(uvs, 2));

// 创建材质
const material = new THREE.MeshBasicMaterial({
  map: texLoader.load('https://threejs.org/examples/textures/uv_grid_opengl.jpg'),//使用UV测试网格纹理
  side: THREE.DoubleSide //side:双面可见(默认只渲染正面)
});

// 创建网格 将几何体与材质组合成可渲染对象,添加到场景根节点

添加到场景根节点
const plane = new THREE.Mesh(geometry, material);
scene.add(plane);

// 动画循环
function animate() {
  requestAnimationFrame(animate);
  
  // UV动画示例
  const uvAttribute = plane.geometry.attributes.uv;
  for(let i=0; i<uvAttribute.count; i++){
    uvAttribute.setX(i, uvAttribute.getX(i) + 0.001);// X轴偏移
  }
  uvAttribute.needsUpdate = true;// 标记属性需要更新
    // 渲染
  renderer.render(scene, camera);
}
animate();
graph LR
    A[帧开始] --> B[遍历所有UV点]
    B --> C[修改X坐标]
    C --> D[标记数据更新]
    D --> E[GPU渲染]
    E --> F[下一帧请求]

2025-03-30T06_23_05.380Z-618863.gif

也有更多好玩的功能,可以拿好兄弟的照片恶搞:

// main.js
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

// 初始化场景
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

// 添加控制器
const controls = new OrbitControls(camera, renderer.domElement);
camera.position.z = 8;

// 创建平面几何体
const geometry = new THREE.PlaneGeometry(6, 6); // 增大平面尺寸
geometry.rotateX(Math.PI); // 修正默认朝向

// 设置扩展UV坐标
const uvs = new Float32Array([
  0, 0,  // 左下
  3, 0,  // 右下
  3, 3,  // 右上
  0, 3   // 左上
]);
geometry.setAttribute('uv', new THREE.BufferAttribute(uvs, 2));

// 加载纹理(带错误处理)
const texLoader = new THREE.TextureLoader();
const texture = texLoader.load(
  './textures/dachun.jpg',
  undefined,
  undefined,
  (err) => console.error('纹理加载失败:', err)
);

// 配置纹理参数
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
texture.minFilter = THREE.LinearFilter;
texture.magFilter = THREE.LinearFilter;
/*
// 设置纹理在 U 方向(水平方向)的包裹方式
// THREE.RepeatWrapping 表示纹理在 U 方向上会重复平铺
// 当纹理坐标超出 [0, 1] 范围时,纹理会重复显示
texture.wrapS = THREE.RepeatWrapping;

// 设置纹理在 V 方向(垂直方向)的包裹方式
// THREE.RepeatWrapping 表示纹理在 V 方向上会重复平铺
// 当纹理坐标超出 [0, 1] 范围时,纹理会重复显示
texture.wrapT = THREE.RepeatWrapping;

// 设置纹理在缩小时的过滤方式
// THREE.LinearFilter 表示使用线性过滤,效果更平滑
// 当纹理被缩小时(例如纹理贴图的分辨率高于屏幕分辨率),
// 线性过滤会通过插值计算像素值,使纹理看起来更平滑,但可能会稍微模糊
texture.minFilter = THREE.LinearFilter;

// 设置纹理在放大时的过滤方式
// THREE.LinearFilter 表示使用线性过滤,效果更平滑
// 当纹理被放大时(例如纹理贴图的分辨率低于屏幕分辨率),
// 线性过滤会通过插值计算像素值,使纹理看起来更平滑,但可能会稍微模糊
texture.magFilter = THREE.LinearFilter;
*/
// 创建材质
const material = new THREE.MeshBasicMaterial({
  map: texture,
  side: THREE.DoubleSide,
  transparent: true
});

// 创建网格
const plane = new THREE.Mesh(geometry, material);
scene.add(plane);

// 添加辅助工具
scene.add(new THREE.AxesHelper(5));
scene.add(new THREE.GridHelper(10, 10));

// 动画循环
function animate() {
  requestAnimationFrame(animate);

  // 更高效的UV动画实现
  const uvAttribute = plane.geometry.attributes.uv;
  const arr = uvAttribute.array;
  for (let i = 0; i < arr.length; i += 2) {
    arr[i] += 0.002; // 仅修改U坐标
  }
  uvAttribute.needsUpdate = true;

  controls.update();
  renderer.render(scene, camera);
}

// 响应窗口尺寸变化
window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
});

animate();

我还提供了3d的代码,哈哈真好玩:

// main.js
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

// 初始化场景
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
/*
场景 (Scene): 创建了一个 THREE.Scene 对象,用于容纳所有的 3D 对象。
相机 (Camera): 使用 THREE.PerspectiveCamera 创建了一个透视相机,视角为 75 度,宽高比为窗口的宽高比,近裁剪面为 0.1,远裁剪面为 1000。
渲染器 (Renderer): 使用 THREE.WebGLRenderer 创建了一个 WebGL 渲染器,并启用了抗锯齿。渲染器的尺寸设置为窗口的宽高,并将其添加到页面的 body 中。
*/
// 添加控制器
const controls = new OrbitControls(camera, renderer.domElement);
camera.position.set(5, 5, 5);
controls.update();
/*
OrbitControls: 使用 OrbitControls 控制器,允许用户通过鼠标控制相机的视角。相机的位置被设置为 (5, 5, 5)。
*/
// 创建立方体几何体
const geometry = new THREE.BoxGeometry(3, 3, 3);
// 设置自定义UV坐标(每个面单独设置)
const uvs = [];
/*
BoxGeometry: 创建了一个边长为 3 的立方体几何体。
自定义 UV 坐标: 为立方体的每个面设置了自定义的 UV 坐标。UV 坐标决定了纹理如何映射到几何体的表面上。
*/
const faceUVs = [
  /* 前后面 */
  [[0, 0], [1, 0], [1, 1], [0, 1]], // 前面
  [[0, 0], [1, 0], [1, 1], [0, 1]], // 后面

  /* 左右面 */
  [[0, 0], [2, 0], [2, 1], [0, 1]], // 左面
  [[0, 0], [2, 0], [2, 1], [0, 1]], // 右面

  /* 顶底面 */
  [[0, 0], [1, 0], [1, 2], [0, 2]], // 顶面
  [[0, 0], [1, 0], [1, 2], [0, 2]]  // 底面
];
faceUVs.forEach(face => {
  uvs.push(...face.flat());
});
geometry.setAttribute('uv', new THREE.BufferAttribute(new Float32Array(uvs), 2));

// 加载纹理
const texLoader = new THREE.TextureLoader();
const texture = texLoader.load('./textures/dachun.jpg');
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
/*
TextureLoader: 使用 THREE.TextureLoader 加载了一个纹理图片 dachun.jpg,并设置了纹理的重复模式为 RepeatWrapping,即纹理在 UV 坐标超出 [0, 1] 范围时会重复。
*/
// 创建材质
const material = new THREE.MeshBasicMaterial({
  map: texture,
  side: THREE.DoubleSide
});
/*
MeshBasicMaterial: 创建了一个基础材质,并将加载的纹理应用为材质的 map。材质的 side 属性设置为 THREE.DoubleSide,表示材质会渲染在几何体的两面。
*/
// 创建三维物体
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
/*
Mesh: 使用几何体和材质创建了一个三维物体(立方体),并将其添加到场景中。
*/
// 添加辅助工具
scene.add(new THREE.AxesHelper(5));
scene.add(new THREE.GridHelper(10, 10));
/*
AxesHelper: 添加了一个坐标轴辅助工具,用于显示场景的 X、Y、Z 轴。
GridHelper: 添加了一个网格辅助工具,用于显示场景中的网格。
*/
// 动画控制参数
let speed = 0.005;
let animateU = true;
let animateV = false;

// 动画循环
function animate() {
  requestAnimationFrame(animate);

  // 更新UV坐标
  const uvAttribute = cube.geometry.attributes.uv;
  const arr = uvAttribute.array;

  for (let i = 0; i < arr.length; i += 2) {
    if (animateU) arr[i] += speed;
    if (animateV) arr[i + 1] += speed;
  }
  uvAttribute.needsUpdate = true;

  // 自动旋转
  cube.rotation.x += 0.01;
  cube.rotation.y += 0.01;

  controls.update();
  renderer.render(scene, camera);
}
/*
UV 坐标动画: 在动画循环中,动态更新立方体的 UV 坐标,使得纹理在立方体表面上移动。animateU 和 animateV 控制是否在 U 或 V 方向上移动纹理。
立方体旋转: 立方体在 X 和 Y 方向上以每帧 0.01 弧度的速度旋转。
*/

// 窗口自适应
window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
});
/*
resize 事件: 监听窗口的 resize 事件,当窗口大小改变时,更新相机的宽高比和渲染器的尺寸。
*/
animate();

2025-03-30T06_39_29.903Z-989668.gif

三维UV映射核心要点

📦 立方体UV结构解析
graph LR
    A[立方体6个面] --> B[每个面4个顶点]
    B --> C[每个顶点对应UV坐标]
    
    subgraph 前面
    C1[UV:0,0] --> D1[顶点0]
    C2[UV:1,0] --> D2[顶点1]
    C3[UV:1,1] --> D3[顶点2]
    C4[UV:0,1] --> D4[顶点3]
    end
    
    subgraph 顶面
    E1[UV:0,0] --> F1[顶点0]
    E2[UV:1,0] --> F2[顶点1]
    E3[UV:1,2] --> F3[顶点2]
    E4[UV:0,2] --> F4[顶点3]
    end

15-Three.js 法向量应用指南

📚 核心概念图解

graph TD
    A[法向量] --> B[属性作用]
    A --> C[辅助器应用]
    B --> D[光照计算]
    B --> E[材质表现]
    B --> F[碰撞检测]
    C --> G[可视化调试]
    C --> H[方向验证]

💻 完整代码实现

// main.js
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { VertexNormalsHelper } from 'three/addons/helpers/VertexNormalsHelper.js';

// 初始化场景
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth/innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

// 添加光源
const light = new THREE.PointLight(0xffffff, 800, 50);
light.position.set(5, 5, 5);
scene.add(light);
scene.add(new THREE.AmbientLight(0x404040));

// 创建自定义几何体(二十面体)
const geometry = new THREE.IcosahedronGeometry(3, 1);
const material = new THREE.MeshPhongMaterial({
    color: 0x2194ce,
    shininess: 100,
    flatShading: true // 启用平面着色
});

// 创建法向量辅助器
const normalsHelper = new VertexNormalsHelper(
    new THREE.Mesh(geometry, material),
    0.5,  // 法线长度
    0xff0000 // 颜色
);
scene.add(normalsHelper.object);
scene.add(normalsHelper);

// 添加轨道控制器
const controls = new OrbitControls(camera, renderer.domElement);
camera.position.z = 15;

// 法向量修改函数
function modifyNormals() {
    const positions = geometry.attributes.position.array;
    const normals = geometry.attributes.normal.array;
    
    // 随机扰动法向量
    for(let i=0; i<normals.length; i+=3){
        const noise = 0.3 * Math.random();
        normals[i] += noise;
        normals[i+1] += noise;
        normals[i+2] += noise;
    }
    
    geometry.attributes.normal.needsUpdate = true;
    normalsHelper.update();
}

// 动画循环
function animate() {
    requestAnimationFrame(animate);
    
    // 旋转模型
    normalsHelper.object.rotation.x += 0.01;
    normalsHelper.object.rotation.y += 0.01;
    
    controls.update();
    renderer.render(scene, camera);
}

// 界面控制
document.querySelector('#modifyBtn').addEventListener('click', modifyNormals);

animate();

🔍 关键代码解析

1. 法向量辅助器配置

new VertexNormalsHelper(
    mesh,       // 目标网格
    0.5,        // 法线显示长度
    0xff0000    // 法线颜色
);

2. 法向量属性结构

索引含义类型
0顶点X法线Float32
1顶点Y法线Float32
2顶点Z法线Float32

3. 法向量修改流程

sequenceDiagram
    participant 用户点击
    participant 修改函数
    participant 几何体
    participant 辅助器
    
    用户点击->>修改函数: 触发修改
    修改函数->>几何体: 更新normal数组
    几何体->>辅助器: 标记needsUpdate
    辅助器->>辅助器: update()

🛠️ 常用法向量操作

1. 法向量归一化

geometry.computeVertexNormals(); // 自动计算

2. 自定义法向量

// 创建法向量数组
const normals = new Float32Array([
    0, 1, 0,  // 顶点0法线向上
    -1, 0, 0, // 顶点1法线向左
    // ...其他顶点
]);
geometry.setAttribute('normal', new THREE.BufferAttribute(normals, 3));

3. 动态更新优化

// 共享ArrayBuffer提高性能
const normalArray = geometry.attributes.normal.array;

function updateNormals() {
    // 直接操作数组
    for(let i=0; i<normalArray.length; i++){
        normalArray[i] += Math.sin(Date.now()*0.001) * 0.1;
    }
    geometry.attributes.normal.needsUpdate = true;
}

🌈 效果对比演示

法向量影响表现

法线状态光照效果材质表现
原始法线平滑渐变自然高光
随机扰动法线表面凹凸感金属质感
垂直统一法线均匀亮度无立体感

📌 调试技巧

  1. 长度调节:通过修改辅助器长度参数适配场景

    normalsHelper.size = 2; // 实时生效
    
  2. 颜色区分:使用不同颜色区分面法线和顶点法线

    new FaceNormalsHelper(mesh, 2, 0x00ff00); // 面法线
    
  3. 选择查看:针对特定顶点调试

    // 仅显示第一个顶点的法线
    normalsHelper.setVisible(false);
    normalsHelper.setVisible(true, 0); 
    

⚠️ 常见问题排查

问题:法线辅助器不显示

解决方案:

  1. 检查辅助器是否加入场景
  2. 确认法线长度不为0
  3. 验证几何体包含有效法线数据

问题:修改法线后无变化

检查清单:

  1. 确认needsUpdate = true
  2. 检查材质是否响应法线(如MeshPhongMaterial)
  3. 确保未启用flatShading模式

16-Three.js 包围盒与世界矩阵转换指南

📚 核心知识点

graph TD
    A[包围盒] --> B[创建方法]
    A --> C[类型]
    A --> D[应用场景]
    B --> E[geometry.computeBoundingBox]
    B --> F[new THREE.Box3]
    C --> G[轴对齐包围盒 AABB]
    C --> H[方向包围盒 OBB]
    D --> I[碰撞检测]
    D --> J[视锥裁剪]
    D --> K[物体定位]
    
    L[世界矩阵] --> M[组成要素]
    L --> N[转换方法]
    M --> O[位置 Position]
    M --> P[旋转 Rotation]
    M --> Q[缩放 Scale]
    N --> R[matrixWorld]
    N --> S[applyMatrix4]

🧠 知识详解

1. 包围盒(Bounding Box)

  • 轴对齐包围盒(AABB):基于世界坐标系轴对齐的立方体

  • 创建方式

    const box = new THREE.Box3();
    box.setFromObject(mesh); // 自动计算
    

2. 世界矩阵(World Matrix)

  • 组成
    WorldMatrix = Position × Rotation × Scale
    
  • 关键属性
    mesh.matrixWorld // 自动更新的世界矩阵
    

3. 矩阵转换原理

  • 局部空间 → 世界空间
    box.applyMatrix4(mesh.matrixWorld);
    

💻 完整代码实现

import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { GUI } from 'three/examples/jsm/libs/lil-gui.module.min.js';

// 初始化场景
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth/innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

// 创建测试物体
const geometry = new THREE.BoxGeometry(2, 2, 2);
const material = new THREE.MeshNormalMaterial();
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

// 初始化包围盒
const localBox = new THREE.Box3();
localBox.setFromObject(mesh); 

// 创建包围盒辅助器
const boxHelper = new THREE.BoxHelper(mesh, 0xffff00);
scene.add(boxHelper);

// 控制器
const controls = new OrbitControls(camera, renderer.domElement);
camera.position.set(5, 5, 5);

// GUI参数
const guiParams = {
  positionX: 0,
  positionY: 0,
  positionZ: 0,
  rotationSpeed: 0.01,
  showBoundingBox: true
};

// GUI设置
const gui = new GUI();
gui.add(mesh.position, 'x', -5, 5).name('X Position');
gui.add(mesh.position, 'y', -5, 5).name('Y Position');
gui.add(mesh.position, 'z', -5, 5).name('Z Position');
gui.add(guiParams, 'rotationSpeed', 0, 0.1).name('旋转速度');
gui.add(guiParams, 'showBoundingBox').name('显示包围盒').onChange(val => {
  boxHelper.visible = val;
});

// 动画循环
function animate() {
  requestAnimationFrame(animate);
  
  // 更新物体变换
  mesh.rotation.x += guiParams.rotationSpeed;
  mesh.rotation.y += guiParams.rotationSpeed;
  
  // 更新包围盒(需要先更新世界矩阵)
  mesh.updateMatrixWorld(true);
  localBox.setFromObject(mesh);
  
  // 获取世界空间包围盒
  const worldBox = localBox.clone();
  worldBox.applyMatrix4(mesh.matrixWorld);
  
  // 更新辅助器
  boxHelper.update();
  
  // 控制台输出
  console.log('局部空间包围盒:', localBox);
  console.log('世界空间包围盒:', worldBox);
  
  controls.update();
  renderer.render(scene, camera);
}

// 响应窗口变化
window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
});

animate();

2025-03-30T07_58_00.701Z-578123.gif

🎯 关键代码解析

1. 包围盒更新流程

sequenceDiagram
    participant 用户操作
    participant 物体变换
    participant 包围盒系统
    
    用户操作->>物体变换: 改变位置/旋转/缩放
    物体变换->>包围盒系统: updateMatrixWorld(true)
    包围盒系统->>包围盒系统: setFromObject(mesh)
    包围盒系统->>包围盒系统: applyMatrix4(matrixWorld)

2. 重要方法说明

方法作用调用时机
updateMatrixWorld()更新物体及其子元素的世界矩阵物体变换后
setFromObject()计算物体包围盒需要获取最新包围盒时
applyMatrix4()应用矩阵变换到包围盒坐标空间转换时

📌 使用技巧

  1. 性能优化

    // 对静态物体只需计算一次
    geometry.computeBoundingBox();
    const staticBox = geometry.boundingBox.clone();
    
  2. 碰撞检测

    const box1 = mesh1.geometry.boundingBox.clone();
    const box2 = mesh2.geometry.boundingBox.clone();
    box1.applyMatrix4(mesh1.matrixWorld);
    box2.applyMatrix4(mesh2.matrixWorld);
    const isColliding = box1.intersectsBox(box2);
    
  3. 可视化调试

    // 显示所有包围盒
    scene.traverse(obj => {
      if(obj.isMesh) {
        scene.add(new THREE.BoxHelper(obj, 0xffff00));
      }
    });
    

⚠️ 常见问题

Q:为什么包围盒不随物体移动?
A:需要按顺序执行:

  1. mesh.updateMatrixWorld(true)
  2. box.setFromObject(mesh)
  3. boxHelper.update()

Q:如何获取精确的包围盒?
A:使用几何体本身的包围盒计算:

geometry.computeBoundingBox();
const preciseBox = geometry.boundingBox.clone();
preciseBox.applyMatrix4(mesh.matrixWorld);

17-Three.js 几何体中心与几何体获取指南

📚 核心概念

graph TD
    A[几何体操作] --> B[中心点计算]
    A --> C[数据获取]
    B --> D[包围盒中心]
    B --> E[顶点平均中心]
    C --> F[顶点数据]
    C --> G[面数据]
    C --> H[属性访问]

🧠 知识详解

1. 几何体中心类型

类型计算方法特点
包围盒中心geometry.boundingBox.getCenter()基于AABB快速计算
顶点平均中心顶点坐标平均值精确但计算量大
原点中心geometry.center()将几何体移动到本地坐标系原点

2. 几何体数据获取

const geometry = new THREE.BoxGeometry(2, 2, 2);

// 获取顶点数据
const vertices = geometry.attributes.position.array; // Float32Array

// 获取面数据
const faces = geometry.index.array; // Uint16Array (索引面)

💻 完整代码实现

import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { GUI } from 'three/examples/jsm/libs/lil-gui.module.min.js';

// 初始化场景
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth/innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

// 创建可变形几何体
const geometry = new THREE.IcosahedronGeometry(2, 2);
const material = new THREE.MeshNormalMaterial({ wireframe: true });
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

// 计算初始中心点
geometry.computeBoundingBox();
const center = new THREE.Vector3();
geometry.boundingBox.getCenter(center);

// 创建中心点标记
const centerMarker = new THREE.Mesh(
  new THREE.SphereGeometry(0.1),
  new THREE.MeshBasicMaterial({ color: 0xff0000 })
);
centerMarker.position.copy(center);
scene.add(centerMarker);

// GUI控制面板
const gui = new GUI();
const params = {
  showWireframe: true,
  vertexCount: geometry.attributes.position.count,
  faceCount: geometry.index ? geometry.index.count / 3 : 0,
  autoCenter: false
};

gui.add(params, 'showWireframe').name('显示线框').onChange(val => {
  material.wireframe = val;
});
gui.add(params, 'autoCenter').name('自动居中').onChange(val => {
  if(val) geometry.center();
});

// 控制器
const controls = new OrbitControls(camera, renderer.domElement);
camera.position.set(5, 5, 5);

// 动画循环
function animate() {
  requestAnimationFrame(animate);
  
  // 动态修改几何体
  modifyGeometry();
  
  // 更新中心点
  updateCenterPoint();
  
  controls.update();
  renderer.render(scene, camera);
}

// 几何体修改函数
function modifyGeometry() {
  const positions = geometry.attributes.position.array;
  const time = Date.now() * 0.001;
  
  for(let i = 0; i < positions.length; i += 3) {
    positions[i] += Math.sin(time + i) * 0.01;
    positions[i+1] += Math.cos(time + i) * 0.01;
  }
  
  geometry.attributes.position.needsUpdate = true;
  geometry.computeBoundingBox();
}

// 更新中心点位置
function updateCenterPoint() {
  geometry.boundingBox.getCenter(center);
  centerMarker.position.copy(center);
  
  // 同步到世界坐标
  mesh.updateMatrixWorld(true);
  centerMarker.updateMatrixWorld(true);
}

// 响应窗口变化
window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
});

animate();

🎯 关键代码解析

1. 中心点计算流程

sequenceDiagram
    participant 几何体
    participant 包围盒
    participant 标记对象
    
    几何体->>包围盒: computeBoundingBox()
    包围盒->>中心点: getCenter()
    中心点->>标记对象: position.copy()
    标记对象->>世界坐标: updateMatrixWorld()

2. 几何体数据操作

方法作用使用场景
geometry.center()将顶点数据平移到本地坐标系原点模型居中
geometry.rotateX()绕X轴旋转几何体调整朝向
geometry.merge()合并多个几何体模型组合

📌 使用技巧

  1. 性能优化

    // 避免频繁计算
    const cachedCenter = geometry.boundingBox.getCenter(new THREE.Vector3());
    
  2. 顶点遍历

    const positionAttr = geometry.attributes.position;
    for(let i = 0; i < positionAttr.count; i++) {
      const x = positionAttr.getX(i);
      const y = positionAttr.getY(i);
      const z = positionAttr.getZ(i);
    }
    
  3. 几何体克隆

    const clonedGeo = geometry.clone();
    clonedGeo.scale(2, 2, 2); // 非破坏性操作
    

⚠️ 常见问题

Q:修改顶点后中心点不更新?
A:需要按顺序执行:

  1. geometry.attributes.position.needsUpdate = true
  2. geometry.computeBoundingBox()
  3. geometry.boundingBox.getCenter()

Q:如何获取世界坐标中心?

const worldCenter = new THREE.Vector3();
mesh.getWorldPosition(worldCenter);

18-Three.js 多物体包围盒获取指南

📚 核心方法

graph TD
    A[多物体包围盒] --> B[遍历场景对象]
    A --> C[批量计算]
    A --> D[碰撞检测]
    B --> E[场景树遍历]
    C --> F[矩阵更新]
    C --> G[包围盒计算]
    D --> H[两两检测]

💻 完整代码实现

import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { GUI } from 'three/examples/jsm/libs/lil-gui.module.min.js';

// 初始化场景
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth/innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

// 创建多个测试物体
const objects = [];
for(let i = 0; i < 5; i++) {
  const geometry = new THREE.BoxGeometry(1, 1, 1);
  const material = new THREE.MeshStandardMaterial({ 
    color: new THREE.Color().setHSL(Math.random(), 0.7, 0.5),
    metalness: 0.3,
    roughness: 0.7
  });
  const mesh = new THREE.Mesh(geometry, material);
  mesh.position.set(
    (Math.random() - 0.5) * 10,
    (Math.random() - 0.5) * 10,
    (Math.random() - 0.5) * 10
  );
  mesh.rotation.set(
    Math.random() * Math.PI,
    Math.random() * Math.PI,
    Math.random() * Math.PI
  );
  scene.add(mesh);
  objects.push(mesh);
}

// 包围盒管理器
const boundingBoxes = {
  boxes: new Map(),
  helpers: new Set(),
  
  // 更新所有包围盒
  updateAll: function() {
    this.boxes.clear();
    scene.traverse(obj => {
      if(obj.isMesh) {
        obj.updateMatrixWorld(true);
        const box = new THREE.Box3().setFromObject(obj);
        this.boxes.set(obj.uuid, box);
      }
    });
  },
  
  // 创建可视化辅助器
  createHelpers: function() {
    this.clearHelpers();
    this.boxes.forEach((box, uuid) => {
      const helper = new THREE.Box3Helper(box, 0xffff00);
      this.helpers.add(helper);
      scene.add(helper);
    });
  },
  
  // 清除辅助器
  clearHelpers: function() {
    this.helpers.forEach(helper => scene.remove(helper));
    this.helpers.clear();
  }
};

// GUI控制面板
const gui = new GUI();
const params = {
  autoUpdate: true,
  showHelpers: true,
  collisionCheck: false,
  updateInterval: 100
};

gui.add(params, 'autoUpdate').name('自动更新');
gui.add(params, 'showHelpers').name('显示包围盒').onChange(val => {
  val ? boundingBoxes.createHelpers() : boundingBoxes.clearHelpers();
});
gui.add(params, 'collisionCheck').name('碰撞检测');

// 控制器
const controls = new OrbitControls(camera, renderer.domElement);
camera.position.set(15, 15, 15);

// 动画循环
let lastUpdate = 0;
function animate() {
  requestAnimationFrame(animate);
  
  // 定时更新包围盒
  if(params.autoUpdate && Date.now() - lastUpdate > params.updateInterval) {
    boundingBoxes.updateAll();
    if(params.showHelpers) boundingBoxes.createHelpers();
    lastUpdate = Date.now();
  }
  
  // 碰撞检测
  if(params.collisionCheck) {
    checkCollisions();
  }
  
  controls.update();
  renderer.render(scene, camera);
}

// 碰撞检测函数
function checkCollisions() {
  const boxesArray = Array.from(boundingBoxes.boxes.values());
  
  for(let i = 0; i < boxesArray.length; i++) {
    for(let j = i + 1; j < boxesArray.length; j++) {
      if(boxesArray[i].intersectsBox(boxesArray[j])) {
        console.log(`物体 ${i}${j} 发生碰撞`);
      }
    }
  }
}

// 初始化
boundingBoxes.updateAll();
boundingBoxes.createHelpers();

// 响应窗口变化
window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
});

animate();

🎯 关键功能解析

1. 批量更新流程

sequenceDiagram
    participant 场景
    participant 物体
    participant 包围盒系统
    
    场景->>物体: 遍历所有网格
    物体->>包围盒系统: 更新世界矩阵
    包围盒系统->>包围盒系统: 计算新包围盒
    包围盒系统->>辅助器: 更新可视化

2. 性能优化技巧

方法效果适用场景
增量更新仅更新变化物体动态场景
空间分割使用BVH加速检测大规模物体
简化计算使用Sphere代替Box快速检测

📌 使用技巧

  1. 选择性更新
// 只更新移动的物体
objects.forEach(obj => {
  if(obj.userData.isMoving) {
    obj.updateMatrixWorld(true);
    boundingBoxes.boxes.set(obj.uuid, new THREE.Box3().setFromObject(obj));
  }
});
  1. 组合包围盒
// 计算群体包围盒
const groupBox = new THREE.Box3();
objects.forEach(obj => {
  groupBox.union(boundingBoxes.boxes.get(obj.uuid));
});
  1. 射线检测优化
// 先检测包围盒再检测详细碰撞
const raycaster = new THREE.Raycaster();
const intersects = raycaster.intersectObjects(
  objects.filter(obj => {
    return raycaster.ray.intersectsBox(boundingBoxes.boxes.get(obj.uuid))
  })
);

⚠️ 常见问题

Q:包围盒更新不及时?
A:确保调用顺序:

  1. obj.updateMatrixWorld(true)
  2. box.setFromObject(obj)

Q:如何提高碰撞检测效率?
A:采用分级检测:

  1. 使用空间划分(八叉树/BVH)
  2. 先检测球形包围盒
  3. 最后检测精确AABB

19-Three.js 边缘几何体与线框几何体指南

📚 核心概念对比

graph TD
    A[线框几何体] --> B[面片显示]
    A --> C[材质控制]
    A --> D[整体结构]
    
    E[边缘几何体] --> F[边界提取]
    E --> G[角度检测]
    E --> H[轮廓强调]

💻 完整代码实现

import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { GUI } from 'three/examples/jsm/libs/lil-gui.module.min.js';

// 初始化场景
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth/innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

// 创建基础几何体
const baseGeometry = new THREE.IcosahedronGeometry(3, 1);
const baseMaterial = new THREE.MeshStandardMaterial({ 
  color: 0x2194ce,
  metalness: 0.3,
  roughness: 0.7
});

// 创建线框几何体
const wireframeGeometry = new THREE.WireframeGeometry(baseGeometry);
const wireframeMaterial = new THREE.LineBasicMaterial({
  color: 0xffff00,
  linewidth: 2
});
const wireframe = new THREE.LineSegments(wireframeGeometry, wireframeMaterial);

// 创建边缘几何体
const edgesGeometry = new THREE.EdgesGeometry(baseGeometry, {
  thresholdAngle: 15 // 角度阈值(度)
});
const edgesMaterial = new THREE.LineBasicMaterial({ 
  color: 0xff0000,
  linewidth: 3
});
const edges = new THREE.LineSegments(edgesGeometry, edgesMaterial);

// 组合显示对象
const mainMesh = new THREE.Mesh(baseGeometry, baseMaterial);
const compositeGroup = new THREE.Group();
compositeGroup.add(mainMesh, wireframe, edges);
scene.add(compositeGroup);

// GUI控制面板
const gui = new GUI();
const params = {
  wireframeVisible: true,
  wireframeColor: '#ffff00',
  edgesVisible: true,
  edgesColor: '#ff0000',
  thresholdAngle: 15
};

gui.add(params, 'wireframeVisible').name('显示线框').onChange(val => {
  wireframe.visible = val;
});
gui.addColor(params, 'wireframeColor').name('线框颜色').onChange(val => {
  wireframeMaterial.color.set(val);
});
gui.add(params, 'edgesVisible').name('显示边缘').onChange(val => {
  edges.visible = val;
});
gui.addColor(params, 'edgesColor').name('边缘颜色').onChange(val => {
  edgesMaterial.color.set(val);
});
gui.add(params, 'thresholdAngle', 1, 45).name('边缘角度阈值').onChange(val => {
  edges.geometry.dispose();
  edges.geometry = new THREE.EdgesGeometry(baseGeometry, { thresholdAngle: val });
});

// 灯光配置
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(5, 5, 5);
scene.add(ambientLight, directionalLight);

// 控制器
const controls = new OrbitControls(camera, renderer.domElement);
camera.position.set(10, 10, 10);

// 动画循环
function animate() {
  requestAnimationFrame(animate);
  
  compositeGroup.rotation.x += 0.01;
  compositeGroup.rotation.y += 0.01;
  
  controls.update();
  renderer.render(scene, camera);
}

// 窗口自适应
window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
});

animate();

🎯 核心功能解析

1. 几何体类型对比

特性线框几何体边缘几何体
数据结构显示所有边仅显示特征边
生成方式WireframeGeometryEdgesGeometry
应用场景结构分析轮廓强调
性能消耗
可配置性颜色/线宽角度阈值/颜色

2. 边缘检测原理

graph TD
    A[输入几何体] --> B[遍历所有边]
    B --> C{相邻面夹角 > 阈值?}
    C -->|是| D[标记为特征边]
    C -->|否| E[忽略该边]
    D --> F[生成边缘几何体]

📌 使用技巧

  1. 性能优化
// 复用几何体数据
const sharedGeometry = new THREE.EdgesGeometry(baseGeometry);
const edges1 = new THREE.LineSegments(sharedGeometry, material1);
const edges2 = new THREE.LineSegments(sharedGeometry, material2);
  1. 动态更新
// 修改基础几何体后更新
baseGeometry.attributes.position.needsUpdate = true;
edges.geometry = new THREE.EdgesGeometry(baseGeometry);
  1. 高级效果
// 发光边缘效果
const edgesMaterial = new THREE.LineMaterial({
  color: 0x00ff00,
  linewidth: 3,
  dashed: true,
  dashSize: 1,
  gapSize: 0.5
});

⚠️ 常见问题

Q:线框显示不完整?
A:检查材质设置:

material.wireframe = true;  // 必须设置线框模式
renderer.setPixelRatio(window.devicePixelRatio); // 解决抗锯齿问题

Q:边缘几何体缺失边?
A:调整阈值角度:

new THREE.EdgesGeometry(geometry, { thresholdAngle: 30 }); // 默认15度

Q:线条显示锯齿?
A:启用抗锯齿:

new THREE.WebGLRenderer({ 
  antialias: true,
  powerPreference: "high-performance"
});

2025-03-30T08_41_23.574Z-284259.gif