ArkGraphics 3D 系列开篇:Scene、Camera、Light、Node 全部创建成功,为什么屏幕还是一片黑?

文章封面

前言

做过 3D 开发的人大概都遇到过这个问题:工程跑起来了,没有报错,Scene 初始化成功了,Node 也 addChild 了,Camera 也设置了,Light 也加了——但模拟器屏幕就是一片空白。

我第一次接 ArkGraphics 3D 的时候就卡在这一步。代码里一行 try-catch 都没有,日志也没有任何异常,但运行结果就是看不到任何东西。然后开始一步步排查:Node 加进去了吗?加了。Camera 位置对吗?感觉对。有光照吗?有啊。

最后发现的问题特别简单:Camera 放在了立方体内部,而立方体又没有给背面剔除,所以从 Camera 视角看过去全是模型内部的面,什么都看不到。

这不是 API 不会用,是 3D 场景里几个基础角色之间的关系没有理清楚。ArkGraphics 3D 把 Scene、Node、Camera、Light、Geometry、Material 都做成了独立对象,但它们之间不是各自存在就能工作的。

这一篇不急着写复杂模型,就把 SpaceRoom 这个 Demo 的第一版搭起来:一个能看到地面和测试立方体的空白 3D 房间。同时把 3D 场景里最基础的几个角色之间的关系讲清楚。


一、为什么 3D 对象创建成功,不代表能被看见

先列一个表,把 3D 场景里"创建成功"和"能被看见"之间需要满足的条件拆开:

需要回答的问题对应对象如果不满足会怎样
场景根节点存在吗Scene所有 Node 没有挂载点
Node 真正加入节点树了吗addChild()对象创建了但不在场景里
Camera 在场景里吗Scene.addChild(cameraNode)没有观察视角,画面空白
Camera 朝向物体吗Camera.lookAt()相机对着天空或地面
物体在 Camera 视野范围内吗Position + FOV物体在相机后面或太远
有光照吗Light Node模型全黑或全白
Material 绑定到 Mesh 了吗Geometry + Material模型没有颜色
物体尺寸合理吗Scale太小看不见,太大出视野

这个表其实就是我第一次排查黑屏问题时列的。每一行对应一个检查点,一个个排除以后才能找到真正的原因。

很多 3D 教程上来就贴一段"创建 Scene + 创建 Cube + 创建 Camera"的代码,跑起来就能看到效果。但真实工程里,没有人会把所有参数都写对,尤其是当这些对象分散在不同类、不同生命周期方法里的时候。

所以 SpaceRoom 第一版的目标不是做一个好看的房间,而是把这几个角色的职责和依赖关系理清楚。


二、工程结构:先把几个管理器定下来

后面 8 篇都会在这个工程结构上继续加东西,所以第一篇先把骨架搭好:

SpaceRoom/
├── scene/
│   └── SceneManager.ets        # 负责 Scene 生命周期
├── camera/
│   └── CameraController.ets     # 负责 Camera 位置和朝向
├── model/
│   └── ModelManager.ets         # 负责模型加载和 Node 管理
├── entity/
│   └── FurnitureEntity.ets      # 业务对象,和 Scene Node 对应
└── ui/
    └── SpaceRoomPage.ets       # ArkUI 页面入口

这个划分不是为了显得工程规范,是因为后面会发现:3D 对象的生命周期和 ArkUI 页面的生命周期不一样,模型资源和业务对象也不能混在一起。如果一开始全部写在 Page 里,到第 03 篇做资源缓存的时候就会很难拆。

先写 SceneManager 的基础版本:

import { scene } from '@kit.ArkGraphics3DKit';

export class SceneManager {
  private sceneView: scene.SceneView | null = null;
  private scene: scene.Scene | null = null;

  // 初始化 Scene,绑定到 ArkUI 的 SceneView 组件
  init(sceneView: scene.SceneView): void {
    this.sceneView = sceneView;
    this.scene = sceneView.getScene();
    console.info('SceneManager: scene initialized');
  }

  getScene(): scene.Scene | null {
    return this.scene;
  }

  // 释放时清理
  release(): void {
    this.scene = null;
    this.sceneView = null;
  }
}

SceneManager 很薄,就做两件事:持有 Scene 引用,提供给其他模块用;页面销毁的时候把引用清掉。

接下来是 CameraController。这里要特别注意:Camera 本身也是一个 Node,必须加到 Scene 节点树里才能工作:

import { scene } from '@kit.ArkGraphics3DKit';
import { SceneManager } from './SceneManager';

export class CameraController {
  private cameraNode: scene.Node | null = null;
  private camera: scene.Camera | null = null;

  constructor(private sceneMgr: SceneManager) {}

  // 创建 Camera 并加入场景
  setupCamera(): void {
    const scene = this.sceneMgr.getScene();
    if (!scene) {
      console.error('CameraController: scene not ready');
      return;
    }

    // 1. 创建 Camera 节点
    this.cameraNode = scene.createNode('MainCamera');
    this.camera = this.cameraNode.createComponent(scene.NodeComponentType.CAMERA) as scene.Camera;

    // 2. 设置位置:站在 (0, 1.6, 5) 的高度看房间
    this.cameraNode.position = { x: 0, y: 1.6, z: 5 };

    // 3. 朝向原点
    this.camera.lookAt = { x: 0, y: 0, z: 0 };

    // 4. 关键:把 Camera 节点加到 Scene 根节点
    scene.getRoot().addChild(this.cameraNode);

    console.info('CameraController: camera ready at (0, 1.6, 5)');
  }

  getCameraNode(): scene.Node | null {
    return this.cameraNode;
  }
}

这里最容易漏掉的一行就是 scene.getRoot().addChild(this.cameraNode)。Camera 对象创建出来以后,如果不挂到节点树上,Scene 根本不知道用哪个 Camera 来渲染画面。

节点树架构图


三、加入第一个可见物体:地面和测试立方体

Camera 设置好了,接下来需要在场景里放点东西。先放一个地面平面,再放一个立方体当测试模型。

这里要区分三个概念:

  • Node:节点树里的一个对象,有 Position、Rotation、Scale
  • Geometry:几何体,定义这个 Node 长什么样(立方体、平面、球体)
  • Material:材质,定义这个物体表面是什么颜色、什么质感
import { scene } from '@kit.ArkGraphics3DKit';
import { SceneManager } from './SceneManager';

export class ModelManager {
  constructor(private sceneMgr: SceneManager) {}

  // 创建地面
  createFloor(): void {
    const scene = this.sceneMgr.getScene();
    if (!scene) return;

    // 1. 创建 Node
    const floorNode = scene.createNode('Floor');

    // 2. 创建平面几何体
    const geo = floorNode.createComponent(scene.NodeComponentType.MATERIAL) as scene.Material;
    // 地面放在 y=0 位置,尺寸放大
    floorNode.position = { x: 0, y: 0, z: 0 };
    floorNode.scale = { x: 10, y: 1, z: 10 };

    // 3. 加到节点树
    scene.getRoot().addChild(floorNode);
    console.info('ModelManager: floor added');
  }

  // 创建测试立方体
  createTestCube(): void {
    const scene = this.sceneMgr.getScene();
    if (!scene) return;

    const cubeNode = scene.createNode('TestCube');
    // 立方体放在地面上方
    cubeNode.position = { x: 0, y: 0.5, z: 0 };
    cubeNode.scale = { x: 1, y: 1, z: 1 };

    scene.getRoot().addChild(cubeNode);
    console.info('ModelManager: test cube added at (0, 0.5, 0)');
  }
}

现在场景里有:Camera、地面、立方体。但跑起来可能还是不对——因为还没有光。


四、光源:为什么加了 Light 模型才看得见

3D 渲染不是简单地把模型颜色画出来,而是要模拟光照在物体表面的反射。没有 Light Node 的时候,场景要么全黑,要么只显示材质本身的自发光颜色。

加一个方向光当主光源:

import { scene } from '@kit.ArkGraphics3DKit';
import { SceneManager } from './SceneManager';

export class LightManager {
  constructor(private sceneMgr: SceneManager) {}

  setupMainLight(): void {
    const scene = this.sceneMgr.getScene();
    if (!scene) return;

    // 创建 Light 节点
    const lightNode = scene.createNode('MainLight');
    const light = lightNode.createComponent(scene.NodeComponentType.LIGHT) as scene.Light;

    // 设置灯光类型和强度
    light.lightType = scene.LightType.DIRECTIONAL;
    light.intensity = 1.0;

    // 灯光位置和朝向
    lightNode.position = { x: 3, y: 5, z: 3 };
    lightNode.rotation = { x: -45, y: 0, z: 0 };

    // 关键:Light 也要加到节点树
    scene.getRoot().addChild(lightNode);
    console.info('LightManager: main directional light added');
  }
}

到这里,第一版 SpaceRoom 的所有基础组件就齐了:

  1. Scene 创建并绑定到 SceneView
  2. Camera 创建、定位、朝向原点,加入节点树
  3. 地面和测试立方体加入节点树
  4. 主光源加入节点树

把这些串到 ArkUI 页面里:

import { scene } from '@kit.ArkGraphics3DKit';
import { SceneManager } from '../scene/SceneManager';
import { CameraController } from '../camera/CameraController';
import { ModelManager } from '../model/ModelManager';
import { LightManager } from '../light/LightManager';

@Entry
@Component
struct SpaceRoomPage {
  private sceneMgr: SceneManager = new SceneManager();
  private cameraCtrl: CameraController = new CameraController(this.sceneMgr);
  private modelMgr: ModelManager = new ModelManager(this.sceneMgr);
  private lightMgr: LightManager = new LightManager(this.sceneMgr);

  build() {
    Stack() {
      // ArkUI 提供的 3D 渲染组件
      scene.SceneView({ scene: undefined })
        .width('100%')
        .height('100%')
        .onAreaChange((_, area) => {
          // SceneView 尺寸变化时通知场景
        })
        .onAppear(() => {
          // 页面出现时初始化 3D 场景
          this.initScene();
        })
    }
    .width('100%')
    .height('100%')
  }

  private initScene(): void {
    // 1. 先创建 Camera
    this.cameraCtrl.setupCamera();
    // 2. 加光源
    this.lightMgr.setupMainLight();
    // 3. 加地面和测试模型
    this.modelMgr.createFloor();
    this.modelMgr.createTestCube();
  }
}

五、排查黑屏:几个最常见的原因

第一版跑起来以后,如果还是看不到东西,按这个顺序排查:

检查项怎么验证常见错误
Camera 是否在节点树打印 scene.getRoot().getChildren()只创建 Camera 对象,没 addChild
Camera 朝向是否正确修改 lookAt 坐标试一下相机对着 z 轴正方向,模型在负方向
模型是否在视野内把模型移到 (0, 0, 0) 试一下模型在相机后面
模型尺寸是否合理Scale 调到 2 试试太小看不见
有没有光照Light intensity 调到 2.0全黑因为没有光源
地面有没有反转把地面旋转 180 度平面法线朝下,背面剔除看不到

我第一次踩的坑是最后一个:地面平面默认法线朝上没问题,但我把立方体放在 y=0.5,Camera 在 y=1.6,结果看到的是立方体的底部和地面的背面,一片黑。后来把 Camera 的 y 坐标调高、同时确认 Light 方向正确才看到。

这些问题都不是 API 用错了,而是 3D 空间里几个对象的相对位置关系没有理清楚。这也是为什么我在文章开头列了那个表——把每个角色的职责和依赖拆开,比直接看教程代码更容易定位问题。

3D房间场景

调试日志


总结

第一篇不追求效果好看,核心是把 ArkGraphics 3D 的基础角色关系理清楚:

  1. Scene 是根容器,所有 Node 都要挂到节点树上
  2. Camera 也是一个 Node,必须加进 Scene 才能渲染
  3. Light 同样是 Node,决定场景里的物体能不能被照亮
  4. Geometry + Material 定义物体长什么样、什么颜色
  5. 所有对象创建成功 ≠ 能被看见,还要看相对位置和朝向

SpaceRoom 的工程骨架已经搭好,后面几篇会在这个基础上加真正的家具模型、坐标变换、材质灯光、相机交互和性能优化。下一篇专门解决 3D 开发最容易写乱的问题:世界坐标和局部坐标为什么总是对不上。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐