Created
December 28, 2024 03:26
-
-
Save Denchyaknow/e7cdf46d45c21b45cf82c1a402cf6c7d to your computer and use it in GitHub Desktop.
HowTo_BoxOutlineWithProceduralQuads
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| public class BoxOutlineFace : MonoBehaviour | |
| { | |
| MeshFilter meshFilter; | |
| Mesh mesh; | |
| bool isDirty = false; | |
| Vector3[] vertices; | |
| Vector3[] normals; | |
| Vector2[] uv; | |
| int[] triangles; | |
| private void Awake() | |
| { | |
| ValidateMesh(); | |
| } | |
| private void OnEnable() | |
| { | |
| isDirty = true; | |
| } | |
| void Update() | |
| { | |
| if (isDirty) | |
| { | |
| isDirty = false; | |
| UpdateMesh(mesh); | |
| } | |
| } | |
| void ValidateMesh() | |
| { | |
| // Attach to mesh and filter | |
| if (meshFilter == null || mesh == null) | |
| { | |
| meshFilter = GetComponent<MeshFilter>(); | |
| mesh = meshFilter.mesh = new(); | |
| } | |
| // Assign shared mesh to collider if present | |
| if (TryGetComponent(out MeshCollider collider)) | |
| collider.sharedMesh = mesh; | |
| UpdateMesh(mesh); | |
| } | |
| [Header("Sizing")] | |
| [SerializeField] float width = 1.0f; | |
| [SerializeField] float height = 1.0f; | |
| [SerializeField] float lineThickness = 0.1f; | |
| [Header("Texturing")] | |
| [SerializeField] bool createUV = true; | |
| internal void SetThickness(float thickness) | |
| { | |
| lineThickness = thickness; | |
| isDirty = true; | |
| if (!Application.isPlaying) | |
| { | |
| isDirty = false; | |
| UpdateMesh(mesh); | |
| } | |
| } | |
| internal void SetSize(float quadWidth, float quadHeight, float thickness) | |
| { | |
| width = quadWidth; | |
| height = quadHeight; | |
| lineThickness = thickness; | |
| isDirty = true; | |
| if(!Application.isPlaying) | |
| { | |
| isDirty = false; | |
| UpdateMesh(mesh); | |
| } | |
| } | |
| void UpdateMesh(Mesh mesh) | |
| { | |
| // Outer corners | |
| // (-width/2, height/2), ( width/2, height/2), | |
| // ( width/2, -height/2),(-width/2, -height/2) | |
| // Inner corners are offset inward by lineThickness. | |
| float halfWidth = width * 0.5f; | |
| float halfHeight = height * 0.5f; | |
| // Prevent inner rectangle from inverting if lineThickness is too large | |
| float innerHalfWidth = Mathf.Max(0, halfWidth - lineThickness); | |
| float innerHalfHeight = Mathf.Max(0, halfHeight - lineThickness); | |
| // We’ll store 4 corners for the outer ring and 4 for the inner ring. | |
| // That’s 8 vertices total if single‐sided, or 16 if double‐sided. | |
| int ringVertexCount = 4; | |
| int totalRingCount = 1;//doubleSided ? 2 : 1; | |
| int vCount = ringVertexCount * 2 * totalRingCount; // outer + inner, possibly doubled | |
| // Each “ring” is effectively a loop of 4 corners. Connecting outer/inner forms 4 quads => 8 triangles. | |
| // Single‐sided => 8 triangles => 24 indices. Double‐sided => double that => 48 indices. | |
| int triCount = ringVertexCount * 2 * totalRingCount; | |
| int indexCount = triCount * 3; | |
| // Create arrays | |
| if (vertices == null || vertices.Length != vCount) | |
| { | |
| vertices = new Vector3[vCount]; | |
| normals = new Vector3[vCount]; | |
| uv = new Vector2[vCount]; | |
| } | |
| if (triangles == null || triangles.Length != indexCount) | |
| { | |
| triangles = new int[indexCount]; | |
| } | |
| // Define 4 corners (outer ring) | |
| Vector3[] outerCorners = new Vector3[4]; | |
| outerCorners[0] = new Vector3(-halfWidth, halfHeight, 0f); // top‐left | |
| outerCorners[1] = new Vector3(halfWidth, halfHeight, 0f); // top‐right | |
| outerCorners[2] = new Vector3(halfWidth, -halfHeight, 0f); // bottom‐right | |
| outerCorners[3] = new Vector3(-halfWidth, -halfHeight, 0f); // bottom‐left | |
| // Define 4 corners (inner ring) offset inward by lineThickness | |
| Vector3[] innerCorners = new Vector3[4]; | |
| innerCorners[0] = new Vector3(-innerHalfWidth, innerHalfHeight, 0f); | |
| innerCorners[1] = new Vector3(innerHalfWidth, innerHalfHeight, 0f); | |
| innerCorners[2] = new Vector3(innerHalfWidth, -innerHalfHeight, 0f); | |
| innerCorners[3] = new Vector3(-innerHalfWidth, -innerHalfHeight, 0f); | |
| // For building UV, we can do a simple [0..1] mapping across the outer extents | |
| Func<Vector3, Vector2> getUV = (pos) => { | |
| float u = (pos.x + halfWidth) / width; | |
| float v = (pos.y + halfHeight) / height; | |
| return new Vector2(u, v); | |
| }; | |
| // Fill front vertices | |
| int frontOuterStart = 0; | |
| int frontInnerStart = 4; // ringVertexCount | |
| for (int i = 0; i < 4; i++) | |
| { | |
| vertices[frontOuterStart + i] = outerCorners[i]; | |
| vertices[frontInnerStart + i] = innerCorners[i]; | |
| normals[frontOuterStart + i] = -Vector3.forward; | |
| normals[frontInnerStart + i] = -Vector3.forward; | |
| if (createUV) | |
| { | |
| uv[frontOuterStart + i] = getUV(outerCorners[i]); | |
| uv[frontInnerStart + i] = getUV(innerCorners[i]); | |
| } | |
| } | |
| int backOuterStart = 8; // ringVertexCount * 2 | |
| int backInnerStart = 12; // ringVertexCount * 3 | |
| // Build triangles | |
| // We'll connect corners in a loop: 0->1->2->3->(wrap to0). | |
| // For each edge of outer ring, connect to inner ring => 2 triangles per edge. | |
| int triIndex = 0; | |
| Action<int, int> buildQuads = (localIndexStart, ringIndexOffset) => { | |
| // 4 corners => edges i=0..3 | |
| for (int i = 0; i < 4; i++) | |
| { | |
| int iNext = (i + 1) % 4; | |
| int outerA = localIndexStart + i; | |
| int outerB = localIndexStart + iNext; | |
| int innerA = localIndexStart + ringIndexOffset + i; | |
| int innerB = localIndexStart + ringIndexOffset + iNext; | |
| // 2 triangles per edge | |
| triangles[triIndex + 0] = outerA; | |
| triangles[triIndex + 1] = outerB; | |
| triangles[triIndex + 2] = innerB; | |
| triangles[triIndex + 3] = outerA; | |
| triangles[triIndex + 4] = innerB; | |
| triangles[triIndex + 5] = innerA; | |
| triIndex += 6; | |
| } | |
| }; | |
| // Build front side | |
| buildQuads(0, 4); | |
| // Assign mesh | |
| mesh.Clear(); | |
| mesh.vertices = vertices; | |
| mesh.normals = normals; | |
| mesh.uv = uv; | |
| mesh.triangles = triangles; | |
| mesh.RecalculateBounds(); | |
| mesh.RecalculateNormals(); | |
| } | |
| private void OnDrawGizmosSelected() | |
| { | |
| if (!Application.isPlaying) | |
| { | |
| ValidateMesh(); | |
| } | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| public class BoxOutlineMesh : MonoBehaviour | |
| { | |
| //Vars for Calcs and serialized refs for handles | |
| public Action OutlineUpdated;//Called when the box oulines shape is updated | |
| Vector3[] corners = new Vector3[8]; | |
| Vector3[] originalCorners = new Vector3[8]; | |
| [Header("Setup Required")] | |
| [SerializeField] BoxOutlineFace[] faces = new BoxOutlineFace[6]; | |
| [SerializeField] PinchHandle[] pinchHandles = new PinchHandle[8]; // Must be length 8 | |
| Vector3 boundsMin = Vector3.one * float.MaxValue; | |
| Vector3 boundsMax = Vector3.one * float.MinValue; | |
| [SerializeField] Vector3 size = Vector3.zero; | |
| [SerializeField, Min(0.002f)] float edgeThickness = 0.03f; | |
| void CalculateBoundsByHandles() | |
| { | |
| boundsMin = Vector3.one * float.MaxValue; | |
| boundsMax = Vector3.one * float.MinValue; | |
| //We know that pinchHandles are children of this transform | |
| //So we can just loop through them and get the min and max dimensions the box should be | |
| for (int i = 0; i < pinchHandles.Length; i++) | |
| { | |
| Transform handle = pinchHandles[i].transform; | |
| // Update boundsMin and boundsMax based on the position of the current handle | |
| Vector3 position = handle.localPosition;// - transform.position); | |
| boundsMin = Vector3.Min(boundsMin, position); | |
| boundsMax = Vector3.Max(boundsMax, position); | |
| } | |
| size = boundsMax - boundsMin; | |
| } | |
| void UpdateFaces() | |
| { | |
| // Update the corners array | |
| for (int i = 0; i < pinchHandles.Length; i++) | |
| { | |
| corners[i] = pinchHandles[i].transform.localPosition; | |
| } | |
| // Update the faces | |
| for (int i = 0; i < faces.Length; i++) | |
| { | |
| // Adjust size based on rotation | |
| Vector3 faceSize = Vector3.zero; | |
| if (i == 0 || i == 1) // Front and Back | |
| faceSize = new Vector3(size.x, size.y, 0); | |
| else if (i == 2 || i == 3) // Left and Right | |
| faceSize = new Vector3(size.z, size.y, 0); | |
| else if (i == 4 || i == 5) // Top and Bottom | |
| faceSize = new Vector3(size.x, size.z, 0); | |
| // Set size for the face | |
| faces[i].SetSize(faceSize.x, faceSize.y, edgeThickness); | |
| // Determine the rotation for each face | |
| Quaternion faceRotation = Quaternion.identity; | |
| Vector3 facePosition = Vector3.zero; | |
| switch (i) | |
| { | |
| case 0: // Front face | |
| faceRotation = transform.rotation * Quaternion.Euler(0, 0, 0); | |
| facePosition = new Vector3(0, 0, size.z / 2); | |
| break; | |
| case 1: // Back face | |
| faceRotation = transform.rotation * Quaternion.Euler(0, 180, 0); | |
| facePosition = new Vector3(0, 0, -size.z / 2); | |
| break; | |
| case 2: // Left face | |
| faceRotation = transform.rotation * Quaternion.Euler(0, -90, 0); | |
| facePosition = new Vector3(-size.x / 2, 0, 0); | |
| break; | |
| case 3: // Right face | |
| faceRotation = transform.rotation * Quaternion.Euler(0, 90, 0); | |
| facePosition = new Vector3(size.x / 2, 0, 0); | |
| break; | |
| case 4: // Top face | |
| faceRotation = transform.rotation * Quaternion.Euler(-90, 0, 0); | |
| facePosition = new Vector3(0, size.y / 2, 0); | |
| break; | |
| case 5: // Bottom face | |
| faceRotation = transform.rotation * Quaternion.Euler(90, 0, 0); | |
| facePosition = new Vector3(0, -size.y / 2, 0); | |
| break; | |
| default: | |
| Debug.LogError("Invalid face index: " + i); | |
| break; | |
| } | |
| // Apply the rotation to the face | |
| faces[i].transform.rotation = faceRotation; | |
| faces[i].transform.localPosition = facePosition; | |
| } | |
| } | |
| [SerializeField] bool debugHandles = false; | |
| private void OnDrawGizmosSelected() | |
| { | |
| Gizmos.color = Color.yellow; | |
| if (!Application.isPlaying) | |
| { | |
| if (debugHandles || size == Vector3.zero) | |
| CalculateBoundsByHandles(); | |
| UpdateFaces(); | |
| } | |
| var center = transform.position; | |
| var rot = transform.rotation; | |
| var gizmoLength = 0.085f; | |
| var leftEdge = center + (-transform.right * size.x * 0.5f); | |
| var rightEdge = center + (transform.right * size.x * 0.5f); | |
| var bottomEdge = center + (-transform.up * size.y * 0.5f); | |
| var topEdge = center + (transform.up * size.y * 0.5f); | |
| var backEdge = center + (-transform.forward * size.z * 0.5f); | |
| var frontEdge = center + (transform.forward * size.z * 0.5f); | |
| Gizmos.color = Color.red; | |
| Gizmos.DrawLine(leftEdge, rightEdge); | |
| Gizmos.DrawLine(leftEdge + (rot * Vector3.up * gizmoLength), leftEdge + (rot * Vector3.down * gizmoLength)); | |
| Gizmos.DrawLine(rightEdge + (rot * Vector3.up * gizmoLength), rightEdge + (rot * Vector3.down * gizmoLength)); | |
| Gizmos.DrawLine(leftEdge + (rot * Vector3.forward * gizmoLength), leftEdge + (rot * Vector3.back * gizmoLength)); | |
| Gizmos.DrawLine(rightEdge + (rot * Vector3.forward * gizmoLength), rightEdge + (rot * Vector3.back * gizmoLength)); | |
| Gizmos.color = Color.green; | |
| Gizmos.DrawLine(topEdge, bottomEdge); | |
| Gizmos.DrawLine(topEdge + (rot * Vector3.left * gizmoLength), topEdge + (rot * Vector3.right * gizmoLength)); | |
| Gizmos.DrawLine(topEdge + (rot * Vector3.forward * gizmoLength), topEdge + (rot * Vector3.back * gizmoLength)); | |
| Gizmos.DrawLine(bottomEdge + (rot * Vector3.left * gizmoLength), bottomEdge + (rot * Vector3.right * gizmoLength)); | |
| Gizmos.DrawLine(bottomEdge + (rot * Vector3.forward * gizmoLength), bottomEdge + (rot * Vector3.back * gizmoLength)); | |
| Gizmos.color = Color.blue; | |
| Gizmos.DrawLine(frontEdge, backEdge); | |
| Gizmos.DrawLine(frontEdge + (rot * Vector3.left * gizmoLength), frontEdge + (rot * Vector3.right * gizmoLength)); | |
| Gizmos.DrawLine(frontEdge + (rot * Vector3.up * gizmoLength), frontEdge + (rot * Vector3.down * gizmoLength)); | |
| Gizmos.DrawLine(backEdge + (rot * Vector3.left * gizmoLength), backEdge + (rot * Vector3.right * gizmoLength)); | |
| Gizmos.DrawLine(backEdge + (rot * Vector3.up * gizmoLength), backEdge + (rot * Vector3.down * gizmoLength)); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment