移动/复制实体:空间中的平移、旋转与阵列复制基础操作
摘要
在三维空间开发与计算机图形学中,实体(Entity)的移动与复制是最基础也是最核心的操作之一。无论是游戏开发中的角色控制、CAD软件中的建模操作,还是三维可视化应用中的场景搭建,都离不开对实体进行平移、旋转以及阵列复制的能力。本文将深入探讨这些基础操作的原理与实现,提供完整的代码示例,帮助读者建立起对三维空间变换的扎实理解。
引言
当我们第一次接触三维世界时,最直观的感受就是"物体可以在空间中自由移动"。然而,这种看似简单的操作背后,隐藏着线性代数、坐标变换、矩阵运算等一系列技术挑战。实体操作主要包含三个基本类型:
- 平移(Translation):改变实体在空间中的位置
- 旋转(Rotation):改变实体的朝向
- 阵列复制(Array Duplication):按照特定规则批量复制实体
这些操作在游戏引擎(如Unity、Unreal)、三维建模软件(如Blender、3ds Max)、以及Web三维库(如Three.js)中都有广泛的应用。本文将使用Three.js作为示例库,因为它提供了直观的API和良好的可视化效果。
1. 准备工作:搭建三维场景
在开始操作实体之前,我们需要一个基础的三维场景。这里我们使用Three.js创建一个包含相机、灯光和参考网格的场景。
1.1 基础场景代码
javascript
// 引入Three.js核心库和扩展库
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import { CSS2DRenderer, CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer.js';
// 创建场景
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x1a1a2e);
// 创建相机(透视相机)
const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(10, 8, 10);
camera.lookAt(0, 0, 0);
// 创建WebGL渲染器
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.setPixelRatio(window.devicePixelRatio);
document.body.appendChild(renderer.domElement);
// 创建CSS2D渲染器(用于显示文字标签)
const labelRenderer = new CSS2DRenderer();
labelRenderer.setSize(window.innerWidth, window.innerHeight);
labelRenderer.domElement.style.position = 'absolute';
labelRenderer.domElement.style.top = '0px';
labelRenderer.domElement.style.left = '0px';
labelRenderer.domElement.style.pointerEvents = 'none'; // 允许点击穿透
document.body.appendChild(labelRenderer.domElement);
// 轨道控制器(方便观察)
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
// 添加环境光和定向光
const ambientLight = new THREE.AmbientLight(0x404040);
scene.add(ambientLight);
const dirLight = new THREE.DirectionalLight(0xffffff, 1);
dirLight.position.set(5, 10, 7);
dirLight.castShadow = true;
scene.add(dirLight);
// 添加辅助网格和坐标轴
const gridHelper = new THREE.GridHelper(20, 20, 0x888888, 0x444444);
scene.add(gridHelper);
const axesHelper = new THREE.AxesHelper(5);
scene.add(axesHelper);
// 动画循环
function animate() {
requestAnimationFrame(animate);
controls.update();
// 先渲染WebGL内容
renderer.render(scene, camera);
// 再渲染CSS2D标签(覆盖在3D内容之上)
labelRenderer.render(scene, camera);
}
animate();
// 窗口大小自适应
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
labelRenderer.setSize(window.innerWidth, window.innerHeight);
});
1.2 创建基础实体
为了方便演示,我们创建一个带有标签的立方体作为操作对象:
javascript
// 创建标签辅助函数
function createLabel(text, color = '#ffffff') {
const div = document.createElement('div');
div.textContent = text;
div.style.color = color;
div.style.fontSize = '16px';
div.style.fontWeight = 'bold';
div.style.fontFamily = 'Arial, sans-serif';
div.style.textShadow = '2px 2px 4px rgba(0,0,0,0.8)';
div.style.background = 'rgba(0,0,0,0.5)';
div.style.padding = '4px 8px';
div.style.borderRadius = '4px';
return new CSS2DObject(div);
}
// 创建一个带标签的立方体
function createBoxWithLabel(size, color, position, label) {
const geometry = new THREE.BoxGeometry(size, size, size);
const material = new THREE.MeshStandardMaterial({
color: color,
roughness: 0.3,
metalness: 0.1
});
const mesh = new THREE.Mesh(geometry, material);
mesh.position.copy(position);
mesh.castShadow = true;
mesh.receiveShadow = true;
// 创建标签
const labelObj = createLabel(label);
labelObj.position.set(0, size + 0.5, 0);
mesh.add(labelObj);
return mesh;
}
// 创建原始立方体(红色)
const originalBox = createBoxWithLabel(
1.5,
0xff4444,
new THREE.Vector3(-3, 0, 0),
'原始实体'
);
scene.add(originalBox);
2. 平移操作(Translation)
平移是最基本的空间变换,它改变实体在三维空间中的位置坐标(x, y, z)。在Three.js中,平移可以通过直接修改position属性或使用translateX/Y/Z方法实现。
2.1 平移的原理
在数学上,平移是通过向量加法实现的。假设实体的原始位置为 P = (x, y, z),平移向量为 T = (tx, ty, tz),则平移后的位置为:
P' = P + T = (x + tx, y + ty, z + tz)
2.2 代码实现
javascript
// 创建一个用于演示平移的立方体
const translateBox = createBoxWithLabel(
1.5,
0x44ff44,
new THREE.Vector3(0, 0, 0),
'平移演示'
);
scene.add(translateBox);
// 平移操作演示函数
function demonstrateTranslation() {
console.log('=== 平移操作演示 ===');
// 方法1:直接修改position属性
translateBox.position.x = 2;
translateBox.position.y = 1;
translateBox.position.z = 1;
// 方法2:使用set方法批量设置
// translateBox.position.set(2, 1, 1);
console.log(`平移后位置: (${translateBox.position.x.toFixed(2)}, ${translateBox.position.y.toFixed(2)}, ${translateBox.position.z.toFixed(2)})`);
// 方法3:相对平移(在当前位置基础上移动)
setTimeout(() => {
translateBox.position.x += 1; // 再向右移动1单位
console.log(`再次平移后位置: (${translateBox.position.x.toFixed(2)}, ${translateBox.position.y.toFixed(2)}, ${translateBox.position.z.toFixed(2)})`);
}, 2000);
}
// 执行演示
demonstrateTranslation();
2.3 世界坐标与局部坐标平移
在三维空间中,平移可以分为世界坐标平移和局部坐标平移:
javascript
// 创建一个子实体来演示局部平移
const parentGroup = new THREE.Group();
parentGroup.position.set(-2, 0, 3);
scene.add(parentGroup);
const childBox = createBoxWithLabel(
1.0,
0x4444ff,
new THREE.Vector3(0, 0, 0),
'子实体'
);
parentGroup.add(childBox);
// 世界坐标平移(相对于世界原点)
function worldSpaceTranslation() {
// 将实体移动到世界坐标 (3, 2, 0)
childBox.position.set(3, 2, 0); // 注意:这是在父级局部空间中的位置
console.log(`子实体在父级局部空间中的位置: (${childBox.position.x}, ${childBox.position.y}, ${childBox.position.z})`);
// 获取世界坐标
const worldPos = new THREE.Vector3();
childBox.getWorldPosition(worldPos);
console.log(`子实体在世界空间中的位置: (${worldPos.x.toFixed(2)}, ${worldPos.y.toFixed(2)}, ${worldPos.z.toFixed(2)})`);
}
// 局部坐标平移(相对于父级)
function localSpaceTranslation() {
// 重置位置
childBox.position.set(0, 0, 0);
// 在父级局部空间中平移
childBox.translateX(1.5);
childBox.translateY(0.5);
console.log(`局部平移后位置: (${childBox.position.x}, ${childBox.position.y}, ${childBox.position.z})`);
}
3. 旋转操作(Rotation)
旋转操作使实体围绕某个轴转动。在三维空间中,旋转比平移复杂得多,因为它涉及到角度表示和旋转顺序的问题。
3.1 旋转的表示方法
Three.js支持三种主要的旋转表示方法:
- 欧拉角(Euler Angles):使用绕X、Y、Z轴的旋转角度表示
- 四元数(Quaternion):使用复数扩展的数学表示,避免万向锁
- 旋转矩阵(Rotation Matrix):使用3x3矩阵表示
3.2 欧拉角旋转
javascript
// 创建用于演示旋转的立方体
const rotateBox = createBoxWithLabel(
1.5,
0xffaa44,
new THREE.Vector3(3, 0, 0),
'旋转演示'
);
scene.add(rotateBox);
// 欧拉角旋转演示
function demonstrateEulerRotation() {
console.log('=== 欧拉角旋转演示 ===');
// 方法1:直接设置欧拉角(弧度制)
rotateBox.rotation.x = Math.PI / 4; // 绕X轴旋转45度
rotateBox.rotation.y = Math.PI / 3; // 绕Y轴旋转60度
rotateBox.rotation.z = 0;
console.log(`旋转角度: X=${(rotateBox.rotation.x * 180 / Math.PI).toFixed(1)}°, Y=${(rotateBox.rotation.y * 180 / Math.PI).toFixed(1)}°`);
// 方法2:使用set方法
// rotateBox.rotation.set(Math.PI/4, Math.PI/3, 0);
// 方法3:使用角度制(需要转换)
// rotateBox.rotation.set(
// THREE.MathUtils.degToRad(45),
// THREE.MathUtils.degToRad(60),
// 0
// );
}
// 演示旋转顺序的影响
function demonstrateRotationOrder() {
console.log('=== 旋转顺序演示 ===');
// 创建两个相同的立方体,使用不同的旋转顺序
const box1 = createBoxWithLabel(1.2, 0xff0000, new THREE.Vector3(5, 2, 0), '顺序XYZ');
const box2 = createBoxWithLabel(1.2, 0x00ff00, new THREE.Vector3(5, -2, 0), '顺序ZYX');
scene.add(box1);
scene.add(box2);
// 设置相同的旋转角度,但顺序不同
box1.rotation.order = 'XYZ';
box1.rotation.set(Math.PI/2, Math.PI/4, Math.PI/6);
box2.rotation.order = 'ZYX';
box2.rotation.set(Math.PI/2, Math.PI/4, Math.PI/6);
console.log(`Box1 (XYZ) 最终旋转: (${box1.rotation.x.toFixed(3)}, ${box1.rotation.y.toFixed(3)}, ${box1.rotation.z.toFixed(3)})`);
console.log(`Box2 (ZYX) 最终旋转: (${box2.rotation.x.toFixed(3)}, ${box2.rotation.y.toFixed(3)}, ${box2.rotation.z.toFixed(3)})`);
}
3.3 四元数旋转(避免万向锁)
javascript
// 四元数旋转演示
function demonstrateQuaternionRotation() {
console.log('=== 四元数旋转演示 ===');
const quatBox = createBoxWithLabel(
1.5,
0x44aaff,
new THREE.Vector3(-3, 2, 3),
'四元数旋转'
);
scene.add(quatBox);
// 方法1:使用轴角创建四元数
const axis = new THREE.Vector3(1, 1, 0).normalize(); // 旋转轴
const angle = Math.PI / 3; // 旋转60度
const quaternion = new THREE.Quaternion();
quaternion.setFromAxisAngle(axis, angle);
quatBox.quaternion.copy(quaternion);
console.log(`四元数: (${quaternion.x.toFixed(3)}, ${quaternion.y.toFixed(3)}, ${quaternion.z.toFixed(3)}, ${quaternion.w.toFixed(3)})`);
// 方法2:使用欧拉角创建四元数
const euler = new THREE.Euler(Math.PI/4, Math.PI/3, 0);
const quatFromEuler = new THREE.Quaternion().setFromEuler(euler);
// 方法3:四元数插值(Slerp)- 用于平滑旋转动画
const startQuat = new THREE.Quaternion().setFromAxisAngle(
new THREE.Vector3(0, 1, 0), 0
);
const endQuat = new THREE.Quaternion().setFromAxisAngle(
new THREE.Vector3(0, 1, 0), Math.PI
);
// 在0到1之间插值
const t = 0.5; // 中间状态
const interpolatedQuat = new THREE.Quaternion().slerpQuaternions(
startQuat, endQuat, t
);
console.log(`插值四元数(t=0.5): (${interpolatedQuat.x.toFixed(3)}, ${interpolatedQuat.y.toFixed(3)}, ${interpolatedQuat.z.toFixed(3)}, ${interpolatedQuat.w.toFixed(3)})`);
}
4. 阵列复制(Array Duplication)
阵列复制是按照特定规则批量复制实体的操作。常见的阵列类型包括线性阵列、环形阵列和矩形阵列。
4.1 线性阵列
线性阵列沿直线方向复制实体,可以控制数量和间距。
javascript
// 线性阵列函数
function createLinearArray(originalMesh, count, spacing, direction) {
const clones = [];
const directionVec = direction.clone().normalize();
for (let i = 0; i < count; i++) {
// 克隆实体(深拷贝)
const clone = originalMesh.clone();
// 计算位置偏移
const offset = directionVec.clone().multiplyScalar(i * spacing);
clone.position.copy(originalMesh.position).add(offset);
// 添加到场景
scene.add(clone);
clones.push(clone);
}
return clones;
}
// 创建线性阵列演示
function demonstrateLinearArray() {
console.log('=== 线性阵列演示 ===');
// 创建一个模板实体
const template = createBoxWithLabel(
0.8,
0xff66aa,
new THREE.Vector3(-6, 0, -3),
'模板'
);
scene.add(template);
// 沿X轴创建5个副本,间距2单位
const xArray = createLinearArray(
template,
5,
2,
new THREE.Vector3(1, 0, 0)
);
console.log(`沿X轴创建了 ${xArray.length} 个副本`);
// 沿对角线方向创建副本
const diagArray = createLinearArray(
template,
4,
1.5,
new THREE.Vector3(1, 1, 0)
);
console.log(`沿对角线创建了 ${diagArray.length} 个副本`);
}
4.2 环形阵列
环形阵列围绕中心点旋转复制实体。
javascript
// 环形阵列函数
function createCircularArray(originalMesh, count, radius, center) {
const clones = [];
const angleStep = (2 * Math.PI) / count;
for (let i = 0; i < count; i++) {
const clone = originalMesh.clone();
// 计算角度
const angle = i * angleStep;
// 计算位置
const x = center.x + radius * Math.cos(angle);
const z = center.z + radius * Math.sin(angle);
clone.position.set(x, center.y, z);
// 可选:让实体朝向中心
clone.lookAt(center);
scene.add(clone);
clones.push(clone);
}
return clones;
}
// 创建环形阵列演示
function demonstrateCircularArray() {
console.log('=== 环形阵列演示 ===');