前言
小编今天学了几节,分享给大家。也可以回顾一下往节。
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[下一帧请求]
也有更多好玩的功能,可以拿好兄弟的照片恶搞:
// 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();
三维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;
}
🌈 效果对比演示
法向量影响表现
法线状态 | 光照效果 | 材质表现 |
---|---|---|
原始法线 | 平滑渐变 | 自然高光 |
随机扰动法线 | 表面凹凸感 | 金属质感 |
垂直统一法线 | 均匀亮度 | 无立体感 |
📌 调试技巧
-
长度调节:通过修改辅助器长度参数适配场景
normalsHelper.size = 2; // 实时生效
-
颜色区分:使用不同颜色区分面法线和顶点法线
new FaceNormalsHelper(mesh, 2, 0x00ff00); // 面法线
-
选择查看:针对特定顶点调试
// 仅显示第一个顶点的法线 normalsHelper.setVisible(false); normalsHelper.setVisible(true, 0);
⚠️ 常见问题排查
问题:法线辅助器不显示
解决方案:
- 检查辅助器是否加入场景
- 确认法线长度不为0
- 验证几何体包含有效法线数据
问题:修改法线后无变化
检查清单:
- 确认
needsUpdate = true
- 检查材质是否响应法线(如MeshPhongMaterial)
- 确保未启用
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();
🎯 关键代码解析
1. 包围盒更新流程
sequenceDiagram
participant 用户操作
participant 物体变换
participant 包围盒系统
用户操作->>物体变换: 改变位置/旋转/缩放
物体变换->>包围盒系统: updateMatrixWorld(true)
包围盒系统->>包围盒系统: setFromObject(mesh)
包围盒系统->>包围盒系统: applyMatrix4(matrixWorld)
2. 重要方法说明
方法 | 作用 | 调用时机 |
---|---|---|
updateMatrixWorld() | 更新物体及其子元素的世界矩阵 | 物体变换后 |
setFromObject() | 计算物体包围盒 | 需要获取最新包围盒时 |
applyMatrix4() | 应用矩阵变换到包围盒 | 坐标空间转换时 |
📌 使用技巧
-
性能优化:
// 对静态物体只需计算一次 geometry.computeBoundingBox(); const staticBox = geometry.boundingBox.clone();
-
碰撞检测:
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);
-
可视化调试:
// 显示所有包围盒 scene.traverse(obj => { if(obj.isMesh) { scene.add(new THREE.BoxHelper(obj, 0xffff00)); } });
⚠️ 常见问题
Q:为什么包围盒不随物体移动?
A:需要按顺序执行:
mesh.updateMatrixWorld(true)
box.setFromObject(mesh)
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() | 合并多个几何体 | 模型组合 |
📌 使用技巧
-
性能优化:
// 避免频繁计算 const cachedCenter = geometry.boundingBox.getCenter(new THREE.Vector3());
-
顶点遍历:
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); }
-
几何体克隆:
const clonedGeo = geometry.clone(); clonedGeo.scale(2, 2, 2); // 非破坏性操作
⚠️ 常见问题
Q:修改顶点后中心点不更新?
A:需要按顺序执行:
geometry.attributes.position.needsUpdate = true
geometry.computeBoundingBox()
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 | 快速检测 |
📌 使用技巧
- 选择性更新:
// 只更新移动的物体
objects.forEach(obj => {
if(obj.userData.isMoving) {
obj.updateMatrixWorld(true);
boundingBoxes.boxes.set(obj.uuid, new THREE.Box3().setFromObject(obj));
}
});
- 组合包围盒:
// 计算群体包围盒
const groupBox = new THREE.Box3();
objects.forEach(obj => {
groupBox.union(boundingBoxes.boxes.get(obj.uuid));
});
- 射线检测优化:
// 先检测包围盒再检测详细碰撞
const raycaster = new THREE.Raycaster();
const intersects = raycaster.intersectObjects(
objects.filter(obj => {
return raycaster.ray.intersectsBox(boundingBoxes.boxes.get(obj.uuid))
})
);
⚠️ 常见问题
Q:包围盒更新不及时?
A:确保调用顺序:
obj.updateMatrixWorld(true)
box.setFromObject(obj)
Q:如何提高碰撞检测效率?
A:采用分级检测:
- 使用空间划分(八叉树/BVH)
- 先检测球形包围盒
- 最后检测精确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. 几何体类型对比
特性 | 线框几何体 | 边缘几何体 |
---|---|---|
数据结构 | 显示所有边 | 仅显示特征边 |
生成方式 | WireframeGeometry | EdgesGeometry |
应用场景 | 结构分析 | 轮廓强调 |
性能消耗 | 低 | 中 |
可配置性 | 颜色/线宽 | 角度阈值/颜色 |
2. 边缘检测原理
graph TD
A[输入几何体] --> B[遍历所有边]
B --> C{相邻面夹角 > 阈值?}
C -->|是| D[标记为特征边]
C -->|否| E[忽略该边]
D --> F[生成边缘几何体]
📌 使用技巧
- 性能优化:
// 复用几何体数据
const sharedGeometry = new THREE.EdgesGeometry(baseGeometry);
const edges1 = new THREE.LineSegments(sharedGeometry, material1);
const edges2 = new THREE.LineSegments(sharedGeometry, material2);
- 动态更新:
// 修改基础几何体后更新
baseGeometry.attributes.position.needsUpdate = true;
edges.geometry = new THREE.EdgesGeometry(baseGeometry);
- 高级效果:
// 发光边缘效果
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"
});