using System; using System.Collections.Generic; using UnityEngine; namespace TrailsFX { public enum TrailStyle { Color, TextureStamp, Clone, Outline, SpaceDistortion, Dash, Custom } public enum ColorSequence { Fixed, Cycle, PingPong, Random, FixedRandom } public enum PositionChangeRelative { World, OtherGameObject } public static class TrailStyleProperties { public static bool supportsColor(this TrailStyle s) { return s != TrailStyle.SpaceDistortion; } } [ExecuteInEditMode] [HelpURL("https://kronnect.com/support")] [DefaultExecutionOrder(100)] public partial class TrailEffect : MonoBehaviour { #region Public Properties public TrailEffectProfile profile; [Tooltip("If enabled, settings will be synced with profile.")] public bool profileSync; public Transform target; [SerializeField] bool _active = true; public bool active { get { return _active; } set { _active = value; if (!_active) wasInactive = true; } } [Tooltip("By default, trails are not generated if the renderer is not visibile. This option ignores renderer visibility.")] public bool ignoreVisibility; public bool executeInEditMode; public int ignoreFrames; [Tooltip("The duration of this trail.")] public float duration = 0.5f; public bool continuous; [Tooltip("Use max steps to create a smooth trail if trigger condition is satisfied.")] public bool smooth; public bool checkWorldPosition; public float minDistance = 0.1f; public PositionChangeRelative worldPositionRelativeOption = PositionChangeRelative.World; public Transform worldPositionRelativeTransform; public bool checkScreenPosition = true; public int minPixelDistance = 10; public int stepsBufferSize = 1023; public int maxStepsPerFrame = 12; public bool checkTime; public float timeInterval = 1f; public bool checkCollisions; public bool orientToSurface = true; public bool ground; public float surfaceOffset = 0.05f; public LayerMask collisionLayerMask = -1; [Tooltip("Optional mask texture to be applied to the effect. Uses the red channel as an alpha (transparency) multiplier.")] public Texture2D mask; public bool drawBehind = true; public UnityEngine.Rendering.CullMode cullMode = UnityEngine.Rendering.CullMode.Back; public int subMeshMask = -1; [GradientUsage(hdr: true)] public Gradient colorOverTime; public bool colorRamp; public Texture2D colorRampTexture; public Transform colorRampStart, colorRampEnd; public bool fadeOut = true; public ColorSequence colorSequence = ColorSequence.Fixed; [ColorUsage(showAlpha: true, hdr: true)] public Color color = Color.white; public float colorCycleDuration = 3f; public bool colorCycleLoop = true; public float pingPongSpeed = 1f; [GradientUsage(hdr: true)] public Gradient colorStartPalette; public Camera cam; public TrailStyle effect = TrailStyle.Color; public Material customMaterial; public Texture2D texture; public Vector3 scale = Vector3.one, scaleStartRandomMin = Vector3.one, scaleStartRandomMax = Vector3.one; public AnimationCurve scaleOverTime; [Tooltip("Ignores object scale when calculating trail scale.")] public bool ignoreTransformScale; [Tooltip("Applies an uniform scale to x/y/z axis.")] public bool scaleUniform; [Tooltip("If set, trail will be parented to this gameobject")] public Transform parent; public Vector3 localPositionRandomMin, localPositionRandomMax; public float laserBandWidth = 0.1f, laserIntensity = 20f, laserFlash = 0.2f; [ColorUsage(showAlpha: true, hdr: true)] public Color trailTint = new Color(0f, 0, 0.1f); [Tooltip("Fades out effects based on distance to camera")] public bool cameraDistanceFade; [Tooltip("The closest distance particles can get to the camera before they fade from the camera’s view.")] public float cameraDistanceFadeNear; [Tooltip("The farthest distance particles can get away from the camera before they fade from the camera’s view.")] public float cameraDistanceFadeFar = 1000; [Tooltip("Add trails only during these animation states. Optionally include start and end time, example: Attack or Attack(0.1-1.5)")] public string animationStates; [Tooltip("The animator component. If not specified, first animator component found in children or parent will be used.")] public Animator animator; public Transform lookTarget; public bool lookToCamera = true; [Range(0, 1)] public float textureCutOff = 0.25f; [Range(0, 1)] public float normalThreshold = 0.3f; public bool useLastAnimationState; public int maxBatches = 50; public int meshPoolSize = 256; [Tooltip("Interpolate vertices to provide a smoother effect.")] public bool interpolate; #endregion static Color colorTransparent = new Color(0, 0, 0, 0); const int MAX_BATCH_INSTANCES = 1023; // max number of instances submitted to GPU in a batch. This limit is defined by Unity. const int BAKED_GRADIENTS_LENGTH = 256; // number of baked values for the gradients struct SnapshotTransform { public Matrix4x4 matrix, parentMatrix; public float time; public int meshIndex; public Color color; public Vector3 rampStartPos; public Vector3 rampEndPos; } public struct SnapshotIndex { public float t; public int index; } static class ShaderParams { public static int ColorArray = Shader.PropertyToID("_Colors"); public static int SubFrameKeyds = Shader.PropertyToID("_SubFrameKeys"); public static int ColorRamp = Shader.PropertyToID("_ColorRamp"); public static int CutOff = Shader.PropertyToID("_CutOff"); public static int NormalThreshold = Shader.PropertyToID("_NormalThreshold"); public static int AdditiveTint = Shader.PropertyToID("_AdditiveTint"); public static int LaserData = Shader.PropertyToID("_LaserData"); public static int Cull = Shader.PropertyToID("_Cull"); public static int RampStartPositions = Shader.PropertyToID("_RampStartPos"); public static int RampEndPositions = Shader.PropertyToID("_RampEndPos"); public static int MaskTex = Shader.PropertyToID("_MaskTex"); public static int ParentMatricesArray = Shader.PropertyToID("_ParentMatrices"); public static int PivotMatrix = Shader.PropertyToID("_PivotMatrix"); public const string SKW_MASK = "TRAIL_MASK"; public const string SKW_ALPHACLIP = "TRAIL_ALPHACLIP"; public const string SKW_INTERPOLATE = "TRAIL_INTERPOLATE"; public const string SKW_COLOR_RAMP = "TRAIL_COLOR_RAMP"; public const string SKW_LOCAL = "TRAIL_LOCAL"; } struct AnimationStatesInfo { public int hash; public float startTime; public float endTime; } SnapshotTransform[] trail; SnapshotIndex[] sortIndices; int trailIndex; Mesh[] meshPool; int meshPoolIndex; readonly List prevBakedMeshVertices = new List(); float[] subFrameKeys; Material trailMask, trailClearMask; Material[] trailMaterial; Renderer theRenderer; Vector3 lastCornerMinPos, lastCornerMaxPos, lastPosition, lastRandomizedPosition, lastRelativePosition; Quaternion lastRotation; float lastIntervalTimeCheck; MaterialPropertyBlock properties; Matrix4x4[] matrices; Matrix4x4[] parentMatrices; Vector4[] colors; Vector4[] rampStartPositions, rampEndPositions; Vector3 rampLastStartPosition, rampLastEndPosition; static int globalRenderQueue = 3100; int renderQueue; SkinnedMeshRenderer skinnedMeshRenderer; [NonSerialized] public bool isSkinned; int bakeTime; int batchNumber; static Mesh quadMesh; Dictionary effectMaterials; bool orient; Vector3 groundNormal; int startFrameCount; float startTime; float smoothDuration; bool isLimitedToAnimationStates; AnimationStatesInfo[] stateHashes; bool supportsGPUInstancing; MaterialPropertyBlock propertyBlock; Color colorRandomAtStart; Color[] bakedColorOverTime, bakedColorStartPalette; float[] bakedScaleOverTime; bool wasInactive; bool interpolating; bool usingColorRamp; static readonly char[] commaSeparator = new char[] { ',' }; static readonly char[] dashSeparator = new char[] { '-' }; bool hasParent; Vector3 parentPosition, lastParentPosition; Quaternion parentRotation, lastParentRotation; void OnEnable() { CheckEditorSettings(); // setup materials renderQueue = globalRenderQueue; globalRenderQueue += maxBatches + 2; if (globalRenderQueue > 3500) { globalRenderQueue = 3100; } if (trailMask == null) { trailMask = new Material(Shader.Find("TrailsFX/Mask")); trailMask.hideFlags = HideFlags.DontSave; } trailMask.renderQueue = renderQueue; if (trailClearMask == null) { trailClearMask = Instantiate(Resources.Load("TrailsFX/TrailClearMask")); trailClearMask.hideFlags = HideFlags.DontSave; } if (properties == null) { properties = new MaterialPropertyBlock(); } else { properties.Clear(); } supportsGPUInstancing = SystemInfo.supportsInstancing; if (!supportsGPUInstancing) { if (propertyBlock == null) { propertyBlock = new MaterialPropertyBlock(); } else { propertyBlock.Clear(); } } if (profileSync && profile != null) { profile.Load(this); } Clear(); } void DestroyMaterial(Material mat) { if (mat != null) { DestroyImmediate(mat); } } void OnDestroy() { DestroyMaterial(trailMask); DestroyMaterial(trailClearMask); if (trailMaterial != null) { for (int k = 0; k < trailMaterial.Length; k++) { DestroyMaterial(trailMaterial[k]); } } if (effectMaterials != null) { foreach (KeyValuePair kvp in effectMaterials) { DestroyMaterial(kvp.Value); } } if (isSkinned && meshPool != null) { for (int k = 0; k < meshPool.Length; k++) { if (meshPool[k] != null) { DestroyImmediate(meshPool[k]); } } } } void OnValidate() { CheckEditorSettings(); } void Start() { startFrameCount = Time.frameCount; if (executeInEditMode || Application.isPlaying) { UpdateMaterialProperties(); } colorRandomAtStart = bakedColorStartPalette[UnityEngine.Random.Range(0, BAKED_GRADIENTS_LENGTH)]; } #if UNITY_EDITOR private void OnDisable() { UnityEditor.EditorApplication.update -= ExecuteInEditor; } void ExecuteInEditor() { UnityEditor.EditorUtility.SetDirty(this); } #endif void LateUpdate() { if (!executeInEditMode && !Application.isPlaying) return; if (trail == null) return; if (cam == null) { cam = Camera.main; if (cam == null) { cam = FindObjectOfType(); if (cam == null) return; } } AddSnapshot(); RenderTrail(); } void OnCollisionEnter(Collision collision) { if (!checkCollisions || !_active) return; if (((1 << collision.gameObject.layer) & collisionLayerMask) == 0) return; Quaternion rotation; ContactPoint contact = collision.contacts[0]; Vector3 pos = contact.point; pos += contact.normal * surfaceOffset; if (orientToSurface) { rotation = Quaternion.LookRotation(-contact.normal); } else { if (lookTarget != null) { rotation = Quaternion.LookRotation(pos - lookTarget.transform.position); } else if (lookToCamera) { Camera camera = cam; if (camera == null) { camera = Camera.main; } if (camera != null) { rotation = Quaternion.LookRotation(pos - camera.transform.position); } else { rotation = target.rotation; } } else { rotation = target.rotation; } } AddSnapshot(pos, rotation); } public void CheckEditorSettings() { if (target == null) { target = transform; } if (colorOverTime == null) { colorOverTime = new Gradient(); GradientColorKey[] colorKeys = new GradientColorKey[2]; colorKeys[0].color = Color.yellow; colorKeys[0].time = 0f; colorKeys[1].color = Color.yellow; colorKeys[1].time = 1f; GradientAlphaKey[] alphaKeys = new GradientAlphaKey[2]; alphaKeys[0].alpha = 1f; alphaKeys[0].time = 0f; alphaKeys[1].alpha = 1f; alphaKeys[1].time = 10f; colorOverTime.SetKeys(colorKeys, alphaKeys); } if (scaleOverTime == null) { scaleOverTime = new AnimationCurve(); Keyframe[] keys = new Keyframe[2]; keys[0].value = 1f; keys[1].time = 0; keys[1].value = 1f; keys[1].time = 1; scaleOverTime.keys = keys; } if (colorStartPalette == null) { colorStartPalette = new Gradient(); GradientColorKey[] colorKeys = new GradientColorKey[3]; colorKeys[0].color = Color.red; colorKeys[0].time = 0f; colorKeys[1].color = Color.green; colorKeys[1].time = 0.5f; colorKeys[2].color = Color.blue; colorKeys[2].time = 1f; colorStartPalette.colorKeys = colorKeys; } } /// /// Clears current trail and restarts cycle /// public void Clear() { UpdateMaterialProperties(); if (theRenderer != null) { StoreCurrentPositions(); } lastRandomizedPosition = GetRandomizedPosition(); lastRotation = GetRotation(); meshPoolIndex = 0; prevBakedMeshVertices.Clear(); if (trail != null) { for (int k = 0; k < trail.Length; k++) { trail[k].time = float.MinValue; } } trailIndex = -1; startFrameCount = Time.frameCount; startTime = Time.time; CopyRampPositions(); } /// /// Restarts current trail cycle but keeps existing trail /// public void Restart() { startFrameCount = Time.frameCount; startTime = Time.time; } void CopyRampPositions() { if (colorRampStart != null) rampLastStartPosition = colorRampStart.position; if (colorRampEnd != null) rampLastEndPosition = colorRampEnd.position; } public void UpdateMaterialProperties() { #if UNITY_EDITOR UnityEditor.EditorApplication.update -= ExecuteInEditor; if (executeInEditMode) { UnityEditor.EditorApplication.update += ExecuteInEditor; } #endif CheckEditorSettings(); if (bakedColorOverTime == null || bakedColorOverTime.Length == 0) { bakedColorOverTime = new Color[BAKED_GRADIENTS_LENGTH]; } if (bakedScaleOverTime == null || bakedScaleOverTime.Length == 0) { bakedScaleOverTime = new float[BAKED_GRADIENTS_LENGTH]; } if (bakedColorStartPalette == null || bakedColorStartPalette.Length == 0) { bakedColorStartPalette = new Color[BAKED_GRADIENTS_LENGTH]; } for (int k = 0; k < BAKED_GRADIENTS_LENGTH; k++) { float t = (float)k / BAKED_GRADIENTS_LENGTH; bakedColorOverTime[k] = colorOverTime.Evaluate(t); bakedScaleOverTime[k] = scaleOverTime.Evaluate(t); bakedColorStartPalette[k] = colorStartPalette.Evaluate(t); } groundNormal = Vector3.up; skinnedMeshRenderer = null; theRenderer = target.GetComponentInChildren(); if (theRenderer == null) { trail = null; if (Application.isPlaying) { enabled = false; } return; } isLimitedToAnimationStates = false; if (!string.IsNullOrEmpty(animationStates)) { if (animator == null) { animator = target.GetComponentInChildren(); if (animator == null) { animator = target.GetComponentInParent(); } } isLimitedToAnimationStates = animator != null; if (isLimitedToAnimationStates) { string[] names = animationStates.Split(commaSeparator, StringSplitOptions.RemoveEmptyEntries); int hashCount = names.Length; stateHashes = new AnimationStatesInfo[hashCount]; for (int k = 0; k < hashCount; k++) { string name = null; float startTime = 0, endTime = 0; string data = names[k].Trim(); int par0 = data.IndexOf("("); int par1 = data.IndexOf(")"); if (par1 > par0 && par0 > 0) { name = data.Substring(0, par0).Trim(); string interval = data.Substring(par0 + 1, par1 - par0 - 1); string[] times = interval.Split(dashSeparator, StringSplitOptions.RemoveEmptyEntries); if (times.Length == 2) { float.TryParse(times[0], System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out startTime); float.TryParse(times[1], System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out endTime); } } else { name = data; } stateHashes[k].hash = Animator.StringToHash(name); stateHashes[k].startTime = startTime; stateHashes[k].endTime = endTime; } } } isSkinned = theRenderer is SkinnedMeshRenderer; if (isSkinned) { skinnedMeshRenderer = (SkinnedMeshRenderer)theRenderer; int poolSize = useLastAnimationState ? 1 : meshPoolSize; if (meshPool == null || meshPool.Length != poolSize) { meshPool = new Mesh[meshPoolSize]; } int meshPoolLength = meshPool.Length; for (int k = 0; k < meshPoolLength; k++) { if (meshPool[k] == null) { meshPool[k] = new Mesh(); meshPool[k].hideFlags = HideFlags.DontSave; } } } else { MeshCollider mc = theRenderer.GetComponent(); if (meshPool == null || meshPool.Length != 1) { meshPool = new Mesh[1]; } if (mc != null) { meshPool[0] = mc.sharedMesh; } else { MeshFilter mf = theRenderer.GetComponent(); if (mf != null) { meshPool[0] = mf.sharedMesh; } } } // Runtime only setup if (!executeInEditMode && !Application.isPlaying) return; orient = false; if (trailMask == null) return; trailMask.DisableKeyword(ShaderParams.SKW_ALPHACLIP); trailMask.mainTexture = null; trailMask.SetInt(ShaderParams.Cull, (int)cullMode); Material trailMat = null; switch (effect) { case TrailStyle.Color: trailMat = GetEffectMaterial("TrailEffectColor"); break; case TrailStyle.TextureStamp: trailMat = GetEffectMaterial("TrailEffectTextureStamp"); if (quadMesh == null) { quadMesh = BuildQuadMesh(); } orient = (ground && orientToSurface) || lookToCamera || lookTarget != null; break; case TrailStyle.Clone: trailMat = GetEffectMaterial("TrailEffectClone"); break; case TrailStyle.Outline: trailMat = GetEffectMaterial("TrailEffectOutline"); break; case TrailStyle.SpaceDistortion: trailMat = GetEffectMaterial("TrailEffectDistort"); break; case TrailStyle.Dash: trailMat = GetEffectMaterial("TrailEffectLaser"); break; case TrailStyle.Custom: trailMat = customMaterial; break; } if (trailMat == null) { trail = null; enabled = false; return; } trailMat.SetInt(ShaderParams.Cull, (int)cullMode); interpolating = isSkinned && interpolate && !useLastAnimationState; usingColorRamp = colorRamp && colorRampTexture != null && colorRampStart != null && colorRampEnd != null; if (trailMaterial == null || trailMaterial.Length != maxBatches) { if (trailMaterial != null) { for (int k = 0; k < trailMaterial.Length; k++) { DestroyMaterial(trailMaterial[k]); } } trailMaterial = new Material[maxBatches]; } for (int k = 0; k < trailMaterial.Length; k++) { if (trailMaterial[k] == null || trailMaterial[k].shader != trailMat.shader) { trailMaterial[k] = Instantiate(trailMat); trailMaterial[k].hideFlags = HideFlags.DontSave; } SetMaterialProperties(trailMaterial[k]); trailMaterial[k].renderQueue = renderQueue + k + 1; } trailClearMask.renderQueue = renderQueue + maxBatches + 1; trailClearMask.SetInt(ShaderParams.Cull, (int)cullMode); if (trail == null || trail.Length != stepsBufferSize) { trail = new SnapshotTransform[stepsBufferSize]; for (int k = 0; k < trail.Length; k++) { trail[k].time = float.MinValue; } trailIndex = -1; } if (sortIndices == null || sortIndices.Length != stepsBufferSize) { sortIndices = new SnapshotIndex[stepsBufferSize]; } if (matrices == null || matrices.Length != MAX_BATCH_INSTANCES) { matrices = new Matrix4x4[MAX_BATCH_INSTANCES]; } if (parentMatrices == null || parentMatrices.Length != MAX_BATCH_INSTANCES) { parentMatrices = new Matrix4x4[MAX_BATCH_INSTANCES]; } if (colors == null || colors.Length != MAX_BATCH_INSTANCES) { colors = new Vector4[MAX_BATCH_INSTANCES]; } if (subFrameKeys == null || subFrameKeys.Length != MAX_BATCH_INSTANCES) { subFrameKeys = new float[MAX_BATCH_INSTANCES]; } if (rampStartPositions == null || rampStartPositions.Length != MAX_BATCH_INSTANCES) { rampStartPositions = new Vector4[MAX_BATCH_INSTANCES]; } if (rampEndPositions == null || rampEndPositions.Length != MAX_BATCH_INSTANCES) { rampEndPositions = new Vector4[MAX_BATCH_INSTANCES]; } StoreCurrentPositions(); } /// /// Loads and applies a different profile /// public void SetProfile(TrailEffectProfile profile) { if (profile != null) { profile.Load(this); } } void SetMaterialProperties(Material trailMat) { switch (effect) { case TrailStyle.Color: trailMat.SetTexture(ShaderParams.ColorRamp, colorRampTexture); break; case TrailStyle.TextureStamp: trailMat.renderQueue = renderQueue + 1; trailMat.mainTexture = texture; trailMat.SetFloat(ShaderParams.CutOff, textureCutOff); trailMask.mainTexture = texture; trailMask.SetFloat(ShaderParams.CutOff, textureCutOff); trailMask.EnableKeyword(ShaderParams.SKW_ALPHACLIP); break; case TrailStyle.Clone: Material origMat = theRenderer.sharedMaterial; if (origMat != null) { trailMat.mainTexture = origMat.mainTexture; trailMat.mainTextureScale = origMat.mainTextureScale; trailMat.mainTextureOffset = origMat.mainTextureOffset; trailMat.SetFloat(ShaderParams.CutOff, textureCutOff); if (textureCutOff > 0) { trailMat.EnableKeyword(ShaderParams.SKW_ALPHACLIP); } else { trailMat.DisableKeyword(ShaderParams.SKW_ALPHACLIP); } } break; case TrailStyle.Outline: trailMat.SetFloat(ShaderParams.NormalThreshold, normalThreshold); break; case TrailStyle.SpaceDistortion: trailMat.SetColor(ShaderParams.AdditiveTint, trailTint); break; case TrailStyle.Dash: trailMat.SetVector(ShaderParams.LaserData, new Vector3(laserBandWidth, laserIntensity, laserFlash)); break; } if (mask != null) { trailMat.SetTexture(ShaderParams.MaskTex, mask); trailMat.EnableKeyword(ShaderParams.SKW_MASK); } else { trailMat.DisableKeyword(ShaderParams.SKW_MASK); } if (interpolating) { trailMat.EnableKeyword(ShaderParams.SKW_INTERPOLATE); } else { trailMat.DisableKeyword(ShaderParams.SKW_INTERPOLATE); } if (usingColorRamp) { trailMat.EnableKeyword(ShaderParams.SKW_COLOR_RAMP); } else { trailMat.DisableKeyword(ShaderParams.SKW_COLOR_RAMP); } hasParent = parent != null; if (hasParent) { trailMat.EnableKeyword(ShaderParams.SKW_LOCAL); } else { trailMat.DisableKeyword(ShaderParams.SKW_LOCAL); } } Material GetEffectMaterial(string materialName) { if (effectMaterials == null) { effectMaterials = new Dictionary(); } Material mat; if (!effectMaterials.TryGetValue(materialName, out mat)) { mat = Resources.Load("TrailsFX/" + materialName); if (mat == null) { Debug.LogError("Could not find trail material " + materialName); return null; } mat = Instantiate(mat); mat.hideFlags = HideFlags.DontSave; effectMaterials[materialName] = mat; } return mat; } Mesh BuildQuadMesh() { Mesh mesh = new Mesh(); mesh.name = "TrailQuadMesh"; // Setup vertices Vector3[] newVertices = new Vector3[4]; float halfHeight = 0.5f; float halfWidth = 0.5f; newVertices[0] = new Vector3(-halfWidth, -halfHeight, 0); newVertices[1] = new Vector3(-halfWidth, halfHeight, 0); newVertices[2] = new Vector3(halfWidth, -halfHeight, 0); newVertices[3] = new Vector3(halfWidth, halfHeight, 0); // Setup UVs Vector2[] newUVs = new Vector2[newVertices.Length]; newUVs[0] = new Vector2(0, 0); newUVs[1] = new Vector2(0, 1); newUVs[2] = new Vector2(1, 0); newUVs[3] = new Vector2(1, 1); // Setup triangles int[] newTriangles = new int[] { 0, 1, 2, 3, 2, 1 }; // Setup normals Vector3[] newNormals = new Vector3[newVertices.Length]; for (int i = 0; i < newNormals.Length; i++) { newNormals[i] = Vector3.forward; } // Create quad mesh.vertices = newVertices; mesh.uv = newUVs; mesh.triangles = newTriangles; mesh.normals = newNormals; mesh.RecalculateBounds(); return mesh; } Vector3 GetSnapshotScale() { Vector3 objectScale = ignoreTransformScale ? Vector3.one : target.lossyScale; if (scaleUniform) { float t = UnityEngine.Random.Range(scaleStartRandomMin.x, scaleStartRandomMax.x); if (isSkinned) { return new Vector3(t * scale.x, t * scale.y, t * scale.z); } else { return new Vector3(objectScale.x * scale.x * t, objectScale.y * scale.y * t, objectScale.z * scale.z * t); } } else { if (isSkinned) { return new Vector3(scale.x * UnityEngine.Random.Range(scaleStartRandomMin.x, scaleStartRandomMax.x), scale.y * UnityEngine.Random.Range(scaleStartRandomMin.y, scaleStartRandomMax.y), scale.z * UnityEngine.Random.Range(scaleStartRandomMin.z, scaleStartRandomMax.z)); } else { return new Vector3(objectScale.x * scale.x * UnityEngine.Random.Range(scaleStartRandomMin.x, scaleStartRandomMax.x), objectScale.y * scale.y * UnityEngine.Random.Range(scaleStartRandomMin.y, scaleStartRandomMax.y), objectScale.z * scale.z * UnityEngine.Random.Range(scaleStartRandomMin.z, scaleStartRandomMax.z)); } } } Quaternion GetRotation() { Quaternion rot = target.rotation; return rot; } Vector3 GetRandomizedPosition() { Vector3 localPos = new Vector3(UnityEngine.Random.Range(localPositionRandomMin.x, localPositionRandomMax.x), UnityEngine.Random.Range(localPositionRandomMin.y, localPositionRandomMax.y), UnityEngine.Random.Range(localPositionRandomMin.z, localPositionRandomMax.z)); Vector3 wpos = target.position; Vector3 pos; if (lastPosition == wpos) { pos = localPos + wpos; } else { pos = (Quaternion.LookRotation(wpos - lastPosition) * localPos) + wpos; } if (ground) { Ray ray = new Ray(target.position, Vector3.down); RaycastHit hit; if (Physics.Raycast(ray, out hit)) { pos = hit.point + pos - target.position; groundNormal = hit.normal; } } else { if (effect == TrailStyle.TextureStamp) { pos += theRenderer.bounds.center - target.position; } } return pos; } Color GetSnapshotColor() { Color snapshotColor; if (effect == TrailStyle.SpaceDistortion) { Vector2 scrPos0 = cam.WorldToViewportPoint(target.position); Vector2 scrPos1 = cam.WorldToViewportPoint(lastPosition); Vector2 diff = (scrPos0 - scrPos1).normalized; diff.x += 0.5f; diff.y += 0.5f; snapshotColor.r = diff.x; snapshotColor.g = diff.y; snapshotColor.b = 0; snapshotColor.a = 1f; } else { switch (colorSequence) { case ColorSequence.Random: snapshotColor = bakedColorStartPalette[UnityEngine.Random.Range(0, BAKED_GRADIENTS_LENGTH)]; break; case ColorSequence.FixedRandom: snapshotColor = colorRandomAtStart; break; case ColorSequence.Cycle: { if (colorCycleDuration < 0) { colorCycleDuration = 0.01f; } float t = (Time.time - startTime) / colorCycleDuration; if (t > 1f && !colorCycleLoop) { return colorTransparent; } int it = (int)((t - (int)t) * BAKED_GRADIENTS_LENGTH); snapshotColor = bakedColorStartPalette[it]; } break; case ColorSequence.PingPong: { float t = Mathf.PingPong((Time.time - startTime) * pingPongSpeed, 0.999f); int it = (int)(t * BAKED_GRADIENTS_LENGTH); snapshotColor = bakedColorStartPalette[it]; } break; default: snapshotColor = color; break; } } if (cameraDistanceFade) { snapshotColor.a *= ComputeCameraDistanceFade(target.position, cam.transform); } return snapshotColor; } float ComputeCameraDistanceFade(Vector3 position, Transform cameraTransform) { Vector3 heading = position - cameraTransform.position; float distance = Vector3.Dot(heading, cameraTransform.forward); if (distance < cameraDistanceFadeNear) { return 1f - Mathf.Min(1f, cameraDistanceFadeNear - distance); } if (distance > cameraDistanceFadeFar) { return 1f - Mathf.Min(1f, distance - cameraDistanceFadeFar); } return 1f; } void StoreCurrentPositions() { if (executeInEditMode || Application.isPlaying) { Bounds bounds = theRenderer.bounds; lastCornerMinPos = bounds.min; lastCornerMaxPos = bounds.max; lastPosition = target.position; lastRelativePosition = lastPosition; if (worldPositionRelativeOption == PositionChangeRelative.OtherGameObject && worldPositionRelativeTransform != null) { lastRelativePosition -= worldPositionRelativeTransform.position; } lastRotation = GetRotation(); CopyRampPositions(); if (parent != null) { lastParentPosition = parent.position; lastParentRotation = parent.rotation; } } } void AddSnapshot() { if (!_active || (!theRenderer.enabled && !ignoreVisibility)) { wasInactive = true; return; } if (wasInactive) { wasInactive = false; lastRandomizedPosition = GetRandomizedPosition(); StoreCurrentPositions(); } bool skip = Time.frameCount - startFrameCount < ignoreFrames || Time.timeScale == 0; if (isLimitedToAnimationStates && !skip) { skip = true; int layersCount = animator.layerCount; int stateHashesLength = stateHashes.Length; for (int l = 0; l < layersCount && skip; l++) { AnimatorStateInfo stateInfo = animator.GetCurrentAnimatorStateInfo(l); int shortNameHash = stateInfo.shortNameHash; for (int k = 0; k < stateHashesLength; k++) { if (stateHashes[k].hash != shortNameHash) continue; // check animation interval constraint if (stateHashes[k].endTime == 0) { // no constraint skip = false; break; } float animTime = stateInfo.normalizedTime * stateInfo.length; if (animTime < stateHashes[k].startTime || animTime > stateHashes[k].endTime) continue; skip = false; break; } } } if (skip) { prevBakedMeshVertices.Clear(); lastRandomizedPosition = GetRandomizedPosition(); StoreCurrentPositions(); return; } float now = Time.time; int steps = continuous ? maxStepsPerFrame : 0; if (steps == 0) { if (checkWorldPosition) { Vector3 referencePosition = target.position; Vector3 referenceLastPos = lastPosition; if (worldPositionRelativeOption == PositionChangeRelative.OtherGameObject && worldPositionRelativeTransform != null) { referencePosition -= worldPositionRelativeTransform.position; referenceLastPos = lastRelativePosition; } float distance = Vector3.Distance(referencePosition, referenceLastPos); if (distance >= minDistance) { if (smooth) { smoothDuration = now + 1f; } else { steps = (int)(distance / minDistance); } } } if (checkScreenPosition) { if (minPixelDistance <= 0) { minPixelDistance = 1; } // Difference of corners in viewport from last frame Vector2 viewportPos0 = cam.WorldToViewportPoint(lastCornerMinPos); Vector2 viewportPos1 = cam.WorldToViewportPoint(theRenderer.bounds.min); int pixelDistance = Mathf.Max(Mathf.CeilToInt(Mathf.Abs(viewportPos1.x - viewportPos0.x) * cam.pixelWidth), Mathf.CeilToInt(Mathf.Abs(viewportPos1.y - viewportPos0.y) * cam.pixelHeight)); int stepsCornerMin = pixelDistance / minPixelDistance; viewportPos0 = cam.WorldToViewportPoint(lastCornerMaxPos); viewportPos1 = cam.WorldToViewportPoint(theRenderer.bounds.max); pixelDistance = Mathf.Max((int)(Mathf.Abs(viewportPos1.x - viewportPos0.x) * cam.pixelWidth), (int)(Mathf.Abs(viewportPos1.y - viewportPos0.y) * cam.pixelHeight)); if (pixelDistance >= minPixelDistance) { if (smooth) { smoothDuration = now + 1f; } else { int stepsCornerMax = pixelDistance / minPixelDistance; steps = Mathf.Max(steps, Mathf.Max(stepsCornerMax, stepsCornerMin)); } } } if (checkTime) { if (now - lastIntervalTimeCheck >= timeInterval) { lastIntervalTimeCheck = now; steps = Mathf.Max(1, steps); } } } if (now < smoothDuration) { steps = maxStepsPerFrame; } if (steps <= 0) return; Color color = GetSnapshotColor(); if (color.a == 0) return; if (steps > maxStepsPerFrame) { steps = maxStepsPerFrame; } SetupMesh(true); Vector3 pos = GetRandomizedPosition(); Vector3 targetPos = Vector3.zero; Vector3 upwards = Vector3.up; if (ground && orientToSurface) { targetPos = pos + groundNormal; if (target.position != lastPosition) { upwards = target.position - lastPosition; } else { upwards = target.forward; } } else if (orient) { if (lookTarget != null) { targetPos = lookTarget.position; } else { Camera camera = cam; if (camera == null) { camera = Camera.main; } if (camera != null) { targetPos = camera.transform.position; } else { orient = false; } } } Vector3 scale = GetSnapshotScale(); float lastFrameTime = now - Time.deltaTime; Quaternion rotation = GetRotation(); bool hasParent = parent != null; if (hasParent) { parentPosition = parent.position; parentRotation = parent.rotation; } for (int k = 0; k < steps; k++) { trailIndex++; if (trailIndex >= trail.Length) { trailIndex = 0; } float t = (k + 1f) / steps; Vector3 p = Vector3.Lerp(lastRandomizedPosition, pos, t); if (orient) { trail[trailIndex].matrix = Matrix4x4.TRS(p, Quaternion.LookRotation(p - targetPos, upwards), scale); } else { Quaternion rot = Quaternion.Slerp(lastRotation, rotation, t); trail[trailIndex].matrix = Matrix4x4.TRS(p, rot, scale); if (hasParent) { Vector3 ppos = Vector3.Lerp(lastParentPosition, parentPosition, t); Quaternion prot = Quaternion.Slerp(lastParentRotation, parentRotation, t); trail[trailIndex].parentMatrix = Matrix4x4.TRS(ppos, prot, parent.localScale).inverse; } } trail[trailIndex].time = (lastFrameTime * (1f - t)) + now * t; trail[trailIndex].meshIndex = meshPoolIndex; trail[trailIndex].color = color; if (usingColorRamp) { trail[trailIndex].rampStartPos = Vector3.Lerp(rampLastStartPosition, colorRampStart.position, t); trail[trailIndex].rampEndPos = Vector3.Lerp(rampLastEndPosition, colorRampEnd.position, t); } } lastRandomizedPosition = pos; StoreCurrentPositions(); } void AddSnapshot(Vector3 pos, Quaternion rotation) { if (!_active || (!theRenderer.enabled && !ignoreVisibility)) return; Color color = GetSnapshotColor(); if (color.a == 0) return; SetupMesh(true); Vector3 scale = GetSnapshotScale(); trailIndex++; if (trailIndex >= trail.Length) { trailIndex = 0; } trail[trailIndex].matrix = Matrix4x4.TRS(pos, rotation, scale); trail[trailIndex].time = Time.time; trail[trailIndex].meshIndex = meshPoolIndex; trail[trailIndex].color = color; } void RenderTrail() { if (duration < 0) { duration = 0.001f; } int count = 0; float now = Time.time; // Pick entries and compute transition for (int i = 0; i < trail.Length; i++) { float t = now - trail[i].time; if (t < duration) { sortIndices[count].t = t / duration; sortIndices[count].index = i; count++; if (count >= stepsBufferSize) break; } } if (count == 0) return; // Sort indices QuickSort(0, count - 1); // Build batches batchNumber = 0; bool singleBatch = useLastAnimationState || effect == TrailStyle.TextureStamp; if (singleBatch && count <= MAX_BATCH_INSTANCES) { SendToGPU(meshPoolIndex, 0, count); } else { int batchMeshIndex = trail[sortIndices[0].index].meshIndex; int batchStartIndex = 0; int batchInstancesCount = 1; for (int k = 1; k < count; k++) { int i = sortIndices[k].index; int meshIndex = trail[i].meshIndex; if (meshIndex != batchMeshIndex || batchInstancesCount >= MAX_BATCH_INSTANCES) { // send previous batch SendToGPU(batchMeshIndex, batchStartIndex, batchInstancesCount); // prepare new batch batchMeshIndex = meshIndex; batchStartIndex += batchInstancesCount; batchInstancesCount = 0; } batchInstancesCount++; } if (batchInstancesCount > 0) { // send last batch SendToGPU(batchMeshIndex, batchStartIndex, batchInstancesCount); } } } void SendToGPU(int meshIndex, int startIndex, int count) { if (meshIndex < 0 || meshIndex >= meshPool.Length) return; Mesh batchMesh = effect == TrailStyle.TextureStamp ? quadMesh : meshPool[meshIndex]; if (batchMesh == null) return; int layer = target.gameObject.layer; if (drawBehind && batchNumber == 0 && (theRenderer.isVisible || !ignoreVisibility)) { Vector3 pos = target.position; Vector3 sca; Mesh mesh; if (isSkinned) { mesh = SetupMesh(false); sca = Vector3.one; } else { mesh = meshPool[meshIndex]; sca = target.lossyScale; } if (mesh != null) { Matrix4x4 m = Matrix4x4.TRS(pos, target.rotation, sca); if (subMeshMask > 0) { int subMeshCount = mesh.subMeshCount; for (int k = 0; k < subMeshCount; k++) { if (((1 << k) & subMeshMask) != 0) { Graphics.DrawMesh(mesh, m, trailMask, layer, null, k); // runs first in render queue Graphics.DrawMesh(mesh, m, trailClearMask, layer, null, k); // runs last in render queue } } } else { Graphics.DrawMesh(mesh, m, trailMask, layer); // runs first in render queue Graphics.DrawMesh(mesh, m, trailClearMask, layer); // runs last in render queue } } } // Pack for instancing for (int o = 0; o < count; o++, startIndex++) { int index = sortIndices[startIndex].index; float t = sortIndices[startIndex].t; int it = (int)(BAKED_GRADIENTS_LENGTH * t) % BAKED_GRADIENTS_LENGTH; // Assign RGBA Color baseColor = trail[index].color; Color color = bakedColorOverTime[it]; colors[o].x = color.r * baseColor.r; colors[o].y = color.g * baseColor.g; colors[o].z = color.b * baseColor.b; colors[o].w = color.a * baseColor.a; if (fadeOut) colors[o].w *= (1f - t); // Pass subframe key subFrameKeys[o] = (float)o / count; // Set matrix float scale = bakedScaleOverTime[it]; matrices[o].m00 = trail[index].matrix.m00 * scale; matrices[o].m01 = trail[index].matrix.m01 * scale; matrices[o].m02 = trail[index].matrix.m02 * scale; matrices[o].m03 = trail[index].matrix.m03; matrices[o].m10 = trail[index].matrix.m10 * scale; matrices[o].m11 = trail[index].matrix.m11 * scale; matrices[o].m12 = trail[index].matrix.m12 * scale; matrices[o].m13 = trail[index].matrix.m13; matrices[o].m20 = trail[index].matrix.m20 * scale; matrices[o].m21 = trail[index].matrix.m21 * scale; matrices[o].m22 = trail[index].matrix.m22 * scale; matrices[o].m23 = trail[index].matrix.m23; matrices[o].m30 = trail[index].matrix.m30; matrices[o].m31 = trail[index].matrix.m31; matrices[o].m32 = trail[index].matrix.m32; matrices[o].m33 = trail[index].matrix.m33; // Color ramp positions if (usingColorRamp) { rampStartPositions[o].x = trail[index].rampStartPos.x; rampStartPositions[o].y = trail[index].rampStartPos.y; rampStartPositions[o].z = trail[index].rampStartPos.z; rampEndPositions[o].x = trail[index].rampEndPos.x; rampEndPositions[o].y = trail[index].rampEndPos.y; rampEndPositions[o].z = trail[index].rampEndPos.z; } if (hasParent) { parentMatrices[o].m00 = trail[index].parentMatrix.m00; parentMatrices[o].m01 = trail[index].parentMatrix.m01; parentMatrices[o].m02 = trail[index].parentMatrix.m02; parentMatrices[o].m03 = trail[index].parentMatrix.m03; parentMatrices[o].m10 = trail[index].parentMatrix.m10; parentMatrices[o].m11 = trail[index].parentMatrix.m11; parentMatrices[o].m12 = trail[index].parentMatrix.m12; parentMatrices[o].m13 = trail[index].parentMatrix.m13; parentMatrices[o].m20 = trail[index].parentMatrix.m20; parentMatrices[o].m21 = trail[index].parentMatrix.m21; parentMatrices[o].m22 = trail[index].parentMatrix.m22; parentMatrices[o].m23 = trail[index].parentMatrix.m23; parentMatrices[o].m30 = trail[index].parentMatrix.m30; parentMatrices[o].m31 = trail[index].parentMatrix.m31; parentMatrices[o].m32 = trail[index].parentMatrix.m32; parentMatrices[o].m33 = trail[index].parentMatrix.m33; } } // Send batch to pipeline properties.SetVectorArray(ShaderParams.ColorArray, colors); if (interpolating || usingColorRamp) { properties.SetFloatArray(ShaderParams.SubFrameKeyds, subFrameKeys); } if (usingColorRamp) { properties.SetVectorArray(ShaderParams.RampStartPositions, rampStartPositions); properties.SetVectorArray(ShaderParams.RampEndPositions, rampEndPositions); } if (hasParent) { properties.SetMatrixArray(ShaderParams.ParentMatricesArray, parentMatrices); properties.SetMatrix(ShaderParams.PivotMatrix, parent.localToWorldMatrix); } if (batchNumber < trailMaterial.Length - 1) { batchNumber++; } int batchMeshSubMeshCount = batchMesh.subMeshCount; if (supportsGPUInstancing) { for (int s = 0; s < batchMeshSubMeshCount; s++) { if (((1 << s) & subMeshMask) != 0) { Graphics.DrawMeshInstanced(batchMesh, s, trailMaterial[batchNumber], matrices, count, properties, UnityEngine.Rendering.ShadowCastingMode.Off, false, layer); } } // Clear stencil buffer for (int s = 0; s < batchMeshSubMeshCount; s++) { if (((1 << s) & subMeshMask) != 0) { Graphics.DrawMeshInstanced(batchMesh, s, trailClearMask, matrices, count, null, UnityEngine.Rendering.ShadowCastingMode.Off, false, layer); } } } else { // Fallback for GPUs not supporting instancing; better than nothing :( for (int i = 0; i < count; i++) { propertyBlock.SetVector(ShaderParams.ColorArray, colors[i]); for (int s = 0; s < batchMeshSubMeshCount; s++) { if (((1 << s) & subMeshMask) != 0) { Graphics.DrawMesh(batchMesh, matrices[i], trailMaterial[batchNumber], layer, null, s, propertyBlock, false, false); } } } // Clear stencil buffer for (int s = 0; s < batchMeshSubMeshCount; s++) { if (((1 << s) & subMeshMask) != 0) { for (int i = 0; i < count; i++) { Graphics.DrawMesh(batchMesh, matrices[i], trailClearMask, layer, null, s, null, false, false); } } } } } Mesh SetupMesh(bool bakeMeshNow) { if (isSkinned && bakeMeshNow) { int thisFrame = Time.frameCount; if (thisFrame != bakeTime) { bakeTime = thisFrame; meshPoolIndex++; if (meshPoolIndex >= meshPool.Length) { meshPoolIndex = 0; } skinnedMeshRenderer.BakeMesh(meshPool[meshPoolIndex]); if (interpolating) { if (prevBakedMeshVertices.Count > 0) { meshPool[meshPoolIndex].SetUVs(1, prevBakedMeshVertices); meshPool[meshPoolIndex].GetVertices(prevBakedMeshVertices); } else { meshPool[meshPoolIndex].GetVertices(prevBakedMeshVertices); meshPool[meshPoolIndex].SetUVs(1, prevBakedMeshVertices); } } } } return meshPool[meshPoolIndex]; } void QuickSort(int min, int max) { int i = min; int j = max; float x = sortIndices[(min + max) / 2].t; do { while (sortIndices[i].t < x) { i++; } while (sortIndices[j].t > x) { j--; } if (i <= j) { SnapshotIndex h = sortIndices[i]; sortIndices[i] = sortIndices[j]; sortIndices[j] = h; i++; j--; } } while (i <= j); if (min < j) { QuickSort(min, j); } if (i < max) { QuickSort(i, max); } } /// /// Returns the position of a trail snapshot /// /// Index of the trail snapshot (0 to step buffer size defined in Trail Effect component) /// Returns the world space position of the trail snapshot public Vector3 GetTrailPosition(int index) { return new Vector3(trail[index].matrix.m03, trail[index].matrix.m13, trail[index].matrix.m23); } } }