back to index

drawing festival layouts in vr

Problem: A VR environment is useless if designers cannot draw in it. Festival layouts need polylines for fencing, circles for tents, rectangles for stages, and arbitrary polygons for vendor areas. All of these need real-time mesh generation, precise snapping, and immediate export to CAD software.

Solution: A shape system with ear-clipping concave polygon triangulation, a command-pattern tool system for VR controllers, and configurable snapping that maps VR coordinates to real-world precision.

Shape System

The Shape class is the base for all drawable primitives. It stores a list of 3D points in local coordinates relative to a pivot, plus metadata for color, type, and CAD export. The factory method creates the right subclass based on type.

public static Shape GetShape(ShapeController c, Vector3 pivot, 
    Type t, ColorType color, bool assignNewID = true)
{
    switch (t)
    {
        case Shape.Type.PolyLine:
            return new PolyLine(c, pivot, color, assignNewID);
        case Shape.Type.Circle:
            return new Circle(c, pivot, color, assignNewID);
        case Shape.Type.Rectangle:
            return new Rectangle(c, pivot, color, assignNewID);
        default:
            return new Shape(c, pivot, color, assignNewID);
    }
}

Each shape type defines PolyLine, Circle, and Rectangle. Colors map to functional categories: General (white), Nature (green), Water (blue), Living (brown), Work (grey), Food (red). The color system doubles as a layer system. Designers assign semantic meaning to shapes, and those categories carry through to the CAD export.

When a shape is finalized, it enforces counter-clockwise winding order and recalculates the pivot to the centroid. Counter-clockwise order is required for correct hatching (filled polygon rendering) and consistent CAD export.

Mesh Generation

ShapeMesh handles all rendering. Each shape produces three meshes: the primary 3D wall geometry, a flat minimap projection, and (for closed shapes) a top cap polygon. The line mesh generation creates geometry along both sides of the shape outline, with configurable height.

For closed shapes, the system needs to fill arbitrary concave polygons. The ear-clipping algorithm handles this: it walks the polygon vertices, identifies "ears" (triangles that don't intersect the polygon boundary), clips them off, and repeats until only one triangle remains.

while (true) {
    if (EditableVertices.Count == 3) {
        triangles.Add(new Geometry.Triangle(
            EditableVertices[0], EditableVertices[1], EditableVertices[2]));
        break;
    }

    Geometry.Vertex earVertex = earVertices[0];
    Geometry.Vertex earVertexPrev = earVertex.prev;
    Geometry.Vertex earVertexNext = earVertex.next;

    triangles.Add(new Geometry.Triangle(
        earVertex, earVertexPrev, earVertexNext));

    earVertices.Remove(earVertex);
    EditableVertices.Remove(earVertex);

    earVertexPrev.next = earVertexNext;
    earVertexNext.prev = earVertexPrev;

    Geometry.CheckReflexOrConvex(earVertexPrev);
    Geometry.CheckReflexOrConvex(earVertexNext);
    Geometry.IsVertexEar(earVertexPrev, EditableVertices, earVertices);
    Geometry.IsVertexEar(earVertexNext, EditableVertices, earVertices);
}

After each ear is clipped, the algorithm re-evaluates the neighboring vertices. A vertex that was reflex (concave) might become convex after its neighbor is removed, creating a new ear. This handles L-shaped vendor areas, irregular fencing paths, and any polygon shape a designer might draw.

Tool System

Tools follow the command pattern. Each tool inherits from RayCastTool and implements Fire(), Release(), and UpdateTool(). The Grab tool demonstrates the pattern: trigger press picks up the object under the raycast, controller movement translates it, touchpad rotation rotates it, trigger release drops it.

public override bool Fire(GameObject selected)
{
    touchRotation = new Vector3(0.0f, 0.0f, 0.0f);
    isHolding = true;
    heldObject = selected;
    rotationOrigin = heldObject.transform.localEulerAngles 
        - GetHandRotation();
    return true;
}

On release, the tool checks whether a CAD connection is active. If so, it serializes the shape or object and sends it to the drafting software immediately. Every placement in VR is reflected in the CAD drawing within seconds. The tool then switches back to Select mode, ready for the next interaction.

Snapping

Precise placement requires snapping. The SnapBehavior system provides three modes: grid snapping (round to the nearest unit on each axis), position snapping (constrain distance from the previous point), and angle snapping (round the angle between consecutive line segments).

Angle snapping is the most complex. Given three points (current position, midpoint, previous point), it calculates the angle between the two line segments using atan2, rounds to the configured unit (typically 15 or 45 degrees), and rotates the current point to the snapped angle while preserving the original distance.

public static Vector3 SnapAngle(Vector3 current, Vector3 mid, Vector3 prev) {
    Vector3 one = current - mid;
    Vector3 two = mid - prev;
    float dot = (one.x * two.x + one.z * two.z) / (one.magnitude * two.magnitude);
    float det = (one.x * two.z - one.z * two.x) / (one.magnitude * two.magnitude);
    
    float radians = Mathf.Atan2(det, dot);
    float angle = -MathBuddy.Numeric.RoundTo(
        MathBuddy.Numeric.ToDegrees(radians), Settings.RotationUnit);
    radians = MathBuddy.Numeric.ToRadians(angle);
    
    Vector3 output;
    output.x = Mathf.Cos(radians) * two.x - Mathf.Sin(radians) * two.z;
    output.z = Mathf.Sin(radians) * two.x + Mathf.Cos(radians) * two.z;
    return (output.normalized * one.magnitude) + mid;
}

The PolyLine class chains these snapping modes during point placement. If angle snapping is enabled and the line has at least two existing points, the new point snaps to the nearest configured angle relative to the previous segment. Then grid or position snapping applies on top. This produces clean, engineering-grade geometry from freehand VR controller input.

From VR to CAD

Every shape converts to a SCAD-compatible representation through ToSCAD(). Points transform from Unity's 3D coordinate system to 2D plan coordinates (X and Z become the plan's X and Y). The ConnectionManager serializes the result to JSON and sends it over the WCF channel. Polylines become CAD polylines. Circles become CAD circles. Closed shapes include a hatching flag so the CAD side renders them as filled areas.

Result: Designers draw festival layouts at 1:1 scale in VR, with engineering precision from snapping and immediate CAD export from every placement.

I was Technical Producer at Chasing the Hihat and built the festVR prototype from 2015 to 2018.