Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- using System.Collections.Generic;
- using UnityEngine;
- using UnityEngine.Splines;
- using System.Linq;
- #if UNITY_EDITOR
- using UnityEditor;
- #endif
- /// <summary>
- /// A component for creating a flat-shaded extruded mesh from a Spline.
- /// </summary>
- [RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
- [AddComponentMenu("Splines/Flat Shaded Spline Extrude")]
- [ExecuteAlways]
- public class FlatShadedSplineExtrude : 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 = true;
- [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);
- [SerializeField, Tooltip("Rotation offset in degrees around the spline path axis.")]
- float m_RotationOffsetDegrees = 0f;
- // Normals debugging gizmos
- [SerializeField, Tooltip("Draw normals in the scene view.")]
- bool m_DebugDrawNormals = false;
- Mesh m_Mesh;
- bool m_RebuildRequested;
- float m_NextScheduledRebuild;
- /// <summary>The SplineContainer of the <see cref="Spline"/> to extrude.</summary>
- public SplineContainer Container
- {
- get => m_Container;
- set => m_Container = value;
- }
- /// <summary>
- /// Enable to regenerate the extruded mesh when the target Spline is modified. Disable this option if the Spline
- /// will not be modified at runtime.
- /// </summary>
- public bool RebuildOnSplineChange
- {
- get => m_RebuildOnSplineChange;
- set => m_RebuildOnSplineChange = value;
- }
- /// <summary>The maximum number of times per-second that the mesh will be rebuilt.</summary>
- public int RebuildFrequency
- {
- get => m_RebuildFrequency;
- set => m_RebuildFrequency = Mathf.Max(value, 1);
- }
- /// <summary>How many sides make up the radius of the mesh.</summary>
- public int Sides
- {
- get => m_Sides;
- set => m_Sides = Mathf.Max(value, 3);
- }
- /// <summary>How many edge loops comprise the one unit length of the mesh.</summary>
- public float SegmentsPerUnit
- {
- get => m_SegmentsPerUnit;
- set => m_SegmentsPerUnit = Mathf.Max(value, .0001f);
- }
- /// <summary>Whether the start and end of the mesh is filled. This setting is ignored when spline is closed.</summary>
- public bool Capped
- {
- get => m_Capped;
- set => m_Capped = value;
- }
- /// <summary>The radius of the extruded mesh.</summary>
- public float Radius
- {
- get => m_Radius;
- set => m_Radius = Mathf.Max(value, .00001f);
- }
- /// <summary>
- /// The section of the Spline to extrude.
- /// </summary>
- public Vector2 Range
- {
- get => m_Range;
- set => m_Range = new Vector2(Mathf.Min(value.x, value.y), Mathf.Max(value.x, value.y));
- }
- /// <summary>
- /// Rotation offset in degrees around the spline path axis.
- /// </summary>
- public float RotationOffsetDegrees
- {
- get => m_RotationOffsetDegrees;
- set => m_RotationOffsetDegrees = value;
- }
- /// <summary>The main Spline to extrude.</summary>
- public Spline Spline
- {
- get => m_Container?.Spline;
- }
- /// <summary>The Splines to extrude.</summary>
- public IReadOnlyList<Spline> Splines
- {
- get => m_Container?.Splines;
- }
- void Reset()
- {
- TryGetComponent(out m_Container);
- if (TryGetComponent<MeshFilter>(out var filter))
- filter.sharedMesh = m_Mesh = CreateMeshAsset();
- if (TryGetComponent<MeshRenderer>(out var renderer) && renderer.sharedMaterial == null)
- {
- // Create a default material
- var defaultMaterial = new Material(Shader.Find("Standard"));
- renderer.sharedMaterial = defaultMaterial;
- }
- Rebuild();
- }
- void Start()
- {
- #if UNITY_EDITOR
- if (EditorApplication.isPlaying)
- #endif
- {
- if (m_Container == null || m_Container.Spline == null || m_Container.Splines.Count == 0)
- return;
- if ((m_Mesh = GetComponent<MeshFilter>().sharedMesh) == null)
- return;
- }
- Rebuild();
- }
- static readonly string k_EmptyContainerError = "Spline Extrude does not have a valid SplineContainer set.";
- bool IsNullOrEmptyContainer()
- {
- var isNull = m_Container == null || m_Container.Spline == null || m_Container.Splines.Count == 0;
- if (isNull)
- {
- if (Application.isPlaying)
- Debug.LogError(k_EmptyContainerError, this);
- }
- return isNull;
- }
- static readonly string k_EmptyMeshFilterError = "SplineExtrude.createMeshInstance is disabled," +
- " but there is no valid mesh assigned. " +
- "Please create or assign a writable mesh asset.";
- bool IsNullOrEmptyMeshFilter()
- {
- var isNull = (m_Mesh = GetComponent<MeshFilter>().sharedMesh) == null;
- if (isNull)
- Debug.LogError(k_EmptyMeshFilterError, this);
- return isNull;
- }
- void OnEnable()
- {
- if (Spline != null)
- Spline.Changed += OnSplineChanged;
- }
- void OnDisable()
- {
- if (Spline != null)
- Spline.Changed -= OnSplineChanged;
- }
- void OnSplineChanged(Spline spline, int knotIndex, SplineModification modificationType)
- {
- if (m_Container != null && Splines != null && Splines.Contains(spline) && m_RebuildOnSplineChange)
- {
- if (Application.isPlaying)
- {
- m_RebuildRequested = true;
- }
- else
- {
- Rebuild();
- #if UNITY_EDITOR
- // Force the scene view to repaint
- SceneView.RepaintAll();
- #endif
- }
- }
- }
- void Update()
- {
- if (m_RebuildRequested && Time.time >= m_NextScheduledRebuild)
- {
- Rebuild();
- m_RebuildRequested = false;
- }
- }
- /// <summary>
- /// Triggers the rebuild of a Spline's extrusion mesh and collider.
- /// </summary>
- public void Rebuild()
- {
- if (IsNullOrEmptyContainer() || IsNullOrEmptyMeshFilter())
- return;
- ExtrudeMeshWithFlatShading();
- m_NextScheduledRebuild = Time.time + 1f / m_RebuildFrequency;
- #if UNITY_PHYSICS_MODULE
- if (m_UpdateColliders)
- {
- if (TryGetComponent<MeshCollider>(out var meshCollider))
- meshCollider.sharedMesh = m_Mesh;
- if (TryGetComponent<BoxCollider>(out var boxCollider))
- {
- boxCollider.center = m_Mesh.bounds.center;
- boxCollider.size = m_Mesh.bounds.size;
- }
- if (TryGetComponent<SphereCollider>(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);
- }
- }
- #endif
- }
- #if UNITY_EDITOR
- void OnValidate()
- {
- if (EditorApplication.isPlaying)
- return;
- Rebuild();
- }
- #endif
- Mesh CreateMeshAsset()
- {
- var mesh = new Mesh();
- mesh.name = name;
- #if UNITY_EDITOR
- var scene = UnityEngine.SceneManagement.SceneManager.GetActiveScene();
- var sceneDataDir = "Assets";
- if (!string.IsNullOrEmpty(scene.path))
- {
- var dir = System.IO.Path.GetDirectoryName(scene.path);
- sceneDataDir = $"{dir}/{System.IO.Path.GetFileNameWithoutExtension(scene.path)}";
- if (!System.IO.Directory.Exists(sceneDataDir))
- System.IO.Directory.CreateDirectory(sceneDataDir);
- }
- var path = AssetDatabase.GenerateUniqueAssetPath($"{sceneDataDir}/SplineExtrude_{mesh.name}.asset");
- AssetDatabase.CreateAsset(mesh, path);
- EditorGUIUtility.PingObject(mesh);
- #endif
- return mesh;
- }
- // Custom method to extrude mesh with flat shading
- void ExtrudeMeshWithFlatShading()
- {
- // Prepare variables
- Spline spline = m_Container.Spline;
- if (spline == null)
- return;
- List<Vector3> vertices = new List<Vector3>();
- List<int> triangles = new List<int>();
- List<Vector2> uvs = new List<Vector2>();
- List<Vector3> normals = new List<Vector3>();
- // Calculate total length and number of segments
- float splineLength = spline.GetLength();
- int totalSegments = Mathf.Max(1, Mathf.CeilToInt(splineLength * m_SegmentsPerUnit));
- // Adjust range
- float startRange = Mathf.Clamp01(m_Range.x);
- float endRange = Mathf.Clamp01(m_Range.y);
- // Calculate rotation offset in radians
- float rotationOffsetRadians = m_RotationOffsetDegrees * Mathf.Deg2Rad;
- // Generate vertices and triangles
- for (int i = 0; i < totalSegments; i++)
- {
- float t0 = Mathf.Lerp(startRange, endRange, (float)i / totalSegments);
- float t1 = Mathf.Lerp(startRange, endRange, (float)(i + 1) / totalSegments);
- Vector3 pos0 = spline.EvaluatePosition(t0);
- Vector3 pos1 = spline.EvaluatePosition(t1);
- Vector3 tangent0 = spline.EvaluateTangent(t0);
- Vector3 tangent1 = spline.EvaluateTangent(t1);
- Vector3 up0 = spline.EvaluateUpVector(t0);
- Vector3 up1 = spline.EvaluateUpVector(t1);
- Quaternion rot0 = Quaternion.LookRotation(tangent0, up0);
- Quaternion rot1 = Quaternion.LookRotation(tangent1, up1);
- for (int j = 0; j < m_Sides; j++)
- {
- float angle0 = ((float)j / m_Sides) * Mathf.PI * 2f + rotationOffsetRadians;
- float angle1 = ((float)(j + 1) / m_Sides) * Mathf.PI * 2f + rotationOffsetRadians;
- Vector3 offset0 = new Vector3(Mathf.Cos(angle0), Mathf.Sin(angle0), 0) * m_Radius;
- Vector3 offset1 = new Vector3(Mathf.Cos(angle1), Mathf.Sin(angle1), 0) * m_Radius;
- // Positions of the four corners of the face
- Vector3 v0 = pos0 + rot0 * offset0;
- Vector3 v1 = pos1 + rot1 * offset0;
- Vector3 v2 = pos1 + rot1 * offset1;
- Vector3 v3 = pos0 + rot0 * offset1;
- // Add vertices (duplicated for each face)
- int baseIndex = vertices.Count;
- vertices.Add(v0);
- vertices.Add(v1);
- vertices.Add(v2);
- vertices.Add(v3);
- // Triangles (corrected winding order)
- triangles.Add(baseIndex + 2);
- triangles.Add(baseIndex + 1);
- triangles.Add(baseIndex);
- triangles.Add(baseIndex + 3);
- triangles.Add(baseIndex + 2);
- triangles.Add(baseIndex);
- // Normal per face
- Vector3 faceNormal = Vector3.Cross(v1 - v0, v2 - v0).normalized * -1;
- normals.Add(faceNormal);
- normals.Add(faceNormal);
- normals.Add(faceNormal);
- normals.Add(faceNormal);
- // UVs
- uvs.Add(new Vector2(0, t0));
- uvs.Add(new Vector2(0, t1));
- uvs.Add(new Vector2(1, t1));
- uvs.Add(new Vector2(1, t0));
- }
- }
- // Generate end caps if required
- if (m_Capped && !spline.Closed)
- {
- GenerateEndCaps(vertices, normals, uvs, triangles, rotationOffsetRadians);
- }
- // Create the mesh
- if (m_Mesh == null)
- m_Mesh = CreateMeshAsset();
- m_Mesh.Clear();
- m_Mesh.SetVertices(vertices);
- m_Mesh.SetTriangles(triangles, 0);
- m_Mesh.SetNormals(normals);
- m_Mesh.SetUVs(0, uvs);
- m_Mesh.RecalculateBounds();
- GetComponent<MeshFilter>().sharedMesh = m_Mesh;
- }
- // Method to generate end caps with flat shading
- void GenerateEndCaps(List<Vector3> vertices, List<Vector3> normals, List<Vector2> uvs, List<int> triangles, float rotationOffsetRadians)
- {
- Spline spline = m_Container.Spline;
- // Start Cap
- {
- float t = Mathf.Clamp01(m_Range.x);
- Vector3 position = spline.EvaluatePosition(t);
- Vector3 tangent = spline.EvaluateTangent(t);
- Vector3 up = spline.EvaluateUpVector(t);
- Quaternion rotation = Quaternion.LookRotation(-tangent, up);
- int baseIndex = vertices.Count;
- // Center vertex
- vertices.Add(position);
- normals.Add(-tangent.normalized);
- uvs.Add(new Vector2(0.5f, 0.5f));
- // Rim vertices
- for (int i = 0; i < m_Sides; i++)
- {
- float angle = ((float)i / m_Sides) * Mathf.PI * 2f - rotationOffsetRadians;
- Vector3 offset = new Vector3(Mathf.Cos(angle), Mathf.Sin(angle), 0) * m_Radius;
- Vector3 v = position + rotation * offset;
- vertices.Add(v);
- normals.Add(-tangent.normalized);
- uvs.Add(new Vector2(Mathf.Cos(angle) * 0.5f + 0.5f, Mathf.Sin(angle) * 0.5f + 0.5f));
- }
- // Triangles
- // Triangles
- for (int i = 0; i < m_Sides; i++)
- {
- int nextI = (i + 1) % m_Sides;
- triangles.Add(baseIndex);
- triangles.Add(baseIndex + i + 1);
- triangles.Add(baseIndex + nextI + 1);
- }
- }
- // End Cap
- {
- float t = Mathf.Clamp01(m_Range.y);
- Vector3 position = spline.EvaluatePosition(t);
- Vector3 tangent = spline.EvaluateTangent(t);
- Vector3 up = spline.EvaluateUpVector(t);
- Quaternion rotation = Quaternion.LookRotation(tangent, up);
- int baseIndex = vertices.Count;
- // Center vertex
- vertices.Add(position);
- normals.Add(tangent.normalized);
- uvs.Add(new Vector2(0.5f, 0.5f));
- // Rim vertices
- for (int i = 0; i < m_Sides; i++)
- {
- float angle = ((float)i / m_Sides) * Mathf.PI * 2f + rotationOffsetRadians;
- Vector3 offset = new Vector3(Mathf.Cos(angle), Mathf.Sin(angle), 0) * m_Radius;
- Vector3 v = position + rotation * offset;
- vertices.Add(v);
- normals.Add(tangent.normalized);
- uvs.Add(new Vector2(Mathf.Cos(angle) * 0.5f + 0.5f, Mathf.Sin(angle) * 0.5f + 0.5f));
- }
- // Triangles (corrected winding order)
- for (int i = 0; i < m_Sides; i++)
- {
- int nextI = (i + 1) % m_Sides;
- triangles.Add(baseIndex);
- triangles.Add(baseIndex + i + 1);
- triangles.Add(baseIndex + nextI + 1);
- }
- }
- }
- void OnDrawGizmos()
- {
- if (m_Mesh == null) return;
- if(m_DebugDrawNormals == false) return;
- // Draw normals in the scene view
- for (int i = 0; i < m_Mesh.vertexCount; i++)
- {
- Gizmos.color = Color.yellow;
- // Get the vertex and normal from the mesh
- Vector3 vertex = transform.TransformPoint(m_Mesh.vertices[i]); // Transform to world space
- Vector3 normal = m_Mesh.normals[i];
- // Draw the normal starting from the vertex
- Gizmos.DrawLine(vertex, vertex + normal * 0.2f); // Scale for visibility
- }
- }
- }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement