The FootballWorldPanel class initializes and manages the rendering of the soccer pitch surface, including turf markings and various boundary lines. It uses the sandbox API to create dynamic geometry for ground markings and handles the cleanup or assignment of pitch models.
using Sandbox;
using Socer;
using System;
using System.Collections.Generic;
using System.Linq;
public sealed partial class FootballWorldPanel
{
// SceneDynamicObject.Material only passes a native handle to the renderer.
// The engine's resource cache is weak after its short keepalive expires;
// own the managed resource for the entire lifetime of our dynamic geometry.
Material dynamicGeometryMaterial;
Material DynamicGeometryMaterial=>dynamicGeometryMaterial??=Material.Load("materials/socer/vertex-alpha.vmat");
ModelRenderer pitchSurface;
readonly Dictionary<ModelRenderer[],SceneDynamicObject> groundRings=new();
readonly SceneDynamicObject[] groundPings=new SceneDynamicObject[5];
readonly Vertex[] ringVertices=new Vertex[32*18];
int visibleCursorSegments;
SceneDynamicObject groundLineObject;
readonly ModelRenderer[] groundLines=new ModelRenderer[36];
readonly Vertex[] groundLineVertices=new Vertex[36*18];
int groundLineCount;
void InitializePitchSurface()
{
if(pitchSurface!=null)return;
if(authoredEnvironment!=null)
{
pitchSurface=authoredEnvironment.GameObject.GetComponentsInChildren<ModelRenderer>().FirstOrDefault(r=>r.GameObject.Tags.Has("football-pitch"));
return;
}
// Retire the old ten grass slabs and elevated line boxes on hotload.
// Corner flags and the foundation are separate geometry and stay intact.
foreach(var old in RenderScene.GetAllComponents<ModelRenderer>().ToArray())
if((old.GameObject.Name=="Pitch" && Math.Abs(old.WorldPosition.z+.04f*Units)<.01f) ||
(old.GameObject.Name=="Geometry" && Math.Abs(old.WorldPosition.z-.025f*Units)<.01f))
old.GameObject.Destroy();
Vector3 a=WorldPoint(new Point(-1,-1)),b=WorldPoint(new Point(106,-1));
Vector3 c=WorldPoint(new Point(106,69)),d=WorldPoint(new Point(-1,69));
Vertex V(Vector3 p,Vector2 uv)=>new(p,Vector3.Up,Vector3.Forward,uv);
var vertices=new[]{V(a,new(0,0)),V(c,new(1,1)),V(b,new(1,0)),V(a,new(0,0)),V(d,new(0,1)),V(c,new(1,1))};
var bounds=new BBox(WorldPoint(new Point(-1,69),-.01f),WorldPoint(new Point(106,-1),.01f));
var mesh=new Mesh(Material.Load("materials/pitch/turf-markings.vmat"));
mesh.CreateVertexBuffer(vertices.Length,vertices);mesh.Bounds=bounds;
pitchSurface=Create(Model.Builder.AddMesh(mesh).WithViewBounds(bounds).Create(),"Textured turf and markings");
environmentSourceVertices[pitchSurface.Model]=vertices;
}
SceneDynamicObject GroundObject()
{
// Ship the material and shader with the game; editor tools are absent on clients.
var obj=new SceneDynamicObject(RenderScene.SceneWorld){Material=DynamicGeometryMaterial};
obj.Attributes.SetCombo("D_NO_CULLING",1);
obj.Flags.CastShadows=false;
return obj;
}
float GroundPixelSize(Point center)
{
Vector2 a=Project(center),x=Project(center+new Point(.25f,0)),y=Project(center+new Point(0,.25f));
var sx=new Vector2((x.x-a.x)*Box.Rect.Width,(x.y-a.y)*Box.Rect.Height)*4;
var sy=new Vector2((y.x-a.x)*Box.Rect.Width,(y.y-a.y)*Box.Rect.Height)*4;
float aa=sx.LengthSquared,bb=sy.LengthSquared,cross=sx.x*sy.y-sx.y*sy.x;
float minor=MathF.Sqrt(Math.Max(.01f,(aa+bb-MathF.Sqrt(Math.Max(0,(aa+bb)*(aa+bb)-4*cross*cross)))*.5f));
return Math.Clamp(1/minor,.005f,.20f);
}
void GroundRing(SceneDynamicObject obj,Point center,float radius,float width,string color,int segments,bool visible,float opacity=1)
{
obj.RenderingEnabled=visible;
if(!visible)return;
// Subpixel solid boxes flicker as camera coverage changes. A flat stroke
// with a one-pixel alpha fringe stays depth-tested in the turf plane.
float pixel=GroundPixelSize(center),half=Math.Max(width*.5f,pixel*.52f),feather=pixel*.85f;
var ink=Ink(color);int count=0;
float Radius(int band)=>Math.Max(.002f,radius+(band switch {0=>-half-feather,1=>-half,2=>half,_=>half+feather}));
Vertex V(int segment,int band)
{
float angle=segment*MathF.Tau/segments;
var at=center+new Point(MathF.Cos(angle),MathF.Sin(angle))*Radius(band);
var tint=new Color32((byte)(ink.r*255),(byte)(ink.g*255),(byte)(ink.b*255),(byte)(band is 0 or 3?0:255*opacity));
return new Vertex(WorldPoint(at,.012f),tint);
}
for(int i=0;i<segments;i++)for(int band=0;band<3;band++)
{
var a=V(i,band);var b=V(i+1,band);var c=V(i+1,band+1);var d=V(i,band+1);
ringVertices[count++]=a;ringVertices[count++]=b;ringVertices[count++]=c;
ringVertices[count++]=a;ringVertices[count++]=c;ringVertices[count++]=d;
}
obj.Init(Graphics.PrimitiveType.Triangles);obj.AddVertex(ringVertices.AsSpan(0,count));
}
void GroundLine(ModelRenderer line)
{
groundLines[groundLineCount++]=line;line.Enabled=false;
}
void DrawGroundLines()
{
groundLineObject??=GroundObject();groundLineObject.RenderingEnabled=groundLineCount>0;
if(groundLineCount==0)return;
int count=0;
for(int i=0;i<groundLineCount;i++)
{
var line=groundLines[i];var tr=line.WorldTransform;
var direction=tr.Rotation.Forward;var axis=new Point(direction.x,-direction.y).Unit;
var at=new Point(tr.Position.x/Units,-tr.Position.y/Units);
var side=new Point(-axis.Y,axis.X);float pixel=GroundPixelSize(at);
float half=Math.Max(tr.Scale.y/Units*.5f,pixel*.52f),feather=pixel*.85f;
var a=at-axis*(tr.Scale.x/Units*.5f);var b=at+axis*(tr.Scale.x/Units*.5f);var ink=line.Tint;
Vertex V(Point p,int band)
{
float offset=band switch {0=>-half-feather,1=>-half,2=>half,_=>half+feather};
return new Vertex(WorldPoint(p+side*offset,.014f),new Color32((byte)(ink.r*255),(byte)(ink.g*255),(byte)(ink.b*255),(byte)(band is 0 or 3?0:ink.a*255)));
}
for(int band=0;band<3;band++)
{
var p=V(a,band);var q=V(b,band);var r=V(b,band+1);var s=V(a,band+1);
groundLineVertices[count++]=p;groundLineVertices[count++]=q;groundLineVertices[count++]=r;
groundLineVertices[count++]=p;groundLineVertices[count++]=r;groundLineVertices[count++]=s;
}
}
groundLineObject.Init(Graphics.PrimitiveType.Triangles);groundLineObject.AddVertex(groundLineVertices.AsSpan(0,count));
}
}