- 投稿日:2022-03-03T21:16:43+09:00
【Unity】UniRxでアニメーションの開始と終了をスクリプトで受け取る
概要 Unityのアニメーションの開始と終了をスクリプトで受け取って操作する手順です。 サンプル 実装するサンプルです。Attackアニメーションの開始、終了および、Combinationアニメーションの開始、終了時にテキストが更新されていることが分かります。 無料Assetの以下を使わせていただきました。 必要なもの 事前にUniRxをimportしておいてください。動かしたいオブジェクトやアニメーションも必要になります。 実装手順 AnimationControllerの設定 動かしたいモデルにアタッチするAnimationControllerの設定をします。 今回は1つの攻撃アニメーションのStateとコンビネーションの攻撃を合わせたSub State Machineを用意しました。 アニメーションの開始と終了を受け取るためにObservableStateMachineTriggerをアタッチしてください。 読んでも詳しく書いてないんですが、ObservableStateMachineTriggerについては以下から参照できます。 Base Layer Combination(Sub-State-Machine) サンプルコード AnimationControllerと合わせてアタッチするスクリプトです。 トリガーイベントとして以下を設定しています。他にもトリガーはあるので以下を参考にしてください。 https://github.com/neuecc/UniRx/wiki/UniRx.Triggers OnStateEnterAsObservable Stateが開始したとき OnStateExitAsObservable Stateが終了したとき トリガー内で指定のStateを検知したときにテキストを書き換える処理にしています。 ここはご自身の好きな処理で大丈夫です。 AnimationUniRxSample.cs using System.Collections; using System.Collections.Generic; using System; using UnityEngine; using UnityEngine.UI; using UniRx; using UniRx.Triggers; public class AnimationUniRxSample : MonoBehaviour { public Text text; void Start() { Animator animator = this.GetComponent<Animator>(); // AnimatorからObservableStateMachineTriggerの参照を取得 ObservableStateMachineTrigger trigger = animator.GetBehaviour<ObservableStateMachineTrigger>(); // Stateの開始イベント IDisposable enterState = trigger .OnStateEnterAsObservable() .Subscribe(onStateInfo => { AnimatorStateInfo info = onStateInfo.StateInfo; // Base Layer if (info.IsName("Base Layer.Attack")) { text.text = "攻撃中: Attack"; } // Sub State Machine if (info.IsName("Combination.WGS_attackA1")) { text.text = "攻撃中: Combination"; } }).AddTo(this); // Stateの終了イベント IDisposable exitState = trigger .OnStateExitAsObservable() .Subscribe(onStateInfo => { AnimatorStateInfo info = onStateInfo.StateInfo; // Base Layer if (info.IsName("Base Layer.Attack")) { text.text = "攻撃終了: Attack"; } // Sub State Machine if (info.IsName("Combination.WGS_attackA3")) { text.text = "攻撃終了: Combination"; } }).AddTo(this); } } ※Sub-State-Machineの名前については、Base LayerからではなくSub-State-Machineの名前.Sub-State-MachineのStateで書いてください。 今回の場合、開始部分についてはCombination.WGS_attackA1、終了部分についてはCombination.WGS_attackA3にしております。 動かすオブジェクトにアタッチする 動かすモデルにAnimatorControllerとスクリプトをアタッチしてください。Animatorの設定等については省略します。 すると実行してTriggerをオンにするとアニメーションと同時にテキストが更新されます。 動作テスト 動作させる方法については環境によってことなるので割愛しますが、私の場合はAnimatorControllerの編集画面で適当にTriggerパラメータを設定し、対象のステートに向かってMake Transition > ConditionsでTriggerを設定の流れにしています。 まとめ 超簡単に開始と終了が作れたのでターン制のゲームなんかではすごく役に立つと思います。 ただ、UniRx最強だけど覚えることが多すぎる。 参考記事
- 投稿日:2022-03-03T16:58:48+09:00
【Unity】Advanced Mesh APIでメッシュの頂点をCompute Shaderで動かす
Unity2019.3からAdvanced Mesh APIとして、よりローレベルでメッシュを操作できるようにMesh APIが拡張されています。その一環で、Unity2021.2からメッシュをCompute Shaderで直接操作できるようになりました。 この記事では、Advanced Mesh APIを使用してメッシュの頂点をCompute Shaderで動かしてみます。結果はこのようになります。 検証で使用しているUnityのバージョンは2021.2.9f1です。 まず、C#のスクリプトです。 MeshFilterからメッシュを取得して参照用のメッシュoriginalMeshと変更用のメッシュdisplacedMeshをそれぞれ作成します。変更用のメッシュをMeshFilter#meshに設定しておき、こののメッシュの頂点をCompute Shaderで動かします。参照用のメッシュはCompute Shaderに渡す頂点位置のバッファだけが必要なのでMeshにする必要は必ずしもないですが、ここでは手を抜いてメッシュを生成してそこからバッファを取得しています。Update内でCompute Shaderを実行して頂点を動かします。 ComputeMeshDisplace.cs using UnityEngine; using UnityEngine.Rendering; [RequireComponent(typeof(MeshFilter))] public class ComputeMeshDisplace : MonoBehaviour { [System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)] struct DisplaceVertex { public Vector3 position; public Vector3 normal; } [SerializeField, HideInInspector] ComputeShader compute; Mesh originalMesh, displacedMesh; GraphicsBuffer originalIndexBuffer; GraphicsBuffer originalVertexBuffer; GraphicsBuffer displacedVertexBuffer; void Awake() { var meshFilter = GetComponent<MeshFilter>(); originalMesh = CreateMesh(meshFilter.sharedMesh); displacedMesh = CreateMesh(meshFilter.sharedMesh); originalVertexBuffer = originalMesh.GetVertexBuffer(0); originalIndexBuffer = originalMesh.GetIndexBuffer(); displacedVertexBuffer = displacedMesh.GetVertexBuffer(0); meshFilter.mesh = displacedMesh; } Mesh CreateMesh(Mesh original) { var originalTriangles = original.triangles; var originalPositions = original.vertices; var originalNormals = original.normals; var mesh = new Mesh(); mesh.indexBufferTarget |= GraphicsBuffer.Target.Raw; mesh.vertexBufferTarget |= GraphicsBuffer.Target.Raw; var pDesc = new VertexAttributeDescriptor(VertexAttribute.Position, VertexAttributeFormat.Float32, 3); var nDesc = new VertexAttributeDescriptor(VertexAttribute.Normal, VertexAttributeFormat.Float32, 3); mesh.SetVertexBufferParams(original.vertexCount, pDesc, nDesc); mesh.SetIndexBufferParams(originalTriangles.Length, IndexFormat.UInt32); mesh.SetSubMesh(0, new SubMeshDescriptor(0, originalTriangles.Length), MeshUpdateFlags.DontRecalculateBounds); mesh.SetIndexBufferData(originalTriangles, 0, 0, originalTriangles.Length); var vertices = new DisplaceVertex[original.vertexCount]; for (var i = 0; i < original.vertexCount; ++i) { vertices[i].position = originalPositions[i]; vertices[i].normal = originalNormals[i]; } mesh.SetVertexBufferData(vertices, 0, 0, original.vertexCount); mesh.bounds = original.bounds; return mesh; } void OnDestroy() { originalVertexBuffer?.Dispose(); originalVertexBuffer = null; originalIndexBuffer?.Dispose(); originalIndexBuffer = null; displacedVertexBuffer?.Dispose(); displacedVertexBuffer = null; Destroy(originalMesh); originalMesh = null; Destroy(displacedMesh); displacedMesh = null; } void Update() { DisplaceMesh(); } void DisplaceMesh() { compute.SetFloat("Time", Time.time); compute.SetBuffer(0, "OriginalIndices", originalIndexBuffer); compute.SetBuffer(0, "OriginalVertices", originalVertexBuffer); compute.SetBuffer(0, "DisplacedVertices", displacedVertexBuffer); DispatchThreads(0, originalMesh.triangles.Length / 3); } void DispatchThreads(int kernel, int count) { uint x, y, z; compute.GetKernelThreadGroupSizes(kernel, out x, out y, out z); var groups = (count + (int)x - 1) / (int)x; compute.Dispatch(kernel, groups, 1, 1); } } 次に、Compute Shaderです。 DisplaceMeshがメッシュを構成する三角形ポリゴンの数だけ実行されるので、参照用のメッシュのバッファから三角形を構成する3つの頂点位置を取得して摂動を加えてから、法線を再計算して変更用のメッシュのバッファに頂点位置と法線を格納しています。 ComputeMeshDisplace.compute #pragma kernel DisplaceMesh float Time; ByteAddressBuffer OriginalIndices; ByteAddressBuffer OriginalVertices; RWByteAddressBuffer DisplacedVertices; float3 LoadOriginalVertex(uint index) { uint pi = index * 6 * 4; // index * (3[position.xyz] + 3[normal.xyz]) * 4[bytes] return asfloat(OriginalVertices.Load3(pi)); } void StoreDisplacedVertex(uint index, float3 position, float3 normal) { uint pi = index * 6 * 4; // index * (3[position.xyz] + 3[normal.xyz]) * 4[bytes] uint ni = pi + 3 * 4; // pi + 3[position.xyz] * 4[bytes] DisplacedVertices.Store3(pi, asuint(position)); DisplacedVertices.Store3(ni, asuint(normal)); } float3 DispalcePosition(float3 p) { float3 disp = float3( 0.1 * (sin(1.3 * p.y + 4.1 * Time) + sin(2.9 * p.z + 5.3 * Time)), 0.1 * (sin(1.9 * p.z + 4.3 * Time) + sin(3.1 * p.x + 5.9 * Time)), 0.1 * (sin(2.3 * p.x + 4.7 * Time) + sin(3.7 * p.y + 6.1 * Time)) ); return p + disp; } float3 CalculateNormal(float3 p0, float3 p1, float3 p2) { float3 d10 = p1 - p0; float3 d20 = p2 - p0; return normalize(cross(d10, d20)); } [numthreads(256, 1, 1)] void DisplaceMesh(uint id : SV_DispatchThreadID) { // Load original vertices uint3 triIndex = OriginalIndices.Load3(id * 3 * 4); // id * 3[triangle vertices] * 4[bytes] float3 p0 = LoadOriginalVertex(triIndex.x); float3 p1 = LoadOriginalVertex(triIndex.y); float3 p2 = LoadOriginalVertex(triIndex.z); // Displace positions float3 dp0 = DispalcePosition(p0); float3 dp1 = DispalcePosition(p1); float3 dp2 = DispalcePosition(p2); // Calculate normals float3 cn = CalculateNormal(dp0, dp1, dp2); // Store modified vertices StoreDisplacedVertex(triIndex.x, dp0, cn); StoreDisplacedVertex(triIndex.y, dp1, cn); StoreDisplacedVertex(triIndex.z, dp2, cn); } 先ほどのComputeMeshDisplaceコンポーネントをMeshFilterを持つオブジェクトに追加して、computeプロパティにこのCompute Shaderを設定するとメッシュの頂点が動くようになります。Compute Shaderのコードを見るとわかるように、この方法だとフラットシェーディングになってしまうので、記事先頭の結果ではあらかじめフラットシェーディング用のメッシュに適用しています。 以下、参考にした記事です。 UnityEngine.Mesh - Unity スクリプトリファレンス 2019.3 Mesh API Improvements - Google ドキュメント 2020.1 Mesh API Improvements - Google ドキュメント 2021.2 Mesh API Compute Shader Access improvements - Google ドキュメント 鬼弾幕!新Mesh APIで本気出したら凄いことになった…【Unity】 - YouTube UnityグラフィックスAPI総点検!〜最近こんなの増えてました〜 - Unityステーション - YouTube keijiro/NoiseBall6: Unity sample project: Direct mesh data access from compute shaders keijiro/ComputeMarchingCubes: [Unity] GPU-optimized marching cubes isosurface reconstruction
- 投稿日:2022-03-03T13:19:08+09:00
UniTask.WaitForEndOfFrameが正しく動くようになった
以前 UnityTestRunnerでUniTaskを使おう! にて、 UniTask.WaitForEndOfFrame は正常に動かない旨を書きましたが、 UniTask 2.3.1 (https://github.com/Cysharp/UniTask/releases/tag/2.3.1) にて、MonoBehaviour を引数に渡すことにより正常に動くようになり、引数無しのものは Obsolete になりました。 内部では渡した MonoBehaviour を使ってコルーチンが走っていますが、 yield return を使わない実装によりアロケーションはしないようになっています。 以前のPlayModeのテストコードをほぼそのまま使ってみましょう。 /// <summary> /// 例えばスクリーンショットを撮るテスト(UniTask) /// </summary> [UnityTest] public IEnumerator WaitForEndOfFrameUniTaskTest() => UniTask.ToCoroutine(async () => { new GameObject("camera").AddComponent<Camera>(); // 適当なMonoBehaviourをコルーチン用に生成する var runner = new GameObject("Runner").AddComponent<CoroutineRunner>(); // EndOfFrameまで待てる!!!!!!!!! await UniTask.WaitForEndOfFrame(runner); var tex = new Texture2D(Screen.width, Screen.height); // // EndOfFrameでしか動かない tex.ReadPixels(new Rect(0, 0, Screen.width, Screen.height), 0, 0); tex.Apply(); var sprite = Sprite.Create( tex, new Rect(0, 0, tex.width, tex.height), new Vector2(0.5f, 0.5f) ); var renderer = new GameObject("sprite").AddComponent<SpriteRenderer>(); renderer.sprite = sprite; await UniTask.Delay(5000); }); 今回はテストコードということでコルーチンを走らせる用に適当な MonoBehaviour を生成して引数に渡しています。 public class CoroutineRunner : MonoBehaviour {} // 適当なMonoBehaviourをコルーチン用に生成する var runner = new GameObject("Runner").AddComponent<CoroutineRunner>(); // EndOfFrameまで待てる!!!!!!!!! await UniTask.WaitForEndOfFrame(runner); 正常に動いています。きれいなサンセットが撮れました。