// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using UnityEngine;
namespace Microsoft.MixedReality.GraphicsTools.Samples.MeshInstancing
{
///
/// Simulates a bunch of swimming fish within a containment radius.
///
public class InstancingContainment : MonoBehaviour
{
[SerializeField]
private MeshInstancer instancer = null;
[Header("Simulation Properties")]
[SerializeField, Min(0)]
private float containmentRadius = 3.0f;
[Header("Instance Properties")]
[SerializeField, Min(1)]
private int instanceCount = 20000;
[SerializeField, Min(0.01f)]
private float instanceSizeMin = 0.02f;
[SerializeField, Min(0.01f)]
private float instanceSizeMax = 0.08f;
private class UserData
{
public Vector3 velocity;
public Vector3 targetVelocity;
public Quaternion targetRotation;
public float changeTargetTime;
}
///
/// Re-spawn instances when a property changes.
///
private void OnValidate()
{
if (instancer != null && instancer.InstanceCount != 0)
{
CreateInstances();
}
}
///
/// Create instances on enable.
///
private void OnEnable()
{
CreateInstances();
}
///
/// Render the bounds of the instances.
///
private void OnDrawGizmosSelected()
{
Gizmos.color = Color.yellow;
Gizmos.matrix = transform.localToWorldMatrix;
Gizmos.DrawWireSphere(Vector3.zero, containmentRadius);
}
///
/// Create a bunch of swimming fish.
///
private void CreateInstances()
{
// Clear any existing instances.
instancer = (instancer == null) ? GetComponent() : instancer;
instancer.Clear();
float minMass = instanceSizeMin * instanceSizeMin * instanceSizeMin;
float maxMass = instanceSizeMax * instanceSizeMax * instanceSizeMax;
int colorID = Shader.PropertyToID("_Color");
int swimSpeed = Shader.PropertyToID("_SwimSpeed");
for (int i = 0; i < instanceCount; ++i)
{
// Create some random initial properties.
Vector3 scale = Vector3.one * Random.Range(instanceSizeMin, instanceSizeMax);
float normalizedMass = ((scale.x * scale.y * scale.z) - minMass) / maxMass;
Vector3 velocity = Random.onUnitSphere * ((1.2f - normalizedMass) * 0.2f);
Quaternion rotation = Quaternion.LookRotation(velocity);
// Create an instance object at a random position within the containment radius.
var instance = instancer.Instantiate(Random.onUnitSphere * containmentRadius,
rotation,
scale);
instance.SetVector(colorID, Random.ColorHSV(normalizedMass, normalizedMass, 1.0f, 1.0f, 1.0f, 1.0f));
instance.SetFloat(swimSpeed, Mathf.Lerp(400.0f, 20.0f, normalizedMass));
// Set user data to use during update.
instance.UserData = new UserData()
{
velocity = Vector3.zero,
targetVelocity = velocity,
targetRotation = rotation,
changeTargetTime = Random.Range(4.0f, 20.0f)
};
instance.SetParallelUpdate(ParallelUpdate);
}
}
///
/// Method called potentially concurrently across many threads. Make sure all function calls are thread safe.
///
private void ParallelUpdate(float deltaTime, MeshInstancer.Instance instance)
{
// Cast the user data to our data type.
UserData data = (UserData)instance.UserData;
// Euler integration.
data.velocity = Vector3.Lerp(data.velocity, data.targetVelocity, deltaTime);
instance.LocalPosition += data.velocity * deltaTime;
instance.LocalRotation = Quaternion.Slerp(instance.LocalRotation, data.targetRotation, deltaTime * data.targetVelocity.sqrMagnitude * 40.0f);
data.changeTargetTime -= deltaTime;
// Time to pick a new target.
if (data.changeTargetTime <= 0.0f)
{
Vector3 randomTarget = ThreadSafeRandom.onUnitSphere * containmentRadius;
Vector3 direction = (randomTarget - instance.LocalPosition).normalized;
data.targetVelocity = direction * data.targetVelocity.magnitude;
data.targetRotation = Quaternion.LookRotation(data.targetVelocity);
data.changeTargetTime = ThreadSafeRandom.Range(4.0f, 20.0f);
}
// Update the user data state.
instance.UserData = data;
}
}
}