A component that procedurally builds various kit piece meshes (tiles, walls, ramps, flag, tee pad, cups) and their collision for a mini-golf style hole. It generates vertex/index buffers per material, creates a Model, assigns a ModelRenderer/ModelCollider, and provides a welded collision mesh for use by a parent GolfHoleCollision.
using Sandbox;
using System;
using System.Collections.Generic;
using System.Linq;
/// <summary>
/// One piece of the hole kit, built as a single clean mesh with a matching collision mesh when
/// the piece is enabled, in the editor and in the game alike. The kit's prefabs each carry one of
/// these; the material is the shared kit material. Grid: 50 units, tiles 200 x 200 with their top
/// at the piece's own height, walls 100 long sitting on a tile edge, ramps 200 long rising 50.
/// </summary>
[Title( "Kit Shape" ), Icon( "category" )]
public sealed class KitShape : Component, Component.ExecuteInEditor
{
public enum ShapeKind
{
/// <summary>200 x 200 tile, 50 thick, top at the piece's height.</summary>
Tile,
/// <summary>100 long rail, 10 thick, 30 high, centred on a tile edge.</summary>
Wall,
/// <summary>An L of two 100 rails meeting at the piece, running +x and +y.</summary>
Corner,
/// <summary>200 long wedge rising 50 towards +x, flush with the tiles at both ends.</summary>
Ramp,
/// <summary>A flat round pad on the floor where the balls start.</summary>
TeePad,
/// <summary>A thin pole with a small flag.</summary>
Flag,
/// <summary>A tile with a round cup opening dropping straight down.</summary>
CupStraight,
/// <summary>A tile with a smooth bowl curving down into the cup opening.</summary>
CupCurved,
}
[Property]
public ShapeKind Kind { get; set; } = ShapeKind.Tile;
/// <summary>The material for the whole kit. Change it to restyle every piece.</summary>
[Property]
public string MaterialPath { get; set; } = "prefabs/holekit/kit.vmat";
/// <summary>
/// Length of a Wall piece, in units. 100 is the kit's standard rail; the wall builder sets any
/// length so a wall fits exactly between two posts.
/// </summary>
[Property, Range( 10f, 5000f )]
public float Length { get; set; } = WallLength;
/// <summary>
/// Height difference between the two ends of a Wall piece, in units: the +x end sits this much
/// higher than the -x end. The ends stay vertical and the top and bottom edges follow the slope,
/// so a wall on a hill is a parallelogram with no wedge gap at the posts. 0 is a flat wall.
/// </summary>
[Property, Range( -500f, 500f )]
public float Rise { get; set; } = 0f;
/// <summary>The wall length actually built: a missing or broken value (0 after a code reload or an old save) means the standard 100.</summary>
float SafeLength => Length >= 1f && !float.IsNaN( Length ) ? Length : WallLength;
/// <summary>Whether the piece has collision (the flag and the tee pad don't block anything).</summary>
[Property]
public bool Collision { get; set; } = true;
public const float Tile = 200f;
public const float TileThickness = 50f;
public const float WallLength = 100f;
public const float WallThickness = 10f;
public const float WallHeight = 30f;
public const float RampRise = 50f;
public const float CupRadius = 6f;
public const float CupDepth = 48f;
public const float BowlDepth = 12f;
public const float BowlRadius = 60f;
/// <summary>Flag pole: an engine dev metal, so nothing is downloaded.</summary>
public const string PoleMaterial = "materials/dev/dev_metal_rough30.vmat";
/// <summary>The flag itself: plain engine red.</summary>
public const string FlagMaterial = "materials/dev/primary_red.vmat";
/// <summary>Tee pad top and side: rubber, about four repeats across the pad.</summary>
public const string TeeMaterial = "textures/tcom_rubber_damaged_0.5x0.5_1k.vmat";
/// <summary>The thin ring around the tee pad: plain engine white.</summary>
public const string RingMaterial = "materials/dev/primary_white.vmat";
public const float TeePadRadius = 24f;
public const float TeePadHeight = 1.5f;
public const float TeeRingWidth = 1.5f;
public const float PoleHeight = 72f;
public const float PoleRadius = 1f;
Model _model;
ShapeKind _builtKind;
string _builtMaterial;
bool _builtCollision;
float _builtLength;
float _builtRise;
/// <summary>One mesh of the piece: its material and its own vertex buffer. Most pieces are one
/// part in the kit material; the flag and the tee pad have a part per material.</summary>
sealed class Part
{
public string MaterialPath;
public readonly List<SimpleVertex> Vertices = new();
public readonly List<int> Indices = new();
public readonly List<Vector3> Positions = new();
}
readonly List<Part> _parts = new();
Part _part;
float _uvScale = 100f;
readonly List<Vector3> _positions = new(); // every part, for bounds and collision
readonly List<int> _indices = new(); // into _positions
readonly List<Vector3> _collisionPositions = new();
readonly List<int> _collisionIndices = new();
// Flag sway: a small yaw wobble in play. The pole and its cap are round, so only the flag shows it.
bool _swayInit;
float _swayPhase;
Rotation _swayBase;
/// <summary>The piece's collision mesh in piece space, with shared vertices welded. Used by the
/// hole's <see cref="GolfHoleCollision"/> to build one collider for the whole hole.</summary>
public IReadOnlyList<Vector3> CollisionPositions => _collisionPositions;
public IReadOnlyList<int> CollisionIndices => _collisionIndices;
/// <summary>Builds the piece if it hasn't been built yet in this session (after a code reload the
/// lists start empty), so the hole collider never reads stale data.</summary>
public void EnsureBuilt()
{
if ( _model is null || (Collision && _collisionIndices.Count == 0) ) Rebuild();
}
protected override void OnEnabled()
{
Rebuild();
}
TimeSince _sinceSpareCheck;
protected override void OnUpdate()
{
// A saved scene can hand the piece a second renderer or collider after it was first built
// (an older save's copies, applied with the prefab patch): rebuild to drop them.
if ( !Game.IsPlaying && _sinceSpareCheck > 1f )
{
_sinceSpareCheck = 0;
if ( GameObject.GetComponents<ModelRenderer>( true ).Count() > 1 || GameObject.GetComponents<ModelCollider>( true ).Count() > 1 )
Rebuild();
}
// Rebuild when a property changed in the Inspector.
if ( _model is null || Kind != _builtKind || MaterialPath != _builtMaterial || Collision != _builtCollision || SafeLength != _builtLength || Rise != _builtRise )
Rebuild();
// Gentle flag wave: a 1.5 degree sine sway with a random phase per flag, only while playing
// so the saved transform never drifts.
if ( Kind == ShapeKind.Flag && Game.IsPlaying )
{
if ( !_swayInit )
{
_swayInit = true;
_swayBase = GameObject.LocalRotation;
_swayPhase = Game.Random.Float( 0f, MathF.Tau );
}
GameObject.LocalRotation = _swayBase * Rotation.FromYaw( MathF.Sin( Time.Now * 1.5f + _swayPhase ) * 1.5f );
}
}
/// <summary>How many kit shapes have been rebuilt since load (see golf_perf).</summary>
public static int RebuildCount;
void Rebuild()
{
RebuildCount++;
_parts.Clear();
_positions.Clear();
_indices.Clear();
BeginPart( MaterialPath );
switch ( Kind )
{
case ShapeKind.Tile: Box( new Vector3( -100, -100, -TileThickness ), new Vector3( 100, 100, 0 ) ); break;
case ShapeKind.Wall: WallShape( SafeLength, Rise ); break;
case ShapeKind.Corner: CornerShape(); break;
case ShapeKind.Ramp: RampShape(); break;
case ShapeKind.TeePad: TeePadShape(); break;
case ShapeKind.Flag: FlagShape(); break;
case ShapeKind.CupStraight: CupShape( false ); break;
case ShapeKind.CupCurved: CupShape( true ); break;
}
var builder = Model.Builder;
foreach ( var part in _parts )
{
if ( part.Indices.Count == 0 ) continue;
var mesh = new Mesh( Material.Load( part.MaterialPath ) );
mesh.CreateVertexBuffer( part.Vertices.Count, part.Vertices );
mesh.CreateIndexBuffer( part.Indices.Count, part.Indices );
mesh.Bounds = BBox.FromPoints( part.Positions );
builder = builder.AddMesh( mesh );
}
_collisionPositions.Clear();
_collisionIndices.Clear();
if ( Collision )
{
// The render mesh keeps a vertex per face corner (flat shading). Physics gets a welded copy with
// shared vertices, so the triangles of one face join up and the ball can't catch on their edges.
var welded = _collisionPositions;
var weldedIndices = _collisionIndices;
welded.Clear();
weldedIndices.Clear();
var lookup = new Dictionary<Vector3, int>();
foreach ( var i in _indices )
{
var p = _positions[i];
if ( !lookup.TryGetValue( p, out var w ) )
{
w = welded.Count;
welded.Add( p );
lookup[p] = w;
}
weldedIndices.Add( w );
}
builder.AddCollisionMesh( welded, weldedIndices );
}
_model = builder.Create();
_builtKind = Kind;
_builtLength = SafeLength;
_builtRise = Rise;
_builtMaterial = MaterialPath;
_builtCollision = Collision;
// One renderer and one collider, driven from here and never saved: they are rebuilt from the
// piece on load. Spare copies (older saves stored them, and re-applied prefab patches can
// stack them up) would keep showing the engine's error model, so they go.
var renderers = GameObject.GetComponents<ModelRenderer>( true ).ToList();
var renderer = renderers.FirstOrDefault() ?? GameObject.AddComponent<ModelRenderer>();
foreach ( var spare in renderers.Skip( 1 ) ) spare.Destroy();
renderer.Flags |= ComponentFlags.NotSaved;
renderer.Model = _model;
var colliders = GameObject.GetComponents<ModelCollider>( true ).ToList();
var collider = colliders.FirstOrDefault();
foreach ( var spare in colliders.Skip( 1 ) ) spare.Destroy();
if ( collider.IsValid() ) collider.Flags |= ComponentFlags.NotSaved;
var hole = GameObject.GetComponentInParent<GolfHoleCollision>( true );
if ( hole is not null )
{
// Part of a hole: the hole builds one welded collider for all its pieces (GolfHoleCollision).
if ( collider.IsValid() ) collider.Enabled = false;
hole.MarkDirty();
}
else if ( Collision )
{
collider ??= GameObject.AddComponent<ModelCollider>();
collider.Flags |= ComponentFlags.NotSaved;
collider.Model = _model;
collider.Static = true;
collider.Enabled = true;
// Barely bouncy surface (Assets/surface/golf_green.surface) so the seams between pieces don't launch the ball.
collider.Surface = Surface.FindByName( "golf_green" ) ?? collider.Surface;
}
else if ( collider.IsValid() )
{
collider.Enabled = false;
}
}
// ---------------------------------------------------------------- mesh helpers (flat shaded unless noted)
/// <summary>Starts a new part of the piece in the given material; uvScale is the world size of one texture repeat.</summary>
void BeginPart( string materialPath, float uvScale = 100f )
{
_part = new Part { MaterialPath = materialPath };
_parts.Add( _part );
_uvScale = uvScale;
}
/// <summary>Box-projected texture coordinate for a point with this normal, one repeat per _uvScale units.</summary>
Vector2 Uv( Vector3 p, Vector3 normal )
=> (MathF.Abs( normal.z ) > 0.5f ? new Vector2( p.x, p.y ) : new Vector2( MathF.Abs( normal.x ) > 0.5f ? p.y : p.x, p.z )) / _uvScale;
void AddVertex( Vector3 p, Vector3 normal, Vector3 tangent, Vector2 uv )
{
_part.Indices.Add( _part.Vertices.Count );
_part.Vertices.Add( new SimpleVertex( p, normal, tangent, uv ) );
_part.Positions.Add( p );
_indices.Add( _positions.Count );
_positions.Add( p );
}
void Triangle( Vector3 a, Vector3 b, Vector3 c )
{
var normal = Vector3.Cross( b - a, c - a );
if ( normal.IsNearlyZero() ) return;
normal = normal.Normal;
var tangent = MathF.Abs( normal.z ) < 0.9f ? Vector3.Cross( Vector3.Up, normal ).Normal : Vector3.Forward;
foreach ( var p in new[] { a, b, c } )
AddVertex( p, normal, tangent, Uv( p, normal ) );
}
/// <summary>A triangle with its own normal at each corner (smooth shading across neighbours).</summary>
void SmoothTriangle( Vector3 a, Vector3 na, Vector3 b, Vector3 nb, Vector3 c, Vector3 nc )
{
if ( Vector3.Cross( b - a, c - a ).IsNearlyZero() ) return;
foreach ( var (p, n) in new[] { (a, na), (b, nb), (c, nc) } )
{
var tangent = MathF.Abs( n.z ) < 0.9f ? Vector3.Cross( Vector3.Up, n ).Normal : Vector3.Forward;
AddVertex( p, n, tangent, Uv( p, n ) );
}
}
/// <summary>A quad a-b-c-d, counter-clockwise seen from its front.</summary>
void Quad( Vector3 a, Vector3 b, Vector3 c, Vector3 d )
{
Triangle( a, b, c );
Triangle( a, c, d );
}
/// <summary>
/// A wall of the given length, sheared by rise: every corner's z is shifted by rise * x / length,
/// so the +x end stands rise higher than the -x end while both ends stay vertical.
/// </summary>
void WallShape( float length, float rise )
{
var min = new Vector3( -length / 2, -WallThickness / 2, 0 );
var max = new Vector3( length / 2, WallThickness / 2, WallHeight );
var p = new Vector3[8];
for ( var i = 0; i < 8; i++ )
{
var x = (i & 1) == 0 ? min.x : max.x;
p[i] = new Vector3( x, (i & 2) == 0 ? min.y : max.y, ((i & 4) == 0 ? min.z : max.z) + rise * x / length );
}
Quad( p[4], p[5], p[7], p[6] ); // top
Quad( p[0], p[2], p[3], p[1] ); // bottom
Quad( p[0], p[1], p[5], p[4] ); // -y
Quad( p[2], p[6], p[7], p[3] ); // +y
Quad( p[0], p[4], p[6], p[2] ); // -x end
Quad( p[1], p[3], p[7], p[5] ); // +x end
}
void Box( Vector3 min, Vector3 max )
{
var p = new Vector3[8];
for ( var i = 0; i < 8; i++ )
p[i] = new Vector3( (i & 1) == 0 ? min.x : max.x, (i & 2) == 0 ? min.y : max.y, (i & 4) == 0 ? min.z : max.z );
Quad( p[4], p[5], p[7], p[6] ); // top
Quad( p[0], p[2], p[3], p[1] ); // bottom
Quad( p[0], p[1], p[5], p[4] ); // -y
Quad( p[3], p[2], p[6], p[7] ); // +y
Quad( p[2], p[0], p[4], p[6] ); // -x
Quad( p[1], p[3], p[7], p[5] ); // +x
}
/// <summary>A flat polygon (counter-clockwise, in the xy plane) extruded from z0 up to z1.</summary>
void Extrude( IReadOnlyList<Vector2> polygon, float z0, float z1 )
{
var n = polygon.Count;
// caps: fan triangulation works for the shapes here (convex or the L, which is fanned from a reflex-safe corner)
var cap = Triangulate( polygon );
for ( var i = 0; i < cap.Count; i += 3 )
{
var a = polygon[cap[i]]; var b = polygon[cap[i + 1]]; var c = polygon[cap[i + 2]];
Triangle( new Vector3( a.x, a.y, z1 ), new Vector3( b.x, b.y, z1 ), new Vector3( c.x, c.y, z1 ) );
Triangle( new Vector3( a.x, a.y, z0 ), new Vector3( c.x, c.y, z0 ), new Vector3( b.x, b.y, z0 ) );
}
for ( var i = 0; i < n; i++ )
{
var a = polygon[i]; var b = polygon[(i + 1) % n];
Quad( new Vector3( a.x, a.y, z0 ), new Vector3( b.x, b.y, z0 ), new Vector3( b.x, b.y, z1 ), new Vector3( a.x, a.y, z1 ) );
}
}
/// <summary>Ear clipping for a simple counter-clockwise polygon.</summary>
static List<int> Triangulate( IReadOnlyList<Vector2> polygon )
{
var result = new List<int>();
var left = new List<int>();
for ( var i = 0; i < polygon.Count; i++ ) left.Add( i );
var guard = 0;
while ( left.Count > 3 && guard++ < 1000 )
{
var clipped = false;
for ( var i = 0; i < left.Count; i++ )
{
var i0 = left[(i + left.Count - 1) % left.Count];
var i1 = left[i];
var i2 = left[(i + 1) % left.Count];
var a = polygon[i0]; var b = polygon[i1]; var c = polygon[i2];
var cross = (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
if ( cross <= 0f ) continue;
var inside = false;
foreach ( var j in left )
{
if ( j == i0 || j == i1 || j == i2 ) continue;
if ( PointInTriangle( polygon[j], a, b, c ) ) { inside = true; break; }
}
if ( inside ) continue;
result.Add( i0 ); result.Add( i1 ); result.Add( i2 );
left.RemoveAt( i );
clipped = true;
break;
}
if ( !clipped ) break;
}
if ( left.Count == 3 ) { result.Add( left[0] ); result.Add( left[1] ); result.Add( left[2] ); }
return result;
}
static bool PointInTriangle( Vector2 p, Vector2 a, Vector2 b, Vector2 c )
{
float Sign( Vector2 p1, Vector2 p2, Vector2 p3 ) => (p1.x - p3.x) * (p2.y - p3.y) - (p2.x - p3.x) * (p1.y - p3.y);
var d1 = Sign( p, a, b ); var d2 = Sign( p, b, c ); var d3 = Sign( p, c, a );
var neg = d1 < 0 || d2 < 0 || d3 < 0;
var pos = d1 > 0 || d2 > 0 || d3 > 0;
return !(neg && pos);
}
/// <summary>A vertical cylinder around a centre, from z0 to z1.</summary>
void Cylinder( Vector3 centre, float radius, float z0, float z1, int segments, bool capTop, bool capBottom )
{
for ( var i = 0; i < segments; i++ )
{
var a0 = MathF.Tau * i / segments;
var a1 = MathF.Tau * (i + 1) / segments;
var p0 = centre + new Vector3( MathF.Cos( a0 ) * radius, MathF.Sin( a0 ) * radius, 0 );
var p1 = centre + new Vector3( MathF.Cos( a1 ) * radius, MathF.Sin( a1 ) * radius, 0 );
Quad( p0.WithZ( z0 ), p1.WithZ( z0 ), p1.WithZ( z1 ), p0.WithZ( z1 ) );
if ( capTop ) Triangle( centre.WithZ( z1 ), p0.WithZ( z1 ), p1.WithZ( z1 ) );
if ( capBottom ) Triangle( centre.WithZ( z0 ), p1.WithZ( z0 ), p0.WithZ( z0 ) );
}
}
/// <summary>A vertical cylinder with a smooth side (radial normals); the caps stay flat.</summary>
void SmoothCylinder( Vector3 centre, float radius, float z0, float z1, int segments, bool capTop, bool capBottom )
{
for ( var i = 0; i < segments; i++ )
{
var a0 = MathF.Tau * i / segments;
var a1 = MathF.Tau * (i + 1) / segments;
var n0 = new Vector3( MathF.Cos( a0 ), MathF.Sin( a0 ), 0 );
var n1 = new Vector3( MathF.Cos( a1 ), MathF.Sin( a1 ), 0 );
var p0 = centre + n0 * radius;
var p1 = centre + n1 * radius;
SmoothTriangle( p0.WithZ( z0 ), n0, p1.WithZ( z0 ), n1, p1.WithZ( z1 ), n1 );
SmoothTriangle( p0.WithZ( z0 ), n0, p1.WithZ( z1 ), n1, p0.WithZ( z1 ), n0 );
if ( capTop ) Triangle( centre.WithZ( z1 ), p0.WithZ( z1 ), p1.WithZ( z1 ) );
if ( capBottom ) Triangle( centre.WithZ( z0 ), p1.WithZ( z0 ), p0.WithZ( z0 ) );
}
}
/// <summary>A smooth sphere (latitude rings by longitude segments).</summary>
void SmoothSphere( Vector3 centre, float radius, int rings, int segments )
{
Vector3 N( int ring, int seg )
{
var lat = -MathF.PI / 2 + MathF.PI * ring / rings;
var lon = MathF.Tau * seg / segments;
return new Vector3( MathF.Cos( lat ) * MathF.Cos( lon ), MathF.Cos( lat ) * MathF.Sin( lon ), MathF.Sin( lat ) );
}
for ( var r = 0; r < rings; r++ )
for ( var s = 0; s < segments; s++ )
{
var n00 = N( r, s ); var n10 = N( r, s + 1 ); var n11 = N( r + 1, s + 1 ); var n01 = N( r + 1, s );
SmoothTriangle( centre + n00 * radius, n00, centre + n10 * radius, n10, centre + n11 * radius, n11 );
SmoothTriangle( centre + n00 * radius, n00, centre + n11 * radius, n11, centre + n01 * radius, n01 );
}
}
/// <summary>A flat ring (or a disc when r0 is 0) at height z, facing up.</summary>
void Ring( float z, float r0, float r1, int segments )
{
var centre = new Vector3( 0, 0, z );
for ( var i = 0; i < segments; i++ )
{
var a0 = MathF.Tau * i / segments;
var a1 = MathF.Tau * (i + 1) / segments;
var d0 = new Vector3( MathF.Cos( a0 ), MathF.Sin( a0 ), 0 );
var d1 = new Vector3( MathF.Cos( a1 ), MathF.Sin( a1 ), 0 );
if ( r0 <= 0f ) Triangle( centre, centre + d0 * r1, centre + d1 * r1 );
else Quad( centre + d0 * r1, centre + d1 * r1, centre + d1 * r0, centre + d0 * r0 );
}
}
// ---------------------------------------------------------------- the pieces
void CornerShape()
{
var t = WallThickness / 2;
var l = WallLength;
var shape = new List<Vector2> { new( -t, -t ), new( l, -t ), new( l, t ), new( t, t ), new( t, l ), new( -t, l ) };
Extrude( shape, 0f, WallHeight );
}
void RampShape()
{
// Cross-section in x/z, extruded along y: flat bottom at -50, top rising from 0 to 50.
var a = new Vector3( -100, -100, -TileThickness ); var b = new Vector3( 100, -100, -TileThickness );
var c = new Vector3( 100, -100, RampRise ); var d = new Vector3( -100, -100, 0 );
var a2 = a.WithY( 100 ); var b2 = b.WithY( 100 ); var c2 = c.WithY( 100 ); var d2 = d.WithY( 100 );
Quad( d, c, c2, d2 ); // sloped top
Quad( a, a2, b2, b ); // bottom
Quad( b, b2, c2, c ); // +x end (tall)
Quad( a, d, d2, a2 ); // -x end (short)
Quad( a, b, c, d ); // -y side
Quad( a2, d2, c2, b2 ); // +y side
}
/// <summary>A smooth 2-wide metal pole with a round cap and a red triangular flag (20 long, 12 high) at the top, pointing +y.</summary>
void FlagShape()
{
BeginPart( PoleMaterial );
SmoothCylinder( Vector3.Zero, PoleRadius, 0f, PoleHeight, 24, false, false );
SmoothSphere( new Vector3( 0, 0, PoleHeight ), PoleRadius * 1.6f, 6, 24 );
BeginPart( FlagMaterial );
var top = new Vector3( 0, PoleRadius, PoleHeight );
var bottom = new Vector3( 0, PoleRadius, PoleHeight - 12f );
var tip = new Vector3( 0, PoleRadius + 20f, PoleHeight - 6f );
Triangle( top, bottom, tip ); // one side
Triangle( top, tip, bottom ); // the other
}
/// <summary>The round rubber tee pad, 1.5 high, with a thin white ring around the top edge.</summary>
void TeePadShape()
{
BeginPart( TeeMaterial, TeePadRadius * 2f / 4f ); // four texture repeats across the pad
SmoothCylinder( Vector3.Zero, TeePadRadius, 0f, TeePadHeight, 32, false, true );
Ring( TeePadHeight, 0f, TeePadRadius - TeeRingWidth, 32 );
BeginPart( RingMaterial );
Ring( TeePadHeight, TeePadRadius - TeeRingWidth, TeePadRadius, 32 );
}
/// <summary>
/// A 200 x 200 tile with a round opening of CupRadius at the centre: the top is a band from the
/// square edge to the circle (bowl-shaped for the curved cup), the opening drops to the cup floor.
/// </summary>
void CupShape( bool bowl )
{
const int segments = 48;
var rimZ = bowl ? -BowlDepth : 0f;
var floorZ = -CupDepth - (bowl ? BowlDepth : 0f);
// Radii from the opening out to the square edge, with the height of the top surface at each.
var radii = bowl
? new[] { CupRadius, 10f, 16f, 24f, 34f, 46f, BowlRadius, 80f, float.NaN }
: new[] { CupRadius, 50f, float.NaN };
Vector3 Top( float radius, float angle )
{
var cos = MathF.Cos( angle ); var sin = MathF.Sin( angle );
if ( float.IsNaN( radius ) )
{
// the square edge
var m = MathF.Max( MathF.Abs( cos ), MathF.Abs( sin ) );
return new Vector3( 100f * cos / m, 100f * sin / m, 0f );
}
var z = 0f;
if ( bowl && radius < BowlRadius )
{
var t = (radius - CupRadius) / (BowlRadius - CupRadius);
z = -BowlDepth * (0.5f + 0.5f * MathF.Cos( MathF.PI * t ));
}
return new Vector3( cos * radius, sin * radius, z );
}
for ( var i = 0; i < segments; i++ )
{
var a0 = MathF.Tau * i / segments;
var a1 = MathF.Tau * (i + 1) / segments;
// top surface, ring by ring
for ( var r = 0; r < radii.Length - 1; r++ )
Quad( Top( radii[r], a0 ), Top( radii[r + 1], a0 ), Top( radii[r + 1], a1 ), Top( radii[r], a1 ) );
// the opening's wall and the cup floor
var i0 = Top( CupRadius, a0 ); var i1 = Top( CupRadius, a1 );
Quad( i0.WithZ( rimZ ), i1.WithZ( rimZ ), i1.WithZ( floorZ ), i0.WithZ( floorZ ) );
Triangle( new Vector3( 0, 0, floorZ ), i0.WithZ( floorZ ), i1.WithZ( floorZ ) );
// outer side and the flat bottom
var e0 = Top( float.NaN, a0 ); var e1 = Top( float.NaN, a1 );
Quad( e0.WithZ( -TileThickness ), e1.WithZ( -TileThickness ), e1, e0 );
Triangle( new Vector3( 0, 0, -TileThickness ), e1.WithZ( -TileThickness ), e0.WithZ( -TileThickness ) );
}
}
}