More accurate & controllable pointlight attenuation (#6160)

* modify light attenuation function

* support for changing attenuation curve type + lots of docs

* this is what i defaulted to typing in a prototype, so i guess it should just be this instead

* Allow a continuous range of values between inverse and inversequadratic rather than two set curves

* calc is slang for calculator

* fix

* oops committed it at 1 while testing i think, values are balanced for 0
This commit is contained in:
Kara
2025-09-18 14:58:35 +00:00
committed by GitHub
parent c6863033a5
commit 94e60e0b10
6 changed files with 85 additions and 2 deletions

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);

View File

@@ -14,6 +14,10 @@ public sealed class PointLightComponentState : ComponentState
public float Softness;
public float Falloff;
public float CurveFactor;
public bool CastShadows;
public bool Enabled;

View File

@@ -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;
/// <summary>
/// Controls how quickly the light falls off in power in its radius.
/// A higher value means a stronger falloff.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
[DataField, Animatable]
public float Falloff { get; set; } = 6.8f;
/// <summary>
/// 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)
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
[DataField, Animatable]
public float CurveFactor { get; set; } = 0.0f;
/// <summary>
/// Whether this pointlight should cast shadows
/// </summary>

View File

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