Three.js 坐标系系统与单位理解教程
在上一篇文章中,我们学习了three.js的基础三大组件:Three.js三大组件:场景(Scene)、相机(Camera)、渲染器(Renderer)https://blog.csdn.net/XXCJLRC/article/details/150447375?fromshare=blogdetail&sharetype=blogdetail&sharerId=150447375&sharerefer=PC&sharesource=XXCJLRC&sharefrom=from_link
在这篇文章,我们开始学习Three.js 中的坐标系系统与单位。
一、Three.js 坐标系系统
Three.js 使用右手坐标系系统(Right-Handed Coordinate System),这是计算机图形学中的标准:
-
X轴:水平向右为正方向
-
Y轴:垂直向上为正方向
-
Z轴:垂直于屏幕向观察者方向为正方向
1.坐标系类型
Three.js中有几种重要的坐标系:
(1)世界坐标系(World Coordinate System)
-
场景的全局坐标系
-
所有对象的最终位置都是相对于世界坐标系表示的
-
原点(0,0,0)是场景的中心点
(2)局部坐标系(Local Coordinate System)
-
每个对象都有自己的局部坐标系
-
对象的变换(位置、旋转、缩放)都是相对于其父对象的局部坐标系
-
当没有父对象时,局部坐标系与世界坐标系重合
(3)相机坐标系(Camera Coordinate System)
-
以相机为原点的坐标系
-
Z轴指向相机观察方向的反方向
-
用于确定哪些对象在相机视野内
(4)屏幕坐标系(Screen Coordinate System)
-
2D坐标系,用于渲染最终图像
-
X轴向右,Y轴向下
-
原点在左上角,范围通常为[-1,1](标准化设备坐标)或[0,width/height](像素坐标)
2、 坐标系转换
Three.js提供了在不同坐标系间转换的方法:
// 世界坐标转局部坐标
object.worldToLocal(vector);// 局部坐标转世界坐标
object.localToWorld(vector);// 世界坐标转屏幕坐标
vector.project(camera);// 屏幕坐标转世界坐标
vector.unproject(camera);
二、Three.js 中的单位系统
Three.js没有强制规定特定的物理单位,但理解单位的使用方式对于创建比例正确的场景非常重要。
1. 无固定单位系统
Three.js采用"单位无关"系统:
-
1个单位可以代表1米、1厘米或1千米,取决于你的设计约定
-
保持场景中所有对象使用一致的单位比例至关重要
2. 常用单位参考
虽然灵活,但通常遵循这些约定:
场景类型 | 推荐单位 | 说明 |
---|---|---|
室内场景 | 米 | 符合人类日常感知 |
建筑可视化 | 米 | 与CAD软件标准一致 |
微观场景 | 厘米/毫米 | 适合小物体 |
地理/天文场景 | 千米 | 适合大规模场景 |
抽象/艺术场景 | 任意 | 只需保持内部一致性 |
3. 实际应用中的单位考虑
(1) 相机设置
相机的近剪裁平面和远剪裁平面应与场景单位匹配:
// 对于以米为单位的室内场景
const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000);
// 近剪裁面0.1米,远剪裁面1000米
(2) 光照衰减
光的衰减参数需要根据场景比例调整:
const light = new THREE.PointLight(0xffffff, 1, 100);
// 衰减距离100单位,应与场景尺寸匹配
(3) 物理模拟
当使用物理引擎时,必须明确单位:
// Cannon.js示例 - 通常假设1单位=1米
const sphereShape = new CANNON.Sphere(0.5); // 半径0.5米
const sphereBody = new CANNON.Body({ mass: 5 }); // 5千克
4. 单位转换技巧
在不同系统间转换时:
// 假设1单位=1厘米,但需要以米为单位导出
object.scale.set(0.01, 0.01, 0.01); // 转换为米// 或者调整整个场景的比例
scene.scale.set(100, 100, 100); // 1单位=1米变为1单位=1厘米
三、实际应用示例
1. 创建比例正确的室内场景
// 假设1单位=1米
const roomWidth = 5; // 5米宽
const roomHeight = 3; // 3米高
const roomDepth = 4; // 4米深const roomGeometry = new THREE.BoxGeometry(roomWidth, roomHeight, roomDepth);
const room = new THREE.Mesh(roomGeometry, material);// 添加一个0.8米高的桌子
const table = new THREE.Mesh(new THREE.BoxGeometry(1.2, 0.8, 0.6),tableMaterial
);
table.position.set(0, 0.4, 0); // 桌子中心在0.4米高度
2. 坐标系转换实践
// 获取对象在世界坐标系中的位置
const worldPosition = new THREE.Vector3();
object.getWorldPosition(worldPosition);// 将世界坐标转换为屏幕坐标
const screenPosition = worldPosition.clone().project(camera);// 转换为标准化设备坐标(NDC) [-1,1] 到 [0,1]
const x = (screenPosition.x + 1) / 2 * window.innerWidth;
const y = (-(screenPosition.y - 1) / 2) * window.innerHeight;
3. 处理不同单位的模型
// 当导入的模型单位不一致时(如1单位=1厘米)
const model = await loadModel(); // 假设模型以厘米为单位// 转换为米为单位
model.scale.set(0.01, 0.01, 0.01);// 或者调整整个场景
scene.scale.set(100, 100, 100); // 现在1场景单位=1厘米
4.完整案例
<template><div ref="container" class="three-container"><div class="controls"><div><label>单位比例: </label><select v-model="unitScale"><option value="1">1单位 = 1米</option><option value="0.01">1单位 = 1厘米</option><option value="1000">1单位 = 1千米</option></select></div><div><button @click="toggleAxesHelper">切换坐标轴显示</button><button @click="toggleGridHelper">切换网格显示</button></div><div class="coord-info"><p>世界坐标: {{ worldCoord.x.toFixed(2) }}, {{ worldCoord.y.toFixed(2) }}, {{ worldCoord.z.toFixed(2) }}</p><p>屏幕坐标: {{ screenCoord.x.toFixed(2) }}, {{ screenCoord.y.toFixed(2) }}</p></div></div></div>
</template><script setup>
import { ref, onMounted, onUnmounted, watch } from 'vue';
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { useMouse } from '@vueuse/core';// 场景元素引用
const container = ref(null);// 单位比例
const unitScale = ref('1');
const scaleFactor = ref(1);// 坐标信息
const worldCoord = ref({ x: 0, y: 0, z: 0 });
const screenCoord = ref({ x: 0, y: 0 });// 鼠标位置
const { x: mouseX, y: mouseY } = useMouse();// Three.js 相关变量
let scene, camera, renderer, controls;
let room, table, chair;
let axesHelper, gridHelper;
let raycaster = new THREE.Raycaster();// 初始化场景
function initScene() {// 创建场景scene = new THREE.Scene();scene.background = new THREE.Color(0xf0f0f0);// 创建相机 (使用透视相机)camera = new THREE.PerspectiveCamera(75, container.value.clientWidth / container.value.clientHeight, 0.1 * scaleFactor.value, 1000 * scaleFactor.value);camera.position.set(2 * scaleFactor.value, 2 * scaleFactor.value, 5 * scaleFactor.value);// 创建渲染器renderer = new THREE.WebGLRenderer({ antialias: true });console.log("Canvas element:", renderer.domElement);renderer.setSize(container.value.clientWidth, container.value.clientHeight);renderer.shadowMap.enabled = true;container.value.appendChild(renderer.domElement);// 添加轨道控制器controls = new OrbitControls(camera, renderer.domElement);controls.enableDamping = true;// 添加光源addLights();// 添加坐标系辅助axesHelper = new THREE.AxesHelper(2 * scaleFactor.value);scene.add(axesHelper);// 添加网格辅助gridHelper = new THREE.GridHelper(10 * scaleFactor.value, 10);scene.add(gridHelper);// 创建房间和家具createRoom();// 开始动画循环animate();
}// 添加光源
function addLights() {// 环境光const ambientLight = new THREE.AmbientLight(0x404040);scene.add(ambientLight);// 平行光 (模拟太阳光)const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);directionalLight.position.set(5 * scaleFactor.value, 10 * scaleFactor.value, 7 * scaleFactor.value);directionalLight.castShadow = true;directionalLight.shadow.mapSize.width = 2048;directionalLight.shadow.mapSize.height = 2048;scene.add(directionalLight);// 点光源 (模拟台灯)const pointLight = new THREE.PointLight(0xffaa00, 1, 10 * scaleFactor.value);pointLight.position.set(0, 1.5 * scaleFactor.value, 0);pointLight.castShadow = true;scene.add(pointLight);
}// 创建房间和家具
function createRoom() {// 清除现有对象if (room) scene.remove(room);if (table) scene.remove(table);if (chair) scene.remove(chair);// 房间尺寸 (假设1单位=1米)const roomWidth = 5 * scaleFactor.value;const roomHeight = 3 * scaleFactor.value;const roomDepth = 4 * scaleFactor.value;// 创建房间 (只显示内部,所以反转法线)const roomGeometry = new THREE.BoxGeometry(roomWidth, roomHeight, roomDepth);const roomMaterial = new THREE.MeshStandardMaterial({ color: 0xcccccc,side: THREE.BackSide,roughness: 0.8});room = new THREE.Mesh(roomGeometry, roomMaterial);room.receiveShadow = true;scene.add(room);// 创建桌子 (1.2m长, 0.8m高, 0.6m宽)const tableGeometry = new THREE.BoxGeometry(1.2 * scaleFactor.value, 0.8 * scaleFactor.value, 0.6 * scaleFactor.value);const tableMaterial = new THREE.MeshStandardMaterial({ color: 0x8B4513,roughness: 0.7});table = new THREE.Mesh(tableGeometry, tableMaterial);table.position.set(0, 0.4 * scaleFactor.value, 0);table.castShadow = true;scene.add(table);// 创建椅子 (0.4m长, 0.4m高, 0.4m宽)const chairGeometry = new THREE.BoxGeometry(0.4 * scaleFactor.value, 0.4 * scaleFactor.value, 0.4 * scaleFactor.value);const chairMaterial = new THREE.MeshStandardMaterial({ color: 0x4682B4,roughness: 0.6});chair = new THREE.Mesh(chairGeometry, chairMaterial);chair.position.set(-0.8 * scaleFactor.value, 0.2 * scaleFactor.value, 0);chair.castShadow = true;scene.add(chair);
}// 动画循环
function animate() {requestAnimationFrame(animate);// 更新控制器controls.update();// 更新射线检测updateRaycaster();// 渲染场景renderer.render(scene, camera);
}// 更新射线检测 (用于坐标转换演示)
function updateRaycaster() {// 将鼠标位置归一化为设备坐标 (NDC)const mouse = new THREE.Vector2();mouse.x = (mouseX.value / container.value.clientWidth) * 2 - 1;mouse.y = -(mouseY.value / container.value.clientHeight) * 2 + 1;// 更新射线raycaster.setFromCamera(mouse, camera);// 计算与桌子的交点const intersects = raycaster.intersectObject(table);if (intersects.length > 0) {const point = intersects[0].point;// 存储世界坐标worldCoord.value = {x: point.x,y: point.y,z: point.z};// 转换为屏幕坐标const screenPos = point.clone().project(camera);screenCoord.value = {x: (screenPos.x + 1) / 2 * container.value.clientWidth,y: (-(screenPos.y - 1) / 2) * container.value.clientHeight};}
}// 切换坐标轴显示
function toggleAxesHelper() {axesHelper.visible = !axesHelper.visible;
}// 切换网格显示
function toggleGridHelper() {gridHelper.visible = !gridHelper.visible;
}// 处理窗口大小变化
function onWindowResize() {camera.aspect = container.value.clientWidth / container.value.clientHeight;camera.updateProjectionMatrix();renderer.setSize(container.value.clientWidth, container.value.clientHeight);
}// 监听单位比例变化
watch(unitScale, (newVal) => {scaleFactor.value = parseFloat(newVal);// 更新相机剪裁平面camera.near = 0.1 * scaleFactor.value;camera.far = 1000 * scaleFactor.value;camera.updateProjectionMatrix();// 重新创建场景createRoom();// 更新辅助工具大小axesHelper.scale.set(scaleFactor.value, scaleFactor.value, scaleFactor.value);gridHelper.scale.set(scaleFactor.value, scaleFactor.value, scaleFactor.value);
});// 组件挂载时初始化
onMounted(() => {initScene();window.addEventListener('resize', onWindowResize);
});// 组件卸载时清理
onUnmounted(() => {window.removeEventListener('resize', onWindowResize);if (renderer) {renderer.dispose();}
});
</script><style scoped>
.three-container {position: absolute;left: 0;top: 0;width: 100vw;height: 100vh;overflow: hidden; /* 避免滚动条 */
}.controls {position: absolute;top: 20px;left: 20px;background: rgba(255, 255, 255, 0.8);padding: 10px;border-radius: 5px;z-index: 100;
}.controls > div {margin-bottom: 10px;
}.coord-info {background: #fff;padding: 8px;border-radius: 4px;border: 1px solid #ddd;
}button {margin-right: 5px;padding: 5px 10px;cursor: pointer;
}select {padding: 5px;
}
</style>