なおしのこれまで、これから

学んだこと・感じたこと・やりたいこと

Graphics.DrawMeshInstancedIndirectまとめ

f:id:vxd-naoshi-19961205-maro:20200823230310p:plain

始めに

去年ちょっとだけGraphics.DrawMeshInstancedIndirectを触りました。

shitakami.hatenablog.com


今回はその復習も兼ねてこの機能についてまとめていこうと思います。



Graphics.DrawMeshInstancedIndirectについて

このメソッドを使用することによって、同じメッシュをGPUInstancingすることができます。

簡単に言いますと滅茶苦茶たくさんのメッシュを描画することが出来ます。


この関数の引数について簡単に以下にまとめます。 また、その後に追加の解説をいくつか載せます。

引数 解説
mesh 描画するメッシュデータ
submeshIndex 描画するサブメッシュのインデックス(基本0が代入される)
material 使用するマテリアル
bounds 描画する範囲
bufferWithArgs 描画する個数などが入ったデータ
argsOffset bufferWithArgsが何バイト目から始まるか(デフォルトは0)
properties MaterialPropertyBlockを入れる。未調査です。(デフォルトはnull)
castShadows 描画したメッシュの影を描画するか(デフォルトはOn)
receiveShadows 描画したメッシュに他のオブジェクトの影を描画するか (デフォルトはtrue)
layer 使用するレイヤー(デフォルトは0)
camera 描画させるカメラ(デフォルトは0)
lightProbeUsage LightProbeの指定(デフォルトはBlendProbe)
lightProbeProxyVolume わからない. . .公式にも載ってない(デフォルトはnull)

特に細かく設定しないのであれば第五引数(bufferWithArgs)までを与えれば良いです。



bounds

boundsについては「描画する範囲」と解説していますが、これは「カメラがboundsで指定した範囲内を描画すれば、メッシュを描画する」となります。

例えば、中心座標を(x, y, z) = (0, 0, 0)、大きさを(x, y, z) = (0, 0, 0)にした場合はカメラが座標(0, 0, 0)を映していないと描画されたメッシュが消えてしまいます。 以下gifがその例です。

f:id:vxd-naoshi-19961205-maro:20200823223337g:plain


メッシュが描画される座標すべてを含める範囲を指定した方がいいかもしれません。



bufferWithArgs

BufferWithArgsは5つのuint型の値を持つComputeBufferになります。

  • サブメッシュのインデックスの数
  • 描画する個数
  • メッシュバッファの開始インデックス
  • 基底頂点位置(base vertex location)
  • スタートインスタンス位置(start instance location)

1番目についてはMesh.GetIndexCount(0)で求めることが出来ます。

2番目についてはここで描画させたメッシュの個数をしていすれば、その分だけのオブジェクトを描画することが出来ます。

それ以外についてですが色々調べてもあまり情報が出ず、基本的に0で初期化すれば問題ないようです。



castShadows、receiveShadows

この項目では、「影を出すか」「影を受けるか」を指定することが出来ます。 デフォルトでは両方有効になっているため、軽量化したい場合はこの項目を変更すると良いかもしれません。

f:id:vxd-naoshi-19961205-maro:20200823225421p:plain



layer、camera

この項目を変更することで、描画するカメラを指定したりCullingMaskなどで描画するかを判定することが出来ます。

この内容については以下のサイトが参考になるかもしれません。

unity-guide.moon-bear.com



簡単なサンプル

調べた内容を元にして実装したサンプルになります。

TestDrawMesh.cs

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

public class TestDrawMesh : MonoBehaviour
{

    [Header("DrawMeshInstancedIndirectのパラメータ")]
    [SerializeField]
    private Mesh m_mesh;

    [SerializeField]
    private Material m_instanceMaterial;

    [SerializeField]
    private Bounds m_bounds;

    [SerializeField]
    private ShadowCastingMode m_shadowCastingMode;

    [SerializeField]
    private bool m_receiveShadows;

    [SerializeField]
    private string m_layerName;

    [SerializeField]
    private Camera m_camera;


    [Space(20)]
    [SerializeField]
    private int m_instanceCount;

    private ComputeBuffer m_argsBuffer;

    private ComputeBuffer m_positionBuffer;

    private ComputeBuffer m_eulerAngleBuffer;

    private int m_layer;

    // Start is called before the first frame update
    void Start()
    {
        InitializeArgsBuffer();
        InitializePositionBuffer();
        InitializeEulerAngleBuffer();

        m_layer = LayerMask.NameToLayer(m_layerName);
    }

    // Update is called once per frame
    void Update()
    {

        Graphics.DrawMeshInstancedIndirect(
            m_mesh,
            0,
            m_instanceMaterial,
            m_bounds,
            m_argsBuffer,
            0,
            null,
            m_shadowCastingMode,
            m_receiveShadows,
            m_layer,
            m_camera,
            LightProbeUsage.BlendProbes,
            null
        );

    }

    private void InitializeArgsBuffer() {

        uint[] args = new uint[5] { 0, 0, 0, 0, 0 };

        uint numIndices = (m_mesh != null) ? (uint) m_mesh.GetIndexCount(0) : 0;

        args[0] = numIndices;
        args[1] = (uint)m_instanceCount;
        
        m_argsBuffer = new ComputeBuffer(1, args.Length * sizeof(uint), ComputeBufferType.IndirectArguments);
        m_argsBuffer.SetData(args);

    }

    private void InitializePositionBuffer() {

        // xyz:座標   w:スケール
        Vector4[] positions = new Vector4[m_instanceCount];

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

            positions[i].x = Random.Range(-100.0f, 100.0f);
            positions[i].y = Random.Range(-100.0f, 100.0f);
            positions[i].z = Random.Range(-100.0f, 100.0f);

            positions[i].w = Random.Range(0.1f, 1f);

        }

        m_positionBuffer = new ComputeBuffer(m_instanceCount, 4 * 4);
        m_positionBuffer.SetData(positions);

        m_instanceMaterial.SetBuffer("positionBuffer", m_positionBuffer);

    }

    private void InitializeEulerAngleBuffer() {

        Vector3[] angles = new Vector3[m_instanceCount];

        for(int i = 0; i < m_instanceCount; ++i) {
            angles[i].x = Random.Range(-180.0f, 180.0f);
            angles[i].y = Random.Range(-180.0f, 180.0f);
            angles[i].z = Random.Range(-180.0f, 180.0f);
        }

        m_eulerAngleBuffer = new ComputeBuffer(m_instanceCount, 4 * 3);
        m_eulerAngleBuffer.SetData(angles);

        m_instanceMaterial.SetBuffer("eulerAngleBuffer", m_eulerAngleBuffer);

    }

    // 領域の解放
    private void OnDisable() {

        if(m_positionBuffer != null)
            m_positionBuffer.Release();
        m_positionBuffer = null;

        if(m_eulerAngleBuffer != null)
            m_eulerAngleBuffer.Release();
        m_eulerAngleBuffer = null;

        if(m_argsBuffer != null)
            m_argsBuffer.Release();
        m_argsBuffer = null;

    }

}


TestInstancedIndirectSurf.shader

Shader "Custom/TestInstancedIndirectSurf"
{
    Properties
    {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        _Glossiness ("Smoothness", Range(0,1)) = 0.5
        _Metallic ("Metallic", Range(0,1)) = 0.0
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 200

        CGPROGRAM
        // Physically based Standard lighting model, and enable shadows on all light types
        #pragma surface surf Standard addshadow fullforwardshadows // 影を描画するためにはaddshadowが必要
        #pragma multi_compile_instancing    // GPU Instancingを可能にする
        #pragma instancing_options procedural:setup // setup関数を呼び出す

        sampler2D _MainTex;

        struct Input
        {
            float2 uv_MainTex;
        };

#ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED
    StructuredBuffer<float4> positionBuffer;
    StructuredBuffer<float3> eulerAngleBuffer;
#endif

        #define Deg2Rad 0.0174532924

        float4x4 eulerAnglesToRottationMatrix(float3 angles) {

            float cx = cos(angles.x * Deg2Rad); float sx = sin(angles.x * Deg2Rad);
            float cy = cos(angles.z * Deg2Rad); float sy = sin(angles.z * Deg2Rad);
            float cz = cos(angles.y * Deg2Rad); float sz = sin(angles.y * Deg2Rad);

            return float4x4(
                cz*cy + sz*sx*sy, -cz*sy + sz*sx*cy, sz*cx, 0,
                sy*cx, cy*cx, -sx, 0,
                -sz*cy + cz*sx*sy, sy*sz + cz*sx*cy, cz*cx, 0,
                0, 0, 0, 1);

        }


        void setup() {

        #ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED
            float4 data = positionBuffer[unity_InstanceID];
            float3 angles = eulerAngleBuffer[unity_InstanceID];

            // スケーリング
            unity_ObjectToWorld._11_21_31_41 = float4(data.w, 0, 0, 0);
            unity_ObjectToWorld._12_22_32_42 = float4(0, data.w, 0, 0);
            unity_ObjectToWorld._13_23_33_43 = float4(0, 0, data.w, 0);

            // 回転
            unity_ObjectToWorld = mul(eulerAnglesToRottationMatrix(angles), unity_ObjectToWorld);

            // 座標
            unity_ObjectToWorld._14_24_34_44 = float4(data.xyz, 1);

            // モデル行列を求める
            unity_WorldToObject = unity_ObjectToWorld;
            unity_WorldToObject._14_24_34 *= -1;
            unity_WorldToObject._11_12_13 = unity_ObjectToWorld._11_21_31;
            unity_WorldToObject._21_22_23 = unity_ObjectToWorld._12_22_32;
            unity_WorldToObject._31_32_33 = unity_ObjectToWorld._13_23_33;
            unity_WorldToObject._11_12_13 /= data.w * data.w;
            unity_WorldToObject._21_22_23 /= data.w * data.w;
            unity_WorldToObject._31_32_33 /= data.w * data.w;
        #endif

        }


        half _Glossiness;
        half _Metallic;
        fixed4 _Color;

        // Add instancing support for this shader. You need to check 'Enable Instancing' on materials that use the shader.
        // See https://docs.unity3d.com/Manual/GPUInstancing.html for more information about instancing.
        // #pragma instancing_options assumeuniformscaling
        UNITY_INSTANCING_BUFFER_START(Props)
            // put more per-instance properties here
        UNITY_INSTANCING_BUFFER_END(Props)

        void surf (Input IN, inout SurfaceOutputStandard o)
        {
            // Albedo comes from a texture tinted by color
            fixed4 cy = tex2D (_MainTex, IN.uv_MainTex) * _Color;
            o.Albedo = cy.rgb;
            // Metallic and smoothness come from slider variables
            o.Metallic = _Metallic;
            o.Smoothness = _Glossiness;
            o.Alpha = cy.a;
        }
        ENDCG
    }
    FallBack "Diffuse"
}



解説

bufferWithArgsの初期化

bufferWithArgsの初期化を行います。ここでは変数m_argsBufferに相当します。

始めにuint型の配列を作成し、そこに必要なデータを渡します。ここでは0番目にMesh.GetIndexCountの値を、1番目に描画したいメッシュの個数を入れます。

uint[] args = new uint[5] { 0, 0, 0, 0, 0 };

uint numIndices = (m_mesh != null) ? (uint) m_mesh.GetIndexCount(0) : 0;

args[0] = numIndices;
args[1] = (uint)m_instanceCount;


次に、m_argsBufferの領域を作成します。 ComputeBufferのコンストラクターでは、要素数1、データの個数5 * uintのバイト数、ComputeBufferType.IndirectArgumentsを指定します。

その次に、SetData関数を使って先ほどのargsの値をm_argsBufferに入れます。

m_argsBuffer = new ComputeBuffer(1, args.Length * sizeof(uint), ComputeBufferType.IndirectArguments);
m_argsBuffer.SetData(args);



座標、回転、大きさの初期化

すべてのメッシュが同じ座標、同じ回転であったらちょっと嫌なのでランダムに描画したいと思います。

描画する際に座標や回転などをDrawMeshInstancedIndirectで指定する方法は無いので、ComputeBufferに座標などの値を入れて描画で使用するMaterialに渡します。

注意点としてbufferWithArgsと異なり、ComputeBufferのコンストラクタ-に描画するメッシュの個数、型の大きさを入れます。

また、ComputeBufferのコンストラクタの第二引数で"4 * 4"、"4 * 3"と指定されていますが、これはfloat型のbyte数*要素数(Vector4なら4つ、Vector3なら3つ)を表します。

追記:System.Runtime.InteropServices.Marshal.SizeOfでVector4やVector3のサイズを取得することが出来ます。


    private void InitializePositionBuffer() {

        // xyz:座標   w:スケール
        Vector4[] positions = new Vector4[m_instanceCount];

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

            positions[i].x = Random.Range(-100.0f, 100.0f);
            positions[i].y = Random.Range(-100.0f, 100.0f);
            positions[i].z = Random.Range(-100.0f, 100.0f);

            positions[i].w = Random.Range(0.1f, 1f);

        }

        m_positionBuffer = new ComputeBuffer(m_instanceCount, 4 * 4);
        m_positionBuffer.SetData(positions);

        m_instanceMaterial.SetBuffer("positionBuffer", m_positionBuffer);

    }

    private void InitializeEulerAngleBuffer() {

        Vector3[] angles = new Vector3[m_instanceCount];

        for(int i = 0; i < m_instanceCount; ++i) {
            angles[i].x = Random.Range(-180.0f, 180.0f);
            angles[i].y = Random.Range(-180.0f, 180.0f);
            angles[i].z = Random.Range(-180.0f, 180.0f);
        }

        m_eulerAngleBuffer = new ComputeBuffer(m_instanceCount, 4 * 3);
        m_eulerAngleBuffer.SetData(angles);

        m_instanceMaterial.SetBuffer("eulerAngleBuffer", m_eulerAngleBuffer);

    }



座標、回転、大きさの計算

ここでは先ほどマテリアルに渡された座標、回転、大きさのデータの扱いについて解説します。

ComputeBufferを使用してMaterialに渡されたデータはStructuredBufferに保存されます。

次に、unity_InstanceIDを使用してデータを取り出し座標計算を行います。

このsetup関数はvert関数やsurf関数が実行される前に呼び出されます。

#ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED
    StructuredBuffer<float4> positionBuffer;
    StructuredBuffer<float3> eulerAngleBuffer;
#endif

        #define Deg2Rad 0.0174532924

        float4x4 eulerAnglesToRottationMatrix(float3 angles) {

            float cx = cos(angles.x * Deg2Rad); float sx = sin(angles.x * Deg2Rad);
            float cy = cos(angles.z * Deg2Rad); float sy = sin(angles.z * Deg2Rad);
            float cz = cos(angles.y * Deg2Rad); float sz = sin(angles.y * Deg2Rad);

            return float4x4(
                cz*cy + sz*sx*sy, -cz*sy + sz*sx*cy, sz*cx, 0,
                sy*cx, cy*cx, -sx, 0,
                -sz*cy + cz*sx*sy, sy*sz + cz*sx*cy, cz*cx, 0,
                0, 0, 0, 1);

        }


        void setup() {

        #ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED
            float4 data = positionBuffer[unity_InstanceID];
            float3 angles = eulerAngleBuffer[unity_InstanceID];

            // スケーリング
            unity_ObjectToWorld._11_21_31_41 = float4(data.w, 0, 0, 0);
            unity_ObjectToWorld._12_22_32_42 = float4(0, data.w, 0, 0);
            unity_ObjectToWorld._13_23_33_43 = float4(0, 0, data.w, 0);

            // 回転
            unity_ObjectToWorld = mul(eulerAnglesToRottationMatrix(angles), unity_ObjectToWorld);

            // 座標
            unity_ObjectToWorld._14_24_34_44 = float4(data.xyz, 1);

            // モデル行列を求める
            unity_WorldToObject = unity_ObjectToWorld;
            unity_WorldToObject._14_24_34 *= -1;
            unity_WorldToObject._11_12_13 = unity_ObjectToWorld._11_21_31;
            unity_WorldToObject._21_22_23 = unity_ObjectToWorld._12_22_32;
            unity_WorldToObject._31_32_33 = unity_ObjectToWorld._13_23_33;
            unity_WorldToObject._11_12_13 /= data.w * data.w;
            unity_WorldToObject._21_22_23 /= data.w * data.w;
            unity_WorldToObject._31_32_33 /= data.w * data.w;
        #endif

        }



描画

描画する際はUpdate関数で毎フレーム呼び出して描画します。

ここではすべての引数を指定していますが、詳細を設定しないのであればbufferWithArgsまでで大丈夫です。

    void Update()
    {

        Graphics.DrawMeshInstancedIndirect(
            m_mesh,
            0,
            m_instanceMaterial,
            m_bounds,
            m_argsBuffer,
            0,
            null,
            m_shadowCastingMode,
            m_receiveShadows,
            m_layer,
            m_camera,
            LightProbeUsage.BlendProbes,
            null
        );

    }



使用したComputeBufferの解放

ComputeBufferは使用した後に解放しないと警告が出るので忘れずにしましょう。

    private void OnDisable() {

        if(m_positionBuffer != null)
            m_positionBuffer.Release();
        m_positionBuffer = null;

        if(m_eulerAngleBuffer != null)
            m_eulerAngleBuffer.Release();
        m_eulerAngleBuffer = null;

        if(m_argsBuffer != null)
            m_argsBuffer.Release();
        m_argsBuffer = null;

    }



結果

サムネでもお見せしましたが、このように大量のCubeを出力することが出来ました。

f:id:vxd-naoshi-19961205-maro:20200824001701p:plain


また、100万個のCubeを出力しても60fps以上でました。 f:id:vxd-naoshi-19961205-maro:20200824001931p:plain


遠くから見ると流石に恐怖でした。

f:id:vxd-naoshi-19961205-maro:20200824002242p:plain



感想

前に触った際はあまり覚えきれていなかったので、今回はがっつりまとめてみました。

まだまだ、分からない点はありますが使おうと思えば使えるレベルにはまとめきれたかなと思います。

これからまた、分かったことがあれば追記していこうかなと思います。



参考

今回は多くのUnity公式マニュアルを参考にしました。

docs.unity3d.com

docs.unity3d.com


また、サンプルとして以下のリポジトリがとても参考になりました。

github.com


その他参考にしたサイトです。

edom18.hateblo.jp

docs.google.com

VRでオブジェクトを掴む処理を自作する

f:id:vxd-naoshi-19961205-maro:20200712013321p:plain

始めに

過去記事です

shitakami.hatenablog.com


開発中にオブジェクトを掴むプログラムを探してた際にほとんどがAssetStoreのライブラリだったので、自分が使いやすいようにするため自作しました。

また、このプログラムを書くのが2回目だったのでブログにまとめようと思います。



仕様について

これから解説するプログラムの仕様は以下の通りです。

  • オブジェクトを掴むのは1つまで
  • 片方の手で持っているオブジェクトをもう片方が掴み直すことは不可
  • 当たり判定はTrigger
  • コントローラーではなくオブジェクトにRigidbodyを付ける



処理の流れ

簡単に処理の流れについて解説します。


コントローラーとオブジェクトに当たり判定を付ける

コントローラーとオブジェクトに当たり判定(Trigger)を付けます。図では緑色がコントローラー、灰色が掴むオブジェクトです。

f:id:vxd-naoshi-19961205-maro:20200712023618p:plain



触れたオブジェクトを登録する

コントローラーがオブジェクトに触れた際にリストに登録します。

f:id:vxd-naoshi-19961205-maro:20200712024825p:plain



リストからコントローラーに一番近いオブジェクトを選択する

リストに登録されているオブジェクトとコントローラーとの距離を求め、一番距離が短いオブジェクトを選択します。

f:id:vxd-naoshi-19961205-maro:20200712025130p:plain


コントローラーが複数のオブジェクトに触れている場合でも一番近くのオブジェクト一つを選択します。

f:id:vxd-naoshi-19961205-maro:20200712030332p:plain



オブジェクトが離れたら登録を消す

コントローラーからオブジェクトが離れた際に登録から消します。

f:id:vxd-naoshi-19961205-maro:20200712025328p:plain



選択されているオブジェクトを所持する

オブジェクトを所持する際は現在選択されているオブジェクトをコントローラーの子に設定します。

f:id:vxd-naoshi-19961205-maro:20200818211254p:plain



実装

これらの処理を実装したプログラムです。 GrabManager.csはコントローラーにアタッチされます。

GrabManager.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Assertions;
using UnityEngine.UI;

public class GrabManager : MonoBehaviour
{
    [SerializeField]
    private GameObject m_controller;

    [SerializeField]
    private GameObject m_selectTextUI;

    private IControllerInput m_controllerInput;

    private GrabObject m_nowGrabbingObject;

    private GrabObject m_selectingObject;

    private List<GrabObject> m_touchingObjects = new List<GrabObject>();

    private GameObject m_InstancedSelectTextUI;

    public bool IsGrabbing { get { return m_nowGrabbingObject; } }

    // Start is called before the first frame update
    void Start()
    {
        
        m_controllerInput = m_controller.GetComponent<IControllerInput>();

        Assert.IsNotNull(m_controllerInput, name + " : IControllerInputが設定されていません!");

    }


    // Update is called once per frame
    void Update()
    {

        /*
         * 物を掴んでいる場合は物を離すか検知する
         * そうでない場合は、物を掴むか検知する
         */
        if(m_nowGrabbingObject) 
            CheckRelease();
        else
            CheckGrab();

    }

    private void CheckRelease() {

        if(m_controllerInput.IsGrabDown())
            Release();

    }

    private void CheckGrab() {
        
        var nearObject = GetNearObject();

        // 何も触れていないのにメッセージがある場合はメッセージを消す
        if(m_InstancedSelectTextUI && !m_selectingObject) 
            Destroy(m_InstancedSelectTextUI);

        // 一番近いオブジェクトに更新 メッセージの表示
        if(nearObject != m_selectingObject) {

            m_selectingObject = nearObject;
            DisplaySelectMessage();

        }

        if(!nearObject)
            return;

        if(m_controllerInput.IsGrabDown())
            Grab();
        
    }
    
    private void DisplaySelectMessage() {

        if(m_InstancedSelectTextUI != null)
            Destroy(m_InstancedSelectTextUI);

        if(m_selectingObject == null)
            return;

        Transform selectObjTransform = m_selectingObject.transform;
        m_InstancedSelectTextUI = Instantiate(m_selectTextUI, selectObjTransform.position, selectObjTransform.rotation);

        m_InstancedSelectTextUI.transform.GetChild(0).GetComponent<Text>().text = m_selectingObject.name;

    }

    private GrabObject GetNearObject() {

        GrabObject retObject = null;
        float minDisntace = float.MaxValue;

        Vector3 pos = transform.position;

        m_touchingObjects.RemoveAll(obj => obj == null);

        foreach(var obj in m_touchingObjects) {
            
            if(obj.IsGrabbed)
                continue;

            float distance = Vector3.SqrMagnitude(pos - obj.transform.position);

            if(distance < minDisntace) {
                retObject = obj;
                minDisntace = distance;
            }

        }

        return retObject;

    }


    // 持っているオブジェクトからも呼ばれる
    public void Release() {

        m_nowGrabbingObject.IsGrabbed = false;
        m_nowGrabbingObject.transform.SetParent(null);
        m_nowGrabbingObject.FinalizeRelease();

        m_nowGrabbingObject = null;

    }

    private void Grab() {

        // 有り得ないかもしれないけど、一応nullチェックしとく
        // 表示されてる選択メッセージを消す
        if(m_InstancedSelectTextUI != null) 
            Destroy(m_InstancedSelectTextUI);

        m_nowGrabbingObject = m_selectingObject;
        m_selectingObject = null;

        m_nowGrabbingObject.transform.SetParent(this.transform, true);
        m_nowGrabbingObject.IsGrabbed = true;
        m_nowGrabbingObject.InitializeGrab(m_controllerInput);
        m_nowGrabbingObject.SetGrabManager(this);

    }


    private void OnTriggerEnter(Collider other) {

        if(!other.CompareTag("GrabObject"))
            return;
        
        var grabObj = other.GetComponent<GrabObject>();

        Assert.IsNotNull(grabObj, other.name + " : GrabObjectが見つかりません");

        m_touchingObjects.Add(grabObj);

    }


    private void OnTriggerExit(Collider other) {

        if(!other.CompareTag("GrabObject"))
            return;

        m_touchingObjects.Remove(other.GetComponent<GrabObject>());


    }


}



また、オブジェクトにはGrabObject.csをアタッチします。

GrabObject.cs

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


[RequireComponent(typeof(Rigidbody))] // OnTrigger~~を使うために必要
public class GrabObject : MonoBehaviour
{

    private bool m_isGrabbed = false;

    // GrabManagerから設定される
    public bool IsGrabbed { get { return m_isGrabbed; } set { m_isGrabbed = value; } }

    protected IControllerInput m_controllerInput;

    public virtual void InitializeGrab(IControllerInput controller) {

        m_controllerInput = controller;

    }


    public virtual void FinalizeRelease() {

        m_grabManager = null;

    }



解説

オブジェクトの登録、登録解除

この処理はOnTriggerEnterとOnTriggerExitで行います。 また、登録されたオブジェクトは m_touchingObjects に保存されます。

    private void OnTriggerEnter(Collider other) {

        if(!other.CompareTag("GrabObject"))
            return;
        
        var grabObj = other.GetComponent<GrabObject>();

        Assert.IsNotNull(grabObj, other.name + " : GrabObjectが見つかりません");

        m_touchingObjects.Add(grabObj);

    }


    private void OnTriggerExit(Collider other) {

        if(!other.CompareTag("GrabObject"))
            return;

        m_touchingObjects.Remove(other.GetComponent<GrabObject>());

    }



一番近いオブジェクトの選択

m_touchingObjectsの中からコントローラーとの距離が一番近いオブジェクトを返す関数GetNearObjectを作成します。もし、何も登録されていない場合はnullを返します。

距離を計算する際はDistance関数やmagnitude関数ではなくSqrMagnitude関数を使用します。(出来るだけ計算を軽くするため)

オブジェクトとの距離を計算する前にそのオブジェクトがもう片方のコントローラーに握られている際場合は無視されます。

    private GrabObject GetNearObject() {

        GrabObject retObject = null;
        float minDisntace = float.MaxValue;

        Vector3 pos = transform.position;

        m_touchingObjects.RemoveAll(obj => obj == null);

        foreach(var obj in m_touchingObjects) {

            // 片方のコントローラーに握られている場合は計算しない
            if(obj.IsGrabbed)
                continue;

            float distance = Vector3.SqrMagnitude(pos - obj.transform.position);

            if(distance < minDisntace) {
                retObject = obj;
                minDisntace = distance;
            }

        }

        return retObject;

    }


また、一番近いオブジェクトを選択した際にm_selectingObjectに登録します。

もし、新しく選択されたオブジェクトとm_selectingObjectが異なる場合はUIの更新を行います。

    private void CheckGrab() {
        
        var nearObject = GetNearObject();

        // 何も触れていないのにメッセージがある場合はメッセージを消す
        if(m_InstancedSelectTextUI && !m_selectingObject) 
            Destroy(m_InstancedSelectTextUI);

        // 一番近いオブジェクトに更新 メッセージの表示
        if(nearObject != m_selectingObject) {

            m_selectingObject = nearObject;
            DisplaySelectMessage();

        }

        if(!nearObject)
            return;

        if(m_controllerInput.IsGrabDown())
            Grab();
        
    }



オブジェクトを掴む、離す

オブジェクトを掴む、離す際の処理は次のようになります。

オブジェクトを掴むときに注意することはUIの表示を消すこと、掴んだオブジェクトの初期化処理を行うことです。

オブジェクトを離すときもほぼ同様です。

    // 持っているオブジェクトからも呼ばれる
    public void Release() {

        m_nowGrabbingObject.IsGrabbed = false;
        m_nowGrabbingObject.transform.SetParent(null);
        m_nowGrabbingObject.FinalizeRelease();

        m_nowGrabbingObject = null;

    }

    private void Grab() {

        // 有り得ないかもしれないけど、一応nullチェックしとく
        // 表示されてる選択メッセージを消す
        if(m_InstancedSelectTextUI != null) 
            Destroy(m_InstancedSelectTextUI);

        m_nowGrabbingObject = m_selectingObject;
        m_selectingObject = null;

        m_nowGrabbingObject.transform.SetParent(this.transform, true);
        m_nowGrabbingObject.IsGrabbed = true;
        m_nowGrabbingObject.InitializeGrab(m_controllerInput);

    }


オブジェクト側の初期化処理と終了処理は仮想関数になっているので、クラスを継承することによってオブジェクトごとに異なる処理を行うことが出来ます。

    public virtual void InitializeGrab(IControllerInput controller) {

        m_controllerInput = controller;

    }


    public virtual void FinalizeRelease() {

        m_controllerInput = null;

    }



結果

オブジェクトを選択する、UIを表示する、掴む、離す等違和感なく操作を行うことが出来ました。

f:id:vxd-naoshi-19961205-maro:20200818223527g:plain


また、このgif動画で出てくる指し棒はGrabObjectの仮想関数InitializeGrabとFinalizeReleaseでアニメーションを行っています。 GrabObjectの継承の例として指し棒のプログラムを載せておきます。

StickObject.cs

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

public class StickObject : GrabObject
{

    [SerializeField]
    private TrailRenderer m_trail;

    [SerializeField]
    private Animator m_animator;

    private int m_extendID = Animator.StringToHash("Extend");

    private void Update() {

        if(IsGrabbed == false)
            return;

        if(m_controllerInput.IsTriggerDown()) {
            m_trail.enabled = true;
        }
        else if(m_controllerInput.IsTriggerUp()) {
            m_trail.enabled = false;
        }

    }

    public override void InitializeGrab(IControllerInput controller) {

        m_controllerInput = controller;
        IsGrabbed = true;
        m_animator.SetBool(m_extendID, true);

    }

    public override void FinalizeRelease() {

        IsGrabbed = false;
        m_animator.SetBool(m_extendID, false);

    }

}



最後に

オブジェクトを掴む処理って意外と複雑で難しかったと感じました。

今回はテキストで選択しているオブジェクトを表示しましたが、他の方法としてはマテリアルの色を変えたりアウトラインを表示するなど様々あると思うのでコンテンツにあったものでやってください。

また、いつかこのプログラムを使用することがあるかもしれないので変更があったら更新します。



参考

UIの表示を最前面にするために以下のサイトを参考にさせて頂きました。

tama-lab.net

www.gunsturn.com

Gray-Scottモデルで遊ぶ(頂点アニメーション、テクスチャ書き込み等)

f:id:vxd-naoshi-19961205-maro:20200816015539p:plain

始めに

過去記事です。

aizu-vr.hatenablog.com

shitakami.hatenablog.com


色々触ってみてもうちょっと面白いことが出来るんじゃないかと検証してみました。

半分今までの復習みたいな感じです。


プロジェクトはこちらです。

github.com


VFT+テッセレーション

VTF(Vertex Texture Fetch)とは頂点シェーダー内でテクスチャを参照することを指すそうです。 (参照: wgld.org | WebGL: 頂点テクスチャフェッチ(VTF) |

テッセレーションはポリゴンメッシュをさらに分割して表現することを意味します。(参照: テッセレーション - Wikipedia  テッセレーション基礎 - しゅみぷろ


今回はGrayScottで得た模様テクスチャを使って頂点を上下したいと思います。

 

シェーダープログラム
 

 

Shader "Unlit/HeightShader"
{
    Properties{
        _TessFactor("Tess Factor", Vector) = (2, 2, 2, 2)
        _LODFactor("LOD Factor", Range(0, 10)) = 1
        _MainTex("Main Texture", 2D) = "white" {}

        _ParallaxScale("ParallaxScale", Float) = 1


    }

        SubShader{
                Pass {

                Tags { "LightMode" = "ForwardBase" }

                CGPROGRAM
    #include "UnityCG.cginc"
    #include "UnityLightingCommon.cginc"

    #pragma vertex VS
    #pragma fragment FS
    #pragma hull HS
    #pragma domain DS
    #define INPUT_PATCH_SIZE 3
    #define OUTPUT_PATCH_SIZE 3

        uniform vector _TessFactor;
        uniform float _LODFactor;
        uniform sampler2D _MainTex;

        uniform sampler2D _ParallaxMap;
        uniform float _ParallaxScale;


        struct appdata {
            float4 w_vert : POSITION;
            float2 texcoord : TEXCOORD0;
            float3 normal : NORMAL;
        };

        struct v2h {
            float4 pos : POS;
            float2 texcoord : TEXCOORD0;
            float3 normal : NORMAL;
            float4 worldPos : TEXCOORD1;
        };

        struct h2d_main {
            float3 pos : POS;
            float2 texcoord : TEXCOORD0;
            float3 normal : NORMAL;
            float4 worldPos : TEXCOORD1;
        };

        struct h2d_const {
            float tess_factor[3] : SV_TessFactor;
            float InsideTessFactor : SV_InsideTessFactor;
        };

        struct d2f {
            float4 pos : SV_Position;
            float2 uv : TEXCOORD0;
            float3 normal : NORMAL;
            float3 worldPos : TEXCOORD1;
        };

        struct f_input {
            float4 vertex : SV_Position;
            float2 uv : TEXCOORD0;
            float3 normal : NORMAL;
            float3 worldPos : TEXCOORD1;
        };

        v2h VS(appdata i) {
            v2h o = (v2h)0;

            o.pos = float4(i.w_vert.xyz, 1.0f);

            o.texcoord = i.texcoord;

            o.normal = i.normal;

            o.worldPos = mul(unity_ObjectToWorld, i.w_vert);
            return o;
        }

        h2d_const HSConst(InputPatch<v2h, INPUT_PATCH_SIZE> i) {
            h2d_const o = (h2d_const)0;
            o.tess_factor[0] = _TessFactor.x * _LODFactor;
            o.tess_factor[1] = _TessFactor.y * _LODFactor;
            o.tess_factor[2] = _TessFactor.z * _LODFactor;
            o.InsideTessFactor = _TessFactor.w * _LODFactor;
            return o;
        }

        [domain("tri")]
        [partitioning("integer")]
        [outputtopology("triangle_cw")]
        [outputcontrolpoints(OUTPUT_PATCH_SIZE)]
        [patchconstantfunc("HSConst")]
        h2d_main HS(InputPatch<v2h, INPUT_PATCH_SIZE> i, uint id : SV_OutputControlPointID) {
            h2d_main o = (h2d_main)0;
            o.pos = i[id].pos;
            o.texcoord = i[id].texcoord;
            o.normal = i[id].normal;
            o.worldPos = i[id].worldPos;
            return o;
        }

        [domain("tri")]
        d2f DS(h2d_const hs_const_data, const OutputPatch<h2d_main, OUTPUT_PATCH_SIZE> i, float3 bary:SV_DomainLocation) {
            d2f o = (d2f)0;
            float3 pos = i[0].pos * bary.x + i[1].pos * bary.y + i[2].pos * bary.z;
            float2 uv = i[0].texcoord * bary.x + i[1].texcoord * bary.y + i[2].texcoord * bary.z;
            float3 normal = i[0].normal * bary.x + i[1].normal * bary.y + i[2].normal * bary.z;
            float3 worldPos = i[0].worldPos * bary.x + i[1].worldPos * bary.y + i[2].worldPos * bary.z;


            // 表面の凹凸を計算
            float parallax = tex2Dlod(_MainTex, float4(uv.xy, 0, 0)).r;
            float parallaxHeight = parallax * _ParallaxScale;

            pos += parallaxHeight * normal;

            o.pos = UnityObjectToClipPos(float4(pos, 1));
            o.uv = uv;
            o.normal = UnityObjectToWorldNormal(normal);
            o.worldPos = worldPos;

            return o;
        }



        float4 FS(f_input i) : SV_Target{

            // テクスチャマップからカラー値をサンプリング
            float4 tex = tex2D(_MainTex, i.uv);

            return float4(tex.r, tex.r, tex.r, 1);
        }
            ENDCG
        }
    }
}

 


こちらのシェーダーは過去にやった内容を使用しています。

shitakami.hatenablog.com


結果

このシェーダーはComputeShaderとCustomRenderTextureの両方に適用できます。

f:id:vxd-naoshi-19961205-maro:20200816005542g:plain



テクスチャ書き込み+動的パラメータ変更

今まではシーンを再生するとパラメータを変更しても模様に変化はありませんでした。

そこで、シーンを再生しながらパラメータを変更するとどうなるのか確かめることが出来るシーンを作成しました。加えて、マウスからテクスチャに値を書き込み出来るようにしました。

 

プログラムについてですが、突貫で書いた部分が多くあまりまとまっていないのでお見せするのが難しいです。

パラメータの変更はLerpを使って、あるパラメータから別のパラメータへ緩やかに変更している形になります。

テクスチャの書き込みは、RaycastHitを使ってuv座標を受け取りComputeShaderでそこに値を書き込む流れです。


結果

触った感じとても面白いです!



シェーダー内でパラメータを変更

最後に、CustomRenderTexture内でsin波を流して2つのパラメータが連続して切り替わるシェーダーを作成しました。

やることはテクスチャの中心から広がるようにsin波を流してその値に合わせてパラメータを変更します。 こちらの内容もほぼほぼ前回と同じです。

 

シェーダープログラム
 

 

Shader "Unlit/UpdateGrayScott_Changing"
{
    Properties
    {
        _F1("F1", Float) = 0.04
        _K1("K1", Float) = 0.06

        _F2("F2", Float) = 0.035
        _K2("K2", Float) = 0.065

        _SimulateSpeed1("SimulateSpeed1", Float) = 1
        _SimulateSpeed2("SimulateSpeed2", Float) = 1

        [Space(10)]
        _WaveSpeed("WaveSpeed", Float) = 1

        _T("T", Float) = 1
        
        _GridSize("Delta UV", Float) = 1

        _Du("Du", Float) = 0.2
        _Dv("Dv", Float) = 0.1

    }
    SubShader
    {
        Cull Off
        ZWrite Off
        ZTest Always

        Pass
        {
            Name "Update"
            CGPROGRAM
            #pragma vertex CustomRenderTextureVertexShader
            #pragma fragment frag

            #include "UnityCustomRenderTexture.cginc"

            float _F1;
            float _K1;

            float _F2;
            float _K2;

            fixed _WaveSpeed;
            fixed _T;

            float _SimulateSpeed1;
            float _SimulateSpeed2;

            float _GridSize;
            
            float _Du;
            float _Dv;

            fixed4 frag (v2f_customrendertexture i) : SV_Target
            {
                float2 uv = i.globalTexcoord;

                float distance = (uv.x-0.5)*(uv.x-0.5)+(uv.y-0.5)*(uv.y-0.5);

                float sinValue = sin(_Time.y * _WaveSpeed - distance * _T);
                float rate = (sinValue + 1) / 2.0;

                float f = lerp(_F1, _F2, rate);
                float k = lerp(_K1, _K2, rate);

                // 1pxあたりの単位を計算
                float du = 1.0 / _CustomRenderTextureWidth;
                float dv = 1.0 / _CustomRenderTextureHeight;
                float3 duv = float3(du, dv, 0) * _GridSize;

                // 現在のテクスチャの取得
                float2 c = tex2D(_SelfTexture2D, uv);
                float u = c.x;
                float v = c.y;

                // ラプラスの演算を行う
                float2 laplacian = 
                    tex2D(_SelfTexture2D, uv - duv.zy).xy +
                    tex2D(_SelfTexture2D, uv + duv.zy).xy +
                    tex2D(_SelfTexture2D, uv - duv.xz).xy +
                    tex2D(_SelfTexture2D, uv + duv.xz) - 4.0*float2(u, v);

                float uvv = u*v*v;

                // Gray-Scottモデルの反応拡散方程式
                float dudt = _Du * laplacian.x - uvv + f * (1.0 - u);
                float dvdt = _Dv * laplacian.y + uvv - (f + k) * v;

                float speed = lerp(_SimulateSpeed1, _SimulateSpeed2, rate);

                return float4(saturate(u + dudt * speed), saturate(v + dvdt * speed), 0, 0);
                
            }
            ENDCG
        }
    }
}

 


結果

f:id:vxd-naoshi-19961205-maro:20200816012828g:plain

f:id:vxd-naoshi-19961205-maro:20200816012548g:plain



最後に

これまでにやったことを使って色々遊んでみました。

Gray-Scottを使うだけで面白い模様も作成できますし、インタラクションを追加すればさらに面白いことが出来ると感じました。

これでGray-Scottの内容については終わりになると思います。また、何かしらで興味が出たらまた触るかもしれません。


参考

参考と言っていいかわかりませんが、この動画が今回のブログを書くモチベーションとなりました。

vimeo.com

CustomRenderTextureで実装するGray-Scottモデル

f:id:vxd-naoshi-19961205-maro:20200814004309j:plain

始めに

つい最近、部活のブログでComputeShaderを使用したGray-Scottモデルについて紹介しました。

aizu-vr.hatenablog.com



このブログを書きながら、「これってCustomRenderTextureでも実装できるんじゃね?」と思ったのでやってみようと思います。


今回のプロジェクトはこちらになります。

github.com

CustomRenderTextureについて

カスタムレンダーテクスチャはレンダーテクスチャの拡張機能で、これを使うと簡単にシェーダー付きのテクスチャを作成できます。これは、コースティクス、雨の効果に使われるリップルシミュレーション、壁面へぶちまけられた液体など、あらゆる種類の複雑なシミュレーションを実装するのに便利です。また、カスタムレンダーテクスチャはスクリプトやシェーダーのフレームワークを提供し、部分的更新、または、マルチパスの更新、更新頻度の変更などのさらに複雑な設定をサポートします。

Unity公式から引用しました。( カスタムレンダーテクスチャ - Unity マニュアル )


私なりに要約すると、「機能を盛り込んだシェーダーをテクスチャに付加して動きを付けることが出来る」ということだと思います。



実装

では早速スクリプトを紹介したいと思います。

UpdateGrayScott.shader

Shader "Unlit/UpdateGrayScott"
{
    Properties
    {
        _F("F", Float) = 0.04
        _K("K", Float) = 0.06

        _SimulateSpeed("SimulateSpeed", Float) = 1
        
        _GridSize("Delta UV", Float) = 1

        _Du("Du", Float) = 0.2
        _Dv("Dv", Float) = 0.1

    }
    SubShader
    {
        Cull Off
        ZWrite Off
        ZTest Always

        Pass
        {
            Name "Update"
            CGPROGRAM
            #pragma vertex CustomRenderTextureVertexShader
            #pragma fragment frag

            #include "UnityCustomRenderTexture.cginc"

            float _F;
            float _K;
            float _SimulateSpeed;

            float _GridSize;
            
            float _Du;
            float _Dv;

            fixed4 frag (v2f_customrendertexture i) : SV_Target
            {
                float2 uv = i.globalTexcoord;

                // 1pxあたりの単位を計算
                float du = 1.0 / _CustomRenderTextureWidth;
                float dv = 1.0 / _CustomRenderTextureHeight;
                float3 duv = float3(du, dv, 0) * _GridSize;

                // 現在のテクスチャの取得
                float2 c = tex2D(_SelfTexture2D, uv);
                float u = c.x;
                float v = c.y;

                // ラプラスの演算を行う
                float2 laplacian = 
                    tex2D(_SelfTexture2D, uv - duv.zy).xy +
                    tex2D(_SelfTexture2D, uv + duv.zy).xy +
                    tex2D(_SelfTexture2D, uv - duv.xz).xy +
                    tex2D(_SelfTexture2D, uv + duv.xz) - 4.0*float2(u, v);

                float uvv = u*v*v;

                // Gray-Scottモデルの反応拡散方程式
                float dudt = _Du * laplacian.x - uvv + _F * (1.0 - u);
                float dvdt = _Dv * laplacian.y + uvv - (_F + _K) * v;

                return float4(saturate(u + dudt * _SimulateSpeed), saturate(v + dvdt * _SimulateSpeed), 0, 0);
            }
            ENDCG
        }
    }
}


InitializeGrayScott

Shader "Unlit/InitializeGrayScott"
{
    Properties
    {
        _MainTex ("NoiseTexture", 2D) = "white" {}
        _QuadWidth("QuadWidth", Range(0, 0.5)) = 0.2
    }
    SubShader
    {
        Cull Off ZWrite Off ZTest Always
        Pass
        {
            Name "Initialize"
            CGPROGRAM

            #pragma vertex InitCustomRenderTextureVertexShader
            #pragma fragment frag
            
            #include "UnityCustomRenderTexture.cginc"

            sampler2D _MainTex;
            float4 _MainTex_ST;
            fixed _QuadWidth;

            fixed4 frag (v2f_init_customrendertexture i) : SV_Target
            {
                float2 uv = i.texcoord;

                // ノイズテクスチャを取得
                fixed4 col = tex2D(_MainTex, uv) * 0.1;

                // テクスチャの中央の閾値を求める
                fixed minThreadhold = 0.5 - _QuadWidth;
                fixed maxThreadhold = 0.5 + _QuadWidth;

                if(minThreadhold < uv.x && uv.x < maxThreadhold &&
                    minThreadhold < uv.y && uv.y < maxThreadhold)
                    col.xy += float2(0.5, 0.25);
                else
                    col.xy += float2(1, 0);

                return col;
            }
            ENDCG
        }
    }
}


RenderTextureUpdater

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

public class RenderTextureUpdater : MonoBehaviour
{
    [SerializeField]
    private CustomRenderTexture m_texture;

    [SerializeField]
    private int m_updateStep = 1;

    void Start()
    {
        m_texture.Initialize();
    }

    void Update()
    {
        m_texture.Update(m_updateStep);
    }
}



解説

主にCustomRenderTextureについて解説します。

また、ここではGray-Scottについては解説しません。もし興味がある方は冒頭でも紹介した記事を参照してください。

aizu-vr.hatenablog.com



CustomRenderTextureの作成

まずはCustomRenderTextureを作成します。

f:id:vxd-naoshi-19961205-maro:20200813235308p:plain


作成したCustomRenderTextureの設定は次のようになります。 Color Formatを"R16G16_UNORM"、DoubleBufferedにチェックを入れてください

f:id:vxd-naoshi-19961205-maro:20200814000609j:plain



Gray-Scottのシミュレーションでは2つの成分しか使用しないので2チャンネルのカラーフォーマットを使用します。

DoubleBufferedをオンにすることで、値を更新する際にテクスチャにアクセスすることが可能になります。



初期化処理

こちらも冒頭記事の内容と同様に以下の処理を行っております。

  • テクスチャの中央に指定された幅で(u, v) = (0.5, 0.25)、それ以外は(u, v) = (1, 0)で初期化
  • ランダム性を持たせるために、ノイズテクスチャを加算
            // InitializeGrayScott.shaderの25行目から
            fixed4 frag (v2f_init_customrendertexture i) : SV_Target
            {
                float2 uv = i.texcoord;

                // ノイズテクスチャを取得
                fixed4 col = tex2D(_MainTex, uv) * 0.1;

                // テクスチャの中央の閾値を求める
                fixed minThreadhold = 0.5 - _QuadWidth;
                fixed maxThreadhold = 0.5 + _QuadWidth;

                if(minThreadhold < uv.x && uv.x < maxThreadhold &&
                    minThreadhold < uv.y && uv.y < maxThreadhold)
                    col.xy += float2(0.5, 0.25);
                else
                    col.xy += float2(1, 0);

                return col;
            }


結果は以下のようになります。

f:id:vxd-naoshi-19961205-maro:20200814001604p:plain



更新処理

こちらはそのまま数式を実装した形になります。ほぼほぼComputeShaderの実装と同じです。

            // UpdateGrayScott.shaderの40行目から
            fixed4 frag (v2f_customrendertexture i) : SV_Target
            {
                float2 uv = i.globalTexcoord;

                // 1pxあたりの単位を計算
                float du = 1.0 / _CustomRenderTextureWidth;
                float dv = 1.0 / _CustomRenderTextureHeight;
                float3 duv = float3(du, dv, 0) * _GridSize;

                // 現在のテクスチャの取得
                float2 c = tex2D(_SelfTexture2D, uv);
                float u = c.x;
                float v = c.y;

                // ラプラスの演算を行う
                float2 laplacian = 
                    tex2D(_SelfTexture2D, uv - duv.zy).xy +
                    tex2D(_SelfTexture2D, uv + duv.zy).xy +
                    tex2D(_SelfTexture2D, uv - duv.xz).xy +
                    tex2D(_SelfTexture2D, uv + duv.xz) - 4.0*float2(u, v);

                float uvv = u*v*v;

                // Gray-Scottモデルの反応拡散方程式
                float dudt = _Du * laplacian.x - uvv + _F * (1.0 - u);
                float dvdt = _Dv * laplacian.y + uvv - (_F + _K) * v;

                // return float4(saturate(u + dudt * _SimulateSpeed), saturate(v + dvdt * _SimulateSpeed), 0, 0);
                return float4(c, 0, 0);
            }


比較用としてComputeShaderでの実装も載せておきます。

GrayScottCalculator.compute

// GrayScottCalculator.computeの38行目から
void UpdateGrayScotte(uint3 id : SV_DispatchThreadID) {

    float u = Texture[id.xy].x;
    float v = Texture[id.xy].y;

    float2 laplacian =
        Texture[id.xy + uint2(-1, 0)] +
        Texture[id.xy + uint2(1, 0)] +
        Texture[id.xy + uint2(0, -1)] +
        Texture[id.xy + uint2(0, 1)] - 4 * Texture[id.xy];

    laplacian /= (dx*dx);
    
    float dudt = Du * laplacian.x - u * v * v + f * (1.0 - u);
    float dvdt = Dv * laplacian.y + u * v * v - (f + k) * v;
    
    Texture[id.xy] = float2(u + dt * dudt, v + dt * dvdt);
}



C#プログラムから初期化、更新処理を呼び出す

CustomRenderTextureはC#からパス処理を呼び出すことが出来ます。 また、更新処理は指定された回数分行われます。

    // RenderTextureUpdater.csの13行目から
    void Start()
    {
        m_texture.Initialize();
    }

    void Update()
    {
        m_texture.Update(m_updateStep);
    }



マテリアルをCustomRenderTextureに設定する

2つシェーダーからマテリアルを作成して、CustomRenderTextureに設定してください。また、この時Initialize ModeとUpdate ModeはOnLoadに設定しましょう。

f:id:vxd-naoshi-19961205-maro:20200814002833j:plain

結果

ComputeShaderの時と似た形を生成することが出来ました。

f:id:vxd-naoshi-19961205-maro:20200814003519g:plain


あとは同じようにパラメータを変更したり、マテリアルを変更することで以下のような模様も作成ることが出来ます。

f:id:vxd-naoshi-19961205-maro:20200814003808p:plain



感想

前に波動方程式の実装を調べた際にComputeShaderとCustomRenderTextureの2種類の実装を見つけました。今回はそれと同じようにGrayScottを2種類の実装を提示できました。

正直どっちがいいのかと言われればケースバイケースだと思うので好きな方で実装するのがいいと思います。


次は、Gray-Scottで簡単に遊んだ記事でも書こうかと思います。


追記

遊んでみた記事書きました。

shitakami.hatenablog.com



参考

light11.hatenadiary.com

tips.hecomi.com

edom18.hateblo.jp

画面をキャプチャしてgif変換してはてなブログに載せるまで

始めに

1カ月くらいブログを書いてなかったので久々に更新したいと思います。

今回は自分のメモ用として書いていく予定です。

また、今回はWindows向けとなるのでご了承ください。



画面のキャプチャ、録画

私がブログをまとめる際によく使っているショートカットキーがこれらです。

Shift + Windowsキー + s(画面の切り取り)はかなり便利でスクショしてからトリミングする作業を一度に行うことが出来ます。

また、GoogleスライドやはてなブログではスクショしたものをCttl + v(貼り付け)で直に載せるとが可能です。

一つ欠点としては切り取られた画面はクリップボードに保存されるだけなので、画像として保存する場合はペイントソフトを経由する必要があります。


Windowsキー + Altキー + r(録画)では画面の撮影を行うことができます。私はこの機能を使用してUnityなどを撮影しています。

また、撮影した動画は再生時間を調整することが出来るのでブログに載せる際は基本的に短めにしています。

f:id:vxd-naoshi-19961205-maro:20200812231311p:plain



これらのショートカットキーですが、Windowsの設定で変更することが可能です。

また、録画のフレームレートや画質等も変更することが出来ます。

f:id:vxd-naoshi-19961205-maro:20200812235301p:plain



動画の範囲指定

動画撮影した際に、余分なスペース(Unityエディタやタスクバーなど)が映ってしまい見せたい部分が小さくなってしまう問題が多々ありました。

そこで最近このサイト利用させて頂いています。

online-video-cutter.com


このサイトでは動画の領域を指定してトリミングすることが可能です。また、再生時間の調整や動画の品質変更も行えます。

f:id:vxd-naoshi-19961205-maro:20200812231833p:plain



動画のgif変換

先程のサイトで動画のトリミングを行った後にgif動画に変更します。

mp4をgifに変換する際はこのサイトを使っています。

www.aconvert.com

gifに変換する前にビデオサイズやフレームレート、アスペクト比などを調整することが出来ます。

f:id:vxd-naoshi-19961205-maro:20200812232605p:plain

gif動画の注意

はてなブログではサイズの大きいgif動画をアップロードすることは出来ません。 試しに、10MB以上のgifをアップロードしようとしましたが怒られました。

なので、gif変換する際に出来るだけサイズを小さくする必要があります。

f:id:vxd-naoshi-19961205-maro:20200812233057p:plain


検証

gif変換でどのようにしたら上手くサイズを小さく出来るか検証してみることにしました。


デフォルト設定でgif変換

デフォルト設定で変換した場合、約300KBの動画が約20MBのgif動画になりました。 このままで上げるのは無理です。


ビデオサイズの変更

一度、ビデオサイズを640x480で行ってみます。結果は8.5MBとなり、アップロードにも成功しました。 以下結果のgifとなります。

f:id:vxd-naoshi-19961205-maro:20200812235702g:plain


次に480x272で行ってみました。結果は4MBでした。こちらも同様に載せておきます。

f:id:vxd-naoshi-19961205-maro:20200813000008g:plain


サイズの変更はかなりサイズ削減につながりますが、ブログに載せたときにgif動画の大きさに影響が出ることがわかりました。



フレームレートの変更

デフォルトのフレームレートは30fpsなので一度20fpsまで落としてみたいと思います。それ以外の設定はデフォルトのままとします。

結果は13.6MBでアップロードできるサイズにはなりませんでした。


次に10fpsで変換してみました。結果は7.6MBでおおよそ20MBから1 / 3になりました。

以下結果のgif動画です。意外とfpsは気にならないことが分かりました。(でも先程と比べて低いのは分かる程度です)

f:id:vxd-naoshi-19961205-maro:20200813000634g:plain

恐らく、フレームレートとgif動画のサイズの比はおおよそ同じになっているかもしれません。



ビット率

あまりビット率について把握していないので調べてみます。

試しにビット率を512kで変換してみましたが、結果は19MBとほぼ変わらず。

続いて256kで変換してみましたが、これも変わらず。

最後に64kで変換してみましたが、同様の結果となりました。

この項目については触る必要はないようです。



動画トリミング時に品質変更

f:id:vxd-naoshi-19961205-maro:20200813001736p:plain

これはおまけですが、動画のトリミングをする際にも品質を変更することが出来たのでこちらも調べてみます。

デフォルト(Quality:Same)トリミングをした場合は動画サイズは345KBとなり、デフォルト設定でgif変換した結果は18MBでした。


次にQuality:480pで変換を行った場合は191KBとなり、同様にgif変換した結果5.9MBとなりました。


最後にQuality:240pで変換した場合は110KBで、gif変換後は1.7MBでした。 以下のgif動画が結果となります。

f:id:vxd-naoshi-19961205-maro:20200813002338g:plain



gif変換時のサイズ変更と同様に載せた際にサイズが小さくなりました。



感想

ブログに載せる際の大きさを考えて動画トリミング時の品質設定、もしくはgif変換時のサイズ設定を決めてからfpsの調整をするのが良いかもしれません。

ただ、gif動画が小さくても良いならば動画トリミング時の品質設定だけで良いかもしれません。


最後にこのブログを書きながら気づいたことなのですが画面の切り取りで撮った画像ってそのままはてなブログに載せることが出来たんですね。今まで保存してから載せてました。

これからのブログ執筆がちょっと楽になるかもしれません。

AtCoder緑になった

f:id:vxd-naoshi-19961205-maro:20200705232257p:plain

始めに

AtCoderを続けて緑になれたので、ブログにまとめようと思います。

過去記事です。

shitakami.hatenablog.com

shitakami.hatenablog.com



2か月間やらなかった

早速ですが、4月の頭ぐらいから6月の頭ぐらいまでコンテストに出ない期間がありました。 この期間に問題を解くことすらしませんでした。

理由として、茶色になった後のコンテストの成績が悪かったことが一番かもしれません。また、別のことに集中しすぎて疎かになってしまいました。



AtCoderを再開したきっかけ

6月の頭から復帰した理由としてはやることが少し落ち着いたことと、友人も始めたことが大きな理由です。

また、少しずつではありますがコンテストの成績が良くなっていることが実感できたことも理由としてあります。


緑になるまでにやったこと

紹介された問題を解く

こちらの問題を幅優先探索の途中まで解きました。個人的にとても難しいと感じているので余裕があるときに少しずつ解いてます。

qiita.com


ABCのD問題を解く

f:id:vxd-naoshi-19961205-maro:20200706001045p:plain
ABC126~のD問題を少しずつ解いています。すべて解ききれてはいませんがあと少しでD問題を埋めることが出来そうです。


AtCoder ProblemsのRecommendatoinを解く

最近メインでやっているのがRecommendationで出された問題を解くことです。

f:id:vxd-naoshi-19961205-maro:20200705235230p:plain


Recommendationの良いところは使うアルゴリズムを自分で考えることだと思います。

紹介された問題を解く場合、「このアルゴリズムを使えば解けるんだ」と解く前から意識してしまいます。しかし、Recommendationではそれがなく、自分で解法を考えなくてはいけません。それが個人的にとても楽しいと感じています。

また、解けるか解けないかの問題が出題されるため解けたときには嬉しかったり、逆に解けなかった際は悔しさをバネにそれに関する問題を解くことに躍起になったりします。



緑レートになるまでに思ったこと

私自身緑レートにはなれると思っていましたが、ここまで時間が掛かるとは思ってもいませんでした。

paizaでは1問解けば簡単にレートが上がりますが、AtCoderの場合はそうではなくコンテストに出てよい成績が出なければレートが上がることもないですし、最悪下がることもあります。

そういう点で舐めてかかっていたなと反省しています。

しかし、緑レートになるまで5か月と時間が掛かりました(実際にやっていたのは3か月)がその時間が努力した時間だと考えれば緑レートの価値ってかなりあるのではないかなと思います。

周りの方々はさらに長い時間競技プログラミングを行っているので私も見習ってこれからも続けたいと思います。



これからについて

コンテストに出場したり、過去問を解いて今の私には緑レートが精一杯だと感じています。

主な理由としては

  • 数学的思考が出来ていない
  • 経験が浅い
  • 難しいアルゴリズムを実装できない

これらは私が過去に何度も挫折した問題でもあります。逆にこれらを解決できれば次のステップに進むことが出来るのではないかなとも感じています。

これからの目標として、

  • 上記サイトの水色になるための問題を埋める
  • D問題を埋める
  • 緑diffの問題をすべて解く

をコツコツとやっていこうと思います。

Viveコントローラーでマウスを動かす (Unity)

f:id:vxd-naoshi-19961205-maro:20200705163234j:plain



始めに

前回の記事です。

shitakami.hatenablog.com


VTuberで勉強会をする際にDiscordをいじったりしたかったので、Viveコントローラーでマウスを動かす機能を作成しました。

あと、マウスの入力に関する記事はたくさんありますが逆にマウスを動かす内容の記事があまりなかったのでブログでまとめようと思います。



dllファイルをPluginsフォルダに入れる

マウスを動かすためにSystem.Windows.Forms.dllファイルとSystem.Drawing.dllファイルをUnityプロジェクトのAssets/Pluginsに入れる必要があります。


実はこれらのファイルはUnityのフォルダの中に入っております。

私の場合は\Unity\Hub\Editor\2019.2.6f1\Editor\Data\Mono\lib\mono\2.0 にありました。


これらのファイルを先程説明した通り、プロジェクトのAssets/Pluginsの中にコピーしてください。 dllファイルを入れることで、これらをスクリプトで使用することが出来ます。

f:id:vxd-naoshi-19961205-maro:20200705004003j:plain




キーボードでマウス操作する

始めに、キーボードでマウスを操作してみます。

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

public class TestMouseController : MonoBehaviour
{

    [SerializeField]
    private int m_mouseSpeed;

    [DllImport("user32.dll")]
    static extern void mouse_event(int dwFlag, int dx, int dy, int cButtons, int dwExtraInfo);

    private const int m_mouse_leftDown = 0x2;
    private const int m_mouse_leftUp = 0x4;
    private const int m_mouse_rightDown = 0x8;
    private const int m_mouse_rightUp = 0x10;

    // Update is called once per frame
    void Update()
    {
        
        int x = System.Windows.Forms.Cursor.Position.X;
        int y = System.Windows.Forms.Cursor.Position.Y;


        if(Input.GetKey(KeyCode.RightArrow)) 
            x += m_mouseSpeed;
        if(Input.GetKey(KeyCode.LeftArrow))
            x -= m_mouseSpeed;

        if(Input.GetKey(KeyCode.UpArrow))
            y -= m_mouseSpeed;
        if(Input.GetKey(KeyCode.DownArrow))
            y += m_mouseSpeed;


        System.Windows.Forms.Cursor.Position = new System.Drawing.Point(x, y);
        
        

        if(Input.GetKeyDown(KeyCode.Space)) {
            mouse_event(m_mouse_leftDown, 0, 0, 0, 0);
            Debug.Log("左クリック、Down");

        }
        else if(Input.GetKeyUp(KeyCode.Space)) {
            mouse_event(m_mouse_leftUp, 0, 0, 0, 0);
            Debug.Log("左クリック、Up");
        }

        if(Input.GetKeyDown(KeyCode.R)) {
            mouse_event(m_mouse_rightDown, 0, 0, 0, 0);
            Debug.Log("右クリック、Down");
        }
        else if(Input.GetKeyUp(KeyCode.R)) {
            mouse_event(m_mouse_rightUp, 0, 0, 0, 0);
            Debug.Log("右クリック、Up");
        }

        

    }
}


もし、コンパイルエラーになっていた際はdllファイルがちゃんと入っているかを確認してください。



マウスの移動

初めにマウスの移動について解説します。

System.Windows.Forms.Cursor.Posiotionから現在のマウスの座標を取得できます。 Xは横方向、Yは縦方向となります。

int x = System.Windows.Forms.Cursor.Position.X;
int y = System.Windows.Forms.Cursor.Position.Y;



注意する点として、マウスの原点 (0, 0) は画面の左上になります。

f:id:vxd-naoshi-19961205-maro:20200705012536p:plain



あとは軸を合わせてマウスの座標を計算してSystem.Windows.Forms.Cursor.Positionに代入します。

        if(Input.GetKey(KeyCode.RightArrow)) 
            x += m_mouseSpeed;
        if(Input.GetKey(KeyCode.LeftArrow))
            x -= m_mouseSpeed;

        if(Input.GetKey(KeyCode.DownArrow))
            y += m_mouseSpeed;
        if(Input.GetKey(KeyCode.UpArrow))
            y -= m_mouseSpeed;


        System.Windows.Forms.Cursor.Position = new System.Drawing.Point(x, y);




クリック操作

マウスのクリックイベントを行うために、dllファイルの関数を呼び出す必要があります。 そこでDllImportを行います。

    [DllImport("user32.dll")]
    static extern void mouse_event(int dwFlag, int dx, int dy, int cButtons, int dwExtraInfo);

DllImportについてはUnity公式を参照してください。(ネイティブプラグイン - Unity マニュアル



あとはこの関数の第一引数にイベントの種類を表す値を入れて呼び出すことでマウスの左クリックや右クリックが出来ます。

    . . . . . 
    private const int m_mouse_leftDown = 0x2;
    private const int m_mouse_leftUp = 0x4;
    private const int m_mouse_rightDown = 0x8;
    private const int m_mouse_rightUp = 0x10;
    . . . . .

        if(Input.GetKeyDown(KeyCode.Space)) {
            mouse_event(m_mouse_leftDown, 0, 0, 0, 0);
            Debug.Log("左クリック、Down");

        }
        else if(Input.GetKeyUp(KeyCode.Space)) {
            mouse_event(m_mouse_leftUp, 0, 0, 0, 0);
            Debug.Log("左クリック、Up");
        }

        if(Input.GetKeyDown(KeyCode.R)) {
            mouse_event(m_mouse_rightDown, 0, 0, 0, 0);
            Debug.Log("右クリック、Down");
        }
        else if(Input.GetKeyUp(KeyCode.R)) {
            mouse_event(m_mouse_rightUp, 0, 0, 0, 0);
            Debug.Log("右クリック、Up");
        }



マウスイベントの種類についてはこちらの記事を参考にしています。

nonsoft.la.coocan.jp




Viveコントローラーでマウスを動かす

次にViveコントローラーでマウスを動かすプログラムです。

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

public class MouseController : MonoBehaviour
{

    [SerializeField]
    private GameObject m_controller;

    [SerializeField]
    private float m_mouseSpeed;

    private IControllerInput m_controllerInput;

    private Vector2 m_lastMousePosition;

    [DllImport("user32.dll")]
    static extern void mouse_event(int dwFlag, int dx, int dy, int cButtons, int dwExtraInfo);

    private const int m_mouse_leftDown = 0x2;
    private const int m_mouse_leftUp = 0x4;
    private const int m_mouse_rightDown = 0x8;
    private const int m_mouse_rightUp = 0x10;

    // Start is called before the first frame update
    void Start()
    {
        m_controllerInput = m_controller.GetComponent<IControllerInput>();
    }

    // Update is called once per frame
    void Update()
    {
        
        CheckMouseLeftClick();

        CheckMouseRightClick();

        MouseCursorMove();

    }


    private void CheckMouseLeftClick() {

        if(m_controllerInput.IsTriggerDown())
            mouse_event(m_mouse_leftDown, 0, 0, 0, 0);
        else if(m_controllerInput.IsTriggerUp())
            mouse_event(m_mouse_leftUp, 0, 0, 0, 0);

    }

    private void CheckMouseRightClick() {

        if(m_controllerInput.IsOptionDown())
            mouse_event(m_mouse_rightDown, 0, 0, 0, 0);
        else if(m_controllerInput.IsOptionUp())
            mouse_event(m_mouse_rightUp, 0, 0, 0, 0);

    }


    private void MouseCursorMove() {

        if(m_controllerInput.IsTouchpadTouch()) {

            m_lastMousePosition = m_controllerInput.GetTouchPadAxis();

        }
        else if(m_controllerInput.IsTouchpadTouching()) {

            var newMousePosition = m_controllerInput.GetTouchPadAxis();
            var touchpadVector = newMousePosition - m_lastMousePosition;

            touchpadVector *= m_mouseSpeed;

            float x = System.Windows.Forms.Cursor.Position.X;
            float y = System.Windows.Forms.Cursor.Position.Y;

            x += touchpadVector.x;
            y -= touchpadVector.y;

            System.Windows.Forms.Cursor.Position = new System.Drawing.Point((int)x, (int)y);

            m_lastMousePosition = newMousePosition;

        }


    }

}




Viveコントローラーを使用すると言いながら、このプログラムではインターフェースを使ってコントローラーからの入力を受け取っています。

    . . . . . . .
    [SerializeField]
    private GameObject m_controller;
    . . . . . . .
    . . . . . . .

    // Start is called before the first frame update
    void Start()
    {
        m_controllerInput = m_controller.GetComponent<IControllerInput>();
    }


現在はViveコントローラーを使用していますが、後々Oculus Touch等に切り替わる可能性があるのでインターフェースでコントローラー入力をラッピングしました。

基本的にはこれら記事を参考にしてコントローラーの入力を受け取っています。

framesynthesis.jp

qiita.com



マウスのクリック処理

マウスの左クリックと右クリックはViveコントローラーのトリガーとメニューボタンを使用しています。ボタンを押し込んだときにマウスを押し込んだイベントを呼び出し、ボタンを離した際にマウスを離したイベントを呼び出しています。

    private void CheckMouseLeftClick() {

        if(m_controllerInput.IsTriggerDown())
            mouse_event(m_mouse_leftDown, 0, 0, 0, 0);
        else if(m_controllerInput.IsTriggerUp())
            mouse_event(m_mouse_leftUp, 0, 0, 0, 0);

    }

    private void CheckMouseRightClick() {

        if(m_controllerInput.IsOptionDown())
            mouse_event(m_mouse_rightDown, 0, 0, 0, 0);
        else if(m_controllerInput.IsOptionUp())
            mouse_event(m_mouse_rightUp, 0, 0, 0, 0);

    }




マウスの移動処理

マウスの移動は、タッチパッドに振れている座標の変化を使って実装しています。

タッチパッドに触れた瞬間はその触れた座標を保存しています。

    if(m_controllerInput.IsTouchpadTouch()) {

            m_lastMousePosition = m_controllerInput.GetTouchPadAxis();

    }



次にタッチパッドに触れている状態で現在の座標を取得して、前フレームの座標との差を求めます。

  else if(m_controllerInput.IsTouchpadTouching()) {

            var newMousePosition = m_controllerInput.GetTouchPadAxis();
            var touchpadVector = newMousePosition - m_lastMousePosition;



後は現在のマウスの座標にその差分を加えて移動させます。基本的にタッチパッドの座標は 0 ~ 1 の浮動小数になっており、逆にマウスの座標は整数になっているので計算する際はfloat型で行い代入する際はint型にキャストしています。

マウスの座標計算が終わったら現在のタッチパッドの座標を更新します。

            touchpadVector *= m_mouseSpeed;

            float x = System.Windows.Forms.Cursor.Position.X;
            float y = System.Windows.Forms.Cursor.Position.Y;

            x += touchpadVector.x;
            y -= touchpadVector.y;

            System.Windows.Forms.Cursor.Position = new System.Drawing.Point((int)x, (int)y);

            m_lastMousePosition = newMousePosition;




デモ

HMDを付けながらだと画面の確認が出来ないので、こちらを使用させて頂きました。

tips.hecomi.com

https://youtu.be/2G5FVXoSrxw


Viveコントローラーでマウスを動かす [Unity]



動画では行っていませんが、ドラッグアンドドロップも可能です。



まとめ

Unityの機能というより、C#の機能を使うことでマウス操作を行うことが出来ました。個人的に勉強会で使用してみて特に大きな不満はありませんが、いずれはレーザーポインターみたくマウスを操作したいなと思っております。