Part of the FootballWorldPanel partial class. Builds an editor-only source stadium (pitch, corner flags, goal previews, batched editor detail meshes), clones a scene's FootballEnvironment into the broadcast RenderScene, combines opaque stadium meshes into batched renderers, and copies crowd and fine-stroke data from authoring sections into runtime coordinates.
using Sandbox;
using Socer;
using System;
using System.Collections.Generic;
using System.Linq;
public sealed partial class FootballWorldPanel
{
FootballEnvironment authoredEnvironment;
readonly Dictionary<ModelRenderer,string> environmentSourceSections=new();
readonly Dictionary<GoalStroke,string> environmentSourceStrokeSections=new();
readonly Dictionary<Model,Vertex[]> environmentSourceVertices=new();
public IReadOnlyDictionary<ModelRenderer,string> EnvironmentSourceSections=>environmentSourceSections;
public bool UsesAuthoredEnvironment=>authoredEnvironment!=null;
public int EnvironmentSpectatorCount=>stadiumSpectators;
public Vertex[] EnvironmentSourceVertices(Model model)=>environmentSourceVertices.TryGetValue(model,out var vertices)?vertices:null;
// One-time authoring source, never used to rebuild an authored scene on load.
// The editor bakes these models to ordinary source .vmdl assets.
public static FootballWorldPanel BuildEnvironmentSource()
{
var panel=new FootballWorldPanel{RenderScene=new Scene()};
panel.ownedRenderScene=panel.RenderScene;
panel.sceneCamera=new GameObject(panel.RenderScene,true,"Source camera").Components.Create<CameraComponent>();
panel.box=MakeBox();sharedStadium=null;sharedTechnicalSurface=null;
panel.BuildStadium();
var before=panel.RenderScene.GetAllComponents<ModelRenderer>().ToHashSet();
panel.AddBox(new Point(52.5f,34),-.4f,new Vector3(123,84,.5f),"#1a4033");
foreach(var renderer in panel.RenderScene.GetAllComponents<ModelRenderer>().Where(r=>!before.Contains(r)))panel.environmentSourceSections[renderer]="Pitch foundation";
panel.InitializePitchSurface();panel.environmentSourceSections[panel.pitchSurface]="Playing surface";
panel.environmentSourceSections[panel.technicalSurface]="Technical-area paint";
foreach(var position in new[]{new Point(0,0),new Point(0,68),new Point(105,0),new Point(105,68)})
{
before=panel.RenderScene.GetAllComponents<ModelRenderer>().ToHashSet();
panel.Beam(position,0,position,1.5f,.04f,"#e9e8d4");
panel.AddBox(position+new Point(.18f,0),1.4f,new Vector3(.38f,.025f,.24f),"#efbc4b");
foreach(var renderer in panel.RenderScene.GetAllComponents<ModelRenderer>().Where(r=>!before.Contains(r)))panel.environmentSourceSections[renderer]=$"Corner flag {position.X:0}-{position.Y:0}";
}
panel.BuildEnvironmentEditorDetails();
return panel;
}
void BuildEnvironmentEditorDetails()
{
var batches=new Dictionary<(string Section,string Colour),List<Vertex>>();
var cubeVertices=box.GetVertices();var cubeIndices=box.GetIndices();
var cube=cubeIndices?.Length>0?cubeIndices.Select(i=>cubeVertices[i]).ToArray():cubeVertices;
void Rod(string section,GoalStroke stroke)
{
var a=WorldPoint(stroke.A,stroke.ZA);var b=WorldPoint(stroke.B,stroke.ZB);var delta=b-a;
if(delta.Length<.001f)return;
var key=(section,stroke.Color);
if(!batches.TryGetValue(key,out var list))batches[key]=list=new();
var rotation=Rotation.LookAt(delta.Normal,Math.Abs(delta.Normal.z)>.95f?Vector3.Forward:Vector3.Up);
var transform=new Transform((a+b)*.5f,rotation,new Vector3(delta.Length,stroke.Width*Units,stroke.Width*Units));
foreach(var vertex in cube)list.Add(new Vertex(transform.PointToWorld(vertex.Position),transform.NormalToWorld(vertex.Normal).Normal,Vector3.Up,Vector2.Zero));
}
foreach(var pair in environmentSourceStrokeSections)Rod(pair.Value,pair.Key);
foreach(float x in new[]{0f,105f})foreach(var edge in PitchPerspective.Goal(x))
Rod(x==0?"West goal preview":"East goal preview",new(edge.A,edge.ZA,edge.B,edge.ZB,edge.Net?.012f:PitchPerspective.PostWidth,edge.Net?"#a1c6b9":"#f4f1dc"));
foreach(var pair in batches)
{
var bounds=new BBox(pair.Value[0].Position,pair.Value[0].Position);
foreach(var vertex in pair.Value)bounds=bounds.AddPoint(vertex.Position);
var mesh=new Mesh(Material.Load("materials/default.vmat"));mesh.CreateVertexBuffer(pair.Value.Count,pair.Value);mesh.Bounds=bounds;
var renderer=Create(Model.Builder.AddMesh(mesh).WithViewBounds(bounds).Create(),"Editor detail");
renderer.Tint=Ink(pair.Key.Colour);renderer.Tags.Add("football-editor-detail");
environmentSourceSections[renderer]=pair.Key.Section;
}
}
public void CopySectionMetadata(FootballEnvironmentSection section)
{
string[] stands={"North stand","South stand","West stand","East stand"};
var transform=section.WorldTransform;
foreach(var fan in animatedFans.Where(f=>stands[f.Stand]==section.SectionId))
{
var at=WorldPoint(fan.Position,fan.Level);
var right=fan.Stand<2?Vector3.Forward:new Vector3(0,-1,0);
section.Fans.Add(new(){LocalBase=transform.PointToLocal(at),LocalRight=transform.PointToLocal(at+right)-transform.PointToLocal(at),
LocalUp=transform.PointToLocal(at+Vector3.Up)-transform.PointToLocal(at),Stand=fan.Stand,Hash=fan.Hash});
}
foreach(var pair in environmentSourceStrokeSections.Where(p=>p.Value==section.SectionId))
{
var stroke=pair.Key;
section.FineStrokes.Add(new(){LocalA=transform.PointToLocal(WorldPoint(stroke.A,stroke.ZA)),LocalB=transform.PointToLocal(WorldPoint(stroke.B,stroke.ZB)),Width=stroke.Width*Units,Color=stroke.Color});
}
}
bool InitializeAuthoredEnvironment()
{
var source=Game?.Scene.GetAllComponents<FootballEnvironment>().FirstOrDefault(e=>e.Enabled && e.GameObject.Enabled);
if(source==null)return false;
// The game camera only displays UI over an empty main-scene background.
// Render the stadium once, through the existing broadcast ScenePanel.
foreach(var renderer in source.GameObject.GetComponentsInChildren<ModelRenderer>())renderer.Tags.Add(FootballEnvironment.SceneTag);
foreach(var camera in Game.Scene.GetAllComponents<CameraComponent>())camera.RenderExcludeTags.Add(FootballEnvironment.SceneTag);
var anchor=new GameObject(RenderScene,true,"Authored football environment");
GameObject clone;
using(RenderScene.Push())clone=source.GameObject.Clone(source.WorldTransform,anchor,true,"Football environment");
authoredEnvironment=clone.Components.Get<FootballEnvironment>();
if(authoredEnvironment==null)throw new InvalidOperationException("The authored stadium clone lost its environment marker.");
foreach(var renderer in clone.GetComponentsInChildren<ModelRenderer>())
if(renderer.Tags.Has("football-editor-detail"))renderer.Enabled=false;
BatchAuthoredStadium(clone);
foreach(var section in clone.GetComponentsInChildren<FootballEnvironmentSection>())
{
var transform=section.WorldTransform;
foreach(var fan in section.Fans)
{
var at=transform.PointToWorld(fan.LocalBase);
var right=transform.PointToWorld(fan.LocalBase+fan.LocalRight)-at;
var up=transform.PointToWorld(fan.LocalBase+fan.LocalUp)-at;
animatedFans.Add(new(new Point(at.x/Units,-at.y/Units),at.z/Units,fan.Stand,fan.Hash,right,up));
}
foreach(var stroke in section.FineStrokes)
{
var a=transform.PointToWorld(stroke.LocalA);var b=transform.PointToWorld(stroke.LocalB);
fineStadiumStrokes.Add(new(new Point(a.x/Units,-a.y/Units),a.z/Units,new Point(b.x/Units,-b.y/Units),b.z/Units,
stroke.Width/Units*Math.Max(Math.Abs(transform.Scale.x),Math.Max(Math.Abs(transform.Scale.y),Math.Abs(transform.Scale.z))),stroke.Color));
}
}
stadiumBatches=RenderScene.GetAllComponents<ModelRenderer>().Count(r=>r.Enabled && r.GameObject.Name=="Stadium batch");
// Static supporters live in the baked meshes; animated supporters keep
// their section-local anchors and inherit editor translations/rotations.
stadiumSpectators=authoredEnvironment.SpectatorCount;
InitializeAnimatedCrowd();InitializeTechnicalSurface();
return true;
}
// Keep useful section objects in the editor, then combine their opaque
// default-material geometry once for the broadcast. Editing a section's
// transform/tint is applied before combining; authored sources stay intact.
void BatchAuthoredStadium(GameObject root)
{
var groups=root.GetComponentsInChildren<ModelRenderer>()
.Where(r=>r.Enabled && r.GameObject.Name=="Stadium batch" && r.MaterialOverride==null && !r.HasMaterialGroups && r.Model!=null && r.Model.Materials.Length==1 &&
r.Model.Materials[0].ResourcePath=="materials/default.vmat")
.GroupBy(r=>(r.Tint,r.RenderType)).ToArray();
foreach(var group in groups)
{
var renderers=group.ToArray();
if(renderers.Length<2)continue;
var vertices=new List<Vertex>();
BBox bounds=default;bool hasBounds=false;
foreach(var renderer in renderers)
{
var source=renderer.Model.GetVertices();var indices=renderer.Model.GetIndices();
var transform=renderer.WorldTransform;
int start=renderer.Model.GetIndexStart(0),count=renderer.Model.GetIndexCount(0),offset=renderer.Model.GetBaseVertex(0);
if(count<=0){start=0;count=indices?.Length>0?indices.Length:source.Length;offset=0;}
for(int i=start;i<start+count;i++)
{
int index=indices?.Length>0?offset+(int)indices[i]:i;
var vertex=source[index];var point=transform.PointToWorld(vertex.Position);
vertices.Add(new Vertex(point,transform.NormalToWorld(vertex.Normal).Normal,Vector3.Forward,
new Vector2(vertex.TexCoord0.x,vertex.TexCoord0.y)));
bounds=hasBounds?bounds.AddPoint(point):new BBox(point,point);hasBounds=true;
}
}
if(vertices.Count==0)continue;
var mesh=new Mesh(Material.Load("materials/default.vmat"));mesh.CreateVertexBuffer(vertices.Count,vertices);mesh.Bounds=bounds;
var batch=Create(Model.Builder.AddMesh(mesh).WithViewBounds(bounds).Create(),"Stadium batch");
batch.Tint=group.Key.Tint;batch.RenderType=group.Key.RenderType;batch.Tags.Add(FootballEnvironment.SceneTag);
foreach(var renderer in renderers)renderer.Enabled=false;
}
}
}