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,
};
}