テラシュールブログ

旧テラシュールウェアブログUnity記事。主にUnityのTipsやAR・VR、ニコニコ動画についてのメモを残します。

【Unity】UnityPhysicsで、オブジェクト同士が接触した判定を取得する

f:id:tsubaki_t1:20190425223640g:plain

物理演算でオブジェクト同士が接触した時のイベントを発行する方法についてです。

なお、現段階では明らかに低レベルAPIのみしか提供されておらず、未完成です。

この記事は「今すぐ触ってみたい」という人向けで、正直あまり有益ではありません。

接触イベント

榴弾の発射」や「玉を地面に落とした時」、「キャラクターが特定の範囲に到達した時」など、動く物体がオブジェクトに接触した時に何らかのイベントを発行したくなります。

UnityのPhysics(GameObjectがPhysXを利用)ではOnCollisionEnterやOnTriggerEnterを設定すれば良いのですが、UnityPhysicsは現時点では低レベルAPIしか提供していないため、割と面倒くさい処理が必要になります。

使用環境

  • Unity 2019.1f2
  • Unity Physics preview1 0.0.2

セットアップ

イベントを受け取るにはColliderからの変換ではなくPhysicsShapeを使用する必要があります。その上で PhysicsShape > Advance > Raises Collision Event にチェックを入れます。

(イベントを発行しない側はColliderにIsTrigerを設定しUnityPhysicsに変換するだけで良いです)

f:id:tsubaki_t1:20190425224053j:plain

これでチェックを入れたEntityが他のオブジェクトに接触した時、イベントが発行されます。あとは発行されたイベントを総なめで内容を確認するシステムを用意します。

まずシステムの実行順ですが、StepPhysicsWorldの後に実行します。

[UpdateAfter(typeof(StepPhysicsWorld))]
public class ItemGetSystem: ComponentSystem
{

次にイベントの取得と全Colliderの取得用に、BuildPhysicsWorldStepPhysicsWorldを用意します。

    private BuildPhysicsWorld buildPhysicsWorldSystem;
    private StepPhysicsWorld stepPhysicsWorldSystem;

    protected override void OnCreate()
    {
        buildPhysicsWorldSystem = World.GetOrCreateSystem<BuildPhysicsWorld>();
        stepPhysicsWorldSystem = World.GetOrCreateSystem<StepPhysicsWorld>();
    }

実際の中身を処理する部分です。

buildPhysicsWorldSystemからPhysicsWorldを取得します。この中に現時点での全てのColliderの座標等が含まれます。

同様にstepPhysicsWorldSystemからTriggerEventsを取得します。この中にRaises Collision Eventが有効なオブジェクトが発行したイベントが格納されています。もしCollider同士の接触判定を取りたい場合はCollisionEventsです。

あとはEventTriggerをキーに接触中のオブジェクトを取得して、イベント処理するだけです。

    protected override void OnUpdate()
    {
        var physicsWorld = buildPhysicsWorldSystem.PhysicsWorld;
        var triggerEvents = stepPhysicsWorldSystem.Simulation.TriggerEvents;
        var items = GetComponentDataFromEntity<ItemTag>(true);

        foreach (var triggerEvent in triggerEvents)
        {
            // PhysicsWorldに全ての物理演算で動くEntityが含まれる。
            // triggerEventsには、Raises Collision Eventが有効なオブジェクトが接触イベントを格納する
            // TriggerEventの中身を洗えば、接触判定が取れる
            var bodyA = physicsWorld.Bodies[triggerEvent.BodyIndices.BodyAIndex];
            var bodyB = physicsWorld.Bodies[triggerEvent.BodyIndices.BodyBIndex];
            
            Process(bodyA.Entity, items);
            Process(bodyB.Entity, items);
        }
    }

全文はこちら

Unity Physicsの使用例 · GitHub

なお、Unity Physicsはステートを持たないので、「接触開始」や「接触中」「接触終了」といったイベントは取れないみたいです。

特定のオブジェクト同士でのみ接触イベントを行いたい場合、上のコードのようにComponentDataFromEntityで特定のコンポーネントを持っていないかを判断するか、もしくはCustom Flagを設定しておくとBodyA.CustomData で判断が出来ます。

f:id:tsubaki_t1:20190425221913j:plain

単純に接触したくない場合はCollision Filterを調整すると良いみたいです。この辺りのインターフェースはもう少し頑張ってほしい所です。

f:id:tsubaki_t1:20190425222055j:plain

JobSystemで動かしたい

上の処理をJobSystemで動かしてみました。

ポイントは処理の依存関係を設定することです。UnityPhysicsはECSを使用していますがECS上で動作していないので、処理の依存関係を自動的に解決してくれません。システムから持ってくる必要があります。

        inputDeps = JobHandle.CombineDependencies(
            inputDeps, 
            buildPhysicsWorldSystem.FinalJobHandle,
            stepPhysicsWorldSystem.FinalSimulationJobHandle);

全文はこんな感じになります。

using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Mathematics;
using Unity.Physics;
using Unity.Physics.Systems;

[UpdateAfter(typeof(StepPhysicsWorld)), UpdateBefore(typeof(EndFramePhysicsSystem))]
public class ItemGetSystem : JobComponentSystem
{
    BuildPhysicsWorld buildPhysicsWorldSystem;
    StepPhysicsWorld stepPhysicsWorldSystem;
    EntityCommandBufferSystem bufferSystem;

    protected override void OnCreate()
    {
        buildPhysicsWorldSystem = World.GetOrCreateSystem<BuildPhysicsWorld>();
        stepPhysicsWorldSystem = World.GetOrCreateSystem<StepPhysicsWorld>();
        bufferSystem = World.GetOrCreateSystem<BeginSimulationEntityCommandBufferSystem>();
    }

    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        inputDeps = JobHandle.CombineDependencies(
            inputDeps, 
            buildPhysicsWorldSystem.FinalJobHandle,
            stepPhysicsWorldSystem.FinalSimulationJobHandle);

        inputDeps = new HitAndDestroyJob {
            World = buildPhysicsWorldSystem.PhysicsWorld,
            TriggerEvents = stepPhysicsWorldSystem.Simulation.TriggerEvents,
            items = GetComponentDataFromEntity<ItemTag>(true),
            commandBuffer = bufferSystem.CreateCommandBuffer(),
        }.Schedule(1, 1, inputDeps);

        bufferSystem.AddJobHandleForProducer(inputDeps);

        return inputDeps;
    }

    struct HitAndDestroyJob : IJobParallelFor
    {
        [ReadOnly] public PhysicsWorld World;
        [ReadOnly] public TriggerEvents TriggerEvents;
        [ReadOnly] public ComponentDataFromEntity<ItemTag> items;
        [ReadOnly] public EntityCommandBuffer commandBuffer;

        public void Execute(int index)
        {
            foreach (var triggerEvent in TriggerEvents)
            {
                GetItem(World.Bodies[triggerEvent.BodyIndices.BodyAIndex].Entity);
                GetItem(World.Bodies[triggerEvent.BodyIndices.BodyBIndex].Entity);
            }
        }

        void GetItem(Entity entity)
        {
            if (items.Exists(entity) == false)
                return;

            commandBuffer.DestroyEntity(entity);
            UnityEngine.Debug.Log("you get item!");
        }
    }
}

【Unity】GameObjectでもUnity Physicsを使う

f:id:tsubaki_t1:20190422011739j:plain

試しにRoll a BallをUnity Physicsで作ってみた所、このテクニック無しだと少し面倒な所があったので紹介。

GameObjectでUnity Physicsを使う

Unity PhysicsはECSのための機能です。とは言えECSはまだ機能が全くと行っていいほど足りてない玩具であり、これだけでゲームを作るのは正直辛いとです。なので基本的にGameObjectを使いつつUnity Physicsを使う方法を考えてみます。

具体的にはSkinnedMeshRendererで動くキャラクターをUnity Physicsのゲーム空間に含めることを目標とします。

キャラクターが消えるのを解決する

とりあえずConvert To EntitySubScene を設定したところ、ゲーム実行時にキャラクターが消滅しました。これは Convert To EntityやSubSceneが Convert And Destroyの設定になっているためです。この設定だとGameObjectは自動的に破棄されます。

f:id:tsubaki_t1:20190422012357g:plain

なのでConvert To Entity の設定を Convert And Inject GameObjectに変更します。この時、今までのMeshColliderやRigidbodyを設定しているとRigidbodyとPhysicsBodyの両方が設定されてしまうので、Rigidbodyは消してPhysicsBodyやPhysicsShapeのみの設定にします。

f:id:tsubaki_t1:20190422012038j:plain

表示はされますが、動きません

キャラクターが動かない問題を解決する

次にPhysicsBodyを設定しているのに動かない問題です。

これは実際には動いていますが、Transformに反映されないために起こります。これを確認するにはPhysics Debug Displayを使用するのが手っ取り早いです。

これで実際のColliderの位置を確認出来ます。試しに見てみた所、Colliderは動いていますがTransformに反映されていませんでした。

f:id:tsubaki_t1:20190422013026j:plain

f:id:tsubaki_t1:20190422012903g:plain

これの解決にはCopyTransformToProxyを設定するのが良いですが、ConvertToEntityと機能が被るので、IConvertGameObjectToEntity系の機能を自作します。

using Unity.Entities;
using UnityEngine;
using Unity.Transforms;

[DisallowMultipleComponent]
[RequiresEntityConversion]
public class CopyTransformToComponentConvert : MonoBehaviour, IConvertGameObjectToEntity
{
    public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
    {
        dstManager.AddComponentData(entity, new CopyTransformToGameObject());
    }
}

あとはUnity Physicsで動かすキャラクターに上のコードCopyTransformToComponentConvertを設定してやれば、Unity PhysicsでGameObjectが動きます。

f:id:tsubaki_t1:20190422013621g:plain

【Unity】新しい物理演算、Unity Physicsについて

f:id:tsubaki_t1:20190420231232g:plain

Unity Physicsについて紹介します。

Unity Physicsとは?

Unity Physicsは、ECS上で物理演算を動かす為の追加機能です。この物理演算は過去Unityが使用してきた実績と安心のPhysXではなく、完全に新しくC#で作り直した物理演算になります。

Physicsの役割としては下の二通り。

  • 空間を探索して衝突等を探索する(接触判定、線形判定)
  • オブジェクトを実際に動かす(重力、バウンド、摩擦等)

f:id:tsubaki_t1:20190420215138j:plain

Unity Physicsはステートやキャッシュを行わず、複雑な衝突計算に対応しない代わりに軽量です。

また直前の状況に惑わされずその場その場で計算を行うという関係上、ネットワークといった事前状況がバラバラのゲームに向いていると言えるかもしれません。特にゲームでColliderを使用しているけど当たり判定しか使用していないといったケースでは、コチラのほうが良いと言えるのかもしれません。

従来のようなステート有りの物理演算向けとして、内部挙動をUnity PhysicsとHavok Pluginで切り替える事が出来るみたいです。 Havok Pluginでは複雑な状態を管理するのに向いているとの事。まだHavok Pluginはまだ公開されていません。

f:id:tsubaki_t1:20190420220552j:plain

なお現行のPhysXベースのColliderとUnity PhysicsのColliderは接触しません。Unity Physicsを使用する場合は他の全てのColliderをUnity Physics向けに変換する必要が出ます。

事前準備

導入環境はこちら

  • Unity 2019.1f2
  • Unity Physics preview1 0.0.2
  • Hybrid Renderer preview10 0.0.1

f:id:tsubaki_t1:20190420221316j:plain

Hybrid Rendererは実際には無くても良いのですが、描画関連を記述するのが面倒くさいので、Hybrid Rendererパッケージを導入します。*1

とりあえずシーンを用意

まずは、Roll a Ballよろしく板と玉を用意して、玉にRigidbodyを設定します。

再生すると下のように玉が坂を転がります。まだPhysXを使用している状態です。

f:id:tsubaki_t1:20190420222657g:plain

Unity Physicsに移行

Unity Physicsに移行します。

  • SphererにConvert To Entityを追加します。
  • PlaneにConvert To Entityを追加します。

これでSphereとPlaneオブジェクトは GameObjectからECSベースのUnity Physicsに変換 されます。

f:id:tsubaki_t1:20190420222954j:plain

f:id:tsubaki_t1:20190420223148j:plain

変換後は下のように、幾つかのComponentDataに変換されています。Entityを操作する場合はRigidbodyではなく下のコンポーネントを云々する必要がある点に注意してください。

f:id:tsubaki_t1:20190420225154j:plain

また動的にオブジェクトを増やす場合は下の記事を参考に。

tsubakit1.hateblo.jp

ステージのような大量の当たり判定のある物を一気にロード

Convert To Entityを使うのは悪くないのですが、これだと実行時に下のようなフローが入って勿体無い気もします。

  1. シーンをロード
  2. GameObjectをデシリアライズ
  3. Componentをデシリアライズ
  4. ComponentDataを生成
  5. GameObjectの破棄
  6. Convert To Entityの数だけ繰り返す

なのでサブシーンとして一気にデシリアライズしてしまいます。

  • 「とりあえずシーンを用意」の状態からスタート
  • SphererとPlaneを選択して右クリック
  • New SubScene from Selectionを選択
  • SubSceneコンポーネントのCloseを押す

これでステージ上のGameObjectがEntityに変換され、デシリアライズをほぼ挟まずにロード出来るようになります。

f:id:tsubaki_t1:20190420224852g:plain

新しく増えた設定を使いたい

Unity PhysicsはSubSceneやConvertToEntityでGameObjectのRigidbodyから変換するフローが用意されていますが、独自のコンポーネントも用意されています。

まず○○Collider系は、全てPhysicsShapeに統合されています。ねんがんの形状「Plane」も追加されているので、割と良いです

f:id:tsubaki_t1:20190420225530p:plain

Rigidbodyは`PhysicsBodyになります。

f:id:tsubaki_t1:20190420230215j:plain

パフォーマンス

PhysXと比べてどう?というのは機能の方向性が違うのでフェアな比較方法が思いつかなくてアレですが、非常に分かりやすい注意点として「エディターで動かすと遅い」というのがあります。

DOTS系は総じてそうなのですが、このUnity Physicsも同じくエディターで動かすと非常に遅く、IL2CPPでビルドすると化けます。 また実行直後(というかBurstのコンパイルが完了していない間*2)は更に遅いです。

コード制御関係

次で。

関連

Unity Physicsについての説明。意外とエグいと思ったのは自分だけではないと思いたい

www.youtube.com

紹介の公式ブログ

blogs.unity3d.com

nakamura001.hatenablog.com

*1:壊れてると思ったら古いAPIだった

*2:Burstはエディターで実行時に非同期でコードをコンパイルする。何故かキャッシュしてくれない

【Unity】Denoiseを使用してライトマップのベイク時間を劇的に抑える

Unity 2019.1で追加されたDenoiseを使ってみます

Denoiseでライトマップのノイズを除去

Progressive Lightmapperはレイトレースベースのライトマッパーです。そのサンプリング数が高い場合は非常に正しくライトマップがベイクされるのですが、非常に時間がかかります。一方、サンプリング数を減らす事でベイクの時間は大幅に向上しますが、ライトマップはノイズだらけになります。

時間が無限にあるならばサンプル数は多い方が良いですが、実際にはある程度のバランスを取る必要があります。

タイプ 効果
サンプル数が多い ベイクに時間がかかるが、ノイズは少なく正しく表示される
サンプル数が少ない ベイク時間は短いが、ノイズが表示される

f:id:tsubaki_t1:20190416225721j:plain

f:id:tsubaki_t1:20190416225753j:plain

以前はこの問題を、ノイズにフィルターをかけることでボカしていました。これはある程度の効果がありますが、やはりサンプル数が非常に低い場合は見た目が悪くなります。

f:id:tsubaki_t1:20190416230839j:plain

Unity 2019.1ではさらにノイズ除去というテクノロジーを追加することで、より少ないサンプリングでも綺麗に表示出来るようになったという感じみたいです。

下のライトマップは、サンプル数は最低でベイク時間が殆ど無いにも関わらず、それなりに綺麗に表示されています。勿論、よく見ると違和感のある部分はあるので使うかどうかは相談となるでしょうが、それでもサンプル数を減らすという選択肢は取りやすくなりそうです。

f:id:tsubaki_t1:20190416225732j:plain

Optix AI Denoiser

Optix AI DenoiserはNvidia機械学習ベースのノイズ除去システムみたいです。論文はコチラ

動作のためにNvidiaGPUを積んだWindowsのマシンが必要です。

developer.nvidia.com

使ってみる

実際に使ってみます。…といっても、Unity 2019.1では特に何もしなくても使えるならDenoiserが有効になります。

例えば Window > Renderer > LightmapDirect SampleIndirect SamplesEnvironment Sampleの数を劇的に下げてベイクしてみます。

項目 変更前 変更後
Direct Samples 32 1
Indirect Samples 512 8
Environment Samples 256 8

これでベイク時間が超短縮されます。

f:id:tsubaki_t1:20190416231959j:plain

Denoiserの有効・無効は、現在はFilterの項目に含まれます。初期設定では Autoで有効になっていますが、無効にしたい等の場合はFilterの設定を Advanceに変更すれば設定が可能になります。

f:id:tsubaki_t1:20190416232324j:plain

なおUnity 2019.1ではProgressive LightmapperのCPUのみで使用可能、2019.2からGPUにも対応するっぽいです。

感想

ライトマップのベイクに40分とか1時間とかかかっていたのが、本当に5秒とか30秒レベルで終わる割にソレナリに綺麗なので、非常に楽しいです。

とりあえずUnity 2019.1からはライトマップを焼く時にはSampleを減らす系の作業が入りそうな(…プロジェクト生成時に初期設定を手前に作らせろという意見も無きにしもあらず。何故かLightmapの設定はPreset使えないんですよね…)

関連

ライトマップをGPUで焼こうぜって手法。事前準備はありますが焼き始めるとかなり早いです

tsubakit1.hateblo.jp

Preset

tsubakit1.hateblo.jp

2019.1リリースノート

blogs.unity3d.com