1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

TwinMakerで、ログインユーザーの現在地を3D上に出す方法

Last updated at Posted at 2023-06-11

この記事は何?

この記事では、2つのことを解説します

  • TwinMaker上で、ログインユーザーごとに違うコンテンツを表示する方法を解説します
  • TwinMaker上で、リアルタイムな位置情報を、3Dコンテンツの位置に反映させる方法を解説します

この記事で作るもの

この記事では、以下のようなTwinMakerのシーンを作ります

  • Aさんがログインすると、Aさんの部屋のルンバの位置に、ルンバの3Dモデルが表示されます
  • Bさんがログインすると、Bさんの部屋のルンバの位置に、ルンバの3Dモデルが表示されます
  • また、注釈に表示される画像と文言が変わります
ログインユーザー 表示される画面
Aさん
Bさん

※記事では位置情報の取得は作らないため、あくまで3D上でルンバの位置が変わるよ、というところだけ説明します

どうやって実現するの?

  • Pythonの定期実行で、動的にTwinMakerのシーンをコピーします
  • ログインユーザーの情報が必要になるので、iot-app-kitでWebアプリ化します

何が嬉しいの?

  • ユーザーAさんにはAさん向けの3Dを、BさんにはBさん向けの3Dだけを見せることができます

必要なもの

  • AWSアカウント
  • Python3.10
  • VSCode
  • Node.js(記事はv14.15.1で検証していますが、新しいほうがいいです)

まずは下準備: iot-app-kitの導入

AWSマネジメントコンソール上のTwinMakerはAWSアカウントを持っている開発者しか見られないので、ログインユーザーごとに動きを変えるには不適当です。iot-app-kitを利用できるようにします。

iot-app-kitはAWSが公開しているライブラリです。TwinMakerの3Dシーン、グラフなど、ReactのWebアプリとして構築することができます。利用者専用の閲覧用Webページを作ることができます。

(iot-app-kitがまだ歴史の浅いライブラリなこともあって)構築は少し大変です。README通りにやっても動かないので、以下の手順を見ながら始めてください

ステップ1: Reactプロジェクトをつくる

構築手順
# プロジェクトを作りたいフォルダで、reactのプロジェクトを作成します
npx create-react-app app --template typescript 

# プロジェクトができたら、作成したフォルダに移動します
cd app

# 必要な依存ライブラリをインストールします
npm install

ステップ2: iot-app-kitをインストールします

# app-kitをインストールします(※READMEに出ている手順はここまでです)
npm install @iot-app-kit/components 
npm install @iot-app-kit/react-components 
npm install @iot-app-kit/source-iottwinmaker 
npm install @iot-app-kit/scene-composer

# とりあえずエラーが出るので、babelのプラグインをインストールします
npm install @babel/plugin-proposal-private-property-in-object

# sassがないので、sassをインストールします
npm install sass

# ポリフィルが必要なので、react-app-rewiredとポリフィルをインストールします
npm install react-app-rewired --save-dev
npm install node-polyfill-webpack-plugin

ステップ3:プロジェクトのルートに、react-app-wired向けの設定ファイルを置きます

config-overrides.js
const NodePolyfillPlugin = require("node-polyfill-webpack-plugin");

module.exports = function override(config, env) {
  if (!config.plugins) {
    config.plugins = [];
  }
  config.plugins.push(
    new NodePolyfillPlugin({
      excludeAliases: ["console"],
    })
  );
  config.resolve.alias = {
    "react/jsx-runtime.js": "react/jsx-runtime",
    "react/jsx-dev-runtime.js": "react/jsx-dev-runtime",
  };
  return config;
};

ステップ4: package.jsonのscriptsを書き変えて、react-app-rewiredを使えるようにします

package.json
  "scripts": {
    "start": "react-app-rewired start",
    "build": "react-app-rewired build",
    "test": "react-app-rewired test",
    "eject": "react-app-rewired eject"
  },

ステップ5: three.jsのバージョンが合わないので、最新のバージョンを持ってきます

npm install three
npm install @react-three/fiber
npm install @react-three/drei
npm install three-stdlib

ステップ6: 準備ができたので、Reactを起動します

npm run start

多分これで大丈夫です(※2023年6月11日現在)

6月11日時点のバージョン情報: package.json
package.json
{
  "name": "app",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@babel/plugin-proposal-private-property-in-object": "^7.21.11",
    "@iot-app-kit/components": "^6.2.0",
    "@iot-app-kit/react-components": "^6.2.0",
    "@iot-app-kit/scene-composer": "^3.3.0",
    "@iot-app-kit/source-iottwinmaker": "^6.2.0",
    "@react-three/drei": "^9.74.8",
    "@react-three/fiber": "^8.13.1",
    "@testing-library/jest-dom": "^5.16.5",
    "@testing-library/react": "^13.4.0",
    "@testing-library/user-event": "^13.5.0",
    "@types/jest": "^27.5.2",
    "@types/node": "^16.18.35",
    "@types/react": "^18.2.11",
    "@types/react-dom": "^18.2.4",
    "jsx-runtime": "^1.2.0",
    "node-polyfill-webpack-plugin": "^2.0.1",
    "path-browserify": "^1.0.1",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-scripts": "5.0.1",
    "sass": "^1.63.3",
    "three": "^0.153.0",
    "three-stdlib": "^2.23.9",
    "typescript": "^4.9.5",
    "web-vitals": "^2.1.4"
  },
  "scripts": {
    "start": "react-app-rewired  start",
    "build": "react-app-rewired  build",
    "test": "react-app-rewired  test",
    "eject": "react-app-rewired  eject"
  },
  "eslintConfig": {
    "extends": [
      "react-app",
      "react-app/jest"
    ]
  },
  "browserslist": {
    "production": [
      "last 1 chrome version"
    ],
    "development": [
      "last 1 chrome version"
    ]
  },
  "devDependencies": {
    "react-app-rewired": "^2.2.1"
  }
}

構築作業1: テンプレートになるTwinMakerのシーンを作ります

実施する作業

まずはtemplateの名前で、TwinMakerのシーンを作成します
作業は以下の2点です

  • 部屋の3Dモデルを中央に配置する
  • カメラをCamera1の名前で作成する

scene-template.png

部屋のモデルのGLBはSketchfabからお借りしました(ライセンスはCC 4.0です)
https://sketchfab.com/3d-models/studio-apartment-vray-baked-textures-included-cae2d96ede1d4112b1fd391099a43f77

この作業をすると、こうなる

templateの名前でシーンを作成すると、S3の対応するバケット(末尾が-iadのバケット)にtemplate.jsonのJSONファイルが作られます

template-s3.png

TwinMakerのシーンは、このようにS3に作られるシーン名と同じ名前のJSONファイルで全て管理されます

構築作業2: ユーザーと同じ数だけ、空のシーンを作ります

実施すること

今回は2人のユーザーでログインさせる想定ですから、2つの空のシーンを作りました

  • shotaoki_mail_com
  • other_user_mail_com

命名規則の都合で@などが使えないため、メールアドレスの記号部分をアンダーバーに置き換えたものを使っています

3-scenes.png

クオータにある通り、1つのワークスペースに作成することができるシーンの数は100ですから、あと97人まで増やしても大丈夫です

構築作業3: Pythonのスクリプトを作って、templateをコピーする処理を実装します

structure.png

実施すること

  1. Pythonのスクリプトで、S3にあるtemplateのJSONファイルを取得します
  2. オブジェクトの配置情報を書き変えて、ユーザーA向けのJSONを上書きします
  3. オブジェクトの配置情報を書き変えて、ユーザーB向けのJSONを上書きします

こうすると、ユーザーA向けのシーン、ユーザーB向けのシーンができます

ルンバのデータもScketchfabからお借りしました。こちらもライセンスはCC 4.0です
https://sketchfab.com/3d-models/low-poly-roomba-be32b697116f4c70971da8c53b3b6653

ソースコード

以下のpythonファイルを、python app --bucketname ${S3のバケット名}で実行します

実行に必要なライブラリのインストール
pip install pydantic boto3
app.py
from argparse import ArgumentParser
from pydantic import BaseModel
import boto3
import io
import json
from typing import List


class Argments(BaseModel):
    """
    app.pyの引数を定義する

    実行方法:
    python app.py --bucketname ${バケット名}
    """

    bucketname: str

    @classmethod
    def parse_args(cls):
        # pythonファイルの実行引数をpydanticにパースする
        parser = ArgumentParser()
        for k in cls.schema()["properties"].keys():
            parser.add_argument(f"-{k[0:1]}", f"--{k}")
        return cls.parse_obj(parser.parse_args().__dict__)


def add_roomba(
    template: dict, args: Argments, x: float, y: float, z: float, message: str
):
    """
    ルンバを部屋に召喚する

    message: アノテーションに表示するメッセージ
    x, y, z: 表示する座標
    """
    current: List[dict] = template["nodes"]

    # ルンバを配置する(glbがややズレているので、固定値でオフセットを入れる)
    current.append(
        {
            "name": "low-poly_roomba",
            "transform": {
                "position": [-3.056 + x, -0.4 + y, -2.78 + z],
                "rotation": [0, 0, 0],
                "scale": [0.5, 0.5, 0.5],
            },
            "transformConstraint": {},
            "components": [
                {
                    "type": "ModelRef",
                    "uri": f"s3://{args.bucketname}/low-poly_roomba.glb",
                    "modelType": "GLB",
                    "unitOfMeasure": "meters",
                }
            ],
            "properties": {},
        }
    )
    # ルンバの上のアノテーションを配置する
    current.append(
        {
            "name": "Annotation",
            "transform": {
                "position": [-1.2 + x, 0.1671 + y, 1.4 + z],
                "rotation": [0, 0, 0],
                "scale": [0.5, 0.5, 0.5],
            },
            "transformConstraint": {},
            "components": [
                {
                    "type": "DataOverlay",
                    "subType": "TextAnnotation",
                    "valueDataBindings": [],
                    "dataRows": [{"rowType": "Markdown", "content": message}],
                }
            ],
            "properties": {},
        }
    )
    # オブジェクトの配置情報を設定する
    template["nodes"] = current
    # 全てのオブジェクトをルートノードに設定する
    template["rootNodeIndexes"] = [r for r in range(len(current))]


def main(args: Argments):
    """
    ユーザーAのシーンを作成する
    """
    s3 = boto3.resource("s3")
    bucket = s3.Bucket(args.bucketname)

    # テンプレートシーンをコピーする
    template = {}
    with io.BytesIO() as f:
        bucket.download_fileobj("template.json", f)
        template = json.loads(f.getvalue())

    # ルンバ召喚
    add_roomba(
        template,
        args,
        x=0,
        y=0,
        z=0,
        message="""
## Aさんのルンバ
        """,
    )

    # ルンバを召還したシーンを、ユーザーAのシーンとして保存する
    with io.BytesIO(json.dumps(template).encode("utf-8")) as f:
        bucket.upload_fileobj(f, "shotaoki_mail_com.json")


def main2(args: Argments):
    """
    ユーザーBのシーンを作成する
    """
    s3 = boto3.resource("s3")
    bucket = s3.Bucket(args.bucketname)

    # テンプレートシーンをコピーする
    template = {}
    with io.BytesIO() as f:
        bucket.download_fileobj("template.json", f)
        template = json.loads(f.getvalue())

    # ルンバ召喚(※X方向に1.6メートル、Z方向に2.2メートル移動した場所)
    add_roomba(
        template,
        args,
        x=1.6,
        y=0,
        z=-2.2,
        message="""
## Bさんのルンバ
        """,
    )

    # ルンバを召還したシーンを、ユーザーBのシーンとして保存する
    with io.BytesIO(json.dumps(template).encode("utf-8")) as f:
        bucket.upload_fileobj(f, "other_user_mail_com.json")


# 処理を実行する
main(Argments.parse_args())
main2(Argments.parse_args())

環境構築4: iot-app-kitからシーンを開きます

実施すること

下準備で作っていたReactプロジェクトを開いて、src/App.tsxを更新します

import "./App.css";
import { initialize } from "@iot-app-kit/source-iottwinmaker";
import { SceneViewer } from "@iot-app-kit/scene-composer";

function App() {
  const sceneLoader = initialize("${workspace-name}", {
    awsCredentials: {
      accessKeyId: "XXXXXXXXXXXXXXX",
      secretAccessKey: "XXXXXXXXXXXXXXX",
    },
    awsRegion: "${region-name}",
  }).s3SceneLoader("${scene-name}");

  return (
    <div className="App">
      <SceneViewer sceneLoader={sceneLoader} activeCamera="Camera1" />
    </div>
  );
}

export default App;

変数は以下のように、自分の環境に置き換えてください

キー名
${workspace-name} TwinMakerのワークスペースの名前です
${scene-name} シーン名です ※ログインユーザーのユーザー名を当てはめます
AccessKeyId AWSの認証情報です。※実際の運用ではCognitoを使うようにしてください
SecretAccessKey AWSの認証情報です。※実際の運用ではCognitoを使うようにしてください
activeCamera 初期状態で指定するカメラです。シーン内にカメラを置いていないのなら未指定にします

動かしたところ

npm run startで動かして、ブラウザでhttp://localhost:3000を開くと、TwinMakerの画面が表示されます

a-san.png

環境構築5: アノテーションに画像を入れる

アノテーションには画像を表示することができます。

ソースコードで## Aさんのルンバと書いてある部分(アノテーション、オーバーレイ)は、マークダウンを使うことができるため、![](http://url.com/file.png)の書式で、好きな画像を表示させることができます。

テキストの下に画像を表示するには、2行あけることが必要です
必ずテキストが必要です。テキストの内容でアノテーションの表示エリアの大きさが決まるため、画像だけを表示させることはできません

## Aさんのルンバ

![](http://url.com/file.png)

注意:マネジメントコンソールではアノテーション上の画像リンクは動かない
※AWSマネジメントコンソールのTwinMaker上では、接続できるドメインが限定されているため、画像リンクは動きません。iot-app-kitが必要です

まとめ: ログインユーザーの位置情報を3Dに表示する

位置情報を取得してSiteWiseに連携させる必要はありますが、Lambdaの定期実行でシーンコピーを行うことで、ログインユーザーが持っているIoT機器の位置情報を3D上に表示させることができるようになります。

つまりTwinMakerを使うことで、ルンバがバーチャルウォールの向こう側に引っかかって動けなくなっているよみたいなことを、デジタルツインで可視化することができます。複雑なコーディングの必要はなく、Blenderのような3Dソフトを覚える必要もありません。そこが大きな利点です。

また、アノテーションの画像の取得先をApiGatewayやS3 Object Lambdaにすれば、TwinMakerのページを開いた瞬間のグラフを出すこともできます

補足

シーンの更新はリロード時にしか行われず、TwinMaker上で3Dモデルのアニメーションは(※普通の方法では)再生できません。ですので、ダッシュボードを開いたままリアルタイムで追いかける、といったことは難しいです

TwinMakerで表示する3DモデルをLambdaで動的に作って配信することは可能です。ですが、その場合はLambdaの定期実行で出力したファイルをS3に配置する必要があります。Object Lambdaのエンドポイントを通して、TwinMakerが参照する3DモデルをLambdaから直接配信することはできません。(※エイリアスを使うことでS3エンドポイントを通した配信はできますが、Object Lambdaのエンドポイントは厳密にS3エンドポイントとして扱えないので、ドメインの制限に引っかかります)

画像から動的にGLTFを作る方法はこちらを参照してください

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?