この記事は何?
この記事では、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向けの設定ファイルを置きます
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を使えるようにします
"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
{
"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
の名前で作成する
部屋のモデルのGLBはSketchfabからお借りしました(ライセンスはCC 4.0です)
https://sketchfab.com/3d-models/studio-apartment-vray-baked-textures-included-cae2d96ede1d4112b1fd391099a43f77
この作業をすると、こうなる
template
の名前でシーンを作成すると、S3の対応するバケット(末尾が-iadのバケット)にtemplate.json
のJSONファイルが作られます
TwinMakerのシーンは、このようにS3に作られるシーン名と同じ名前のJSONファイル
で全て管理されます
構築作業2: ユーザーと同じ数だけ、空のシーンを作ります
実施すること
今回は2人のユーザーでログインさせる想定ですから、2つの空のシーンを作りました
- shotaoki_mail_com
- other_user_mail_com
命名規則の都合で@などが使えないため、メールアドレスの記号部分をアンダーバーに置き換えたものを使っています
クオータにある通り、1つのワークスペースに作成することができるシーンの数は100ですから、あと97人まで増やしても大丈夫です
構築作業3: Pythonのスクリプトを作って、templateをコピーする処理を実装します
実施すること
- Pythonのスクリプトで、S3にあるtemplateのJSONファイルを取得します
- オブジェクトの配置情報を書き変えて、ユーザーA向けのJSONを上書きします
- オブジェクトの配置情報を書き変えて、ユーザー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
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の画面が表示されます
環境構築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を作る方法はこちらを参照してください