World/BrazierFire.cs
using System;
namespace Prototype;
/// <summary>Local reusable flame meshes; no particles or lights are network-spawned.</summary>
public sealed class BrazierFire : Component
{
[Property] public float Phase { get; set; }
private static Model _flameModelV2;
private readonly GameObject[] _flames = new GameObject[7];
private readonly GameObject[] _sparks = new GameObject[6];
private PointLight _light;
private int _meshRevision;
private static readonly Vector3 Origin = new(46, 0, 2);
private static Vector3 Surface(int ring, int side)
{
float t = ring / 8f, a = side * MathF.PI * 2 / 10f;
float radius = MathF.Sin(MathF.PI * MathF.Pow(t, .7f)) * .48f;
return new Vector3(MathF.Cos(a)*radius + t*t*.3f, MathF.Sin(a)*radius, t);
}
private static Model FlameModel()
{
if (_flameModelV2 is not null) return _flameModelV2;
var vertices = new VertexBuffer(); vertices.Init(false);
void Triangle(Vector3 a, Vector3 b, Vector3 c)
{
var normal = Vector3.Cross(b-a,c-a).Normal;
foreach(var p in new[] {a,b,c}) vertices.Add(new Vertex(p,normal,Vector3.Forward,Vector4.Zero) { Color = Color.White });
}
for(int r=0;r<8;r++) for(int s=0;s<10;s++)
{
var a=Surface(r,s); var b=Surface(r,s+1);var c=Surface(r+1,s+1);var d=Surface(r+1,s);
Triangle(a,b,c);Triangle(a,c,d);
}
var mesh = new Mesh(Material.Load("materials/production/torch_v001/flame.vmat"));mesh.CreateBuffers(vertices);
return _flameModelV2 = new ModelBuilder().AddMesh(mesh).Create();
}
private GameObject Create(string name,Color color)
{
var go=new GameObject(true,name);go.NetworkMode=NetworkMode.Never;go.SetParent(GameObject,false);
var renderer=go.Components.Create<ModelRenderer>();renderer.Model=FlameModel();renderer.Tint=color;
return go;
}
protected override void OnStart()
{
for(int i=0;i<_flames.Length;i++) _flames[i]=Create("Flame tongue", i<4?new Color(1,.24f,.015f):new Color(1,.72f,.18f));
for(int i=0;i<_sparks.Length;i++) _sparks[i]=Create("Rising ember",new Color(1,.48f,.05f));
var glow=new GameObject(true,"Brazier glow");glow.NetworkMode=NetworkMode.Never;glow.SetParent(GameObject,false);glow.LocalPosition=Origin+Vector3.Up*12;
_light=glow.Components.Create<PointLight>();_light.Radius=760;_light.Shadows=false;
}
protected override void OnUpdate()
{
if (_meshRevision != 2)
{
foreach (var go in _flames) if(go.IsValid()) go.Components.Get<ModelRenderer>().Model=FlameModel();
foreach (var go in _sparks) if(go.IsValid()) go.Components.Get<ModelRenderer>().Model=FlameModel();
_meshRevision=2;
}
float time=Time.Now+Phase;
for(int i=0;i<_flames.Length;i++)
{
if(!_flames[i].IsValid())continue;
float a=i*2.39996f, pulse=.82f+.16f*MathF.Sin(time*7.3f+i*1.8f)+.08f*MathF.Sin(time*13.1f+i);
float radius=i<4?4.8f:2.2f;
_flames[i].LocalPosition=Origin+new Vector3(MathF.Cos(a)*radius,MathF.Sin(a)*radius,0);
_flames[i].LocalRotation=Rotation.From(new Angles(MathF.Sin(time*4+i)*9,i*73+MathF.Sin(time*3+i)*18,MathF.Cos(time*5+i)*7));
_flames[i].LocalScale=new Vector3(i<4?12:7,i<4?11:7,(i<4?39:23)*pulse);
}
for(int i=0;i<_sparks.Length;i++)
{
if(!_sparks[i].IsValid())continue;
float age=(time*.45f+i*.173f)%1f, a=i*2.4f+time*.7f;
_sparks[i].LocalPosition=Origin+new Vector3(MathF.Cos(a)*(3+age*7),MathF.Sin(a)*(3+age*7),8+age*56);
_sparks[i].LocalScale=new Vector3(.65f*(1-age),.65f*(1-age),2.4f*(1-age));
}
if(_light.IsValid())_light.LightColor=new Color(1,.34f,.075f)*(7.5f+.7f*MathF.Sin(time*9.3f)+.35f*MathF.Sin(time*17.7f));
}
}