using UnityEngine; #if UNITY_EDITOR using UnityEditor; using UnityEditor.SceneManagement; #if UNITY_2018_3_OR_NEWER using PrefabStage = UnityEditor.SceneManagement.PrefabStage; using PrefabStageUtility = UnityEditor.SceneManagement.PrefabStageUtility; #endif #endif namespace BezierSolution { [AddComponentMenu( "Bezier Solution/Bend Mesh Along Bezier" )] [HelpURL( "https://github.com/yasirkula/UnityBezierSolution" )] [RequireComponent( typeof( MeshFilter ) )] [ExecuteInEditMode] public class BendMeshAlongBezier : MonoBehaviour { public enum VectorMode { DontModify = 0, ModifyOriginals = 1, RecalculateFromScratch = 2 }; public enum Axis { X = 0, Y = 1, Z = 2 }; #pragma warning disable 0649 [SerializeField] private BezierSpline m_spline; public BezierSpline spline { get { return m_spline; } set { if( m_spline != value ) { if( m_spline ) m_spline.onSplineChanged -= OnSplineChanged; m_spline = value; if( m_spline && isActiveAndEnabled ) { m_spline.onSplineChanged -= OnSplineChanged; m_spline.onSplineChanged += OnSplineChanged; OnSplineChanged( m_spline, DirtyFlags.All ); } } } } [SerializeField] [MinMaxRange( 0f, 1f )] private Vector2 m_splineSampleRange = new Vector2( 0f, 1f ); public Vector2 SplineSampleRange { get { return m_splineSampleRange; } set { value.x = Mathf.Clamp01( value.x ); value.y = Mathf.Clamp01( value.y ); if( m_splineSampleRange != value ) { m_splineSampleRange = value; OnSplineChanged( m_spline, DirtyFlags.All ); } } } [Header( "Bend Options" )] [SerializeField] private bool m_highQuality = false; public bool highQuality { get { return m_highQuality; } set { if( m_highQuality != value ) { m_highQuality = value; OnSplineChanged( m_spline, DirtyFlags.All ); } } } [SerializeField] private Axis m_bendAxis = Axis.Y; public Axis bendAxis { get { return m_bendAxis; } set { if( m_bendAxis != value ) { m_bendAxis = value; RecalculateVertexRange(); OnSplineChanged( m_spline, DirtyFlags.All ); } } } [SerializeField] [Range( 0f, 360f )] private float m_extraRotation = 0f; public float extraRotation { get { return m_extraRotation; } set { value = Mathf.Clamp( value, 0f, 360f ); if( m_extraRotation != value ) { m_extraRotation = value; OnSplineChanged( m_spline, DirtyFlags.All ); } } } [SerializeField] private bool m_invertDirection = false; public bool invertDirection { get { return m_invertDirection; } set { if( m_invertDirection != value ) { m_invertDirection = value; OnSplineChanged( m_spline, DirtyFlags.All ); } } } [SerializeField] private Vector2 m_thicknessMultiplier = Vector2.one; public Vector2 thicknessMultiplier { get { return m_thicknessMultiplier; } set { if( m_thicknessMultiplier != value ) { m_thicknessMultiplier = value; OnSplineChanged( m_spline, DirtyFlags.All ); } } } [Header( "Vertex Attributes" )] [SerializeField] private VectorMode m_normalsMode = VectorMode.ModifyOriginals; public VectorMode normalsMode { get { return m_normalsMode; } set { if( m_normalsMode != value ) { m_normalsMode = value; if( mesh ) { if( m_normalsMode == VectorMode.DontModify && originalNormals != null ) { mesh.normals = originalNormals; originalNormals = null; } if( m_normalsMode != VectorMode.ModifyOriginals ) normals = null; } OnSplineChanged( m_spline, DirtyFlags.All ); } } } [SerializeField] private VectorMode m_tangentsMode = VectorMode.ModifyOriginals; public VectorMode tangentsMode { get { return m_tangentsMode; } set { if( m_tangentsMode != value ) { m_tangentsMode = value; if( mesh ) { if( m_tangentsMode == VectorMode.DontModify && originalTangents != null ) { mesh.tangents = originalTangents; originalTangents = null; } if( m_tangentsMode != VectorMode.ModifyOriginals ) tangents = null; } OnSplineChanged( m_spline, DirtyFlags.All ); } } } [Header( "Other Settings" )] [SerializeField] private bool m_autoRefresh = true; public bool autoRefresh { get { return m_autoRefresh; } set { if( m_autoRefresh != value ) { m_autoRefresh = value; OnSplineChanged( m_spline, DirtyFlags.All ); } } } #if UNITY_EDITOR [SerializeField] private bool executeInEditMode = false; [SerializeField, HideInInspector] private BezierSpline prevSpline; [SerializeField, HideInInspector] private VectorMode prevNormalsMode, prevTangentsMode; [SerializeField, HideInInspector] private bool prevHighQuality; #endif [SerializeField, HideInInspector] private Mesh originalMesh; // If this isn't serialized, then sometimes exceptions occur on undo/redo #pragma warning restore 0649 private MeshFilter meshFilter; private Mesh mesh; private Vector3[] vertices, originalVertices; private Vector3[] normals, originalNormals; private Vector4[] tangents, originalTangents; private float minVertex, _1OverVertexRange; private void OnEnable() { #if UNITY_EDITOR // Restore normals and tangents after assembly reload if they are set to DontModify because otherwise they become null automatically (i.e. information gets lost) if( mesh && originalMesh ) { if( m_normalsMode == VectorMode.DontModify ) mesh.normals = originalMesh.normals; if( m_tangentsMode == VectorMode.DontModify ) mesh.tangents = originalMesh.tangents; } EditorSceneManager.sceneSaving -= OnSceneSaving; EditorSceneManager.sceneSaving += OnSceneSaving; EditorSceneManager.sceneSaved -= OnSceneSaved; EditorSceneManager.sceneSaved += OnSceneSaved; #endif if( m_spline ) { m_spline.onSplineChanged -= OnSplineChanged; m_spline.onSplineChanged += OnSplineChanged; OnSplineChanged( m_spline, DirtyFlags.All ); } } private void OnDisable() { if( m_spline ) m_spline.onSplineChanged -= OnSplineChanged; #if UNITY_EDITOR EditorSceneManager.sceneSaving -= OnSceneSaving; EditorSceneManager.sceneSaved -= OnSceneSaved; if( !EditorApplication.isPlaying ) OnDestroy(); #endif } private void OnDestroy() { MeshFilter _meshFilter = meshFilter; meshFilter = null; if( _meshFilter && originalMesh ) _meshFilter.sharedMesh = originalMesh; if( mesh ) DestroyImmediate( mesh ); #if UNITY_EDITOR && UNITY_2018_3_OR_NEWER // This allows removing the 'modified' flag of Mesh Filter's Mesh property but these sorts of things // may cause new problems in edge cases so it is commented out //if( !EditorApplication.isPlaying && _meshFilter && originalMesh ) //{ // // Revert modified status of the prefab instance's MeshFilter Mesh if possible // MeshFilter prefabMeshFilter = null; // if( PrefabUtility.GetPrefabInstanceStatus( _meshFilter ) == PrefabInstanceStatus.Connected ) // prefabMeshFilter = PrefabUtility.GetCorrespondingObjectFromSource( _meshFilter ) as MeshFilter; // if( prefabMeshFilter && prefabMeshFilter.sharedMesh == originalMesh ) // PrefabUtility.RevertPropertyOverride( new SerializedObject( _meshFilter ).FindProperty( "m_Mesh" ), InteractionMode.AutomatedAction ); //} #endif } public void Activate() { enabled = true; } public void Deactivate() { OnDestroy(); enabled = false; } #if UNITY_EDITOR private void OnValidate() { EditorApplication.update -= OnValidateImplementation; EditorApplication.update += OnValidateImplementation; } // Calling this code inside OnValidate throws "SendMessage cannot be called during Awake, CheckConsistency, or OnValidate" warnings private void OnValidateImplementation() { EditorApplication.update -= OnValidateImplementation; if( !this ) return; BezierSpline _spline = m_spline; m_spline = prevSpline; spline = prevSpline = _spline; bool _highQuality = m_highQuality; m_highQuality = prevHighQuality; highQuality = prevHighQuality = _highQuality; VectorMode _normalsMode = m_normalsMode; m_normalsMode = prevNormalsMode; normalsMode = prevNormalsMode = _normalsMode; VectorMode _tangentsMode = m_tangentsMode; m_tangentsMode = prevTangentsMode; tangentsMode = prevTangentsMode = _tangentsMode; RecalculateVertexRange(); if( !executeInEditMode && !EditorApplication.isPlaying ) OnDestroy(); else if( isActiveAndEnabled ) OnSplineChanged( m_spline, DirtyFlags.All ); SceneView.RepaintAll(); } private void OnSceneSaving( UnityEngine.SceneManagement.Scene scene, string path ) { // Restore original mesh before saving the scene if( scene == gameObject.scene ) OnDestroy(); } private void OnSceneSaved( UnityEngine.SceneManagement.Scene scene ) { // Restore modified mesh after saving the scene if( scene == gameObject.scene && isActiveAndEnabled ) OnSplineChanged( m_spline, DirtyFlags.All ); } #endif private void OnSplineChanged( BezierSpline spline, DirtyFlags dirtyFlags ) { #if UNITY_EDITOR if( !executeInEditMode && !EditorApplication.isPlaying ) return; if( BuildPipeline.isBuildingPlayer ) return; #if UNITY_2018_3_OR_NEWER // Don't execute the script in prefab mode UnityEditor.SceneManagement.PrefabStage openPrefabStage = UnityEditor.SceneManagement.PrefabStageUtility.GetCurrentPrefabStage(); if( openPrefabStage != null && openPrefabStage.IsPartOfPrefabContents( gameObject ) ) return; #endif #endif if( m_autoRefresh && ( dirtyFlags & ( DirtyFlags.SplineShapeChanged | DirtyFlags.NormalsChanged ) ) != DirtyFlags.None ) Refresh(); } private void Initialize() { meshFilter = GetComponent(); if( meshFilter.sharedMesh ) // It can sometimes be null during undo&redo which causes issues originalMesh = meshFilter.sharedMesh; if( !originalMesh ) return; if( mesh ) DestroyImmediate( mesh ); mesh = Instantiate( originalMesh ); meshFilter.sharedMesh = mesh; #if UNITY_EDITOR if( !EditorApplication.isPlaying ) mesh.hideFlags = HideFlags.DontSave; #endif originalVertices = mesh.vertices; originalNormals = null; originalTangents = null; RecalculateVertexRange(); } private void RecalculateVertexRange() { if( originalVertices == null ) return; minVertex = float.PositiveInfinity; float maxVertex = float.NegativeInfinity; switch( m_bendAxis ) { case Axis.X: for( int i = 0; i < originalVertices.Length; i++ ) { float vertex = originalVertices[i].x; if( vertex < minVertex ) minVertex = originalVertices[i].x; if( vertex > maxVertex ) maxVertex = originalVertices[i].x; } break; case Axis.Y: for( int i = 0; i < originalVertices.Length; i++ ) { float vertex = originalVertices[i].y; if( vertex < minVertex ) minVertex = originalVertices[i].y; if( vertex > maxVertex ) maxVertex = originalVertices[i].y; } break; case Axis.Z: for( int i = 0; i < originalVertices.Length; i++ ) { float vertex = originalVertices[i].z; if( vertex < minVertex ) minVertex = originalVertices[i].z; if( vertex > maxVertex ) maxVertex = originalVertices[i].z; } break; } _1OverVertexRange = 1f / ( maxVertex - minVertex ); } public void Refresh() { if( !m_spline ) return; if( !meshFilter || ( meshFilter.sharedMesh && meshFilter.sharedMesh != mesh && meshFilter.sharedMesh != originalMesh ) ) Initialize(); if( !originalMesh ) return; if( vertices == null || vertices.Length != originalVertices.Length ) vertices = new Vector3[originalVertices.Length]; if( m_normalsMode == VectorMode.ModifyOriginals ) { if( originalNormals == null ) originalNormals = originalMesh.normals; if( originalNormals == null || originalNormals.Length < originalVertices.Length ) // If somehow above statement returned null normals = null; else if( normals == null || normals.Length != originalNormals.Length ) normals = new Vector3[originalNormals.Length]; } else normals = null; if( m_tangentsMode == VectorMode.ModifyOriginals ) { if( originalTangents == null ) originalTangents = originalMesh.tangents; if( originalTangents == null || originalTangents.Length < originalVertices.Length ) // If somehow above statement returned null tangents = null; else if( tangents == null || tangents.Length != originalTangents.Length ) tangents = new Vector4[originalTangents.Length]; } else tangents = null; Vector2 _splineSampleRange = m_splineSampleRange; if( m_invertDirection ) { float temp = _splineSampleRange.x; _splineSampleRange.x = _splineSampleRange.y; _splineSampleRange.y = temp; } bool isSampleRangeForwards = _splineSampleRange.x <= _splineSampleRange.y; float splineSampleLength = _splineSampleRange.y - _splineSampleRange.x; bool dontInvertModifiedVertexAttributes = ( m_thicknessMultiplier.x > 0f && m_thicknessMultiplier.y > 0f ); BezierSpline.EvenlySpacedPointsHolder evenlySpacedPoints = m_highQuality ? m_spline.evenlySpacedPoints : null; Vector3 initialPoint = m_spline.GetPoint( 0f ); for( int i = 0; i < originalVertices.Length; i++ ) { Vector3 vertex = originalVertices[i]; float vertexPosition; Vector3 vertexOffset; switch( m_bendAxis ) { case Axis.X: vertexPosition = vertex.x; vertexOffset = new Vector3( vertex.z * m_thicknessMultiplier.x, 0f, vertex.y * m_thicknessMultiplier.y ); break; case Axis.Y: default: vertexPosition = vertex.y; vertexOffset = new Vector3( vertex.x * m_thicknessMultiplier.x, 0f, vertex.z * m_thicknessMultiplier.y ); break; case Axis.Z: vertexPosition = vertex.z; vertexOffset = new Vector3( vertex.y * m_thicknessMultiplier.x, 0f, vertex.x * m_thicknessMultiplier.y ); break; } float normalizedT = _splineSampleRange.x + ( vertexPosition - minVertex ) * _1OverVertexRange * splineSampleLength; // Remap from [minVertex,maxVertex] to _splineSampleRange if( m_highQuality ) normalizedT = evenlySpacedPoints.GetNormalizedTAtPercentage( normalizedT ); BezierSpline.Segment segment = m_spline.GetSegmentAt( normalizedT ); Vector3 point = segment.GetPoint() - initialPoint; Vector3 tangent = isSampleRangeForwards ? segment.GetTangent() : -segment.GetTangent(); Quaternion rotation = Quaternion.AngleAxis( m_extraRotation, tangent ) * Quaternion.LookRotation( segment.GetNormal(), tangent ); Vector3 direction = rotation * vertexOffset; vertices[i] = point + direction; if( normals != null ) // The only case this happens is when Normals Mode is ModifyOriginals and the original mesh has normals normals[i] = rotation * ( dontInvertModifiedVertexAttributes ? originalNormals[i] : -originalNormals[i] ); if( tangents != null ) // The only case this happens is when Tangents Mode is ModifyOriginals and the original mesh has tangents { float tangentW = originalTangents[i].w; tangents[i] = rotation * ( dontInvertModifiedVertexAttributes ? originalTangents[i] : -originalTangents[i] ); tangents[i].w = tangentW; } } mesh.vertices = vertices; if( m_normalsMode == VectorMode.ModifyOriginals ) mesh.normals = normals; if( m_tangentsMode == VectorMode.ModifyOriginals ) mesh.tangents = tangents; if( m_normalsMode == VectorMode.RecalculateFromScratch ) { mesh.RecalculateNormals(); #if UNITY_EDITOR // Cache original normals so that we can reset normals in OnValidate when normals are reset back to DontModify if( originalNormals == null ) originalNormals = originalMesh.normals; #endif } if( m_tangentsMode == VectorMode.RecalculateFromScratch ) { mesh.RecalculateTangents(); #if UNITY_EDITOR // Cache original tangents so that we can reset tangents in OnValidate when tangents are reset back to DontModify if( originalTangents == null ) originalTangents = originalMesh.tangents; #endif } mesh.RecalculateBounds(); } } }