资讯中心

跨平台AR/VR天文科普应用开发:从天文算法到Unity渲染

📅 2026/10/6 4:14:19
跨平台AR/VR天文科普应用开发:从天文算法到Unity渲染
去年冬天我在小区里给女儿科普猎户座讲到参宿四是一颗随时可能走向超新星的红色超巨星孩子抬头问“那颗星现在到底在哪个方向”我掏出手机翻了一圈居然没有一个App能对着真实的天空把星座指给她看。恰逢这个系列写到了第12章前面几章已经从天球坐标讲到行星轨道计算数学底子都搭好了那不如干脆自己做一版。这篇文章就是完整记录我如何把一个“科普想法”从天文算法一路做到能在手机和VR眼镜上跑的跨平台AR/VR应用。整个项目折腾下来最大的体会是天文科普应用真正难的不是算法本身而是把“赤经赤纬”这种专业天球坐标变成玩家眼前一个准确、流畅、看得懂的星空。算法层和渲染层只要有一个坐标对不上满屏星星就会变成“抽象艺术”。我会把整条链路分成方案选型、算法解算、渲染实操、性能排坑四个大块中间穿插我在真机调试时踩过的坑和补救过程。如果你正打算做星空导览、天文科普工具或者单纯想学Unity里接AR/VR渲染的完整套路这篇应该能从第一行给你带到打包。1. 为什么这么选型跨平台方案与整体架构1.1 为什么最终选了Unity这条路线这个项目从一开始就定了一个硬指标同一套代码要能在Android、iOS、以及Quest这类VR一体机上跑。如果按传统思路拆成两个原生项目来做光AR/VR两套SDK就能让我维护到崩溃。备选方案其实还有几个但实际比下来差距很明显。方案跨平台UI生态AR/VR能力3D渲染性能踩坑成本iOS原生 Android原生各自为战各自接SDK最高双倍工作量Flutter很强插件不成熟一般做2D没问题3D星空吃力React Native很强需要桥接原生一般不适合沉浸式场景Unity AR Foundation够用原生级高学习曲线中等选Unity是我试过一圈之后做出的决定。Unity 2022 LTS配合AR Foundation底层同时封装了ARCore和ARKit写一套C#就能同时出Android和iOS的AR包。VR那边用XR Interaction Toolkit接OpenXRQuest 2可以直接跑。天文科普这种场景本身需要大量粒子系统、点精灵、Bloom后处理Unity的URP渲染管线和Shader开发效率比Flutter高太多。至于UI部分Unity的UGUI加上TextMeshPro做信息卡片、搜索列表完全够用没必要为了一个设置页单独套一层Flutter壳。注意如果你的核心页面有大量的列表、VStack、复杂动效交互Flutter的优势才会体现出来。天文科普的主界面是3D星空一打开满屏星星这类场景应该把3D渲染能力作为选型第一优先级。1.2 一套数据核心多端渲染前端整个项目我拆成了三层算法层、数据层、渲染层。算法层是完全不依赖Unity的纯C#类库天文坐标计算全部放在这里数据层负责把星表文件从CSV转成紧凑的二进制格式运行时加载进内存渲染层才去碰Unity的GameObject、粒子系统、XR接口。这三层严格分层的好处是我可以在PC上用命令行直接验证“某颗星此刻的方位角到底是不是正确的”不打开Unity就能跑算法测试。工程目录大致长这样Assets/ Scripts/ Astronomy/ JulianDay.cs SolarPosition.cs SkyCoordinates.cs Precession.cs StarCatalog.cs Rendering/ StarFieldRenderer.cs ConstellationRenderer.cs PlanetMarker.cs StarShader.shader UI/ InfoCard.cs SearchPanel.cs TrackingStateHUD.cs Data/ hyg_full.bin constellations.json planets.json Scenes/ ARScene.unity VRScene.unity Sky2D.unity数据流是单向的StarCatalog从hyg_full.bin读出Hipparcos星表算法层根据当前时间和观察者经纬度算坐标渲染层拿到坐标后决定画在哪、画多大。单向的好处是排查问题非常快某一层出错了只需要检查该层的输入输出不用满工程翻依赖关系。1.3 平台差异处理目标设备清单跨平台不是一句口号是实打实要跟各家SDK打交道。AR模式下Android依赖ARCoreiOS依赖ARKitVR模式走OpenXR。这三个API在Unity里虽然被AR Foundation和XR Toolkit统一了接口但底层的权限、生命周期、相机行为还是有差别。比如ARCore对设备列表有严格限制部分国产机型必须先装ARCore APK才能跑ARKit对室内低光环境的追踪能力比ARCore强很多Quest 2则是OpenXR runtime驱动方式跟前两者完全不是一回事。我这边最终的目标设备定为了Android中端机骁龙778G以上、iPhone 12系列以上、Quest 2。这个清单直接影响后面的性能预算和材质规格每台设备的粒子上限、纹理压缩格式、渲染分辨率都是单独配置的。强烈建议拿到设备清单之后再开始写性能相关代码否则后期适配会非常被动。2. 天文算法从时间到坐标的解算链路2.1 儒略日与恒星时先把时间对齐任何天体的位置计算起点都是时间。公历上的“2025年1月1日 20:00”在日历上很好认但天文学计算需要的是连续流逝的日数于是要先转成儒略日Julian Day。我用的Meeus风格公式C#实现很简洁public static double ToJulianDay(DateTime dt) { int y dt.Year; int m dt.Month; double d dt.Day dt.Hour / 24.0 dt.Minute / 1440.0 dt.Second / 86400.0; if (m 2) { y - 1; m 12; } double a Math.Floor(y / 100.0); double b 2 - a Math.Floor(a / 4.0); return Math.Floor(365.25 * (y 4716)) Math.Floor(30.6001 * (m 1)) d b - 1524.5; }接着要算格林尼治平恒星时GMST。恒星时的意义是告诉你“此刻天上的坐标原点在哪”。民用时间是跟着太阳走的恒星时间跟着遥远恒星走两者每天差约4分钟累积起来就是地球自转轴的相对位置在星空背景里移动了。算得公式public static double GMST(double jd) { double t (jd - 2451545.0) / 36525.0; double gmst 280.46061837 360.98564736629 * (jd - 2451545.0) 0.000387933 * t * t - t * t * t / 38710000.0; gmst gmst % 360.0; if (gmst 0) gmst 360.0; return gmst; }单位是度换算成小时要除以15。地方恒星时就是GMST加上观察者的东经度数东经为正再模360。这里有个小坑GPS给的是地理经度东经为正但有些平台输出的西经是负数统一处理好再进公式否则满屏星星都会偏。2.2 太阳与行星位置的实用算法太阳位置是很多计算的地基比如白天判断哪些星星不可见、或者粗略估算行星大致的黄道位置都需要它。严格的做法是套VSOP87完整周期项但科普应用根本不需要角秒级精度我把太阳位置简化为“平黄经 中心差”public static (double lon, double lat) SunEcliptic(double jd) { double t (jd - 2451545.0) / 36525.0; double l0 280.46646 36000.76983 * t; // 太阳平黄经 double m 357.52911 35999.05029 * t; // 太阳平近点角 double c (1.914602 - 0.004817 * t) * Math.Sin(m * Math.PI / 180.0) (0.019993 - 0.000101 * t) * Math.Sin(2 * m * Math.PI / 180.0) 0.000289 * Math.Sin(3 * m * Math.PI / 180.0); double trueLon l0 c; return (trueLon, 0.0); }太阳的黄纬基本为0所以重点是黄经。行星位置我用的是简化周期项表每颗行星存一组“平均要素”精度大约在0.1度以内。对于AR/VR星空科普这个精度够了——毕竟你举起手机时手抖的角度都不止0.1度。2.3 赤道坐标到地平坐标的转换星表和行星数据算出来的都是赤道坐标赤经RA、赤纬DEC但用户看到的是地平坐标高度角alt、方位角az。这时候就要先根据前面算的地方恒星时求时角Hdouble H localSiderealTimeDeg - raDeg;然后转地平坐标最稳的写法是public static (double alt, double az) EquatorialToHorizontal( double raDeg, double decDeg, double latDeg, double lstDeg) { double ra raDeg * Mathf.Deg2Rad; double dec decDeg * Mathf.Deg2Rad; double lat latDeg * Mathf.Deg2Rad; double H (lstDeg - raDeg) * Mathf.Deg2Rad; double sinAlt Math.Sin(dec) * Math.Sin(lat) Math.Cos(dec) * Math.Cos(lat) * Math.Cos(H); double alt Math.Asin(sinAlt); double cosAz (Math.Sin(dec) - Math.Sin(lat) * sinAlt) / (Math.Cos(lat) * Math.Cos(alt)); double az Math.Acos(Math.Clamp(cosAz, -1.0, 1.0)); if (Math.Sin(H) 0) az 360.0 - az; return (alt * Mathf.Rad2Deg, az * Mathf.Rad2Deg); }方位角的定义是从正北起算顺时针为正。上面这个if是为了处理H跨过子午线时的象限翻转实测下来比裸用Atan2少很多冤枉路。用Math.Clamp夹一下cosAz是为了防止浮点数误差导致acos域越界这个小动作能避免屏幕上出现星点突然从地平线下方“弹”上来的现象。2.4 岁差、自行与光行差的取舍星表里的坐标通常标着J2000历元也就是2000年1月1日的状态。但地球自转轴在空间里是做缓慢进动的二十多年过去赤道坐标已经偏移了不少。最简单的处理是给每颗星加上“岁差修正项”把J2000坐标拉到当前年份我用的是经典近似公式double t (currentYear - 2000.0) / 100.0; double deltaRA (3.074 1.336 * Math.Sin(raDeg * Deg2Rad) * Math.Tan(decDeg * Deg2Rad)) * t * 100.0 / 3600.0; double deltaDec (20.043 * Math.Cos(raDeg * Deg2Rad)) * t * 100.0 / 3600.0;这里deltaRA和deltaDec是从J2000岁差到当前历元的角秒变化算完加到原始坐标上即可。如果要求更高精度用IAU 1976岁差矩阵但科普应用这个简化版足够。自行则是对少数距离近、自行大的恒星才做比如巴纳德星一年能跑10角秒不修正的话它在星空里的位置会跟星表对不上。实操提示星座连线画完后一定要做一次真机对比测试。找一个视野开阔的夜晚打开你的AR模式把手机对准猎户座如果腰带三星连线和真实天空一致就说明整条坐标链路没问题如果整条线偏移八成是岁差或者恒星时代入错了。3. 从星表到AR/VR场景的渲染实现3.1 把地平坐标变成Unity世界坐标算法层算出的alt/az是个“东—北—上”方向得映射到Unity的左手法则坐标系。我定义的地平坐标世界映射是Y轴为正北方向Z轴向上X轴为正东方向。于是public static Vector3 AltAzToWorld(float alt, float az, float radius) { float altR alt * Mathf.Deg2Rad; float azR az * Mathf.Deg2Rad; return new Vector3( radius * Mathf.Cos(altR) * Mathf.Sin(azR), radius * Mathf.Cos(altR) * Mathf.Cos(azR), radius * Mathf.Sin(altR) ); }当初在这里踩过一个坑如果按传统的“Z轴北、Y轴上”习惯去写Unity里没法直接用必须先做轴交换。后来我统一约定好“Y北、Z上、X东”并把坐标转换函数放在算法层的最外层渲染层只认这一个函数从此再没出过方向错乱的问题。3.2 AR模式让星星跟着手机的位置和朝向走AR模式的思路是把整个“星空天球”看作一个以相机为圆心、半径1000米的球壳所有星点都固定在这个球壳的内壁。天球本身不参与AR空间定位它只是跟随相机的位置同步同时根据当前时间和经纬度旋转到正确的姿态。这样手机就是一块“活动星盘”举起来对准天空星星就会出现在你眼前的位置。AR Foundation的项目设置里有几个关键点ARCameraManager负责把手机摄像头画面渲染为背景天上3D星点会叠加在真实画面上相机远裁剪面要拉到5000米以上否则天球表面星星会被透视裁剪掉星点不参与光照直接走URP的无光照Shader每帧根据GPS更新经纬度用户移动几十米的量级对恒星视位置影响很小但纬度变了会明显改变地平坐标定位失败的情况下我会自动切换成“模拟模式”默认把观察者放在北纬40度、东经116度至少保证App能看、能演示不至于在室内直接黑屏。3.3 VR模式在天球内部实现星空漫游VR模式就简单暴力了把摄像机放在天球的球心天球半径20米所有星星布置在球面内壁上。用户转动头部就能看遍全天手柄射线指到任意一颗星弹出信息卡片。信息卡片不能放在星星所在位置太远看不清我的做法是射线命中后把卡片吸附在射线方向5米处并让卡片永远面朝相机。星座的连线在VR里特别重要。每个星座是一组预定义好的星点索引运行时根据这两颗星当前的赤道坐标重新计算世界坐标生成LineRenderer的Positions数组。线的宽度可以压得很细配一个半透明材质看起来像是悬浮在星空里的微弱引导线。VR环境下玩家会反复抬头低头LineRenderer需要选择useWorldSpace true否则会跟错父级坐标系。3.4 星点Shader与粒子系统的取舍一开始我发现每颗星都放一个Sprite或Quad一万颗星就是一万个GameObject主线程直接顶不住。后来改成了两层渲染亮星视星等小于2.5数量少单独用Quad渲染走Instancing合批暗星全部塞进一个大粒子系统粒子位置由脚本每帧从坐标计算结果写入粒子数组。粒子的渲染用了一个极简的圆形光斑Shader核心就是“距离越远视觉尺寸越小越接近中心越亮”v2f vert (appdata v) { v2f o; o.vertex UnityObjectToClipPos(v.vertex); float dist distance(_WorldSpaceCameraPos, mul(unity_ObjectToWorld, v.vertex).xyz); o.size _StarSize / dist; return o; } fixed4 frag (v2f i) : SV_Target { float2 uv i.uv - 0.5; float r dot(uv, uv) * 4.0; // 圆形羽化 float alpha smoothstep(1.0, 0.2, r) * _Brightness; return fixed4(1.0, 1.0, 1.0, alpha); }这种圆形光斑比硬邦邦的方形贴图更接近真实星点而且配合URP Volume里的Bloom后处理亮星会有淡淡的辉光暗星则保持自然。4. 性能调优与多设备适配4.1 先定预算再写代码移动端性能优化第一原则是先定预算再写代码。我手里的设备性能差异很大Quest 2和几年前的安卓中端机完全不是一个量级。按设备定了一个明确的性能预算表设备目标帧率粒子预算DrawCall预算内存预算Android中端60fps2万150256MBiPhone 12 Pro60fps3万150512MBQuest 272fps3万120512MB这里的粒子预算不是总粒子数而是“屏幕上同时可见”的粒子数。因为粒子系统被大天球包围玩家永远只能看到一半左右的星星所以即使星表有12万颗我也只会在可见半球范围内生成粒子剩余部分不渲染。4.2 12万颗星的渲染优化方案星表全量灌进去显然不现实按视星等做了三级裁减第一梯队是亮星级视星等小于2.5全天大概24颗包含天狼星、参宿四、织女星这些耳熟能详的目标。这24颗用独立ShellRenderer对象渲染材质带Bloom辉光支持点击高亮。第二梯队是中等亮星2.5到4.5等约400颗放进中等粒子系统。第三梯队是暗星4.5到8等数量庞大放进一个超大粒子系统粒子尺寸更小、透明度更低负责提供银河背景的“颗粒感”。星座连线、行星标记、信息卡片各自独立成DrawCall。最终一帧的DrawCall总数稳定在40-70之间Quest 2跑起来很轻松。对比最初版本“全员GameObject”的做法一帧12000个DrawCall移动端直接卡到不可用差距非常直观。4.3 从卡顿到流畅的实测记录开发过程中最有代表性的一次调优是银河背景那部分。第一次把银河粒子数量拉到10万Quest 2的帧率直接掉到35fps发热严重。排查发现罪魁祸首不只是粒子数量还有每个粒子的贴图采样——粒子的UV动画写得太复杂导致GPU在Fragment阶段出现瓶颈。后来我把银河粒子的贴图改成单张噪声纹理去掉逐粒子的色差动画只保留透明度随距离衰减效果天差地别。同一台设备帧率从35fps拉回到72fps发热也降了一个级别。所以说移动端Shader越简单越好科普应用里星星不需要像游戏技能特效那样华丽准确、干净才是核心。4.4 一个容易被忽略的适配点屏幕亮度和发热AR模式下手机摄像头常亮相机渲染GPSAR追踪叠加在一起发热降频是常态。头戴显示器的VR模式对发热更敏感。我做了两件事帧率动态跟随设备温度温度超过警戒线自动把粒子数量砍半画面亮度用HDR调光而不是纯白色叠加避免屏幕全亮导致功耗飙升。这些小细节最终多设备跑下来非常关键。5. 常见问题与排查技巧实录5.1 星座连线为何在真机上整体偏移这个问题是AR模式第一轮内测时用户反馈最多的手机明明对准了猎户座星座连线的方向却整体偏了几度。一开始怀疑算法写错后来发现是星表坐标没有加岁差修正。我的星表数据是J2000历元的Hyg数据库但运行App时已经过了二十多年不把岁差转过来所有星星在物理上就都“丢”了一段距离。加上岁差修正后连线立即对齐。这个坑对一个天文App来说近乎致命——用户会很直观地发现“对不上”。真机验证必须放在开发流程前面最好是每天晚上晴朗都出去校准一次。5.2 暗星闪烁与点精灵最小尺寸暗星在粒子系统渲染时会出现一个很烦的毛病星星忽明忽暗甚至整排整排地闪。究其原因是暗星在屏幕上投影尺寸小于1像素深度缓冲区里发生抖动。解决办法是给Shader加一个最小像素尺寸保护float projScale o.size * _ScreenParams.y; float safeSize max(projScale, _MinPixelSize); o.vertex.xy normalize(o.vertex.xy) * safeSize * 0.001;加上之后暗星至少占据一个像素闪烁立即消失视觉上虽然亮度略高于真实但科普场景下“稳定可见”比“精确暗淡”更重要。5.3 AR模式下星星缓慢漂移模拟器里一切正常真机上AR星星会随着相机转动慢半拍似地“漂”。这个原因是ARFoundation的相机姿态更新频率和天文计算线程不一致我在协程里用Update更新了恒星时但没跟XRCameraSubsystem的帧回调同步。改成在ARSession的OnFrameReceived事件里触发坐标更新后漂移问题解决。关键教训AR应用里所有和相机姿态同步的更新都必须放在AR Frame事件回调里而不是普通的Unity Update循环。5.4 问题速查表现象可能原因解决方案星座连线整体偏移星表坐标未加岁差在算法层补J2000→当前历元变换暗星闪烁点精灵小于1像素Shader加最小投影尺寸保护AR星星慢半拍漂移坐标更新没跟在相机帧回调里移入OnFrameReceived中同步安卓端初始化失败ARCore未安装或设备不支持打包前加ARCore安装检测和提示页VR开不了星空场景OpenXR Runtime未激活检查Player Settings中的XR Plug-in Management帧率掉到40以下粒子贴图采样过重换单张噪声纹理去掉粒子UV动画5.5 排查经验升华DEBUG要能“跑数据”开发这个项目最大的习惯改变是给所有坐标算法加了一个“Debug Plot”模式——在Unity里只显示星座的参考点并用Debug.DrawLine把每颗星的赤道坐标、地平坐标、世界坐标三个阶段画出来。坐标链路一旦出问题看哪一环节的线条断了就知道谁背锅。这个方法救了我很多次尤其适合这种“算法渲染”混合项目。结尾最后再分享一个心得体会。整个项目做下来我最大的感悟是算法层和渲染层一定要分开来验证千万别等所有模块写完才做联调。所有恒星和行星的坐标我是先在纯C#的测试控制台里跑了一遍确认某颗星在某时刻的方位高度符合Stellarium的预期才把它们接进Unity。如果一上来就直接在URP里调Shader出了问题根本分不清是坐标算错还是材质写错排查成本会翻几倍。这个项目的下一阶段我打算把流星群预报和近地卫星过境加进去计算框架还是这一套跨平台架构只是在算法层多加两个预测器渲染层再加一个轨迹粒子系统。如果你也在做类似的天文科普或XR互动项目希望这篇能帮你少走几个月的弯路。我自己在第一次用真机把猎户座腰带三星准确对齐在手机屏幕上的那一刻忽然觉得这一年多的坑都值了。

看完文章,想为自己的企业也做一次专业网站诊断?

尧图顾问免费为您评估现有网站,并给出建站/改版建议与报价方案。

免费获取方案