《HarmonyOS 7 ArkGraphics 3D 空间设计开发实战》02:世界坐标、局部坐标与Transform空间变换【鸿蒙心迹】

桌子设置到了 (1, 0, 0),为什么显示器跟着跑到了别的位置?

前言

上一篇把 SpaceRoom 的基础场景搭起来了:有地面、有立方体、有相机、有灯光。但只要开始往房间里放家具,第一个问题马上就来了。

我在 SpaceRoom 里加了一张桌子,位置设到 (1, 0, 0),看起来没问题。然后往桌子上放显示器,显示器的位置我也设到了 (1, 1, 0)------逻辑很简单:桌子高 1 米,显示器放在桌子中间。

结果跑起来一看,显示器不在桌子上,而是跑到了房间角落。

我当时第一反应是坐标写错了。检查了一遍,没错啊,x=1, y=1, z=0。然后把显示器单独拿出来,位置又对了。问题出在:我把桌子和显示器做成了父子节点关系,桌子是父节点,显示器是子节点。

这就是 3D 开发里最容易踩的坑:世界坐标和局部坐标不是一回事。你给子节点设置的 Position,是相对于父节点的局部坐标,不是相对于整个房间的世界坐标。

这一篇专门解决这个问题:为什么所有家具都直接挂到 Scene 根节点会乱,为什么要建立父子节点结构,以及 Position、Rotation、Scale 在父子节点之间到底怎么叠加。


一、先列清楚:坐标体系里到底有几种坐标

先把概念拆开,不然后面越讲越乱:

坐标类型 参考原点 什么时候用
世界坐标 Scene 根节点的原点 描述一个物体在整个房间里的绝对位置
局部坐标 父节点的原点 描述一个物体相对于父节点的位置
模型坐标 模型自己的原点 描述模型内部顶点的位置

举个例子:桌子放在 (1, 0, 0),显示器放在桌子上。如果显示器是桌子的子节点,那显示器的局部坐标是 (0, 1, 0)------相对于桌子原点,往上 1 米。它的世界坐标才是 (1, 1, 0)。

这就是为什么上一篇我把显示器位置设成 (1, 1, 0) 却跑错了:我以为是世界坐标,实际上它是相对于桌子的局部坐标,所以变成了 (1+1, 0+1, 0+0) = (2, 1, 0)。

这个错误特别常见,尤其是刚开始做 3D 的时候。大家习惯了 ArkUI 里所有组件都直接挂在根节点下面,位置是相对于屏幕的,到了 3D 里一下子不适应了。


二、为什么不能把所有家具都直接挂到根节点

有人会说:那我不用父子节点不就行了?所有家具都直接挂到 Scene 根节点下面,每个都用世界坐标,不就没这个问题了?

短期看确实能跑,但家具一多就会出问题。

比如我要把整个桌子(包括显示器、键盘、台灯)移动到房间另一边。如果它们都是根节点下面的独立节点,我需要手动移动桌子、显示器、键盘、台灯四个节点,一个都不能漏。

更麻烦的是旋转。我想把桌子转个方向,让它对着窗户。如果桌子和显示器是分开的,我转了桌子,显示器还留在原来的方向,看起来就像显示器悬浮在空中对着原来的方向。

这就是为什么要建立父子节点结构:父子节点的 Transform 会自动叠加。父节点移动,子节点跟着移动;父节点旋转,子节点跟着绕着父节点旋转;父节点缩放,子节点跟着缩放。

先看代码怎么建立这个结构:

typescript 复制代码
import { scene } from '@kit.ArkGraphics3DKit';
import { SceneManager } from './SceneManager';

export class TransformUtils {
  /**
   * 设置节点位置(局部坐标)
   */
  static setPosition(node: scene.Node, x: number, y: number, z: number): void {
    node.position = { x, y, z };
  }

  /**
   * 设置节点旋转(欧拉角度,单位度)
   */
  static setRotation(node: scene.Node, x: number, y: number, z: number): void {
    node.rotation = { x, y, z };
  }

  /**
   * 设置节点缩放
   */
  static setScale(node: scene.Node, x: number, y: number, z: number): void {
    node.scale = { x, y, z };
  }

  /**
   * 建立父子节点关系
   */
  static attachChild(parent: scene.Node, child: scene.Node): void {
    parent.addChild(child);
  }
}

然后用这个工具类建立桌子和桌面设备的结构:

typescript 复制代码
// 1. 创建桌子节点
const deskNode = scene.createNode('Desk');
TransformUtils.setPosition(deskNode, 1, 0, 0);  // 桌子在世界坐标 (1, 0, 0)

// 2. 创建显示器节点
const monitorNode = scene.createNode('Monitor');
// 显示器是桌子的子节点,位置是相对于桌子的局部坐标
TransformUtils.setPosition(monitorNode, 0, 1, 0);  // 桌子高1米,显示器在桌子中间

// 3. 把显示器挂到桌子下面
TransformUtils.attachChild(deskNode, monitorNode);

// 4. 桌子挂到根节点下面
scene.getRoot().addChild(deskNode);

现在显示器的世界坐标是多少?桌子在 (1, 0, 0),显示器相对于桌子在 (0, 1, 0),所以显示器的世界坐标是 (1+0, 0+1, 0+0) = (1, 1, 0)。对了。


三、旋转和缩放:为什么父节点变了子节点也会变

位置好理解,旋转就容易绕晕了。

假设我要把桌子转 90 度,让它对着窗户。我只需要设置桌子节点的旋转:

typescript 复制代码
// 桌子绕Y轴转90度
TransformUtils.setRotation(deskNode, 0, 90, 0);

这时候显示器会怎么样?它会跟着桌子一起转,绕着桌子的原点转 90 度。这正是我想要的------桌子转了,上面的东西当然要跟着转。

如果显示器是独立节点,我还需要手动给显示器也设一个 90 度旋转,少设一个就穿帮了。

缩放也是一样。有时候我需要把整个桌子缩小一点,比如做一个小尺寸的书桌。我只需要设置桌子的缩放:

typescript 复制代码
// 整个桌子缩小到0.8倍
TransformUtils.setScale(deskNode, 0.8, 0.8, 0.8);

显示器和键盘都会跟着缩小。这里有个容易踩的坑:子节点的缩放是相对于父节点的缩放再叠加的。如果父节点缩到 0.8,子节点自己再缩到 0.5,那子节点实际的缩放就是 0.8 × 0.5 = 0.4。

这个叠加关系用表格列出来更清楚:

操作 只改父节点 只改子节点 父子都改
移动 子节点跟着移动 子节点相对于父节点移动 位置叠加
旋转 子节点跟着绕父节点旋转 子节点自己旋转 旋转叠加
缩放 子节点跟着缩放 子节点自己缩放 缩放相乘

四、模型原点为什么总是不对

还有一个很常见的问题:我把模型位置设到 (0, 0, 0),结果模型悬浮在半空中,或者模型一半埋在地下。

这不是位置设错了,是模型自己的原点不对。

比如一个椅子模型,建模的时候原点可能在椅子的几何中心,也可能在椅子的底部,甚至可能在椅子靠背的顶端。导入到 ArkGraphics 3D 以后,你设置 position = (0, 0, 0),实际上是把模型的原点放到了 (0, 0, 0)。

如果模型的原点在椅子几何中心,那椅子就会有一半埋在地下。如果原点在椅子顶端,椅子就会悬浮在半空。

这个问题怎么解决?两种办法:

第一种是建模的时候把原点放在正确的位置,比如椅子底部中心。但很多时候我们用的是现成模型,改不了。

第二种是在代码里给模型加一个偏移。比如椅子模型的原点在几何中心,高度是 1 米,那我就把椅子位置往上移 0.5 米:

typescript 复制代码
// 椅子模型原点在几何中心,需要往上偏移半个高度
TransformUtils.setPosition(chairNode, 0, 0.5, 0);

这个问题在做家具布局的时候特别明显。我第一次放沙发的时候,沙发整个埋到地板下面去了,折腾了半天才发现是模型原点的问题。


五、多层嵌套以后怎么排查位置错误

家具多了以后,节点结构可能是这样的:

复制代码
Scene根节点
└── DeskGroup(桌子组)
    ├── Desk(桌子)
    ├── Monitor(显示器)
    │   └── Stand(显示器支架)
    ├── Keyboard(键盘)
    └── Lamp(台灯)

三层嵌套以后,子节点的世界坐标就是好几层 Transform 叠加出来的。这时候位置不对怎么排查?

一个实用的办法是临时把所有父节点的位置和旋转都打出来,一层一层算:

typescript 复制代码
/**
 * 递归打印节点的世界坐标
 */
static printWorldPosition(node: scene.Node, depth: number = 0): void {
  const indent = '  '.repeat(depth);
  console.info(`${indent}Node: ${node.name}, pos: ${JSON.stringify(node.position)}`);

  const children = node.getChildren();
  children.forEach(child => {
    TransformUtils.printWorldPosition(child, depth + 1);
  });
}

// 调用:从根节点开始打印
TransformUtils.printWorldPosition(scene.getRoot());

打印出来以后,从根节点开始一层一层加:根节点在 (0,0,0),桌子组在 (1,0,0),显示器相对于桌子组在 (0,1,0),那显示器世界坐标就是 (1,1,0)。

这样一层一层算,总能找到是哪一层的位置设错了。


六、几个最容易踩的坐标坑

把这一篇遇到的坑总结一下:

坑 现象 解决办法
把局部坐标当世界坐标 子节点位置比预期偏了父节点的偏移量 记住:子节点的 position 是相对于父节点的
所有家具都挂根节点 移动或旋转一个家具,其他相关家具不动 建立父子节点结构,相关家具放在一个组里
模型原点不对 模型悬浮或埋在地下 给位置加偏移,或者改模型原点
多层嵌套后位置算不清 家具位置不对,不知道哪里错了 递归打印每层节点的位置,一层一层算
父节点缩放导致子节点变形 子节点跟着缩得太小或变形 注意缩放是叠加的,必要时子节点反向缩放

这些问题都不是 API 用错了,是 3D 空间里坐标体系的思维还没转过来。ArkUI 里所有组件都是二维平面上的,位置是相对于父容器的,逻辑相对简单。到了三维空间,有旋转、有缩放、有多层嵌套,坐标关系一下子就复杂了。


总结

第二篇的核心就一句话:节点结构本身就是空间管理的一部分。

  1. 世界坐标是相对于 Scene 根节点的,局部坐标是相对于父节点的
  2. 父子节点的 Position、Rotation、Scale 会自动叠加
  3. 把相关的家具放在一个组里,移动和旋转的时候才会一起动
  4. 模型原点不对是常见问题,需要手动偏移
  5. 多层嵌套后位置错了,就递归打印每层节点的位置,一层一层排查

SpaceRoom 现在有了桌子、显示器、键盘、台灯,节点结构也理清楚了。下一篇开始加入真正的 glTF 模型,解决模型加载多了以后越来越卡的问题:Resource 和 Instance 到底有什么区别,为什么同一把椅子放三次,不应该加载三次资源。

相关推荐
动物园猫2 小时前
Flutter 鸿蒙实战:用 webview_flutter 三方库在 鸿蒙 中内嵌真实网页
flutter·华为·harmonyos
李游Leo2 小时前
HarmonyOS 闪控窗开发复盘:从入口触发到窗口状态管理的完整落地【鸿蒙心迹】
华为·harmonyos
Sunny_G3 小时前
所见即所得编辑器原理实战:源码与渲染永不失真的三层一致性设计(Markdown/CodeMirror 装饰)
ai编程·harmonyos
less_121383 小时前
HarmonyOS WPS Open SDK 二开实践:统一接口如何收敛多套打开链路
sdk·harmonyos·wps·鸿蒙开发
m0_738185823 小时前
Flutter 鸿蒙化实战:flutter_app_badger 适配 OpenHarmony,应用角标
flutter·华为·harmonyos·鸿蒙
袁震5 小时前
HarmonyOS 7 深色模式与全局换肤实战:一套色板管到底
华为·harmonyos
李游Leo5 小时前
《HarmonyOS 7 ArkGraphics 3D 空间设计开发实战》05:Camera控制、手势映射与空间浏览交互【鸿蒙心迹】
3d·交互·harmonyos
resh_people6 小时前
开源鸿蒙平台 KMP/CMP 三方库「Okio」适配全流程
华为·开源·harmonyos
3D阅阅三维模型轻量化及浏览平台6 小时前
非标设计提效:3D 阅图实现模型自检与出图的实战指南
3d·模具·模型比对·模型差异化·版本比对·机加工·审图