【イベントレポート】SIer・業務系エンジニアのXRスタートアップ転職を語る会

こんにちは、エンジニアのクロックスこと黒岩(@kro96_xr)です。
今回は先日行われたSIer・業務系エンジニアのXRスタートアップ転職を語る会のイベントレポートをお届けします。

▼アーカイブはこちら▼
www.youtube.com

イベント概要

本イベントはSynamon社内のSIer出身者が各自のXR転職を振り返りつつパネルディスカッションを行うという形式で行われました。

「業務でXR開発はやったことないけど、XR業界に転職したい」
「SIerの経験って何がどう活かせるの?」
「XR開発やりたいけど、Unity業務経験必須なら無理かな…」
「何はともあれ、XR転職を成功させた人のお話が聞きたい!」

XR転職を目指しながらも、実態がわからない&何をアピールすればいいのかわからないといった方々、中々多いのではないでしょうか。
今回はSynamon内の元SIerメンバーが自身のXR転職を振り返りつつ、皆さんのお悩みに答えていきたいと思います!
(イベントページより引用)

なお、当日は以下の流れで進行しました。
f:id:krocks96:20220206170043p:plain

自己紹介~「VRは体力ですよ」~

自己紹介の見どころはうぃすきーさんが三点倒立し始めたところですね。

うぃすきーさん曰く「VRは体力ですよ」とのことですので、みなさん体力付けていきましょう!

テーマ①「自分、こうやってXR転職しました」

このテーマでは各自がSynamonに入社するまでの経緯をお話ししました。
f:id:krocks96:20220206203456p:plain

クロックス

  • SIerで製造業向けパッケージソフトの運用保守・開発をやっていた
  • 2018年夏の6社合同採用イベントでSynamonを知る
  • 開発スキル不足を自覚していたので未経験待遇でWebエンジニアへ(年収は3桁万減)
  • 2021年にあらためて応募、1回お祈りされてからのスカウトにより入社

うぃすきーさん

  • SIerでインフラ担当をしていた
  • 開発経験を積むためにWeb系の会社に転職(年収は半減)
  • 自力で一から開発する経験を積んでSynamonに入社

じゅーいちさん。

  • Synamonはいつの間にか知っていた
  • おそらくTwitterで界隈の動きを追っているときに知った
  • リアルイベントが多かった時期で、参加している中で開発されている方や会社を知った…と思う
  • 1年間くらいは所属していたSES内で何かできないか探したりはしていた

テーマ②「実際働いてみてどう?働き方の違いは?」

このテーマではざっくばらんに働き方の違いについてお話ししました。
f:id:krocks96:20220206203552p:plain

  • 全部違う、共通点はオフィスがあることくらい
  • 忙しさは波があるので一概には言えない、SIerの方が開発スパンが長いかも
  • SIerだと人数規模が大きく業務の細分化が進んでいてやることが明確
  • Synamonだと「待っていると何も仕事が振られない」「やることは自分で取りにいく」
  • 最初の2週間くらい不安で辛かったが、自分で手をあげて動いていくことで自分の立ち位置を考えることができた
  • 立ち位置を考えた結果、自分の思いもしなかったところが強みになったりする

テーマ③「SIer経験しておいて良かった~!って思う瞬間」

このテーマではSIerで経験したことが活きている場面についてお話ししました。
f:id:krocks96:20220206210533p:plain

  • (SIerでのポジションにもよるが)要件定義や合意形成といったクライアントワーク
  • 見積を作るときの工数や金額の感覚を得ることが出来た(リスクヘッジで見積のバッファを積みすぎになっているかも)
  • SIerの方が大規模開発だったので、サービスがスケールした時に活きるかもしれない
  • SIerで客先で話す必要があったので傾聴力やコミュニケーション力がついた
  • 組織体制がしっかりしたところにいたので、組織作りをするにあたって経験が活きている

質問コーナー

f:id:krocks96:20220206211336p:plain

じゅーいちさんは東京への移住組と伺っていますが、リモートの仕事文化が定着した現在、東京を出るという選択肢はありますか?

  • 全然ありますね~笑
  • 会社の方針、仕組み次第なところはある
  • 会社に来ても来なくてもいいとなると出社もいいものだなと思うこともある
  • 田舎で働くの憧れません?笑

XR企業に転職してからSIer時代に勉強しておいてよかったと思うことはありますか?

  • 全く業務関係ないところでプログラミングの勉強したり資格試験の勉強したりしていたこと
  • サーバサイドは作ったモノが見えないので、見えるものとしてシェーダーを書いてVRChatにもっていったりしていた
  • UML(クラス図やシーケンス図など)の書き方を学んだこと

XR関連の業務経験がない状態でXR業界に転職するのはなかなかハードルが高いんじゃないかと思いますが、どうやって決断したんでしょうか?

  • 結論としては「やりたかったから覚悟を決めた」になる
  • 面接時点でSynamonの抱えている課題を聞いてやるべきことが見えたのでやれると思った
  • 趣味を仕事にするとつまらなくなるというのが心配だったが、チャンスは今しかないと思って覚悟を決めた
  • iPhoneが出た時に波に乗り損ねた経験があり、XRも同様に時代の変わり目になるのではと思って覚悟を決めた

さいごに

以上、イベントの内容を箇条書き形式でまとめてみました。

ここからは個人の感想です。
まずはじめに、イベントを企画してくださったふわりさん、そして視聴してくださった皆さんありがとうございました。他の方の経験、考えを聞けて非常に興味深かったですし、こうしてパブリックな場で話すことが出来て勉強になりました。次回はもう少し緊張せずに話せたらいいなと思います笑
また、イベント後にアンケートも取らせていただいたのですが「転職時に考えていたことが参考になった」という声や「スタートアップでの仕事の進め方を知ることができてためになった」という声をいただき、開催して良かったなと胸をなでおろしております。

そして全体を通して思ったのは、XR転職に必要なことは「勇気をもって一歩踏み出す」ことではないかということです。パネリストそれぞれで転職に至るまでの道筋は異なりますが、最後の質問への回答でもあったように覚悟を決めて現状を変えるために自らアクションをした結果が現在に繋がっているのかなと思いました。

大きく環境を変えることはリスクもあり難しいものですが、出来ることから少しずつ始めてみてはいかがでしょうか。

簡単に出来ることといえば、Synamonではカジュアル面談を実施しております。

カジュアル面談はMeetyから▼
meety.net

エントリーはこちら▼
herp.careers

また、本テックブログやnote記事のお知らせは、Synamon公式Twitterでも発信もしています。
弊社の取り組みに興味を持っていただけたら、ぜひフォローお願いいたします!
twitter.com

WindowとAndroidでNiantic LightshipのARDK Example Scenesをビルドする

WindowとAndroidでの組み合わせでNiantic light ship ARDKのサンプルシーンをビルドする為の手順を纏めます。
詳しくは公式のガイドを見て頂くのが良いかと思いますが、クイックスタートとして使えるメモ書きがあると便利だなと思い書くことにしました。

ビルド設定以外は駆け足で進めますのでご了承ください。

検証環境

  • Unity 2021.4.34f1
  • ARDK 1.1.0(ardk-1.1.0.unitypackage)
  • ARDK Example Scenes (ardk-examples-1.1.0.unitypackage)
  • Android11 (Xperia 1III)

手順

  1. Unityにて3Dプロジェクトを作成
  2. Build SettingにてPlatformをAndroidにSwitchします。
  3. ardk-1.1.0.unitypackageをImport(メニューより Assets > Import Package)
  4. ardk-examples-1.1.0.unitypackageもImport
  5. サンプルシーンを開く(例:Projectより Meshing > ARDKExamples > ContextAwareness > Meshing)
  6. マルチプレイ機能を使用するサンプルの場合はArdkAuthConfig.assetにライセンスキーを設定する(Meshingなどマルチプレイを伴わないサンプルでは不要)
  7. Build SettingにScenes In Buildにビルドするシーンを追加(既存でシーンが設定されている場合は外す)
  8. Project Settingの Player > Android > Other Setting を以下の様に設定します

f:id:Q1J011:20220126173812p:plain

設定が出来たらビルド! Android端末に入れて動作確認をしてみてください。

書き出してみるとそれほど複雑でもないですし、慣れればさくさく進められると思います。
公式のマニュアルも丁寧に書かれているので疑問点があればぜひ参考にしてみてください。

lightship.dev

GUIにおける状態管理の考え方について

はじめに

エンジニアの松原です。業務以前に個人開発でUnityを触っていたのですが、まずはサードパーティ製ライブラリを取り込んでから開発がスタートするのが多かったと思います。(分かりやすい例として、Oculus IntegrationUltraleap Plugin For Unityを真っ先に入れていた気がしています)

当時の個人開発のアプリではあまりアプリケーションの設計を考えておらず、UnityのScene間でデータをやり取りすることが少なく、何かしらの処理は理由もなく多数のクラスや関数に依存していました。 業務でUnityを使った開発をするようになってからは、サーバーサイドのAPIと連携するようになったため、個人開発でもある程度設計を意識するようになりました。

今回は設計のうち、GUIにおける状態管理に対する考えのパターンについて記事にしてみました。(Unityの話だけでなく、Webフロントエンド開発で使われているトピックも入ってきます)Unityで実際にコードを書くことに関してはまた別の機会で行いたいと思います。

今回の記事の注意として、UnityのAnimator Controllerなどで登場するStateはFSM(Finite State Machine)と呼ばれるステートマシンで、同じ状態を表していますが、扱うトピックが異なるため、今回は登場しません。

なぜGUIに状態管理が必要なのか

Unityのみならず、GUIを扱う場合、以下の課題に遭遇することが多いと思います。

  • ステートレスなサーバーサイドAPIとやり取りした結果をGUIに反映したい
  • 複数のUIのコンポーネントやクラス間で共有したいプロパティがある
  • 画面の切り替え(またはUnityのシーン)の切り替え時に変更したプロパティを維持したい

これらの課題を応急処置的なコードを使って開発をすすめていくと、複雑な依存関係が増えてきます。依存関係が増えるということは状態の扱いが難しくなり、設計の変更も容易でなくなってきます。

そこで状態を管理しているオブジェクトをいくつかの設計パターンに切り出して考えてみます。

状態管理を外部オブジェクトとして設置する(Storeパターン)

共有化したい状態をStoreというグローバルなオブジェクトを利用して管理する方法を紹介します。 こちらのパターンはWebフロントエンド開発のJavaScriptフレームワークであるVueの公式ページで紹介されている状態管理のパターンを参考にしたものになります。

jp.vuejs.org

Vue公式の方ではObserverの記述は省略されているのですが、状態管理にリアクティブなプロパティが利用できる、もしくはStateを監視する仕組みをVueでは暗黙的に利用することが多く、JavaScriptフレームワーク以外で一般的に説明するためObserverを書き加えています。

f:id:fb8r5jymw6fd:20220124005617j:plain:h500

上の図に追加して、State書き換え時の処理ギミックを加えたものが以下の図になります。ここのギミックに関しては実装方針でも変わってきますが、最低限イメージしやすい内容にしています。

初期値の設定まわりは省略していますが、Stateの更新サイクルとしては以下のようになります。

  1. GUIパーツの操作(ボタン押下、スライドの値変更時など)に紐づいたトリガーが発火時、そのトリガーに対応する処理が呼び出される
  2. 処理が実行され、Stateの変更がある場合はStateの変更処理を行う命令を呼び出す
  3. Stateの変更命令を受け取り、保持しているStateを書き換える
  4. Stateの書き換えを検知する
  5. 変更されたStateに依存しているPresenterやViewに対して変更を通知する

f:id:fb8r5jymw6fd:20220124173631j:plain:h500

上記で登場したPresenterやViewは MVC / MVP パターンで登場する概念ですが、Unityで分かりやすい例えを考えてみます。
UnityではuGUIを利用してGUIを組むことが一般的だとは思いますが、Store内でStateが更新されても、画面上に反映するためにはGUIコンポーネントの表示部をスクリプトから更新する必要があります。

下図のTextコンポーネントも同様で、このコンポーネントのTextフィールドに対して直接更新処理をかけないと画面上に変更が反映されません。このように、特定のStateに依存しており、Stateの変更に伴ってState変更処理以外の手続きを行う必要がある部分の実装をViewと捉えると分かりやすいかと思います。このStateの書き換え後のギミックを仲介する役割がObserverとなります。

f:id:fb8r5jymw6fd:20220124175051j:plain:h500

状態管理を行うモダンなアーキテクチャを考える(Fluxパターン)

上記のように状態管理をしつつ、処理の依存関係を極力減らすために、処理の構造を単方向にするFluxというコンセプト(またはアーキテクチャ)があります。 現在はFacebookのGitHubにアーカイブとして残っています。このコンセプトを基に実装・派生したReduxMobXを使うことをFacebookは推奨しています。

github.com

Fluxのコンセプトの実装例として以下のようにまとめることができます。先ほど紹介したStoreパターンではStateの書き換えは外部から参照、命令を呼び出すことで直接書き換えができていましたが、こちらは役割をさらに細分化して処理を単方向に扱えるようにしています。 それぞれ以下の役割を持っています。

  • Action - 具体的な処理を記述するためのパターンで、処理単位のテンプレートとして定義されている。成功や失敗などの状態変更の起点を持つことができる
  • Action Creator - Actionを作成する責務を持つ
  • Dispatcher - Action Creatorから作られたActionをReducerに送る責務を持つ(実装によっては省略されることもある)
  • Reducer - Actionを実際に実行し、Stateを書き換える責務を持つ。StateはReducerからでしか書き換えができない
  • State - 保持したい情報の単位または入れ物
  • Watcher(Observer) - Stateの更新を監視し、依存しているコンポーネントに更新を通知する

f:id:fb8r5jymw6fd:20220124192212j:plain:h500

Fluxの実装によっては定義されている言葉が違うことがありますが、かねがねそれぞれの役割としては上記のようになっていることが多いと思います。 Fluxではより役割を厳格に定めることによって、双方向の依存関係を無くすように設計されたアーキテクチャであるといえると思います。

今回のまとめ

今回はWebフロントエンドで登場する状態管理の考え方をJavaScriptフレームワークで利用されている状態管理のパターンを取り上げました。

これらを実際にUnityで扱うにはUniduxなどがありますが、こちらは画面遷移も取り扱っているため、今回はGUIでの状態管理について解説したかったので、また別の機会に紹介したいと思います。 github.com

また、StoreパターンやFluxパターンは MVC / MVP や MVVM のようにアプリケーションの設計に深く根付いているというより、状態管理という責務分担が難しいものをうまくやりくりするためのコンセプトのようなので、派生形のReduxやMobXもどこかの機会で追ってみたいと思います。

Apple Watch(+α)のxR開発で使う座標系を調べた

エンジニアの岡村です。

先月、Synamon初のアドベントカレンダー企画を行いました(無事完走!)。その中で自分は仕事で触っていない技術へのチャレンジを行い、ほぼ初めてのSwiftとApple系デバイスを使った開発を行いました。

synamon.hatenablog.com

自分はUnityやDirectX以外の座標系をあまり触ったことがなく、開発の際に座標系に少し戸惑ったので、この記事に軽く調べてみた情報を纏めておきます。

座標系の種類

座標系には大きく分けて右手系と左手系が存在します。それぞれの座標系は、親指をX軸、人差し指をY軸、中指をZ軸とし、各指が90度で直交する形を作り、回転することでその形と一致するかどうかで確かめることができます。これらの座標系が異なる場合、回転により一致させることが出来ず、相互変換する際に座標の解釈を変更する必要があります。

例えば、Unityの座標系は左手系であり、Blenderの座標系は右手系です。BlenderとUnityを使ってコンテンツを作成している方は、いつもこの座標系の違いを乗り越えているんですね。

また、座標系にはどの軸を上方向とするのか(Y-up、Z-up)という基準もあります。しかし、デバイスの座標系を考えたとき、机に置いた状態の上方向と、立てて使用している時の上方向など、その瞬間におけるデバイスの状態によって解釈が異なる為、この基準を簡単に適用することはできません。代わりに、デバイスのどの方向が何軸に対応しているのかを明示しておきます。

Apple系デバイスの座標系

Apple系デバイスのネイティブアプリを開発する際、モーションセンサーの値を取得するにはCoreMotionというフレームワークを利用します。このフレームワーク内において利用されている座標系は右手系となっており、これは公式ドキュメント内のこちらのページで確認することが出来ます。

https://developer.apple.com/documentation/coremotion/getting_processed_device-motion_data/understanding_reference_frames_and_device_attitude

また、この図によると、ディスプレイ正面方向がZ+ディスプレイ上方向がY+となっており、これが画面が付いてるデバイスにおける標準となっているようです。

f:id:Sokuhatiku:20220117181330p:plain:w300

Apple Watch

Apple Watchでも基本的には同じです。ただし、Apple Watchは左右どちらの手首に装着するか、Digital Clownの向きを左右どちらにするかで4通りの装着方法が考えられます。

f:id:Sokuhatiku:20220117191956p:plain:w300

ですが、どの向きでもディスプレイ正面がZ+Digital Clownのある方向がX+となっています。Digital Clownの向きを変えると画面の上下が反転しますが、CoreMotionにおける軸方向は変化しません。つまり左にDigital Clownが来るような状態にした場合、腕を下に下げるとデバイスのY+方向に加速度がかかることになります。

f:id:Sokuhatiku:20220117193118p:plain

ちなみに

今まで画面が付いているデバイスの話をしていましたが、画面がついていないAirPodsもCoreMotionでモーションセンサーの値を取得出来ます。ただし、AirPodsの場合は、回転量の差分のみが取得可能なようです(3DoF)*1。この場合はデバイスの面と軸の対応を考えることは出来ません。

おまけ(Android)

ついでにAndroidにおけるセンサーの座標系も調べてみました。公式情報はこちらのページにあります。 https://developer.android.com/guide/topics/sensors/sensors_overview?hl=ja#sensors-coords

f:id:Sokuhatiku:20220117193512p:plain:w300

座標系はAppleと同様に右手系ディスプレイ正面がZ+ディスプレイ上がY+ となっているようです。

おわりに

基本的にUnity等のゲームエンジンを使っている限り、それ用に用意されたライブラリが座標系を合わせてくれるのが凄く有難いです。しかし、何らかの事情でネイティブ開発をする必要が出てきた場合、座標系の違いは(特にドキュメントが充実していない場合)大変になるでしょう。それに備えて知識だけは持っておきたいと思います。

Unity2021でVRテンプレートを使ってOculusQuest2の開発環境を構築してみる

こんにちは、Synamon のエンジニアリングマネージャーの渡辺(@mochi_neko_7)です。

自分はエンジニアではあるのですが、肩書きにもあるようにここ1~2年くらいは開発以外の仕事(EM、採用)をすることがほとんどで、実は最近のバージョンのUnityを触れていませんでした。

ですのでリハビリも兼ねて、最新のUnity正式版であるUnity 2021の環境でVRテンプレートを使用して、VRの開発環境をゼロから構築してみたいと思います。

読者の対象としては、以下を想定しています。

  • Unityは触ったことあるが、VRの開発は初心者の方
  • VRの開発をここ1~2年くらい触っていない方

今回の開発環境は以下で構築していますが、バージョンに依って内容に変更があり得ることにご注意ください。

  • Windows 10
  • Unity 2021.2.7f1

ちなみに今回はデバッグのしやすいOculus Linkも使用するのでWindowsを使用していますが、 Quest2(Android)のみで動作確認する場合はMac等でも開発可能かと思います。

またUnity自体の基本的な説明、Unity以外のソフトウェアの設定、ハードウェアに関する説明などは割愛させていただきます。

Unityのインストール

まずはUnity Hubを使ってUnityのインストールをします。

unity3d.com

Unity Hubでは、インストール > インストール(右上の青いボタン) > バージョンを選択 > 追加するモジュールを選択 します。

ここで使用するUnityのバージョンは、2022/01/11で最新の正式版である Unity 2021.2.7f1 とします。(LTSではないです)

念のためですが、実際に製品開発をされる方は (LTS) の付くバージョンを使用することが推奨されていますのでご注意ください。

f:id:mochinekos:20220111104604p:plain
Unityのインストール

追加するモジュールでは以下を選択しておきます。

  • Android Build Support
    • Android SDK & NDK Tools
    • OpenJDK
  • Windows Build Support (IL2CPP)

特に前者のAndroid向けのモジュールは最終的にOculus Quest 2単体で動作させたい場合には必須になります。

f:id:mochinekos:20220111105505p:plain

プロジェクトの作成

Unity Hubでプロジェクトの新規作成を行います。

プロジェクト > 新規作成(右上の青いボタン、の右の▼のボタン) > 2021.2.7f1

右の▼からバージョンを指定するのにご注意ください。

プロジェクト名、保存先は好きに変えてください。

ここで、テンプレートでVRを選択しましょう。(初めての場合はダウンロードが必要です)

f:id:mochinekos:20220111110841p:plain

このVRのテンプレートはVR開発向けに必要なUnity Packageの追加やプロジェクトの設定が適用されているものになります。

これを使用することでそれらの初期設定の手間を省いて、手軽にプロジェクトの作成ができるようになっています。

f:id:mochinekos:20220111111820p:plain

Unityパッケージの確認

画面上のタブ > Window > Package Manager

からPackage Managerを開きます。

VRのテンプレートではXR開発向けのパッケージがデフォルトで追加されているようです。

  • Oculus XR Plugin
  • Windows XR Plugin
  • XR Plugin Management

f:id:mochinekos:20220111112051p:plain

それぞれのバージョンは最新に更新しておいて基本的には問題ないと思いますので、以下のバージョンを使用します。

  • Oculus XR Plugin > 1.11.2
  • Windows XR Plugin > 4.4.1
  • XR Plugin Management > 4.2.1

プロジェクトの設定

今回使用するOculus Quest 2向けに設定を少しだけします。

画面上のタブ > Project Settings > XR Plug-in Management

を開きます。

Oculus Linkを使用するためには、PC向けの設定で Oculus にチェックを入れます。

f:id:mochinekos:20220111113339p:plain

Quest2単体で動かすためには、Android向けの設定で Oculus にチェックを入れます。

f:id:mochinekos:20220111113507p:plain

Editor上で動作確認

Oculus Linkを使って、Editor上で動作確認してみましょう。

Oculus Linkのセットアップはまず公式のこちらをご覧ください。

support.oculus.com

使用するOculus Quest 2は開発者モードに設定しておきましょう。

https://developer.oculus.com/documentation/native/android/mobile-device-setup/?locale=ja_JP

VRテンプレートにはサンプルである SampleScene があり、初回起動時に展開されているのでこちらを使ってみましょう。

Linkを起動した状態で、Unityで再生をします。

うまくつながっている場合、このように白い空間で頭と両手のトラッキングがされているのが分かります。

f:id:mochinekos:20220111140855p:plain

ビルド

同じサンプルシーンが正常にビルドでき、実機で動作することを確認してみましょう。 今回は2通りの方法で動作確認します。

  • Windows向け(Oculus Link)
  • Android向け(Quest2単体)

Windows向け(Oculus Link)

画面上のタブ > File > BuildSettings を開き、Add Open Scenes を押して SampleScene を追加して、 Windowのプラットフォームになっていることを確認してから、Build を押します。

エラーが発生しなければビルド成功です。

指定したフォルダの プロジェクト名.exe のファイルを起動すると、先ほどのEditorと同じ動作をするアプリケーションが立ち上がるかと思います。

Android向け(Quest2単体)

同じくBuild Settingsから、一度PlatformをAndroidに変更します。

Platform > AndroidTexture Compression を推奨の ASTC に変更しておき、それから画面下の Switch Platform を押します。

それから Build もしくは Build And Run を押します。

確認方法はいくつかあるのですがどれでも構いません。

  • Build And Run でビルド後に直接実機で動かす
  • Build してできた .apk ファイルを
    • Side Questでインストールする
    • Android Studioを入れてADBコマンドでインストールする

サンプルがちゃんと動いてトラッキングがうまくされている、画面がちゃんと表示されているのが確認できればOKです。

コントローラーの入力の取得方法

SampleScene を見ると、XR Rig というオブジェクトの下に Camera OffsetRightControllerLeftControllerの3つの子オブジェクトがあり、これらが頭・両手のコントローラーのトラッキングを再現していることが分かります。

つまり、既にトラッキングの情報は取得できています。

後はQuest2のコントローラーの入力の取得さえできれば、最低限基本的な開発ができるでしょう。

こちらの記事

synamon.hatenablog.com

にもあるように、Unityの(New)Input SystemではOculus Quest 2のコントローラーの入力も取得することができます。

詳しくはこちらの記事を参考にしてください。

jmpelletier.com

ちなみに 入力イベントを取得する Player Input でのイベントの戻り値は UnityEngine.InputSystem.InputAction.CallbackContext のようです。

まとめ

以上でできるようになったことをまとめます。

  • 動作
    • UnityのEditor上で、Oculus Linkで動かす
    • Windowビルドして、Oculus Linkで動かす
    • Androidビルドして、Quest2単体で動かす
  • 入力
    • 頭と両手のトラッキングができる
    • コントローラーのボタン等の入力が取れる

ここまで来れば、後は作りたいコンテンツを作るだけですね。

さらっとここまで来ていますが、1~2年前までは他にも

  • Oculus IntegrationなどのSDKをインストールする
  • Android SDKまわりを手動でセットアップしてビルドを通す
  • Oculus Quest 2向けのビルド設定を一つずつ修正する

などをしながら実機で動かすところまでやる必要があり、地味に大変でした。

それが今では、

  • XR Plugin Management & New Input System → OculusのSDKまわりをうまくラップしてくれている
  • Android向け追加モジュール → Android SDKのセットアップまわりをうまくやってくれている
  • VR Template → Quest2向けの細かいセットアップを自動でやってくれている

のように整備されていて、開発環境を構築するまでのハードルがかなり下がっていることが分かります。

また、基本的にQuest2向けに設定したのは XR Plugin Management の部分と、コントローラーの取得の部分だけなので、XR Plugin Managementが対応している範囲なら他のHMDもほぼ同じように対応できると思います。(確認はしてませんがOpenXRの設定欄もありますし)

あと今回はOculus Integrationを自分では入れていないので、このままGitHubに上げてもライセンス上問題ないはずで、GitHubでPublicに開発するハードルも下がるかと思います。

その他、注意事項としては以下を挙げておきます。

  • Unity2021のバージョンはまだLTSではないので、今プロダクションで使用するならUnity2020.3が推奨
  • ちゃんと確認したわけではないのですが、Oculusの最新のSDKを使用したいなら、おそらく手動でOculus Integrationを入れる必要がありそう
  • これも時間がなくて確認できていないのですが、Unityの提供しているUnity Package Oculus XR Plugin がOculusの公式のSDKである Oculus Integration とどうバージョン対応しているのかわからず
    • (2022/01/17追記)新しめのバージョンでは、Oculus Integrationのv33には対応しているようなので、それ以上新しいバージョンを使用したい場合のみ手動でどうにかする必要がありそうです

以上、これからOculus Quest 2を使って新しく開発を始めてみたい方に参考になれば幸いです。

Synamon Advent Calendar 2021完走しました!

あけましておめでとうございます! 🎍
エンジニアリングマネージャーの佐藤(@unsoluble_sugar)です。

新年1記事目ということで、年の瀬のアドベントカレンダーについて振り返ってみます。

アドベントカレンダー戦績発表

2021年12月に実施したSynamon Advent Calendar 2021ですが、見事に完走いたしました!!

f:id:unsoluble_sugar:20220102155436j:plain

なんと、Synamonとしてのアドベントカレンダー初挑戦にもかかわらず、全25記事すべて埋まるという快挙…!

参加していただいたエンジニア、デザイナーメンバー、そして記事のシェアに協力いただいた皆さんの素晴らしい成果です!👏👏👏

記事一覧

せっかくなので本記事でもアドベントカレンダーに投稿された25記事の一覧を列挙しておきます。気になる記事があればぜひ読んでください!

  1. テックブログ運用を回すための取り組み 〜黄金の回転編〜
  2. Unityでのマルチプレイアプリ開発を便利にしてくれるEditor拡張-ParrelSyncを深ぼってみる
  3. Oculus GoのアンロックOSビルドを入れてみた
  4. Netcode for GameObjectsの機能紹介 NetworkTransformコンポーネントについて
  5. Blenderでオブジェクトを統合するとテクスチャが表示されなくなる問題
  6. ラブライブに学ぶリーダーシップ
  7. だれでもできる!Nreal Lightアプリ作成入門
  8. VR企業はこうやって引っ越しをする!!
  9. AR Foundationでユニティちゃん召喚スマホアプリを作ってみた
  10. HubsCloudをDocker環境で動かしてみる
  11. Netcode for GameObjectsでプレイヤー間で同期する物体の生成 / 破棄
  12. NaughtyAttributesの実装を読み解く
  13. VRoid Studioでメタバース時代のオリジナルアバターを自作したい!
  14. HaritoraX完全に理解した
  15. AppleWatchをハンドトラッキング補助デバイスとして使えないか試してみた
  16. .NET 6 MAUI(Preview)をWindowsとMacで触ってみたかった
  17. インフラ周りで直面した課題と対応策について登壇しました
  18. Netcode for GameObjectsでMobをスポーンさせる
  19. 3Dモデリング未経験のエンジニアがBlender入門してみた
  20. PlayMakerとBehaviourTreeを使ってステートマシンにイベントハンドリングをできるようにしてみた
  21. Addressable Asset System入門
  22. Bledner上での確認にLooking Glass Portraitを使う
  23. Houdini Engine For Unityを色々試してみる
  24. SIer経験者がXR業界に入って感じたこと
  25. 修正に強いモデリング方法

「ネタ記事でも大丈夫ですよ!」とは言っていたものの、皆さんわりと真面目な記事を書いてくださった印象ですw

皆さんの得意領域や今回の機会を利用したチャレンジを見ることができて個人的にも嬉しいです!

掲載媒体の内訳について

今回はアドベントカレンダー初挑戦かつ、まだテックブログに書いたことのないメンバーも居たこともあり「Qiitaやnoteなど他媒体でも書いてOK」という体制を取りました。純粋にテックブログ向きではないテーマもありますからね。

投稿媒体の内訳は以下のとおり。

  • テックブログ:15記事
  • note:6記事
  • Qiita:3記事
  • Zenn:1記事

Synamon公式noteにはデザイナーマガジンがあるため、デザイナーさんはこちらに掲載いただいたケースもありました。エンジニア以外に向けた情報発信はnoteが適していますね。

note.synamon.jp

それでも25記事中、15記事もテックブログで書かれたのは好成績です。12月のPV数も年間最高値が出ていました。

f:id:unsoluble_sugar:20220102183154j:plain

右肩上がりで気持ちの良いグラフですね!
公開した記事数自体が多く、目にしていただける機会も一段と多かったようで何よりです。

2022年もよろしくお願いします

昨年の後半から体制を整えだしたテックブログ運営ですが、アドベントカレンダーも無事に完走を果たすことができました。参加いただいた皆さん全員がバリューを体現したと言えるでしょう。

f:id:unsoluble_sugar:20220102171135j:plain

今回書かれた記事はストック型の技術情報として、今後も長く読まれることを期待しています。

Synamonで「技術的にどのような取り組みを行なっているか」「どんなことに興味のあるメンバーが在籍しているか」アドベントカレンダーの記事を通して、少しでも多くの方に伝われば幸いです。

synamon.hatenablog.com

2022年もSynamon’s Engineer blogをよろしくお願いいたします!

最後に

本テックブログやnote記事のお知らせは、Synamon公式Twitterで発信しています。弊社の取り組みに興味を持っていただけましたらぜひフォローお願いします!

twitter.com

カジュアル面談も実施中ですので「詳しく話を聞いてみたい!」という方はチェックいただけると嬉しいです。

▼カジュアル面談はMeetyから   meety.net  

▼エントリーはこちら   herp.careers

UnityのTimelineを利用してGUIのトランジション効果を作ってみる

f:id:fb8r5jymw6fd:20211226110009p:plain

はじめに

UnityでGUIを構築する際、uGUIや新しい機能であるUI Toolkitが利用できますが、画面遷移のアニメーションのふるまいなどはユーザー側に任せられています。ユーザーで用意するぶん、いくらでもカスタマイズできますが、ギミックやアニメーションの処理を用意するのに手間な部分でもあります。

私はUnityのTimelineについて触れる機会が少なく、これまであまり詳しいことが分かっていませんでしたが、Timelineはスクリプトからの操作やプロパティの変更ができる箇所も多く、カットシーンやイベントシーン以外の用途でもTimelineが利用できそうということが分かりました。今回その一例としてGUIの画面遷移(トランジション)の処理をTimelineを使って行うことを記事に書きました。

今回紹介する内容は個人のGitHubにサンプルのプロジェクトファイルを置いていますので、触っていただけると嬉しいです。

事前準備

今回の記事では以下のUnityのバージョンとパッケージを利用しています。

  • Unity 2020.3.25f1
  • Timeline 1.4.8
  • Input System 1.2.0
  • Unity UI 1.0.0

対象のGUIパネルの作成とAnimatorコンポーネントの追加

トランジション効果を持たせたいパネルを作成し、それぞれAnimatorコンポーネントを追加します。これはAnimationとして動かす際に必要になります。Animator ControllerやAvatarはNoneのままにしておきます。

f:id:fb8r5jymw6fd:20211226095217p:plain

 

また、以下のセクションで登場するAnimationはTransformを持つGameObjectから作成する必要があり、RectTransformとTransformの原点座標を揃える必要があります。GUIパネルとして利用したいGameObjectのRectTransformの以下のように設定します。

Anchor Presetsは下図のように[Middle Center]にします。(AnchorsのPivotは x: 0.5, y: 0.5に設定)他設定は上図のAnchorsを参考にしてください。

f:id:fb8r5jymw6fd:20211226095030p:plain

 

Animationを作成する

まずは遷移元になるAnimationと遷移先になるAnimationをそれぞれ作成します。Projectパネルから任意のディレクトリで右クリック、 Create > Animation から2つほどAnimationを作成します。

f:id:fb8r5jymw6fd:20211225173855p:plain

 

それぞれのAnimationに適当な名前を付けたら、Hierarchyに新しくGameObjectを2つ分用意し、先ほど作成したAnimationをD&Dで追加します。

f:id:fb8r5jymw6fd:20211226112331p:plain

 

Projectパネルから作成したAnimationの一つをダブルクリックしてアニメーションを編集します。

f:id:fb8r5jymw6fd:20211225175957p:plain

 

ここで一度Hierarchyから対象のAnimationが追加されているGameObjectを選択します。この操作をしないと右側のAdd Propertyのボタンが押せないので注意してください。

続けてAdd Propertyを押します。

f:id:fb8r5jymw6fd:20211225180325p:plain


追加するプロパティは Transform > Position で、Transformの「▷」マークを押すことで候補が出てきます。右側の「+」マークを押して追加します。

f:id:fb8r5jymw6fd:20211225180711p:plain

 

デフォルトでキーフレームが0:00の箇所と1:00(1秒相当)の箇所に入っていますので、末尾のキーフレームのマーカーをドラッグしてを0:30の位置に動かします。(ここは任意の時間でも大丈夫です)

f:id:fb8r5jymw6fd:20211225180905p:plain

 

キーフレームを上書き編集するため、Recordingボタンを押します。

f:id:fb8r5jymw6fd:20211225181057p:plain

 

一つの目のアニメーションは画面外にスライドするアニメーションを作るので、最後のキーフレームを選択します。

f:id:fb8r5jymw6fd:20211225181328p:plain

 

横の数値はCanvas Scalerの画面幅を基準に設定します。左側へのスライドのため、マイナスの数値が入っています。下記数値と異なる場合は自身のCanvasScalerの数値を基準に入れてください。

f:id:fb8r5jymw6fd:20211225181529p:plain

f:id:fb8r5jymw6fd:20211225181800p:plain

 

最初のキーフレームの値と最後のキーフレームの値が以下のようになっていればOKです。

f:id:fb8r5jymw6fd:20211225182301p:plain

 

もう一方の方は右側の画面外からフレームインするように、終わりのキーフレームの位置を0にするように設定します。

f:id:fb8r5jymw6fd:20211225184140p:plain

 

編集が終わったら、使用したGameObjectと、自動生成されたAnimator Controllerは不要になりますので削除します。

f:id:fb8r5jymw6fd:20211225184415p:plain

f:id:fb8r5jymw6fd:20211225184548p:plain

 

Timelineを作成する

次にTimelineを作成します。Projectパネル上で右クリックし、 Create > Timelineを選択します。

f:id:fb8r5jymw6fd:20211225184649p:plain

 

名前をわかりやすいものに変更しておきます。

f:id:fb8r5jymw6fd:20211226103050p:plain

 

Hierarchyに空のGameObjectを作成し、そのGameObjectにPlayableDirectorのコンポーネントを追加します。

f:id:fb8r5jymw6fd:20211226065048p:plain

 

Inspectorから先ほど作成したTimelineをPlayableDirectorのPlayableの箇所にD&Dします。ついでにPlay On Awakeのチェックも外して、実行時自動的にTimelineが再生されないようにします。

f:id:fb8r5jymw6fd:20211226065423p:plain

 

さきほど作成したTimelineをダブルクリックしてTimelineのパネルを開きます。

f:id:fb8r5jymw6fd:20211226064821p:plain

 

Timelineのパネルが開いたら、一度HierarchyのPlayableDirectorをアタッチしたGameObjectを選択し、Timelineパネルをロックしておきます。

f:id:fb8r5jymw6fd:20211226065725p:plain

f:id:fb8r5jymw6fd:20211226065855p:plain

 

Animation Trackを作成する

Timelineの左側のところで右クリックし、メニューからAnimation Trackを二つ用意します。それぞれ遷移元のGUIパネルのアニメーション、遷移先のGUIパネルのアニメーション用に利用します。

f:id:fb8r5jymw6fd:20211226065944p:plain

 

作成したAnimation Trackをリネームします。Timelineの画面上に追加された項目の、枠の部分もしくは、下図のようにアイコンの部分をクリックすることで選択できます。

f:id:fb8r5jymw6fd:20211226070353p:plain

 

リネームはInspectorから行います。ついでにApply Avatar Maskの箇所のチェックを外します。

f:id:fb8r5jymw6fd:20211226070547p:plain

 

もう一方のAnimation Trackも同じようにリネームします。

f:id:fb8r5jymw6fd:20211226070649p:plain

 

AnimationTrackにAnimationを追加する

それぞれのAnimationTrackに作成していたAnimationを追加します。Animationは任意の位置に追加すればよいですが、今回の画面遷移の仕組みではアニメーションの開始と終了がそろっていた方が良いです。

マウスホイールを使ってTimelineの縮尺を変えることができるので、追加しづらいときは縮尺を変えてみてください。

f:id:fb8r5jymw6fd:20211226071107p:plain

 

追加したTimelineパネル上のAnimationをクリックし、Inspectorから操作を行います。

f:id:fb8r5jymw6fd:20211226071359p:plain

 

Animation Playable Asset内のにある、Remove Start Offsetの項目のチェックを外します。Remove Start Offsetのチェックを外しておくことで、Animationに設定したPositionのアニメーションが絶対位置として反映されるようになります。今回の仕組みには必須です。

Remove Start Offsetの項目は通常のTransformを持ったGameObjectから作ったAnimationのみ利用できます。GUIのようにRectTransformを持ったものからAnimationを作るとこの項目は表示されないので注意してください。*1

f:id:fb8r5jymw6fd:20211226072507p:plain

もう一方のTimeline上に配置したAnimationも同じようにRemove Start Offsetのチェックを外しておきます。

 

アニメーションをチェックする

一度アニメーションの挙動を確認してみます。HierarchyのPlayableDirectorをアタッチしたGameObjectを選択し、Inspectorを操作します。
f:id:fb8r5jymw6fd:20211226065725p:plain

 

PlayableDirectorコンポーネントのBindingsの項目にAnimationTrackが追加されています。それぞれの箇所にHierarhy側のトランジションさせたいGUIを持つGameObjectをセットします。

f:id:fb8r5jymw6fd:20211226073320p:plain

 

Timelineパネルから再生ボタンを押すか、数字目盛の箇所をドラッグして動きを確認します。

f:id:fb8r5jymw6fd:20211226074032p:plain

f:id:fb8r5jymw6fd:20211226134942g:plain

 

うまく動かないときは、Animationの確認、AnimationTrackの確認、Previewのボタンを押したり、パネルのロックを外して再選択してみるなどの操作を行ってください。

f:id:fb8r5jymw6fd:20211226073846p:plain

 

確認が終わったらPlayableDirectorコンポーネントのBindingsから先ほどセットしたGameObjectを外し、Noneに戻しておきます。

f:id:fb8r5jymw6fd:20211226074955p:plain

 

SignalAssetを作成する

次にSignalAssetを作成します。このSignalAssetはTimeline上での再生位置、再生終了の検出のために利用します。Projectパネル上で右クリックし、 Create > Signal を選択しSignalAssetを作成します。

f:id:fb8r5jymw6fd:20211226075220p:plain

 

二つぶん作り、それぞれ 「Start~」、「End~」という感じに名前を付けます。これはトランジション開始、終了をそれぞれ表すためにつけています。このルールは後で必要になってきます。

f:id:fb8r5jymw6fd:20211226075637p:plain

 

次にTimelineにこのSignalAssetをマーカーとして登録します。

マーカーを見えるようにするため、Timelineの数字目盛の箇所を右クリックし、 Show makersを選択し、Markersの項目を表示します。

f:id:fb8r5jymw6fd:20211226082002p:plain

 

SignalAssetを追加し、マーカーとして登録します。それぞれ遷移アニメーションの開始位置、終了位置にマーカーを配置します。

f:id:fb8r5jymw6fd:20211226082517p:plain

 

SignalReceiverコンポーネントを追加する

PlayableDirectorのComponentが追加されているGameObjectにSignalReceiverコンポーネントを追加しておきます。SignalReiverコンポーネントは追加するのみで、このコンポーネントに対してはInspectorからは何も操作しません。(後でスクリプトで操作できるようにするためです)

f:id:fb8r5jymw6fd:20211226082937p:plain

 

トランジションを行うためのスクリプトを追加する

さらに以下のようなスクリプトを作成し、PlayableDirector、SignalReceiverを持っているGameObjectにコンポーネントに追加します。サンプルではMonoBehaviour継承クラスで、TimelineTransitionという名前にしています。

f:id:fb8r5jymw6fd:20211226091726p:plain

 

ソースコード全体はこんな感じです。今回本文が長いため、ソースコードの解説は割愛させて頂きます。StartTransition()のメソッドからトランジション効果を呼び出すことができます。

ギミック自体はとても面白いので、Playable APIを含めて今後別の記事で紹介できるかもしれません。

#nullable enable

using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.Playables;
using UnityEngine.Timeline;

[RequireComponent(typeof(PlayableDirector), typeof(SignalAsset))]
public class TimelineTransition : MonoBehaviour
{
[SerializeField]
private PlayableDirector? playableDirector;
[SerializeField]
private string startMarkerPrefix = "Start";
[SerializeField]
private string stopMarkerPrefix = "End";
[SerializeField]
private string sourceTrackName = "TransitionA";
[SerializeField]
private string destinationTrackName = "TransitionB";

private readonly Dictionary<string, double> startMarkerTimes = new Dictionary<string, double>();

private TrackAsset? sourceTrackAsset;
private TrackAsset? destinationTrackAsset;

public IReadOnlyList<string>? StartMarkers => startMarkerTimes.Keys.Any() ? startMarkerTimes.Keys.ToList() : null;

private GameObject? targetObjectToDisable;

public void StartTransition(
Animator? sourceAnimator,
Animator? destinationAnimator,
string transitionName,
bool disableSourceAfterTransition = false,
bool enableDestinationBeforeTransition = false)
{
if (playableDirector == null) return;
if (playableDirector.state == PlayState.Playing) return;

if (sourceTrackAsset != null && sourceAnimator != null)
{
targetObjectToDisable = disableSourceAfterTransition ? sourceAnimator.gameObject : null;
playableDirector.SetGenericBinding(sourceTrackAsset, sourceAnimator);
}

if (destinationTrackAsset != null && destinationAnimator != null)
{
if (enableDestinationBeforeTransition)
{
destinationAnimator.gameObject.SetActive(true);
}
playableDirector.SetGenericBinding(destinationTrackAsset, destinationAnimator);
}

StartAnimationAtMarker(transitionName);
}

private void StartAnimationAtMarker(string markerName)
{
if (startMarkerTimes.ContainsKey(markerName))
{
if (playableDirector == null) return;
playableDirector.time = startMarkerTimes[markerName];
playableDirector.Play();
}
}

#region MonoBehaviour implements

private void Reset()
{
playableDirector = gameObject.GetComponent<PlayableDirector>();
}

private void Awake()
{
if (playableDirector == null) return;

var stopTimelineEvent = new UnityEvent();
stopTimelineEvent.AddListener(() =>
{
targetObjectToDisable?.SetActive(false);
targetObjectToDisable = null;
playableDirector?.Stop();
});

var signalReceiver = gameObject.GetComponent<SignalReceiver>();

var timelineAsset = playableDirector?.playableAsset as TimelineAsset;
foreach (var marker in timelineAsset!.markerTrack.GetMarkers())
{
var signalEmitter = marker as SignalEmitter;
if (signalEmitter == null) continue;

var signalAsset = signalEmitter.asset;
if (signalAsset.name.Contains(startMarkerPrefix))
{
startMarkerTimes.Add(signalAsset.name, marker.time);
}

if (signalAsset.name.Contains(stopMarkerPrefix))
{
if (!signalReceiver.GetRegisteredSignals().Contains(signalAsset))
{
signalReceiver.AddReaction(signalAsset, stopTimelineEvent);
}
}
}

foreach (var trackAsset in timelineAsset!.GetRootTracks())
{
if (trackAsset as AnimationTrack == null) continue;
if (trackAsset.name.Contains(sourceTrackName))
{
sourceTrackAsset = trackAsset;
}

if (trackAsset.name.Contains(destinationTrackName))
{
destinationTrackAsset = trackAsset;
}
}
}

#endregion
}

 

それぞれのプロパティは下図のような関係持っています。

Start Maker Prefixはアニメーション開始のマーカーを識別するためのキーワード(接頭辞)、Stop Marker Prefixはアニメーション停止のマーカーを識別するためのキーワードとして設定します。このマーカーはSignalAssetの名前を基準にして行っています。

SourceTrackNameとDestinationTrackNameはそれぞれアニメーションをさせたいAnimationTrackの名前を文字列で指定しています。

f:id:fb8r5jymw6fd:20211226094152p:plain

 

トランジション効果を呼び出す処理を追加する

各GUIのボタンに相当するGameObjectにスクリプトを追加します。DestinationAnimatorに切り替え先のパネルをアタッチします。

f:id:fb8r5jymw6fd:20211226104146p:plain

 

TransitionControllerのソースコードはこんな感じです。現在ボタンに設定されている親のGameObjectのAnimatorをsourceAnimatorに指定しています。GUIボタンからはOnButtonClicked()メソッドから呼び出すように定義しています。

timelineTransition?.StartTransition()でトランジション効果を呼び出します。transitionNameのプロパティに指定されている"StartTransition"はTimelineのマーカー名で、アニメーション開始位置として設定しているSignalAssetの名前を入れています。

#nullable enable

using UnityEngine;

public class TransitionController : MonoBehaviour
{
[SerializeField]
private Animator? destinationAnimator;

[SerializeField]
private string transitionName = "StartTransition";

private Animator? sourceAnimator;

private TimelineTransition? timelineTransition;

public void OnButtonClicked()
{
if (timelineTransition != null && sourceAnimator != null && destinationAnimator != null)
{
if (sourceAnimator == destinationAnimator) return;

StartTransition(sourceAnimator, destinationAnimator);
}
}

private void StartTransition(in Animator source, in Animator destination)
{
timelineTransition?.StartTransition(source, destination, transitionName);
}

#region MonoBehaviour implements

private void Start()
{
timelineTransition = FindObjectOfType<TimelineTransition>();

var currentTransform = transform;
while (currentTransform.parent != null)
{
var parent = currentTransform.parent.gameObject;
var animator = parent.GetComponent<Animator>();
if (animator != null)
{
sourceAnimator = animator;
break;
}

currentTransform = currentTransform.parent;
}
}

#endregion
}

ButtonコンポーネントのOnClickイベントに上記スクリプトのメソッドを忘れず追加しておきます。

f:id:fb8r5jymw6fd:20211226105424p:plain

 

他のボタンコンポーネントも同じように、遷移先のGUIパネルを設定します。

 

できたもの

以下の動画が今回できたものになります。

f:id:fb8r5jymw6fd:20211226125314g:plain

 

また、今回の記事には解説していませんが、GitHubサンプルには他の例も入れています。

f:id:fb8r5jymw6fd:20211226125358g:plain

 

終わりに

以前からUnityのTimelineはPlayableTrack をはじめとした独自拡張が可能で、いろいろな用途に応用できそうなのは分かっていましたが、今回改めてその拡張性や新たな可能性を感じることができました!

TimelineそのものはPlayable APIの機能を使っているようなので、こちらへの理解も進めるとさらに広く応用が利きそうです。

お知らせ

弊社では現在様々な職種を募集しています。興味がある方は是非、以下のページを覗いてみてください!

twitter.com

meety.net

herp.careers

 

付録

*1:RectTransformからAnimationを作ろうとする場合、Add Propertyの項目がTransformではなく、RectTransformになっています。

f:id:fb8r5jymw6fd:20211226101751p:plain