compute shader with skinned mesh flocking instances
This is v. similar to the previous flocking post, only now we use a skinned & animated mesh & it's attached Animator component. Something to note, one instance of the prefab has to be in the scene, or it doesn't work (move it off camera or something).
Also - the material that uses the surface shader , must have the GPU instancing enabled! That's what those if-checks are for!
The gist of this is - we need to store the vertex animation into another buffer. The compute shader doesn't deal with this data at all - it is only concerned with the position/direction of each instance. It is the surface/vert-frag shader that will deal with it. The only bit the compute shader does increment is the "frame" of animation, which is determined by the speed at which the boid is moving.
C# SCRIPT
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SkinnedFlocking : MonoBehaviour {
public struct Boid
{
public Vector3 position;
public Vector3 direction;
public float noise_offset;
public float speed;
public float frame;
public Vector3 padding;
public Boid(Vector3 pos, Vector3 dir, float offset)
{
position.x = pos.x;
position.y = pos.y;
position.z = pos.z;
direction.x = dir.x;
direction.y = dir.y;
direction.z = dir.z;
noise_offset = offset;
speed = frame = 0;
padding.x = 0; padding.y = padding.z = 0;
}
}
public ComputeShader shader;
private SkinnedMeshRenderer boidSMR;
public GameObject boidObject;
private Animator animator;
public AnimationClip animationClip;
private int numOfFrames;
public int boidsCount;
public float spawnRadius;
public Transform target;
public float rotationSpeed = 1f;
public float boidSpeed = 1f;
public float neighbourDistance = 1f;
public float boidSpeedVariation = 1f;
public float boidFrameSpeed = 10f;
public bool frameInterpolation = true;
Mesh boidMesh;
private int kernelHandle;
private ComputeBuffer boidsBuffer;
private ComputeBuffer vertexAnimationBuffer;//buffer to store vertices
public Material boidMaterial;
ComputeBuffer argsBuffer;
MaterialPropertyBlock props;
uint[] args = new uint[5] { 0, 0, 0, 0, 0 };
Boid[] boidsArray;
int groupSizeX;
int numOfBoids;
Bounds bounds;
void Start()
{
kernelHandle = shader.FindKernel("CSMain");
uint x;
shader.GetKernelThreadGroupSizes(kernelHandle, out x, out _, out _);
groupSizeX = Mathf.CeilToInt((float)boidsCount / (float)x);
numOfBoids = groupSizeX * (int)x;
bounds = new Bounds(Vector3.zero, Vector3.one * 1000);
// This property block is used only for avoiding an instancing bug.
props = new MaterialPropertyBlock();
props.SetFloat("_UniqueID", Random.value);
InitBoids();
GenerateSkinnedAnimationForGPUBuffer();
InitShader();
}
void InitBoids()
{
boidsArray = new Boid[numOfBoids];
for (int i = 0; i < numOfBoids; i++)
{
Vector3 pos = transform.position + Random.insideUnitSphere * spawnRadius;
Quaternion rot = Quaternion.Slerp(transform.rotation, Random.rotation, 0.3f);
float offset = Random.value * 1000.0f;
boidsArray[i] = new Boid(pos, rot.eulerAngles, offset);
}
}
void InitShader()
{
// Initialize the indirect draw args buffer.
argsBuffer = new ComputeBuffer(
1, 5 * sizeof(uint), ComputeBufferType.IndirectArguments
);
if (boidMesh)//Set by the GenerateSkinnedAnimationForGPUBuffer
{
args[0] = boidMesh.GetIndexCount(0);
args[1] = (uint)numOfBoids;
argsBuffer.SetData(args);
}
boidsBuffer = new ComputeBuffer(numOfBoids, 12 * sizeof(float));
boidsBuffer.SetData(boidsArray);
shader.SetFloat("rotationSpeed", rotationSpeed);
shader.SetFloat("boidSpeed", boidSpeed);
shader.SetFloat("boidSpeedVariation", boidSpeedVariation);
shader.SetVector("flockPosition", target.transform.position);
shader.SetFloat("neighbourDistance", neighbourDistance);
shader.SetFloat("boidFrameSpeed", boidFrameSpeed);
shader.SetInt("boidsCount", numOfBoids);
shader.SetInt("numOfFrames", numOfFrames);
shader.SetBuffer(kernelHandle, "boidsBuffer", boidsBuffer);
boidMaterial.SetBuffer("boidsBuffer", boidsBuffer);
boidMaterial.SetInt("numOfFrames", numOfFrames);
if (frameInterpolation && !boidMaterial.IsKeywordEnabled("FRAME_INTERPOLATION"))
boidMaterial.EnableKeyword("FRAME_INTERPOLATION");
if (!frameInterpolation && boidMaterial.IsKeywordEnabled("FRAME_INTERPOLATION"))
boidMaterial.DisableKeyword("FRAME_INTERPOLATION");
}
void Update()
{
shader.SetFloat("time", Time.time);
shader.SetFloat("deltaTime", Time.deltaTime);
shader.Dispatch(kernelHandle, groupSizeX, 1, 1);
Graphics.DrawMeshInstancedIndirect( boidMesh, 0, boidMaterial, bounds, argsBuffer, 0, props);
}
void OnDestroy()
{
if (boidsBuffer != null) boidsBuffer.Release();
if (argsBuffer != null) argsBuffer.Release();
if (vertexAnimationBuffer != null) vertexAnimationBuffer.Release();
}
private void GenerateSkinnedAnimationForGPUBuffer()
{
boidSMR = boidObject.GetComponentInChildren<SkinnedMeshRenderer>();
boidMesh = boidSMR.sharedMesh;
animator = boidObject.GetComponentInChildren<Animator>();
int iLayer = 0;
AnimatorStateInfo aniStateInfo = animator.GetCurrentAnimatorStateInfo(iLayer);
Mesh bakedMesh = new Mesh();
float sampleTime = 0;
float perFrameTime = 0;
numOfFrames = Mathf.ClosestPowerOfTwo((int)(animationClip.frameRate * animationClip.length));
perFrameTime = animationClip.length / numOfFrames;
var vertexCount = boidSMR.sharedMesh.vertexCount;
vertexAnimationBuffer = new ComputeBuffer(vertexCount * numOfFrames, 16);
Vector4[] vertexAnimationData = new Vector4[vertexCount * numOfFrames];
for (int i = 0; i < numOfFrames; i++)
{
animator.Play(aniStateInfo.shortNameHash, iLayer, sampleTime);
animator.Update(0f);
boidSMR.BakeMesh(bakedMesh);
for(int j = 0; j < vertexCount; j++)
{
Vector4 vertex = bakedMesh.vertices[j];
vertex.w = 1;
vertexAnimationData[(j * numOfFrames) + i] = vertex;
}
sampleTime += perFrameTime;
}
vertexAnimationBuffer.SetData(vertexAnimationData);
boidMaterial.SetBuffer("vertexAnimation", vertexAnimationBuffer);
boidObject.SetActive(false);
}
}
COMPUTE SHADER
#pragma kernel CSMain
#define GROUP_SIZE 256
float hash( float n )
{
return frac(sin(n)*43758.5453);
}
// The noise function returns a value in the range -1.0f -> 1.0f
float noise1( float3 x )
{
float3 p = floor(x);
float3 f = frac(x);
f = f*f*(3.0-2.0*f);
float n = p.x + p.y*57.0 + 113.0*p.z;
return lerp(lerp(lerp( hash(n+0.0), hash(n+1.0),f.x),
lerp( hash(n+57.0), hash(n+58.0),f.x),f.y),
lerp(lerp( hash(n+113.0), hash(n+114.0),f.x),
lerp( hash(n+170.0), hash(n+171.0),f.x),f.y),f.z);
}
struct Boid
{
float3 position;
float3 direction;
float noise_offset;
float speed;
float frame;
float3 padding;
};
RWStructuredBuffer<Boid> boidsBuffer;
float time;
float deltaTime;
float rotationSpeed;
float boidSpeed;
float boidSpeedVariation;
float3 flockPosition;
float neighbourDistance;
uint boidsCount;
float boidFrameSpeed;
int numOfFrames;
[numthreads(GROUP_SIZE,1,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{
uint instanceId = id.x;
Boid boid = boidsBuffer[instanceId];
float noise = clamp(noise1(time / 100.0 + boid.noise_offset), -1, 1) * 2.0 - 1.0;
float velocity = boidSpeed * (1.0 + noise * boidSpeedVariation);
float3 boid_pos = boid.position;
float3 boid_dir = boid.direction;
float3 separation = float3(0, 0.0, 0);
float3 alignment = float3(0.0, 0.0, 0.0);
float3 cohesion = flockPosition;
uint nearbyCount = 1; // Add self that is ignored in loop
for (uint i = 0; i < boidsCount; i++) {
if (i == instanceId)
continue;
float3 tempBoid_position = boidsBuffer[i].position;
float3 offset = boid.position - tempBoid_position;
float dist = max(length(offset), 0.000001);
if (dist < neighbourDistance)
{
separation += offset * (1.0/dist - 1.0/neighbourDistance);
alignment += boidsBuffer[i].direction;
cohesion += tempBoid_position;
nearbyCount += 1;
}
}
float avg = 1.0 / nearbyCount;
alignment *= avg;
cohesion *= avg;
cohesion = normalize(cohesion - boid_pos);
float3 direction = alignment + separation + cohesion;
float ip = exp(-rotationSpeed * deltaTime);
boid.direction = lerp(direction, normalize(boid_dir), ip);
boid.position += boid.direction * velocity * deltaTime;
boid.frame = boid.frame + velocity * deltaTime * boidFrameSpeed;
if (boid.frame >= numOfFrames) boid.frame -= numOfFrames;
boidsBuffer[id.x] = boid;
}
SURFACE SHADER
Shader "Flocking/Skinned" { // StructuredBuffer + SurfaceShader
Properties {
_Color ("Color", Color) = (1,1,1,1)
_MainTex ("Albedo (RGB)", 2D) = "white" {}
_BumpMap ("Bumpmap", 2D) = "bump" {}
_MetallicGlossMap("Metallic", 2D) = "white" {}
_Metallic ("Metallic", Range(0,1)) = 0.0
_Glossiness ("Smoothness", Range(0,1)) = 1.0
}
SubShader {
CGPROGRAM
#include "UnityCG.cginc"
sampler2D _MainTex;
sampler2D _BumpMap;
sampler2D _MetallicGlossMap;
struct appdata_custom {
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
float4 tangent : TANGENT;
uint id : SV_VertexID;
uint inst : SV_InstanceID;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct Input {
float2 uv_MainTex;
float2 uv_BumpMap;
float3 worldPos;
};
half _Glossiness;
half _Metallic;
fixed4 _Color;
#pragma multi_compile __ FRAME_INTERPOLATION
#pragma surface surf Standard vertex:vert addshadow nolightmap
#pragma instancing_options procedural:setup
float4x4 _Matrix;
int _CurrentFrame;
int _NextFrame;
float _FrameInterpolation;
int numOfFrames;
#ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED
struct Boid
{
float3 position;
float3 direction;
float noise_offset;
float speed;
float frame;
float3 padding;
};
StructuredBuffer<Boid> boidsBuffer;
StructuredBuffer<float4> vertexAnimation;
#endif
float4x4 create_matrix(float3 pos, float3 dir, float3 up) {
float3 zaxis = normalize(dir);
float3 xaxis = normalize(cross(up, zaxis));
float3 yaxis = cross(zaxis, xaxis);
return float4x4(
xaxis.x, yaxis.x, zaxis.x, pos.x,
xaxis.y, yaxis.y, zaxis.y, pos.y,
xaxis.z, yaxis.z, zaxis.z, pos.z,
0, 0, 0, 1
);
}
void vert(inout appdata_custom v)
{
#ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED
#ifdef FRAME_INTERPOLATION
v.vertex = lerp(vertexAnimation[v.id * numOfFrames + _CurrentFrame], vertexAnimation[v.id * numOfFrames + _NextFrame], _FrameInterpolation);
#else
v.vertex = vertexAnimation[v.id * numOfFrames + _CurrentFrame];
#endif
v.vertex = mul(_Matrix, v.vertex);
#endif
}
void setup()
{
#ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED
_Matrix = create_matrix(boidsBuffer[unity_InstanceID].position, boidsBuffer[unity_InstanceID].direction, float3(0.0, 1.0, 0.0));
_CurrentFrame = boidsBuffer[unity_InstanceID].frame;
#ifdef FRAME_INTERPOLATION
_NextFrame = _CurrentFrame + 1;
if (_NextFrame >= numOfFrames) _NextFrame = 0;
_FrameInterpolation = frac(boidsBuffer[unity_InstanceID].frame);
#endif
#endif
}
void surf (Input IN, inout SurfaceOutputStandard o) {
fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
fixed4 m = tex2D (_MetallicGlossMap, IN.uv_MainTex);
o.Albedo = c.rgb;
o.Alpha = c.a;
o.Normal = UnpackNormal (tex2D (_BumpMap, IN.uv_BumpMap));
o.Metallic = m.r;
o.Smoothness = _Glossiness * m.a;
}
ENDCG
}
}
Comments
Post a Comment