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 性能优化要点
- 避免每帧创建新对象:复用矩阵和向量对象
- 使用Float32Array:减少内存分配,提高缓存命中率
- 批量矩阵运算:减少函数调用开销
- 使用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的完整实现 |
最佳实践建议
- 建立数学库:构建可复用的3D数学库,避免重复造轮子
- 理解变换顺序:牢记TRS顺序,避免变换错误
- 使用四元数:复杂旋转场景使用四元数避免万向锁
- 注意精度:使用相对误差比较,设置合理的深度缓冲精度
- 性能优化:复用对象、使用TypedArray、批量运算
扩展方向
- 骨骼动画:基于矩阵调色盘的骨骼动画系统
- 物理集成:与物理引擎的坐标系统对接
- LOD系统:基于距离的细节层次管理
- 实例化渲染:GPU实例化提升渲染效率
掌握3D渲染基础是构建复杂3D应用的第一步,后续文章将继续深入OpenGL ES渲染、模型加载、交互处理和性能优化等主题,帮助开发者全面掌握HarmonyOS 3D开发技能。