From f1c36b4ccbdefbaaeaa15e0634765c95c4ff3478 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 12 Oct 2026 03:34:50 +0200 Subject: [PATCH 1/3] Vapour clouds are drawn as fog banks: one ZoneRenderer subscription, VapourCloudInstance presenter, DrawAhead for a free body ZoneRenderer loads and subscribes Zone.Clouds like mines and destroys a bank on removal; VapourCloudInstance places a Fog Patch brush at DrawAhead.Position(cloud.Body, Lead) and thins its _Depth with OpacityAt. The prefab and scene wiring follow in the next commit; this one compiles but CloudPrefab is unassigned. Co-Authored-By: Claude Opus 5.5 --- Assets/Scripts/ServerShared/DrawAhead.cs | 4 ++ .../Zone Display/VapourCloudInstance.cs | 46 +++++++++++++++++++ Assets/Scripts/Zone Display/ZoneRenderer.cs | 22 +++++++++ tests/Aetheria.Shared.Tests/DrawAheadTests.cs | 29 ++++++++++++ 4 files changed, 101 insertions(+) create mode 100644 Assets/Scripts/Zone Display/VapourCloudInstance.cs create mode 100644 tests/Aetheria.Shared.Tests/DrawAheadTests.cs diff --git a/Assets/Scripts/ServerShared/DrawAhead.cs b/Assets/Scripts/ServerShared/DrawAhead.cs index 13da8aae9..c15ce57e2 100644 --- a/Assets/Scripts/ServerShared/DrawAhead.cs +++ b/Assets/Scripts/ServerShared/DrawAhead.cs @@ -19,6 +19,10 @@ public static class DrawAhead ? entity.Position : entity.Position + float3(entity.Velocity.x, 0, entity.Velocity.y) * lead; + // A free body (a vapour cloud) drawn where the frame falls lead sim seconds after the last step, by the velocity + // that step moved it with: the launch velocity and the drift together. + public static float2 Position(KinematicBody body, float lead) => body.Position + body.TotalVelocity * lead; + // The ship's rotation turned about its own up axis by the rate its last step turned, for lead seconds. public static quaternion Rotation(Ship ship, float lead) => ship.WormholeAnimationInProgress ? ship.Rotation : Turned(ship.Rotation, ship.TurnRate * lead); diff --git a/Assets/Scripts/Zone Display/VapourCloudInstance.cs b/Assets/Scripts/Zone Display/VapourCloudInstance.cs new file mode 100644 index 000000000..74ec1abf5 --- /dev/null +++ b/Assets/Scripts/Zone Display/VapourCloudInstance.cs @@ -0,0 +1,46 @@ +using CultMath; +using CultMath.UnityBridge; +using UnityEngine; + +// How one vented vapour cloud looks: a fog-bank brush painted into the Patch layer, which the volumetric pass reads as +// patch thickness. Zone owns every cloud fact (existence, body, radius, opacity); this reads them and draws, and writes +// nothing back (rulings vapour-cloud-obscuration, unity-presents-only-r3). The brush's depth thins with the cloud's +// opacity, so the bank fades to nothing at the cloud's lifetime. +public class VapourCloudInstance : MonoBehaviour +{ + // The world height the scene's patch brushes sit at (Depth Micro), inside the Patch camera's clip range. + public float PatchHeight = -5f; + + private static readonly int Depth = Shader.PropertyToID("_Depth"); + + private Zone _zone; + private VapourCloud _cloud; + private MeshRenderer _renderer; + private MaterialPropertyBlock _block; + private float _authoredDepth; + + public void Bind(Zone zone, VapourCloud cloud) + { + _zone = zone; + _cloud = cloud; + _renderer = GetComponent(); + _block = new MaterialPropertyBlock(); + _authoredDepth = _renderer.sharedMaterial.GetFloat(Depth); + Place(); + } + + private void Update() => Place(); + + private void Place() + { + var p = DrawAhead.Position(_cloud.Body, ActionGameManager.Clock.Lead); + transform.position = new Vector3(p.x, PatchHeight, p.y); + transform.localScale = Vector3.one * (2f * _cloud.Radius); + + // A cloud with no opacity left is already gone from the sim; a zero Opacity would divide by zero. + var thinning = _cloud.Opacity > 0f ? _cloud.OpacityAt(_zone.Time) / _cloud.Opacity : 0f; + _renderer.GetPropertyBlock(_block); + _block.SetFloat(Depth, _authoredDepth * thinning); + _renderer.SetPropertyBlock(_block); + } +} diff --git a/Assets/Scripts/Zone Display/ZoneRenderer.cs b/Assets/Scripts/Zone Display/ZoneRenderer.cs index e969670db..d39e0d2ea 100644 --- a/Assets/Scripts/Zone Display/ZoneRenderer.cs +++ b/Assets/Scripts/Zone Display/ZoneRenderer.cs @@ -45,6 +45,7 @@ public class ZoneRenderer : MonoBehaviour public GridObject GearPickup; public GridObject WeaponPickup; public MineInstance MinePrefab; + public VapourCloudInstance CloudPrefab; public Material[] MapGravityMaterials; [Header("Tour")] public bool Tour; @@ -95,6 +96,7 @@ public class ZoneRenderer : MonoBehaviour public Dictionary WormholeInstances = new Dictionary(); private List _loot = new List(); private Dictionary _mines = new Dictionary(); + private Dictionary _clouds = new Dictionary(); public Zone Zone { get; private set; } public ItemManager ItemManager { get; set; } @@ -234,6 +236,15 @@ public class ZoneRenderer : MonoBehaviour instance.Blast(); })); + // Every vapour cloud, whoever vented it, is drawn through this one subscription. A cloud leaves Zone.Clouds + // when its opacity has reached nothing, so a removal destroys its fog bank with nothing left to show. + foreach (var cloud in zone.Clouds) LoadCloud(zone, cloud); + _zoneSubscriptions.Add(zone.Clouds.ObserveAdd().Subscribe(e => LoadCloud(zone, e.Value))); + _zoneSubscriptions.Add(zone.Clouds.ObserveRemove().Subscribe(e => + { + if (_clouds.Remove(e.Value, out var instance)) Destroy(instance.gameObject); + })); + if (zone.GalaxyZone != null) { foreach (var adjacentZone in zone.GalaxyZone.AdjacentZones) @@ -263,6 +274,13 @@ public class ZoneRenderer : MonoBehaviour _mines.Add(mine, instance); } + private void LoadCloud(Zone zone, VapourCloud cloud) + { + var instance = Instantiate(CloudPrefab); + instance.Bind(zone, cloud); + _clouds.Add(cloud, instance); + } + public void AddWormhole(Wormhole wormhole) { var instance = Instantiate(WormholePrefab); @@ -290,6 +308,10 @@ public class ZoneRenderer : MonoBehaviour if (instance) Destroy(instance.gameObject); _mines.Clear(); + foreach (var instance in _clouds.Values) + if (instance) Destroy(instance.gameObject); + _clouds.Clear(); + foreach(var gridObject in _loot) if(gridObject) Destroy(gridObject.gameObject); _loot.Clear(); diff --git a/tests/Aetheria.Shared.Tests/DrawAheadTests.cs b/tests/Aetheria.Shared.Tests/DrawAheadTests.cs new file mode 100644 index 000000000..208af3589 --- /dev/null +++ b/tests/Aetheria.Shared.Tests/DrawAheadTests.cs @@ -0,0 +1,29 @@ +using Xunit; +using float2 = CultMath.float2; + +// DrawAhead draws a free body where the frame falls lead sim seconds after the last step, by the velocity that step +// moved it with. Expected values are worked from first principles: position plus launch velocity and drift, times lead. +public sealed class DrawAheadTests +{ + [Fact] + public void ABodyIsDrawnAheadByItsTotalVelocity() + { + var body = new KinematicBody { Position = new float2(10f, 20f), Velocity = new float2(3f, -1f), Drift = new float2(.5f, 2f) }; + + var drawn = DrawAhead.Position(body, .5f); + + Assert.Equal(10f + (3f + .5f) * .5f, drawn.x, 4); + Assert.Equal(20f + (-1f + 2f) * .5f, drawn.y, 4); + } + + [Fact] + public void ABodyDrawnWithNoLeadIsWhereTheSimPutIt() + { + var body = new KinematicBody { Position = new float2(10f, 20f), Velocity = new float2(3f, -1f), Drift = new float2(.5f, 2f) }; + + var drawn = DrawAhead.Position(body, 0f); + + Assert.Equal(10f, drawn.x, 4); + Assert.Equal(20f, drawn.y, 4); + } +} From 1660a68a8ab6b47edf652ab7f721d442dbf3eb90 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 12 Oct 2026 03:46:24 +0200 Subject: [PATCH 2/3] Fog Patch prefab carries VapourCloudInstance at the origin and unit scale; ZoneRenderer.CloudPrefab assigned in ARPG Wired through the Editor in batchmode (component, root pose reset, field assignment); the scene diff is the one CloudPrefab line, the Editor's unrelated field churn reverted. Builds (Unity compile 0 error CS). Co-Authored-By: Claude Opus 5.5 --- Assets/Prefabs/RPG/Fog Patch.prefab | 27 ++++++++++++++++--- Assets/Scenes/ARPG.unity | 2 ++ .../Zone Display/VapourCloudInstance.cs.meta | 2 ++ 3 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 Assets/Scripts/Zone Display/VapourCloudInstance.cs.meta diff --git a/Assets/Prefabs/RPG/Fog Patch.prefab b/Assets/Prefabs/RPG/Fog Patch.prefab index 64769c721..52cdefc65 100644 --- a/Assets/Prefabs/RPG/Fog Patch.prefab +++ b/Assets/Prefabs/RPG/Fog Patch.prefab @@ -11,6 +11,7 @@ GameObject: - component: {fileID: 3040023561780424834} - component: {fileID: 8539532519029615782} - component: {fileID: 4655154266675363797} + - component: {fileID: 2914522964471179774} m_Layer: 11 m_Name: Fog Patch m_TagString: Untagged @@ -25,13 +26,13 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 791530951415938407} + serializedVersion: 2 m_LocalRotation: {x: 0.7071068, y: -0, z: -0, w: 0.7071068} - m_LocalPosition: {x: -3148.4, y: -253.6, z: -1108.1} - m_LocalScale: {x: 1513.8295, y: 1513.8295, z: 1513.8295} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 0} - m_RootOrder: 0 m_LocalEulerAnglesHint: {x: 90, y: 0, z: 0} --- !u!33 &8539532519029615782 MeshFilter: @@ -58,6 +59,11 @@ MeshRenderer: m_ReflectionProbeUsage: 1 m_RayTracingMode: 2 m_RayTraceProcedural: 0 + m_RayTracingAccelStructBuildFlagsOverride: 0 + m_RayTracingAccelStructBuildFlags: 1 + m_SmallMeshCulling: 1 + m_ForceMeshLod: -1 + m_MeshLodSelectionBias: 0 m_RenderingLayerMask: 1 m_RendererPriority: 0 m_Materials: @@ -79,7 +85,22 @@ MeshRenderer: m_AutoUVMaxDistance: 0.5 m_AutoUVMaxAngle: 89 m_LightmapParameters: {fileID: 0} + m_GlobalIlluminationMeshLod: 0 m_SortingLayerID: 0 m_SortingLayer: 0 m_SortingOrder: 0 + m_MaskInteraction: 0 m_AdditionalVertexStreams: {fileID: 0} +--- !u!114 &2914522964471179774 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 791530951415938407} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: a792b8f456004d04997a26557b338d1c, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::VapourCloudInstance + PatchHeight: -5 diff --git a/Assets/Scenes/ARPG.unity b/Assets/Scenes/ARPG.unity index 39831ebcf..02b69d869 100644 --- a/Assets/Scenes/ARPG.unity +++ b/Assets/Scenes/ARPG.unity @@ -16227,6 +16227,8 @@ MonoBehaviour: type: 3} MinePrefab: {fileID: 7283754118921506074, guid: b04728b2b65e057429eb6e8529299c8a, type: 3} + CloudPrefab: {fileID: 2914522964471179774, guid: 4084aa3d08e52e04f880ad13daa409b4, + type: 3} MapGravityMaterials: - {fileID: 2100000, guid: 841e4091d19548347bd59115047b4e49, type: 2} - {fileID: 2100000, guid: a92fc6d958edb9f46b6e58134f609cc0, type: 2} diff --git a/Assets/Scripts/Zone Display/VapourCloudInstance.cs.meta b/Assets/Scripts/Zone Display/VapourCloudInstance.cs.meta new file mode 100644 index 000000000..9c645e059 --- /dev/null +++ b/Assets/Scripts/Zone Display/VapourCloudInstance.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: a792b8f456004d04997a26557b338d1c \ No newline at end of file From 3da1622478c43366c6c6c04561edfa4252064c24 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 12 Oct 2026 04:53:18 +0200 Subject: [PATCH 3/3] Vapour presenter: draw the sim's OpacityAt directly, not divided by the cloud's raw Opacity OpacityAt is already saturated to 0..1 by the sim, so a 0.3 vent reads thinner than a full cloud and nothing above full draws thinner. Builds (Unity compile run after). Co-Authored-By: Claude Opus 5.5 --- Assets/Scripts/Zone Display/VapourCloudInstance.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Assets/Scripts/Zone Display/VapourCloudInstance.cs b/Assets/Scripts/Zone Display/VapourCloudInstance.cs index 74ec1abf5..594221332 100644 --- a/Assets/Scripts/Zone Display/VapourCloudInstance.cs +++ b/Assets/Scripts/Zone Display/VapourCloudInstance.cs @@ -4,8 +4,7 @@ using UnityEngine; // How one vented vapour cloud looks: a fog-bank brush painted into the Patch layer, which the volumetric pass reads as // patch thickness. Zone owns every cloud fact (existence, body, radius, opacity); this reads them and draws, and writes -// nothing back (rulings vapour-cloud-obscuration, unity-presents-only-r3). The brush's depth thins with the cloud's -// opacity, so the bank fades to nothing at the cloud's lifetime. +// nothing back (rulings vapour-cloud-obscuration, unity-presents-only-r3). The brush's depth scales with the sim's OpacityAt, so the bank fades to nothing at the cloud's lifetime. public class VapourCloudInstance : MonoBehaviour { // The world height the scene's patch brushes sit at (Depth Micro), inside the Patch camera's clip range. @@ -37,10 +36,9 @@ public class VapourCloudInstance : MonoBehaviour transform.position = new Vector3(p.x, PatchHeight, p.y); transform.localScale = Vector3.one * (2f * _cloud.Radius); - // A cloud with no opacity left is already gone from the sim; a zero Opacity would divide by zero. - var thinning = _cloud.Opacity > 0f ? _cloud.OpacityAt(_zone.Time) / _cloud.Opacity : 0f; + // The sim's own opacity, already saturated to 0..1 (VapourCloud.OpacityAt); the presenter adds no rule of its own. _renderer.GetPropertyBlock(_block); - _block.SetFloat(Depth, _authoredDepth * thinning); + _block.SetFloat(Depth, _authoredDepth * _cloud.OpacityAt(_zone.Time)); _renderer.SetPropertyBlock(_block); } }