2018年06月07日

進捗2 なのか?

 

 


前回に引き続きですが 進捗の2です。

 

今回の動画はAI部分だけを分離して実際にコースを走らせてみるチェックです。 フィジックスの設定が少しおかしくなっているので コースの路面コリジョンの状態によって時々AIカーがポップしていますが、 これは問題ないですね。 ちょっと突っ込みどころを残したほうが、あえてテストっぽさが出るしいいかな?という判断です。 誰が見てもおかしいところは開発者はだいたい把握してますって(笑

シーンレベルのモデルは流用ですが、一応スプラインベンドに対応して背景オブジェとコリジョンが追従するプロシージャルなコンストラクタを作成して これで道具が一通り揃ったので これから背景作成を始められるかなという段階ですね。

Unityでもできなくはないんですけども 全体に描画が重たい...ので 学習も兼ねて今回はUE4で作ってます。 別にUnityをやらないとかUnrealべったりというわけではなく並行していろいろ進めていますけど なんというかこれは頼まれもので、しかもこちらの都合で数年経っているという。 時間のあるときに詰めてしまわないとまた仕事が忙しくなるとテンションが下がってしまうため 今は少しこちらに比重をおいている感じです。

今どきの自動車のメッシュモデルはノーマルマップで見せかけを良くすると言ったごまかしが効かないので (あまりきれいなハイライトが入らない)、ポリゴン数が結構食うんですよね。 最適化しなくても数万ポリゴンのモデルが処理落ちしない これは開発効率いいです。 シーンの容量はでかいですけど許容範囲じゃないかな。

いろいろと触ってみると 作業に対して見え方が変わって効率が上がったりするので、急がば回れです

 

簡単にAI実装のおはなしをしますね

AIは基本スプライン上をclosedポイント(最も近い場所ね)を探索しながら 車体のローテーションを決定してアクセルのボリュームを上げます。

障害物がある場合は左右どちらかにステアリングを切るavoid処理(障害物を避ける)を実行します。進めない場合は一旦バックギアに入れて戻す。

おおまかにはそのようなシーケンスですが、 それだけでは人間の操作ぽさが出にくいので走行スプラインは複数を配列に登録して、ゲームの進行に合わせてそれらスプラインを選択しながら走行します。 ここらへんは人間の思考と同様に状況に応じて複数パターンのラインを使い分けるということです。

そして走行データをすべて記録しておくレコードシステムを実装します。 最速ラップが出るたびにこの記録をもとにスプライン上のウェイポイントを書き換えてデータとして保持します。 ゲーム開発はあまりスケジュールに余裕がないケースが多いため、開発の序盤にレコードシステムを実装してレベルデザインができ次第テストを行いAIを強化していくことになります。 このシステムを流用したものがゴーストカーと呼ばれるもので、開発中のテストモードをそのままオプションに流用しているというか 基本開発はスケジュールがきついのでオプション系は開発に使用したコードの流用など まかないみたいなものが多かった気がします。

ゲームセンターの筐体の場合は電源を落としても基盤内部にランキングなど簡単なデータを残せるバックアップメモリが搭載されていて、この部分だけ電池が入っています。 家庭用のゲームソフトも電池のバックアップが搭載されていたので理解しやすいかと思います。 サーバーに接続していなかった時代はリリース後も筐体内部のメモリーにデータを蓄え続けて、AIが強化され続けるという仕組みになっていました ※ものによります。 ゲームはリアル世界とはどんなに近づけても法則が異なるため プレイヤーが裏技を発見して突然想定外な記録が出る場合がありますが、 そうした場合でもロム交換のようなコストの掛かる方法は回避することができるため 効率が良い実装だったようです。 もちろんメーカーによって差異があるのでどのゲームも同じではないですよ。 ただし筐体の中にデータを持つのでタイムアタックが加熱しているロケーションは敵AIも強めになりますが、過疎っているロケーションであまりプレイヤーがいないと己との戦いになってしまうという 弱点もあります。

ココらへんの昔話は またおいおいしていくとして

 

 

あとUnity感想的なもの

Unity2018からECSとJobSystemという機能がベータ版?として投入されました。 が まだ積極的に移行していないので見当違いの考えかもしれませんが 構造を理解している開発者には メモリ管理が手動でできるようになる スレッドプログラミングが比較的容易になる というのはメリットになるとは思うのですが、当初のゲームエンジンで開発のハードルを下げるという目的からは遠ざかっているような気もします。

開発の利便性を向上させる目的で、構造体に多くのアトリビュート情報を付加してあるのはエンジン側の設計の都合で それで処理の足を引っ張るようならば、そこら辺り考えずに今まで通りのコードの書き方をすれば、ECSやJobSystem使用時と同等の性能が発揮できるようにコンパイル時に最適化されるだけでよいだけでは という気がしなくもないです。 さらなる調整オプションとして使用できる分には良いのですが、 今ひとつ方向性が見えにくくなってきました。 うまく伝わるかわかりませんが、動作が不安定なアプリケーションの改善点が、「新機能のオートセーブがつきました !!」だったような なんとなくですが

今さらですがゲームエンジンもだいぶ市民権を得てきたようで 当初は「ゲームエンジンだけでプログラムの基礎を学ばなければ、ゲームエンジンがなくなったらどうするんだ 」という意見も散見されましたが (長く開発を続けている職人さんね) もうさすがに急になくなるとかは無いでしょう。 スクラッチでゲームエンジンと同等の質と開発スピードに対抗できるならば分かりませんが。 寿司職人さんが、「回転寿司が急になくなったらどうするんですか」といっているようなもので、市場の需要を考えれば、まずないでしょう。

将来 現行ゲームエンジンにとって変わるものが出てきたとして使用するのは人間ですからインターフェースは大きなシェアを持っているツールに寄せてくるでしょうし 導入しやすい似たような環境になると思うので ビッグウェーブに乗っていけば大丈夫ではないでしょうか。

 

問題としてはゲームエンジンは進化が早いので必死に機能をマスターすることを重点にしてしまうとゲームを作れず終わる可能性はあります。しかも割と多いタイプな気がします。 ネットは大量の情報が参照できますけど ゲームはシステムや種類ごとに実装方法が異なるため情報化しにくく ネット上では制作物にぴったりと合った情報が見つけにくいか 見つからない そのため実装して制作ノウハウを蓄積していく必要があってその部分はネットの情報だけでは補強できないからです。

「クックパッドを毎日閲覧して料理の知識を身につけたら いつかはシェフになれるはず!」といった会話を小耳に挟んだとして ナンセンスなのでまずは実際に料理を作ってくださいというアドバイスをすると思いますがどうでしょう。 とりあえず実際に制作してゲーム制作ノウハウを身につけていけば 現行のゲームエンジンが廃れようがどんと来いなので、モノをどんどん作らないとですね。

 

 

ということで、 もうちょっと頑張っていきましょう

ではまた



akinow at 11:12|PermalinkComments(0) Clip to Evernote 日記 | シリーズ講座

2018年05月25日

進捗

 



 


だいぶ更新が空いてしまいましたが すこし進捗をアップしておきます

 

  今回の動画はUE4で作成中のゲーム進捗ですね これは今年の初めあたりの映像で 主な作業は大まかなのゲーム進行の実装とAIの実験です。 現在ではカーセレクトやスコアボードといったシークエンスと マルチプレイに対応して、だいぶ印象は変わっているかと思います。

仕事の合間の息抜きなので まだそれほど進行度は上がっていないですが、基本的な部分は大体押さえられたかと思います。流用できるものは流用して、最終的にはリソースは全部入れ替え予定。

いつも通り開発はじめではデザインは仮でざっくりですけど 見栄えを優先してリソース作成を完成状態に近いところから始めてしまうとゲーム本体に変更があった場合に、修正対応で大きく巻き戻し あるいはそのゲームの売りになるような難易度高めな実装を後回しにした結果 実装できずに作成済みデータが無駄になるといった 開発中にありがちな落とし穴に落ちてしまう危険性があります。

想定される問題は回避せずになるべく序盤で解決にあたっておく。 ゲームエンジンの作りがデータの容易な差し替えを想定した構造になっているので、全体を万遍なく見渡せる この方法がスケジュールを短縮するためにはベストな作り方だと思います。

もっとも ゲームエンジンじゃなくて何かをを作る場合大抵そういう作り方が結果良好なんですけどね。

 

細かい部分のアレコレはまたいずれ解説をしていくとして スケジュールがつまっているので

またしばらくしたら更新します。 ので しばしお待ちを

 

ではまた ☆ミ



akinow at 10:26|PermalinkComments(0) Clip to Evernote 日記 

2018年03月02日

うにばな Editorクラス−Presetsデータへのアクセス

 

 

現在Editorクラスでプリセット周りの実装をしているのですが、古めのスクリプトが仕様の変更で動作しないため しらべている最中です。

Editorクラスの情報は大量に存在しているので需要があるかわかりませんが。せっかくなので今回調べた経過を記事にしておきます。

 

まずUnityのEditorクラスから格納される内部データがどのように格納されているのかとデータへのアクセスのための実装ポイント、を簡単に解説している記事がありました。

 

■Accessing Unity's saved palettes

Q: 1つのパレットを保存する場合、それはスクリプト可能なオブジェクトのように見えるに格納されることに気づいた のですが、そこに保存されている色にアクセスする方法はありますか? できれば本当に便利です!

Screen Shot 2018-01-24 at 5.04.44 pm-CiS9zWDpoW

Screen Shot 2018-01-24 at 5.04.50 pm-VCX4uZJpsD

 

1.クリックしたものの種類を把握するように設定することができます:

  1. public static class EditorCommands {

  2. [MenuItem("Commands/Get Type Of Selected")]

  3. public static void GetTypeOfSelected() {

  4. Debug.Log(Selection.activeObject?.GetType().Name);

  5. }

  6. }

2.これを使用すると、パレットのタイプがColorPresetLibraryであることがわかります。さて、それは私たちが協力できるタイプですか?あなたがそれを行うにはいくつかの方法がありますが、有能なコードエディタがあれば、その名前を検索して、コンパイルされていないバージョンの型を見つけることができます:

  1. namespace UnityEditor

  2. {

  3. internal class ColorPresetLibrary : PresetLibrary

  4.    ...

  5. }

3.まあ内部的なので、スクリプトからアクセスすることはできません。アセットをテキストエディタで開いてその外観を確認してみます。

  1. %YAML 1.1

  2. %TAG !u! tag:unity3d.com,2011:

  3. --- !u!114 &1

  4. MonoBehaviour:

  5.   m_ObjectHideFlags: 52

  6.   m_PrefabParentObject: {fileID: 0}

  7.   m_PrefabInternal: {fileID: 0}

  8.   m_GameObject: {fileID: 0}

  9.   m_Enabled: 1

  10.   m_EditorHideFlags: 1

  11.   m_Script: {fileID: 12323, guid: 0000000000000000e000000000000000, type: 0}

  12.   m_Name:

  13.   m_EditorClassIdentifier:

  14.   m_Presets:

  15. - m_Name:

  16.     m_Color: {r: 1, g: 1, b: 1, a: 1}

  17. - m_Name:

  18.     m_Color: {r: 0.9705882, g: 0.007136685, b: 0.007136685, a: 1}

  19. - m_Name:

  20.     m_Color: {r: 0.13559689, g: 0.4712593, b: 0.5588235, a: 1}

さて、これは簡単に操作できます! そのテキストファイルをつかんで、 "m_Color"で始まるすべての行を探し、色を解析することをお勧めします。それをヘルパーメソッドとして作成するのはかなり簡単です。

 

次にヘルパークラスの実装例を探してみました

ヘルパーメソッドの実装例

 

Unityでプリセット内部変数にアクセスで検索すると上位にくるサイトですが掲載から時間が経過しているためスクリプトは手直しを入れる必要があります。掲載されているHelper関数のスクリプトですが以下のようなメッセージが帰ると思います。

error CS0619: `UnityEngine.Types.GetType(string, string)' is obsolete: `This was an internal method which is no longer used'
unity5以降の仕様変更でこのやり方では内部データにアクセスできないためエラーが返ります。そこで以下のように。
  1. Unityちゃん2Dのインポートのエラー解決(error CS0619)[ver.2017.2.1f1] - Qiita
  2. UnityEngine.Typesが使えなくなりました - FreelyApps
1.のサイトリンクではこのような変更で、エラーの回避ができたということです。
Types.GetType("UnityEditor.AnimationClipEditor", "UnityEditor.dll");
  ? var baseType = Types.GetType("UnityEditor.AnimationClipEditor", "UnityEditor.dll");
  ○ System.Type baseType = System.Reflection.Assembly.Load("UnityEditor.dll").GetType(typeName);
 
 

2.のサイトでは

  •   UnityEngine.Types.GetType(className,"Assembly-CSharp");
  •  System.Reflection.Assembly.Load("Assembly-CSharp").GetType(className);

IDEのサジェストにしたがうとSystem.Reflectionは省略して 良いそうなので

  • Assembly.Load("Assembly-CSharp").GetType(typeName);

このように記述することでエラーは回避できるようです。  さらに以下のような記述があります。

  • System.Type.GetTypeというメソッドでもTypeを取得できるようでした。型の名前を引数にとり、型を返すメソッドです。実行中のアセンブリ(dllと考えていい)かMscorlib.dllに含まれる型であれば名前空間で修飾した型名で型が取れるようです。
  •   System.Type.GetType(className+ ",Assembly-CSharp");

 

 

 

さらに 検索してみたところ以下のようなスクリプトが発見できました、新しめの2018.2月の情報ですが、コメント部分が詳細なので参考にしてみてください。

ColorPresetLibraryCreator.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using System;
using System.IO;
using System.Reflection;
public static class ColorPresetLibraryCreator
{

/*

■1:

Unity creates new preset librarys internally through typed singleton instances of the PresetLibraryManager class.

The two key methods we need to access through reflection are:

public T CreateLibrary(ScriptableObjectSaveLoadHelper helper, string presetLibraryPathWithoutExtension) where T : ScriptableObject

public void SaveLibrary(ScriptableObjectSaveLoadHelper helper, T library, string presetLibraryPathWithoutExtension) where T : ScriptableObject

CreateLibrary does some file path checking before creating the library through the helper object

and registering it with the library cache. SaveLibrary does what it says through the helper object.

In between the two calls is when we can actually add presets to the library.

We could use the helper object directly to save the library but it's probably safer to let the Manager class do it.

*/

    private const string assemblyDef = "UnityEditor.{0},UnityEditor";

    public static void CreateNewLibraryThroughPresetLibraryManager(string name, List colors)
    {

   

//■2:

// The ScriptableSingleton class is public, but because PresetLibraryManager isn't

// we still need to make a generic type and then use reflection to get the static instance property.

// This is assuming that we need the singleton instance for library registration purposes -

// it might not be necessary.


        Type managerType = Type.GetType(string.Format(assemblyDef, "PresetLibraryManager"));
        Type singletonType = typeof(ScriptableSingleton<>).MakeGenericType(managerType);
        PropertyInfo instancePropertyInfo = singletonType.GetProperty("instance", BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy);
        var managerInstance = instancePropertyInfo.GetValue(null, null);

// ■3:

// We create an instance of the save/load helper and then pass it and the path to the CreateLibrary method.

// "colors" is the file extension we want for the library asset without the '.'

 
        Type libraryType = Type.GetType(string.Format(assemblyDef, "ColorPresetLibrary"));
        Type helperType = Type.GetType(string.Format(assemblyDef, "ScriptableObjectSaveLoadHelper`1"))
                              .MakeGenericType(libraryType);
        MethodInfo createMethod = managerType.GetMethod("CreateLibrary", BindingFlags.Instance | BindingFlags.Public)
                                             .MakeGenericMethod(libraryType);
        var helper = Activator.CreateInstance(helperType, new object[] { "colors", SaveType.Text });
        var library = createMethod.Invoke(managerInstance, new object[] { helper, Path.Combine("Assets/Editor", name) });

//■4:

// We can't cast library to the desired type so we get the Add method through reflection

// and add the desired colours as presets through that.

// After that the library can be saved!


        if ((UnityEngine.Object)library != (UnityEngine.Object)null)
        {
            MethodInfo addPresetMethod = libraryType.GetMethod("Add");
            foreach (var color in colors)
            {
                addPresetMethod.Invoke(library, new object[] { color, color.ToString() });
            }

            MethodInfo saveMethod = managerType.GetMethod("SaveLibrary", BindingFlags.Instance | BindingFlags.Public)
                                             .MakeGenericMethod(libraryType);
            saveMethod.Invoke(managerInstance, new object[] { helper, library, Path.Combine("Assets/Editor", name) });
        }

■5:

// The library could be returned as an Object or ScriptableObject reference if that was useful

// I don't know of a way to cast it to the actual ColorPresetLibrary type.

   
    }

// Extension function because why not
    public static void CreateNewPresetLibrary(this List colors, string name)
    {
        CreateNewLibraryThroughPresetLibraryManager(name, colors);
    }
}

※誤訳があるかもしれませんのでスクリプト中のコメントで確認してください。

 1.  Unityは、PresetLibraryManagerクラスの型指定されたシングルトンインスタンスを通じて、内部的に新しいプリセットライブラリを作成します。リフレクションを通じてアクセスする必要がある2つの重要な方法は次のとおりです。

  • public T CreateLibrary(ScriptableObjectSaveLoadHelper helper, string presetLibraryPathWithoutExtension) where T : ScriptableObject
  • public void SaveLibrary(ScriptableObjectSaveLoadHelper helper, T library, string presetLibraryPathWithoutExtension) where T : ScriptableObject


  CreateLibraryは、ヘルパーオブジェクトを通してライブラリを作成する前に、いくつかのファイルパスのチェックを行いライブラリキャッシュに登録します。 SaveLibraryはヘルパーオブジェクトを通して何を言うのかを行います。
2つの呼び出しの間に、ライブラリーに実際にプリセットを追加することができます。
ヘルパーオブジェクトを直接使用してライブラリを保存することもできますが、Managerクラスで行うほうが安全でしょう。

2. ScriptableSingletonクラスはpublicですが、PresetLibraryManagerはpublicではないため ジェネリック型を作成し、リフレクションを使用して静的インスタンスプロパティを取得する必要があります。これは、ライブラリ登録の目的でシングルトンインスタンスが必要であると仮定していますが、必要ないのかもしれません。

3.  セーブ/ロードヘルパーのインスタンスを作成し、それをパスとCreateLibraryメソッドに渡します。"colors"は、ライブラリアセットに必要なファイル拡張子で '.'を必要としません。

4. ライブラリを目的の型にキャストできないため、リフレクションでAddメソッドを取得します プリセットとして必要な色を追加します。その後、ライブラリを保存することができます!
 
5.  有用であれば、ライブラリはObjectまたはScriptableObject参照として返すことができます。 ColorPresetLibraryタイプにキャストする方法はわかりません。
なぜ拡張機能なのでしょう?

 

この解説によればUnityではManagerクラスが用意されているので内部データへのアクセスはそちらを使用することで安全なアクセスができるようです。今回の場合PresetLibraryManagerのコードを参照することになります、

PresetLibraryManageにどのようなクラス定義がされているのか簡単に抜粋してみました

unity-decompiled/UnityEditor/UnityEditor/PresetLibraryManager.cs

 

  • public void GetAvailableLibraries(ScriptableObjectSaveLoadHelper helper, out List preferencesLibs, out List projectLibs) where T : ScriptableObject
  • private string GetLibaryNameFromPath(string filePath)
  • public T CreateLibrary(ScriptableObjectSaveLoadHelper helper, string presetLibraryPathWithoutExtension) where T : ScriptableObject
  • public T GetLibrary(ScriptableObjectSaveLoadHelper helper, string presetLibraryPathWithoutExtension) where T : ScriptableObject
  • public void UnloadAllLibrariesFor(ScriptableObjectSaveLoadHelper helper) where T : ScriptableObject
  • public void SaveLibrary(ScriptableObjectSaveLoadHelper helper, T library, string presetLibraryPathWithoutExtension) where T : ScriptableObject
  • private PresetLibraryManager.LibraryCache GetPresetLibraryCache(string identifier)
  • public List loadedLibraries
  • public List loadedLibraryIDs
  • public LibraryCache(string identifier)
  • public void UnloadScriptableObjects()

 

まだ全体を調べ終わっていないので実装までは時間がかかりそうですが、今回はここまで調べたということでメモ代わりに記事にしておきました。 参考になれば幸いです。 それではまた

 



akinow at 10:27|PermalinkComments(0) Clip to Evernote Unity3d | シリーズ講座

2018年02月19日

うにばな プロシージャルの2_マージドメッシュインスタンシング

 

 

■  1回ドローコールで複数のモデルをインスタンシング表示

今回のマージドメッシュインスタンシングの例

mergedmesh01

 

今回は複数のメッシュモデルをインスタンシングで同時に表示する つまり1ドローコールで何種類ものモデルを描画する方法ですね。

マージドメッシュ(MergedMesh)という呼び方があります。 インスタンシングによるバッチ処理も似たような効果ですがそちらはモデル自体をそのまま結合する方法なのである程度のデータの塊に限定されます。 一つのモデルが65536ポリゴンまで表示可能なこの方法とは少し異なります。

簡単にインスタンシング処理を説明するとCPUとGPUのメモリは分かれているのでメッシュデータはバスを通してGPUに毎回転送することになります。 転送済のデータのシャドーコピーをインスタンスとして複製表示することで描画処理を向上させるのが、メッシュのインスタンシングです。

複数のメッシュモデルはスタティックバッチやダイナミックバッチのような実行時にある程度の塊に結合して転送する方法があります GPUに一度に転送できるサイズは上限があるため複数回に分けて転送が行われますが。その回数分データ転送の処理時間がロスするため実行速度が低下することになります。

今回の方法ではメッシュ自体に形状をもたないトライアングルの塊を転送してGPUの側で復元するためどのような形状でもインスタンシング可能でメモリが許す限り何種類のメッシュでも1ドローコールの表示が可能です。

 

ちなみに画像の右上のstatではドローコールは2となっていますがこれは間違っていると思います 。表示物がないシーンでも

標準のライトパス:1 + シャドーパス:1 + カスケード:3 =5パス

のドローコールが必要なのでインスタンス表示自体のどローコールは1ですが 実際にはStatのドローコールは6程度になるはずです。がそれでも大分少ない描画負荷ですね。 カスケードというのはカメラの距離に応じて手前から奥に向かって3回シャドーのLODなどのレンダリングパスです

■マージドメッシュインスタンシングのあらまし

■fig1

MergeInstance3

fig1は実装例の画像です。同じような解説を繰り返しますが 図のように背景のレベルに同様のモデルが複数存在するような場合に インスタンシングが有効ですが、 インスタンシング描画とは一度GPU側に送ったメッシュデータをコピーして使いまわすことで描画速度を向上させる技術です、 メッシュモデルはそのままだとモデルオブジェクト一つにつき1ドローコールが発生しますが、それを軽減する技術がご存知のようにバッチ処理と呼ばれる複数モデルを結合転送する方法です。GPUに一度に送れる上限が65536ポリゴンまでなので、それ以上のメッシュサイズの場合はある程度何度かに分けて転送されます 分割された結合メッシュの個数分はドローコールが発生してしまうということになります。

そこで何度も描画される形状メッシュモデルをインスタンシング描画することで描画処理の速度を向上することができます。 ただし通常であればインスタンシングはセットされたメッシュモデル単体をコピーするのみなので、メッシュモデルの群衆を描画した場合でもすべて同じモデルが表示されてしまい、複数のモデル種類を表示する場合はモデルデータごと何度かインスタンシングのドローコールが発生してしまうためある程度までのモデル種類までは表示できますが 表現的には若干単調になってしまうかもしれません。 もちろんデスクトップPCなどのパワーの有るハードでであれば多少のドローコールは問題ないですが。ゲームの場合必ずしも高性能ハードで実行されるわけではないので少しでも余力を残したいところです。

 

■実装例:SIGRAPH2012より

■fig2

MergeInstance2

■fig3

MergeInstance1

fig3図のように メッシュインデックスはパックしたモデルデータにパックした順番にIDを割り付けます。 それらIDが示すのは頂点データ配列のインデックス <頂点座標、ノーマル、UV>などが格納されたデータの先頭インデックスを呼び出します。

それにより元のメッシュの頂点データが配列内の頂点データに置き換わり 見かけ上別のメッシュモデルを表示することができるという仕組みになります。

 

■マージドメッシュのポイントスプライト実装例

■fig4

MergrInstance_Ps

 

マージドメッシュを使用した基本コード例としてパーティクルのポイントスプライトが挙げられます。 GL系ではPointSprite命令をPontSizeというポイントに大きさを与える命令で実装しているコードを見かけますが。 HLSLシェーダの場合はマージドメッシュを使用してビルボードポリゴンを描画するという スニペットと呼んでよいのかな このような手法で実装されることが多いようです。

fig4のコードを簡単に解説します

頂点バッファにはこの場合Quad(四角形メッシュ)単位でデータが入力されるのでなので連続した4頂点が四角形ポリゴンの1セットとなります。

入力の id:SV_VERTEXID は頂点シェーダに入力される連続した頂点列に0から順番に頂点数分の割り付けられたIDで

 

uint particleIndex = id / 4;   パーティクルのインデクスは四角形メッシュの先頭を4で割り算して求め

uint VertexInQuad =id % 4; 4で割ったあまりが各四角形メッシュのサブIDで0,1,2,3 頂点順の番号を取得できます。

position.x = (vertexInQuad % 2 )  ? 1.0 : ?1.0; 四角形メッシュの4スミの座標を決定します。

position.y = (vertexInQuad % 2 )  ? -1.0 :  1.0;

position.z = 0.0;

このように入力頂点データから 四角形メッシュを復元します。 入力頂点座標データはどんな値が入ってきてもよいのですが、とりあえず 0で初期化されて入ってくると仮定します。

Position =  mul(Position , (float3x3)g_mInvView   )  +  g_bufPosColor[ ParticleIndex   ].Pos.xyz;

g_bufPosColor[ ParticleIndex ].Pos.xyz はパーティクルIDごとに座標データが帰るので、 Positionに加算することでパーティクルがバッファーの値の座標に移動します。バッファーはパーティクルのPosition座標が計算されて入力された配列です。

position.xy *= PARTICLE_RADIUS; パーティクル大きさにサイズを成分を掛けて 最後に頂点にMVP(プロジェクション行列)を掛けてワールド上のカメラからの見かけ位置に配置するとパーティクルビルボードが完成します いつもの演算ですね。

 

 

■シェーダスクリプトの実装

meshIndex0_thumb[1]

 

頂点IDを取得する場合 HLSLでは標準では以下のようになります

v2f vert (uint id : SV_VertexID, uint inst : SV_InstanceID){}

uint id : SV_VertexID :  頂点IDは入力されたメッシュモデルの頂点番号順に割り付けられるID

uint inst : SV_InstanceID: インスタンシングされたメッシュモデルごとに割り付けられるID

idデータを別のシェーダ内で使いまわしたい場合はPOSITIONデータなど同じく構造体に含める必要があります。

Unityヘルパー関数があるので以下のように

UNITY_VERTEX_INPUT_INSTANCE_ID :頂点IDの取得
UNITY_SETUP_INSTANCE_ID(v);    :インスタンスID の取得
UNITY_TRANSFER_INSTANCE_ID(v, i);
unityマクロでTRANSFERの名称がついている場合は構造体に含める作業を省略できます。

書き方はどちらでもかまわないです。Unity以外の既存のシェーダを書き換える場合などに参考にはなります。

fig2で vertex頂点の処理をfor命令で表現していますが、シェーダを入力されたデータ数だけ処理を実行するFOREACHループ

と捉えると、理解しやすいかもしれません。

vertexシェーダは入力頂点数分のループで、fragmentシェーダはスクリーン上のピクセル数分のループと考えてください。

 

トライアングルメッシュデータを今回は Graphics.DrawProcedural( )  命令で転送しますこれはMeshTopology.Trianglesか MeshTopology.Quadsのようにストリップの方法を指定してインスタンス描画する命令です。

Quads(四角形ポリゴン)は内部的にはTriangles(三角形ポリゴン)が2回記述するのと同義なので、Triangles(三角形ポリゴン)だけを対応します。

シークエンスは以下のようになります

  1. 複数のメッシュモデルからVertexデータを取得してストラクチャバッファないしテクスチャバッファにセットする。データはすべてを連続して配列にまとめて、それぞれのメッシュモデルのトライアングル数をシェーダに配列で渡す。インデックスIDはトライアングル数と対応させておく。
  2. インスタンシングには初期化された大きさのないトライアングルメッシュをセットする トライアングルの全体数は描画するメッシュモデルのポリゴン数をカバーできる大きさに設定する。つまりGPUに一度に転送できる最大ポリゴン数65536であればロスはあるが再設定の必要なくすべてのモデルデータのポリゴンサイズがカバーできることになる。
  3. 頂点シェーダで描画する際にSV_VertexIDをもとにしてバッファ(ストラクチャバッファ)から対応する頂点データを呼び出し頂点データを置き換える。 これによりポリゴントライアングルが大きさやノーマルなど属性を与えられ描画される。今回は頂点データをトライアングルに1体1対応させているので、データサイズは多少大きくなるがポイントスプライト例のようなVertexIDを割り算するような対応は必要ない。
  4. インスタンシング命令の描画時に設定したモデルのポリゴン数を上回った余分なポリゴンメッシュを描画スキップさせる。 頂点シェーダにはフラグメントシェーダのようにスキップ命令がないので、ポリゴンにスケール0を代入するか頂点座標にfloat3(0、0、0)を代入するまたは頂点アルファで非表示にするなどの方法が考えられる。

欠点というか注意点は

  1. ドローコールを抑えるにはマテリアルは基本的に1種類なので同じようなマテリアルを使用するモデル同士で結合する必要がある。
  2. メッシュ結合時に無駄なメッシュ部分をなるべく減らすために大体同じようなサイズのメッシュでまとめる必要がある。

 

■配列の転送

meshIndex

気をつけるポイントは 繰り返しますが

  • なるべく同じポリゴンサイズのメッシュでまとめて表示することでデータのロスを減らすこと
  • 偶数倍のサイズを指定すること :固定サイズの倍数を指定するだけでデータの先頭インデックスを計算できます。 条件分岐を発生させないため演算処理は向上します

下のように先頭インデックスの計算は必要となりますが、データの終了判定が必要なくなる分演算が軽くなります。ハードのスペックにもよりますけど・フレームレートで比較してもらうと条件分岐の処理がありなしで結果がだいぶ変わることがわかるかと思います。

id_offset =  _SegmentsID[inst]* _MaxLength

UNITY_BRANCH

if (id < id_offset + _SubDataSize[_SegmentsID[inst]])

?

id_offset =  _SegmentsID[inst]* _MaxLength

パディング(詰め物)でサイズを調整することでIF分岐の記述が必要なくなり処理速度の最適化につながります。

ちなみに if分岐で 比較時に(uint)型id と int型のindexで型エラーが生じると思いますので、速度は uint<int< float ですのでこの場合はuintをint型にキャストしてください

 

あと掲載するまでもないと思うのですが。 List<> 操作ですね STRUCTを使用する場合とCLASSを使用する場合がありますが

配列の大きさが不定の場合使用すると操作が楽になりますね 一応CLASS多用はメモリ分断が起きやすいのガベージコレクション上の問題があるという最適化の点から構造体でLIST操作をするのが良いかもしれません。 LIST型と配列は相互に変換できますのでケースに応じてします。

とりあえず初心者向けな解説

using System.Collections.Generic;

...
   List ppoints = new List();
    ppoints.Add(new Point {  });

           または

  List DataItems = new List();
   var cd = new ColumnData();
   cd.ColumnName = "Taco";
   DataItems.Add(cd);

string[] stringArray = stringList.ToArray();

 

 

■分岐命令のマクロ

Unityはif分岐で UNITY_BRANCH UNITY_FLATTEN のマクロ命令が定義されていますが これは HLSLのBRANCHとFLATTENを置き換えるだけのマクロ命令なので

  • GLには作用しません
  • #if はUnityのマルチコンパイルのマクロですのでこれにも作用しません

BRANCH オプションは分岐の片方だけを実行して評価するオプションでマルチコンパイルみたいなものでHLSLの場合にのみ有効。

FLATTENは分岐の両側を両サイドを実行して一方の値を選択します。 旧式のGPUで採用されている分岐がFLATTEN型なので処理が重かった

ということですが、 FLATTENの用途としてはif分岐でどちらか一方のみで定義された値がもう一方で参照されてもエラーが起こらない ということですが あまり使い所が思いつかないですね。

BRANCHは本家のフォーラムだとあまり期待するなというアドバイスもありましたが。害がないのでとりあえず書いておけばいいんじゃないかなと

 

■シェーダーの配列サイズ

モデルの種類にIDを指定する配列はコンスタントバッファで指定しています。IDをは入れるインデックスを指定すためにint型で指定しますがUnityでSetIntArrayはサポートされていない様です。しかし SetFloatArrayで代用できるようです コンスタントバッファにInt型で配列を確保して スクリプトから渡すデータはフロート型でシェーダ内はInt型でデータが渡るようなのでこれでいきます。

以前も書きましたが配列はシェーダ内で特に指定をしない場合はコンスタントバッファ扱いのようです コンスタントバッファは1024バイトのストライド長なので 。モデルの種類サイズが大きくなるようであればストラクチャバッファなど大きなサイズの配列を確保してください。もっともそんなに何種類も同時に表示する場面はなさそうなのですが。

シェーダでは配列の動的サイズは確保できませんので、マルチコンパイルオプションでキーワードをセットして

スクリプト側:

Shader.EnableKeyword("MYDEFINE")
Shader.DisableKeyword


シェーダー側:


#if defined(MYDEFINE)
...

#endif

今回の場合例えば次のように#defineで配列を初期化するようにしておけばスクリプト側から配列サイズを制御する。

#define ARRAY_SIZE128

#define ARRAY_SIZE256

#define ARRAY_SIZE512

#define ARRAY_SIZE1024

※シェーダ内部からならKeywordを使用して#defineで書きますか

//////////////////////////////////////////////////////////////////////////

#if defined(ARRAY_SIZE128 )

float _Array[128];

#endif

#if defined(ARRAY_SIZE256 )

float _Array[256];

#endif

#if defined(ARRAY_SIZE512 )

float _Array[512];

#endif

#if defined(ARRAY_SIZE1024)

float _Array[1024];

#endif

この書き方で多分大丈夫だと思いますが SSAOシェーダを参考にすると良いかも。

 

■構造体を定義する場合のTips的なもの

一つの構造体にパックする変数のサイズにも制限があって DirectXは128ビットを一単位として扱うそうで、例えばfloat4型などが転送の最小単位となるようです。 struct構造体単位が128ビットの倍数でパックされるようにしないと 足りないビット分は構造体の次の要素の一部からをセットで転送しようとするため転送ロスが発生するということだそうです。 場合によっては30%程度の速度低下が生じることがあるそうで、公開されてるコードで確認するとだいたいそうならないように最適化されていると思います。

struct Point {
	float3  vertex;
	float3  normal;
	float4  tangent;
	float2  uv;
	float4  color;
};

構造体を設計する場合に128ビットの倍数に足りない分は必要がなくてもfloat4型に定義するか、またはダミーのパディングデータを含めておくことで転送の速度ロスが軽減できるということです。 もちろん実行速度が十分なら気を使う必要はないですよ 既存コードを参考にしたときにデータを軽くしようとしてうっかり最適化部分を削ってしまうことはありそうなので 知識として持っておく程度でかまいません。

struct Point {
	float3  vertex;
	float3  normal;
	float4  tangent;
	float2  uv;
                                float2 uv2;
	float2 dummy;   このようにダミーデータを含めることで全体で128ビットの倍数になるように
     または float4 ....;にしてしまう
};

■テクスチャのアトラス化

モデル単位で複数のテクスチャ指定があると条件分岐が必要になるため 処理負荷軽減のためにもアトラス化が必要になります。 他の理由としてテクスチャ選択に条件分岐があると描画が崩れる場合があったのですが原因はまだわかりません。

スクリプトでメッシュを配列にパックする段階でアトラス化されたUVが指定されるようにすればどのようにUVがアトラス化されていても構いません。あらかじめUVがスクリプトのUV座標の計算部分はスルーして元のモデルからコピーするだけです。

InstancedMesh_Color

■LOD対応

メッシュLODはインスタンシングでオプションパラメータがサポートされていますが 今回の方法の場合はLODモデルを一緒にメッシュ配列にパックしカメラからの距離を参照してLODメッシュに割り付けたIDで選択するようにすれば良いと思います。

通常のインスタンシングの場合pragmaを記述するとlodに自動で対応できます。
 #pragma instancing_options lodfade 

■カリング対応

テセレーションシェーダからの抜粋ですが unity_CameraWorldClipPlanes[planeIndex] とトライアングル頂点の内積(Dot)をとることで トライアングルメッシュがスクリーンの内側にあるかの判定を行い 3頂点とも外部にあれば描画をスキップするようにすることでポリゴン単位でのカリングができます。 トライアングルがスクリーンを覆うように大きく表示される場合もあるので、重心で判定などの手抜きは突然欠けたりで危険ですね。

bool TriangleIsBelowClipPlane ( float3 p0, float3 p1, float3 p2, int planeIndex ) {
float4 plane = unity_CameraWorldClipPlanes[planeIndex];
return
dot(float4(p0, 1), plane) < 0 &&
dot(float4(p1, 1), plane) < 0 &&
dot(float4(p2, 1), plane) < 0;
}

モデル単位であれば、バウンディングボックスにモデルごとにトランスフォーム行列を掛けて 同様にスクリーンの内部にあるか判定をすれば良いと思います。 できる限りシェーダーでできることはスクリプトで書くのを避けたほうが良いと思います。

 

■コリジョンチェック

基本的にはシェーダ側の処理でできることはシェーダ側で済ませないと行けません CPU側からデータを触るとデータの移動が発生するためせっかく描画処理が向上してもそこで処理速度の低下を招きますので、セットするデータはなるべく小さく、できれば数フレームに一回で済むようにしたほうが良いです。シェーダ側のほうがCPUに比べて数十倍実行速度が速いので頻繁に値が書き換わるとデータ取得がコケる。調整すれば使えるかな。という範囲です。

プレイヤーの座標データをシェーダ側にセットすればインスタンスとlength()関数を使用して距離が計測できるので、最短距離または距離を近い順に並べた配列のインスタンスIDをシェーダ側の変数にセットする。プレイヤー座標とインスタンスの座標の方向が取得したい場合はベクトルもセットしておくことで、スクリプト側でGetを使用して変数の値を呼び出せばプレイヤーのヒット情報と状態が取得できます。 描画とはシェーダを分けて書いても良いし 描画数が多ければコンピュートシェーダが有効です。 AIのからみになると背景とのヒットチェックはDepthテクスチャを使用して計算するか または 上方からコリジョンを2Dレンダリングした背景画像を使用するなどで対応はできます。 スワップバッファを用いたスワップチェーン(シェーダで計算結果をレンダテクスチャで連続で入れ替える方法 パーティクル描画などで使いますね)かコンピュートシェーダを使用する方法になると思いますが NVIDIAには無いサンプルでもAMDかINTELのサンプルデモにはあったかと思います。

 

 

■コード・インスペクタなど サンプル

  • インスペクタの表示
mergedmesh02 インスペクタはプレファブエレメンツのサイズを指定して
マテリアルにシェーダをセットするだけです。

PREFABS:
メッシュモデルにMeshFilterをセットしてください IDは0から順番に割り付けます

INSTANCE COUNT:
全体のインスタンシングの数

SEGMENT LENGTH:
モデルの最大ポリゴン(トライアングル)数を セットしますポリゴンサイズが値より小さい場合データの残りがパディングで埋められます。

TEXDIM X、Y:
テクスチャアトラスのx,yの分割数です

 

■SAMPLE CODE A:   メッシュ配列にパディングを追加しているバージョン

"ProceduralMeshIstancing.cs”

 
using UnityEngine;
using System.Runtime.InteropServices;
using System.Collections.Generic;

namespace   ProcedualScene {

public struct Point
{
public Vector3 vertex;
public Vector3 normal;
public Vector4 tangent;
public Vector2 uv;
public Color color;
}

  [ExecuteInEditMode]
    public class ProceduralMeshIstancing : MonoBehaviour
    {
        public Material _Material;

        [SerializeField]
        private GameObject[] _Prefabs;

        public int _InstanceCount = 10;
        private ComputeBuffer _ComputeBuffer;
      
        private int _MeshBufferSize = 0;

        [SerializeField]
        private int _SegmentLength = 65000;

        [SerializeField]
        private int _TexDimX = 2;
        [SerializeField]
        private int _TexDimY = 2;


        void Start()
        {

            Mesh mesh;

            for (int i = 0; i < _Prefabs.Length; ++i)
            {
                mesh = _Prefabs[i].GetComponent().sharedMesh;
                _MeshBufferSize += mesh.triangles.Length;

            }

            _MeshBufferSize = _SegmentLength * _Prefabs.Length;

            Point[] points = new Point[_MeshBufferSize];

            for (int i= 0; i < _MeshBufferSize; ++i)
            {
                points[i].vertex = Vector3.zero;
                points[i].normal = Vector3.zero;
                points[i].tangent = Vector4.zero;
                points[i].uv = Vector2.zero;
                points[i].color = Color.white;
            }


            for (int j = 0; j < _Prefabs.Length; ++j)
            {
                mesh = _Prefabs[j].GetComponent().sharedMesh;

                for (int i = 0; i < mesh.triangles.Length; ++i)
                {
                    points[_SegmentLength*j + i].vertex = mesh.vertices[mesh.triangles[i]];
                    points[_SegmentLength*j + i].normal = mesh.normals[mesh.triangles[i]];
                    points[_SegmentLength*j + i].tangent = mesh.tangents[mesh.triangles[i]];
                    points[_SegmentLength*j + i].uv = mesh.uv[mesh.triangles[i]];

                    points[_SegmentLength*j + i].uv.x /= _TexDimX;
                    points[_SegmentLength*j + i].uv.y /= _TexDimY;
                    points[_SegmentLength*j + i].uv.x += (j % _TexDimX) * 1.0f / _TexDimX;
                    points[_SegmentLength*j + i].uv.y += (1.0f - j / _TexDimX) * 1.0f / _TexDimY;

                    points[_SegmentLength*j + i].color.r = j / 255.0f;

                }
 
            }
          
            _Material.SetInt("_MaxLength", _SegmentLength);

            _ComputeBuffer = new ComputeBuffer(_MeshBufferSize, Marshal.SizeOf(typeof(Point)), ComputeBufferType.Default);
            _ComputeBuffer.SetData(points);
            _Material.SetBuffer("points", _ComputeBuffer);

            var Prefab_id = new float[_InstanceCount];

            for (int i = 0; i < _InstanceCount; ++i)
            {
                Prefab_id[i] = Random.Range(0, _Prefabs.Length);
            }

            _Material.SetFloatArray("_SegmentsID", Prefab_id);

        }

        void OnRenderObject()
        {
            _Material.SetPass(0);
            Graphics.DrawProcedural(MeshTopology.Triangles, _SegmentLength, _InstanceCount);       
        }

        void OnDestroy()
        {
            if (_ComputeBuffer != null)
                _ComputeBuffer.Release();
            _ComputeBuffer = null;

        }
    }
}


 

"DX11 MergedInstancing.shader”

  Shader "DX11 MergedInstancing" {

   Properties
    {
    _MainTex("Texture", 2D) = "white" {}
    }
     SubShader {
         Tags {"LightMode" = "ForwardBase" }
                
         Pass {
         CGPROGRAM
         #include "UnityCG.cginc"
         #pragma target 5.0 

         #pragma vertex vert
         #pragma fragment frag
      
         sampler2D _MainTex;
         float4 _MainTex_ST;

         uniform fixed4 _LightColor0;

CBUFFER_START(UpdatedSegmentVariables)
int _SegmentsID[1024];
int _MaxLength;
CBUFFER_END
       

struct Point {
float3  vertex;
float3  normal;
float4  tangent;
float2  uv;
float4  color;
};

StructuredBuffer points;

         struct v2f {
             float4 pos : SV_POSITION;
             float4 col : COLOR;
             float2 uv : TEXCOORD0;

         };

         v2f vert (uint id : SV_VertexID, uint inst : SV_InstanceID)
         {
             v2f o;

int  id_offset = (int)(_SegmentsID[inst]* _MaxLength);
             id += id_offset;

// 頂点IDごとバッファーから頂点データを取得します
                float4 vertex_position =  float4(points[id].vertex,1.0f);
             float4 vertex_normal = float4(points[id].normal, 1.0f);
// 座標変換はここに記述してください
vertex_position.x += 1.0f*(inst%10);
vertex_position.z -= 1.0f*(inst / 10);

             o.pos = mul (UNITY_MATRIX_VP, vertex_position);
             o.uv =  TRANSFORM_TEX(points[id].uv, _MainTex);

             float3 normalDir = normalize(vertex_normal.xyz);
float4 LightDir = normalize(_WorldSpaceLightPos0);
float4 DiffuseLight = saturate(dot(normalDir, LightDir))*_LightColor0;
             float4 AmbientLight = UNITY_LIGHTMODEL_AMBIENT*3.0;
           
             o.col=float4(AmbientLight + DiffuseLight);

             return o;
         }

         fixed4 frag(v2f i) : SV_Target
         {
    fixed4 finalColor;
   
    finalColor = tex2D(_MainTex, i.uv);
    finalColor *= i.col*1.8;
            return finalColor;
         }
      
         ENDCG
      }
     }
}


 

■SAMPLE CODE B: メッシュデータを配列にパディング無しで詰めたバージョン

"ProceduralMeshIstancing2.cs”

 
using UnityEngine;
using System.Runtime.InteropServices;
using System.Collections.Generic;

namespace   ProcedualScene2 {

public struct Point
{
public Vector3 vertex;
public Vector3 normal;
public Vector4 tangent;
public Vector2 uv;
    public Color color;
}

//  [ExecuteInEditMode]
    public class ProceduralMeshIstancing2 : MonoBehaviour
    {
        public Material _Material;

        [SerializeField]
        private GameObject[] _Prefabs;

        public int _InstanceCount = 10;
        private ComputeBuffer _ComputeBuffer;
      
        private int _MeshBufferSize = 0;

        [SerializeField]
        private int _SegmentLength = 65000;

        [SerializeField]
        private int _TexDimX = 2;
        [SerializeField]
        private int _TexDimY = 2;


      

        void Start()
        {

            Mesh mesh;
           
            var _SubIndex= new float[_Prefabs.Length];
            var _SubDataSize=new float[_Prefabs.Length];



         
            for (int i = 0; i < _Prefabs.Length; ++i)
            {  
                mesh = _Prefabs[i].GetComponent().sharedMesh;

                _MeshBufferSize += mesh.triangles.Length;
                _SubDataSize[i] = mesh.triangles.Length;
               
            }
            _SubIndex[0] = 0;
            for (int i = 0; i < _Prefabs.Length-1; ++i)
            {
                _SubIndex[i+1] += _SubIndex[i]+_SubDataSize[i];
            }

          
              


                List points = new List();

            for (int j = 0; j < _Prefabs.Length; ++j)
            {
                mesh = _Prefabs[j].GetComponent().sharedMesh;

                for (int i = 0; i < mesh.triangles.Length; ++i)
                {

                    var ppoint = new Point();
                    ppoint.vertex = mesh.vertices[mesh.triangles[i]];
                    ppoint.normal = mesh.normals[mesh.triangles[i]];
                    ppoint.tangent = mesh.tangents[mesh.triangles[i]];
                    ppoint.uv = mesh.uv[mesh.triangles[i]];

                    ppoint.uv.x /= _TexDimX;
                    ppoint.uv.y /= _TexDimY;
                    ppoint.uv.x += (j % _TexDimX) * 1.0f / _TexDimX;
                    ppoint.uv.y += (1.0f - j / _TexDimX) * 1.0f / _TexDimY;

                    ppoint.color.r = j / 255.0f;

                    points.Add(ppoint);
                   
                }
 
            }
          
            _Material.SetInt("_MaxLength", _SegmentLength);

            _ComputeBuffer = new ComputeBuffer(_MeshBufferSize, Marshal.SizeOf(typeof(Point)), ComputeBufferType.Default);
            _ComputeBuffer.SetData(points.ToArray());
            _Material.SetBuffer("points", _ComputeBuffer);

            var Prefab_id = new float[_InstanceCount];

            for (int i = 0; i < _InstanceCount; ++i)
            {

            Prefab_id[i] = Random.Range(0, _Prefabs.Length);
               //  Prefab_id[i] = 1;
                Debug.Log("prefab=" + Prefab_id[i]);
            }

            _Material.SetFloatArray("_SegmentsID", Prefab_id);
            _Material.SetFloatArray("_SubIndex", _SubIndex);
            _Material.SetFloatArray("_SubDataSize", _SubDataSize);

        }

        void OnRenderObject()
        {
            _Material.SetPass(0);
            Graphics.DrawProcedural(MeshTopology.Triangles, _SegmentLength, _InstanceCount);       
        }

        void OnDestroy()
        {
            if (_ComputeBuffer != null)
                _ComputeBuffer.Release();
            _ComputeBuffer = null;

        }
    }
}

"DX11 MergedInstancing.shader2”


Shader"custom/DX11 MergedInstancing2" {

   Properties
    {
		_MainTex("Texture", 2D) = "white" {}	
    }
     SubShader {
         Tags {"LightMode" = "ForwardBase" }
                 
         Pass {
         CGPROGRAM
#include "UnityCG.cginc"
         #pragma target 5.0  

         #pragma vertex vert
         #pragma fragment frag
       
sampler2D _MainTex;
float4 _MainTex_ST;
	
uniform fixed4 _LightColor0;

CBUFFER_START(UpdatedSegmentVariables)
int _SegmentsID[256];

//int _MaxLength;
CBUFFER_END

CBUFFER_START(UpdatedSegmentVariables1)

int _SubIndex[256];
int _SubDataSize[256];
CBUFFER_END


struct Point
{
    float3 vertex;
    float3 normal;
    float4 tangent;
    float2 uv;
    float4 color;
};

StructuredBuffer points;

struct v2f
{
    float4 pos : SV_POSITION;
    float4 col : COLOR;
    float2 uv : TEXCOORD0;
			 
};
 
v2f vert(uint id : SV_VertexID, uint inst : SV_InstanceID)
{
	v2f o;

	
	int id_offset = (int)(_SubIndex[_SegmentsID[inst]]);
	id += id_offset;



	float4 vertex_position;
	float4 vertex_normal;
//	頂点IDごとバッファーから頂点データを取得します
	UNITY_BRANCH

	if ((int)id < (int)(id_offset + _SubDataSize[_SegmentsID[inst]]))
	{
	
	vertex_position = float4(points[id].vertex, 1.0f);
	vertex_normal = float4(points[id].normal, 1.0f);

             }
	else
	{
	vertex_position = float4(0,0,0,0);
        vertex_normal = float4(1,1,1,1);
	}

///////////座標変換はここに記述してください
     vertex_position.x += 1.0f * (inst % 10);
    vertex_position.z -= 1.0f * (inst / 10);
///////////////////////////////////////////
    o.pos = mul(UNITY_MATRIX_VP, vertex_position);
    o.uv = TRANSFORM_TEX(points[id].uv, _MainTex);
			
    float3 normalDir = normalize(vertex_normal.xyz);
    float4 LightDir = normalize(_WorldSpaceLightPos0);
    float4 DiffuseLight = saturate(dot(normalDir, LightDir)) * _LightColor0;
    float4 AmbientLight = UNITY_LIGHTMODEL_AMBIENT * 3.0;
            
    o.col = float4(AmbientLight + DiffuseLight);

    return o;
}
 
fixed4 frag(v2f i) : SV_Target
{
    fixed4 finalColor;
    
    finalColor = tex2D(_MainTex, i.uv);
    finalColor *= i.col * 1.8;
    return finalColor;
}
       
         ENDCG
      }
     }
}

 

■その他 今後の展開など

メッシュモデルを何種類表示しても 基本ドローコールは1となりますが 描画処理の負荷はかかるのでFPSは安定して高いというわけにはいきません。 樹木や、建物、プロップ類、ガレキ、キャラクタ あるいはエフェクト類などでまとめられるものを一度に表示することで、描画の負担は軽減できますのでアイデア次第では使えるテクニックです。

今回のサンプルはMeshFilterのみで作成しました。スクリプト部分は難しいところはないので 自分が使いやすいメッシュ結合の スクリプトを書き換えれば良いです。メッシュ結合はあまり処理が早いとはいえないので ゲームに実装する場合はプリプロセッサでメッシュにかためてプレファブ化するかasset化するかして 各メッシュのトライアングル数だけテーブルでシェーダに送れるようにします。

コードを読めばわかりますがトライアングルのデータに合わせて直接メッシュの頂点データを埋め込んでいます。 頂点データは重複しますが余分な計算が入らないので、スタティックメッシュなら多少データ量が増えた場合でも速度面で恩恵があると思います。頂点アニメーションを考慮するならば、頂点データを頂点へのインデックスIDで間接的に指定するほうが、一手間かかりますがアニメーション部分で演算量は減ります。

今回のサンプルコードで構造体の頂点カラーは未使用なのですが、頂点カラー部分に頂点へのインデックスIDを入れられますので インスタンスメッシュに更にサブメッシュを制御したい場合。 例えばキャラクターのメッシュに装備がついている場合や背景にオプション物がある場合などに表示非表示の制御 あるいは部分的に動きを与えるという際にはフラグとして活用することができますので予約として記述してあります。 頂点カラーでなくてもUV2など余っているところに自由にセットしてももちろんかまいません。

全体が65536ポリゴン以下で収まってしまう場合はバッチ結合とどうように、原点で複数メッシュモデルをマージして なんらかの方法(たとえば頂点カラー)でIDを指定して表示をコントロールすれば、同様に見かけ上の複数モデルの同時表示は可能です。 パーティクルと組み合わせる場合はこうした方法でも良いかもしれません。

最近はテクスチャ配列を使用した頂点アニメーションがクローズアップ されていますがわりと昔からある手法で確か2009年のGPUGEMsのインスタンシング記事には登場していました。 以前テクスチャ配列の記事を書いたときは、そこら辺につなげようとしていたのですが、まだUnityのインスタンシング周りが未完成だったため断念した経緯がありました ちょっと手を出すのが早すぎましたね 大分時間が経ってから記事のアクセスが伸び始めましたし。 テクスチャ配列のアニメーションはシンプルなため処理は軽いですが、頂点数が増加するとテクスチャサイズが肥大化するので、いまのところあまり大きなサイズのメッシュには適用できないという弱点もあります。65536頂点なら256*256テクスチャがアニメーションフレーム数分 ノーマルなどの要素もベイクするとさらにその数倍のテクスチャメモリが必要となります。キーフレームを削減するうまい方法ができれば解決できそうですが。 ボーンマトリクスだけベイクしてスキンウェイトシェーダを自前で書く skinweight対応したシェーダは公開されていますかね。あるいはローポリゴンのプロキシからハイメッシュデータに移し替えるような方法であればメモリの少ない低スペックハードにも恩恵がありそうです。 もっともロースペックマシンでそこまで無理する必要があるのかは、わかりません。

時間が許せばその辺も解説するかもしれませんが

 

■リファレンスリンク

 

 

■後記

更新間隔がだいぶ空いてしまっているので、更新のついでに感想を

Unityの記事は何故かゲームメーカに限らずメーカーさんのアクセスが多いのですが、Unityの記事が比較的初心者ユーザが読むことを前提に書かれているため コードを読めばデータの流れから意味が理解できるが、数式が苦手。というゲーム開発者さんが理解しやすく Unityエンジン以外でも参考になると そういう理由だと聞きました。 なんだか責任重大ですね 

数学苦手なプログラマさんとか意外と多いんですけどね(笑

最近は特許関係でいろいろ話もありました アレとかアレなど。 関連した話をしますと 海外では新し目の技術にはなるべく特許を取らないようにして 業界の発展を促そうという思想があるようで、ネット検索すると例えばシェーダーやAIや物理演算やらはソースがほとんどオープンになっていたりするのがわかるかと思います。 そこで特許の代わりになるものが、サイト記事やコードの中に貼られているリファレンスリンクというもので、製作時に参考にしたもの PDFやスライド。サイト、ブログ記事。書籍など できる限り記載することで学習効果をお互いに高めていきましょうという狙いがあるそうです。 国内の現状を見ればお互い権利主張した結果 開発は全体にブレーキがかかって衰退ムードになっていったというのがわかるのでは。 もちろん海外でもすべての新しめのコードやアイデアに特許がないわけではないので侵害してしまう場合もあります 例えば個人ライセンスを記載ミスでオープンにしたりといったような事故も起きることがあるそうなので、問題が起きないため 使用する側も権利侵害に抵触しないか調べる手間が省けるメリットもあるので、なるべくリファレンスの記載をするように心がけるのはマナーとして正しいような気がします。 罰則があるわけではないのであくまでマナーの範疇ですが。 ほとんどリファレンスリンクがないサイトもあるので判断に迷うこともあるかもしれませんが、初心者率の高めなUnityユーザーは後から続く開発者のためにも迷ったらとりあえずリファレンスリンクは貼ってくれるとうれしいかな と思います。

と言うことで今回は、ここまでです。 毎度長文で申し訳ないですが読んでいただいて有難うございました。

※git.gistのHighlighterを前回使用してみたのですが、表示のたびにサイトを呼び出すため重たいそうで、検索したところ評判があまり良くないようです。 今回は見送りましたが次回からシンタックスハイライトをgoogle code prettifyなど JSコードをサイトに埋め込むほうが表示が軽くなるようなので、そちらで対応しようかと考えています。ブログがフリー版の時代はアップロード機能が使用できなかったためうまい方法が見つからずハイライトなしでしたので、見づらいという指摘を受けてましたけど ほんと コード部分見づらくて申し訳ないです。



akinow at 15:41|PermalinkComments(0) Clip to Evernote Unity3d | シリーズ講座