HarmonyOS应用里的空间几何体可视化,听起来像是个必须上游戏引擎才能搞定的需求,但我实际做下来发现,在轻量场景下用ArkUI的Canvas配合一套不算复杂的数学变换,就能把立方体、球体、圆柱体这些常见几何体流畅地渲染出来。这篇文章分享一下这个项目从选型、数学基础、核心实现到调试落地的完整过程,涉及投影与旋转原理、几何体网格生成、触控交互、hdb调试等关键环节。如果你正在做教育类工具、数据可视化应用,或者刚接触HarmonyOS图形开发,这篇文章应该能帮你少踩几个坑。
1. 项目背景:为什么需要空间几何体可视化
1.1 真实场景里的三维可视化需求
先说需求从哪来。教育类App里经常要展示立体几何,比如高中数学里的三棱锥、圆柱、球,学生需要旋转观察、理解空间关系。平面教材里画得再细,也远不如一个可以实时拖拽旋转的三维模型直观。另外工程领域也有类似需求,比如简单设备的结构示意、传感器数据的三维分布展示,都需要把空间数据“画”出来。
这类需求有个共性:几何体并不复杂,顶点数量从几十到几千,但要支持实时旋转、缩放、切换不同类型,而且要在手机、平板甚至后续可能的手表等设备上流畅运行。这不是一个需要加载复杂场景、材质、光照的游戏项目,而是一个“够用就好”的轻量可视化组件。
1.2 这次项目要解决的核心问题
基于这个背景,我把项目目标拆成了三个问题:
第一,怎么用HarmonyOS的ArkUI框架在画布上画出三维图形。ArkUI提供了Canvas组件,但Canvas本质是二维绘图上下文,把所有三维渲染工作都交给了开发者。这一步的选择直接影响整个项目的复杂度。
第二,怎么做到交互实时。用户拖拽旋转时,每一帧都要重新计算所有顶点的位置并重绘,如果数学变换写得低效,或者绘制过程中频繁创建对象,帧率会明显下降,体验非常差。
第三,怎么控制开发成本。引入WebGL或者SceneKit当然性能更好,但它们的学习成本和工程接入成本都高不少。对几何体可视化这个特定场景来说,有没有更轻的替代方案?这是我重点考虑的问题。
带着这三个问题,我开始梳理技术路线。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术选型:不盲目上引擎,先想清楚渲染路径
2.1 三条候选路线的优缺点对比
在HarmonyOS上做三维图形渲染,我评估了三条路线:
路线一是Canvas 2D加手动投影变换。核心思路是:所有几何体都用三维顶点数组表示,自己写旋转矩阵和投影逻辑,把三维坐标换算成屏幕上的二维坐标,然后用Canvas的线段绘制。优点是完全在ArkUI框架内完成,不涉及C++和图形API,工程结构简单;缺点是所有变换都要自己实现,且绘制性能上限不高。
路线二是WebGL或者直接使用EGL接入GPU渲染。这是游戏级方案,能发挥硬件加速能力,支持大量顶点和复杂着色器,但需要处理GL上下文创建、着色器编译、缓冲区管理,代码量成倍增加,在ArkTS里还要做更多的类型适配。
路线三是使用SceneKit等三维渲染框架。HarmonyOS系统级框架对复杂场景支持更好,自带相机、光照、材质系统,但接入成本高,而且对本项目要做的几何体线框展示来说属于大材小用。
三条路线的对比我整理了一下:
| 方案 | 开发成本 | 渲染性能 | 适用场景 |
|---|---|---|---|
| Canvas 2D + 手动投影 | 低 | 中等,适合千级顶点 | 几何体线框、轻量可视化 |
| WebGL / EGL | 高 | 高,GPU加速 | 复杂模型、游戏级场景 |
| SceneKit等高级框架 | 很高 | 高 | 大型3D场景、复杂材质 |
2.2 为什么最终选择Canvas 2D方案
我最终选择了Canvas 2D加手动投影方案,原因很实际。
本项目要展示的是空间几何体,不是雕刻精细的角色模型。几何体的视觉核心是顶点之间的连接关系,也就是线框结构。线框绘制的本质就是画直线,Canvas的moveTo和lineTo天然适合这个任务。顶点数量最多也不过几千个,每帧计算几千次旋转乘法和投影,在当前移动设备上完全能承受。
另一个原因是工程维护成本。Canvas方案的所有代码都集中在ArkTS层,出错时可以在DevEco Studio里直接断点调试,不需要处理JSI桥接、本地库编译等问题。对于团队里没有图形学背景的开发者来说,这套方案几乎是零门槛。
当然也要说明,这个选择是有范围限定的。如果项目后续要加复杂光影纹理,或者要加载几千个网格组成的模型,那就必须切换到WebGL或者SceneKit。到时候Canvas部分积累的数学逻辑也可以复用,不算白做。
2.3 环境准备与工程创建
项目基于DevEco Studio 4.0以上版本创建,API版本选择9或10。创建应用时选择“Empty Ability”模板即可。
关键配置是模块的ArkTS支持范围,建议保持默认的“Super”模式,这样能用上最新的语法特性。Canvas组件需要显式声明CanvasRenderingContext2D对象,这一步在页面build方法里绑定:
typescript复制private settings: RenderingContextSettings = new RenderingContextSettings(true);
private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);
build() {
Canvas(this.context)
.width('100%')
.height('100%')
.onReady(() => {
this.init();
})
}
这里有个细节值得注意:Canvas的RenderingContextSettings构造参数允许抗锯齿,true就开启。几何体线框在旋转时会产生大量斜线,不开抗锯齿会有明显的锯齿感,视觉效果打折扣,建议保持开启。
3. 前置数学基础:把三维坐标画到二维屏幕上的核心原理
3.1 三维坐标系与旋转约定
所有三维图形的基础都是一组三维坐标点。我这里采用右手坐标系:X轴向右,Y轴向上,Z轴指向屏幕外。这个约定和大多数数学教材一致,也方便后续统一旋转公式。
旋转操作分三步理解:绕X轴旋转改变点的Y、Z分量,绕Y轴旋转改变X、Z分量,绕Z轴旋转改变X、Y分量。对空间几何体可视化来说,最常用的是绕X轴和绕Y轴旋转,因为交互上用户习惯上下滑动时绕水平轴转,左右滑动时绕竖直轴转。
旋转矩阵的公式并不复杂。绕X轴旋转角θ时,点的坐标变化是:
code复制y' = y * cosθ - z * sinθ
z' = y * sinθ + z * cosθ
绕Y轴旋转角θ时:
code复制x' = x * cosθ + z * sinθ
z' = -x * sinθ + z * cosθ
这里要特别注意运算符顺序。ArkTS里的三角函数都接受弧度值,而用户交互时拿到的是角度增量,必须用Math.PI / 180换算,否则旋转速度会变得不可控。
3.2 旋转矩阵的代码实现与性能考量
在ArkTS里我习惯把三维坐标定义成interface,方便类型检查:
typescript复制interface Vec3 {
x: number;
y: number;
z: number;
}
旋转函数直接对Vec3做变换,返回新对象:
typescript复制function rotateX(p: Vec3, angle: number): Vec3 {
const c = Math.cos(angle);
const s = Math.sin(angle);
return {
x: p.x,
y: p.y * c - p.z * s,
z: p.y * s + p.z * c
};
}
function rotateY(p: Vec3, angle: number): Vec3 {
const c = Math.cos(angle);
const s = Math.sin(angle);
return {
x: p.x * c + p.z * s,
y: p.y,
z: -p.x * s + p.z * c
};
}
性能层面有一个细节:不要每次旋转时都计算cos和sin。虽然这个开销对单个点微乎其微,但一帧内要处理几百上千个顶点,每个顶点可能要旋转两次,积累起来就很可观。我通常把当前帧的cos、sin值先算好,再循环套用到所有顶点上。
3.3 投影方式的选择:透视还是正交
旋转后的顶点仍然在三维空间,要显示到屏幕上还必须做投影。我实现了两种投影,通过一个布尔开关切换。
正交投影最简单,直接取X、Y坐标作为屏幕坐标,图形大小不随Z距离变化。这个模式适合工程图、结构图,能真实反映物体的比例关系。
透视投影模拟人眼效果,距离屏幕越远的点看起来越小。实现上可以用一个简单的缩放因子:
typescript复制function project(p: Vec3, perspective: boolean, centerX: number, centerY: number): Vec2 {
let factor = 1;
if (perspective) {
const distance = 400;
factor = distance / (distance + p.z);
}
return {
x: centerX + p.x * factor,
y: centerY - p.y * factor
};
}
这里注意distance常量表示相机到物体的距离,数值越大透视效果越弱。我实测下来取300到500之间比较自然。
生活里理解这两种投影的差别很简单:正交投影像工厂里画的三视图,尺寸精确但缺乏立体感;透视投影像人眼看一栋楼,近大远小,立体感真实。对教育展示来说,透视投影更直观,所以我默认开启透视模式。
4. 核心实现:从立方体到复杂几何体的完整流程
4.1 几何体数据模型设计
开始写代码前,先定义几何体的数据结构。我把几何体抽象成顶点列表和边索引列表:
typescript复制interface Geometry {
vertices: Vec3[];
edges: [number, number][];
}
vertices保存所有三维顶点,edges保存顶点索引对。举个例子,一个单位正方体有8个顶点,分别对应坐标(±1, ±1, ±1),12条边对应这些顶点之间的连接关系。旋转时只需要遍历vertices更新坐标,edges完全不变,因为顶点之间的连接关系不会因旋转而改变。
这个设计的优势在于:绘制任何一种几何体,只要给出顶点和边,渲染代码可以完全复用。后面实现球体、圆柱体时,我只需要新增一个生成函数,不需要动任何渲染逻辑。
4.2 立方体的实现与线框绘制
立方体的8个顶点可以这样生成:
typescript复制function createCube(size: number): Geometry {
const s = size / 2;
const vertices: Vec3[] = [
{ x: -s, y: -s, z: -s }, // 0
{ x: s, y: -s, z: -s }, // 1
{ x: s, y: s, z: -s }, // 2
{ x: -s, y: s, z: -s }, // 3
{ x: -s, y: -s, z: s }, // 4
{ x: s, y: -s, z: s }, // 5
{ x: s, y: s, z: s }, // 6
{ x: -s, y: s, z: s } // 7
];
const edges: [number, number][] = [
[0, 1], [1, 2], [2, 3], [3, 0],
[4, 5], [5, 6], [6, 7], [7, 4],
[0, 4], [1, 5], [2, 6], [3, 7]
];
return { vertices, edges };
}
8个顶点分成前后面两组,每组4个,再加上连接前后面的竖边,一共12条边。这样画出来的立方体是完整线框,任何角度看都能正确显示。
绘制时,每一帧的流程是:清空画布,对所有顶点应用旋转矩阵,再应用投影,最后按边索引连线。核心绘制代码:
typescript复制drawGeometry(g: Geometry) {
const ctx = this.context;
const width = this.width;
const height = this.height;
ctx.clearRect(0, 0, width, height);
const centerX = width / 2;
const centerY = height / 2;
const cosX = Math.cos(this.rotationX);
const sinX = Math.sin(this.rotationX);
const cosY = Math.cos(this.rotationY);
const sinY = Math.sin(this.rotationY);
const transformed = new Array<Vec3>(g.vertices.length);
for (let i = 0; i < g.vertices.length; i++) {
let p = g.vertices[i];
let p1 = {
x: p.x,
y: p.y * cosX - p.z * sinX,
z: p.y * sinX + p.z * cosX
};
let p2 = {
x: p1.x * cosY + p1.z * sinY,
y: p1.y,
z: -p1.x * sinY + p1.z * cosY
};
transformed[i] = p2;
}
ctx.strokeStyle = '#00A8FF';
ctx.lineWidth = 2;
ctx.beginPath();
for (let i = 0; i < g.edges.length; i++) {
const idx0 = g.edges[i][0];
const idx1 = g.edges[i][1];
const v0 = this.projectToScreen(transformed[idx0], centerX, centerY);
const v1 = this.projectToScreen(transformed[idx1], centerX, centerY);
ctx.moveTo(v0.x, v0.y);
ctx.lineTo(v1.x, v1.y);
}
ctx.stroke();
}
这个drawGeometry方法对任何几何体都通用,换几何体只需要换Geometry对象,这是整个项目里最高性价比的设计。
4.3 球体网格生成算法
球体比立方体复杂,需要按经纬网方式生成顶点。经度方向从0到2π,纬度方向从-π/2到π/2,每隔一个固定角度取一个点。
typescript复制function createSphere(radius: number, lonSegments: number, latSegments: number): Geometry {
const vertices: Vec3[] = [];
const edges: [number, number][] = [];
for (let lat = 0; lat <= latSegments; lat++) {
const phi = Math.PI * lat / latSegments - Math.PI / 2;
const y = radius * Math.sin(phi);
const r = radius * Math.cos(phi);
for (let lon = 0; lon <= lonSegments; lon++) {
const theta = 2 * Math.PI * lon / lonSegments;
const x = r * Math.cos(theta);
const z = r * Math.sin(theta);
vertices.push({ x, y, z });
}
}
// 纬线:同一纬度的相邻点相连
for (let lat = 0; lat <= latSegments; lat++) {
const rowStart = lat * (lonSegments + 1);
for (let lon = 0; lon < lonSegments; lon++) {
edges.push([rowStart + lon, rowStart + lon + 1]);
}
}
// 经线:同一经度的相邻纬度点相连
for (let lon = 0; lon <= lonSegments; lon++) {
for (let lat = 0; lat < latSegments; lat++) {
const current = lat * (lonSegments + 1) + lon;
edges.push([current, current + lonSegments + 1]);
}
}
return { vertices, edges };
}
这里有个实操经验的细节:分段数不要贪多。lonSegments和latSegments各取24时,顶点数是25乘25等于625个,线框已经足够圆滑。再往上加到48,顶点数接近2400个,视觉差异很小,但每帧计算量翻了几倍。我建议球体默认用24。
4.4 圆柱体的实现与循环逻辑
圆柱体可以拆成顶面圆、底面圆和侧面三部分。侧面展开是个矩形,顶面和底面是圆形轮廓。实际画线框时,只要把侧面竖线和上下圆的边线画出来即可。
我生成的思路是:共用一个角度数组,从0到2π取N个点,N取32。每个角度对应两个顶点:顶上和底下。这样侧面竖线就是每个角度的上下顶点连线,上下圆就是把相邻角度的顶点依次连线。
typescript复制function createCylinder(radius: number, height: number, segments: number): Geometry {
const vertices: Vec3[] = [];
const edges: [number, number][] = [];
const halfH = height / 2;
for (let i = 0; i < segments; i++) {
const theta = 2 * Math.PI * i / segments;
const x = radius * Math.cos(theta);
const z = radius * Math.sin(theta);
vertices.push({ x, y: halfH, z });
vertices.push({ x, y: -halfH, z });
}
// 侧面竖线
for (let i = 0; i < segments; i++) {
edges.push([i * 2, i * 2 + 1]);
}
// 顶面和底面圆
for (let i = 0; i < segments; i++) {
const next = (i + 1) % segments;
edges.push([i * 2, next * 2]);
edges.push([i * 2 + 1, next * 2 + 1]);
}
return { vertices, edges };
}
这里用模运算处理最后一个点回到第一个点的闭合问题,是生成所有圆环型几何体的通用技巧。
4.5 渲染循环的执行时机
渲染循环我用requestAnimationFrame实现,而不是setInterval。requestAnimationFrame由系统根据屏幕刷新率调度,通常为60帧每秒,它会在当前帧准备时执行回调,不会像setInterval那样出现掉帧或者多帧堆积。
typescript复制private animationId: number = 0;
startRenderLoop() {
const loop = () => {
this.drawGeometry(this.currentGeometry);
this.animationId = requestAnimationFrame(loop);
};
this.animationId = requestAnimationFrame(loop);
}
stopRenderLoop() {
if (this.animationId !== 0) {
cancelAnimationFrame(this.animationId);
this.animationId = 0;
}
}
组件退出时必须在aboutToDisappear里调用stopRenderLoop,否则动画帧会持续存在,可能导致页面无法释放,这是我在开发中遇到的一个明显的内存隐患。
5. 交互设计:让模型响应手势
5.1 单指拖拽旋转的加速度与手感
旋转交互直接绑定在Canvas组件的onTouch事件上。单指拖拽时,根据手指移动的水平和垂直距离分别更新绕Y轴和绕X轴的旋转角度。
typescript复制.onTouch((event: TouchEvent) => {
if (event.type === TouchType.Down) {
this.lastX = event.touches[0].x;
this.lastY = event.touches[0].y;
this.isDragging = true;
} else if (event.type === TouchType.Move && this.isDragging) {
const dx = event.touches[0].x - this.lastX;
const dy = event.touches[0].y - this.lastY;
this.rotationY += dx * 0.01;
this.rotationX += dy * 0.01;
this.lastX = event.touches[0].x;
this.lastY = event.touches[0].y;
} else if (event.type === TouchType.Up) {
this.isDragging = false;
}
})
0.01这个系数是旋转灵敏度,实测下来对手机屏幕比较合适。系数太小的话要拖很久才能转一圈,太大会有“发飘”的感觉,很难精确控制角度。Pad屏幕更大,同样的拖拽距离对应的角度变化应该更明显,所以系数可以提高到0.015左右。
有个细节要注意:旋转方向与手指方向的关系。手指向右拖,几何体应该绕Y轴正向旋转,对应Y轴旋转角度增加,这样视觉上模型会跟着手的方向走,不会出现“拧着来”的别扭感。
5.2 双指缩放的实现与防误触
双指缩放需要用event.touches数组里的两个点。实现原理是计算两指间距,与按下时的初始间距比较,得出比例因子,然后应用到物体尺寸上。
typescript复制let initialDistance = 0;
let currentScale = 1;
// Down时记录
if (event.touches.length === 2) {
const dx = event.touches[0].x - event.touches[1].x;
const dy = event.touches[0].y - event.touches[1].y;
initialDistance = Math.sqrt(dx * dx + dy * dy);
}
// Move时
if (event.touches.length === 2) {
const dx = event.touches[0].x - event.touches[1].x;
const dy = event.touches[0].y - event.touches[1].y;
const distance = Math.sqrt(dx * dx + dy * dy);
this.geometryScale = currentScale * distance / initialDistance;
}
这里有一个特别容易踩的坑:从单指变为双指时,TouchEvent的touches长度会从1变成2,如果不做好状态判断,双指落地的一瞬间会触发单指旋转逻辑,导致视角突然跳一下。我的做法是增加一个fingerCount变量记录当前有效手指数,在数量变化时重置所有临时状态。
缩放范围建议限制在0.3到3.0之间,否则物体太小看不清或者太大跑出画布边界,都是很差的操作体验。
5.3 自动旋转与手动操作的切换
除了手动拖拽,我还加了一个自动旋转模式,适合教学演示时无人操作的情况。思路很简单:每帧固定增加一个很小的角速度,比如rotationY每帧加0.005弧度,大约每6秒转一圈,节奏比较舒缓。
自动旋转和手动操作需要互斥。手指按下时立刻停止自动旋转,手指抬起后不自动恢复,需要用户点击一个“自动旋转”按钮重新开启。这样设计的好处是:用户一旦介入,就完全掌握控制权,不会出现“模型自己转着,用户想停下来却停不住”的糟糕体验。
6. 调试实战:命令行与无线调试的落地记录
6.1 用hdb命令行快速抓取应用日志
开发过程中,如果只用DevEco Studio的Log窗口也能看日志,但工程大了以后日志刷得特别快,关键信息一刷而过。我更习惯用hdb命令行抓日志,灵活性和可控性都更高。
hdb是HarmonyOS的调试桥接工具,和Android的adb思路类似。连接真机后,常用命令如下:
bash复制hdb devices
hdb shell
hdb hilog
hdb hilog可以加过滤条件,只打印当前应用进程的日志。比如我的应用包名是com.example.geometryview,可以这样过滤:
bash复制hdb shell hilog -p com.example.geometryview | grep -i geometry
这样每次打印console.info时,都能精准定位。项目里几何体的顶点数、帧率这类数据,我都是通过console.info打出来,再用hdb hilog抓取验证的。
6.2 HarmonyOS 4.2开启无线调试的步骤
很多时候真机插着USB线不方便,尤其是调试旋转算法时需要双手拿着设备反复观察。HarmonyOS 4.2支持无线调试,开启后通过WiFi连接设备,不需要数据线。
具体开启步骤:在真机上进入设置 -> 系统 -> 开发者选项,往下拉找到“无线调试”,打开开关。此时系统会显示设备的IP地址和端口,比如192.168.1.100:39741。然后在电脑上执行:
bash复制hdb connect 192.168.1.100:39741
hdb shell
连接成功后,hdb devices应该能看到设备列表。无线调试和USB调试可以共存,但要注意两者同时连接时,hdb默认优先走USB,如果想走无线方式,可能需要断开USB或者指定传输方式。
我实际遇到过一个情况:无线调试连接显示成功,但部署应用特别慢。排查后发现问题出在WiFi网络环境,办公室路由器信号不稳定,后来换到5G频段后恢复正常。无线调试在开发过程中适合快捷查看日志,但大批量安装应用还是USB稳定。
6.3 渲染异常的三类典型问题排查
调试渲染问题时,我总结了三类高频异常和对应的排查思路。
画面完全空白:先检查Canvas是否真的渲染了东西,可以临时画一条固定位置的测试直线。如果测试直线能看到,说明Canvas组件本身正常,问题在几何体数据或变换逻辑。接着检查是否有NaN值,比如投影时除数出现了0,或者旋转角度异常。我在代码里对每个顶点的变换结果做了范围检查,一旦发现NaN立即用console.warn输出。
旋转卡顿:优先怀疑每帧重复创建大对象。早期版本我在循环里new了太多对象,导致垃圾回收频繁。后来把临时对象复用,卡顿明显缓解。还有一个隐藏原因,如果Canvas的尺寸被设置成动态值,每一帧都会触发布局计算,这会严重影响性能。正确做法是在onReady时缓存画布实际像素宽高。
触摸响应不灵敏:先确认TouchEvent里取到的坐标是相对于组件的还是相对于屏幕的。Canvas的坐标系和事件坐标系如果不同,手指移动方向和模型旋转方向会出现明显偏差。我统一用event.touches[0].x这样的组件相对坐标,问题就消失了。
7. 常见坑位汇总与后续扩展建议
7.1 坐标原点、类型安全、网格密度三个高频坑
坐标原点是我第一次写Canvas绘图时踩得最深的坑。Canvas的坐标原点在左上角,X轴向右,Y轴向下,而数学坐标系Y轴向上。几何体旋转后得到的Y坐标是正的,直接绘制时物体会跑到画布上方甚至完全在屏幕外。解决办法是在project函数里对Y坐标取反,或者绘制时用centerY减去计算得到的Y值。
类型安全也是ArkTS里容易踩的坑。ArkTS对类型的要求比JavaScript严格得多,我在早期版本里习惯用let p = { x: 0, y: 0, z: 0 }这样的推断写法,一旦在函数间传递,ArkTS编译器会要求显式类型。把所有坐标变量统一声明为Vec3、Vec2接口类型后,编译错误少了很多。
网格密度需要按需调整。球体的经纬网格太密则顶点数爆炸,太疏则看起来像个多面体而不是球。圆柱体如果只显示线框,侧面竖线数量非常重要,太少则视觉上圆柱体会显得“塌陷”,我的经验值是至少32段。
7.2 如何扩展材质、光照与更多几何体
目前实现的是线框模式,后续想进一步提升展示效果,可以考虑按面填充。做法是在Geometry里增加faces字段,存储每个面的顶点索引。绘制时先按面填充半透明色块,再叠加线框。半透明度用RGBA的alpha值控制,比如fillStyle设为rgba(0, 168, 255, 0.15),就能做出通透的玻璃质感。
光照效果更复杂一些,需要计算每个面的法向量,根据法向量与光源方向的夹角调整面填充的颜色亮度。这个逻辑在顶点数不多的几何体上完全可行,但对每个三角形面都要做一次点乘和颜色插值,实现量不小。如果项目对视觉效果要求高,这一步值得投入。
其他几何体,比如圆锥、圆环、正多面体,都可以用类似的顶点加边模型生成。正四面体、正六面体这些正多面体的顶点和边可以用公式生成,逻辑相对固定。
7.3 这次项目沉淀下来的通用经验
做这个项目给我的一个明显体会是:三维渲染的门槛并没有想象中那么高。很多人一听到“三维可视化”就条件反射地想上引擎,但实际上很多业务场景的数据量和交互复杂度根本不需要那么重的方案。Canvas 2D加数学变换的组合,在HarmonyOS这样的移动平台上,能覆盖相当大一部分轻量三维展示需求。
关键是数学部分一定要写扎实。旋转矩阵、投影、坐标系转换这些基础概念,用的时候翻书不难,难的是把它们组合成一个高性能、可维护的渲染管线。建议把所有变换逻辑抽成独立的工具函数,和UI代码完全分离,这样后面无论是换渲染方案还是复用代码,都会省很多事。
如果你也正在做类似的功能,我的建议是:先定义好Geometry数据结构,这是整个系统的核心抽象;然后写一个通用的drawGeometry方法,确保它能处理任意Geometry;最后再慢慢加交互和动画。按这个顺序往下走,你会发现整个开发过程非常顺利,不容易返工。
