したかみ ぶろぐ

Unity成分多め

ベクトル場を使った衝突回避Boidsアルゴリズム

始めに

前回のブログで作成したベクトル場を使って衝突回避をするBoidアルゴリズムを実装したいと思います。

今回のサンプルです。

github.com



過去記事です。

shitakami.hatenablog.com

shitakami.hatenablog.com



やったこと

始めにベクトル場を計算します。 このベクトル場はモデルの法線ベクトルを使用しているので、モデルから離れるようなベクトルになります。



Boidsアルゴリズムにベクトル場の力を加えます。 計算は結合・分離・整列の3つの力にベクトル場の力を加えます。こうすることで個体がモデルに近づくと離れる力が働いて逃げるような動きができると考えました。



実装

かなりの行数があるのでプログラムを開く際は注意してください。

ComputeShader

#pragma kernel UpdateVectorField
#pragma kernel InitializeVectorField
#pragma kernel UpdateBoids

#include "Assets/cgincFiles/rotation.cginc"

#define COUNT 3

////////// ベクトル場用変数 ///////////////////////////////////////////

struct VectorFieldSquare {
    float3 position;
    float3 direction;
};


RWStructuredBuffer<VectorFieldSquare> _VectorFieldBuffer;
StructuredBuffer<float3> _PositionBuffer;
StructuredBuffer<float3> _NormalBuffer;

float _SquareScale;
int _VectorFieldLength;

float3 _MinField;
float3 _MaxField;

float3 _ModelPosition[COUNT];
float3 _ModelEulerAngle[COUNT];
float3 _ModelScale[COUNT];

int _VertexCountsThresholds[COUNT];

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

//////////////////// Boidsアルゴリズム用変数 //////////////////////////
struct BoidsData {
    float3 position;
    float3 velocity;
};

RWStructuredBuffer<BoidsData> _BoidsData;

// Boidsパラメータ
float _CohesionForce;
float _SeparationForce;
float _AlignmentForce;

float _CohesionDistance;
float _SeparationDistance;
float _AlignmentDistance;

float _CohesionAngle;
float _SeparationAngle;
float _AlignmentAngle;

float _BoundaryForce;
float _BoundaryRange;
float3 _BoundaryCenter;

float _AvoidForce;

float _MaxVelocity;
float _MinVelocity;

int _InstanceCount;

float _MaxForce;

float _DeltaTime;
float _Epsilon;

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

// 速度と座標から角度を求める
float CalcAngle(float3 velocity, float3 posX, float3 posY) {

    float3 vec = posY - posX;

    return acos(dot(normalize(velocity), normalize(vec)));

}

// 距離の2乗を求める
float CalcSqrDistance(float3 posX, float3 posY) {

    float3 vec = posY - posX;

    return dot(vec, vec);

}

// ベクトルの大きさを制限する
float3 limit(float3 vec, float max)
{
    float length = sqrt(dot(vec, vec)); // 大きさ
    return (length > max && length > 0) ? vec.xyz * (max / length) : vec.xyz;
}

[numthreads(8, 1, 1)]
void UpdateVectorField(uint id : SV_DISPATCHTHREADID) {

    int transformIndex = 0;

    for(int i = 0; (int)id < _VertexCountsThresholds[i]; ++i)
        transformIndex = i;

    float3 pos = _PositionBuffer[id];
    float3 normal = _NormalBuffer[id];
    float4x4 rotMat = EulerAnglesToMatrix(_ModelEulerAngle[transformIndex]);    // 回転行列を求める

    pos = mul(rotMat, pos);

    pos.x *= _ModelScale[transformIndex].x;
    pos.y *= _ModelScale[transformIndex].y;
    pos.z *= _ModelScale[transformIndex].z;

    pos += _ModelPosition[transformIndex];

    // 範囲外は計算しない
    if(pos.x < _MinField.x || _MaxField.x <= pos.x ||
       pos.y < _MinField.y || _MaxField.y <= pos.y ||
       pos.z < _MinField.z || _MaxField.z <= pos.z)
        return;

    // 頂点座標がどのマス目にいるか計算してindexを取得
    int3 index = (pos - _MinField) / _SquareScale;

    normal = mul(rotMat, normal);

    _VectorFieldBuffer[(index.x * _VectorFieldLength * _VectorFieldLength +
                        index.y * _VectorFieldLength +
                        index.z)].direction += normal;

    // ベクトルの要素の符号を取得
    int xd = normal.x / abs(normal.x + _Epsilon);
    int yd = normal.y / abs(normal.y + _Epsilon);
    int zd = normal.z / abs(normal.z + _Epsilon);

    if(0 <= index.x + xd && index.x + xd < _VectorFieldLength)
        _VectorFieldBuffer[(index.x + xd) * _VectorFieldLength * _VectorFieldLength +
                           index.y * _VectorFieldLength +
                           index.z].direction += normal * abs(normal.x);
    
    if(0 <= index.y + yd && index.y + yd < _VectorFieldLength)
        _VectorFieldBuffer[index.x * _VectorFieldLength * _VectorFieldLength +
                           (index.y + yd) * _VectorFieldLength +
                           index.z].direction += normal * abs(normal.y);
    
    if(0 <= index.z + zd && index.z + zd < _VectorFieldLength)
        _VectorFieldBuffer[index.x * _VectorFieldLength * _VectorFieldLength +
                           index.y * _VectorFieldLength +
                           index.z + zd].direction += normal * abs(normal.z);

}


[numthreads(8, 1, 1)]
void InitializeVectorField(uint id : SV_DISPATCHTHREADID) {

    _VectorFieldBuffer[id].direction = float3(0, 0, 0);

}

[numthreads(256, 1, 1)]
void UpdateBoids(uint id : SV_DISPATCHTHREADID) {

        float3 posX = _BoidsData[id.x].position;
    float3 velX = _BoidsData[id.x].velocity;

    float3 cohesionPositionSum = float3(0, 0, 0);
    float3 separationPositionSum = float3(0, 0, 0);
    float3 alignmentVelocitySum = float3(0, 0, 0);

    int cohesionCount = 0;
    int alignmentCount = 0;

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

        // 自身の計算は行わない
        if(i == id.x)
            continue;

        float3 posY = _BoidsData[i].position;
        float3 velY = _BoidsData[i].velocity;

        float sqrDistance = CalcSqrDistance(posX, posY);
        float angle = CalcAngle(velX, posX, posY);

        // 結合
        if(sqrDistance < _CohesionDistance && angle < _CohesionAngle) {
            cohesionPositionSum += posY;
            cohesionCount++;
        }

        // 分離
        if(sqrDistance < _SeparationDistance && angle < _SeparationAngle) {
            separationPositionSum += normalize(posX - posY) / sqrt(sqrDistance);        
        }

        // 整列
        if(sqrDistance < _AlignmentDistance && angle < _AlignmentAngle) {
            alignmentVelocitySum += velY;
            alignmentCount++;
        }

    }

    float3 cohesion = float3(0, 0, 0);
    float3 separation = separationPositionSum;
    float3 alignment = float3(0, 0, 0);
    float3 boundary = float3(0, 0, 0);
    float3 avoid = float3(0, 0, 0);

    if(cohesionCount != 0)
        cohesion = (cohesionPositionSum / (float)cohesionCount - posX) * _CohesionForce;
    
    if(alignmentCount != 0) 
        alignment = (alignmentVelocitySum / (float)alignmentCount - velX) * _AlignmentForce;
    
    separation *= _SeparationForce;

    // 範囲外から出た個体は範囲内に戻る力を加える
    
    float sqrDistFromCenter = dot(_BoundaryCenter - posX, _BoundaryCenter - posX);
    if(sqrDistFromCenter > _BoundaryRange)
        boundary = -_BoundaryForce * (posX - _BoundaryCenter) * (sqrDistFromCenter - _BoundaryRange) / sqrDistFromCenter;

    // ベクトル場内にいる際は、ベクトルの値を取得してその方向に逃げるようにする
    if(_MinField.x <= posX.x && posX.x < _MaxField.x &&
       _MinField.y <= posX.y && posX.y < _MaxField.y &&
       _MinField.z <= posX.z && posX.z < _MaxField.z) {
        int3 index = (posX - _MinField) / _SquareScale;
        avoid = _VectorFieldBuffer[(index.x * _VectorFieldLength * _VectorFieldLength +
                        index.y * _VectorFieldLength +
                        index.z)].direction * _AvoidForce;
       }

    // 結合、分離、整列の力を制限
    cohesion = limit(cohesion, _MaxForce);
    separation = limit(separation, _MaxForce);
    alignment = limit(alignment, _MaxForce);

    velX += (cohesion + separation + alignment + boundary + avoid) * _DeltaTime;

    float velXScale = length(velX);

    // 速度を制限
    if(velXScale < _MinVelocity) {
        velX = _MinVelocity * normalize(velX);
    }
    else if (velXScale > _MaxVelocity) {
        velX = _MaxVelocity * normalize(velX);
    }

    _BoidsData[id.x].velocity = velX;
    _BoidsData[id.x].position += velX;


}


AvoidanceBoids.cs(C#)

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

public class AvoidanceBoids : MonoBehaviour
{
    [SerializeField]
    private ComputeShader m_avoidanceBoidsSimulator;

    [SerializeField]
    private SkinnedMeshRenderer[] m_skinnedMeshRenderer;
    private Transform[] m_skinnedMeshTransform; 

    [Header("ベクトル場のマス目数")]
    [SerializeField]
    private int m_vectorFieldLength;

    [Header("ベクトル場のマスの大きさ")]
    [SerializeField]
    private float m_squareScale;

    [Header("ベクトル場の中心座標")]
    [SerializeField]
    private Vector3 m_vectorFieldCenter;

    private Mesh m_bakeMesh;
    private List<Vector3> m_vertices = new List<Vector3>();
    private List<Vector3> m_normals = new List<Vector3>();

    private Vector4[] m_meshPositions;
    private Vector4[] m_meshEulerAngles;
    private Vector4[] m_meshScales;
    private int[] m_vertexCountThresholds;

    #region ComputeShaderKernelAndGroupSize
    private int m_initializeVectorFieldKernel;
    private Vector3Int m_initializeVectorFieldGroupSize;

    private int m_updateVectorFieldKernel;
    private Vector3Int m_updateVectorFieldGroupSize;
    #endregion

    #region ComputeBuffers
    private ComputeBuffer m_vectorFieldBuffer;
    private ComputeBuffer m_positionBuffer;
    private ComputeBuffer m_normalBuffer;
    #endregion

    #region Shader_PropertyID
    private readonly int m_positionPropID = Shader.PropertyToID("_PositionBuffer");
    private readonly int m_normalPropID = Shader.PropertyToID("_NormalBuffer");
    private readonly int m_modelPositionPropID = Shader.PropertyToID("_ModelPosition");
    private readonly int m_modelEulerAnglePropID = Shader.PropertyToID("_ModelEulerAngle");
    private readonly int m_modelScalePropID = Shader.PropertyToID("_ModelScale");
    #endregion

    #region Properties
    public ComputeBuffer VectorField { get { return m_vectorFieldBuffer; } }
    public float SquareScale { get { return m_squareScale; } }
    public int SquareCount { get { return m_vectorFieldLength*m_vectorFieldLength*m_vectorFieldLength; } }
    
    public ComputeBuffer BoidsDataBuffer { get { return m_boidsDataBuffer; } }
    public int BoidsInstanceCount { get { return m_instanceCount; } }
    #endregion

    // ベクトル場のマス情報
    private struct VectorFieldSquare {
        public Vector3 position;
        public Vector3 direction;
    }

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

    [Header("力の強さ")]
    [Header("Boidsモデルのデータ")]
    [SerializeField]
    private float m_cohesionForce;
    [SerializeField]
    private float m_separationForce;
    [SerializeField]
    private float m_alignmentForce;

    [Space(5)]
    [Header("力の働く距離")]
    [SerializeField]
    private float m_cohesionDistance;
    [SerializeField]
    private float m_separationDistance;
    [SerializeField]
    private float m_alignmentDistance;

    [Space(5)]
    [Header("力の働く角度")]
    [SerializeField]
    private float m_cohesionAngle;
    [SerializeField]
    private float m_separationAngle;
    [SerializeField]
    private float m_alignmentAngle;

    [Space(5)]
    [SerializeField]
    private float m_boundaryForce;
    [SerializeField]
    private float m_boundaryRange;
    [SerializeField]
    private Vector3 m_boundaryCenter;

    [SerializeField]
    private float m_avoidForce;

    [Space(5)]
    [SerializeField]
    private float m_maxVelocity;

    [SerializeField]
    private float m_minVelocity;

    [SerializeField]    
    private float m_maxForce;

    private struct BoidsData {
        public Vector3 position;
        public Vector3 velocity;
    }

    private ComputeBuffer m_boidsDataBuffer;

    private int m_updateBoidsKernel;

    private int m_deltaTimeID = Shader.PropertyToID("_DeltaTime");

    private Vector3Int m_updateBoidsGroupSize;

    [Header("レンダラー")]
    [Space(20)]
    [SerializeField]
    private BoxRenderer m_boxRenderer;
    [SerializeField]
    private BoidsRenderer m_boidsRenderer;

    // Start is called before the first frame update
    void Start()
    {
        
        m_meshPositions = new Vector4[m_skinnedMeshRenderer.Length];
        m_meshEulerAngles = new Vector4[m_skinnedMeshRenderer.Length];
        m_meshScales = new Vector4[m_skinnedMeshRenderer.Length];
        m_vertexCountThresholds = new int[m_skinnedMeshRenderer.Length];

        InitializeAvoidanceBoids();
        InitializeBoidsSimulator();

        // GC.Allocを防ぐため
        m_bakeMesh = new Mesh();

        m_skinnedMeshTransform = new Transform[m_skinnedMeshRenderer.Length];
        for(int i = 0; i < m_skinnedMeshRenderer.Length; ++i)
            m_skinnedMeshTransform[i] = m_skinnedMeshRenderer[i].transform;
        // m_skinnedMeshTransform = m_skinnedMeshRenderer.transform;

        // AvoidanceBoidsの初期化が完了したらRendererを初期化する
        m_boxRenderer?.Initialize(this);
        m_boidsRenderer?.Initialize(this);

    }

    // Update is called once per frame
    void Update()
    {
        UpdateVectorField();
        UpdateBoids();
    }

    private void InitializeAvoidanceBoids() {

        // スレッドサイズ取得に使用
        uint x, y, z;

        // すべてのSkinnedMeshRendererの頂点数を求める
        // また頂点数の累積和を求めてComputeShaderの計算で使用する
        int vertexCount = 0;
        for(int i = 0; i < m_skinnedMeshRenderer.Length; ++i) {
            vertexCount += m_skinnedMeshRenderer[i].sharedMesh.vertexCount;
            m_vertexCountThresholds[i] = vertexCount;
        }
        // int vertexCount = m_skinnedMeshRenderer.sharedMesh.vertexCount;

        InitializeVectorFieldBuffer();
        InitializePositionNormalBuffer(vertexCount);

        m_initializeVectorFieldKernel = m_avoidanceBoidsSimulator.FindKernel("InitializeVectorField");
        m_avoidanceBoidsSimulator.GetKernelThreadGroupSizes(m_initializeVectorFieldKernel, out x, out y, out z);
        int squaresCount = m_vectorFieldLength*m_vectorFieldLength*m_vectorFieldLength;
        m_initializeVectorFieldGroupSize = new Vector3Int(squaresCount/(int)x, (int)y, (int)z);
        m_avoidanceBoidsSimulator.SetBuffer(m_initializeVectorFieldKernel, "_VectorFieldBuffer", m_vectorFieldBuffer);

        m_updateVectorFieldKernel = m_avoidanceBoidsSimulator.FindKernel("UpdateVectorField");
        m_avoidanceBoidsSimulator.GetKernelThreadGroupSizes(m_updateVectorFieldKernel, out x, out y, out z);
        
        m_updateVectorFieldGroupSize = new Vector3Int(vertexCount/(int)x, (int)y, (int)z);

        m_avoidanceBoidsSimulator.SetBuffer(m_updateVectorFieldKernel, "_PositionBuffer", m_positionBuffer);
        m_avoidanceBoidsSimulator.SetBuffer(m_updateVectorFieldKernel, "_NormalBuffer", m_normalBuffer);
        m_avoidanceBoidsSimulator.SetBuffer(m_updateVectorFieldKernel, "_VectorFieldBuffer", m_vectorFieldBuffer);
        m_avoidanceBoidsSimulator.SetFloat("_SquareScale", m_squareScale);
        m_avoidanceBoidsSimulator.SetInt("_VectorFieldLength", m_vectorFieldLength);

        float halfLength = m_squareScale * m_vectorFieldLength/2.0f;
        m_avoidanceBoidsSimulator.SetVector("_MinField", m_vectorFieldCenter - new Vector3(halfLength, halfLength, halfLength));
        m_avoidanceBoidsSimulator.SetVector("_MaxField", m_vectorFieldCenter + new Vector3(halfLength, halfLength, halfLength));

    }


    private void InitializeVectorFieldBuffer() {

        int squaresCount = m_vectorFieldLength*m_vectorFieldLength*m_vectorFieldLength;

        VectorFieldSquare[] squares = new VectorFieldSquare[squaresCount];

        float startPosX = m_vectorFieldCenter.x - m_vectorFieldLength * m_squareScale / 2.0f + m_squareScale / 2.0f;
        float startPosY = m_vectorFieldCenter.y - m_vectorFieldLength * m_squareScale / 2.0f + m_squareScale / 2.0f;
        float startPosZ = m_vectorFieldCenter.z - m_vectorFieldLength * m_squareScale / 2.0f + m_squareScale / 2.0f;

        for(int i = 0; i < m_vectorFieldLength; ++i) {
            for(int j = 0; j < m_vectorFieldLength; ++j) {
                for(int k = 0; k < m_vectorFieldLength; ++k) {
                    int index = i*m_vectorFieldLength*m_vectorFieldLength + j*m_vectorFieldLength + k;
                    squares[index].position = new Vector3(startPosX + i * m_squareScale, startPosY + j * m_squareScale, startPosZ + k * m_squareScale);
                    squares[index].direction = Vector3.zero;
                }
            }
        }

        m_vectorFieldBuffer = new ComputeBuffer(squaresCount, Marshal.SizeOf(typeof(VectorFieldSquare)));
        m_vectorFieldBuffer.SetData(squares);

    }

    private void InitializePositionNormalBuffer(int vertexCount) {

        m_positionBuffer = new ComputeBuffer(vertexCount, Marshal.SizeOf(typeof(Vector3)));
        m_normalBuffer = new ComputeBuffer(vertexCount, Marshal.SizeOf(typeof(Vector3)));

    }

    private void InitializeBoidsDataBuffer() {

        var boidsData = new BoidsData[m_instanceCount];

        for(int i = 0; i < m_instanceCount; ++i) {
            boidsData[i].position = Random.insideUnitSphere * m_boundaryRange;
            var velocity = new Vector3(Random.Range(-1f, 1f), Random.Range(-1f, 1f), Random.Range(-1f, 1f));
            boidsData[i].velocity = velocity.normalized * m_minVelocity; 
        }

        m_boidsDataBuffer = new ComputeBuffer(m_instanceCount, Marshal.SizeOf(typeof(BoidsData)));
        m_boidsDataBuffer.SetData(boidsData);


    }

    private void InitializeBoidsSimulator() {

        // 生成する個体の数を2の累乗にする(計算しやすくするため)
        m_instanceCount = Mathf.ClosestPowerOfTwo(m_instanceCount);

        InitializeBoidsDataBuffer();

        m_updateBoidsKernel = m_avoidanceBoidsSimulator.FindKernel("UpdateBoids");

        m_avoidanceBoidsSimulator.GetKernelThreadGroupSizes(m_updateBoidsKernel, out uint x, out uint y, out uint z);
        m_updateBoidsGroupSize = new Vector3Int(m_instanceCount / (int)x, (int)y, (int)z);

        m_avoidanceBoidsSimulator.SetFloat("_CohesionForce", m_cohesionForce);
        m_avoidanceBoidsSimulator.SetFloat("_SeparationForce", m_separationForce);
        m_avoidanceBoidsSimulator.SetFloat("_AlignmentForce", m_alignmentForce);

        // ComputeShader内の距離判定で2乗の値を使用しているので合わせる
        m_avoidanceBoidsSimulator.SetFloat("_CohesionDistance", m_cohesionDistance);
        m_avoidanceBoidsSimulator.SetFloat("_SeparationDistance", m_separationDistance);
        m_avoidanceBoidsSimulator.SetFloat("_AlignmentDistance", m_alignmentDistance);

        // ComputeShader内ではラジアンで判定するので度数方からラジアンに変更する
        m_avoidanceBoidsSimulator.SetFloat("_CohesionAngle", m_cohesionAngle * Mathf.Deg2Rad);
        m_avoidanceBoidsSimulator.SetFloat("_SeparationAngle", m_separationAngle * Mathf.Deg2Rad);
        m_avoidanceBoidsSimulator.SetFloat("_AlignmentAngle", m_alignmentAngle * Mathf.Deg2Rad);

        m_avoidanceBoidsSimulator.SetFloat("_BoundaryForce", m_boundaryForce);
        m_avoidanceBoidsSimulator.SetFloat("_BoundaryRange", m_boundaryRange * m_boundaryRange);
        m_avoidanceBoidsSimulator.SetVector("_BoundaryCenter", m_boundaryCenter);
        m_avoidanceBoidsSimulator.SetFloat("_AvoidForce", m_avoidForce);

        m_avoidanceBoidsSimulator.SetFloat("_MinVelocity", m_minVelocity);
        m_avoidanceBoidsSimulator.SetFloat("_MaxVelocity", m_maxVelocity);

        m_avoidanceBoidsSimulator.SetInt("_InstanceCount", m_instanceCount);

        m_avoidanceBoidsSimulator.SetFloat("_MaxForce", m_maxForce);
        
        m_avoidanceBoidsSimulator.SetBuffer(m_updateBoidsKernel, "_BoidsData", m_boidsDataBuffer);
        m_avoidanceBoidsSimulator.SetBuffer(m_updateBoidsKernel, "_VectorFieldBuffer", m_vectorFieldBuffer);

        m_avoidanceBoidsSimulator.SetFloat("_Epsilon", Mathf.Epsilon);
    }

    /// <summary>
    /// ベクトル場の更新
    /// </summary>
    private void UpdateVectorField() {
        // ベクトル場の初期化
        m_avoidanceBoidsSimulator.Dispatch(m_initializeVectorFieldKernel,
                                           m_initializeVectorFieldGroupSize.x,
                                           m_initializeVectorFieldGroupSize.y,
                                           m_initializeVectorFieldGroupSize.z);
        
        int bufferIndex = 0;
        for(int i = 0; i < m_skinnedMeshRenderer.Length; ++i) {
            m_meshPositions[i] = m_skinnedMeshTransform[i].position;
            m_meshEulerAngles[i] = m_skinnedMeshTransform[i].eulerAngles;
            m_meshScales[i] = m_skinnedMeshTransform[i].localScale;
            m_skinnedMeshRenderer[i].BakeMesh(m_bakeMesh);
            m_bakeMesh.GetVertices(m_vertices);
            m_bakeMesh.GetNormals(m_normals);
            int vertexCount = m_skinnedMeshRenderer[i].sharedMesh.vertexCount;
            m_positionBuffer.SetData(m_vertices, 0, bufferIndex, vertexCount);
            m_normalBuffer.SetData(m_normals, 0, bufferIndex, vertexCount);
            bufferIndex += vertexCount;
        }

        m_avoidanceBoidsSimulator.SetVectorArray(m_modelPositionPropID, m_meshPositions);
        m_avoidanceBoidsSimulator.SetVectorArray(m_modelEulerAnglePropID, m_meshEulerAngles);
        m_avoidanceBoidsSimulator.SetVectorArray(m_modelScalePropID, m_meshScales);

        // m_avoidanceBoidsSimulator.SetVector(m_modelPositionPropID, m_skinnedMeshTransform.position);
        // m_avoidanceBoidsSimulator.SetVector(m_modelEulerAnglePropID, m_skinnedMeshTransform.eulerAngles);
        // m_avoidanceBoidsSimulator.SetVector(m_modelScalePropID, m_skinnedMeshTransform.localScale);
        m_avoidanceBoidsSimulator.Dispatch(m_updateVectorFieldKernel,
                                           m_updateVectorFieldGroupSize.x,
                                           m_updateVectorFieldGroupSize.y,
                                           m_updateVectorFieldGroupSize.z);

    }

    /// <summary>
    /// Boidsアルゴリズムの更新
    /// </summary>
    private void UpdateBoids() {

        m_avoidanceBoidsSimulator.SetFloat(m_deltaTimeID, Time.deltaTime);
        m_avoidanceBoidsSimulator.Dispatch(m_updateBoidsKernel,
                                           m_updateBoidsGroupSize.x,
                                           m_updateBoidsGroupSize.y,
                                           m_updateBoidsGroupSize.z);

    }

    void OnDestroy() {

        m_vectorFieldBuffer?.Release();
        m_positionBuffer?.Release();
        m_normalBuffer?.Release();
        m_boidsDataBuffer?.Release();

    }

}


BoidsRenderer.cs(C#)

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

public class BoidsRenderer : MonoBehaviour
{
    [Header("DrawMeshInstancedIndirectのパラメータ")]
    [SerializeField]
    private Mesh m_mesh;

    [SerializeField]
    private Material m_material;

    [SerializeField]
    private Bounds m_bounds;

    [SerializeField]
    private ShadowCastingMode m_shadowCastingMode;

    [SerializeField]
    private bool m_receiveShadows;

    private AvoidanceBoids m_avoidanceBoids;

    private ComputeBuffer m_argsBuffer;


    public void Initialize(AvoidanceBoids avoidanceBoids) {
        m_avoidanceBoids = avoidanceBoids;
        InitializeArgsBuffer();

        m_material.SetBuffer("boidsDataBuffer", m_avoidanceBoids.BoidsDataBuffer);

    }

    void LateUpdate() {

        Graphics.DrawMeshInstancedIndirect(
            m_mesh,
            0,
            m_material,
            m_bounds,
            m_argsBuffer,
            0,
            null,
            m_shadowCastingMode,
            m_receiveShadows
        );
    }


    private void InitializeArgsBuffer() {

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

        args[0] = m_mesh.GetIndexCount(0);
        args[1] = (uint)m_avoidanceBoids.BoidsInstanceCount;

        m_argsBuffer = new ComputeBuffer(1, 4 * args.Length, ComputeBufferType.IndirectArguments);

        m_argsBuffer.SetData(args);

    }


    private void OnDestroy() {

        m_argsBuffer?.Release();

    }

}


BoxRenderer.cs(C#)

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

public class BoxRenderer : MonoBehaviour
{
    [Header("DrawMeshInstancedInDirectの項目")]
    [Space(20)]
    [SerializeField]
    private Mesh _mesh;

    [SerializeField]
    private Material _material;

    [SerializeField]
    private Bounds _bounds;

    private ComputeBuffer _argsBuffer;

    private AvoidanceBoids m_avoidanceBoids;


    public void Initialize(AvoidanceBoids avoidanceBoids) {
        m_avoidanceBoids = avoidanceBoids;
        InitializeArgsBuffer();
        _material.SetFloat("_Scale", m_avoidanceBoids.SquareScale);
        _material.SetBuffer("_BoxDataBuffer", m_avoidanceBoids.VectorField);

    }

    void LateUpdate() {

            Graphics.DrawMeshInstancedIndirect(
            _mesh,
            0,
            _material,
            _bounds,
            _argsBuffer,
            0,
            null,
            ShadowCastingMode.Off,
            false
        );
    }

    private void InitializeArgsBuffer() {

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

        args[0] = _mesh.GetIndexCount(0);
        int length = m_avoidanceBoids.SquareCount;
        args[1] = (uint)(length);

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

    void OnDestroy() {

        _argsBuffer?.Release();

    }

}


Shader

Shader "Custom/BoidsShader"
{
    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

        _ScaleX("ScaleX", float) = 1
        _ScaleY("ScaleY", float) = 1
        _ScaleZ("ScaleZ", float) = 1
    }
    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関数を呼び出す


        // Use shader model 3.0 target, to get nicer looking lighting
        #pragma target 3.0

        sampler2D _MainTex;

        struct Input
        {
            float2 uv_MainTex;
        };

        struct BoidsData {
            float3 position;
            float3 velocity;
        };

        #ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED
        StructuredBuffer<BoidsData> boidsDataBuffer;
        #endif

        float4x4 eulerAnglesToRotationMatrix(float3 angles) {

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

            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);

        }

        float4x4 CalcInverseMatrix(float3 position, float3 angle, float3 scale) {

            float4x4 inversScaleeMatrix = float4x4(
                1/scale.x, 0, 0, -position.x,
                0, 1/scale.y, 0, -position.y,
                0, 0, 1/scale.z, -position.z,
                0, 0, 0, 1);

            float4x4 mat = float4x4(
                1, 0, 0, 0,
                0, 1, 0, 0,
                0, 0, 1, 0,
                0, 0, 0, 1);

            float4x4 rotMatrix = mul(eulerAnglesToRotationMatrix(angle), mat);

            float4x4 inverseRotMatrix = float4x4(
                rotMatrix._11, rotMatrix._21, rotMatrix._31, 0,
                rotMatrix._12, rotMatrix._22, rotMatrix._32, 0,
                rotMatrix._13, rotMatrix._23, rotMatrix._33, 0,
                0, 0, 0, 1);

            return mul(inversScaleeMatrix, inverseRotMatrix);

        }

        fixed _ScaleX;
        fixed _ScaleY;
        fixed _ScaleZ;

        void setup() {

        #ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED
            float3 position = boidsDataBuffer[unity_InstanceID].position;
            float3 velocity = boidsDataBuffer[unity_InstanceID].velocity;

            // スケーリング
            unity_ObjectToWorld._11_21_31_41 = float4(_ScaleX, 0, 0, 0);
            unity_ObjectToWorld._12_22_32_42 = float4(0, _ScaleY, 0, 0);
            unity_ObjectToWorld._13_23_33_43 = float4(0, 0, _ScaleZ, 0);

            // 速度から回転を求める
            float3 angle = float3(
                -asin(velocity.y/(length(velocity.xyz) + 1e-8)), // 0除算防止
                atan2(velocity.x, velocity.z),
                0);

            // 回転
            unity_ObjectToWorld = mul(eulerAnglesToRotationMatrix(angle), unity_ObjectToWorld);

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

            // モデル行列を求める(間違っているかも. . .)
            // 参考:https://qiita.com/yuji_yasuhara/items/8d63455d1d277af4c270
            // 参考:http://gamemakerlearning.blog.fc2.com/blog-entry-196.html
            unity_WorldToObject = CalcInverseMatrix(position, angle, float3(_ScaleX, _ScaleY, _ScaleZ));


        #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 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
            o.Albedo = c.rgb;
            // Metallic and smoothness come from slider variables
            o.Metallic = _Metallic;
            o.Smoothness = _Glossiness;
            o.Alpha = c.a;
        }
        ENDCG
    }
    FallBack "Diffuse"
}



解説

複数のSkinnedMeshRendererに対応させる

使用するモデルによっては複数のSkinnedMeshRendererを使用しているので、そのようなモデルでも問題なく動作するようにしました。

ComputeBuffer.SetDataでは値を設定し始めるindexを指定することが出来るので、複数のSkinnedMeshRendererの頂点座標を1つのComputeBufferにいれて計算します。 法線ベクトルも同様に1つのComputeBufferに入れています。

        int bufferIndex = 0;
        for(int i = 0; i < m_skinnedMeshRenderer.Length; ++i) {
   . . . . . . .
   . . . . . . . 
            int vertexCount = m_skinnedMeshRenderer[i].sharedMesh.vertexCount;
            m_positionBuffer.SetData(m_vertices, 0, bufferIndex, vertexCount);
            m_normalBuffer.SetData(m_normals, 0, bufferIndex, vertexCount);
            bufferIndex += vertexCount;
        }


ただし、頂点座標や法線ベクトルを計算する際は各SkinnedMeshRendererのTransform情報が必要なのでそれらもComputeShaderに設定します。計算の際は頂点数の累積和を使用してどのTransform情報を使用するか判断しています。

    int transformIndex = 0;

    // 累積和とidからどのTransform情報を使用するか判断する
    for(int i = 0; (int)id < _VertexCountsThresholds[i]; ++i)
        transformIndex = i;



Boidsアルゴリズムにベクトル場の力を加える

Boidsの結合・分離・整列の計算をした後にベクトル場の力も加えます。ただし、個体がベクトル場の範囲内にいるかを確認してから計算します。

恐らく、ここら辺については計算方法を変えても良いと思います。

    float3 avoid = float3(0, 0, 0);
    // ベクトル場内にいる際は、ベクトルの値を取得してその方向に逃げるようにする
    if(_MinField.x <= posX.x && posX.x < _MaxField.x &&
       _MinField.y <= posX.y && posX.y < _MaxField.y &&
       _MinField.z <= posX.z && posX.z < _MaxField.z) {
        int3 index = (posX - _MinField) / _SquareScale;
        avoid = _VectorFieldBuffer[(index.x * _VectorFieldLength * _VectorFieldLength +
                        index.y * _VectorFieldLength +
                        index.z)].direction * _AvoidForce;
                        
       }

. . . . . . . .
. . . . . . . .
    velX += (cohesion + separation + alignment + boundary + avoid) * _DeltaTime;



レンダラーを分ける

DrawMeshInstancedIndirectで表示する機能も一つのクラスに入れると管理が難しくなると感じたので、Boidsを表示するクラスとベクトル場を表示するクラスを分けました。

注意すべき点として、ComputeBufferが初期化される前にMaterial.SetBufferをしてもマテリアルに設定されないのでBoidsアルゴリズムの初期化が終わった後にレンダラーの初期化を行います。

    void Start()
    {
   . . . . . .
   . . . . . . 
   . . . . . .
        // AvoidanceBoidsの初期化が完了したらRendererを初期化する
        m_boxRenderer?.Initialize(this);
        m_boidsRenderer?.Initialize(this);

    }



結果

VRIKとMapleちゃんを使ったデモです。


ベクトル場を使用した衝突回避Boidsアルゴリズム



まとめ

ある程度ちゃんと動きましたが、もうちょっといい動きが作れるんじゃないかなと感じました。

これからも改善の余地が見つかれば改良していこうと思います。


追記

別の衝突回避について考察しました。

shitakami.hatenablog.com



使用したもの

booth.pm

www.cgtrader.com