diff --git a/Resources/Shaders/Internal/light_shared.swsl b/Resources/Shaders/Internal/light_shared.swsl index fbd2fdb20..6b1825f30 100644 --- a/Resources/Shaders/Internal/light_shared.swsl +++ b/Resources/Shaders/Internal/light_shared.swsl @@ -14,6 +14,8 @@ uniform highp vec2 lightCenter; uniform highp float lightRange; uniform highp float lightPower; uniform highp float lightSoftness; +uniform highp float lightFalloff; +uniform highp float lightCurveFactor; uniform highp float lightIndex; uniform sampler2D shadowMap; @@ -47,8 +49,15 @@ void fragment() discard; } - highp float dist = dot(diff, diff) + LIGHTING_HEIGHT; - highp float val = clamp((1.0 - clamp(sqrt(dist) / lightRange, 0.0, 1.0)) * (1.0 / (sqrt(dist + 1.0))), 0.0, 1.0); + // this implementation of light attenuation primarily adapted from + // https://lisyarus.github.io/blog/posts/point-light-attenuation.html + highp float sqr_dist = dot(diff, diff) + LIGHTING_HEIGHT; + + highp float s = clamp(sqrt(sqr_dist) / lightRange, 0.0, 1.0); + highp float s2 = s * s; + // controls curve by lerping between two variants (inverse-shape and inversequadratic-shape) + highp float curveFactor = mix(s, s2, clamp(lightCurveFactor, 0.0, 1.0)); + highp float val = clamp(((1 - s2) * (1 - s2)) / (1 + lightFalloff * curveFactor), 0.0, 1.0); val *= lightPower; val *= mask; diff --git a/Robust.Client/GameObjects/EntitySystems/PointLightSystem.cs b/Robust.Client/GameObjects/EntitySystems/PointLightSystem.cs index ad4b9336d..6db848821 100644 --- a/Robust.Client/GameObjects/EntitySystems/PointLightSystem.cs +++ b/Robust.Client/GameObjects/EntitySystems/PointLightSystem.cs @@ -29,6 +29,8 @@ namespace Robust.Client.GameObjects component.Enabled = state.Enabled; component.Offset = state.Offset; component.Softness = state.Softness; + component.Falloff = state.Falloff; + component.CurveFactor = state.CurveFactor; component.CastShadows = state.CastShadows; component.Energy = state.Energy; component.Radius = state.Radius; diff --git a/Robust.Client/Graphics/Clyde/Clyde.LightRendering.cs b/Robust.Client/Graphics/Clyde/Clyde.LightRendering.cs index 036fee7e5..cbc2da3e1 100644 --- a/Robust.Client/Graphics/Clyde/Clyde.LightRendering.cs +++ b/Robust.Client/Graphics/Clyde/Clyde.LightRendering.cs @@ -451,6 +451,8 @@ namespace Robust.Client.Graphics.Clyde var lastPower = float.NaN; var lastColor = new Color(float.NaN, float.NaN, float.NaN, float.NaN); var lastSoftness = float.NaN; + var lastFalloff = float.NaN; + var lastCurveFactor = float.NaN; Texture? lastMask = null; using (_prof.Group("Draw Lights")) @@ -504,6 +506,18 @@ namespace Robust.Client.Graphics.Clyde lightShader.SetUniformMaybe("lightSoftness", lastSoftness); } + if (!MathHelper.CloseToPercent(lastFalloff, component.Falloff)) + { + lastFalloff = component.Falloff; + lightShader.SetUniformMaybe("lightFalloff", lastFalloff); + } + + if (!MathHelper.CloseToPercent(lastCurveFactor, component.CurveFactor)) + { + lastCurveFactor = component.CurveFactor; + lightShader.SetUniformMaybe("lightCurveFactor", lastCurveFactor); + } + lightShader.SetUniformMaybe("lightCenter", lightPos); lightShader.SetUniformMaybe("lightIndex", component.CastShadows ? (i + 0.5f) / ShadowTexture.Height : -1); diff --git a/Robust.Shared/GameObjects/Components/Light/PointLightComponentState.cs b/Robust.Shared/GameObjects/Components/Light/PointLightComponentState.cs index cc0d3eb47..9aacc404f 100644 --- a/Robust.Shared/GameObjects/Components/Light/PointLightComponentState.cs +++ b/Robust.Shared/GameObjects/Components/Light/PointLightComponentState.cs @@ -14,6 +14,10 @@ public sealed class PointLightComponentState : ComponentState public float Softness; + public float Falloff; + + public float CurveFactor; + public bool CastShadows; public bool Enabled; diff --git a/Robust.Shared/GameObjects/Components/Light/SharedPointLightComponent.cs b/Robust.Shared/GameObjects/Components/Light/SharedPointLightComponent.cs index 248e7d60e..a1c9d0806 100644 --- a/Robust.Shared/GameObjects/Components/Light/SharedPointLightComponent.cs +++ b/Robust.Shared/GameObjects/Components/Light/SharedPointLightComponent.cs @@ -6,6 +6,7 @@ using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.ViewVariables; using System.Numerics; using Robust.Shared.IoC; +using Robust.Shared.Serialization; namespace Robust.Shared.GameObjects { @@ -33,6 +34,39 @@ namespace Robust.Shared.GameObjects [DataField("softness"), Animatable] public float Softness { get; set; } = 1f; + /// + /// Controls how quickly the light falls off in power in its radius. + /// A higher value means a stronger falloff. + /// + /// + /// The default value of 6.8 might seem suspect, but that's because this is a value which was introduced + /// years after SS14 already standardized its light values using an older attenuation curve, and this was the value + /// which, qualitatively, seemed about equivalent in brightness for the large majority of lights on the station + /// compared to the old function. + /// + /// See https://www.desmos.com/calculator/yjudaha0s6 for a demonstration of how this value affects the shape of the curve + /// for different light radii and curve factors. + /// + [DataField, Animatable] + public float Falloff { get; set; } = 6.8f; + + /// + /// Controls the shape of the curve used for point light attenuation. + /// This value may vary between 0 and 1. + /// A value of 0 gives a shape roughly equivalent to 1/1+distance (more or less realistic), + /// while a value of 1 gives a shape roughly equivalent to 1+distance^2 (closer to a sphere-shaped light) + /// + /// + /// This does not directly control the exponent of the denominator, though it might seem that way. + /// Rather, it just lerps between an inverse-shaped curve and an inverse-quadratic-shaped curve. + /// Values below 0 or above 1 are nonsensical. + /// + /// See https://www.desmos.com/calculator/yjudaha0s6 for a demonstration of how this value affects the shape of the curve + /// for different light radii and falloff values. + /// + [DataField, Animatable] + public float CurveFactor { get; set; } = 0.0f; + /// /// Whether this pointlight should cast shadows /// diff --git a/Robust.Shared/GameObjects/Systems/SharedPointLightSystem.cs b/Robust.Shared/GameObjects/Systems/SharedPointLightSystem.cs index e5cc78799..38102e1d1 100644 --- a/Robust.Shared/GameObjects/Systems/SharedPointLightSystem.cs +++ b/Robust.Shared/GameObjects/Systems/SharedPointLightSystem.cs @@ -89,6 +89,24 @@ public abstract class SharedPointLightSystem : EntitySystem Dirty(uid, comp); } + public void SetFalloff(EntityUid uid, float value, SharedPointLightComponent? comp = null) + { + if (!ResolveLight(uid, ref comp) || MathHelper.CloseToPercent(comp.Falloff, value)) + return; + + comp.Falloff = value; + Dirty(uid, comp); + } + + public void SetCurveFactor(EntityUid uid, float value, SharedPointLightComponent? comp = null) + { + if (!ResolveLight(uid, ref comp) || MathHelper.CloseToPercent(comp.CurveFactor, value)) + return; + + comp.CurveFactor = value; + Dirty(uid, comp); + } + protected static void OnLightGetState( EntityUid uid, SharedPointLightComponent component, @@ -102,6 +120,8 @@ public abstract class SharedPointLightSystem : EntitySystem Offset = component.Offset, Radius = component.Radius, Softness = component.Softness, + Falloff = component.Falloff, + CurveFactor = component.CurveFactor, CastShadows = component.CastShadows, }; }