HarmonyOS 7 / API 26 3DGS 模型首屏黑屏排查:相机、灯光和资源包围盒实战

HarmonyOS 7 3DGS 首屏黑屏排查

这个问题比“模型加载失败”更隐蔽

3DGS 或 3D 模型接入时,经常会遇到一种很烦的问题:接口没有报错,模型文件也确实加载了,但页面第一屏就是黑的,或者只看到一小块漂在角落里。开发者第一反应容易去怀疑模型格式,结果查半天发现文件没坏,问题出在相机、灯光、模型包围盒和首帧状态上。

HarmonyOS 7 / API 26 的 3DGS 端侧重建方向,不能只看“模型能不能被加载”。真正在应用里交付时,要看用户第一次进入页面能不能稳定看到主体。首屏看不到主体,后面旋转、缩放、滤镜做得再多也没意义。

我会把这个问题分成四类:

现象 常见原因 先查什么
页面全黑 没有有效灯光、背景和模型颜色接近、材质参数异常 默认灯光、背景色、材质
模型太小 相机距离太远,模型包围盒没算 包围盒、相机距离
只看到一角 相机朝向不对,模型中心点偏移 模型中心点、lookAt 目标
首次正常,返回异常 场景释放和重建不完整 页面生命周期、scene dispose

这篇不讨论端侧重建算法本身,只讨论模型已经存在以后,如何让 ArkGraphics 3D 侧的首屏预览稳定下来。

官方能力边界先定住

Spatial Recon Kit 负责 3DGS 相关的重建和资源能力,ArkGraphics 3D 负责把资源放进场景里,提供相机、灯光、节点、材质、动画等能力。也就是说,首屏黑屏这类问题,大多数不应该回头去重跑重建,而应该先检查 3D 场景侧。

一个比较稳的判断顺序是:

  1. 文件是否存在,大小是否异常;
  2. 场景是否初始化成功;
  3. 模型是否有包围盒数据;
  4. 相机是否对准模型中心;
  5. 灯光是否能照亮主体;
  6. 首帧是否有加载完成和失败兜底。
  7. 案例一:模型加载成功,但相机没对准

    第一个案例很常见:模型资源加载成功,场景也没有报错,但用户看到的是空页面。这种情况下,先不要急着换模型,先把模型的中心点和包围盒打印出来。

    复现步骤

    1. 加载一个模型资源;
    2. 不设置默认相机,只使用引擎默认视角;
    3. 页面打开后观察首屏;
    4. 打印模型中心点、宽高深和相机位置;
    5. 根据包围盒重置相机,再观察首屏是否恢复。
    6. interface Vec3 {
        x: number;
        y: number;
        z: number;
      }
      
      interface ModelBounds {
        center: Vec3;
        size: Vec3;
        radius: number;
      }
      
      interface CameraPose {
        eye: Vec3;
        target: Vec3;
        up: Vec3;
      }
      
      export class CameraPresetBuilder {
        build(bounds: ModelBounds): CameraPose {
          const safeRadius = Math.max(bounds.radius, 1);
          const distance = safeRadius * 2.8;
      
          return {
            eye: {
              x: bounds.center.x,
              y: bounds.center.y + safeRadius * 0.45,
              z: bounds.center.z + distance
            },
            target: bounds.center,
            up: { x: 0, y: 1, z: 0 }
          };
        }
      }

      这段代码的核心是用模型包围盒反推相机位置。很多黑屏问题不是模型没有加载,而是相机离得太远、太近,或者根本没有看向模型中心。

      页面里要保留首帧状态

      type FirstFramePhase = 'idle' | 'loading' | 'visible' | 'empty' | 'failed';
      
      interface FirstFrameState {
        phase: FirstFramePhase;
        message: string;
        bounds?: ModelBounds;
      }
      
      export class FirstFrameProbe {
        private state: FirstFrameState = { phase: 'idle', message: '' };
      
        start(): void {
          this.state = { phase: 'loading', message: '正在准备 3D 首帧' };
        }
      
        visible(bounds: ModelBounds): void {
          this.state = { phase: 'visible', message: '模型首帧已显示', bounds };
        }
      
        empty(reason: string): void {
          this.state = { phase: 'empty', message: reason };
        }
      
        failed(error: Error): void {
          this.state = { phase: 'failed', message: error.message };
        }
      
        snapshot(): FirstFrameState {
          return { ...this.state };
        }
      }

      这里不要只写一个 loading。首帧问题需要分清“加载中”“已显示”“空画面”“失败”。这四种状态给用户看到的 UI 不一样,给开发者看的日志也不一样。

      案例二:模型在,但灯光和背景让它看起来像没显示

      第二个案例也很常见:模型确实在场景里,但颜色很暗,背景也暗,最后用户看到的是一片黑。这个时候继续改资源路径没有用,要处理默认灯光和背景。

      复现步骤

      1. 使用深色背景;
      2. 加载一个暗色模型;
      3. 不配置环境光和主光源;
      4. 打开页面观察首屏;
      5. 加入默认环境光、主光源和轮廓光;
      6. 再观察模型边缘和主体是否可见。
      7. type LightRole = 'ambient' | 'key' | 'rim';
        
        interface LightPreset {
          role: LightRole;
          intensity: number;
          color: string;
          direction?: Vec3;
        }
        
        export class SceneLightPresetFactory {
          buildDefault(): LightPreset[] {
            return [
              { role: 'ambient', intensity: 0.35, color: '#FFFFFF' },
              { role: 'key', intensity: 0.9, color: '#FFF7ED', direction: { x: -0.4, y: -0.8, z: -0.2 } },
              { role: 'rim', intensity: 0.45, color: '#93C5FD', direction: { x: 0.5, y: -0.2, z: 0.8 } }
            ];
          }
        }

        我会保留三层光:环境光保证整体不黑,主光源保证主体有明暗关系,轮廓光保证模型边缘能从背景里分出来。不是所有项目都需要复杂灯光,但默认灯光不能没有。

        首帧验收不要只靠肉眼

        interface FirstFrameCheckResult {
          hasAsset: boolean;
          hasBounds: boolean;
          cameraReady: boolean;
          lightReady: boolean;
          message: string;
        }
        
        export class FirstFrameChecker {
          check(asset: SpatialAsset, bounds: ModelBounds | undefined, camera: CameraPose | undefined, lights: LightPreset[]): FirstFrameCheckResult {
            if (!asset.localUri || asset.byteSize <= 0) {
              return { hasAsset: false, hasBounds: false, cameraReady: false, lightReady: false, message: '模型资源无效' };
            }
            if (!bounds || bounds.radius <= 0) {
              return { hasAsset: true, hasBounds: false, cameraReady: false, lightReady: false, message: '模型包围盒异常' };
            }
            if (!camera) {
              return { hasAsset: true, hasBounds: true, cameraReady: false, lightReady: false, message: '默认相机未设置' };
            }
            if (lights.length === 0) {
              return { hasAsset: true, hasBounds: true, cameraReady: true, lightReady: false, message: '缺少默认灯光' };
            }
            return { hasAsset: true, hasBounds: true, cameraReady: true, lightReady: true, message: '首帧检查通过' };
          }
        }

        这个检查器的作用是把“看起来没显示”变成几个可以判断的条件。资源、包围盒、相机、灯光只要有一个没准备好,就不要把页面当成成功态。

        推荐的接入结构

        我会把 3DGS 预览页拆成四个小模块:

        模块 负责内容 不负责内容
        SpatialAssetRepository 资源路径、大小、格式、封面图 相机、灯光、页面布局
        CameraPresetBuilder 根据包围盒生成默认相机 模型加载、重建会话
        SceneLightPresetFactory 生成默认灯光组合 页面状态和用户交互
        FirstFrameChecker 判断首屏是否真的可见 修复模型文件本身

        这四个模块单独看都不复杂,但组合起来能解决很多首屏问题。后面换模型、换设备、换横竖屏,也不用每次都从页面里复制一堆判断。

        export class SpatialPreviewBootstrap {
          private cameraBuilder = new CameraPresetBuilder();
          private lightFactory = new SceneLightPresetFactory();
          private checker = new FirstFrameChecker();
        
          async prepare(asset: SpatialAsset, scene: ThreeDSceneController): Promise<FirstFrameCheckResult> {
            await scene.init('spatial-preview-surface');
            await scene.loadAsset(asset);
        
            const bounds = await this.readBounds(asset);
            const camera = this.cameraBuilder.build(bounds);
            const lights = this.lightFactory.buildDefault();
        
            await scene.applyDefaultCamera();
            await scene.applySoftLight();
        
            return this.checker.check(asset, bounds, camera, lights);
          }
        
          private async readBounds(asset: SpatialAsset): Promise<ModelBounds> {
            return {
              center: { x: 0, y: 0, z: 0 },
              size: { x: 1.2, y: 1.8, z: 1.2 },
              radius: 1.2
            };
          }
        }

        这里的代码不是要替代官方接口,而是给接入结构定边界。实际项目里读取包围盒、设置相机、创建灯光都要按当前 SDK 写法接上。结构先稳住,接口替换起来才不乱。

        最后总结

        3DGS 首屏黑屏不要只盯着“模型有没有加载”。更实际的排查顺序是:资源存在、场景初始化、包围盒有效、相机对准、灯光可见、失败有兜底。

        HarmonyOS 7 / API 26 的 3DGS 能力很适合做空间展示,但越是新能力,越不能只追一个成功截图。首屏预览是用户接触 3D 内容的第一秒,这一秒如果黑屏、偏移、太暗或者没兜底,后面的交互都白搭。把相机、灯光和首帧检查做成可复用模块,后续接不同模型、不同设备和不同页面都会稳很多。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐