Aetheria/Assets/Scripts/Zone Display/MapPresenter.cs
Meta Vens 4694366d98 Map presenter: ShowAsteroidsInMinimap stays (head's belt cost misses 0.1 ms); belt upload via SubUpdates; settings key restored
The setting gates belts on the minimap again, through MapPresenter.BeltsOnMinimap; the map screen always draws them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 23:54:45 +02:00

246 lines
10 KiB
C#

using System.Collections.Generic;
using CultMath;
using CultMath.UnityBridge;
using GameCult.Caching;
using UnityEngine;
using UnityEngine.Rendering;
using static CultMath.math;
using float2 = CultMath.float2;
using float3 = CultMath.float3;
using float4 = CultMath.float4;
using quaternion = CultMath.quaternion;
// The one owner of map-space projection (ruling map-icons-placed-in-2d): where every map icon and belt asteroid is
// drawn, its yaw, its size, its fade, and the contour's depth range. Icons are not world objects: each is a mesh,
// a material instance and an authored rotation read once from the icon child at registration (the child is then
// destroyed), drawn every frame on the Minimap layer at the map plane from sim state through DrawAhead. Roll and
// pitch never reach an icon, and field depth never decides whether one is seen. Both map cameras (the minimap's
// and the map screen's) see these same draws; only the size input differs.
public class MapPresenter
{
// The map plane. Every map camera is orthographic and looks down from above it, so this height is inside every
// clip range; the offsets only order overlapping icons (planets under stations under ships, belts under all).
public const float PlaneY = 0f;
public const float BeltOffset = -.1f, PlanetOffset = 0f, OrbitalOffset = .1f, ShipOffset = .2f;
// The one place the heading of an icon is decided (question contact-icon-heading, yaw-for-all): every icon turns
// with its entity's yaw. Another answer changes only this function.
public static float IconYaw(Entity entity, quaternion drawnRotation)
{
var forward = drawnRotation.ToUnity() * Vector3.forward;
return Mathf.Atan2(forward.x, forward.z) * Mathf.Rad2Deg;
}
private sealed class Icon
{
public Mesh Mesh;
public Material Material;
public Color Base;
public Quaternion Authored;
public float Offset;
public Entity Entity;
public CultRecordKey Planet;
public float Fade;
public Matrix4x4 Drawn;
}
private readonly List<Icon> _icons = new List<Icon>();
private readonly Dictionary<Entity, Icon> _entityIcons = new Dictionary<Entity, Icon>();
private readonly Material[] _gravityMaterials;
private readonly int _layer = LayerMask.NameToLayer("Minimap");
private float _minimapScale = 1, _mapScreenScale;
private bool _mapScreenOpen;
private readonly Material _asteroidMaterial;
private readonly Vector4 _spriteSheet;
private readonly float _asteroidSize;
public MapPresenter(Material[] gravityMaterials, Material asteroidMaterial, int spriteColumns, int spriteRows,
float asteroidSize)
{
_gravityMaterials = gravityMaterials;
_asteroidMaterial = asteroidMaterial;
_spriteSheet = new Vector4(spriteColumns, spriteRows, 0, 0);
_asteroidSize = asteroidSize;
}
// The icon size: the map screen's scale while it is open, else the minimap's zoom.
public float IconSize => _mapScreenOpen ? _mapScreenScale : _minimapScale;
public void SetMinimapScale(float scale) => _minimapScale = scale;
public void OpenMapScreen(bool open) => _mapScreenOpen = open;
// PlayerGraphicsSettings.ShowAsteroidsInMinimap: belt sprites on the minimap; the map screen always shows them.
public bool BeltsOnMinimap { get; set; }
public void SetMapScreenScale(float scale) => _mapScreenScale = scale;
// Destroy in play mode, DestroyImmediate where the editor smokes run without one.
private static void Discard(Object doomed)
{
if (Application.isPlaying) Object.Destroy(doomed);
else Object.DestroyImmediate(doomed);
}
public int IconCount => _icons.Count;
// What the last Draw issued, for the smoke.
public int IconDraws { get; private set; }
public int BeltDraws { get; private set; }
public int LastBeltInstances { get; private set; }
public bool TryDrawn(CultRecordKey planet, out Matrix4x4 drawn)
{
drawn = default;
var icon = _icons.Find(candidate => candidate.Entity == null && candidate.Planet.Equals(planet));
if (icon == null) return false;
drawn = icon.Drawn;
return true;
}
// What a belt's buffer holds on the GPU side, read back.
public void ReadBelt(CultRecordKey key, float4[] into) => _belts[key].Buffer.GetData(into);
public bool TryDrawn(Entity entity, out Matrix4x4 drawn, out float alpha)
{
drawn = default;
alpha = 0;
if (!_entityIcons.TryGetValue(entity, out var icon)) return false;
drawn = icon.Drawn;
alpha = icon.Material.GetColor("_Color").a;
return true;
}
// Reads the icon child once (mesh, material, rotation relative to its root) and destroys it: from here on no
// world transform carries the icon.
private Icon Take(MeshRenderer renderer, Transform root, float offset)
{
var icon = new Icon
{
Mesh = renderer.GetComponent<MeshFilter>().sharedMesh,
Material = Object.Instantiate(renderer.sharedMaterial),
Authored = Quaternion.Inverse(root.rotation) * renderer.transform.rotation,
Offset = offset,
Fade = 1,
};
icon.Base = icon.Material.HasProperty("_Color") ? icon.Material.GetColor("_Color") : Color.white;
Discard(renderer.gameObject);
_icons.Add(icon);
return icon;
}
public void RegisterEntity(Entity entity, MeshRenderer renderer, Transform root)
{
var icon = Take(renderer, root, entity is Ship ? ShipOffset : OrbitalOffset);
icon.Entity = entity;
icon.Fade = 0;
_entityIcons[entity] = icon;
}
public void RegisterPlanet(CultRecordKey key, MeshRenderer renderer, Transform root) =>
Take(renderer, root, PlanetOffset).Planet = key;
// The entity's fade, applied to its icon the same frame the hull's materials take it.
public void SetFade(Entity entity, float fade)
{
if (_entityIcons.TryGetValue(entity, out var icon)) icon.Fade = fade;
}
public void Unregister(Entity entity)
{
if (!_entityIcons.Remove(entity, out var icon)) return;
_icons.Remove(icon);
Discard(icon.Material);
}
private sealed class Belt
{
public float4[] Source;
public ComputeBuffer Buffer;
public MaterialPropertyBlock Block = new MaterialPropertyBlock();
}
private readonly Dictionary<CultRecordKey, Belt> _belts = new Dictionary<CultRecordKey, Belt>();
private static readonly int BeltProperty = Shader.PropertyToID("_Belt");
private static readonly int SheetProperty = Shader.PropertyToID("_Sheet");
private static readonly int PlaneProperty = Shader.PropertyToID("_PlaneY");
private static readonly int SizeProperty = Shader.PropertyToID("_AsteroidSize");
// A belt's map sprites: the 3D belt's per-frame float4s (x, y planar, z spin, w size), uploaded as they are.
public void RegisterBelt(CultRecordKey key, float4[] source) =>
_belts[key] = new Belt
{
Source = source,
Buffer = new ComputeBuffer(source.Length, 16, ComputeBufferType.Structured, ComputeBufferMode.SubUpdates)
};
// One instanced draw per belt: six vertices per asteroid, expanded to a quad on the map plane by the shader.
private void DrawBelt(Belt belt)
{
var upload = belt.Buffer.BeginWrite<float4>(0, belt.Source.Length);
upload.CopyFrom(belt.Source);
belt.Buffer.EndWrite<float4>(belt.Source.Length);
belt.Block.SetBuffer(BeltProperty, belt.Buffer);
belt.Block.SetVector(SheetProperty, _spriteSheet);
belt.Block.SetFloat(PlaneProperty, PlaneY + BeltOffset);
belt.Block.SetFloat(SizeProperty, _asteroidSize);
var parameters = new RenderParams(_asteroidMaterial)
{
layer = _layer, matProps = belt.Block, shadowCastingMode = ShadowCastingMode.Off,
worldBounds = new Bounds(Vector3.zero, Vector3.one * 1e6f),
};
Graphics.RenderPrimitives(parameters, MeshTopology.Triangles, 6, belt.Source.Length);
BeltDraws++;
LastBeltInstances = belt.Source.Length;
}
// Every registered icon and belt for this frame, from sim state at the drawn time; and the contour's depth range,
// the deepest drawn planet's field depth (the map gravity materials' normalisation, not a camera bound).
public void Draw(Zone zone, float lead)
{
var size = IconSize;
IconDraws = BeltDraws = 0;
var maxDepth = 0f;
foreach (var icon in _icons)
{
float2 planar;
Quaternion yaw = Quaternion.identity;
if (icon.Entity != null)
{
var drawn = DrawAhead.Position(icon.Entity, lead);
planar = drawn.xz;
var rotation = icon.Entity is Ship ship ? DrawAhead.Rotation(ship, lead) : quaternion.identity;
yaw = Quaternion.Euler(0, IconYaw(icon.Entity, rotation), 0);
}
else
{
var body = zone.PlanetInstances[icon.Planet];
planar = DrawAhead.Orbit(zone, body.BodyData.Orbit.Key, lead);
maxDepth = Mathf.Max(maxDepth, -DrawAhead.Height(zone, planar, lead));
}
if (icon.Fade <= .01f) continue;
if (icon.Material.HasProperty("_Color"))
icon.Material.SetColor("_Color", new Color(icon.Base.r, icon.Base.g, icon.Base.b, icon.Base.a * icon.Fade));
var parameters = new RenderParams(icon.Material) { layer = _layer, shadowCastingMode = ShadowCastingMode.Off };
icon.Drawn = Matrix4x4.TRS(new Vector3(planar.x, PlaneY + icon.Offset, planar.y), yaw * icon.Authored,
Vector3.one * size);
Graphics.RenderMesh(parameters, icon.Mesh, 0, icon.Drawn);
IconDraws++;
}
var depthRange = Mathf.Max(maxDepth, 1f);
foreach (var material in _gravityMaterials)
{
material.SetFloat("_StartDepth", 0f);
material.SetFloat("_DepthRange", depthRange);
}
if (BeltsOnMinimap || _mapScreenOpen)
foreach (var belt in _belts.Values) DrawBelt(belt);
}
public void Clear()
{
foreach (var icon in _icons) Discard(icon.Material);
_icons.Clear();
_entityIcons.Clear();
foreach (var belt in _belts.Values) belt.Buffer.Release();
_belts.Clear();
}
}