SolidWorks第四部分_直接实体建模特征1_移动/复制实体

移动/复制实体:空间中的平移、旋转与阵列复制基础操作

摘要

在三维空间开发与计算机图形学中,实体(Entity)的移动与复制是最基础也是最核心的操作之一。无论是游戏开发中的角色控制、CAD软件中的建模操作,还是三维可视化应用中的场景搭建,都离不开对实体进行平移、旋转以及阵列复制的能力。本文将深入探讨这些基础操作的原理与实现,提供完整的代码示例,帮助读者建立起对三维空间变换的扎实理解。


引言

当我们第一次接触三维世界时,最直观的感受就是"物体可以在空间中自由移动"。然而,这种看似简单的操作背后,隐藏着线性代数、坐标变换、矩阵运算等一系列技术挑战。实体操作主要包含三个基本类型:

  1. 平移(Translation):改变实体在空间中的位置
  2. 旋转(Rotation):改变实体的朝向
  3. 阵列复制(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支持三种主要的旋转表示方法:

  1. 欧拉角(Euler Angles):使用绕X、Y、Z轴的旋转角度表示
  2. 四元数(Quaternion):使用复数扩展的数学表示,避免万向锁
  3. 旋转矩阵(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('=== 环形阵列演示 ===');