Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Implementing Interactive Area and Angle Measurement Tools in Unity

Tech May 9 3

Prerequisites

Prepare the following prefabs before implementing the measurement systems:

  • Vertex Marker: A small sphere to indicate clicked positions in world space
  • Edge Renderer: An empty GameObject with a LineRenderer component attached
  • Measurement Container: An empty parent Transform to organize generated elements hierarchically
  • Angle Label (for angle tool): A 3D TextMesh for displaying values in the scene

Polygon Area Measurement

This system captures user input to construct a closed polygon, then calculates the surface area using vector cross products.

Area Calculator Implementation

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;

public class PolygonAreaTool : MonoBehaviour
{
    [Header("Assets")]
    public GameObject vertexMarker;
    public LineRenderer perimeterRenderer;
    public Transform container;
    
    [Header("Configuration")]
    public float verticalOffset = 0.05f;
    public int decimalPrecision = 1;
    
    private bool isActive;
    private List<Vector3> boundaryPoints = new List<Vector3>();
    private LineRenderer currentPerimeter;
    private GUIStyle hudStyle;

    void Start()
    {
        hudStyle = new GUIStyle { fontSize = 28, fontStyle = FontStyle.Bold };
    }

    public void Activate() => isActive = true;
    
    public void DeactivateAndClear()
    {
        isActive = false;
        boundaryPoints.Clear();
        
        for (int i = container.childCount - 1; i >= 0; i--)
            Destroy(container.GetChild(i).gameObject);
    }

    void Update()
    {
        if (!isActive || EventSystem.current.IsPointerOverGameObject())
            return;

        if (Input.GetMouseButtonUp(0))
        {
            RaycastHit hitInfo;
            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            
            if (Physics.Raycast(ray, out hitInfo))
            {
                AddPolygonVertex(hitInfo.point);
            }
        }
    }

    void AddPolygonVertex(Vector3 surfacePoint)
    {
        Vector3 elevatedPoint = surfacePoint + Vector3.up * verticalOffset;
        
        Instantiate(vertexMarker, elevatedPoint, Quaternion.identity, container);
        
        if (boundaryPoints.Count == 0)
        {
            currentPerimeter = Instantiate(perimeterRenderer, container);
        }
        
        boundaryPoints.Add(elevatedPoint);
        UpdateVisualRepresentation();
    }

    void UpdateVisualRepresentation()
    {
        if (boundaryPoints.Count < 3)
        {
            currentPerimeter.positionCount = boundaryPoints.Count;
            currentPerimeter.SetPositions(boundaryPoints.ToArray());
            return;
        }
        
        // Close the loop for visualization
        Vector3[] closedPath = new Vector3[boundaryPoints.Count + 1];
        boundaryPoints.CopyTo(closedPath);
        closedPath[closedPath.Length - 1] = boundaryPoints[0];
        
        currentPerimeter.positionCount = closedPath.Length;
        currentPerimeter.SetPositions(closedPath);
    }

    void OnGUI()
    {
        if (boundaryPoints.Count < 3) return;
        
        Vector3 center = GetCentroid(boundaryPoints);
        Vector3 screenLocation = Camera.main.WorldToScreenPoint(center);
        
        float area = CalculateSurfaceArea(boundaryPoints);
        string displayText = $"<color=#FF6B6B>{area.ToString($"F{decimalPrecision}")} m²</color>";
        
        Rect labelRect = new Rect(screenLocation.x - 60, Screen.height - screenLocation.y - 15, 120, 30);
        GUI.Label(labelRect, displayText, hudStyle);
    }

    Vector3 GetCentroid(List<Vector3> vertices)
    {
        Vector3 accumulator = Vector3.zero;
        foreach (Vector3 v in vertices)
            accumulator += v;
        return accumulator / vertices.Count;
    }

    float CalculateSurfaceArea(List<Vector3> vertices)
    {
        int n = vertices.Count;
        if (n < 3) return 0f;

        // Calculate normal using Newell's method for robustness
        Vector3 normal = Vector3.zero;
        for (int i = 0; i < n; i++)
        {
            Vector3 current = vertices[i];
            Vector3 next = vertices[(i + 1) % n];
            normal.x += (current.y - next.y) * (current.z + next.z);
            normal.y += (current.z - next.z) * (current.x + next.x);
            normal.z += (current.x - next.x) * (current.y + next.y);
        }
        normal.Normalize();

        // Project onto plane perpendicular to normal and apply shoelace formula
        float totalArea = 0f;
        for (int i = 0; i < n; i++)
        {
            Vector3 v1 = vertices[i];
            Vector3 v2 = vertices[(i + 1) % n];
            totalArea += Vector3.Dot(normal, Vector3.Cross(v1, v2));
        }
        
        return Mathf.Abs(totalArea) * 0.5f;
    }
}

Surface Measurement Adjustment

When measuring on horizontal planes without obstruction, eliminate the vertical offset to prevent floating artfiacts:

// Set verticalOffset to 0 in the inspector for ground-level measurements
// Or modify the elevation logic:
Vector3 measurementPoint = surfacePoint; // No offset for flat surfaces

Vector Angle Measurement

Angle measurement requires three discrete clicks to define a vertex and two connected rays.

Angle Calculator Implementation

using UnityEngine;
using UnityEngine.EventSystems;

public class AngleMeasurementTool : MonoBehaviour
{
    public GameObject markerPrefab;
    public LineRenderer rayVisualizer;
    public TextMesh floatingLabel;
    public Transform measurementRoot;
    
    private enum MeasurementState { Idle, FirstRay, SecondRay }
    private MeasurementState currentState = MeasurementState.Idle;
    
    private Transform originMarker;
    private Transform pivotMarker;
    private Transform terminalMarker;
    private LineRenderer activeRays;
    private TextMesh currentLabel;

    void Update()
    {
        if (EventSystem.current.IsPointerOverGameObject()) return;
        
        HandleInput();
        UpdateDynamicPreview();
    }

    void HandleInput()
    {
        if (!Input.GetMouseButtonUp(0)) return;
        
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        if (!Physics.Raycast(ray, out RaycastHit hit)) return;
        
        switch (currentState)
        {
            case MeasurementState.Idle:
                StartMeasurement(hit.point);
                break;
            case MeasurementState.FirstRay:
                SetPivotPoint(hit.point);
                break;
            case MeasurementState.SecondRay:
                CompleteMeasurement(hit.point);
                break;
        }
    }

    void StartMeasurement(Vector3 position)
    {
        originMarker = Instantiate(markerPrefab, position, Quaternion.identity, measurementRoot).transform;
        
        activeRays = Instantiate(rayVisualizer, measurementRoot);
        activeRays.positionCount = 2;
        activeRays.SetPosition(0, position);
        
        currentState = MeasurementState.FirstRay;
    }

    void SetPivotPoint(Vector3 position)
    {
        pivotMarker = Instantiate(markerPrefab, position, Quaternion.identity, measurementRoot).transform;
        
        activeRays.positionCount = 3;
        activeRays.SetPosition(1, position);
        
        currentLabel = Instantiate(floatingLabel, position + Vector3.up * 0.3f, Quaternion.identity, measurementRoot);
        
        currentState = MeasurementState.SecondRay;
    }

    void CompleteMeasurement(Vector3 position)
    {
        terminalMarker = Instantiate(markerPrefab, position, Quaternion.identity, measurementRoot).transform;
        activeRays.SetPosition(2, position);
        
        ComputeAndDisplayAngle();
        currentState = MeasurementState.Idle;
    }

    void UpdateDynamicPreview()
    {
        if (currentState == MeasurementState.Idle) return;
        
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        if (!Physics.Raycast(ray, out RaycastHit hit)) return;
        
        if (currentState == MeasurementState.FirstRay)
        {
            activeRays.SetPosition(1, hit.point);
        }
        else if (currentState == MeasurementState.SecondRay)
        {
            activeRays.SetPosition(2, hit.point);
            
            Vector3 fromOrigin = (originMarker.position - pivotMarker.position).normalized;
            Vector3 toCursor = (hit.point - pivotMarker.position).normalized;
            
            float angle = Vector3.Angle(fromOrigin, toCursor);
            currentLabel.text = $"{angle:F0}°";
        }
    }

    void ComputeAndDisplayAngle()
    {
        Vector3 vectorA = (originMarker.position - pivotMarker.position).normalized;
        Vector3 vectorB = (terminalMarker.position - pivotMarker.position).normalized;
        
        float degrees = Vector3.Angle(vectorA, vectorB);
        currentLabel.text = $"{degrees:F1}°";
    }

    public void ClearAll()
    {
        currentState = MeasurementState.Idle;
        foreach (Transform child in measurementRoot)
            Destroy(child.gameObject);
    }
}

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.