using System; using System.IO; using UnityEngine; namespace UnityEngine.Splines { /// /// A component for creating a tube mesh from a Spline at runtime. /// [RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))] [AddComponentMenu("Splines/Spline Extrude")] public class SplineExtrude : MonoBehaviour { [SerializeField, Tooltip("The Spline to extrude.")] SplineContainer m_Container; [SerializeField, Tooltip("Enable to regenerate the extruded mesh when the target Spline is modified. Disable " + "this option if the Spline will not be modified at runtime.")] bool m_RebuildOnSplineChange; [SerializeField, Tooltip("The maximum number of times per-second that the mesh will be rebuilt.")] int m_RebuildFrequency = 30; [SerializeField, Tooltip("Automatically update any Mesh, Box, or Sphere collider components when the mesh is extruded.")] bool m_UpdateColliders = true; [SerializeField, Tooltip("The number of sides that comprise the radius of the mesh.")] int m_Sides = 8; [SerializeField, Tooltip("The number of edge loops that comprise the length of one unit of the mesh. The " + "total number of sections is equal to \"Spline.GetLength() * segmentsPerUnit\".")] float m_SegmentsPerUnit = 4; [SerializeField, Tooltip("Indicates if the start and end of the mesh are filled. When the Spline is closed this setting is ignored.")] bool m_Capped = true; [SerializeField, Tooltip("The radius of the extruded mesh.")] float m_Radius = .25f; [SerializeField, Tooltip("The section of the Spline to extrude.")] Vector2 m_Range = new Vector2(0f, 1f); Spline m_Spline; Mesh m_Mesh; bool m_RebuildRequested; float m_NextScheduledRebuild; /// The SplineContainer of the to extrude. public SplineContainer container { get => m_Container; set => m_Container = value; } /// /// Enable to regenerate the extruded mesh when the target Spline is modified. Disable this option if the Spline /// will not be modified at runtime. /// public bool rebuildOnSplineChange { get => m_RebuildOnSplineChange; set => m_RebuildOnSplineChange = value; } /// The maximum number of times per-second that the mesh will be rebuilt. public int rebuildFrequency { get => m_RebuildFrequency; set => m_RebuildFrequency = Mathf.Max(value, 1); } /// How many sides make up the radius of the mesh. public int sides { get => m_Sides; set => m_Sides = Mathf.Max(value, 3); } /// How many edge loops comprise the one unit length of the mesh. public float segmentsPerUnit { get => m_SegmentsPerUnit; set => m_SegmentsPerUnit = Mathf.Max(value, .0001f); } /// Whether the start and end of the mesh is filled. This setting is ignored when spline is closed. public bool capped { get => m_Capped; set => m_Capped = value; } /// The radius of the extruded mesh. public float radius { get => m_Radius; set => m_Radius = Mathf.Max(value, .00001f); } /// /// The section of the Spline to extrude. /// public Vector2 range { get => m_Range; set => m_Range = new Vector2(Mathf.Min(value.x, value.y), Mathf.Max(value.x, value.y)); } /// The Spline to extrude. public Spline spline { get { // m_Spline is cached in the Start() method, meaning that it is not valid in the Editor. #if UNITY_EDITOR return m_Container.Spline; #else return m_Spline; #endif } } void Reset() { TryGetComponent(out m_Container); if (TryGetComponent(out var filter) && filter.sharedMesh != null) m_Mesh = filter.sharedMesh; else filter.sharedMesh = m_Mesh = CreateMeshAsset(); if (TryGetComponent(out var renderer) && renderer.sharedMaterial == null) { // todo Make Material.GetDefaultMaterial() public var cube = GameObject.CreatePrimitive(PrimitiveType.Cube); var mat = cube.GetComponent().sharedMaterial; DestroyImmediate(cube); renderer.sharedMaterial = mat; } Rebuild(); } void Start() { if (m_Container == null || (m_Spline = m_Container.Spline) == null) { Debug.LogError("Spline Extrude does not have a valid SplineContainer set."); return; } if((m_Mesh = GetComponent().sharedMesh) == null) Debug.LogError("SplineExtrude.createMeshInstance is disabled, but there is no valid mesh assigned. " + "Please create or assign a writable mesh asset."); if (m_RebuildOnSplineChange) m_Spline.changed += () => m_RebuildRequested = true; Rebuild(); } void Update() { if(m_RebuildRequested && Time.time >= m_NextScheduledRebuild) Rebuild(); } /// /// Triggers the rebuild of a Spline's extrusion mesh and collider. /// public void Rebuild() { if(m_Mesh == null && (m_Mesh = GetComponent().sharedMesh) == null) return; float span = Mathf.Abs(range.y - range.x); int segments = Mathf.Max((int)Mathf.Ceil(spline.GetLength() * span * m_SegmentsPerUnit), 1); SplineMesh.Extrude(spline, m_Mesh, m_Radius, m_Sides, segments, m_Capped, m_Range); m_NextScheduledRebuild = Time.time + 1f / m_RebuildFrequency; if (m_UpdateColliders) { if (TryGetComponent(out var meshCollider)) meshCollider.sharedMesh = m_Mesh; if (TryGetComponent(out var boxCollider)) { boxCollider.center = m_Mesh.bounds.center; boxCollider.size = m_Mesh.bounds.size; } if (TryGetComponent(out var sphereCollider)) { sphereCollider.center = m_Mesh.bounds.center; var ext = m_Mesh.bounds.extents; sphereCollider.radius = Mathf.Max(ext.x, ext.y, ext.z); } } } void OnValidate() { Rebuild(); } internal Mesh CreateMeshAsset() { var mesh = new Mesh(); mesh.name = name; #if UNITY_EDITOR var scene = SceneManagement.SceneManager.GetActiveScene(); var sceneDataDir = "Assets"; if (!string.IsNullOrEmpty(scene.path)) { var dir = Path.GetDirectoryName(scene.path); sceneDataDir = $"{dir}/{Path.GetFileNameWithoutExtension(scene.path)}"; if (!Directory.Exists(sceneDataDir)) Directory.CreateDirectory(sceneDataDir); } var path = UnityEditor.AssetDatabase.GenerateUniqueAssetPath($"{sceneDataDir}/{mesh.name}.asset"); UnityEditor.AssetDatabase.CreateAsset(mesh, path); mesh = UnityEditor.AssetDatabase.LoadAssetAtPath(path); UnityEditor.EditorGUIUtility.PingObject(mesh); #endif return mesh; } } }