HarmonyOS 6 ArkGraphics 3D精讲:坐标、向量与矩阵——初识3D数学的“空间建模”

发布时间:2026/5/21 7:14:38

HarmonyOS 6 ArkGraphics 3D精讲:坐标、向量与矩阵——初识3D数学的“空间建模” HarmonyOS 6 ArkGraphics 3D精讲坐标、向量与矩阵——初识3D数学的“空间建模”图形图像和数学一直是密不可分的。工程化地理解数学知识不是为了让你手撕公式而是为了帮你更好地理解空间建模——等后面你要自己写 Shader 的时候会发现今天的基础知识是非常重要的3D 的世界和物理世界一样有一套自己的运行规律。就像我们小时候就知道的月亮绕着地球转地球绕着太阳转。这个小小的天文常识放在 3D 场景里就是父子节点层层嵌套、坐标逐级传递的最直观体现。之前我们已经让一个立方体在 HarmonyOS 里跑起来了。但只要你继续往下做就会发现 3D 并不是“把模型加载出来”这么简单物体为什么出现在这个位置相机为什么能看到它怎么实现地球带着月亮一起绕太阳转这些问题的答案都藏在坐标、向量和矩阵里。这一篇不打算写成数学课。公式能看懂最好看不懂也没关系——我们只讲 ArkGraphics 3D 里代码实操的部分position、rotation、scale、父子节点、相机、光照方向以及最终怎么屏幕渲染上。配套代码里我设计了个可以边看边玩的案例太阳-地球-月亮公转自转模型让你亲眼看看层级变换是怎么传递下去的这一篇只解决一个问题当我在 ArkGraphics 3D 里改一个节点的坐标、旋转、缩放时画面为什么会跟着变一、为什么绕不开数学你说得对这段确实太“教程腔”了。改成下面这样更像个老手在聊经验一、为什么绕不开数学先别急着看代码说个实在的。我做数字孪生这几年见过不少入坑 3D 的同学上来就是一顿操作glTF 加载出来了相机能动了立方体也转起来了。然后就觉得“行了够用了数学那套以后再说”。直到有一天产品提了个需求让这块仪表盘跟着那个机械臂一起转。然后就开始懵了。明明设了坐标为什么位置不对明明加了旋转为什么转的方向是反的明明父子节点挂好了为什么子节点到处乱飘这就是 3D 开发的“新手墙”——看起来能跑但一碰就倒。拿太阳系这个案例来说如果你只是想让一个圆绕着另一个圆转2D 动画确实能糊弄过去。但那不是 3D。真正的 3D 场景里地球和月亮的位置是这样来的this.solarRoot.children.append(this.earthOrbit);this.earthOrbit.children.append(this.earth);this.earthOrbit.children.append(this.moonOrbit);this.moonOrbit.children.append(this.moon);这几行代码背后是这么一串关系地球的世界坐标 地球公转轨道的变换 × 地球自己的局部坐标 月亮的世界坐标 地球公转轨道的变换 × 月亮公转轨道的变换 × 月亮自己的局部坐标看不懂这个你会发现想让月球跟着地球转结果它围着太阳转想让相机盯着地球结果视角不知道歪到哪去了想做射线检测点选物体结果点的位置和实际完全不搭数学不是用来炫的是用来定位问题的。这一篇不教你手撸矩阵。ArkGraphics 3D 已经把矩阵计算封装好了。你只需要知道三件事写node.position→ 动的是模型矩阵写camera.position和朝向 → 动的是视图矩阵创建相机、调视野 → 动的是投影矩阵记住这三条剩下的代码怎么写心里就有底了。明白去掉“偷懒2D”这种说法改成纯粹的技术表述。下面是修改后的版本二、坐标系一个点四种身份3D 场景里最容易晕的一件事是同一个点可以有四种不同的“身份”。一个立方体的某个顶点在建模软件里有它的坐标局部坐标放到场景里它有了一个世界位置世界坐标相机拍它的时候它相对于镜头有一个位置相机坐标最后显示在屏幕上它变成了一个像素位置屏幕坐标。它们之间的关系是一条链局部坐标 → 世界坐标 → 相机坐标 → 屏幕坐标在 ArkGraphics 3D 里这条链的每个环节都有对应的 API坐标系什么意思ArkGraphics 3D 里怎么看局部坐标模型“自己家”的坐标glTF 里的顶点、Geometry的顶点数据世界坐标在场景里的真实位置Node.position 父节点的变换叠加相机坐标从相机镜头看过去的位置Camera的位置和朝向决定了怎么转屏幕坐标最终显示在屏幕上的 2D 坐标Component3D渲染结果、raycast的输入拿太阳系案例来具体感受一下地球节点本身并没有每一帧去改它的世界坐标它的局部位置是固定的this.earth.position{x:this.earthOrbitRadius,y:0,z:0};this.earthOrbit.children.append(this.earth);地球之所以会“公转”是因为它的父节点earthOrbit在旋转this.earthOrbit.rotationthis.makeYRotation(this.earthAngle);这就是局部坐标和世界坐标的区别——地球在父节点EarthOrbit的局部坐标系里一直老老实实待在(earthOrbitRadius, 0, 0)没动过。但因为父节点在转它的世界坐标每时每刻都在变。我们调用API时都是使用的世界坐标系只有在对模型的node拆解打组。组内部的操作往往都是局部坐标系。再说一下右手坐标系。ArkGraphics 3D 用的是右手坐标系X 向右Y 向上Z 向屏幕外相机看向 -Z 方向。第 1 篇我们把相机放在z 4this.camera.position.z4;因为相机看向 -Z 方向所以相机在 Z 正半轴时才能看到原点处的物体。太阳系案例里我们用CalcUtils.lookAt让相机从斜上方看向原点CalcUtils.lookAt(this.camera,{x:0,y:4.2,z:7.8},// 相机放哪儿{x:0,y:0,z:0},// 往哪儿看{x:0,y:1,z:0}// 哪边是“上”);这三个参数背后就是向量和矩阵在干活。用 3D 节点来搭太阳系本质上是把坐标系嵌套这件事摆在台面上。节点结构长这样SolarRoot EarthOrbit ← 地球公转的“局部宇宙” Earth ← 局部位置固定但跟着父节点转 MoonOrbit ← 月球公转的“局部宇宙” Moon ← 局部位置固定但叠了两层变换你看到的月亮世界位置是EarthOrbit和MoonOrbit两个坐标系叠加之后的结果。如果你把这段结构跑通了以后碰到任何父子嵌套的 3D 场景心里都会有底。三、向量既是位置也是方向还能算夹角向量可以粗暴地理解为“带方向的数字”。在 3D 里一个{ x, y, z }可以当位置用也可以当方向用还可以表示两个点之间的位移。举个例子地球的世界坐标大概是这样的constearthWorld{x:1.94,y:0,z:1.94};从太阳原点指向地球的方向就是两个位置相减constlightDirection{x:earthWorld.x-0,y:earthWorld.y-0,z:earthWorld.z-0};如果只关心方向、不关心距离通常会做归一化把长度变成 1constlengthMath.hypot(lightDirection.x,lightDirection.y,lightDirection.z);constnormalized{x:lightDirection.x/length,y:lightDirection.y/length,z:lightDirection.z/length};向量的几个基本操作不用背公式知道它们是干嘛的就行操作能干啥什么时候用加法位移叠加物体从 A 走到 B减法得到方向从 A 指向 B点乘判断夹角大小光照强弱、视线和法线的夹角叉乘得到垂直方向计算法线、相机的“右方向”这次太阳系调试光照的时候向量就派上用场了。太阳在原点地球在轨道上跑光从太阳指向地球。一个球体只有朝向光源的那一面应该亮背光面应该暗。这个关系用一句话就能说清楚亮度 ≈ max(0, dot(表面法线, 光线方向))这就是点乘的威力。点乘越接近 1 → 表面正对光线 → 越亮接近 0 → 光线擦着表面过 → 比较暗小于 0 → 光线在背面 → 当前看到的这一面就是黑的。所以你会发现当相机正好对着地球的背光面时地球看起来是黑的——这不是 Bug是实时光照的真实表现。不过作为教学 demo地球长期黑着不利于观察所以案例里加了三个辅助手段「受光面」相机视角专门把相机转到能看见被照亮的那一侧「太阳光强」滑条把光源效果放大看得更清楚黄色点阵光线用一串小点标出从太阳到地球的方向那条黄色点阵不是真光线是教学辅助线。它的位置是太阳到地球之间的插值constt(i1)/(rayCount1);this.lightRayDots[i].position{x:earthWorld.x*t,y:0,z:earthWorld.z*t};本质就是向量插值从原点沿着地球的方向走一段。它不影响渲染只负责告诉你“光是从这边打过来的”。四、矩阵平移、旋转、缩放怎么“打包”在一起如果说向量是“表达位置和方向”的语言那矩阵就是“表达怎么变”的语言。平移、旋转、缩放在 3D 图形里都可以用一个矩阵来表示。ArkGraphics 3D 不需要你手写矩阵但你写的这些属性背后都会被转成矩阵node.position{x:1,y:0,z:0};node.rotation{x:0,y:0.7,z:0,w:0.7};node.scale{x:1,y:1,z:1};对应关系大致是这样的你写的代码背后在做什么position构造一个平移矩阵rotation构造一个旋转矩阵通过四元数转换scale构造一个缩放矩阵父子节点挂载父矩阵 × 子矩阵在之前的例子里单个立方体的变换很适合用来理解 Model 矩阵this.cube.position{x:this.translateX,y:this.translateY,z:0};this.cube.scale{x:this.scaleValue,y:this.scaleValue,z:this.scaleValue};this.cube.rotation{x:0,y:Math.sin(halfRadian),z:0,w:Math.cos(halfRadian)};这个 demo 还手动算了一个局部点(0.5, 0.5, 0.5)经过缩放、旋转、平移之后的世界位置constscaledXlocalX*this.scaleValue;constscaledYlocalY*this.scaleValue;constscaledZlocalZ*this.scaleValue;constrotatedXscaledX*Math.cos(radian)scaledZ*Math.sin(radian);constrotatedZ-scaledX*Math.sin(radian)scaledZ*Math.cos(radian);constworldXrotatedXthis.translateX;constworldYscaledYthis.translateY;constworldZrotatedZ;这段代码其实就是在表达这个公式world Translate × RotateY × Scale × local顺序很重要。矩阵乘法不满足交换律——先平移再旋转和先旋转再平移结果是完全不一样的。地球绕太阳转就是靠这个顺序实现的。如果直接让地球自己旋转那是自转this.earth.rotationthis.makeYRotation(angle);但如果让父节点EarthOrbit旋转地球就会公转this.earth.position{x:this.earthOrbitRadius,y:0,z:0};this.earthOrbit.children.append(this.earth);this.earthOrbit.rotationthis.makeYRotation(this.earthAngle);地球的局部位置没变但它所在的“局部坐标系”在转。世界坐标 父节点矩阵 × 地球局部矩阵。月亮同理只是多套了一层this.moonOrbit.position{x:this.earthOrbitRadius,y:0,z:0};this.earthOrbit.children.append(this.moonOrbit);this.moon.position{x:this.moonOrbitRadius,y:0,z:0};this.moonOrbit.children.append(this.moon);月亮的最终世界位置不是简单的moon.position而是MoonWorld EarthOrbit × MoonOrbit × MoonLocal这就是矩阵最实在的价值。你不需要自己手撸 4x4 矩阵但必须理解父子节点就是在做矩阵乘法。否则哪天模型不在你预期的位置或者子节点跟着父节点乱跑你根本不知道从哪查起。五、MVP从 3D 世界到屏幕中间经历了什么3D 图形里有一个核心公式叫 MVP最终位置 Projection × View × Model × 局部坐标这三个矩阵各司其职矩阵干什么的ArkGraphics 里对应什么Model局部坐标 → 世界坐标Node的 position/rotation/scaleView世界坐标 → 相机坐标Camera的位置和朝向Projection相机坐标 → 屏幕空间相机的 FOV、near、far好消息是ArkGraphics 3D 已经把 MVP 的大部分工作封装好了。你创建Scene、创建Camera然后交给Component3Dthis.sceneOpt{scene:this.scene,modelType:ModelType.SURFACE}asSceneOptions;引擎会自动根据每个节点的变换、相机的视图矩阵和投影矩阵把 3D 内容画到屏幕上。但你还是得理解它因为很多问题就出在 MVP 的某一环物体位置不对先查 Modelnode.position、node.rotation、node.scale、node.parent物体存在但看不见先查 Viewcamera.position、lookAt有没有设对物体被“切”掉一半查 Projection相机的 near/far 平面是不是太近了/太远了太阳系案例里的几个视角按钮本质上就是在改 View 矩阵setCameraView(mode:string):void{if(modetop){CalcUtils.lookAt(this.camera,{x:0,y:8.2,z:0.1},{x:0,y:0,z:0},{x:0,y:0,z:-1});this.cameraMode俯视;return;}if(modelit){CalcUtils.lookAt(this.camera,{x:3.8,y:2.2,z:4.8},{x:1.6,y:0,z:-1.0},{x:0,y:1,z:0});this.cameraMode受光面;return;}CalcUtils.lookAt(this.camera,{x:0,y:4.2,z:7.8},{x:0,y:0,z:0},{x:0,y:1,z:0});this.cameraMode斜视;}同一个太阳系Model 没变只是相机的 View 变了画面就完全不同。俯视适合看轨道形状斜视适合感受空间深度受光面适合观察光照方向。这个按钮比干讲“视图矩阵”直观多了。六、搭个实验台太阳-地球-月亮建议你跟着一起边看边玩。虽然样式有点简陋但它非常适合用来理解坐标、矩阵和父子变换——因为局部坐标、世界坐标、父子嵌套、相机视角、光照方向全都在一个画面里。创建流程不复杂创建空场景创建相机创建太阳的光源创建太阳、地球、月亮的球体用父子节点把轨道关系搭起来每帧让EarthOrbit和MoonOrbit转一点UI 上实时显示坐标变化初始化代码大致长这样Scene.load().then(async(result:Scene){this.sceneresult;this.scene.environment.backgroundTypeEnvironmentBackgroundType.BACKGROUND_NONE;letrf:SceneResourceFactorythis.scene.getResourceFactory();this.cameraawaitrf.createCamera({name:Article04Camera});this.camera.enabledtrue;this.camera.clearColor{r:0.02,g:0.03,b:0.06,a:1};this.setCameraView(oblique);awaitthis.createSunPointLights(rf);awaitthis.createSolarSystem(rf);this.applyLightingMode();this.applyOrbitTransforms();this.sceneOpt{scene:this.scene,modelType:ModelType.SURFACE}asSceneOptions;});节点层级是核心this.solarRootawaitrf.createNode({name:SolarRoot});this.scene.root.children.append(this.solarRoot);this.earthOrbitawaitrf.createNode({name:EarthOrbit});this.solarRoot.children.append(this.earthOrbit);this.earthawaitthis.createSphereNode(rf,Earth,0.30,32,earthMaterial);this.earth.position{x:this.earthOrbitRadius,y:0,z:0};this.earthOrbit.children.append(this.earth);this.moonOrbitawaitrf.createNode({name:MoonOrbit});this.moonOrbit.position{x:this.earthOrbitRadius,y:0,z:0};this.earthOrbit.children.append(this.moonOrbit);this.moonawaitthis.createSphereNode(rf,Moon,0.13,24,moonMaterial);this.moon.position{x:this.moonOrbitRadius,y:0,z:0};this.moonOrbit.children.append(this.moon);每一帧只需要改两个父节点的旋转this.earthOrbit.rotationthis.makeYRotation(this.earthAngle);this.moonOrbit.rotationthis.makeYRotation(this.moonAngle);这就是矩阵继承最直观的体现——地球和月亮都没有被直接设置世界坐标它们的位置完全由父节点的旋转“带”着走。为了让数值变化看得见案例里还手动算了Earth world、Moon local和Moon worldconstearthWorldthis.rotateYPoint(this.earthOrbitRadius,0,this.earthAngle);constmoonLocalthis.rotateYPoint(this.moonOrbitRadius,0,this.moonAngle);constmoonWorldthis.rotateYPoint(this.earthOrbitRadiusmoonLocal.x,moonLocal.z,this.earthAngle);拖拽“地球公转角”和“月亮公转角”的时候你可以亲眼看到地球世界坐标随EarthOrbit旋转变化月亮局部坐标只描述它绕地球的位置月亮世界坐标同时受地球公转和月亮公转影响三个最适合截图的状态状态适合观察什么俯视轨道XZ 平面的轨道形状、坐标数值变化斜视 3D空间层级、Y 轴深度、真实 3D 感受光面光照方向、点乘和明暗关系建议你先看俯视把轨道和坐标的关系看明白再看斜视感受 3D 空间最后看受光面理解光照方向怎么影响明暗。七、五个最容易踩的坑坑一把局部坐标当成世界坐标earth.position { x: 2.75, y: 0, z: 0 }—— 这不代表地球的世界坐标永远是(2.75, 0, 0)它只是说地球在父节点EarthOrbit里的局部位置是这个值。父节点一转世界坐标就跟着变了。坑二搞乱父子节点的顺序月亮不是直接挂在太阳下面而是挂在MoonOrbit下面MoonOrbit又挂在EarthOrbit下面。这个结构决定了月亮会同时继承地球的公转和它自己绕地球的公转。挂错了层级运动轨迹就全乱了。坑三以为相机只要挪位置就行只改camera.position不处理朝向画面很可能啥也看不见。CalcUtils.lookAt的价值就在这里——它用 eye相机在哪儿、center往哪儿看、up哪边是上三个向量帮你把相机姿态算好。坑四光照问题当成颜色问题地球变黑了不一定是材质坏了很可能只是你刚好看到的是背光面。光照强弱和表面法线、光线方向的关系背后就是点乘。调试光照的时候可以先打开光线辅助线案例里的黄色点阵再切到“受光面”相机视角。坑五一上来就想手写矩阵没必要。ArkGraphics 3D 已经通过Node、Camera和Component3D封装了大部分矩阵流程。你需要掌握的是每个属性影响哪一段Node.position/rotation/scale → Model Camera position/lookAt → View Camera projection → Projection Component3D → 输出到屏幕最后这一篇“数学浓度”比较大。如果你想要我概况成几句话那就是坐标系是分层的局部、世界、相机、屏幕不是一回事。向量是方向的语言位置相减得方向点乘解释光照强弱。矩阵是变换的语言平移、旋转、缩放和父子继承都是矩阵乘法。MVP 是渲染的流水线Model 决定物体在哪儿View 决定相机怎么看Projection 决定怎么投到屏幕。学习的过程不需要会推导所有公式但希望你开始形成一种判断习惯画面出问题时先判断它是坐标问题、向量问题、矩阵问题还是相机投影问题。这个判断建立起来之后后面的旋转、四元数、场景图、相机交互、射线检测就不再是孤立的 API而是一条完整的数学链路在不同环节的落地。旋转为什么会有万向锁为什么 ArkGraphics 3D 的节点旋转要用四元数怎么让一个物体平滑地转向目标方向这些都要建立在本篇的坐标和向量基础之上。我们后续见。

相关新闻