Mobile Development 16 min read

HarmonyOS 6.1.1 3DGS Tutorial: Cube, glTF Loading & Touch Rotation

This tutorial walks through building a 3D rendering pipeline on HarmonyOS 6.1.1 using SceneView, starting with a programmatic cube, loading a glTF model from rawfile, adding directional lighting, and implementing single-finger touch rotation, all tested on the phone simulator.

51CTO HarmonyOS Developer Community
51CTO HarmonyOS Developer Community
51CTO HarmonyOS Developer Community
HarmonyOS 6.1.1 3DGS Tutorial: Cube, glTF Loading & Touch Rotation

Introduction

HarmonyOS 6.1.1 highlights on-device 3D Gaussian Splatting (3DGS) reconstruction. This article records a hands-on journey from a simple cube to loading an external glTF model and adding finger-rotation interaction, running entirely on the HarmonyOS 6.1.1 phone simulator.

Environment Setup

DevEco Studio 6.1.1 Release (Build #6.1.1.300, 2026-07-27)

HarmonyOS SDK 6.1.1 (API Version 24)

Simulator: Phone form factor, HarmonyOS 6.1.1 system image

Create an Empty Ability project with SDK API 24, device type Phone. In module.json5 set targetApiVersion and compatibleApiVersion to 24.

3DGS Background

3D Gaussian Splatting represents scenes with thousands of tiny ellipsoids (position, size, color, opacity) instead of triangle meshes. Benefits: faster rendering and natural handling of transparency and blur. HarmonyOS integrated this capability from 5.0; 6.1.1 provides mature APIs and tooling. On-device reconstruction uses Camera Kit and Modeling3D Kit with NPU acceleration, but the simulator cannot run reconstruction — only rendering of pre-built models.

Model File Preparation

3DGS outputs .ply or .splat, but HarmonyOS SceneView does not yet support them directly. Use glTF 2.0 ( .glb or .gltf) instead. Place the model in src/main/resources/rawfile/ (create the directory if missing), e.g., sample_model.glb.

Step 1: Minimal Cube Rendering

Create CubePage.ets to verify the rendering pipeline with a procedural cube.

// CubePage.ets
import { scene } from '@kit.ArkGraphics3D';

@Entry
@Component
struct CubePage {
  private sceneView: SceneView | null = null;

  build() {
    Column() {
      SceneView({
        scroller: this.sceneView,
        onCreate: (view: SceneView) => {
          this.setupScene(view);
        }
      })
        .width('100%')
        .height('60%')
        .backgroundColor(Color.Gray)

      Text('立方体 - SceneView 最简渲染')
        .fontSize(16)
        .padding(12)
    }
    .width('100%')
    .height('100%')
  }

  private setupScene(view: SceneView) {
    const scene = view.getScene();
    if (!scene) return;

    const rootNode = scene.getRootNode();
    if (!rootNode) return;

    const cubeNode = scene.createNode({ name: 'cube' });
    if (!cubeNode) return;

    cubeNode.setGeometry(scene.createBoxGeometry({ width: 2, height: 2, depth: 2 }));
    cubeNode.setMaterial(scene.createMaterial({ baseColor: { r: 0.2, g: 0.6, b: 0.8, a: 1.0 } }));
    rootNode.addChild(cubeNode);

    const camera = scene.getCamera();
    if (camera) {
      camera.setPosition({ x: 0, y: 0, z: 5 });
      camera.lookAt({ x: 0, y: 0, z: 0 });
    }
  }
}

The code obtains the scene, creates a cube node with geometry and material, attaches it to the root node, and positions the camera at (0,0,5) looking at the origin. A blue-green cube appears on a gray background.

Step 2: Loading an External glTF Model

Create ModelPage.ets to load the .glb file from rawfile.

// ModelPage.ets
import { scene } from '@kit.ArkGraphics3D';

@Entry
@Component
struct ModelPage {
  private sceneView: SceneView | null = null;

  build() {
    Column() {
      SceneView({
        scroller: this.sceneView,
        onCreate: (view: SceneView) => {
          this.setupScene(view);
        }
      })
        .width('100%')
        .height('60%')
        .backgroundColor(Color.Gray)

      Text('glTF 模型加载')
        .fontSize(16)
        .padding(12)
    }
    .width('100%')
    .height('100%')
  }

  private setupScene(view: SceneView) {
    const sceneObj = view.getScene();
    if (!sceneObj) return;

    const rootNode = sceneObj.getRootNode();
    if (!rootNode) return;

    try {
      const modelNode = sceneObj.createNodeFromFile({
        uri: 'rawfile://sample_model.glb',
        name: 'loaded_model'
      });
      if (modelNode) {
        rootNode.addChild(modelNode);
      }
    } catch (err) {
      console.error('模型加载失败: ' + JSON.stringify(err));
    }

    const lightNode = sceneObj.createNode({ name: 'light' });
    if (lightNode) {
      lightNode.setLight(sceneObj.createLight({
        type: scene.LightType.DIRECTIONAL,
        color: { r: 1.0, g: 1.0, b: 1.0, a: 1.0 },
        direction: { x: -0.5, y: -1, z: -0.5 }
      }));
      rootNode.addChild(lightNode);
    }

    const camera = sceneObj.getCamera();
    if (camera) {
      camera.setPosition({ x: 0, y: 1, z: 3 });
      camera.lookAt({ x: 0, y: 0, z: 0 });
    }
  }
}

Key points:

Resource path: Use rawfile:// protocol; subdirectories become rawfile://models/sample_model.glb.

Lighting: glTF models include materials but need a light source; a white directional light from above-front is added.

Camera: Position (0,1,3) works for many models; adjust z for scale.

Error handling: Catch load failures and log to console.

Step 3: Adding Touch Rotation Interaction

Extend ModelPage with single-finger drag rotation.

// ModelPage.ets - with rotation
import { scene } from '@kit.ArkGraphics3D';

@Entry
@Component
struct ModelPage {
  private sceneView: SceneView | null = null;
  private modelNode: scene.Node | null = null;
  private lastX: number = 0;
  private lastY: number = 0;
  private rotateX: number = 0;
  private rotateY: number = 0;

  build() {
    Column() {
      SceneView({
        scroller: this.sceneView,
        onCreate: (view: SceneView) => {
          this.setupScene(view);
        }
      })
        .width('100%')
        .height('60%')
        .backgroundColor(Color.Gray)
        .onTouch((event: TouchEvent) => {
          this.handleTouch(event);
        })

      Text('手指滑动旋转模型')
        .fontSize(16)
        .padding(12)
    }
    .width('100%')
    .height('100%')
  }

  private setupScene(view: SceneView) {
    const sceneObj = view.getScene();
    if (!sceneObj) return;

    const rootNode = sceneObj.getRootNode();
    if (!rootNode) return;

    try {
      this.modelNode = sceneObj.createNodeFromFile({
        uri: 'rawfile://sample_model.glb',
        name: 'loaded_model'
      });
      if (this.modelNode) {
        rootNode.addChild(this.modelNode);
      }
    } catch (err) {
      console.error('模型加载失败: ' + JSON.stringify(err));
    }

    const lightNode = sceneObj.createNode({ name: 'light' });
    if (lightNode) {
      lightNode.setLight(sceneObj.createLight({
        type: scene.LightType.DIRECTIONAL,
        color: { r: 1.0, g: 1.0, b: 1.0, a: 1.0 },
        direction: { x: -0.5, y: -1, z: -0.5 }
      }));
      rootNode.addChild(lightNode);
    }

    const camera = sceneObj.getCamera();
    if (camera) {
      camera.setPosition({ x: 0, y: 0.5, z: 3 });
      camera.lookAt({ x: 0, y: 0, z: 0 });
    }
  }

  private handleTouch(event: TouchEvent) {
    if (!this.modelNode) return;

    if (event.type === TouchType.Down) {
      this.lastX = event.touches[0].x;
      this.lastY = event.touches[0].y;
    } else if (event.type === TouchType.Move) {
      const deltaX = event.touches[0].x - this.lastX;
      const deltaY = event.touches[0].y - this.lastY;

      this.rotateY += deltaX * 0.5;
      this.rotateX += deltaY * 0.5;

      this.modelNode.setRotation({ x: this.rotateX, y: this.rotateY, z: 0 });

      this.lastX = event.touches[0].x;
      this.lastY = event.touches[0].y;
    }
  }
}

Rotation logic: on Down record finger coordinates; on Move compute deltas, multiply by sensitivity factor 0.5, accumulate Euler angles ( rotateX for vertical tilt, rotateY for horizontal spin), and apply via setRotation.

Full Project Integration

Entry page Index.ets provides navigation buttons.

// Index.ets
import router from '@ohos.router';

@Entry
@Component
struct Index {
  build() {
    Column({ space: 20 }) {
      Text('3DGS 初体验 Demo')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 40 })

      Button('立方体渲染')
        .width('70%')
        .onClick(() => {
          router.pushUrl({ url: 'pages/CubePage' });
        })

      Button('glTF 模型加载')
        .width('70%')
        .onClick(() => {
          router.pushUrl({ url: 'pages/ModelPage' });
        })
    }
    .width('100%')
    .height('100%')
    .backgroundColor(Color.White)
  }
}

Register pages in main_pages.json:

{
  "src": [
    "pages/Index",
    "pages/CubePage",
    "pages/ModelPage"
  ]
}

Running Results

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

51CTO HarmonyOS Developer Community
Written by

51CTO HarmonyOS Developer Community

The HarmonyOS Developer Community is a learning-oriented community for developers to learn, communicate, ask questions, and share.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.