当前位置: 首页 > news >正文

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>

实现效果:

http://www.dtcms.com/a/338111.html

相关文章:

  • 安装pnpm i -D @types/wechat-miniprogram报错,版本不匹配
  • 使用 Zed + Qwen Code 搭建轻量化 AI 编程 IDE
  • 【CF】Day129——杂题 (状压DP + 图论 | 贪心 + 数论 + 构造 | 构造 + 贪心 | 构造 + 模拟)
  • Python装饰器:从入门到精通
  • 【STM32】SPI 与 Flash 笔记
  • 【深度长文】Anthropic发布Prompt Engineering全新指南
  • 启发式合并
  • 1、代码相关优化建议
  • 数据分析进阶——解读文本分析模型【附全文阅读】
  • 第十六届蓝桥杯青少组C++省赛[2025.8.10]第二部分编程题(5、环形取硬币游戏)
  • 虚幻基础:动作时间窗
  • Kafka文件存储机制
  • 录音转文字,如何做到“快、准、狠“多格式通吃?
  • 自学中医笔记(二)
  • 大模型对齐算法(四): DAPO,VAPO,GMPO,GSPO, CISPO,GFPO
  • 如何平衡电竞酒店和高校宿舍对AI云电竞游戏盒子的不同需求?
  • 【Python】Python 多进程与多线程:从原理到实践
  • NVIDIA CWE 2025 上海直击:从 GPU 集群到 NeMo 2.0,企业 AI 智能化的加速引擎
  • 软件定义汽车---创新与差异化之路
  • C/C++ 中 str、str、*str 在指针语境下的具体含义(以 char* str 为例):
  • 深化中东战略承诺,联想集团宣布在利雅得设区域总部
  • wait / notify、单例模式
  • 【深度学习基础】PyTorch Tensor生成方式及复制方法详解
  • 【每日一题】Day 7
  • Linux——进程间、线程间的通信
  • 【C++】 using声明 与 using指示
  • 《彩色终端》诗解——ANSI 艺术解码(DeepSeek)
  • C++设计模式:建造者模式
  • 《若依》权限控制
  • ESP32小智-语音活动(VAD)检测流程