mirror of
https://github.com/Facepunch/sbox-public.git
synced 2026-07-31 15:58:27 -04:00
Fix Hammer Shadows Not Rendering Properly (#5334)
* Fix hammer lights not casting shadows and having weird lighting after they became components * Hammer lights have cone mask from the lightmap itself, we can take advantage of that and tighten the cone for sharper shadows * Depth buffer bias should track cone width and map resolution, not light radius A range-5000 light could get ~25x bias and heavy peter-panning, same for overly-wide spots * Hammer lights have no concept of "hardness" - let's make them reasonably sharp by default a bit like previous behaviour * Tweaks * format
This commit is contained in:
@@ -153,7 +153,8 @@ public class SceneMapLoader : MapLoader
|
||||
Attenuation0 = kv.GetValue( "attenuation0", 0.0f ),
|
||||
Attenuation1 = kv.GetValue( "attenuation1", 0.0f ),
|
||||
Attenuation2 = kv.GetValue( "attenuation2", 1.0f ),
|
||||
CastShadows = kv.GetValue<int>( "castshadows" ) == 1,
|
||||
// Default 1 matches Hammer ShadowType.Yes. 0 = none, 1 = realtime (+ baked), 2 = baked-only.
|
||||
CastShadows = kv.GetValue( "castshadows", 1 ) == 1,
|
||||
LightCookie = kv.GetResource<Texture>( "lightcookie" ),
|
||||
BakeLightIndex = kv.GetValue( "bakelightindex", -1 ),
|
||||
BakeLightIndexScale = kv.GetValue( "bakelightindexscale", 1.0f ),
|
||||
@@ -173,6 +174,7 @@ public class SceneMapLoader : MapLoader
|
||||
public readonly Light.LegacyLightData ToLegacyData() => new()
|
||||
{
|
||||
DirectLight = DirectLight,
|
||||
CastShadows = CastShadows,
|
||||
BakeLightIndex = BakeLightIndex,
|
||||
BakeLightIndexScale = BakeLightIndexScale,
|
||||
BakedLightIndexing = BakedLightIndexing,
|
||||
|
||||
@@ -23,6 +23,7 @@ public abstract class Light : Component, IColorProvider, ExecuteInEditor, ITinta
|
||||
internal struct LegacyLightData
|
||||
{
|
||||
public int DirectLight;
|
||||
public bool CastShadows;
|
||||
public int BakeLightIndex;
|
||||
public float BakeLightIndexScale;
|
||||
public bool BakedLightIndexing;
|
||||
@@ -31,21 +32,30 @@ public abstract class Light : Component, IColorProvider, ExecuteInEditor, ITinta
|
||||
public bool RenderSpecular;
|
||||
public int FogLighting;
|
||||
|
||||
// Only set by light types whose components don't expose these (falloff, point cookie).
|
||||
// Only set by light types whose components don't expose these (falloff, constant attn, point cookie).
|
||||
public float? ConstantAttenuation;
|
||||
public float? LinearAttenuation;
|
||||
public float? QuadraticAttenuation;
|
||||
public float? FallOff;
|
||||
public Texture Cookie;
|
||||
|
||||
public readonly void ApplyTo( CSceneLightObject light )
|
||||
{
|
||||
// Lightbinner only allocates realtime shadow maps for BAKED lights when MIXED_SHADOWS
|
||||
// is set (Stationary, or Baked with castshadows enabled). Without it, indexed/baked
|
||||
// lights light the scene but never cast shadows.
|
||||
switch ( DirectLight )
|
||||
{
|
||||
case 3: // HAMMER_DIRECT_LIGHT_STATIONARY
|
||||
case 3: // HAMMER_DIRECT_LIGHT_STATIONARY — always mixed
|
||||
light.SetLightFlags( light.GetLightFlags() | 16 ); // LIGHTTYPE_FLAGS_MIXED_SHADOWS
|
||||
light.SetLightFlags( light.GetLightFlags() | 32 ); // LIGHTTYPE_FLAGS_BAKED
|
||||
break;
|
||||
case 1: // HAMMER_DIRECT_LIGHT_BAKED
|
||||
light.SetLightFlags( light.GetLightFlags() | 32 ); // LIGHTTYPE_FLAGS_BAKED
|
||||
// Hammer default is castshadows=Yes on baked lights; treat that as mixed so
|
||||
// indexed map lights keep realtime dynamic shadows (static comes from lightmaps).
|
||||
if ( CastShadows )
|
||||
light.SetLightFlags( light.GetLightFlags() | 16 ); // LIGHTTYPE_FLAGS_MIXED_SHADOWS
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -57,8 +67,10 @@ public abstract class Light : Component, IColorProvider, ExecuteInEditor, ITinta
|
||||
light.SetRenderSpecular( RenderSpecular );
|
||||
light.SetFogLightingMode( FogLighting );
|
||||
|
||||
if ( ConstantAttenuation is { } constant ) light.SetConstantAttn( constant );
|
||||
if ( LinearAttenuation is { } linear ) light.SetLinearAttn( linear );
|
||||
if ( QuadraticAttenuation is { } quadratic ) light.SetQuadraticAttn( quadratic );
|
||||
if ( FallOff is { } fallOff ) light.SetFallOff( fallOff );
|
||||
if ( Cookie is not null ) light.SetLightCookie( Cookie.native );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -678,8 +678,8 @@ file class MapComponentMapLoader : SceneMapLoader
|
||||
//
|
||||
void CreateLightComponent( GameObject go, ObjectEntry kv, LightType type )
|
||||
{
|
||||
// Never network these - LegacyData is internal (not serialized), so a snapshot copy
|
||||
// would arrive half-configured, and every client builds an identical light from the vpk anyway.
|
||||
// Never network these — LegacyData is internal (not serialized), so a snapshot copy would
|
||||
// arrive half-configured. Every client builds an identical light from the map VPK anyway.
|
||||
go.NetworkMode = NetworkMode.Never;
|
||||
|
||||
var data = LightData.Parse( kv, type );
|
||||
@@ -698,8 +698,16 @@ file class MapComponentMapLoader : SceneMapLoader
|
||||
spot.ConeOuter = data.OuterConeAngle;
|
||||
spot.Cookie = data.LightCookie;
|
||||
|
||||
// Hammer lights have attenuation and cone mask from the lightmap itself
|
||||
// We can take advantage of that and tighten the cone for sharper shadows
|
||||
spot.ConeInner = MathF.Min( data.InnerConeAngle, 80.0f );
|
||||
spot.ConeOuter = MathF.Min( data.OuterConeAngle, 80.0f );
|
||||
|
||||
// Native quadratic is the Hammer coefficient; SceneLight.QuadraticAttenuation scales by 10000.
|
||||
legacy.ConstantAttenuation = data.Attenuation0;
|
||||
legacy.LinearAttenuation = data.Attenuation1;
|
||||
legacy.QuadraticAttenuation = data.Attenuation2;
|
||||
legacy.FallOff = data.FallOff;
|
||||
|
||||
light = spot;
|
||||
}
|
||||
@@ -708,6 +716,7 @@ file class MapComponentMapLoader : SceneMapLoader
|
||||
var point = go.Components.Create<PointLight>();
|
||||
point.Radius = data.Range;
|
||||
|
||||
legacy.ConstantAttenuation = data.Attenuation0;
|
||||
legacy.LinearAttenuation = data.Attenuation1;
|
||||
legacy.QuadraticAttenuation = data.Attenuation2;
|
||||
legacy.Cookie = data.LightCookie; // PointLight doesn't expose a cookie property
|
||||
@@ -719,8 +728,13 @@ file class MapComponentMapLoader : SceneMapLoader
|
||||
light = go.Components.Create<DirectionalLight>();
|
||||
}
|
||||
|
||||
// Hammer lights have no concept of "hardness" — let's make them reasonably sharp by default.
|
||||
light.ShadowHardness = 0.5f;
|
||||
|
||||
light.LightColor = data.FinalColor;
|
||||
light.Shadows = data.CastShadows;
|
||||
// Fog mode / render diffuse-specular / attenuation ride in LegacyData — applied when the
|
||||
// scene light is created (OnEnabled). Component FogMode can't express Hammer's "Baked" fog.
|
||||
light.LegacyData = legacy;
|
||||
}
|
||||
|
||||
|
||||
@@ -251,20 +251,20 @@ internal partial class ShadowMapper
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes a per-light bias scale factor based on the shadow frustum's texel size.
|
||||
/// Wider cones and larger ranges produce bigger shadow map texels in world space,
|
||||
/// requiring proportionally more bias to prevent acne. Matches Unity URP's approach
|
||||
/// of scaling bias by <c>frustumSize / resolution</c>.
|
||||
/// Scale for rasterizer depth bias. Uses texel-to-depth ratio (tanθ / res),
|
||||
/// same idea as CSM Width/Far — not world-space texel size.
|
||||
/// Normalized so a 45° half-angle map at BiasScaleReferenceResolution is 1.0.
|
||||
/// </summary>
|
||||
|
||||
static float ComputeBiasScale( float halfAngleDegrees, float range, int resolution )
|
||||
{
|
||||
float frustumSize = MathF.Tan( halfAngleDegrees * MathF.PI / 180f ) * range;
|
||||
float texelSize = frustumSize / resolution;
|
||||
const int BiasScaleReferenceResolution = 1024;
|
||||
|
||||
// Normalize against a reference texel size so that a typical mid-range spotlight
|
||||
// (e.g. 45° half-angle, 200 range, 1024 res) gets a scale of ~1.0.
|
||||
const float ReferenceTexelSize = 0.2f;
|
||||
return MathF.Max( 1f, texelSize / ReferenceTexelSize );
|
||||
// (tanθ / resolution) / (tan45° / referenceRes) — range is unused (cancels for depth-unit bias).
|
||||
// Cap at 1: scaling *up* for low-res/wide cones only detaches shadows (peter-panning).
|
||||
return Math.Min( 1f, MathF.Tan( halfAngleDegrees * MathF.PI / 180f )
|
||||
* BiasScaleReferenceResolution
|
||||
/ Math.Max( resolution, 1 ) );
|
||||
}
|
||||
|
||||
internal static int GetDesiredResolution( float screenSizePercent, int viewportSize )
|
||||
|
||||
Reference in New Issue
Block a user