【共创稿事节】HarmonyOS 7之3DGS 重建在折叠屏/平板的适配差异
3DGS 重建在折叠屏/平板的适配差异
折叠屏展开/合上时,3DGS 渲染区域和视锥跟着变,重建参数也要跟着调。我们在折叠屏上跑 3DGS 场景,展开瞬间帧率掉到 0,持续约 300ms,用户看到一帧黑屏。平板上更大的视场角让同屏高斯点数翻倍,帧率从 58fps 掉到 28fps。这两个问题不是 bug,是折叠屏和平板的物理形态对 3DGS 渲染管线的真实影响,需要应用层主动适配。
能力面:铰链状态监听与视锥动态调整
HarmonyOS 提供折叠屏铰链状态监听 API,可以拿到折叠角度和屏幕展开状态。3DGS 渲染的视锥由相机参数决定,铰链变化时渲染区域尺寸变化,相机宽高比和 FOV 要跟着调。
// ArkTS:折叠屏铰链状态监听
import { display } from '@kit.ArkUI';
class FoldAdaptManager {
private currentFoldStatus: 'folded' | 'half' | 'unfolded' = 'unfolded';
private currentDisplayWidth: number = 0;
private currentDisplayHeight: number = 0;
init(): void {
// 监听折叠状态变化
display.on('foldStatusChange', (foldStatus: display.FoldStatus) => {
this.handleFoldChange(foldStatus);
});
// 监听屏幕尺寸变化(展开/合上时触发)
display.on('displaySizeChange', (size: display.Size) => {
this.handleSizeChange(size.width, size.height);
});
}
private handleFoldChange(foldStatus: display.FoldStatus): void {
switch (foldStatus) {
case display.FoldStatus.FOLDED:
this.currentFoldStatus = 'folded';
this.adjustForFolded();
break;
case display.FoldStatus.HALF:
this.currentFoldStatus = 'half';
this.adjustForHalf();
break;
case display.FoldStatus.UNFOLDED:
this.currentFoldStatus = 'unfolded';
this.adjustForUnfolded();
break;
}
}
private adjustForUnfolded(): void {
// 展开态:大屏幕,大视场角
// 但同屏高斯点数增加,要降渲染距离或 LOD
this.updateCameraFOV(70); // 展开态 FOV 70 度
this.updateRenderDistance(8.0); // 缩短渲染距离,减少同屏点数
this.updateLODBias(1.2); // LOD 偏高精度档,补偿大屏幕的清晰度需求
}
private adjustForFolded(): void {
// 折叠态:小屏幕,正常视场角
this.updateCameraFOV(60);
this.updateRenderDistance(12.0);
this.updateLODBias(1.0);
}
private adjustForHalf(): void {
// 半折叠态:中间值
this.updateCameraFOV(65);
this.updateRenderDistance(10.0);
this.updateLODBias(1.1);
}
}
三个状态对应三组参数。参数不是拍脑袋定的,是按各形态下的渲染区域尺寸和同屏点数实测调出来的。下面给数据。
形态切换时参数更新的完整链路如下,注意我们是用尺寸回调而不是铰链回调来判形态:
先靠尺寸变化抢在铰链回调之前更新参数,再调 LOD 和布局,最后才恢复渲染。
约束面:折叠切换中断与平板 fillrate 压力
折叠切换瞬间的渲染中断
折叠屏展开或合上时,屏幕尺寸变化触发 Surface 重建。Surface 重建期间渲染管线暂停,3DGS 渲染中断。我们测了展开瞬间的帧率变化:
| 时间点 | 帧率 | 备注 |
|---|---|---|
| 展开前 100ms | 58fps | 正常 |
| 展开触发 | 0fps | Surface 重建 |
| 展开后 100ms | 0fps | 仍在重建 |
| 展开后 200ms | 0fps | 仍在重建 |
| 展开后 300ms | 35fps | 恢复渲染,但参数未更新 |
| 展开后 400ms | 52fps | 参数更新完成 |
| 展开后 500ms | 56fps | 稳定 |
Surface 重建约 300ms,期间帧率掉到 0。300ms 用户感知是一帧黑屏或卡顿,不算长但能注意到。恢复渲染后帧率 35fps,因为相机参数还没更新(用的还是折叠态的小 FOV),渲染区域变大但视锥没跟上,看起来场景被拉伸。参数更新后帧率回到 52fps,最终稳定 56fps。
这个 300ms 的中断无法消除,是系统层 Surface 重建的固有耗时。应用层能做的是在中断期间显示一个占位帧(最后一帧的截图),避免黑屏。
平板大视场下的 fillrate 压力
平板屏幕大,同样 FOV 下渲染区域比手机大。更大的渲染区域意味着更多高斯点投影到屏幕上,fillrate 压力增加。
| 设备 | 屏幕尺寸 | 渲染分辨率 | FOV | 同屏高斯点数 | 帧率 |
|---|---|---|---|---|---|
| 手机折叠态 | 6.4" | 1080×2520 | 60° | 12 万 | 58fps |
| 手机展开态 | 7.9" | 2208×2244 | 70° | 28 万 | 38fps |
| 平板 | 12.6" | 2560×1600 | 75° | 41 万 | 28fps |
同一个 3DGS 场景(总高斯点 35 万),手机折叠态同屏 12 万点,帧率 58fps。平板同屏 41 万点,帧率掉到 28fps。
同屏高斯点数是帧率主要瓶颈,这是 3DGS 渲染的硬约束。平板大视场把更多点拉进视锥,帧率必然下降。要维持帧率,必须减少同屏点数——要么缩短渲染距离(远处的点不渲染),要么用更激进的 LOD(远处用低精度点)。
场景落地:三形态渲染对比
我们用同一个 3DGS 场景(一只陶瓷摆件,35 万高斯点)在三种形态下渲染,记录参数和效果差异。
手机折叠态(6.4")
折叠态屏幕小,FOV 60 度,同屏点数 12 万。渲染距离设 12 米,场景全部可见。帧率 58fps,内存 363MB,体验流畅。
折叠态的问题是沉浸感弱——屏幕小,3DGS 场景看起来像缩略图。用户反馈"看不清细节"。
手机展开态(7.9")
展开态屏幕变大,FOV 调到 70 度。如果不调渲染距离,同屏点数 28 万,帧率 38fps,能感知到卡顿。
我们把渲染距离从 12 米缩到 8 米,远处的高斯点不渲染,同屏点数降到 18 万,帧率回到 48fps。代价是场景边缘被裁掉——摆件底座远端看不到。对这个场景可以接受,因为用户关注的是摆件主体。但大场景(展厅、房间)不能这么裁,会裁掉重要区域。
展开态的另一个调整是 LOD bias。大屏幕对清晰度要求更高,同样的 LOD 在大屏幕上显得模糊。把 LOD bias 从 1.0 调到 1.2,强制用更高精度档,但内存增加约 15%。
平板(12.6")
平板是最难适配的形态。屏幕大、视场大、同屏点数多。
不调参数直接渲染,同屏 41 万点,帧率 28fps,明显卡。调渲染距离到 6 米,同屏点数降到 25 万,帧率 38fps,还是不够流畅。再上 LOD 分级,远处用 1/4 精度的点,同屏点数降到 16 万,帧率 50fps。
但 LOD 降级后远处明显模糊。平板大屏幕上模糊更显眼,用户能看出来远处点云变糙了。这是个两难——不降 LOD 帧率不够,降了 LOD 视觉质量掉。
我们的折中是动态 LOD:用户交互时(拖动、缩放)用低 LOD 保帧率,静止观看时切高 LOD 保质量。切换有 200ms 的过渡,用户不太能察觉。
// ArkTS:动态 LOD 切换
class DynamicLOD {
private isInteracting: boolean = false;
private idleTimer: number = -1;
onInteractionStart(): void {
this.isInteracting = true;
clearTimeout(this.idleTimer);
// 交互中用低 LOD,保帧率
this.updateLODBias(1.5); // 偏低精度
}
onInteractionEnd(): void {
this.isInteracting = false;
// 停止交互 200ms 后切高 LOD
this.idleTimer = setTimeout(() => {
this.updateLODBias(1.0); // 偏高精度
}, 200);
}
}
真机数据汇总
| 形态 | FOV | 渲染距离 | LOD bias | 同屏点数 | 帧率 | 内存 | 视觉评分 |
|---|---|---|---|---|---|---|---|
| 手机折叠态 | 60° | 12m | 1.0 | 12万 | 58fps | 363MB | 6.5/10 |
| 手机展开态(不调参) | 70° | 12m | 1.0 | 28万 | 38fps | 363MB | 7.5/10 |
| 手机展开态(调参) | 70° | 8m | 1.2 | 18万 | 48fps | 418MB | 7.2/10 |
| 平板(不调参) | 75° | 12m | 1.0 | 41万 | 28fps | 363MB | 8.0/10 |
| 平板(调参+动态LOD) | 75° | 6m | 1.0-1.5 | 16万 | 50fps | 410MB | 7.0/10 |
视觉评分由 5 人独立打分取平均。不调参的视觉评分最高(因为不裁不降级),但帧率不可用。调参后帧率可用但视觉评分降——展开态裁掉了远处区域,平板降了 LOD。
没有两全的方案,大屏幕上要么牺牲帧率要么牺牲视觉。我们选保帧率,因为卡顿比模糊更影响体验。
踩坑与取舍
坑 1:折叠切换瞬间帧率掉到 0
前面说的 300ms Surface 重建中断。第一版我们没有处理,用户展开折叠屏看到一帧黑屏,以为应用崩了。
后来加了占位帧:折叠切换前截取当前渲染帧,切换期间显示这张截图,切换完成后恢复渲染。用户看到的是"画面静止 300ms"而不是黑屏,体验好很多。
// ArkTS:折叠切换占位帧
class FoldTransitionHandler {
private placeholderImage: image.PixelMap | null = null;
onFoldStart(): void {
// 切换前截屏
this.placeholderImage = captureCurrentFrame();
// 显示占位图,隐藏渲染组件
AppStorage.setOrCreate('showPlaceholder', true);
AppStorage.setOrCreate('placeholderImage', this.placeholderImage);
}
onFoldEnd(): void {
// 切换完成,恢复渲染
setTimeout(() => {
AppStorage.setOrCreate('showPlaceholder', false);
this.placeholderImage = null;
}, 350); // 等 Surface 重建完
}
}
// UI 层
@Component
struct GsRenderer {
@StorageLink('showPlaceholder') showPlaceholder: boolean = false;
@StorageLink('placeholderImage') placeholder: image.PixelMap | null = null;
build() {
Stack() {
if (this.showPlaceholder && this.placeholder) {
Image(this.placeholder).width('100%').height('100%')
}
Component3D({ scene: this.scene }).width('100%').height('100%')
}
}
}
占位图的尺寸跟展开后的屏幕尺寸不匹配——折叠态截的图是 1080×2520,展开后屏幕 2208×2244。直接拉伸显示会变形。我们做的是居中裁剪显示,虽然不完美但比黑屏好。
坑 2:展开后参数没跟上,场景被拉伸
Surface 重建完成后,渲染恢复了但相机参数还是折叠态的。FOV 60 度在小屏幕上正常,到大屏幕上视锥太窄,场景看起来被放大了。
原因是折叠状态回调比 Surface 重建完成晚 50-100ms。参数更新有个延迟,期间用旧参数渲染了几帧。
解决方法是在 displaySizeChange 回调里立刻更新参数,不等 foldStatusChange。尺寸变化本身就够判断该用什么参数了。
private handleSizeChange(width: number, height: number): void {
// 根据尺寸直接判断形态,不等 foldStatusChange
if (width > 2000) {
this.adjustForUnfolded();
} else if (width > 1400) {
this.adjustForHalf();
} else {
this.adjustForFolded();
}
}
坑 3:平板大视场同屏点数翻倍卡顿
平板 FOV 75 度,比手机折叠态 60 度大很多。FOV 大意味着视锥张角大,更多高斯点落在视锥内。35 万总点数中 41 万同屏——等等,41 万比总点数 35 万还多?
没算错。同屏点数统计的是投影到屏幕上的高斯点数,一个高斯点可以投影到多个像素但只算一次。35 万总点数中,平板大 FOV 下几乎全部落在视锥内,加上 LOD 切换时高低精度档的点会短暂同时存在,峰值同屏 41 万。
这个峰值卡顿是 LOD 切换的代价。切 LOD 时旧精度档的点还没卸载,新精度档的点已经加载,短暂叠加。我们改成先卸载旧档再加载新档,峰值降到 35 万,但切换时有 100ms 的空档,那段时间远处没点云,看到背景色。两个方案都有缺陷,最后选了先卸后载,因为 100ms 空档比卡顿好接受。
坑 4:半折叠态的参数没人调
半折叠态(悬停态)是折叠屏特有的形态,屏幕一部分弯折。这个态的渲染区域不是矩形,是不规则形状,FOV 和渲染距离该调多少没有标准答案。
我们测了半折叠态的同屏点数,介于折叠态和展开态之间。参数也取中间值(FOV 65、距离 10、LOD 1.1),效果还行。但半折叠态下屏幕弯折处的渲染有畸变——弯折面两侧的渲染内容接不上,因为 3DGS 渲染假设的是平面屏幕。
这个畸变在当前版本没法修,3DGS 渲染管线不支持非平面屏幕。我们的处理是半折叠态下显示提示"建议完全展开以获得最佳体验",引导用户展开。
取舍:各形态的适配策略
| 形态 | FOV | 渲染距离 | LOD 策略 | 同屏点数上限 | 备注 |
|---|---|---|---|---|---|
| 手机折叠态 | 60° | 12m | 静态 1.0 | 15万 | 默认体验 |
| 手机半折叠 | 65° | 10m | 静态 1.1 | 20万 | 提示展开 |
| 手机展开态 | 70° | 8m | 静态 1.2 | 20万 | 裁远处 |
| 平板 | 75° | 6m | 动态 1.0-1.5 | 18万 | 动态 LOD |
同屏点数上限是我们设的帧率红线——超过 20 万点帧率会掉到 45fps 以下,体感卡顿。各形态的参数都调到同屏点数不超过这个红线。
重建参数的形态适配
前面说的是渲染适配。重建参数也要跟形态调——不同形态的采集预览不同,重建时输入素材的分辨率和帧数可以按形态优化。
折叠态采集时,预览画面小,可以用低分辨率预览(720p)省算力。展开态预览画面大,用高分辨率(1080p)保清晰度。但重建用的原始素材始终是最高分辨率,预览分辨率不影响重建质量。
// ArkTS:按形态调采集参数
class CaptureAdaptManager {
getCaptureConfig(foldStatus: string): CaptureConfig {
switch (foldStatus) {
case 'folded':
return { previewResolution: 720, captureResolution: 1080, frameRate: 30 };
case 'half':
return { previewResolution: 1080, captureResolution: 1080, frameRate: 30 };
case 'unfolded':
return { previewResolution: 1080, captureResolution: 1080, frameRate: 60 };
default:
return { previewResolution: 1080, captureResolution: 1080, frameRate: 30 };
}
}
}
展开态帧率提到 60fps,因为大屏幕上 30fps 的预览能看出卡。采集帧率不影响重建(重建用的是关键帧),但影响拍摄时的预览体验。
小小注意的点
- 折叠切换时 Surface 重建约 300ms,期间帧率掉到 0,要显示占位帧
- 折叠状态回调比 Surface 重建晚 50-100ms,参数更新要靠尺寸变化回调
- 平板大 FOV 下同屏点数翻倍,必须缩短渲染距离或降 LOD
- 同屏高斯点数超过 20 万帧率掉到 45fps 以下,设为红线
- 平板用动态 LOD:交互时低精度保帧率,静止时高精度保质量
- LOD 切换先卸旧档再载新档,避免点数峰值叠加卡顿
- 半折叠态渲染有畸变,3DGS 不支持非平面屏幕,引导用户完全展开
- 采集预览分辨率按形态调,原始素材始终最高分辨率
更多推荐


所有评论(0)