虎の穴開発室ブログ

虎の穴ラボ株式会社所属のエンジニアが書く技術ブログです

MENU

Flutter GPUで簡単に作る!3Dモデルビュアー

こんにちは、虎の穴ラボの吉岡です。

今回はFlutter 3.24で追加された Flutter GPU公式サンプルを参考に簡単な3DCGビュアーを作ってみようと思います。
Flutter GPUはまだ初期プレビュー版のため iOS, Android, MacOSにのみ対応しています。
そのため今回はMacOSアプリとして実行します。

事前準備

Flutter GPUを使うためにはFlutterの最新版3.24を使用し、main(master)チャンネルに切り替える必要があります。
そのため普段stableチャンネル等を使用している場合はmainに切り替えてFlutterをアップグレードをしてください。

$ flutter channel main
$ flutter upgrade
$ flutter --version
Flutter 3.24.0-1.0.pre.539 ・channel main・
~~~

プロジェクト作成

今回使用するプロジェクトを作成して、必要な設定の反映し、パッケージの追加を行っていきます

$ flutter create my_3d_app
$ flutter config --enable-native-assets
$ flutter pub add flutter_scene vector_math

有効にした native-assets はDartでネイティブライブラリなどの自動ビルドや自動インポートをサポートする機能です。
今回は3Dモデルの自動インポートをするために使用します。

パッケージは2つ追加しています。

  • flutter_scene: Flutterで3Dを扱うためのライブラリです。
  • vector_math: ベルトルの計算などをするためのライブラリです。

3Dモデルの準備

今回表示する3DモデルはglTF-Sample-AssetsのDamagedHelmetを使用させていただきます。

3DモデルのファイルをFlutterのプロジェクトルートへ置きます。

$ curl -O https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Models/main/2.0/DamagedHelmet/glTF-Binary/DamagedHelmet.glb

また、glb 形式はFlutter上で扱えないため、model ファイルに変換します。
コマンドでファイルを変換する方法もありますが今回は native_assets_cli のライブラリを使いhookで自動変換されるようにしていきます。
プロジェクトの中にhook ディレクトリを作成しその中に build.dart ファイルを用意します。

import 'package:native_assets_cli/native_assets_cli.dart';
import 'package:flutter_scene_importer/build_hooks.dart';

void main(List<String> args) {
  build(args, (config, output) async {
    buildModels(buildConfig: config, inputFilePaths: [
      'DamagedHelmet.glb', // プロジェクトルートに置いた3Dモデルのファイル
    ]);
  });
}

これでFlutterを実行、ビルドする際に自動で.model形式のファイルを生成してくれるようになります。
変換されたファイルをアプリの中で使用するためにpubspec.yaml へassetsの読み込みに追加します.

flutter:
  assets:
    - build/models/

実装

今回はサンプルを参考に、3Dモデルを自由に横方向に回して表示できるビュアーを作ります。

lib/main.dart

アプリ起動時の読み込みと基本的な表示、操作周りはmain.dartにまとめています。

import 'dart:math';

import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_scene/camera.dart';
import 'package:flutter_scene/node.dart';
import 'package:flutter_scene/scene.dart';
import 'package:my_3d_app/scene_painter.dart';
import 'package:vector_math/vector_math.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  MyAppState createState() => MyAppState();
}

class MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {
  double horizontal = 10.0; // 横(x軸)の入力を受けるstate
  Scene scene = Scene();

  @override
  void initState() {
    // 3Dモデルの読み込み、hookで自動ビルドされた .modelのファイルを読み込む
    Node.fromAsset('build/models/DamagedHelmet.model').then((model) {
      model.name = 'Helmet';
      // 読み込んだ3DモデルをSceneへ追加する
      scene.add(model);
    });

    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    // 読み込んだ3Dモデルを持つSceneを使用する
    // CustomPainterを継承したクラス(後記)のオブジェクトを生成する
    // ユーザのマウス入力があった際は3Dモデルの表示を自動で更新する
    final painter = ScenePainter(
      scene: scene,
      camera: PerspectiveCamera(
        position: Vector3(sin(horizontal) * 3, 2, cos(horizontal) * 3),
        target: Vector3(0, 0, 0),
      ),
    );

    return MaterialApp(
      title: 'My 3D app',
      // ユーザのマウス(画面タップ)操作を検知するために
      // GestureDetector Widgetを使用する
      home: GestureDetector(
        // 表示は上で作ったpainterを渡したCustomPaint Widgetが行う
        child: CustomPaint(painter: painter),
        // 今回は横(x軸)方向の入力だけ受けるため
        // onHorizontalDragUpdateのコールバックに処理を記述する
        onHorizontalDragUpdate: (DragUpdateDetails details) {
          // 入力された方向に3Dモデルを回転させるために
          // x軸の入力を加算する(そのままでは入力量が大きいため 1/10にしておく)
          setState(() {
            horizontal += details.delta.dx / 10;
          });
        },
      ),
    );
  }
}

lib/scene_painter.dart

今回3Dを表示するために使用する CustomPaint を継承したクラスを用意します。 ロードした3Dモデルを持つ Scene を持つのと shouldRepaint を trueにすることで再描画を有効にします。

import 'package:flutter/material.dart';
import 'package:flutter_scene/camera.dart';
import 'package:flutter_scene/scene.dart';

class ScenePainter extends CustomPainter {
  ScenePainter({required this.scene, required this.camera});
  Scene scene;
  Camera camera;

  @override
  void paint(Canvas canvas, Size size) {
    scene.render(camera, canvas, viewport: Offset.zero & size);
  }

  // 入力情報が更新されたら描画も更新するために trueを返す
  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}

実行

これで3D表示する準備はできたので後は実行するだけです!

$ flutter run -d macos --enable-impeller

実行結果

3Dビュアー動作結果

マウスをクリックした状態で左右に動かすことで連動して表示されている3Dモデルも左右に動く様に表示されました!

まとめ

これだけのコードで3Dモデルを表示できるようになりました。
今まではFlutterを使う場合はそれぞれSwiftやKotlinなどネイティブのコードを書く必要があったのが、 Flutter(Dart)のみで解決できるようになったのでとても難易度が下がった気がします。
まだ実験的な機能ではありますがこれからWebなどへの対応などアップデートを楽しみにして待っていましょう。

採用情報

虎の穴ラボでは一緒に働く仲間を募集中です!
この記事を読んで、興味を持っていただけた方はぜひ弊社の採用情報をご覧ください。
toranoana-lab.co.jp