Add planet lighting (#32522)
* Implements a Dynamic Lighting System on maps. * Edit: the night should be a little bit brighter and blue now. * Major edit: everything must be done on the client side now, with certain datafield replicated. Changes were outlined in the salvage to accommodate the new lighting system. * Edit: The offset is now serverside, this makes the time accurate in all situations. * Removing ununsed import * Minor tweaks * Tweak in time precision * Minor tweak + Unused import removed * Edit: apparently RealTime is better for what I'm looking for * Fix: Now the time is calculated correctly. * Minor tweaks * Adds condition for when the light should be updated * Add planet lighting * she * close-ish * c * bittersweat * Fixes * Revert "Merge branch '22719' into 2024-09-29-planet-lighting" This reverts commit 9f2785bb16aee47d794aa3eed8ae15004f97fc35, reversing changes made to 19649c07a5fb625423e08fc18d91c9cb101daa86. * Europa and day-night * weh * rooves working * Clean * Remove Europa * Fixes * fix * Update * Fix caves * Update for engine * Add sun shadows (planet lighting v2) For now mostly targeting walls and having the shadows change over time. Got the basic proof-of-concept working just needs a hell of a lot of polish. * Documentation * a * Fixes * Move blur to an overlay * Slughands * Fixes * Remove v2 work * Finalise --------- Co-authored-by: DoutorWhite <thedoctorwhite@gmail.com>
This commit is contained in:
56
Content.Shared/Light/Components/LightCycleComponent.cs
Normal file
56
Content.Shared/Light/Components/LightCycleComponent.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Map.Components;
|
||||
|
||||
namespace Content.Shared.Light.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Cycles through colors AKA "Day / Night cycle" on <see cref="MapLightComponent"/>
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
|
||||
public sealed partial class LightCycleComponent : Component
|
||||
{
|
||||
[DataField, AutoNetworkedField]
|
||||
public Color OriginalColor = Color.Transparent;
|
||||
|
||||
/// <summary>
|
||||
/// How long an entire cycle lasts
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public TimeSpan Duration = TimeSpan.FromMinutes(30);
|
||||
|
||||
[DataField, AutoNetworkedField]
|
||||
public TimeSpan Offset;
|
||||
|
||||
[DataField, AutoNetworkedField]
|
||||
public bool Enabled = true;
|
||||
|
||||
/// <summary>
|
||||
/// Should the offset be randomised upon MapInit.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public bool InitialOffset = true;
|
||||
|
||||
/// <summary>
|
||||
/// Trench of the oscillation.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public float MinLightLevel = 0f;
|
||||
|
||||
/// <summary>
|
||||
/// Peak of the oscillation
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public float MaxLightLevel = 3f;
|
||||
|
||||
[DataField, AutoNetworkedField]
|
||||
public float ClipLight = 1.25f;
|
||||
|
||||
[DataField, AutoNetworkedField]
|
||||
public Color ClipLevel = new Color(1f, 1f, 1.25f);
|
||||
|
||||
[DataField, AutoNetworkedField]
|
||||
public Color MinLevel = new Color(0.1f, 0.15f, 0.50f);
|
||||
|
||||
[DataField, AutoNetworkedField]
|
||||
public Color MaxLevel = new Color(2f, 2f, 5f);
|
||||
}
|
||||
13
Content.Shared/Light/Components/RoofComponent.cs
Normal file
13
Content.Shared/Light/Components/RoofComponent.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared.Light.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Will draw shadows over tiles flagged as roof tiles on the attached map.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
|
||||
public sealed partial class RoofComponent : Component
|
||||
{
|
||||
[DataField, AutoNetworkedField]
|
||||
public Color Color = Color.Black;
|
||||
}
|
||||
16
Content.Shared/Light/Components/TileEmissionComponent.cs
Normal file
16
Content.Shared/Light/Components/TileEmissionComponent.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared.Light.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Will draw lighting in a range around the tile.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
|
||||
public sealed partial class TileEmissionComponent : Component
|
||||
{
|
||||
[DataField, AutoNetworkedField]
|
||||
public float Range = 0.25f;
|
||||
|
||||
[DataField(required: true), AutoNetworkedField]
|
||||
public Color Color = Color.Transparent;
|
||||
}
|
||||
42
Content.Shared/Light/EntitySystems/SharedRoofSystem.cs
Normal file
42
Content.Shared/Light/EntitySystems/SharedRoofSystem.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using Content.Shared.Light.Components;
|
||||
using Content.Shared.Maps;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
|
||||
namespace Content.Shared.Light.EntitySystems;
|
||||
|
||||
/// <summary>
|
||||
/// Handles the roof flag for tiles that gets used for the RoofOverlay.
|
||||
/// </summary>
|
||||
public abstract class SharedRoofSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly SharedMapSystem _maps = default!;
|
||||
|
||||
public void SetRoof(Entity<MapGridComponent?, RoofComponent?> grid, Vector2i index, bool value)
|
||||
{
|
||||
if (!Resolve(grid, ref grid.Comp1, ref grid.Comp2, false))
|
||||
return;
|
||||
|
||||
if (!_maps.TryGetTile(grid.Comp1, index, out var tile))
|
||||
return;
|
||||
|
||||
var mask = (tile.Flags & (byte)TileFlag.Roof);
|
||||
var rooved = mask != 0x0;
|
||||
|
||||
if (rooved == value)
|
||||
return;
|
||||
|
||||
Tile newTile;
|
||||
|
||||
if (value)
|
||||
{
|
||||
newTile = tile.WithFlag((byte)(tile.Flags | (ushort)TileFlag.Roof));
|
||||
}
|
||||
else
|
||||
{
|
||||
newTile = tile.WithFlag((byte)(tile.Flags & ~(ushort)TileFlag.Roof));
|
||||
}
|
||||
|
||||
_maps.SetTile((grid.Owner, grid.Comp1), index, newTile);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Light.Components;
|
||||
using Content.Shared.Movement.Systems;
|
||||
using Content.Shared.Tools;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Array;
|
||||
using Robust.Shared.Utility;
|
||||
@@ -118,4 +120,11 @@ namespace Content.Shared.Maps
|
||||
TileId = id;
|
||||
}
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum TileFlag : byte
|
||||
{
|
||||
None = 0,
|
||||
Roof = 1 << 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,36 @@
|
||||
using Content.Shared.Maps;
|
||||
using Robust.Shared.Noise;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
||||
|
||||
namespace Content.Shared.Parallax.Biomes.Layers;
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed partial class BiomeTileLayer : IBiomeLayer
|
||||
{
|
||||
[DataField("noise")] public FastNoiseLite Noise { get; private set; } = new(0);
|
||||
[DataField] public FastNoiseLite Noise { get; private set; } = new(0);
|
||||
|
||||
/// <inheritdoc/>
|
||||
[DataField("threshold")]
|
||||
[DataField]
|
||||
public float Threshold { get; private set; } = 0.5f;
|
||||
|
||||
/// <inheritdoc/>
|
||||
[DataField("invert")] public bool Invert { get; private set; } = false;
|
||||
[DataField] public bool Invert { get; private set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Which tile variants to use for this layer. Uses all of the tile's variants if none specified
|
||||
/// </summary>
|
||||
[DataField("variants")]
|
||||
[DataField]
|
||||
public List<byte>? Variants = null;
|
||||
|
||||
[DataField("tile", required: true, customTypeSerializer: typeof(PrototypeIdSerializer<ContentTileDefinition>))]
|
||||
public string Tile = string.Empty;
|
||||
[DataField(required: true)]
|
||||
public ProtoId<ContentTileDefinition> Tile = string.Empty;
|
||||
|
||||
// TODO: Need some good engine solution to this, see FlagSerializer for what needs changing.
|
||||
/// <summary>
|
||||
/// Flags to set on the tile when placed.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public byte Flags = 0;
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@ public abstract class SharedBiomeSystem : EntitySystem
|
||||
if (layer is not BiomeTileLayer tileLayer)
|
||||
continue;
|
||||
|
||||
if (TryGetTile(indices, noiseCopy, tileLayer.Invert, tileLayer.Threshold, ProtoManager.Index<ContentTileDefinition>(tileLayer.Tile), tileLayer.Variants, out tile))
|
||||
if (TryGetTile(indices, noiseCopy, tileLayer.Invert, tileLayer.Threshold, ProtoManager.Index(tileLayer.Tile), tileLayer.Flags, tileLayer.Variants, out tile))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -142,7 +142,7 @@ public abstract class SharedBiomeSystem : EntitySystem
|
||||
/// <summary>
|
||||
/// Gets the underlying biome tile, ignoring any existing tile that may be there.
|
||||
/// </summary>
|
||||
private bool TryGetTile(Vector2i indices, FastNoiseLite noise, bool invert, float threshold, ContentTileDefinition tileDef, List<byte>? variants, [NotNullWhen(true)] out Tile? tile)
|
||||
private bool TryGetTile(Vector2i indices, FastNoiseLite noise, bool invert, float threshold, ContentTileDefinition tileDef, byte tileFlags, List<byte>? variants, [NotNullWhen(true)] out Tile? tile)
|
||||
{
|
||||
var found = noise.GetNoise(indices.X, indices.Y);
|
||||
found = invert ? found * -1 : found;
|
||||
@@ -163,7 +163,7 @@ public abstract class SharedBiomeSystem : EntitySystem
|
||||
variant = _tile.PickVariant(tileDef, (int) variantValue);
|
||||
}
|
||||
|
||||
tile = new Tile(tileDef.TileId, 0, variant);
|
||||
tile = new Tile(tileDef.TileId, flags: tileFlags, variant);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
116
Content.Shared/SharedLightCycleSystem.cs
Normal file
116
Content.Shared/SharedLightCycleSystem.cs
Normal file
@@ -0,0 +1,116 @@
|
||||
using Content.Shared.Light.Components;
|
||||
using Robust.Shared.Map.Components;
|
||||
|
||||
namespace Content.Shared;
|
||||
|
||||
public abstract class SharedLightCycleSystem : EntitySystem
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<LightCycleComponent, MapInitEvent>(OnCycleMapInit);
|
||||
SubscribeLocalEvent<LightCycleComponent, ComponentShutdown>(OnCycleShutdown);
|
||||
}
|
||||
|
||||
protected virtual void OnCycleMapInit(Entity<LightCycleComponent> ent, ref MapInitEvent args)
|
||||
{
|
||||
if (TryComp(ent.Owner, out MapLightComponent? mapLight))
|
||||
{
|
||||
ent.Comp.OriginalColor = mapLight.AmbientLightColor;
|
||||
Dirty(ent);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCycleShutdown(Entity<LightCycleComponent> ent, ref ComponentShutdown args)
|
||||
{
|
||||
if (TryComp(ent.Owner, out MapLightComponent? mapLight))
|
||||
{
|
||||
mapLight.AmbientLightColor = ent.Comp.OriginalColor;
|
||||
Dirty(ent.Owner, mapLight);
|
||||
}
|
||||
}
|
||||
|
||||
public static Color GetColor(Entity<LightCycleComponent> cycle, Color color, float time)
|
||||
{
|
||||
if (cycle.Comp.Enabled)
|
||||
{
|
||||
var lightLevel = CalculateLightLevel(cycle.Comp, time);
|
||||
var colorLevel = CalculateColorLevel(cycle.Comp, time);
|
||||
return new Color(
|
||||
(byte)Math.Min(255, color.RByte * colorLevel.R * lightLevel),
|
||||
(byte)Math.Min(255, color.GByte * colorLevel.G * lightLevel),
|
||||
(byte)Math.Min(255, color.BByte * colorLevel.B * lightLevel)
|
||||
);
|
||||
}
|
||||
|
||||
return color;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates light intensity as a function of time.
|
||||
/// </summary>
|
||||
public static double CalculateLightLevel(LightCycleComponent comp, float time)
|
||||
{
|
||||
var waveLength = MathF.Max(1, (float) comp.Duration.TotalSeconds);
|
||||
var crest = MathF.Max(0f, comp.MaxLightLevel);
|
||||
var shift = MathF.Max(0f, comp.MinLightLevel);
|
||||
return Math.Min(comp.ClipLight, CalculateCurve(time, waveLength, crest, shift, 6));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// It is important to note that each color must have a different exponent, to modify how early or late one color should stand out in relation to another.
|
||||
/// This "simulates" what the atmosphere does and is what generates the effect of dawn and dusk.
|
||||
/// The blue component must be a cosine function with half period, so that its minimum is at dawn and dusk, generating the "warm" color corresponding to these periods.
|
||||
/// As you can see in the values, the maximums of the function serve more to define the curve behavior,
|
||||
/// they must be "clipped" so as not to distort the original color of the lighting. In practice, the maximum values, in fact, are the clip thresholds.
|
||||
/// </summary>
|
||||
public static Color CalculateColorLevel(LightCycleComponent comp, float time)
|
||||
{
|
||||
var waveLength = MathF.Max(1f, (float) comp.Duration.TotalSeconds);
|
||||
|
||||
var red = MathF.Min(comp.ClipLevel.R,
|
||||
CalculateCurve(time,
|
||||
waveLength,
|
||||
MathF.Max(0f, comp.MaxLevel.R),
|
||||
MathF.Max(0f, comp.MinLevel.R),
|
||||
4f));
|
||||
|
||||
var green = MathF.Min(comp.ClipLevel.G,
|
||||
CalculateCurve(time,
|
||||
waveLength,
|
||||
MathF.Max(0f, comp.MaxLevel.G),
|
||||
MathF.Max(0f, comp.MinLevel.G),
|
||||
10f));
|
||||
|
||||
var blue = MathF.Min(comp.ClipLevel.B,
|
||||
CalculateCurve(time,
|
||||
waveLength / 2f,
|
||||
MathF.Max(0f, comp.MaxLevel.B),
|
||||
MathF.Max(0f, comp.MinLevel.B),
|
||||
2,
|
||||
waveLength / 4f));
|
||||
|
||||
return new Color(red, green, blue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a sinusoidal curve as a function of x (time). The other parameters serve to adjust the behavior of the curve.
|
||||
/// </summary>
|
||||
/// <param name="x"> It corresponds to the independent variable of the function, which in the context of this algorithm is the current time. </param>
|
||||
/// <param name="waveLength"> It's the wavelength of the function, it can be said to be the total duration of the light cycle. </param>
|
||||
/// <param name="crest"> It's the maximum point of the function, where it will have its greatest value. </param>
|
||||
/// <param name="shift"> It's the vertical displacement of the function, in practice it corresponds to the minimum value of the function. </param>
|
||||
/// <param name="exponent"> It is the exponent of the sine, serves to "flatten" the function close to its minimum points and make it "steeper" close to its maximum. </param>
|
||||
/// <param name="phase"> It changes the phase of the wave, like a "horizontal shift". It is important to transform the sinusoidal function into cosine, when necessary. </param>
|
||||
/// <returns> The result of the function. </returns>
|
||||
public static float CalculateCurve(float x,
|
||||
float waveLength,
|
||||
float crest,
|
||||
float shift,
|
||||
float exponent,
|
||||
float phase = 0)
|
||||
{
|
||||
var sen = MathF.Pow(MathF.Sin((MathF.PI * (phase + x)) / waveLength), exponent);
|
||||
return (crest - shift) * sen + shift;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user