【個人メモ】ゲーム個人開発でアジャイル/スクラムを考えてみる

こんにちは、やまだたいし( やまだ たいし (@OrotiYamatano) | Twitter )です。
個人でゲーム開発をしているのですが、完成せず「開発手法の一つであるスクラムの考え方を個人の開発に取り入れられないか」と考え、本記事を作成しました。
名付けてソロスクラムなんちって。
本記事は(途中アドバイス口調ですが)こうしたら良いよ等のアドバイスではなく、私の頭の中を整理するために書いたものですので、
参考にするのは辞めたほうが良いかもしれません。
逆に個人開発では、こうしてるよ等あれば聞いてみたいです。

目次


個人ゲーム開発でゲームが完成しない訳


そもそも、スケジュールやコストを決めるプロデューサー役割が存在せず、
一人でやるとディレクター(ものづくり)視線でゲームを考えてしまい、工数度外視で制作してしまいます。
それで完成すればいいのですが、ゲームを完成させることが出来るのはごく一握りです。
ゲームジャムのように期限が決まっていればまだしも、一人だと「まぁ良いや」とズルズル引き伸ばしてしまいます。
壮大なゲームを作ろうとしすぎたり、ゲームについての案が途中で思いついて収集がつかなくなったり、本当にいいゲームなのか疑心暗鬼になったり……。
いわゆる「エターナる」。

では、完成させるためにはどうすればいいか


シンプルです。終わりを決めてソコで打ち切ればいいのです。
それが出来れば苦労はない!!!シンプルではあるが、イージーでは無い……。

どれだけ途中だったとしても個人制作の場合は自分が終わりだと思えば終わりです。
だとするなら、おわりとなる何らかの区切り(期間や実装機能)を決めて、やれば良い。
しかしながら、クリエイターとしてはどうしても納得いかなかったり、機能追加をしたくなることがあります。

私は前職基幹系Webシステムを作ったりもしていたのですが
Webアプリなら機能面が重視され、ある程度妥協しやすい部分もあります。
しかし、ゲームの場合「音楽」「シナリオ」「ビジュアル」「プログラム」とアート的な側面が大半を占め、こだわり始めるとキリがないです。
また、神は細部に宿るなんて言葉が信じられるゲーム業界では工数度外視の要件定義をひっくり返すレベルの仕様変更も少なくない……。

それなら、ウォーターフォール的な工数設定よりも、運用保守に特化したスクラム的な作り方の方がゲーム開発に向いているのではないかと考えました。
個人で開発する場合も、リリースしてからパッチで追加するなんて事も最近は多くなってきてますからね。

個人開発にスクラムを採用するワケ


もうちょっとだけ、工数管理の話に突っ込んで考えてみます。
実際大型開発やいくつかの保守案件に入って気がついたのですが、前職IT系の職種で働いていた時と比べ、工数管理が意外とゲーム業界は雑だと感じました。
なんというか、自走力が弱いように感じました。
ゲーム業界でも、それぞれ大体どのぐらいの工数が掛かるのか工数出しをしますが、
工数出しをしたとしても、要件レベルで仕様が変わったり作業差し込みが入ったりします。
また、ゲーム系はノウハウに関しては企業秘密にしている部分が多く、様々な知識が行き渡っていません。

ですので、ほぼ同じような一つの機能だとしても「案件ごとに異なる実装をしていたり」「イチから作り直して考えていたり」します。
いわゆる業界全体のフレームワーク的な物が存在せず、
大変な思いをしながら手探りで進むことが多いため予想以上に工数が膨らみます。
とはいえ、ゲーム業界以外でも予想以上に工数が膨らむことは少なくないとは思いますが……。

そうなると、工数がかなり曖昧なものになってしまいます。
その上、工数管理方法や工数だし方法についてもノウハウと同じ様に既存のフレームワーク的な物が存在する会社は少なく、管理者たちは毎回大変な思いをしているように思います。

定量的な作業が推測できないのですから、そりゃ工数も見積もれないに決まっています……。

それなら、既存の古いゲーム業界のやり方に載っかるよりは、Web系などの工数管理の考え方を真似たほうがまだ明確に工数出しが出来るのではないかと思いました。
会社と違って個人開発ならスクラム採用しやすいですしね……。

Web系の個人開発を参考にする


基本的にスクラムベースで考えたいとは思いますが、Web系の個人開発は開発時の工数管理ノウハウを展開しているものが多いです。
まずは、いくつかの工数管理について調べてみようと思います。

Zennを作った人の話。
youtu.be

Mentaを作った人の話。
youtu.be

スケブを作った人の話。
and-engineer.com

どれも、何を作るか明確化し基本機能だけ作って短い期間でリリースされているようですね……。
ゲームについてはリリース後が一番大事だと言われており、マーケティングを行ってからリリースしなければ、
そのゲームがある程度面白くても売れないと聞きます。

とはいえ、開発するモチベーションを維持するためには基本機能を先に作るのはアリに感じます。

実際のスクラムとの差異


因みにアジャイルスクラムについて違いわからない人の為に一言説明すると、
スクラムアジャイル開発の一種になります。
アジャイルと言いつつ、ほとんどの場所がスクラムにも触れて解説されているところが多いです。
スクラムについて詳しく知るならスクラムの本を読んだ方が良いですが、まずはアジャイルについて正しく理解しましょう。

アジャイルについて学ぶところは沢山ありますが、こちらのサイトが概要を理解するのには良さそうです。
agile-douga.tv

本だとアジャイルサムライとかがオススメです。
スクラムについてだと個人的にはScram Boot Camp The Bookがオススメです。

しかしながら、チームで運用するのに特化しているため、このまま個人で使うことは出来ません。

顧客について


個人開発なら顧客は実際にプレイするユーザーと思うかも知れませんが、意見を聞くことは出来ませんので、
実際の所、要件は自分で出すことになり自分が納得すればリリースになります。
つまり、自分自信がユーザーです。(実際のゲーム開発ではパブリッシャーやディレクターがユーザーでしょうか……)
通常発生するやり取りは存在しませんので顧客とのやり取りという部分は存在しないので省いていいと思います。
(誰かに作りたいとか品質について口を出してプレイしてくれる人がいれば、その方をユーザーにしても良いかもですね)

今回は自分がユーザーとして考えを進めていきます。

要件について


開発は不確実性のコーンと呼ばれるプロジェクトが進行するにつれて見積もりのバラツキが存在してしまいます。
本来ならユーザーの要求や要件、ユーザーとの専門性の壁を取り除くことが不確実性のコーンを明確化することが不確実性のコーンは狭く出来ます。
ユーザー=自身になります。専門性の壁はありません。
しかしながら、要件は個人で開発する場合だとしても、自分が自分自身のゲームに求める要件がブレてしまうと、ゲーム開発の期限が無限に延びてしまいます。
例えば、見た目は二の次にしようと思っていたのに、細やかな見た目が気に入らなくて、何回も絵やプログラムを修正したりなどがソレに当てはまります。
最初に何が重要かしっかり要件定義が必要なのは個人でもチームでも変わらないように感じます。
個人開発の場合でも、自分の作業が定義した要件からズレていないか振り返ることが大事かも知れません。

役割について


本来ならスクラムには開発側にプロダクトオーナー、開発者、スクラムマスターが居ます。
個人の場合全部が自分担当になります。
また、開発外にはステークホルダー(利害関係者)という役職も居ます。
ここがチーム開発と一番違い偏らないような方法を考える必要性があるかも知れません。

変更コスト


こちらについては、チームだろうが個人だろうが期間やコード量が増えるたびにコストが増大します。
逆に言うと小さいプロジェクトならば、変更コストは多くないです。
変更が少なかったり、小さなゲームなら考えなくても良いかも知れません。

1週間でやること


リファインメント、スプリントレビュー、デイリースクラム、レトロスペクティブ、スプリントプランニング。
ここはチームによって違うかも知れませんが、こういうことを取り組んだりします。

それぞれ、個人の場合はどうやって実現できそうか考えてみます。

リファインメント (頻度は1週間に1回とか2回とか)
プロダクトバックログ(要件リストみたいなもの)を見直し、優先度の高いPBI(プロダクトバックログアイテム 以下PBI)を具体化します。

チームの場合:
プロダクトバックログを見直し、優先度の高いPBIを具体化する
大きい項目を具体的なPBIにスライスを行い、どうやって実装をするか開発者が理解しているか確認など
INVEST:プロダクトバックログの書き方
「独立している」「交渉可能」「価値がある」「見積もり可能」「適切な大きさ(1週間or2週間に6~10個終わる)」「テスト可能」

個人の場合:
優先度の高いPBIを具体化、ココは替わりません。
どうやって実装をするか具体化
INVEST:プロダクトバックログの書き方
「独立している」「交渉可能」「価値がある」「見積もり可能」「適切な大きさ(1スプリントに6~10個終わる)」「テスト可能」
交渉可能な所、以外は同じ様にすべきかも知れません。

要件や工数的に許容範囲なら実際のPBIとして見積もりします。

スプリントプランニング (頻度はスプリントの最初に行う)
リファインメントで出たやることを更に細かく分離(分離作業は設計込み)させ、タスク化します。

チームの場合:
2~4時間ほどで出来る内容に落とし込み出来るようにします。
PBI別に作業完成しているかを判断する条件を定義します。

個人の場合:
そもそも個人開発の場合、1日に取れる開発の時間は少ない場合が多いです。
何度も粒度分解する作業をするより開発時間に当てたくなります。
リファインメント時点で、タスク並みに粒度を落とし込んだほうが良いかも知れません。
PBI別に作業完成しているかを判断する条件の定義はしても良いかもですね。

具体的な完成の定義はココでは語りません別途調べてみてください。

デイリースクラム(朝会) (頻度は毎日)
スプリントに向けて何するか明確化と再計画をします。
こちらはウォーターフォールでもやっているチームが多いですね。

チームの場合:
昨日やったこと、今日やること、問題点3つを報告し、チーム内共有します。
問題が出たらどうするかを考える。

個人の場合:
問題点などは理解しているため、これはやる必要があるか分かりませんが、問題が出たらどうするかを考える。というタスクの見直しの面では必要性を感じます。
頭を冷やして自身を見つめ直す為に一定間隔でやったほうが良いかも知れません。

レトロスペクティブ(振り返り) (頻度はスプリントの最後)
活動についての振り返りになります。

チームの場合:
現状の確認、KPTA,KPTを使う。

個人の場合:
現状の確認、KPTA,KPTを使う。

ココは変わらないかもですね。強いて言うなら、チームの場合は指摘出来る場面ですが、
個人の場合は自身を振り返る部分のため、振り返る内容は違ってくるかも知れません。

スプリントレビュー(振り返り) (頻度はスプリントの最後)
こちらはスプリントで出来た成果物についての振り返りになります。
いわゆる成果物レビューです。

チームの場合:
ステークホルダーに見せてフィードバックを受ける。(ゲームならα版などのロム提出に当たりそうですね)

個人の場合:
なし?自身がステークホルダーになると思いますのでコレは要らないかも知れません。
成果物について気になるならレトロスペクティブと一緒にやってしまって良いかも知れませんね。

個人で、こんなにしていられない


これらを見た方、「個人でこんなにしていられないよ!」って思ったかも知れません。
実際問題、仕様変更が頻繁でなく、仕様変更がないのならチーム開発でも、ウォーターフォール開発の方が開発が早いみたいです。
詳しくは知らんけど……

じゃあ、なぜスクラムをやるのかというと、仕様変更で工数が膨らむ「からこそ」どこまでやるか、方向性を定期的に見直す必要があるというワケですね。

個人ゲーム開発でゲームが完成しない人ほど、目標がブレやすいです。
ウォーターフォールで出来るのであればそれに越したことはないですが、無理な仕様をそのまま突き進めてしまうよりは、
多少時間がかかっても定期的に見直して、「どこに着地するか」「方向性が間違っていないか」「どのように方向転換するか」を考えるのがスクラムです。
つまり、「転ばないように頑張るんじゃなく、転んでもすぐ起き上がれるようにする」というやり方です。そのようなやり方では、もちろん時間がかかります。
その上でどちらを選ぶかはあなた次第だと思います。

これらを踏まえて実際に疑似スクラム化してみる


さて、チームと個人でやる場合の違いを述べてきましたが、なんとなく私自身も頭の中を整理出来てきた気がします。
上のことを考慮し、5ステップに落とし込んでみました。

1.自分が作りたい物を具体化する/コンセプトを作る


スクラムで本来ならステークホルダーからヒアリングするニーズ(要件)ですが、最初の段階では「具体的に自分が何を作りたいのか」を明確化することが大事に思えました。
ゲーム開発では企画意図やキャッチコピー、ゲームをプレイするターゲットやペルソナ定義、コンセプト、プレイ端末、ゲーム内容や期間、プレイ人数などがあります。

アジャイルでは、インセプションデッキと呼び何をするか具体化します。
我々は何故ココにいるのか、やらないことリスト、エレベーターピッチなどなど、ココでは割愛しますが言ってしまえば、
最初にやるべき事は自分たちがブレないように何をしたいかを明確化、明文化すると言うことです。

特に、一番大事なのはキャッチコピーやエレベーターピッチだと思います。
この時点でまだ不明確な部分があってもいいとは思いますが、ゲームを一言で表す言葉は作りましょう。
(後、ゲームでは、デザインの方向性を決めるために、この時点でコンセプトアートを作ったりしますね)

ここで決めたものは絶対にブレないようにしましょう。
不確実性のコーンが大きくなってしまいます。

2.プロダクトバックログを作る


Web開発者たちを見習い3ヶ月で作り切るという期限をまずは設定し、
1スプリントでまずは動くという所を目指し、実装できるゲームのコアとなる部分を定義し、プロダクトバックログを作ってみます。
一旦今回は1スプリント1週間と定義します。
優先度も決めましょう。優先度の指標は時間・予算・品質・スコープです。

3.タスク化をしよう


何がしたいかを明文化したら少なくとも、今からやるスプリントでどんなタスクをやるかをはっきりさせます。(つまり毎スプリントのはじめに実行)
スクラムで言う所のリファインメントとスプリントプランニングを同時にやってしまうという話です。
チームなら詳しく話し合う必要がありますが、個人制作ですので一気にタスク化や設計までやってしまっていいと思います。
1週間にこなせるタスク量は個人開発では1日あたり2時間以下が妥当でしょう。
疲れてできない日もあったりするので土日だけ頑張って1日あたり4時間定義するなどはやめましょう。
土日頑張ったとしても案外2時間ですら捻出は難しいものです。それに長期間開発するとなると、「短距離走長距離走の違い」と同じ様にペース配分を考える必要があります。
気長にやりましょう。
1スプリントあたり1日はタスク化や設計に注力するとして、1タスク2時間と考えるとタスク枚数は多くとも6枚。
1日休みを考えると5枚。少ない場合は、1タスク4時間と考えるとその半分の2.5枚程度でしょう。
PBIの粒度は1スプリントに6~10個終わるとされていましたが、PBIの粒度とタスクの粒度が合わず、この時点でチーム開発との違い乖離が生じます。
個人開発の場合のPBIの粒度は1スプリントに6~10個ではなく、もう少し大きく定義するしか無さそうです。(2スプリントで6~10個にするとか?1スプリントの長さを長くするとか?)
ココに関してはスプリントの考え方をそのまま適用できませんので、ココに関しては新たな手法を、考えたほうが良さそうです。

4.やるタスクを決める


昨日やったタスクの反映とその日やるタスクの決定。(タスクボードの作成)
スプリントが終わるまでの期間を考えて次にタスクに移るかなど、毎日決めましょう。

5.スプリント振り返り


スプリントレビュー兼レトロスペクティブです。
1スプリントに1回行います。

一人で振り返りをやるのは大変です。
ちゃんと振り返られているかわからないからです。
出来るならチームでやったり客観的な意見が欲しいですが、個人開発では難しいです。
とはいえ振り返らないワケにもいかないので出来る範囲でやっていきましょう。
個人での振り返りでは、タスクボードが役立つと思います。
どんなタスクをしたかはもちろん、そのスプリントでやれたタスク数は定量的で客観的データになります。
前週と比べて数がこなせたかなどだけでも、判断基準になると思います。

本当にうまくいくの?


しらん!!!!!
そもそも、個人開発をやる上で、私自身がスケジュール管理やタスク管理に大失敗している。
少しでもフレームワークに当てはめてやることで、全然出来ないというのを回避できないかを考えた結果、生まれた記事です。
チームでのスケジュール管理は世にいっぱいあるが、個人開発でのスケジュール管理はあまり見かけない。
あったとしてもせいぜい、TODOリストやToggleでのタイムトラッキング、スケジュール帳などの自身の生活におけるタイムマネジメントばかりで、作業に対するスケジュール管理ではない事が多い。
まぁ、そもそも個人開発をしてる人間なんて言う層が少ないからなのかも知れないが……。
ともあれ、今回仮説の一つとしてこれはどうだろうというたたき台を提案&自分の考え方を書き留めて整理したかったので書いてみました。
コレがうまくいけばイラストレーターさんや夏休み宿題に追われる学生さんなど締め切り駆動している人の助けになるはず……。しらんけど
何はともあれ、続けられるかは不安ですが、一旦やってみたいと思います。

まとめというか、あとがきみたいなもの


この記事を書く上でタイトルに迷いました。
ソロスクラム、一人スクラム、個人スクラムどれもスッキリ来なかったですし、そう堂々と名乗れるものでも無く感じたので、一人を表す言葉はアジャイルスクラムに付け加えないようにしました。
そもそも私自身ゲーム完成できてないですしね。
まぁ、ともかく色々書きましたが、この記事を書いて思ったのは個人開発をやる上で大切なのは「 自分が納得できるゲームとは何か、自分が目指すゲームの道標になるものは何かを明確化させること 」だと思いました。
結局自分が納得しないから完成しないワケですし、長期的に作成し続けるにはモチベーションだけではやっていけません。
そのため、長期間に渡るゲーム開発をするには人からよく見られたいだとか、いっぱい売れてお金が欲しいだとか、人に依存するような動機は辞めたほうが良さそうです。辛くなります。
流行り廃りを追いかけていると流行りが過ぎた時点でやる気を無くしてしまいます。
チームで開発するならともかく少なくとも個人開発を続けるのなら内発的動機付けがしっかりしていないと、どうしても軸がブレてしまいます。
いわゆる好奇心とか、成長感とか感覚的好奇心です。「好きだからやる」とか理屈じゃなく感情部分が大事です。
その内発的動機を忘れないためにも定期的な振り返りを行い自身を立て直す仕組みや短いやる気でも完成に持っていけるようなワークフローが必要です。
自分のやる気だけで立て直すのは大変なため、出来るだけ仕組みに依存し内発的動機を言語化、リマインドする仕組みの一つとしてスクラムを取り入れたいと思いました。
スクラムなら、やる気が続かなくてもリリースできるように組み立てるため途中で断念できますしね。

なんとか完成できる方が一人でも多くなればきっと楽しいだろうなと思います。
以上、やまだたいしでした。

超初心者シェーダー前提入門

こんにちは、やまだたいし( やまだ たいし (@OrotiYamatano) | Twitter )です。
Shader勉強したいって方の入り口になる部分がめっちゃ狭いなと思ったので
今回はデザイナーでも分かるような入門窓口となる記事を書いてみました。

目次


具体的にシェーダーって何が出来るの?


その前に描画の手順をカンタンに説明します

  1. 3Dモデルを準備
  2. 3Dモデルをディスプレイに表示するために座標変換
  3. 3Dモデルのバーテックス(頂点)ごとに描画位置を算出
  4. 裏表を調べて描画
  5. 描画する点の場所を確定
  6. 点ごとに描画優先度をデプスバッファ(深度情報)を比較
  7. 描画する点の色を確定
  8. 点を打つ

という流れで描画されます。
使用するアーキテクチャ等によって前後したり若干違ったりしますが大体こんな流れです。

(因みに3次元モデルのデータから2次元画像のイメージを作り出す多段階の過程全体のことをレンダリングパイプラインと呼びます)

で、シェーダーでは太文字の部分に手が加えられます。
3番に対してのプログラムをバーテックス(頂点)シェーダー
7番に対してのプログラムをフラグメント(ピクセル)シェーダー
とそれぞれ呼びます。

他にも出来ることはあるのですが、基本的にシェーダーで、この過程に手を加えられるのは、この2つと覚えてもらえばいいです。

シェーダーとマテリアルとテクスチャの違い


勘違いする人もいるので、ちゃんとした説明をする前にカンタンに解説

  • シェーダー
  • マテリアル
    • そのシェーダーに渡す値(設定)。
    • (マテリアルを設定してくださいって言うとシェーダー込みで指定されることが大半なので誤解する人も居るかも?)
  • テクスチャ
    • マテリアルに設定する2D情報(データ)のこと
      テクスチャ ≠ 2D画像(2D画像のこともテクスチャと呼ぶけど、同義じゃない)

バーテックスシェーダーで出来ること


こういう水面の歪み(反射はフラグメントシェーダー)

モデルを歪ませたり……。

めっちゃ頂点動かしてみたり

こういったエフェクト的表現に向いてる。
3Dモデラーさん向けにカンタンに言うと、シェイプキーを設定せずに3Dモデルの頂点をプログラムで動かすことが出来るってこと。

フラグメントシェーダーで出来ること


ディゾルブ表現とか……

マッピングされているテクスチャに対しての処理とか(セルルックシェーダーなど影やライティングの調整が出来る)

こういった歪みシェーダーとかもできる。

Blenderのこういった雲をつくる処理もフラグメントシェーダー

その他のシェーダー(ジオメトリ)


雑草作ったり、ファーシェーダー

フラグメントシェーダーとバーテックスシェーダーのあわせ技


因みにポストエフェクトのシェーダーは……


  1. 3Dモデルを準備
  2. 3Dモデルをディスプレイに表示するために座標変換
  3. 3Dモデルのバーテックス(頂点)ごとに描画位置を算出
  4. 裏表を調べて描画
  5. 描画する点の場所を確定
  6. 点ごとに描画優先度をデプスバッファ(深度情報)を比較
  7. 描画する点の色を確定
  8. 点を打つ
  9. ココ!で行われる

レンダリング結果に対してエフェクトをかけるので、
ロジックは2Dのドットに対して加工する処理になるのでピクセルシェーダーになります。
画面全体に対して1ピクセルずつ処理をするのでエフェクトが重なるほど重いです。

RGBずらしとか……

いわゆるカメラのエフェクト(ブラーとか)はポストエフェクト!

実際にシェーダーをイジってみたいと思ったら


いきなり数式とか仕組みやプログラムから入るのは辛い!
ノードでフラグメントシェーダーから始めてみよう!

まとめ


勉強にきっかけになる記事になればなぁと思い衝動的に作りました。
参考になれば幸いです。

【Unity】URPで頂点テクスチャフェッチ(VTF:Vertex Texture Fetching)に触れる

こんにちは、やまだたいし( やまだ たいし (@OrotiYamatano) | Twitter )です。
たまにはシェーダーの勉強をしようと思いVTFに触れたので
今回はその時の知見を共有したいと思い記事化しました。

UnityバージョンはUnity2020.3です。

目次


VTFとは


超簡単に言うと「頂点シェーダ内でテクスチャを参照すること」らしいです。

↓このような感じで水面を作ったり。
https://news.mynavi.jp/article/graphics-56/images/005l.jpg
参照元
news.mynavi.jp

風ノ旅ビトのような足跡を残したりするのにも使えます。(実際に風ノ旅ビトがVTFで実装されているかどうかは分かりません)
f:id:OrotiYamatano:20210706021524p:plain

実装結果


今回は雪山などで使えるキャラクターが歩いた位置が凹む動作をするものを実装に挑戦しました。

結果としては、影の付け方が分からなかったのですが、一応実装できました。
影についてはタイミングがあればリベンジしようと思います。

↓影が無いから分かりづらいケド……
f:id:OrotiYamatano:20210706032521g:plain

↓ちゃんと、凹んだように実装できてる……!
f:id:OrotiYamatano:20210706021808g:plain

いくつかのサイトを参考にしながら、実装しました。
しかしながら、ただVTFをするのもツマラナイと思い、今回はURPで実装に挑戦。

では、実装の説明に移ろうと思います。

テッセレーション


参考にしたプログラムではテッセレーションというものが使われてたので、今回はテッセレーションを利用したもので実装しています。

テッセレーション is 何


簡単に言うと、頂点シェーダから送られてきたポリゴンを分割することができるものらしいです。
処理的にいうと頂点シェーダーとピクセルシェーダーの間に属するみたいです。

具体的にはハイポリモデルをローポリ化してプログラム側でLODを実現したり、
逆にローポリモデルを分割してハイポリ化することも出来るようです。

LODをプログラム側で実現できるならデザイナーの手を煩わせずに済むので嬉しいですね!
しかしながら、せっかくポリゴンを分割するプログラムを作ってもピクセルシェーダー側で簡略化されてしまうこともあり、無駄な処理になりやすく、最適化が難しいとされているようです。
使う際は、気をつけて使ったほうが良さそうです。

テッセレーションの詳細


テッセレーションはDirectX11ではハルシェーダーとテッセレータとドメインシェーダに分割されるようです。

ハルシェーダー
与えられた頂点ポリゴンをどの様に分割するかを決めるシェーダー。
設定情報はパッチという物に保存されるようです。
パッチにはコントロールポイントと呼ばれる分割用の制御点を持っており1~32までのポイントを設置することが可能で、
複数のポイントを設定することでポリゴンをより細やかに分割出来るようです。(参照サイトの受け売り)

テッセレータ
DirectX11では触ることの出来ない固定機能です。
ハルシェーダで設定を行ったパッチをもとに頂点分割を行うシェーダ。

ドメインシェーダ
分割されたポリゴンに対して、様々な変更を行うためのシェーダーです。
曲線を描くようにしたり、凹凸が出来るようにしたり。

テッセレーションはビルドインならUnity側でメソッドが用意されていて、UnityDistanceBasedTessという関数で実現できるようですが
URPはそうもいかない(詳しくは知らんけど)みたいなので、(とりあえず)自前で書いてみます。

とはいえ……


今回は私が使ったのでテッセレーションの説明をしましたが雪山に足跡を残す処理をつくるなら
テッセレーションを使わずにハイトマップから法線を取り頂点位置を調整(バーテックスシェーダーで調整)するシェーダーを書くだけで良いかも知れません。

ハイトマップにペイント


雪道や砂漠などにキャラクターが通った後をつけるためには、頂点座標を移動させる処理が必要です。
では、何を元に頂点座標をどの程度動かすのかを決めるのかと言うと、ハイトマップというものを使います。
ハイトマップとは平らの表面のオブジェクトに凹凸の情報を加えるテクスチャーのことです。
今回、どうやってハイトマップを作っていくか、いくつか検討しました。

カメラを使う方法


カメラを Orthographic にしてデプスを取得しレンダーテクスチャーに深度情報を渡すことで、ハイトマップを作るやりかたです。
URPのカメラではDon't Clearフラグがなくなったのでそのまま使えません。

以下のようなスクリプトを用意して、カメラ情報をクリアされないようにすることで、足跡を作ることが出来ます。
(↓コレとか使えそう?)

github.com

比較的カンタンに実装できますが、平面な土地にしか使えないため、使い勝手は微妙です。
他の実装方法を検討してみます。

「いけにえと雪のセツナ」ではこの方法の応用を利用しているっぽいです。
このカメラを使う方法は海や湖と言った平面にしかなりえない水に表現に影響を及ぼすハイトマップになら利用してもいいかも知れないですね。
しかしながら、カメラを使うのは少々重いので、使わないことを推奨します。
Unity4系まではこのやり方が主流だったようです。

因みにURPでは↓のようにも実装できるようです。
https://www.patreon.com/posts/47452596www.patreon.com

座標位置を渡す方法


キャラクターなどの座標位置を元にレンダーテクスチャーに書き込む方法です。
CommandBufferなどを使ってレンダーテクスチャに書き込みを行います。
山のような地形でもやろうと思えば使用可能だと思いますが、ちょっとロジックを考えなくてはいけません。

コライダー衝突箇所位置座標を渡す方法


座標位置を渡す方法とあまり変わらないのですが、コライダーを使うことで、地面に対してコライダーがあるものはすべて反応します。
欠点としては地面にはある程度細かいメッシュコライダー等を設定する必要があるので少々重いです。

しかしながら、山のような地形でも使うことが出来ます。
(山のような地形のメッシュでは試してないので言い切れないで部分はありますが)
今回はこのやり方で実装してみようと思いコレで実装しました。

衝突位置からUVの座標位置を特定するのにはESさんのUnityテクスチャーペイントを参考に作っていきます。

↓ESさんの記事
esprog.hatenablog.com

スクリプト


ESさんのコードを一部借りていますし、スクリプト量もそんなに多くないので、今回はGitHubはナシでコードをベタ貼りする形にしようと思います。
実装をみたい方がいらっしゃったら、githubにあげようと思います。(気軽にTwitterアカウントにDM、リプしてね)

Shader "Custom/VTX_Test"
{
    Properties
    {
        [MainTexture] _MainTex ("MainTex", 2D) = "white" {}
        _Glossiness ("Smoothness", Range(0,1)) = 0.5
        _Metallic ("Metallic", Range(0,1)) = 0.0
        
        [MainTexture]_DispTex ("Disp Texture", 2D) = "gray" { }
        [MainTexture]_NormalMap ("Normalmap", 2D) = "bump" { }
        _SpecColor ("Spec color", color) = (0.5, 0.5, 0.5, 0.5)
        _MinDist ("Min Distance", Range(0.1, 50)) = 10
        _MaxDist ("Max Distance", Range(0.1, 50)) = 25
        _TessFactor ("Tessellation", Range(1, 50)) = 10
        _Displacement ("Displacement", Range(0, 1.0)) = 0.3
        
        [HideInInspector] _Cull("__cull", Float) = 2.0
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" "RenderPipeline" = "UniversalPipeline" }
        LOD 200

        Pass
        {
            Name "ForwardVTX_Test"
            Tags
            {  "LightMode"="UniversalForward" }
            HLSLPROGRAM
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" 
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Shadows.hlsl"
            
            // Physically based Standard lighting model, and enable shadows on all light types
            #pragma require tessellation
            #pragma fragment frag
            #pragma vertex TessellationVertexProgram
            #pragma hull hullExec
            #pragma domain domainExec
            #pragma target 5.0

            CBUFFER_START(UnityPerMaterial)
            sampler2D _MainTex;
            sampler2D _DispTex;
            sampler2D _NormalMap;
            float _Displacement;
            float _TessFactor;
            float _MinDist;
            float _MaxDist;
            half _Glossiness;
            half _Metallic;
            half4 _Color;
            CBUFFER_END
            
            
            struct Input
            {
                float2 uv_MainTex;
            };
            
            struct Attributes
            {
                float4 vertex   : POSITION;
                half3 normal      : NORMAL;
                float2 uv           : TEXCOORD0;
                float4 color : COLOR;
            };
            
            struct Varyings
            {
                float4 color : COLOR;
                float3 normal : NORMAL;
                float2 uv                       : TEXCOORD0;
                float4 positionCS               : POSITION;
                float3 lightTS                  : TEXCOORD3; // light Direction in tangent space
            };
            struct TessellationFactors
            {
                float edge[3] : SV_TessFactor;
                float inside : SV_InsideTessFactor;
            };

            struct ControlPoint
            {
                float4 vertex : INTERNALTESSPOS;
                float2 uv : TEXCOORD0;
                float4 color : COLOR;
                float3 normal : NORMAL;
            };

            float CalcDistanceTessFactor(float4 vertex, float minDist, float maxDist, float tess)
            {
                float dist = 0.;
                float f = clamp(1.0 - ( dist - minDist) / (maxDist - minDist), 0.01, 1.0) * tess;
                return (f);
            }
            
            [domain("tri")]
            [partitioning("integer")]
            [outputcontrolpoints(3)]
            [outputtopology("triangle_cw")]
            [patchconstantfunc("patchConstantFunction")]
            ControlPoint hullExec(InputPatch<ControlPoint, 3> patch, uint id : SV_OutputControlPointID)
            {
                return patch[id];
            }

            //これがパッチの設定
            TessellationFactors patchConstantFunction(const InputPatch<ControlPoint, 3> patch)
            {
                
                TessellationFactors f;
                
                float edge0 = CalcDistanceTessFactor(patch[0].vertex, _MinDist, _MaxDist, _TessFactor);
                float edge1 = CalcDistanceTessFactor(patch[1].vertex, _MinDist, _MaxDist, _TessFactor);
                float edge2 = CalcDistanceTessFactor(patch[2].vertex, _MinDist, _MaxDist, _TessFactor);

                
                f.edge[0] = (edge1 + edge2) / 2;
                f.edge[1] = (edge2 + edge0) / 2;
                f.edge[2] = (edge0 + edge1) / 2;
                f.inside = (edge0 + edge1 + edge2) / 3;
                
                return f;
            }


            //フラグメントシェーダーにわたす前の最後の頂点処理
            Varyings vert(Attributes input)
            {

                Varyings output;
                //凹みを適用させる処理-----------------------------------------------------------------------------
                const float d = tex2Dlod(_DispTex, float4(1 - input.uv.x, input.uv.y, 0, 0)).r * _Displacement; 
                input.vertex.xyz += input.normal * (1 - d);
                //----------------------------------------------------------------------------------------------
                
                output.positionCS = TransformObjectToHClip(input.vertex.xyz);   //Vertの反映
                output.normal = input.normal;
                
                output.uv = input.uv;   //UVテクスチャ反映
                output.color = input.color; //色反映
                
                VertexNormalInputs vertex_normal_input = GetVertexNormalInputs(input.normal, input.color);
                const Light main_light = GetMainLight();
                const float3x3 tangent_mat = float3x3(vertex_normal_input.tangentWS, vertex_normal_input.bitangentWS, vertex_normal_input.normalWS);
                output.lightTS = mul(tangent_mat, main_light.direction);
                return output;

            }

            [domain("tri")]
            Varyings domainExec(TessellationFactors factors, OutputPatch<ControlPoint, 3> patch, float3 barycentricCoordinates : SV_DomainLocation)
            {
                Attributes v;
         
                #define DomainPos(fieldName) v.fieldName = \
                        patch[0].fieldName * barycentricCoordinates.x + \
                        patch[1].fieldName * barycentricCoordinates.y + \
                        patch[2].fieldName * barycentricCoordinates.z;
         
                    DomainPos(vertex)
                    DomainPos(uv)
                    DomainPos(color)
                    DomainPos(normal)
         
                    return vert(v);
            }
            
            ControlPoint TessellationVertexProgram(Attributes v)
            {   
                ControlPoint p;
         
                p.vertex = v.vertex;
                p.uv = v.uv;
                p.normal = v.normal;
                p.color = v.color;
         
                return p;
            }

            
            half4 frag (const Varyings input) : SV_Target
            {
                half4 c = tex2D (_MainTex, input.uv);
                
                
                const float3 normal = UnpackNormal(tex2D(_NormalMap, input.uv));
                const float diff = saturate(dot(input.lightTS, normal));
                
                c *= diff;
                return c;
            }
            
            ENDHLSL
        }

    }
    FallBack "Diffuse"
}

↑ちょっと、雑いですが、シェーダーのコードになります。
#pragma require tessellation
と書くとテッセレーションがUnityで有効になるようです。

因みにテッセレーションはコードじゃないと実装できないです。

他の #pragma 定義は各処理がどの関数で実装されているかを表します。
例えば #pragma vertex TessellationVertexProgram ですがこれは、バーテックスシェーダーの処理を TessellationVertexProgram 関数で実装するという意味です。
今回のシェーダーでは、ポイントとなってくるのはテッセレーションの処理ですが、先程解説した通りの実装しかしておらず、比較的単純なので大体わかると思います。
(凹みを適用する部分はコメント書いてあるし)
強いていうなら、 hullExec 関数の前に定義されているものが分からなないと思いますが、これらは「どのようにテッセレーションをするか」という設定です。
DirectXの設定になるので、理解したいなら、そちらを参考にしつつUnityの公式を見ると良いと思います。

↓このへん見ればよさそう?
docs.microsoft.com

テッセレーションの解説はKENTOさんの解説がわかりやすい
zenn.dev

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PaintObject : MonoBehaviour
{
    private void OnCollisionStay(Collision collision) {
        foreach (var collisionContact in collision.contacts) {
            var ground = collisionContact.otherCollider.GetComponent<GroundPaint>();
            if (ground != null) {
                ground.Paint(collisionContact.point);
            }
        } 
    }
}

球体に貼り付けているスクリプトです。

衝突を取得。GroundPaint というスクリプト(後述)が検出されたら、
ContactPoint を使い座標位置を渡し、書き込み処理を促します。
docs.unity3d.com

(それはそうと ContactPoint って言うのがあるって今回初めて知りました。
FPSの銃弾が当たったときのパーティクルの出現位置を決めるのに使いやすそうですね!)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Experimental.TerrainAPI;
using UnityEngine.Rendering;

public class GroundPaint : MonoBehaviour {
    // Start is called before the first frame update
    private const float TOLERANCE = 1E-2f;
    private Mesh mesh;
    private int[] meshTriangles;
    private Vector3[] meshVertices;
    private Vector2[] meshUV;
    [SerializeField] 
    private RenderTexture renderTexture;
    [SerializeField] 
    private Vector3 paintSize;
    
    void Start() {
        MeshFilter meshfil = GetComponent<MeshFilter>();

        var mesh1 = meshfil.mesh;
        meshTriangles = mesh1.triangles;
        meshVertices = mesh1.vertices;
        meshUV = mesh1.uv;
        mesh = new Mesh();
        PolyMesh(0.1f,10);
    }

    void PolyMesh(float radius, int n)
    {
        //verticies
        List<Vector3> verticiesList = new List<Vector3> { };
        float x;
        float y;
        for (int i = 0; i < n; i ++)
        {
            x = radius * Mathf.Sin((2 * Mathf.PI * i) / n);
            y = radius * Mathf.Cos((2 * Mathf.PI * i) / n);
            verticiesList.Add(new Vector3(x, y, 0f));
        }
        Vector3[] verticies = verticiesList.ToArray();

        //triangles
        List<int> trianglesList = new List<int> { };
        for(int i = 0; i < (n-2); i++)
        {
            trianglesList.Add(0);
            trianglesList.Add(i+1);
            trianglesList.Add(i+2);
        }
        int[] triangles = trianglesList.ToArray();

        //normals
        List<Vector3> normalsList = new List<Vector3> { };
        for (int i = 0; i < verticies.Length; i++)
        {
            normalsList.Add(-Vector3.forward);
        }
        Vector3[] normals = normalsList.ToArray();

        //initialise
        mesh.vertices = verticies;
        mesh.triangles = triangles;
        mesh.normals = normals;

    }
    public void Paint(Vector3 worldPos, Camera renderCamera = null) {
        
        var material = new Material(Shader.Find("Unlit/Color"));
        material.SetColor("_Color", Color.red);
        Vector2 uv;
        if(renderCamera == null)
            renderCamera = Camera.main;
        
        Vector3 p = transform.InverseTransformPoint(worldPos);
        Matrix4x4 mvp = renderCamera.projectionMatrix * renderCamera.worldToCameraMatrix * transform.localToWorldMatrix;
        if (LocalPointToUV(p, mvp, out uv)) {
            var cmd = new CommandBuffer(); // コマンドバッファを作る
            
            cmd.SetViewProjectionMatrices(Matrix4x4.identity, Matrix4x4.identity); // 2Dとして描画する
            cmd.SetRenderTarget(renderTexture); // RenderTargetを設定
            uv = new Vector2(uv.x*-2+1.0f,uv.y*2-1.0f);
            cmd.DrawMesh(mesh, Matrix4x4.TRS((uv), Quaternion.identity, paintSize), material, 0, 0, default); // 位置、回転、大きさを指定してMeshを描画
            Graphics.ExecuteCommandBuffer(cmd); // コマンドバッファを実行
        }
    }
    
    
    /// <summary>
    /// Convert local-space point to texture coordinates.
    /// </summary>
    /// <param name="localPoint">Local-Space Point</param>
    /// <param name="matrixMVP">World-View-Projection Transformation matrix.</param>
    /// <param name="uv">UV coordinates after conversion.</param>
    /// <returns>Whether the conversion was successful.</returns>
    public bool LocalPointToUV(Vector3 localPoint, Matrix4x4 matrixMVP, out Vector2 uv)
    {
        int index0;
        int index1;
        int index2;
        Vector3 t1;
        Vector3 t2;
        Vector3 t3;
        Vector3 p = localPoint;

        for(var i = 0; i < meshTriangles.Length; i += 3)
        {
            index0 = i + 0;
            index1 = i + 1;
            index2 = i + 2;

            t1 = meshVertices[meshTriangles[index0]];
            t2 = meshVertices[meshTriangles[index1]];
            t3 = meshVertices[meshTriangles[index2]];

            if(!ExistPointInPlane(p, t1, t2, t3))
                continue;
            if(!ExistPointOnTriangleEdge(p, t1, t2, t3) && !ExistPointInTriangle(p, t1, t2, t3))
                continue;

            var uv1 = meshUV[meshTriangles[index0]];
            var uv2 = meshUV[meshTriangles[index1]];
            var uv3 = meshUV[meshTriangles[index2]];
            uv = TextureCoordinateCalculation(p, t1, uv1, t2, uv2, t3, uv3, matrixMVP);

            return true;
        }
        uv = default(Vector3);
        return false;
    }
    
    /// <summary>
    /// Investigate whether a point exists inside the triangle.
    /// All points to be entered must be on the same plane.
    /// </summary>
    /// <param name="p">Points to investigate.</param>
    /// <param name="t1">Vertex of triangle.</param>
    /// <param name="t2">Vertex of triangle.</param>
    /// <param name="t3">Vertex of triangle.</param>
    /// <returns>Whether the point exists inside the triangle.</returns>
    public static bool ExistPointInTriangle(Vector3 p, Vector3 t1, Vector3 t2, Vector3 t3)
    {
        var a = Vector3.Cross(t1 - t3, p - t1).normalized;
        var b = Vector3.Cross(t2 - t1, p - t2).normalized;
        var c = Vector3.Cross(t3 - t2, p - t3).normalized;

        var d_ab = Vector3.Dot(a, b);
        var d_bc = Vector3.Dot(b, c);

        if(1 - TOLERANCE < d_ab && 1 - TOLERANCE < d_bc)
            return true;
        return false;
    }

    /// <summary>
    /// Investigate whether a point exists on a side of a triangle.
    /// </summary>
    /// <param name="p">Points to investigate.</param>
    /// <param name="t1">Vertex of triangle.</param>
    /// <param name="t2">Vertex of triangle.</param>
    /// <param name="t3">Vertex of triangle.</param>
    /// <returns>Whether points lie on the sides of the triangle.</returns>
    public static bool ExistPointOnTriangleEdge(Vector3 p, Vector3 t1, Vector3 t2, Vector3 t3)
    {
        if(ExistPointOnEdge(p, t1, t2) || ExistPointOnEdge(p, t2, t3) || ExistPointOnEdge(p, t3, t1))
            return true;
        return false;
    }
    /// <summary>
    /// Investigate whether a point exists on an edge.
    /// </summary>
    /// <param name="p">Points to investigate.</param>
    /// <param name="v1">Edge forming point.</param>
    /// <param name="v2">Edge forming point.</param>
    /// <returns>Whether a point exists on an edge.</returns>
    public static bool ExistPointOnEdge(Vector3 p, Vector3 v1, Vector3 v2)
    {
        return 1 - TOLERANCE < Vector3.Dot((v2 - p).normalized, (v2 - v1).normalized);
    }
    /// <summary>
    /// Determine if there are points in the plane.
    /// </summary>
    /// <param name="p">Points to investigate.</param>
    /// <param name="t1">Plane point.</param>
    /// <param name="t2">Plane point.</param>
    /// <param name="t3">Plane point.</param>
    /// <returns>Whether points exist in the triangle plane.</returns>
    public static bool ExistPointInPlane(Vector3 p, Vector3 t1, Vector3 t2, Vector3 t3)
    {
        var v1 = t2 - t1;
        var v2 = t3 - t1;
        var vp = p - t1;

        var nv = Vector3.Cross(v1, v2);
        var val = Vector3.Dot(nv.normalized, vp.normalized);
        if(-TOLERANCE < val && val < TOLERANCE)
            return true;
        return false;
    }
    
    /// <summary>
    /// Calculate UV coordinates within a triangle of points.
    /// The point to be investigated needs to be a point inside the triangle.
    /// </summary>
    /// <param name="p">Points to investigate.</param>
    /// <param name="t1">Vertex of triangle.</param>
    /// <param name="t1UV">UV coordinates of t1.</param>
    /// <param name="t2">Vertex of triangle.</param>
    /// <param name="t2UV">UV coordinates of t2.</param>
    /// <param name="t3">Vertex of triangle.</param>
    /// <param name="t3UV">UV coordinates of t3.</param>
    /// <param name="transformMatrix">MVP transformation matrix.</param>
    /// <returns>UV coordinates of the point to be investigated.</returns>
    public static Vector2 TextureCoordinateCalculation(Vector3 p, Vector3 t1, Vector2 t1UV, Vector3 t2, Vector2 t2UV, Vector3 t3, Vector2 t3UV, Matrix4x4 transformMatrix)
    {
        Vector4 p1_p = transformMatrix * new Vector4(t1.x, t1.y, t1.z, 1);
        Vector4 p2_p = transformMatrix * new Vector4(t2.x, t2.y, t2.z, 1);
        Vector4 p3_p = transformMatrix * new Vector4(t3.x, t3.y, t3.z, 1);
        Vector4 p_p = transformMatrix * new Vector4(p.x, p.y, p.z, 1);
        Vector2 p1_n = new Vector2(p1_p.x, p1_p.y) / p1_p.w;
        Vector2 p2_n = new Vector2(p2_p.x, p2_p.y) / p2_p.w;
        Vector2 p3_n = new Vector2(p3_p.x, p3_p.y) / p3_p.w;
        Vector2 p_n = new Vector2(p_p.x, p_p.y) / p_p.w;
        var s = 0.5f * ((p2_n.x - p1_n.x) * (p3_n.y - p1_n.y) - (p2_n.y - p1_n.y) * (p3_n.x - p1_n.x));
        var s1 = 0.5f * ((p3_n.x - p_n.x) * (p1_n.y - p_n.y) - (p3_n.y - p_n.y) * (p1_n.x - p_n.x));
        var s2 = 0.5f * ((p1_n.x - p_n.x) * (p2_n.y - p_n.y) - (p1_n.y - p_n.y) * (p2_n.x - p_n.x));
        var u = s1 / s;
        var v = s2 / s;
        var w = 1 / ((1 - u - v) * 1 / p1_p.w + u * 1 / p2_p.w + v * 1 / p3_p.w);
        return w * ((1 - u - v) * t1UV / p1_p.w + u * t2UV / p2_p.w + v * t3UV / p3_p.w);
    }

}

床のオブジェクトに貼り付けるスクリプトです。
ESさんのコードをココで拝借しています。

そのせいで私が完全理解していない部分がありますが、
衝突位置からUV座標を検出するプログラムの詳細についてはコチラ↓を参考にすると良さそうです。
qiita.com

今回は詳細に理解していなかったせいで uv = new Vector2(uv.x*-2+1.0f,uv.y*2-1.0f); とちょっと荒っぽく座標位置の変換を実装しました。
本来であれば、 TextureCoordinateCalculation の処理を書き換えたり、 SetViewMatrix にて変換を行うのが正しいのだと思いますが、私の勉強不足で分からなかったので、この様に実装しました。
(MVP行列の勉強とかしたのに結局分からなかった)

後、使用するレンダーテクスチャーのフォーマットは AUTO_Depth を指定しないとうまく動かないと思います。注意。

とりあえず、実装については以上になります。

参考記事等


参考にした資料を貼っておきます。
他にもありましたが、覚えていないです。

www.youtube.com

www.youtube.com

baba-s.hatenablog.com

docs.unity3d.com

docs.unity3d.com

esprog.hatenablog.com

tips.hecomi.com

yttm-work.jp

tositeru.github.io

Unityのシェーダーについて、もっと深く知りたい人へ


今回勉強をしてみて痛感したのはUnityのシェーダー自身だけでなく、ともかくHLSL、DX(DirectX)についての理解が少なかったという痛感です。
UnityのシェーダーはHLSL、DXベースで作られており、URPを書く上では生のHLSLやDXを理解していたほうが有利だと感じました。
私はまだ読んでないですが↓といった書籍を読むと理解が深まりそうなので参考までに貼っておきます。

DirectX 12の魔導書 3Dレンダリングの基礎からMMDモデルを踊らせるまで Amazon.co.jp: DirectX 12の魔導書 3Dレンダリングの基礎からMMDモデルを踊らせるまで eBook : 川野 竜一: 本

HLSL シェーダーの魔導書 シェーディングの基礎からレイトレーシングまで HLSL シェーダーの魔導書 シェーディングの基礎からレイトレーシングまで | 清原 隆行 | コンピュータ・IT | Kindleストア | Amazon

まとめ


以上、【Unity】URPで頂点テクスチャフェッチ(VTF:Vertex Texture Fetching)に触れるでした。
実は言うと、最近技術ブログ書けてないなと思い突拍子もなく勉強して記事化しました。
結果としては勉強不足を痛感することになり、中途半端な実装になってしまい、申し訳ないです。
(影については途中に貼った https://www.patreon.com/posts/47452596 を参考にすればいい感じに出るんじゃないかなぁとは思いますが)
今回に関しては勘弁してください。
Unityのシェーダーについてはコレまでフラグメントシェーダーを少々イジったことがある程度で、ちゃんと触ったことがなかったので今の私にはコレが限界でした。
まぁ、今回VTFを勉強したことで、Unityのシェーダーの理解がかなり深まったように感じます。

暇があれば今度は水面シェーダーや視差マッピングなどに取り組みたいと思います。(本当にやるかは分からないですが……)

最後に、記事を最後まで読んでくれた方に感謝いたします。
後、色々Unityゲーム開発者ギルドにて教えてくださった方々に感謝。(主に、のたぐすさん)

以上、やまだたいしでした。

Affinity DesignerのPSDファイルをUnityのuGUIとして取り込んでみた

こんにちは、やまだたいし( やまだ たいし (@OrotiYamatano) | Twitter )です。
普段、個人制作でAffinity Designerを使っており、Affinity DesignerのPSDファイルを取り込めたら作業工数削減できるのでは?と思いやってみました。
今回はその時の知見を共有したいと思い記事化しました。

目次


なぜ、PSDファイルを読み込もうと思ったのか


Affinity DesignerはAdobe Illustratorのように使える買い切りの商品です。
(↓これがAffinity Designer)
affinity.serif.com

スマホゲームの制作会社ではAdobe Illustratorを使い、UIを作るということが行われています。
そしてIllustratorを使った場合は、以下ライブラリ(Baum2)などを使いPSDファイルをUnity用に変換して使うことが出来ます。
かなり便利です。

github.com

Affinity DesignerでもBaum2を使いPSDをインポート出来たら良かったのですが、
Affinity DesignerにはScript機能が無いため、同じ様にエクスポート/インポートは出来ません。

そこで、UnityでAffinity DesignerのPSDをインポート出来る機能が出来れば作業効率化できて欲しいのでは?と思い今回挑戦してみました。
PSD to uGUIができれば一々Unity側でレイアウトをしなくても済むし超嬉しい!

(因みに、こんな↓UIとかをAffinity Designerで作ったりしてUI勉強しています)

出来たのか?


結論を先にいうと、
Affinity DesignerのPSDはレイヤー2層までのものなら、Unityに取り込みが出来た
という感じです。
(今回はuGUIのImageのみ対応)

なぜ、このような中途半端な感じになってしまったのかというと
Affinity DesignerのPSD出力が完全なものではなくレイヤー2層より下は、どんな設定をしてもラスタライズされレイヤーが統合されてしまうためです。(悲しい)
(なにか設定があるなら誰か教えて欲しい)

もしレイヤー階層の深いPSDを出力してUnityに取り込んで使いたいのなら、
Affinity Designerが対応してくれるのを待つか、
素直に他のソフトを使うしかないと思います。

動作環境


今回は、以下の条件でAffinity DesignerのPSDファイルを取り込む機能を作りました。

Unity:2019.4.16f1
Affinity Designer:1.9.2.1035
OS:Windows10

今回の内容はPSDを読み込み、uGUIのImageとして出力するものです。
スライダーやボタンの差し替えの対応までは正直な所、面倒くさかったので対応してません。
(そもそもレイヤーが2層までしか動かないので作るモチベーションが下がってしまった……)

Unity公式の2D PSD Importerが動くバージョンなら、どのバージョンのUnityでも動くとは思いますが、
全てのバージョンで私が使用したUnityのスクリプトが挙動するかまでは確認してないので
私が作った環境のバージョン以降で使用することをオススメします。

また、Macでも動くとは思いますが、もしかしたらpngを生成するscriptは環境に依存する部分があり書き換える必要があるかも知れません。
詳細はスクリプトの解説部分で話します。

リポジトリ


github.com

内容の解説


今回Githubリポジトリを公開しますので、使い方の解説を行った後、
順を追って説明したい思います。
手っ取り早く使いたい人はコードの説明は飛ばしてもらって大丈夫です。

今回のコードの内容の説明は分類すると3つになります。

  1. PSDの取り込み
  2. SpriteRendererで参照しているPSDのテクスチャのpng
  3. SpriteRendererのuGUIへの変換とImport設定

利用方法


github.com

こちらのリポジトリの内容を既存のプロジェクトに反映します。
(入っているPSDとシーンファイルは動作確認で利用したものなので消して構いません)
中に入っているので2D PSD Importerは入っていると思いますが、もし2D PSD Importerがインポートされなかった場合、
Unityのパーケージマネージャーから2D PSD Importerのインストールをお願いします。


1.まずはPSDをUnityに取り込み f:id:OrotiYamatano:20210530212950p:plain (プロジェクト名がPDFtoUnityになってるのは気にしないでw)
Defaultで読み込まれます。
このままでは、1枚の画像としか使えないので、変換します。


2.インポートしたアセットを右クリック、[2D Importer]→[Change PSD File Importer] f:id:OrotiYamatano:20210530213221p:plain ↓以下のように変換される

f:id:OrotiYamatano:20210530212655p:plain

このまま、PSDファイルをuGUIのImageとして使いたいところですが、
取り込んでみると分かると思いますが、SpriteRendererとして読み込まれます。

そのためuGUIで使えるようにSpriteRenderer→Imageへ変換をします。
後、ついでにpsdファイルの画像参照を使い続けるのは個人的に気持ちが悪いので、
psdの画像をpngとしてpsdのアセットの置いてあるフォルダに出力します。

(インポートされるときはSpriteRendererの構成のままインポートされるように作ってあるので、
描画順が気になる方はスクリプトをイジって、Order in Layer順になるようにGameObjectの順番を入れ替えるように変更を加えてください)


3.一旦、変換するためにシーン上に読み込み f:id:OrotiYamatano:20210530214558p:plain


4.右クリック、[SpriteRendereToUGUI]を選択
(すみませんが、一つずつ選択するのしか対応してません)
f:id:OrotiYamatano:20210530214641p:plain

↓uGUI用のアセットが出来上がる

f:id:OrotiYamatano:20210530214714p:plain


5.後は、Canvasを追加して、GameObjectの順番を整えたらインポート完了です! f:id:OrotiYamatano:20210530214914p:plain

Scriptの解説


1. PSDの取り込み


PSDの取り込み自体はUnityで対応してますが、1枚の画像として取り込まれてしまいます。
その場合、2D PSD Importerを使ってPSDを取り込みたいところですが、2D PSD ImporterはPSD Importerという名前のくせして、
PSBファイルしかデフォルトで対応していません。

そこで、PSDファイルをPSBファイルとして読み込ませるように変換取り込み機能を実装します。
実はPSBとPSDは同じようなファイル構成になっており、PSB→PSDは出来ないですが、PSD→PSBは、ほぼほぼ互換性があり、
拡張子を変えるだけでPSBとして読み込まれます。

とは言いつつも一々拡張子を変えて保存するのも億劫です。
スクリプトで変換できるのならスクリプトで対応してしまったほうが楽だなということで、スクリプトで対応しました。

Asset>Editor>PSDImporterOverride.csと、Asset>Editor>PSDImporterOverrideEditor.csがその変換スクリプトにあたります。

実はUnity公式でほぼ同じスクリプトが公開されていたので、それを丸パクリしてきた感じです。

↓パクリ元
docs.unity3d.com

コードを見ていただければ、解説するまでも無いとは思いますが、ざっくり簡単に説明すると、
PSBファイルインポート処理であるPSDImporterの処理をOverrideし、指定したアセットをPSDImporterOverrideでインポートする処理を走らせているだけです。

2.SpriteRendererで参照しているPSDのテクスチャのpng


画像はCreateReadabeTexture2DでTexture2Dで参照しているspriteをrectで切り抜いてpng化しています。

    /// <summary>
    /// Texture2Dから切り取って画像を生成
    /// </summary>
    /// <param name="texture2d"></param>
    /// <param name="rect"></param>
    /// <returns></returns>
    private static Texture2D CreateReadabeTexture2D(Texture2D texture2d,Rect rect)
    {
        RenderTexture renderTexture = RenderTexture.GetTemporary(
            texture2d.width,
            texture2d.height,
            0,
            RenderTextureFormat.Default,
            RenderTextureReadWrite.Linear);
        

        Graphics.Blit(texture2d, renderTexture);
        
        RenderTexture previous = RenderTexture.active;
        RenderTexture.active = renderTexture;
        Texture2D readableTextur2D = new Texture2D((int) rect.width, (int) rect.height);
        readableTextur2D.ReadPixels(new Rect((int) rect.xMin, texture2d.height-rect.yMax, rect.width, rect.height),  0, 0);
        readableTextur2D.Apply();
        RenderTexture.active = previous;
        RenderTexture.ReleaseTemporary(renderTexture);
        return readableTextur2D;
    }

texture2dの内容をrenderTextureに一旦書き込み、編集可能にした後、
readableTextur2Dに対してReadPixelsで読み込み適用しています。

コードを見たとおり
あんまり、難しいことはしていないです。

詰まったのが、texture2d.height-rect.yMax設定箇所なのですが、destX,DestYの0,0は実は左下を表しておりUnityの通常座標系とは異なるようです。
Texture2D-ReadPixels - Unity スクリプトリファレンス

ちなみにMacはグラフィックス APIがMetalで動いているためdestX,DestYの位置がズレるかも知れないです。
同じく実機でも差が出てしまう可能性があるため、この機能を実機で使う場合はご注意ください!
↓参考元

https://anz-note.tumblr.com/post/150526871876/unitytexturereadpixels%E3%81%AE%E5%9F%BA%E6%BA%96%E7%82%B9%E3%81%A3%E3%81%A6%E5%B7%A6%E4%B8%8B00%E3%81%98%E3%82%83%E3%81%AA%E3%81%84%E3%81%AE%E3%81%A3%E3%81%A6%E3%81%84%E3%81%86%E3%81%8A
anz-note.tumblr.com
qiita.com

3. SpriteRendererのuGUIへの変換とImport設定


コードの大半は簡単なので、一部のみ解説します。

    /// <summary>
    /// SpriteRendererをImageに変換
    /// </summary>
    /// <param name="target"></param>
    /// <param name="copy"></param>
    private static void AddImageSprite(SpriteRenderer target, GameObject copy) {
        if (target == null) return;
        var image = copy.AddComponent<UnityEngine.UI.Image>();
            
        var saveBytes = CreateReadabeTexture2D(target.sprite.texture,target.sprite.rect).EncodeToPNG();
            
        Object parentObject = PrefabUtility.GetCorrespondingObjectFromSource(Selection.activeGameObject);
        var path = AssetDatabase.GetAssetPath(parentObject);
        path = Path.GetDirectoryName(path);
        path += "/" + target.sprite.name+".png";
        File.WriteAllBytes(path, saveBytes);

        AssetDatabase.ImportAsset(path);
            
        var importer = AssetImporter.GetAtPath(path) as TextureImporter;
        var settings = new TextureImporterSettings();
        if (!(importer is null)) {
            importer.ReadTextureSettings(settings);
                
            settings.textureType = TextureImporterType.Sprite;
            settings.npotScale = TextureImporterNPOTScale.None;
            settings.spriteMode = (int) SpriteImportMode.Single;
                
            importer.SetTextureSettings(settings);
                
            importer.SaveAndReimport();
        }

        Sprite s = (Sprite)AssetDatabase.LoadAssetAtPath(path, typeof(Sprite));
        image.sprite = s;
    }

まず、引数の説明から、
引数は画像化するSpriteRendererのtargetと
アセットのspriteを最終的にアタッチするcopyゲームオブジェクト
で構成されています。

後、外部変数として選択したオブジェクトを表すSelectionが使われています。
引数からSelectionのオブジェクトを取得するような形にして、依存性の低い丁寧な実装にしても良かったんですが、
ぶっちゃけ、手直しも面倒だったので、テキトーです。

まず、CreateReadabeTexture2Dにてtargetの画像イメージをbyteで取得。
その後、GetCorrespondingObjectFromSourceにて選択したゲームオブジェクトを取得。
GetAssetPathから現在の格納先フォルダを取得して、sprite.nameでpngとして、そのフォルダに吐き出しています。

byteをそのままimage.spriteにアタッチしても良いのですが、
メタファイルはUnity側のデフォルト設定はテキストデータのためメタファイルにbyteを持つのは正直好ましくないです。
なので、pngをアタッチしていきます。

保存したpngをUnityから認識できるようにAssetDatabase.ImportAsset(path);でインポートします。
そのままだと、textureTypeがDefaultになってしまうため、
TextureImporterSettingsを使い、
ReadTextureSettingsで既存のインポート設定を読み込んだ後
textureTypeとnpotScaleとspriteModeだけ変更し
importer.SetTextureSettings(settings);にて設定。

そして、セーブと再インポートをimporter.SaveAndReimport();にて実行します。

後は、保存されたpngをAssetDatabaseで読み込んでimageのspriteに設定しています。

参考元:
qiita.com
hacchi-man.hatenablog.com

まとめ


以上、Affinity DesignerのPSDファイルをUnityのuGUIとして取り込んでみたでした!
Affinity Designerからの読み込みは残念な結果になってしまいましたが、PSDファイルの読み込みは今回のScriptを使えば読み込めると思いますので、
皆さんも試してみてはいかがでしょうか!?

個人的には早くAffinity DesignerがPSDの多重レイヤーを実装して欲しい&実装してくれると信じて待とうかと思います。

今回、久しぶりにアセット拡張周りを触れたので、結構楽しかったし、勉強にもなりました。
特にアセットインポート周りの設定はチマチマしないでスクリプトで設定する方法を学ぶことが出来たのは良かったです。
UI Elementsもマトモに使えるようになってきてると思うので機会があれば、またアセット拡張ネタとか書きたいですね。
個人的には個人制作の作業が効率化出来るようなネタがかけたら嬉しいなぁと思っています。

以上、やまだたいしでした。

UnityでFMODを使おう!【後編】

こんにちは、やまだたいし( やまだ たいし (@OrotiYamatano) | Twitter )です。
FMODの使い方を探した時に日本語記事がなかったので本記事を作成しました。
他の人の取っ掛かりになればなと思います。

前編はコチラ
orotiyamatano.hatenablog.com

目次


FMODをUnityで使う


前回はFMOD Studioのクイックチュートリアルを行ったので、今回は実際にUnityで再生して確認していきたいと思います。

使用するUnityバージョンは2018.4以降とのことですが、私は2019.4を使っていこうと思います。
さっそくFMODのプラグインをダウンロードして行こうと思います。

FMODはアセットストアからダウンロード出来ます。(お目当てのバージョンではない場合、 こちら からダウンロード可能)

assetstore.unity.com

早速、パッケージマネージャーを使って、ダウンロード、インポートします。
(ダウンロードするバージョンは、FMOD Studioと同じであることを確認してください)

今回は単純な翻訳記事ではなく、前回クイックチュートリアルで作ったプロジェクトを鳴らすようにしてみたいと思います。
正式なチュートリアルをやりたい方は コチラ もしくは コチラ の英語のチュートリアルを参考にしてみてください。

UnityでFMODを使う


1.FMODのプラグインアセットのインストール完了後、パッケージのインポートが終わったら、[FMOD]>[Edit Settings]を選択

f:id:OrotiYamatano:20210402015405p:plain

InspectorにFMODStudioSettingsが開かれます。
2.[Browse] をクリック。

f:id:OrotiYamatano:20210403050801p:plain

3.前編で作成したFMODStudioプロジェクトフォルダー内の* .fsproを選択します。(私はNewProjectという名前で作成したのでNewProject.fsproを選択)

f:id:OrotiYamatano:20210403051056p:plain

正しく完了すると、いくつかの追加設定が表示されます。

今回FMODを使うのでUnityの標準のサウンドは使えなくなります。
Aoudio関連のComponentを削除する必要があります。
消していきましょう。

4.SampleSceneを開き、HierachyのMainCameraを選択、InspectorよりAudioListenerをRemoveComponentで削除

f:id:OrotiYamatano:20210403051613p:plain

5.代わりに[AddComponent]で[FMOD Studio]>[FMOD Studio Listener]を追加

f:id:OrotiYamatano:20210403051822p:plain

エラーが出なければOKです。
推奨
後、完全にUnityのオーディオを使えなくして少しでも処理を軽くしたい方は、
Project Settings>Audio>Disable Unity Audioにチェックを入れると良いでしょう

f:id:OrotiYamatano:20210404013837p:plain

さて、聞く側は出来たので次は鳴らす側です。

6.Hierachyに適当なGameObjectを追加し、カメラから近い位置に設置、Inspectorより[AddComponent]で[FMOD Studio]>[FMOD Studio Event Emitter]を追加

f:id:OrotiYamatano:20210403052338p:plain

7.PlayEventの項目をObjectStartに変更し、Eventの虫眼鏡のマークをクリックしてEvent>NewEvent(前編で作ったEvent)を選択

f:id:OrotiYamatano:20210403052729p:plain

ちなみに今回はparameterの値は入れないで再生しようと思います。
前回入れたparameterの値を使いたい方は自分で調べてください。

8.再生ボタンを押して、今回設定した音が再生されることを確認しましょう

これで以上です!

Q.音がならない

実装したのに音が鳴らない方もいるかも知れません。
いくつか原因となりそうなことを列挙したので確認してみてください。

原因1
FMOD StudioとUnityにインポートしたバージョンが違う場合、音がならないことがあるようです。
FMOD Studioと同じバージョンを使用しているか確認しましょう。
FMODのバージョンは[Help]>[About FMOD Studio]より確認が出来る。
f:id:OrotiYamatano:20210404013257p:plain

原因2
Unityのバージョンが対応しているか確認する。
FMODのプラグインのバージョンによっては対応していないことがあるようです。
確認しましょう。

原因3
うっかりやってしまうのが、単純ですが、Unity側のMute AudioがOnになっている場合です。
もちろん聞こえないのでOnになっていないか確認しましょう。

f:id:OrotiYamatano:20210404013548p:plain

原因4
オブジェクトがカメラから遠すぎる。
FMODで作った音は減衰機能が標準で入っています。
FMOD Studio ListenerをAddComponentしたカメラから遠すぎる場合、音が聞こえなくなってしまいます。
気をつけましょう。

これで一通り完了です


これでFMODが使えるようになりました。
具体的な制御やフェードなど、FMODの使い方が知りたい方は、コチラチュートリアルやドキュメントを参考にしてみてください。
また、海外では結構頻繁に使われるようなので、英語でYoutubeを検索すると、減衰機能を生かした実装のやり方などを紹介している方が出てきます。
Youtubeの英語翻訳字幕機能は自動でも割と精度がいいので、日本語に翻訳してみれば大体理解できます。
ドキュメントがどうしてもわからない方は動画を参考にしてみると良いかも知れません。

まとめ


以上、UnityでFMODを使おうでした。
日本では使う人はまだまだ少ないですが、WEBGL形式でも使うことが出来るのでUnity1WeekJamなどで試しに使ってみても良いかも知れません。
今後私も使っていきたいので、いろいろな方々が日本語記事を書いてわかりやすくなっていいったら、嬉しいです。
また要望があったら、ダッキングなどの処理なども書こうと思います。
皆、FMOD使ってみようぜ!以上、やまだたいしでした。

UnityでFMODを使おう!【前編】

こんにちは、やまだたいし( やまだ たいし (@OrotiYamatano) | Twitter )です。
FMODの使い方を探した時に日本語記事がなかったので本記事を作成しました。
他の人の取っ掛かりになればなと思います。

後編はコチラ
orotiyamatano.hatenablog.com

目次


そもそもミドルウェア・オーディオエンジンが使えると何が嬉しいのか


  • サウンドミキサー機能がUnity標準以外も使えるようになる!

バーブとか色々

ゼルダやオクトパストラベラーとかにも使われてましたよね note.com

BGMが再生されているときにSEやキャラクターの音声が埋もれてしまったり、音割れしてしまうことはありませんか?
BGM側の音をリアルタイムに小さくなるように音量調整する機能をダッキングと言います。

studio-sunny-side.hatenablog.com

  • 負荷が小さい

サウンドレイテンシー、メモリ消費、ストリーム負荷、圧縮音声のデコード負荷等、ミドルウェア側でやってくれる。

  • ランダム再生などが楽

ランダム再生させるときはUnityの標準機能で行う場合自分でロジックをプログラミングすることになるが、ミドルウェア側で設定すればロジックを組むことなく再生できる。

他にも使う理由はあると思いますが、このあたりが主に皆さんが使う理由の中心だと思います。

FMODとは


ミドルウェア・オーディオエンジンです。
ゲームなどで使われ、音声の管理や再生をやりやすくするするミドルウェアです。
類似のサウンドエンジンとしてCRI(ADX等),Wwiseなどが存在します。

ちなみにUnityのオーディオ機能の内部ではFMODが使われてるらしい。
(なので相性は良いかも?)

サウンドエンジンと比較してみる


CRI Wwise FMOD
小規模開発環境の価格 個人/小規模向け(前年度年商が1,000万円以下)ADX2 LEなら無料。
その売上以上はADX2に移行。
開発予算$150k以下なら無料。
現在$150kは日本円だと1600万くらい?
開発予算$500k規模まで無料、
1ゲームあたり$2000(年間売上$200k以下なら無料。
現在$200kは日本円だと2100万くらい?)
商用の価格 ADX2なら初期費用Androidなら35万、月間250万以上超えると更に増えていく?
詳しくは知らない
プロジェクトごとの料金。$1500k以下なら$7200 ベーシックなら開発予算$500k~1500kだったら1ゲームあたり$5000
ロゴ表記 ADX2 LE は必要なし。ADX2は25万払えばロゴ表記破棄できる 必要なし? インディー版なら必須、ベーシックなら$5000払えばロゴ表記を破棄できる
機能制限 LEはブラウザや家庭ゲーム機未対応。暗号化機能もなし。 無料版だとサウンドファイル、メディアアセット数が500まで インディー版でも、機能には制限なし
対応プラットフォーム
(エンジン側が対応していないのもあるので注意)
Swich,PSVita,PS4/PS5,
iOS,Android,Webブラウザ(WebGL),Xbox Series,Stadia,
アーケード,macOS,Windows,Xbox One(ADX2の一覧。他自動車向けのソフトウェアなどもあるみたい?)
Android,Linux,iOS,maxOS,Windows,
PS4/PS5,Swich,Xbox One,Xbox Series, Stadia,tvOS,Universal Windows Platform,android tv
Windows,macOS,Linux,iOS,Android,
HTML5(WebGL),PS5/PS4,Xbox,Xbox Series,Switch
未対応プラットフォームについて
(公式ではVR機器対応と書いてないものもあったが基本的にはどれもOculusは動きそう)
ADX2LEの場合はWebGLや家庭ゲーム機未対応 WebGL未対応 Stadia未対応
対応エンジン Unity,UE4,Cocos2d-x Unity,UE4(推してるのはUnityとUE4だが、Cocosなどでも使えるっぽい?) Unity,UE4,CryEngine
使っている作品(画像を右クリックして別タブで開けば大きな画像がみれます)
スマホ以外にもタイトルはあるが、スマホ推し?

他にもいっぱいタイトルがあった。表示してるものは現在最新

価格の面からかインディータイトルが多め?
特徴 日本企業。リップシンクなどCRI独自の多彩な機能がある。
日本語資料が多いので、日本でよく使われる。
カナダ企業。世界で最も利用されているっぽい。日本語の説明もある。 オーストラリア企業。競合と比べ安い。少し機能は少なめ。
UXが良い。日本語対応は今は無し

詳細に関してはコチラ。

https://www.fmod.com/licensing#premium

  • FMODの採用タイトル

https://www.fmod.com/games

どうしてFMODを使うのか


価格が良心的で、UXが良くて、WebGLプラットフォーム対応無料で使えるものがFMODしか無いから!

今までFMODが日本で話題にならなかった理由


  • 日本語に一切対応していない
  • インディー版が無料になったのが2020年12月頃に発表と、割と最近

jp.gamesindustry.biz

FMODの使い方


英語が使える方ならFMODの使い方を覚えるのは容易だと思います。
ラーニング用の教材も豊富でドキュメントもしっかりある。

https://www.fmod.com/learn

とはいえ、私を含め英語が達者ではない人も多いと思います。
なので今回はクイックチュートリアルとUnityに組み込むまでを簡単にではありますが、日本語訳してみようと思います。
本記事ではクイックチュートリアルのみ翻訳します。Unityへの組み込みは後編にて取り扱います。

FMOD Studio - クイックスタートチュートリアル


始める前に


ダウンロードはコチラから。
https://www.fmod.com/download

(FMODのサイトへの登録が必須)

本編


補足しながら、ざっくり翻訳(というか意訳)していきます。
↓翻訳元ページ
https://www.fmod.com/resources/documentation-studio?version=2.1&page=quick-start-tutorial.html

本記事はWindowsベースで記述しますが、多分Macでも、ほぼ同じだと思います。

チュートリアルは概念を学んでから本チュートリアルを読むのがオススメします。
(概念についてはこちら。今回は飛ばします https://www.fmod.com/resources/documentation-studio?version=2.1&page=fmod-studio-concepts.html )

1.FMOD Studioの起動

ダイアログが表示されます。
f:id:OrotiYamatano:20210330224850p:plain

2.チュートリアルでは新規プロジェクトが必要なので、右下の[New Project]をクリック

最初のダイアログが消え、「EventEditerWindow(イベントエディタウィンドウ)」に[Untitled]という名前で新規のプロジェクトが表示されます。

(EventEditerWindowとは→ https://www.fmod.com/resources/documentation-studio?version=2.1&page=glossary.html#event-editor-window )

f:id:OrotiYamatano:20210330225113p:plain

このまま作業開始することも出来ますが、最初にプロジェクトを保存しておくのをオススメします。

3.左上の[File]を開き、[Save]を選択

4.場所を選択し、覚えやすいプロジェクト名を命名して[保存(S)]ボタンをクリック

(私はNewProjectという名前で今回は保存します)
これでプロジェクトをいつでも保存、ロード可能になりました。

早速、コンテンツの作成をしていきます。
FMOD Studioのコンテンツの基本単位は、「Event(イベント)」と呼ばれます。
(Eventとは→ https://www.fmod.com/resources/documentation-studio?version=2.1&page=glossary.html#event )
EventはEventEditerWindowの左側にある、「EventBrowser(イベントブラウザ)」で作成、編集します。
(EventBrowserとは→ https://www.fmod.com/resources/documentation-studio?version=2.1&page=glossary.html#events-browser )

f:id:OrotiYamatano:20210330231313p:plain

5.EventBrowserを右クリックし、「ContextMenu(コンテキストメニュー)」から[New Event]>[New 3D Event Timeline]を選択、[Enter]キーを押して名前を確定

(ContextMenuとは→ https://www.fmod.com/resources/documentation-studio?version=2.1&page=glossary.html#context-menu )

f:id:OrotiYamatano:20210331000852p:plain

すると、[New Event]という名前のEventが作成されます。
[New Event]の「Timeline(タイムライン)」と「ParameterSheet(パラメーターシート)」のプロパティが
EventBrowserの右側の「Editor(エディタ)」と「OverView(オーバービュー)」と「Deck(デッキ)」に表示されます。

(Timelineとは→ https://www.fmod.com/resources/documentation-studio?version=2.1&page=glossary.html#timeline )
(ParameterSheetとは→ https://www.fmod.com/resources/documentation-studio?version=2.1&page=glossary.html#parameter-sheet )
(Editorとは→ https://www.fmod.com/resources/documentation-studio?version=2.1&page=glossary.html#editor )
(Deckとは→ https://www.fmod.com/resources/documentation-studio?version=2.1&page=glossary.html#deck )
(OverViewとは一番右側の概要説明ウィンドウのこと。プレビューとかが表示される)

f:id:OrotiYamatano:20210331000927p:plain

EventEditorWindowの詳細について知りたい場合は コチラ を参照ください。

[New Event]は作成したばかりのため、再生ボタンを押しても何も音が鳴りません。
音を鳴らすためにはオーディオファイルをインポートする必要があります。

6.EventBrowserのAssetsタブをクリック

「AssetsBrowser(アセットブラウザ)」が表示されます。

(AssetsBrowserとは→ https://www.fmod.com/resources/documentation-studio?version=2.1&page=glossary.html#assets-browser )

f:id:OrotiYamatano:20210330234140p:plain

AssetsBrowserにはプロジェクトにインポートされた全てのオーディオファイルが表示されます。
今はまだインポートしていないため空です。

7.[File]>[Import Assets...]をクリック

インポートするファイルが選択出来るようになります。

8.今回はサンプルとして コチラ からQuick Start Tutorial Assets(「one.ogg」「two.ogg」「three.ogg」)をダウンロードし、解凍。その3つを選択して[開く(O)]をクリック すると、アセットがインポートされAssetsBrowserに3つのアセットが表示されます。

AudioAssetsの詳細について知りたい場合は コチラ を参照ください。

9.Editerで[Audio 1]のオーディオトラックを右クリック、ContextMenuから[Add Multi instrument]をクリック

f:id:OrotiYamatano:20210331003224p:plain

オーディオトラックに青い(?)ボックスが表示されます。
コレがMulti Instrumentです。

f:id:OrotiYamatano:20210331003339p:plain

10.Multi Instrumentをクリックして選択

するとMulti InstrumentのプロパティがDeckに表示されます

f:id:OrotiYamatano:20210331003548p:plain

11.AssetsBrowserで一番上のアセットをクリックし、Shiftキーを押しながら一番下のアセットをクリックして全てのアセットを選択

12.アセットをドラックしてMulti Instrumentのプレイリストにドラッグします

f:id:OrotiYamatano:20210331003848p:plain

Multi Instrumentは実行(トリガー)されるとプレイリストの内容に応じて音声を出力します。
再生ボタンを押してEventを再生し試聴することでテストできます。

13.TransportBar(トランスポートバー)の再生ボタンをクリック

f:id:OrotiYamatano:20210331004327p:plain

TimeLineの再生位置がInstrumentまで行くと、instrumentが実行(トリガー)されると(今回の設定の場合)3つのアセットの中からランダムに再生されます。

14.TransportBarの停止ボタンをクリックしてオーディオを停止します。もう一度聴きたい場合は再生ボタンを再度クリックしてください。

Instrumentの詳細が知りたい場合は コチラ を参照ください。

現在の設定のままだと、Eventをゲーム中からパラメーターを変更する方法がありません。それを修正しましょう。
パラメーターを使用すると、パラメーターの値を変更することによってEventの動作を変更させることが出来ます。

パラメーターを元に動作するイベントを追加するには、まず最初にプリセットパラメーターをプロジェクトに追加する必要があります。

15.ツールバーの[Window]>[Preset Browser]を選択

Preset Browserが表示されます。
ここでは、プリセットパラメーターとプリセットエフェクトを作成、編集できます。

16.ParametersBrowserの空の部分を右クリックし、ContextMenuに表示される[New Parameter]をクリック

f:id:OrotiYamatano:20210331234520p:plain

[Add Parameter]ダイアログが表示されます。

f:id:OrotiYamatano:20210331234721p:plain

17.デフォルト設定で問題ないので、[OK]ボタンをクリック

パラメータの追加ウィンドウが消え、「Parameter 1」という名前のパラメータがParametersBrowserに表示されます。

パラメータとプリセットパラメータの詳細については、 パラメータパラメータリファレンス の章を参照してください。

Preset Browserはもう必要ないので閉じます。

18.Preset Browserウィンドウの☓ボタンをクリックして閉じます

このチュートリアルでは、Eventのボリューム(何故かチュートリアルとUIが違うので今回はボリュームでいきます)をパラメーターの値に依存させるようにします。

19.Event Master Trackを選択するには、Master Trackのヘッドをクリックします。

f:id:OrotiYamatano:20210331235332p:plain

トラックの シグナルチェーン がDeckに表示されます。さらに、EventのMacros DrawerがDeckの右端に表示されます。

20.ピッチナンバーボックスを右クリックし、ContextMenuから[Add Automation]を選択します

f:id:OrotiYamatano:20210331235851p:plain

「自動化および変調ドロワー」が表示され、マクロのドロワーの右側のセクションラベルは「Automation: Volume」が表示されます。

21.Volume Automationセクションの[Add Curve]ボタンをクリックし、ContextMenuから[Browse]を選択してから、[Select Parameter]ポップアップから[Parameter 1]パラメーターを選択します

f:id:OrotiYamatano:20210401001321p:plain

Volume Automationセクションに赤い破線が表示されます。
これはオートメーションカーブであり、パラメーターのすべての可能な値での自動化されたプロパティの値を示す折れ線グラフです。
現在、オートメーションカーブは完全に水平です。つまり、プロパティの値は、どんなパラメーターが渡されても同じ値です。

f:id:OrotiYamatano:20210401001825p:plain

22.赤い線をクリック

オートメーションカーブが実線になり、クリックしたポイントにオートメーションポイントが表示されます。

f:id:OrotiYamatano:20210401001915p:plain

オートメーションポイントは、オートメーションパラメーターに打った地点でのオートメーションプロパティの値を決定し、オートメーションカーブの形状を定義します。

23.オートメーションカーブの別の部分をクリックして、2番目のオートメーションポイントを追加します

24.新しいオートメーションポイントをクリックして上下にドラッグし、値を変更します

f:id:OrotiYamatano:20210401002234p:plain

このオートメーションカーブにより、パラメーターの値に応じてイベントのボリュームが変化します。

25.TransportBarの再生ボタンをクリック

Parameter 1のデフォルト値0のオートメーションによって設定されたボリュームで再生されます。

26.Eventの再生が終了したら、TransportBarの[Parameter 1]パラメーターのナンバーボックスでマウスボタンをクリックして押したままにし、パラメーターが最大値の10db(大きすぎると思うので程々に調整してください)に達するまでマウスカーソルを上にドラッグします

27.TransportBarの再生ボタンをクリック

Eventのボリュームは、タイムラインの再生位置の速度とイベントの出力のピッチに影響を与えるため、パラメーターの値が変更されたため、イベントのサウンドが異なることに気付くでしょう。

オートメーションの詳細については、オーサリングイベントの章の オートメーション セクションを参照してください。

ゲームでこのイベントを聞くには、それを Bank に割り当てて、そのBankを構築する必要があります。
ビルドは、プロジェクトをFMOD StudioAPIが使用できる形式に変換します。

28.「New Event」イベントを右クリックし、ContextMenuから「Assign to Bank>Master」を選択します

f:id:OrotiYamatano:20210401010933p:plain

EventがそのBankに割り当てられたので、Eventを作成するたびにそのBankに含まれます。

29.「File>Build」を選択します

f:id:OrotiYamatano:20210401011101p:plain

FMOD Studioは、プロジェクトのBankファイルを作成します。

30.フォルダーまたはエクスプローラーを開き、このFMODStudioプロジェクトを保存した場所に移動します

内部FMOD Studioプロジェクトフォルダには、「Build」という名前のサブフォルダを見つけることができます。

f:id:OrotiYamatano:20210401011417p:plain

このフォルダーには、「Desktop」という名前の別のサブフォルダーが含まれています。
このサブフォルダーには、「Master.bank」および「Master.strings.bank」というファイルが含まれています。
これらのファイルはプロジェクトで構築されたバンクであり、作成したイベントをプレイするためにゲームで必要になります。

ゲームでビルドされたバンクファイルのコンテンツを再生する方法については、 FMODAPIユーザーマニュアル を参照してください。

以上!


これで一通りのクイックチュートリアルは終了です。
長かった……。 分かりづらい所は極力スクリーンショットを貼り付けましたので詰まることなくクイックチュートリアルが出来ると思います。
それでもわからない方は、やはり原文を読みに行ってください。
それが結局早いです。

まとめ


FMODは良いかも!
まだ私も記事を書きながら試しているため、実際に鳴らす所まで試していませんが、
実際に使って見るのはありなんじゃないかと思い始めています。
近いうちに後編を書こうと思うので2週間以内に記事を作成したいと思っていますので、ゆっくりお待ちいただければと思います。
以上です。

後編はコチラ
orotiyamatano.hatenablog.com

ADHDの私が診断されて障害者手帳を貰うまで

こんにちは、やまだたいし( やまだ たいし (@OrotiYamatano) | Twitter )です。
今日はただの日記です。
ADHDと診断されてついに明日障害者手帳を貰うことになったので、
日記を書くことにしました。

ADHDだと思うけど、診断されたら気が楽になるのかな?
病院行くの面倒くさいなと思ってる人に参考になれば幸いです。

目次


前回、前々回の記事


orotiyamatano.hatenablog.com

orotiyamatano.hatenablog.com

診断→通院するまで


ここに関しては前々回の記事でも軽く触れたのでざっくり、話します。
元々ADHDじゃないかと疑ってはいました。
リモートワークが始まり、本格的に集中できず業務に支障が出ていたため、病院に行きました。

ADHDと診断できる医者は少ないので↓で調べてから行きましょう
(細かいことを言うならストラテラしか処方できないお医者さんもいるので、何が処方できるお医者さんなのかも調べると良さそう)
www.mental-navi.net

すると、ADHDに診断されました。

先生の判断基準としては、幼い頃からそういった傾向があるか?
具体的なエピソードがあるか?
自分だけではなく、客観的なデータが存在するか?
という3点を見ているようでした。(あくまで客観的で何を見ていたのか詳しく聞いたわけじゃないので違うかもしれませんが)

私の場合、親がADHDだったということもあり、幼い頃の通知表を見せる必要は有りませんでしたが、
人によっては通知表に落ち着きがないなど書かれていたりするのが非常に重要な判断基準となるようです。

一発で診断されたい方は持っていったほうがいいでしょう。

ぶっちゃけ診断を受ける必要ある?


あります。
薬があるのと無いのとでは雲泥の差って人もいます。
自分の症状を認知することで向き合い方がかわれるのが一番大事というか、良かったなと思うことです。
例えば、仕事とか集中できないことや、忘れ物する理由、遅刻する理由すべてをADHDのせいに出来るので気が楽になります。
「そんなADHDとか大げさすぎw」って言う人の苦労をわからない人にも薬を見せれば一発です。
管理的なことや、細かい作業(誤字脱字の確認など)が苦手なのも分かったので無理してやらなくなりました。

薬に関しては、ぶっちゃけ、ADHDが治るわけでもないし、
効き具合いが人によっても違い、
副作用が酷すぎて薬が飲めない方もいますし、
薬を飲んで性格が変わってしまう人もいるので期待はしない方が良いかもしれません。

私の場合、そこそこ自分で気付けるぐらいには薬で症状が緩和されます。
洗濯で家の鍵を洗濯しなくなったのと、万年眠気が少しはマシになったのが嬉しかったです。

6ヶ月が経つと


病院に通うようになって薬を毎月処方されると、薬の量にもよりますが、毎月約1万円程かかります。
そこで通院して6ヶ月が経つと自立支援医療受給証と障害者手帳の申請が出来るようになります。
病院から説明があると思いますが、
地域支援庁舎などに行き、申請書を貰い、
病院に数千円(5千円くらいだったかな)払って申請書を書いて貰い、
申請書を書けば申請できます。

なんと、ADHD関連の医療費に限り、登録した薬局とその病院の負担費用が1割になります(確か所得にも寄るけどそのぐらい割引される)。
自立支援医療受給証(精神病院)最高かよ!
ちなみに発行されるのは大体申請してから約2~3ヶ月後に発送されてきます。(私は3ヶ月かかった)
申請さえしてしまえば、受給社証が届くまでの間、申請書の写しで1割負担を認めてくれる病院、薬局もありますので、そこは確認しておくとお得かも。

障害者手帳の利点


障害者手帳の利点は少ないですが、人によっては就職に有利に働きます。
ちなみに障害者手帳は発行に2~3ヶ月かかります。(私の場合はぎり4ヶ月かかった)
紙とカードそれぞれ障害者手帳があるのですが、紙の場合2ヶ月、カードの場合3ヶ月発行にかかります。
私はカードを選びましたが、なぜかというと紙の障害者手帳ADHDは洗濯してしまって使い物にならなくしてしまう方が結構多いからです。
私も洗濯してしまいそうだなと思ったので多少発行に時間がかかってもカードにしたほうが利点が大きいと思いカードを申請しました。

で、障害者手帳の具体的な利点ですが、
さまざまなサービスを安く受けられることです。
交通費が安くなるところも結構あるようです。調べてみると良いかもしれません。

shogaisha-techo.com

個人的に一番嬉しいのが映画館が安くなることです。
私はエヴァを見に行くんだッ!

help.tohotheater.jp

他にも地区によっては住民税・所得税の控除があったりします。
といっても精神障害者保健福祉手帳3級の場合軽度なので控除対象じゃない可能性もあるので各地区の障害者分類を要確認です。
年金とかそのまわりも安くなったりするのかとか、そのあたりはまだ私は詳しくないので分かり次第また記載しようと思います。

後、先程就職に有利になる方もいると書きましたが、必ずしも有利になるわけじゃないです。
軽度のADHDなら隠していたほうが偏見の目で見られることもなく良いかもしれません。
しかしながら、どこを面接しても必ずと言っていいほど落ちてしまうというような人は障害者手帳をアピールしても良いかもしれません。
なぜなら、障害者雇用率制度といって「常用労働者100人超の事業主」はある一定数障害者を雇う必要があるからです。
もし雇うべき障害者の人数を下回っていた場合、障害者雇用納付金として不足1人につき月額50,000円を国に収める必要があります。
なので比較的軽度なADHDは意外と重宝されます。(手帳もらってから転職活動したことはないので詳しいことは知りませんが)就職する際に多少は役立つのでは無いでしょうか。

まとめ


以上、ADHDの私が診断されて障害者手帳を貰うまででした!
今日はADHDの薬が効きってない時に記事を書いたので、支離滅裂、誤字脱字あったらすみません!
同じADHDに苦しめられる同士に訳にたてばとおもいます!
さよなら!