一、问题场景:从一次坐标计算bug说起

做3D前端开发的人,大概率都碰过这样的坑:从外部导入的模型,明明看起来位置对,但用代码获取它的坐标、做碰撞检测、或者和其他模型联动时,结果完全不对。比如你想让一个子弹模型打到一个导入的怪物模型上,代码里判断两者坐标距离小于1就触发命中,结果子弹都穿过去了才命中,甚至有时候子弹还没碰到怪物,系统就判定命中了。

我之前做过一个3D寻宝小游戏,就踩过这个坑。当时找设计师做了个宝箱模型,导入Three.js后,把宝箱的position设成(10, 0, 5),然后写了个逻辑:玩家走到宝箱1米范围内,就弹出“找到宝箱”的提示。结果玩家站在宝箱正前方半米的地方没反应,再往前迈一步,直接穿到宝箱里面才弹出提示。一开始我以为是碰撞检测的逻辑写错了,反复检查坐标计算的代码都没问题,最后才发现是模型原点偏移惹的祸。

1.1 什么是模型原点偏移?

简单说,模型原点就是模型自己的“中心基准点”。你可以把它想象成模型的“肚脐”,模型的所有坐标、旋转、缩放都是围绕这个点来算的。比如一个正方体,正常的原点应该在正方体的正中心,你把它的position设成(0,0,0),整个正方体就会以中心为基准,对齐到世界坐标的原点。

但外部导入的模型(比如从Blender、3ds Max导出的gltf、glb文件),原点经常不在模型的正中心,甚至可能跑到模型外面去。比如设计师做宝箱的时候,可能不小心把原点放到了宝箱的底部,或者干脆放到了场景的某个角落,导出的时候没改过来。这就导致你在Three.js里设置模型的position,其实是在调整这个偏移原点的位置,而不是你看到的模型本身的位置,坐标计算自然就乱了。

1.2 坐标计算异常的具体表现

原点偏移带来的坐标问题,主要有三种常见情况: 第一,模型的实际位置和你设置的position不匹配。比如你设position为(10,0,5),模型看起来却在(9.5, 0, 4.8)的位置,因为原点不在模型中心,整个模型相对于原点偏移了。 第二,碰撞检测误差大。比如两个模型的原点距离是2米,但模型的实际表面距离只有0.5米,或者反过来,表面距离2米,原点距离只有0.5米,碰撞逻辑就完全不准。 第三,模型变换出错。比如你想让模型绕自身中心旋转,结果模型却绕着一个奇怪的点转,甚至转着转着就跑到画面外面去了。

二、核心原理:Three.js的坐标体系与变换矩阵

要解决原点偏移的问题,得先搞懂Three.js里的坐标是怎么算的。不用怕,我用生活化的例子给你讲明白。

2.1 世界坐标、本地坐标与模型原点的关系

Three.js里有两个核心的坐标概念:世界坐标和本地坐标。 世界坐标可以理解为整个3D场景的“大地图原点”,所有模型的最终位置,都是以这个大地图的原点为基准来算的。 本地坐标是每个模型自己的“小地图原点”,也就是模型的原点。模型的所有属性(position、rotation、scale),都是基于自己的本地坐标来设置的。

举个例子:你在一个大操场(世界坐标)上,操场的原点在操场的正中心。你(模型)的口袋里有个小指南针(本地坐标),小指南针的原点(模型原点)不在你身体的正中心,而是在你左手边1米的位置。当你站在操场的正中心(世界坐标(0,0,0))时,你自己的实际位置是对的,但如果有人问“你站在操场的哪个位置”,你用左手边的小指南针算出来的位置,就会是左手边1米的位置,这就是模型原点偏移导致的坐标差。

2.2 变换矩阵的作用:把本地坐标转成世界坐标

Three.js里,每个模型都有一个变换矩阵,这个矩阵的作用,就是把模型的本地坐标,转换成世界坐标。变换矩阵包含了模型的位置、旋转、缩放三种信息,是3D坐标计算的核心。

简单说,变换矩阵就像一个“翻译器”:模型自己说“我在本地坐标的(0,0,0)点”,翻译器就会把这句话翻译成“在世界坐标的(10,0,5)点”。如果模型的本地原点偏移了,这个翻译器翻译出来的结果,自然就不对了。

三、解决方案:统一坐标基线与修正变换矩阵

知道了问题的根源,接下来就讲两种解决方法,一种是统一坐标基线(适合导入模型前处理),另一种是修正变换矩阵(适合导入模型后处理),两种方法都有完整的代码示例。

3.1 方法一:统一坐标基线——导入前修正模型原点

这是最推荐的方法,就是在把模型导出成gltf/glb之前,先把模型的原点调整到你想要的位置(比如模型的正中心、底部中心等),这样导入Three.js后,模型的本地坐标就是正常的,不会有偏移。

完整示例:Blender中修正模型原点

技术栈:Blender 3.6 + Three.js 0.158.0 我以Blender为例,给你讲具体操作步骤,非常简单:

  1. 打开Blender,导入你的模型(比如宝箱模型)。
  2. 选中模型,按快捷键Tab进入编辑模式,选中模型的所有顶点(按A键全选)。
  3. 按快捷键Shift+S,弹出的菜单里选择“游标到选中项”,这一步会把Blender的游标(相当于Blender里的坐标原点)移动到模型的几何中心。
  4. 再按Tab退出编辑模式,回到物体模式,选中模型,按快捷键Ctrl+Alt+Shift+C,弹出的菜单里选择“原点到游标”,这一步就把模型的原点设置到了模型的几何中心。
  5. 最后导出模型为glb格式,导入Three.js即可。

Three.js中验证模型原点

把修正后的模型导入Three.js,写一段代码验证坐标是否正常:

// 技术栈:Three.js 0.158.0
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.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 loader = new GLTFLoader();
let treasureBox;
loader.load('treasure-box-fixed.glb', (gltf) => {
  treasureBox = gltf.scene;
  // 设置宝箱的世界坐标为(10, 0, 5)
  treasureBox.position.set(10, 0, 5);
  scene.add(treasureBox);

  // 验证坐标:打印宝箱的世界坐标和本地坐标
  console.log('宝箱世界坐标:', treasureBox.position); // 预期输出:x=10, y=0, z=5
  console.log('宝箱本地坐标:', treasureBox.children[0].position); // 预期输出:x=0, y=0, z=0(原点修正后,模型的子物体本地坐标为0)
});

// 动画循环
function animate() {
  requestAnimationFrame(animate);
  renderer.render(scene, camera);
}
animate();

3.2 方法二:修正变换矩阵——导入后调整模型坐标

如果模型已经导入,没法再修改源文件,或者需要动态调整模型的原点,就可以用修正变换矩阵的方法。核心思路是:把模型的本地原点平移到正确的位置,然后更新变换矩阵,让Three.js的坐标计算基于新的原点。

完整示例:动态修正模型原点

技术栈:Three.js 0.158.0 比如我们导入一个原点偏移的宝箱模型,原点在模型的底部,现在要把原点调整到模型的正中心,代码如下:

// 技术栈:Three.js 0.158.0
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.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 loader = new GLTFLoader();
let treasureBox;
loader.load('treasure-box-offset.glb', (gltf) => {
  treasureBox = gltf.scene;
  scene.add(treasureBox);

  // 步骤1:获取模型的包围盒,计算模型的几何中心
  const box = new THREE.Box3().setFromObject(treasureBox);
  const center = new THREE.Vector3();
  box.getCenter(center); // center就是模型的几何中心的世界坐标

  // 步骤2:计算原点偏移量:把原来的原点(模型的position)平移到几何中心
  // 这里的offset是本地坐标下的偏移,需要把世界坐标的center转成本地坐标
  const offset = new THREE.Vector3();
  offset.subVectors(center, treasureBox.position); // 计算偏移量:几何中心 - 原原点位置

  // 步骤3:更新模型的变换矩阵,修正原点
  // 把模型的本地坐标平移offset,相当于把原点移到几何中心
  treasureBox.position.sub(offset); // 调整模型的位置,抵消偏移
  // 遍历模型的所有子物体,调整子物体的本地坐标
  treasureBox.traverse((child) => {
    if (child.isMesh) {
      child.position.sub(offset);
    }
  });
  // 强制更新模型的变换矩阵
  treasureBox.updateMatrix();
  treasureBox.updateMatrixWorld(true);

  // 验证坐标:现在设置宝箱的位置为(10, 0, 5)
  treasureBox.position.set(10, 0, 5);
  console.log('修正后宝箱的世界坐标:', treasureBox.position); // 预期输出:x=10, y=0, z=5
  console.log('修正后模型几何中心的世界坐标:', center); // 预期输出:x=10, y=0, z=5(原点移到了几何中心)
});

// 动画循环
function animate() {
  requestAnimationFrame(animate);
  renderer.render(scene, camera);
}
animate();

四、应用场景、优缺点与注意事项

4.1 应用场景

这两种方法适用于所有需要精确坐标计算的3D场景,常见的有:

  • 3D游戏的碰撞检测、命中判定、角色移动;
  • 3D可视化项目(比如数字孪生、工业仿真)的设备定位、数据绑定;
  • 3D互动项目(比如AR/VR)的模型交互、位置同步;
  • 3D编辑器的模型对齐、批量变换。

4.2 技术优缺点

方法 优点 缺点
统一坐标基线(导入前修正) 1. 一次修改,永久有效,所有项目都可以复用修正后的模型;2. 坐标计算逻辑简单,性能好;3. 不会出现后续变换的兼容问题 1. 需要修改源模型,适合有模型编辑权限的情况;2. 批量处理大量模型时,操作比较繁琐
修正变换矩阵(导入后修正) 1. 不需要修改源模型,适合没有模型编辑权限的情况;2. 可以动态调整原点,适合需要灵活变换的场景 1. 每个项目都需要写修正代码,重复劳动;2. 变换矩阵更新不当,可能导致模型旋转、缩放出错;3. 复杂模型的遍历调整,性能略低

4.3 注意事项

  1. 修正模型原点时,要根据实际需求选择原点位置:比如角色模型的原点适合放在脚底(方便移动时对齐地面),家具模型的原点适合放在底部中心(方便摆放时对齐),道具模型的原点适合放在正中心(方便旋转、碰撞)。
  2. 用变换矩阵修正时,一定要注意坐标的转换:世界坐标转本地坐标、本地坐标转世界坐标的逻辑不能搞反,否则会导致模型位置出错。
  3. 修正后一定要验证坐标:可以在场景中添加一个辅助的立方体,放在你预期的位置,看模型是否和立方体对齐,或者打印坐标验证。
  4. 批量处理模型时,可以把修正逻辑封装成工具函数,提高开发效率。

五、文章总结

模型原点偏移导致的坐标计算异常,本质上是模型的本地坐标基准和世界坐标基准不匹配的问题。解决这个问题的核心,就是统一两个坐标体系的基准:要么在导入前把模型的本地原点调整到正确的位置(统一坐标基线),要么在导入后通过修正变换矩阵,把模型的坐标计算基准调整到正确的位置。

不管用哪种方法,核心都是搞清楚Three.js的坐标体系和变换矩阵的逻辑,只要抓住了这个核心,就能轻松解决坐标计算的问题,避免踩坑。