using System; using System.Buffers; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Unity.Mathematics; using UnityEditor; using UnityEngine; using UnityEngine.Rendering; using Random = Unity.Mathematics.Random; namespace Misaki.ArtTool { [ExecuteInEditMode] public class Cloner : MonoBehaviour { public List inputObjects = new(); public DistributionMode distributionMode = DistributionMode.Grid; public bool isRandomDistribution = false; public uint seed = 123456; public bool autoGenerate; public bool isRenderInstancing; public SplineDistributionSetting splineDistributionSetting = new(); public LinearDistributionSetting linearDistributionSetting = new(); public GridDistributionSetting gridDistributionSetting = new(); public List effectors; public DebugMode debugMode; private static readonly ArrayPool _pointPool = ArrayPool.Shared; private int _pointSize; private PointData[] _points; private List _instantiateObjects = new(); private readonly Dictionary> _objectGroup = new(); private bool _isPointsDirty = false; private const float GIZMOS_LINE_BASE_LENGTH = 0.5f; private void OnEnable() { foreach (var item in effectors) { if (item.effector == null) { return; } item.effector.propertyChanged += OnPropertyChanged; } if (transform.childCount > 0 && _instantiateObjects.Count == 0) { for (var i = 0; i < transform.childCount; i++) { _instantiateObjects.Add(transform.GetChild(i).gameObject); } } } private void OnDisable() { foreach (var item in effectors) { if (item.effector == null) { return; } item.effector.propertyChanged -= OnPropertyChanged; } } //private void OnDestroy() //{ // Clear(); //} public void OnPropertyChanged(object sender, EventArgs e) { if (sender is not EffectorBase && sender is not FieldBase) { return; } _isPointsDirty = true; } private void Update() { if (_isPointsDirty && autoGenerate) { GeneratePoints(); InstantiateGameObjectOnPoints(); } if (_objectGroup == null || _objectGroup.Count <= 0 || !isRenderInstancing) { return; } foreach (var item in _objectGroup) { if (inputObjects.Count <= item.Key || inputObjects[item.Key] == null) { continue; } var objectData = inputObjects[item.Key]; var mat = objectData.Material; var renderParams = new RenderParams(mat) { shadowCastingMode = ShadowCastingMode.On }; Graphics.RenderMeshInstanced(renderParams, objectData.Mesh, 0, item.Value, _pointSize); } } public void GeneratePoints() { Clear(); switch (distributionMode) { case DistributionMode.Object: break; case DistributionMode.Spline: _pointSize = splineDistributionSetting.DistributionCount; break; case DistributionMode.Linear: _pointSize = (int)linearDistributionSetting.count; break; case DistributionMode.Grid: _pointSize = gridDistributionSetting.DistributionCount; break; case DistributionMode.Radial: break; case DistributionMode.Honeycomb: break; default: break; } if (_pointSize == 0) { return; } if (_points == null || _pointSize > _points.Length) { _points = _pointPool.Rent(_pointSize); } foreach (var effectorData in effectors) { effectorData.effector.Initialize(); } var splineLength = 0.0f; var splineMatrix = float4x4.identity; if (distributionMode == DistributionMode.Spline && splineDistributionSetting.spline != null) { splineLength = splineDistributionSetting.spline.CalculateLength(); splineMatrix = splineDistributionSetting.spline.transform.localToWorldMatrix; } var worldMatrix = transform.localToWorldMatrix; Parallel.For(0, _pointSize, i => { var pointMatrix = float4x4.identity; var isValid = true; switch (distributionMode) { case DistributionMode.Object: break; case DistributionMode.Spline: Distribution.SplineDistribution(i, _pointSize, splineLength, splineMatrix, splineDistributionSetting, out pointMatrix, out isValid); break; case DistributionMode.Linear: Distribution.LinearDistribution(i, linearDistributionSetting, out pointMatrix, out isValid); break; case DistributionMode.Grid: Distribution.GridDistribution(i, gridDistributionSetting, out pointMatrix, out isValid); break; case DistributionMode.Radial: break; case DistributionMode.Honeycomb: break; default: break; } pointMatrix = math.mul(worldMatrix, pointMatrix); _points[i].matrix = pointMatrix; _points[i].isValid = isValid; }); Parallel.For(0, _pointSize, i => { for (var e = 0; e < effectors.Count; e++) { if (!effectors[e].enable) { continue; } if (effectors[e].effector == null) { continue; } effectors[e].effector.Operate(i, worldMatrix, _points, ref _points[i].matrix, ref _points[i].isValid); } if (math.all(_points[i].matrix.GetScale() == float3.zero)) { _points[i].isValid = false; } }); _isPointsDirty = false; } public void InstantiateGameObjectOnPoints() { if (_points == null && _pointSize <= 0) { return; } if (_instantiateObjects.Count > 0) { for (var i = 0; i < _instantiateObjects.Count; i++) { if (_instantiateObjects[i] == null) { continue; } #if UNITY_EDITOR DestroyImmediate(_instantiateObjects[i]); #else Destroy(child.gameObject); #endif } } if (isRandomDistribution) { // Use Fisher-Yates Shuffle algorithm to swap value var random = new Random(seed); for (var i = _pointSize - 1; i > 0; i--) { var k = random.NextInt(0, i + 1); (_points[i], _points[k]) = (_points[k], _points[i]); } } foreach (var item in _objectGroup) { item.Value.Clear(); } var currentIndex = 0; var objectIndex = 0; while (currentIndex < _pointSize) { if (!_points[currentIndex].isValid) { currentIndex++; continue; } objectIndex %= inputObjects.Count; if (inputObjects[objectIndex].gameObject == null) { currentIndex++; continue; } if (!_objectGroup.ContainsKey(objectIndex)) { _objectGroup[objectIndex] = new(); } for (var f = 0; f < inputObjects[objectIndex].frequency; f++) { _objectGroup[objectIndex].Add(_points[currentIndex].matrix); currentIndex++; if (currentIndex >= _pointSize) { break; } } objectIndex++; } for (var i = 0; i < _objectGroup.Count; i++) { var group = _objectGroup.ElementAt(i); if (group.Value.Count <= 0) { _objectGroup.Remove(group.Key); } } if (!isRenderInstancing) { foreach (var item in _objectGroup) { Span positionsSpan = stackalloc Vector3[item.Value.Count]; Span rotationsSpan = stackalloc Quaternion[item.Value.Count]; var scaleArray = new Vector3[item.Value.Count]; MatrixHelper.DecomposeMatrixListAsSpan(item.Value, positionsSpan, rotationsSpan, scaleArray); var iop = InstantiateAsync( inputObjects[item.Key].gameObject, item.Value.Count, transform, positionsSpan, rotationsSpan); iop.completed += (op) => { var instantiatedObjects = iop.GetAwaiter().GetResult(); for (var i = 0; i < instantiatedObjects.Length; i++) { var instantiatedObject = instantiatedObjects[i]; instantiatedObject.transform.localScale = scaleArray[i]; _instantiateObjects.Add(instantiatedObject); } }; } } } public void Clear(bool isClearPoints = true, bool isClearObjects = true) { if (isClearPoints) { _pointPool.Return(_points, true); } if (isClearObjects) { _objectGroup.Clear(); if (_instantiateObjects.Count <= 0) { return; } for (var i = 0; i < _instantiateObjects.Count; i++) { if (_instantiateObjects[i] == null) { continue; } #if UNITY_EDITOR DestroyImmediate(_instantiateObjects[i]); #else Destroy(child.gameObject); #endif } } } private void OnDrawGizmosSelected() { if (_pointSize <= 0 || _points.Length < _pointSize) { return; } for (var i = 0; i < _pointSize; i++) { var point = _points[i]; switch (debugMode) { case DebugMode.None: break; case DebugMode.Transform: if (!point.isValid) { continue; } var scale = point.matrix.GetScale(); Gizmos.matrix = point.matrix; // Draw the x-axis in red Gizmos.color = Color.red; Gizmos.DrawLine(Vector3.zero, Vector3.right * scale * GIZMOS_LINE_BASE_LENGTH); // Draw the y-axis in green Gizmos.color = Color.green; Gizmos.DrawLine(Vector3.zero, Vector3.up * scale * GIZMOS_LINE_BASE_LENGTH); // Draw the z-axis in blue Gizmos.color = Color.blue; Gizmos.DrawLine(Vector3.zero, Vector3.forward * scale * GIZMOS_LINE_BASE_LENGTH); break; case DebugMode.Index: if (!point.isValid) { continue; } Handles.matrix = point.matrix; Handles.Label(Vector3.zero, i.ToString()); break; case DebugMode.Validity: if (point.isValid) { Gizmos.color = Color.green; } else { Gizmos.color = Color.red; } Gizmos.DrawSphere(point.matrix.c3.xyz, GIZMOS_LINE_BASE_LENGTH / 2.0f); break; default: break; } } } } }