HarmonyOS游戏开发:3D渲染基础与坐标系统

HarmonyOS游戏开发:3D渲染基础与坐标系统

核心要点

  • 理解3D坐标系(世界坐标、局部坐标、相机坐标)的变换关系
  • 掌握矩阵运算在3D变换中的核心作用
  • 实现基础3D渲染管线:顶点处理→投影→光栅化
  • 使用ArkTS构建可复用的3D数学库

一、背景与动机

随着HarmonyOS生态的快速发展,游戏开发、AR/VR应用、数据可视化等场景对3D图形渲染的需求日益增长。3D渲染基础是构建复杂3D应用的基石,理解坐标系统和变换原理对于开发者至关重要。

传统2D应用开发中,开发者习惯于屏幕坐标系(左上角为原点),而3D开发需要处理多个坐标系的转换,包括世界坐标系、局部坐标系、相机坐标系、裁剪坐标系等。这种多坐标系体系让初学者容易迷失方向,导致渲染结果不符合预期。

HarmonyOS提供了NAPI机制,允许开发者通过OpenGL ES或Vulkan进行高性能3D渲染。但要充分发挥这些图形API的能力,必须深入理解3D数学基础。本文将从零构建一个轻量级3D数学库,帮助开发者建立坚实的理论基础。


二、核心原理

2.1 坐标系统体系

3D图形渲染涉及多个坐标系的转换,每个坐标系都有其特定用途:
#mermaid-svg-hP9jxWifqizcq8Xa{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-hP9jxWifqizcq8Xa .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-hP9jxWifqizcq8Xa .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-hP9jxWifqizcq8Xa .error-icon{fill:#552222;}#mermaid-svg-hP9jxWifqizcq8Xa .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-hP9jxWifqizcq8Xa .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-hP9jxWifqizcq8Xa .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-hP9jxWifqizcq8Xa .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-hP9jxWifqizcq8Xa .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-hP9jxWifqizcq8Xa .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-hP9jxWifqizcq8Xa .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-hP9jxWifqizcq8Xa .marker{fill:#333333;stroke:#333333;}#mermaid-svg-hP9jxWifqizcq8Xa .marker.cross{stroke:#333333;}#mermaid-svg-hP9jxWifqizcq8Xa svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-hP9jxWifqizcq8Xa p{margin:0;}#mermaid-svg-hP9jxWifqizcq8Xa .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-hP9jxWifqizcq8Xa .cluster-label text{fill:#333;}#mermaid-svg-hP9jxWifqizcq8Xa .cluster-label span{color:#333;}#mermaid-svg-hP9jxWifqizcq8Xa .cluster-label span p{background-color:transparent;}#mermaid-svg-hP9jxWifqizcq8Xa .label text,#mermaid-svg-hP9jxWifqizcq8Xa span{fill:#333;color:#333;}#mermaid-svg-hP9jxWifqizcq8Xa .node rect,#mermaid-svg-hP9jxWifqizcq8Xa .node circle,#mermaid-svg-hP9jxWifqizcq8Xa .node ellipse,#mermaid-svg-hP9jxWifqizcq8Xa .node polygon,#mermaid-svg-hP9jxWifqizcq8Xa .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-hP9jxWifqizcq8Xa .rough-node .label text,#mermaid-svg-hP9jxWifqizcq8Xa .node .label text,#mermaid-svg-hP9jxWifqizcq8Xa .image-shape .label,#mermaid-svg-hP9jxWifqizcq8Xa .icon-shape .label{text-anchor:middle;}#mermaid-svg-hP9jxWifqizcq8Xa .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-hP9jxWifqizcq8Xa .rough-node .label,#mermaid-svg-hP9jxWifqizcq8Xa .node .label,#mermaid-svg-hP9jxWifqizcq8Xa .image-shape .label,#mermaid-svg-hP9jxWifqizcq8Xa .icon-shape .label{text-align:center;}#mermaid-svg-hP9jxWifqizcq8Xa .node.clickable{cursor:pointer;}#mermaid-svg-hP9jxWifqizcq8Xa .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-hP9jxWifqizcq8Xa .arrowheadPath{fill:#333333;}#mermaid-svg-hP9jxWifqizcq8Xa .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-hP9jxWifqizcq8Xa .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-hP9jxWifqizcq8Xa .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-hP9jxWifqizcq8Xa .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-hP9jxWifqizcq8Xa .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-hP9jxWifqizcq8Xa .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-hP9jxWifqizcq8Xa .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-hP9jxWifqizcq8Xa .cluster text{fill:#333;}#mermaid-svg-hP9jxWifqizcq8Xa .cluster span{color:#333;}#mermaid-svg-hP9jxWifqizcq8Xa div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-hP9jxWifqizcq8Xa .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-hP9jxWifqizcq8Xa rect.text{fill:none;stroke-width:0;}#mermaid-svg-hP9jxWifqizcq8Xa .icon-shape,#mermaid-svg-hP9jxWifqizcq8Xa .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-hP9jxWifqizcq8Xa .icon-shape p,#mermaid-svg-hP9jxWifqizcq8Xa .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-hP9jxWifqizcq8Xa .icon-shape .label rect,#mermaid-svg-hP9jxWifqizcq8Xa .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-hP9jxWifqizcq8Xa .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-hP9jxWifqizcq8Xa .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-hP9jxWifqizcq8Xa :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-hP9jxWifqizcq8Xa .localStyle>*{fill:#FF6B6B!important;stroke:#C0392B!important;stroke-width:2px!important;color:#fff!important;}#mermaid-svg-hP9jxWifqizcq8Xa .localStyle span{fill:#FF6B6B!important;stroke:#C0392B!important;stroke-width:2px!important;color:#fff!important;}#mermaid-svg-hP9jxWifqizcq8Xa .localStyle tspan{fill:#fff!important;}#mermaid-svg-hP9jxWifqizcq8Xa .worldStyle>*{fill:#4ECDC4!important;stroke:#16A085!important;stroke-width:2px!important;color:#fff!important;}#mermaid-svg-hP9jxWifqizcq8Xa .worldStyle span{fill:#4ECDC4!important;stroke:#16A085!important;stroke-width:2px!important;color:#fff!important;}#mermaid-svg-hP9jxWifqizcq8Xa .worldStyle tspan{fill:#fff!important;}#mermaid-svg-hP9jxWifqizcq8Xa .viewStyle>*{fill:#45B7D1!important;stroke:#2980B9!important;stroke-width:2px!important;color:#fff!important;}#mermaid-svg-hP9jxWifqizcq8Xa .viewStyle span{fill:#45B7D1!important;stroke:#2980B9!important;stroke-width:2px!important;color:#fff!important;}#mermaid-svg-hP9jxWifqizcq8Xa .viewStyle tspan{fill:#fff!important;}#mermaid-svg-hP9jxWifqizcq8Xa .clipStyle>*{fill:#96CEB4!important;stroke:#27AE60!important;stroke-width:2px!important;color:#fff!important;}#mermaid-svg-hP9jxWifqizcq8Xa .clipStyle span{fill:#96CEB4!important;stroke:#27AE60!important;stroke-width:2px!important;color:#fff!important;}#mermaid-svg-hP9jxWifqizcq8Xa .clipStyle tspan{fill:#fff!important;}#mermaid-svg-hP9jxWifqizcq8Xa .ndcStyle>*{fill:#FFEAA7!important;stroke:#F39C12!important;stroke-width:2px!important;color:#333!important;}#mermaid-svg-hP9jxWifqizcq8Xa .ndcStyle span{fill:#FFEAA7!important;stroke:#F39C12!important;stroke-width:2px!important;color:#333!important;}#mermaid-svg-hP9jxWifqizcq8Xa .ndcStyle tspan{fill:#333!important;}#mermaid-svg-hP9jxWifqizcq8Xa .screenStyle>*{fill:#DDA0DD!important;stroke:#9B59B6!important;stroke-width:2px!important;color:#fff!important;}#mermaid-svg-hP9jxWifqizcq8Xa .screenStyle span{fill:#DDA0DD!important;stroke:#9B59B6!important;stroke-width:2px!important;color:#fff!important;}#mermaid-svg-hP9jxWifqizcq8Xa .screenStyle tspan{fill:#fff!important;} 坐标系体系
模型矩阵 Model
观察矩阵 View
投影矩阵 Projection
透视除法
视口变换
局部坐标系

Local Space
世界坐标系

World Space
相机坐标系

View/Camera Space
裁剪坐标系

Clip Space
标准化设备坐标

NDC
屏幕坐标系

Screen Space

坐标系详解:

坐标系 原点位置 坐标范围 用途
局部坐标系 模型几何中心 无限制 定义模型顶点数据
世界坐标系 场景原点 无限制 定位场景中的所有物体
相机坐标系 相机位置 无限制 从相机视角观察场景
裁剪坐标系 视锥体中心 -w, w 判断顶点是否可见
NDC 中心 -1, 1 标准化坐标便于插值
屏幕坐标系 左上角 0, width×0, height 最终显示坐标

2.2 变换矩阵原理

3D变换通过4×4齐次矩阵实现,支持平移、旋转、缩放和投影变换。

齐次坐标的优势:

  • 统一表示点和向量
  • 支持仿射变换的矩阵乘法
  • 便于透视投影处理

基础变换矩阵:

复制代码
平移矩阵 T(tx, ty, tz):
┌              ┐
│ 1  0  0  tx  │
│ 0  1  0  ty  │
│ 0  0  1  tz  │
│ 0  0  0  1   │
└              ┘

缩放矩阵 S(sx, sy, sz):
┌              ┐
│ sx 0  0  0   │
│ 0  sy 0  0   │
│ 0  0  sz 0   │
│ 0  0  0  1   │
└              ┘

绕Z轴旋转矩阵 Rz(θ):
┌                   ┐
│ cosθ -sinθ 0  0   │
│ sinθ  cosθ 0  0   │
│ 0     0    1  0   │
│ 0     0    0  1   │
└                   ┘

2.3 渲染管线流程

#mermaid-svg-rA4WgkJ7Gya6RH7v{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-rA4WgkJ7Gya6RH7v .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-rA4WgkJ7Gya6RH7v .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-rA4WgkJ7Gya6RH7v .error-icon{fill:#552222;}#mermaid-svg-rA4WgkJ7Gya6RH7v .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-rA4WgkJ7Gya6RH7v .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-rA4WgkJ7Gya6RH7v .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-rA4WgkJ7Gya6RH7v .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-rA4WgkJ7Gya6RH7v .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-rA4WgkJ7Gya6RH7v .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-rA4WgkJ7Gya6RH7v .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-rA4WgkJ7Gya6RH7v .marker{fill:#333333;stroke:#333333;}#mermaid-svg-rA4WgkJ7Gya6RH7v .marker.cross{stroke:#333333;}#mermaid-svg-rA4WgkJ7Gya6RH7v svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-rA4WgkJ7Gya6RH7v p{margin:0;}#mermaid-svg-rA4WgkJ7Gya6RH7v .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-rA4WgkJ7Gya6RH7v .cluster-label text{fill:#333;}#mermaid-svg-rA4WgkJ7Gya6RH7v .cluster-label span{color:#333;}#mermaid-svg-rA4WgkJ7Gya6RH7v .cluster-label span p{background-color:transparent;}#mermaid-svg-rA4WgkJ7Gya6RH7v .label text,#mermaid-svg-rA4WgkJ7Gya6RH7v span{fill:#333;color:#333;}#mermaid-svg-rA4WgkJ7Gya6RH7v .node rect,#mermaid-svg-rA4WgkJ7Gya6RH7v .node circle,#mermaid-svg-rA4WgkJ7Gya6RH7v .node ellipse,#mermaid-svg-rA4WgkJ7Gya6RH7v .node polygon,#mermaid-svg-rA4WgkJ7Gya6RH7v .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-rA4WgkJ7Gya6RH7v .rough-node .label text,#mermaid-svg-rA4WgkJ7Gya6RH7v .node .label text,#mermaid-svg-rA4WgkJ7Gya6RH7v .image-shape .label,#mermaid-svg-rA4WgkJ7Gya6RH7v .icon-shape .label{text-anchor:middle;}#mermaid-svg-rA4WgkJ7Gya6RH7v .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-rA4WgkJ7Gya6RH7v .rough-node .label,#mermaid-svg-rA4WgkJ7Gya6RH7v .node .label,#mermaid-svg-rA4WgkJ7Gya6RH7v .image-shape .label,#mermaid-svg-rA4WgkJ7Gya6RH7v .icon-shape .label{text-align:center;}#mermaid-svg-rA4WgkJ7Gya6RH7v .node.clickable{cursor:pointer;}#mermaid-svg-rA4WgkJ7Gya6RH7v .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-rA4WgkJ7Gya6RH7v .arrowheadPath{fill:#333333;}#mermaid-svg-rA4WgkJ7Gya6RH7v .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-rA4WgkJ7Gya6RH7v .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-rA4WgkJ7Gya6RH7v .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-rA4WgkJ7Gya6RH7v .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-rA4WgkJ7Gya6RH7v .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-rA4WgkJ7Gya6RH7v .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-rA4WgkJ7Gya6RH7v .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-rA4WgkJ7Gya6RH7v .cluster text{fill:#333;}#mermaid-svg-rA4WgkJ7Gya6RH7v .cluster span{color:#333;}#mermaid-svg-rA4WgkJ7Gya6RH7v div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-rA4WgkJ7Gya6RH7v .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-rA4WgkJ7Gya6RH7v rect.text{fill:none;stroke-width:0;}#mermaid-svg-rA4WgkJ7Gya6RH7v .icon-shape,#mermaid-svg-rA4WgkJ7Gya6RH7v .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-rA4WgkJ7Gya6RH7v .icon-shape p,#mermaid-svg-rA4WgkJ7Gya6RH7v .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-rA4WgkJ7Gya6RH7v .icon-shape .label rect,#mermaid-svg-rA4WgkJ7Gya6RH7v .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-rA4WgkJ7Gya6RH7v .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-rA4WgkJ7Gya6RH7v .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-rA4WgkJ7Gya6RH7v :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-rA4WgkJ7Gya6RH7v .inputStyle>*{fill:#E74C3C!important;stroke:#C0392B!important;stroke-width:2px!important;color:#fff!important;}#mermaid-svg-rA4WgkJ7Gya6RH7v .inputStyle span{fill:#E74C3C!important;stroke:#C0392B!important;stroke-width:2px!important;color:#fff!important;}#mermaid-svg-rA4WgkJ7Gya6RH7v .inputStyle tspan{fill:#fff!important;}#mermaid-svg-rA4WgkJ7Gya6RH7v .processStyle>*{fill:#3498DB!important;stroke:#2980B9!important;stroke-width:2px!important;color:#fff!important;}#mermaid-svg-rA4WgkJ7Gya6RH7v .processStyle span{fill:#3498DB!important;stroke:#2980B9!important;stroke-width:2px!important;color:#fff!important;}#mermaid-svg-rA4WgkJ7Gya6RH7v .processStyle tspan{fill:#fff!important;}#mermaid-svg-rA4WgkJ7Gya6RH7v .outputStyle>*{fill:#2ECC71!important;stroke:#27AE60!important;stroke-width:2px!important;color:#fff!important;}#mermaid-svg-rA4WgkJ7Gya6RH7v .outputStyle span{fill:#2ECC71!important;stroke:#27AE60!important;stroke-width:2px!important;color:#fff!important;}#mermaid-svg-rA4WgkJ7Gya6RH7v .outputStyle tspan{fill:#fff!important;} 渲染管线
顶点数据
顶点着色器

坐标变换
图元装配
裁剪
透视除法
视口变换
光栅化
片段着色器

颜色计算
深度测试
颜色混合
帧缓冲


三、代码实战

3.1 3D向量类实现

typescript 复制代码
// 3D向量类 - 基础数学运算
export class Vector3 {
  public x: number;
  public y: number;
  public z: number;

  constructor(x: number = 0, y: number = 0, z: number = 0) {
    this.x = x;
    this.y = y;
    this.z = z;
  }

  // 向量加法
  add(v: Vector3): Vector3 {
    return new Vector3(this.x + v.x, this.y + v.y, this.z + v.z);
  }

  // 向量减法
  subtract(v: Vector3): Vector3 {
    return new Vector3(this.x - v.x, this.y - v.y, this.z - v.z);
  }

  // 标量乘法
  multiply(scalar: number): Vector3 {
    return new Vector3(this.x * scalar, this.y * scalar, this.z * scalar);
  }

  // 点积(内积)
  dot(v: Vector3): number {
    return this.x * v.x + this.y * v.y + this.z * v.z;
  }

  // 叉积(外积)
  cross(v: Vector3): Vector3 {
    return new Vector3(
      this.y * v.z - this.z * v.y,
      this.z * v.x - this.x * v.z,
      this.x * v.y - this.y * v.x
    );
  }

  // 向量长度
  magnitude(): number {
    return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z);
  }

  // 归一化
  normalize(): Vector3 {
    const mag = this.magnitude();
    if (mag === 0) {
      return new Vector3(0, 0, 0);
    }
    return this.multiply(1 / mag);
  }

  // 向量距离
  distance(v: Vector3): number {
    return this.subtract(v).magnitude();
  }

  // 线性插值
  lerp(v: Vector3, t: number): Vector3 {
    return this.add(v.subtract(this).multiply(t));
  }

  // 克隆向量
  clone(): Vector3 {
    return new Vector3(this.x, this.y, this.z);
  }

  // 静态方法:零向量
  static zero(): Vector3 {
    return new Vector3(0, 0, 0);
  }

  // 静态方法:单位向量
  static one(): Vector3 {
    return new Vector3(1, 1, 1);
  }

  // 静态方法:向上向量
  static up(): Vector3 {
    return new Vector3(0, 1, 0);
  }

  // 静态方法:向前向量
  static forward(): Vector3 {
    return new Vector3(0, 0, 1);
  }
}

3.2 4×4矩阵类实现

typescript 复制代码
// 4×4矩阵类 - 3D变换核心
export class Matrix4 {
  private elements: Float32Array;

  constructor() {
    // 初始化为单位矩阵
    this.elements = new Float32Array([
      1, 0, 0, 0,
      0, 1, 0, 0,
      0, 0, 1, 0,
      0, 0, 0, 1
    ]);
  }

  // 获取元素
  get(row: number, col: number): number {
    return this.elements[row * 4 + col];
  }

  // 设置元素
  set(row: number, col: number, value: number): void {
    this.elements[row * 4 + col] = value;
  }

  // 矩阵乘法
  multiply(m: Matrix4): Matrix4 {
    const result = new Matrix4();
    const a = this.elements;
    const b = m.elements;

    for (let i = 0; i < 4; i++) {
      for (let j = 0; j < 4; j++) {
        let sum = 0;
        for (let k = 0; k < 4; k++) {
          sum += a[i * 4 + k] * b[k * 4 + j];
        }
        result.set(i, j, sum);
      }
    }
    return result;
  }

  // 平移变换
  static translate(x: number, y: number, z: number): Matrix4 {
    const m = new Matrix4();
    m.set(0, 3, x);
    m.set(1, 3, y);
    m.set(2, 3, z);
    return m;
  }

  // 缩放变换
  static scale(x: number, y: number, z: number): Matrix4 {
    const m = new Matrix4();
    m.set(0, 0, x);
    m.set(1, 1, y);
    m.set(2, 2, z);
    return m;
  }

  // 绕X轴旋转
  static rotateX(angle: number): Matrix4 {
    const m = new Matrix4();
    const cos = Math.cos(angle);
    const sin = Math.sin(angle);
    m.set(1, 1, cos);
    m.set(1, 2, -sin);
    m.set(2, 1, sin);
    m.set(2, 2, cos);
    return m;
  }

  // 绕Y轴旋转
  static rotateY(angle: number): Matrix4 {
    const m = new Matrix4();
    const cos = Math.cos(angle);
    const sin = Math.sin(angle);
    m.set(0, 0, cos);
    m.set(0, 2, sin);
    m.set(2, 0, -sin);
    m.set(2, 2, cos);
    return m;
  }

  // 绕Z轴旋转
  static rotateZ(angle: number): Matrix4 {
    const m = new Matrix4();
    const cos = Math.cos(angle);
    const sin = Math.sin(angle);
    m.set(0, 0, cos);
    m.set(0, 1, -sin);
    m.set(1, 0, sin);
    m.set(1, 1, cos);
    return m;
  }

  // 透视投影矩阵
  static perspective(fov: number, aspect: number, near: number, far: number): Matrix4 {
    const m = new Matrix4();
    const f = 1.0 / Math.tan(fov / 2);
    const nf = 1 / (near - far);

    m.set(0, 0, f / aspect);
    m.set(1, 1, f);
    m.set(2, 2, (far + near) * nf);
    m.set(2, 3, 2 * far * near * nf);
    m.set(3, 2, -1);
    m.set(3, 3, 0);

    return m;
  }

  // 正交投影矩阵
  static orthographic(left: number, right: number, bottom: number, top: number, near: number, far: number): Matrix4 {
    const m = new Matrix4();
    m.set(0, 0, 2 / (right - left));
    m.set(1, 1, 2 / (top - bottom));
    m.set(2, 2, 2 / (near - far));
    m.set(0, 3, -(right + left) / (right - left));
    m.set(1, 3, -(top + bottom) / (top - bottom));
    m.set(2, 3, -(far + near) / (far - near));
    return m;
  }

  // 观察矩阵(LookAt)
  static lookAt(eye: Vector3, target: Vector3, up: Vector3): Matrix4 {
    const zAxis = eye.subtract(target).normalize();
    const xAxis = up.cross(zAxis).normalize();
    const yAxis = zAxis.cross(xAxis);

    const m = new Matrix4();
    m.set(0, 0, xAxis.x);
    m.set(0, 1, xAxis.y);
    m.set(0, 2, xAxis.z);
    m.set(0, 3, -xAxis.dot(eye));

    m.set(1, 0, yAxis.x);
    m.set(1, 1, yAxis.y);
    m.set(1, 2, yAxis.z);
    m.set(1, 3, -yAxis.dot(eye));

    m.set(2, 0, zAxis.x);
    m.set(2, 1, zAxis.y);
    m.set(2, 2, zAxis.z);
    m.set(2, 3, -zAxis.dot(eye));

    return m;
  }

  // 变换向量
  transformVector(v: Vector3): Vector3 {
    const e = this.elements;
    const w = e[12] * v.x + e[13] * v.y + e[14] * v.z + e[15];
    return new Vector3(
      (e[0] * v.x + e[1] * v.y + e[2] * v.z + e[3]) / w,
      (e[4] * v.x + e[5] * v.y + e[6] * v.z + e[7]) / w,
      (e[8] * v.x + e[9] * v.y + e[10] * v.z + e[11]) / w
    );
  }

  // 获取原始数据(用于传递给GPU)
  getElements(): Float32Array {
    return this.elements;
  }

  // 矩阵求逆
  invert(): Matrix4 {
    const m = this.elements;
    const inv = new Float32Array(16);

    inv[0] = m[5] * m[10] * m[15] - m[5] * m[11] * m[14] - m[9] * m[6] * m[15] +
             m[9] * m[7] * m[14] + m[13] * m[6] * m[11] - m[13] * m[7] * m[10];

    inv[4] = -m[4] * m[10] * m[15] + m[4] * m[11] * m[14] + m[8] * m[6] * m[15] -
             m[8] * m[7] * m[14] - m[12] * m[6] * m[11] + m[12] * m[7] * m[10];

    inv[8] = m[4] * m[9] * m[15] - m[4] * m[11] * m[13] - m[8] * m[5] * m[15] +
             m[8] * m[7] * m[13] + m[12] * m[5] * m[11] - m[12] * m[7] * m[9];

    inv[12] = -m[4] * m[9] * m[14] + m[4] * m[10] * m[13] + m[8] * m[5] * m[14] -
              m[8] * m[6] * m[13] - m[12] * m[5] * m[10] + m[12] * m[6] * m[9];

    let det = m[0] * inv[0] + m[1] * inv[4] + m[2] * inv[8] + m[3] * inv[12];

    if (det === 0) {
      return new Matrix4();
    }

    det = 1.0 / det;

    const result = new Matrix4();
    for (let i = 0; i < 16; i++) {
      result.elements[i] = inv[i] * det;
    }

    return result;
  }
}

3.3 相机系统实现

typescript 复制代码
// 3D相机类 - 视角控制核心
export class Camera3D {
  // 相机位置
  public position: Vector3;
  // 目标位置
  public target: Vector3;
  // 上方向
  public up: Vector3;
  
  // 投影参数
  private fov: number;
  private aspect: number;
  private near: number;
  private far: number;

  // 旋转角度(欧拉角)
  private yaw: number = 0;
  private pitch: number = 0;

  constructor(fov: number = 60, aspect: number = 1.0, near: number = 0.1, far: number = 1000) {
    this.position = new Vector3(0, 0, 5);
    this.target = new Vector3(0, 0, 0);
    this.up = Vector3.up();
    
    this.fov = fov * Math.PI / 180; // 转换为弧度
    this.aspect = aspect;
    this.near = near;
    this.far = far;
  }

  // 获取观察矩阵
  getViewMatrix(): Matrix4 {
    return Matrix4.lookAt(this.position, this.target, this.up);
  }

  // 获取投影矩阵
  getProjectionMatrix(): Matrix4 {
    return Matrix4.perspective(this.fov, this.aspect, this.near, this.far);
  }

  // 获取组合矩阵(MVP)
  getVPMatrix(): Matrix4 {
    return this.getProjectionMatrix().multiply(this.getViewMatrix());
  }

  // 设置宽高比
  setAspect(aspect: number): void {
    this.aspect = aspect;
  }

  // 移动相机
  move(delta: Vector3): void {
    this.position = this.position.add(delta);
    this.target = this.target.add(delta);
  }

  // 围绕目标旋转(轨道相机)
  orbit(angleX: number, angleY: number): void {
    this.yaw += angleX;
    this.pitch = Math.max(-Math.PI / 2 + 0.01, Math.min(Math.PI / 2 - 0.01, this.pitch + angleY));

    const radius = this.position.distance(this.target);
    
    this.position.x = this.target.x + radius * Math.cos(this.pitch) * Math.sin(this.yaw);
    this.position.y = this.target.y + radius * Math.sin(this.pitch);
    this.position.z = this.target.z + radius * Math.cos(this.pitch) * Math.cos(this.yaw);
  }

  // 缩放(改变相机距离)
  zoom(delta: number): void {
    const direction = this.position.subtract(this.target).normalize();
    const distance = this.position.distance(this.target);
    const newDistance = Math.max(1, Math.min(100, distance * (1 - delta * 0.1)));
    
    this.position = this.target.add(direction.multiply(newDistance));
  }

  // 屏幕坐标转世界射线
  screenToWorldRay(screenX: number, screenY: number, screenWidth: number, screenHeight: number): { origin: Vector3, direction: Vector3 } {
    // 转换到NDC坐标
    const ndcX = (2.0 * screenX / screenWidth) - 1.0;
    const ndcY = 1.0 - (2.0 * screenY / screenHeight);

    // 逆投影
    const invProj = this.getProjectionMatrix().invert();
    const invView = this.getViewMatrix().invert();

    // 近平面点
    const nearPoint = invView.transformVector(
      invProj.transformVector(new Vector3(ndcX, ndcY, -1))
    );

    // 远平面点
    const farPoint = invView.transformVector(
      invProj.transformVector(new Vector3(ndcX, ndcY, 1))
    );

    return {
      origin: this.position.clone(),
      direction: farPoint.subtract(nearPoint).normalize()
    };
  }
}

3.4 变换组件实现

typescript 复制代码
// 3D变换组件 - 游戏对象基础
export class Transform3D {
  public position: Vector3;
  public rotation: Vector3; // 欧拉角(弧度)
  public scale: Vector3;

  constructor() {
    this.position = Vector3.zero();
    this.rotation = Vector3.zero();
    this.scale = Vector3.one();
  }

  // 获取模型矩阵
  getModelMatrix(): Matrix4 {
    // T * Rz * Ry * Rx * S
    const translationMatrix = Matrix4.translate(this.position.x, this.position.y, this.position.z);
    const rotationX = Matrix4.rotateX(this.rotation.x);
    const rotationY = Matrix4.rotateY(this.rotation.y);
    const rotationZ = Matrix4.rotateZ(this.rotation.z);
    const scaleMatrix = Matrix4.scale(this.scale.x, this.scale.y, this.scale.z);

    return translationMatrix
      .multiply(rotationZ)
      .multiply(rotationY)
      .multiply(rotationX)
      .multiply(scaleMatrix);
  }

  // 设置位置
  setPosition(x: number, y: number, z: number): void {
    this.position.x = x;
    this.position.y = y;
    this.position.z = z;
  }

  // 设置旋转(角度)
  setRotation(x: number, y: number, z: number): void {
    this.rotation.x = x * Math.PI / 180;
    this.rotation.y = y * Math.PI / 180;
    this.rotation.z = z * Math.PI / 180;
  }

  // 设置缩放
  setScale(x: number, y: number, z: number): void {
    this.scale.x = x;
    this.scale.y = y;
    this.scale.z = z;
  }

  // 获取前方向
  getForward(): Vector3 {
    const cosY = Math.cos(this.rotation.y);
    const sinY = Math.sin(this.rotation.y);
    const cosX = Math.cos(this.rotation.x);
    const sinX = Math.sin(this.rotation.x);

    return new Vector3(
      sinY * cosX,
      -sinX,
      cosY * cosX
    );
  }

  // 获取右方向
  getRight(): Vector3 {
    return this.getForward().cross(Vector3.up()).normalize();
  }

  // 获取上方向
  getUp(): Vector3 {
    return this.getRight().cross(this.getForward()).normalize();
  }

  // 围绕点旋转
  rotateAround(point: Vector3, axis: Vector3, angle: number): void {
    const rotationMatrix = Matrix4.rotateZ(angle); // 简化:假设绕Z轴
    const translated = this.position.subtract(point);
    const rotated = rotationMatrix.transformVector(translated);
    this.position = point.add(rotated);
  }
}

3.5 完整渲染示例

typescript 复制代码
// 3D渲染管理器 - 整合所有组件
export class Renderer3D {
  private camera: Camera3D;
  private objects: Map<string, { transform: Transform3D, mesh: MeshData }> = new Map();
  private gl: WebGLRenderingContext | null = null;

  constructor() {
    this.camera = new Camera3D(60, 1.0, 0.1, 1000);
  }

  // 初始化渲染器
  initialize(canvas: HTMLCanvasElement): void {
    this.gl = canvas.getContext('webgl');
    if (!this.gl) {
      console.error('WebGL not supported');
      return;
    }

    // 设置视口
    this.gl.viewport(0, 0, canvas.width, canvas.height);
    this.camera.setAspect(canvas.width / canvas.height);

    // 启用深度测试
    this.gl.enable(this.gl.DEPTH_TEST);
    this.gl.depthFunc(this.gl.LEQUAL);

    // 设置清除颜色
    this.gl.clearColor(0.1, 0.1, 0.2, 1.0);
  }

  // 添加3D对象
  addObject(id: string, mesh: MeshData): void {
    this.objects.set(id, {
      transform: new Transform3D(),
      mesh: mesh
    });
  }

  // 获取对象变换
  getTransform(id: string): Transform3D | null {
    const obj = this.objects.get(id);
    return obj ? obj.transform : null;
  }

  // 渲染帧
  render(): void {
    if (!this.gl) return;

    // 清除缓冲
    this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT);

    // 获取VP矩阵
    const vpMatrix = this.camera.getVPMatrix();

    // 渲染所有对象
    this.objects.forEach((obj, id) => {
      const mvpMatrix = vpMatrix.multiply(obj.transform.getModelMatrix());
      this.renderMesh(obj.mesh, mvpMatrix);
    });
  }

  // 渲染网格
  private renderMesh(mesh: MeshData, mvpMatrix: Matrix4): void {
    // 实际渲染逻辑(简化示例)
    // 将mvpMatrix传递给着色器进行顶点变换
    console.log('Rendering mesh with MVP matrix');
  }

  // 相机控制
  getCamera(): Camera3D {
    return this.camera;
  }

  // 窗口大小变化
  onResize(width: number, height: number): void {
    if (this.gl) {
      this.gl.viewport(0, 0, width, height);
      this.camera.setAspect(width / height);
    }
  }
}

// 网格数据接口
interface MeshData {
  vertices: Float32Array;
  normals: Float32Array;
  indices: Uint16Array;
  textureCoords?: Float32Array;
}

四、踩坑与注意事项

4.1 矩阵乘法顺序

问题: 变换矩阵的乘法顺序至关重要,错误的顺序会导致变换结果不符合预期。

解决方案: 记住变换顺序为 TRS(Translate × Rotate × Scale),先缩放,再旋转,最后平移。

typescript 复制代码
// ❌ 错误示例:顺序颠倒
const wrongMatrix = scaleMatrix.multiply(rotationMatrix).multiply(translationMatrix);

// ✅ 正确示例:T * R * S
const correctMatrix = translationMatrix.multiply(rotationMatrix).multiply(scaleMatrix);

4.2 欧拉角万向锁

问题: 使用欧拉角表示旋转时,当两个轴重合时会出现万向锁(Gimbal Lock),导致失去一个自由度。

解决方案: 对于复杂旋转场景,建议使用四元数(Quaternion)代替欧拉角。

typescript 复制代码
// 四元数基础实现
export class Quaternion {
  public x: number;
  public y: number;
  public z: number;
  public w: number;

  constructor(x: number = 0, y: number = 0, z: number = 0, w: number = 1) {
    this.x = x;
    this.y = y;
    this.z = z;
    this.w = w;
  }

  // 从欧拉角创建四元数
  static fromEuler(x: number, y: number, z: number): Quaternion {
    const cx = Math.cos(x / 2);
    const cy = Math.cos(y / 2);
    const cz = Math.cos(z / 2);
    const sx = Math.sin(x / 2);
    const sy = Math.sin(y / 2);
    const sz = Math.sin(z / 2);

    return new Quaternion(
      sx * cy * cz - cx * sy * sz,
      cx * sy * cz + sx * cy * sz,
      cx * cy * sz - sx * sy * cz,
      cx * cy * cz + sx * sy * sz
    );
  }

  // 四元数乘法(组合旋转)
  multiply(q: Quaternion): Quaternion {
    return new Quaternion(
      this.w * q.x + this.x * q.w + this.y * q.z - this.z * q.y,
      this.w * q.y - this.x * q.z + this.y * q.w + this.z * q.x,
      this.w * q.z + this.x * q.y - this.y * q.x + this.z * q.w,
      this.w * q.w - this.x * q.x - this.y * q.y - this.z * q.z
    );
  }

  // 归一化
  normalize(): Quaternion {
    const mag = Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z + this.w * this.w);
    return new Quaternion(this.x / mag, this.y / mag, this.z / mag, this.w / mag);
  }

  // 转换为旋转矩阵
  toMatrix(): Matrix4 {
    const m = new Matrix4();
    const xx = this.x * this.x;
    const yy = this.y * this.y;
    const zz = this.z * this.z;
    const xy = this.x * this.y;
    const xz = this.x * this.z;
    const yz = this.y * this.z;
    const wx = this.w * this.x;
    const wy = this.w * this.y;
    const wz = this.w * this.z;

    m.set(0, 0, 1 - 2 * (yy + zz));
    m.set(0, 1, 2 * (xy - wz));
    m.set(0, 2, 2 * (xz + wy));

    m.set(1, 0, 2 * (xy + wz));
    m.set(1, 1, 1 - 2 * (xx + zz));
    m.set(1, 2, 2 * (yz - wx));

    m.set(2, 0, 2 * (xz - wy));
    m.set(2, 1, 2 * (yz + wx));
    m.set(2, 2, 1 - 2 * (xx + yy));

    return m;
  }
}

4.3 坐标系手性

问题: 不同图形API使用不同的坐标系手性(左手或右手),混用会导致镜像翻转。

HarmonyOS OpenGL ES: 使用右手坐标系,Z轴正方向指向屏幕外。

typescript 复制代码
// 右手坐标系验证
const rightHandCheck = () => {
  const a = new Vector3(1, 0, 0); // X轴
  const b = new Vector3(0, 1, 0); // Y轴
  const c = a.cross(b); // Z轴 = X × Y
  
  console.log(`Z轴方向: (${c.x}, ${c.y}, ${c.z})`); // 应为 (0, 0, 1)
};

4.4 精度问题

问题: 浮点数精度误差累积会导致渲染异常,如Z-fighting(深度冲突)。

解决方案:

  • 使用相对误差比较而非精确相等
  • 设置合理的深度缓冲精度
  • 避免过大的远裁剪面距离
typescript 复制代码
// 浮点数比较工具
export class MathUtils {
  static readonly EPSILON = 1e-6;

  static approximately(a: number, b: number): boolean {
    return Math.abs(a - b) < MathUtils.EPSILON;
  }

  static clamp(value: number, min: number, max: number): number {
    return Math.max(min, Math.min(max, value));
  }

  // 平滑插值
  static smoothStep(edge0: number, edge1: number, x: number): number {
    const t = MathUtils.clamp((x - edge0) / (edge1 - edge0), 0, 1);
    return t * t * (3 - 2 * t);
  }
}

4.5 性能优化要点

  1. 避免每帧创建新对象:复用矩阵和向量对象
  2. 使用Float32Array:减少内存分配,提高缓存命中率
  3. 批量矩阵运算:减少函数调用开销
  4. 使用SIMD指令:HarmonyOS NAPI支持SIMD加速
typescript 复制代码
// 对象池模式 - 复用临时对象
export class MatrixPool {
  private pool: Matrix4[] = [];
  private index: number = 0;

  get(): Matrix4 {
    if (this.index >= this.pool.length) {
      this.pool.push(new Matrix4());
    }
    return this.pool[this.index++];
  }

  reset(): void {
    this.index = 0;
  }
}

// 使用示例
const matrixPool = new MatrixPool();

function updateTransforms(objects: Transform3D[]): void {
  matrixPool.reset();
  
  objects.forEach(obj => {
    const tempMatrix = matrixPool.get();
    // 使用临时矩阵进行计算
  });
}

五、总结

本文系统讲解了HarmonyOS 3D图形渲染的基础知识,从坐标系统体系到变换矩阵原理,再到完整的代码实现。核心要点如下:

技术要点回顾

知识点 关键内容
坐标系统 局部→世界→相机→裁剪→NDC→屏幕的完整转换链
变换矩阵 平移、旋转、缩放、投影的4×4齐次矩阵实现
渲染管线 顶点处理→投影→光栅化→片段处理的完整流程
相机系统 LookAt矩阵、透视投影、轨道控制的实现
数学工具 Vector3、Matrix4、Quaternion的完整实现

最佳实践建议

  1. 建立数学库:构建可复用的3D数学库,避免重复造轮子
  2. 理解变换顺序:牢记TRS顺序,避免变换错误
  3. 使用四元数:复杂旋转场景使用四元数避免万向锁
  4. 注意精度:使用相对误差比较,设置合理的深度缓冲精度
  5. 性能优化:复用对象、使用TypedArray、批量运算

扩展方向

  • 骨骼动画:基于矩阵调色盘的骨骼动画系统
  • 物理集成:与物理引擎的坐标系统对接
  • LOD系统:基于距离的细节层次管理
  • 实例化渲染:GPU实例化提升渲染效率

掌握3D渲染基础是构建复杂3D应用的第一步,后续文章将继续深入OpenGL ES渲染、模型加载、交互处理和性能优化等主题,帮助开发者全面掌握HarmonyOS 3D开发技能。