Skip to content

Instantly share code, notes, and snippets.

@Denchyaknow
Created December 28, 2024 03:26
Show Gist options
  • Select an option

  • Save Denchyaknow/e7cdf46d45c21b45cf82c1a402cf6c7d to your computer and use it in GitHub Desktop.

Select an option

Save Denchyaknow/e7cdf46d45c21b45cf82c1a402cf6c7d to your computer and use it in GitHub Desktop.
HowTo_BoxOutlineWithProceduralQuads
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();
}
}
}
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