14#include "spdlog/spdlog.h"
20 constexpr float kEps = 1e-4f;
22 float square(
const float value)
27 float saturate(
const float value)
29 return std::clamp(value, 0.0f, 1.0f);
32 float smoothstep(
const float edge0,
const float edge1,
const float x)
34 if (std::abs(edge1 - edge0) <= 1e-6f) {
35 return x < edge0 ? 0.0f : 1.0f;
37 const float t = saturate((x - edge0) / (edge1 - edge0));
38 return t * t * (3.0f - 2.0f * t);
41 bool nearlyEqual(
const float a,
const float b,
const float eps = kEps)
43 return std::abs(a - b) <= eps;
46 float getFalloffLinear(
const float lightRadius,
const Vector3& lightDir)
48 const float radius = std::max(lightRadius, 1e-4f);
49 const float d = lightDir.length();
50 return std::max((radius - d) / radius, 0.0f);
53 float getFalloffInvSquared(
const float lightRadius,
const Vector3& lightDir)
55 const float sqrDist = lightDir.lengthSquared();
56 float falloff = 1.0f / (sqrDist + 1.0f);
57 const float invRadius = 1.0f / std::max(lightRadius, 1e-4f);
60 falloff *= square(saturate(1.0f - square(sqrDist * square(invRadius))));
64 float getSpotEffect(
const Vector3& lightSpotDir,
const float lightInnerConeAngle,
65 const float lightOuterConeAngle,
const Vector3& lightDirNorm)
67 const float cosAngle = lightDirNorm.dot(lightSpotDir);
68 return smoothstep(lightOuterConeAngle, lightInnerConeAngle, cosAngle);
71 struct LocalLightEvalInput
73 Vector3 lightPosition;
74 Vector3 spotDirection;
76 bool linearFalloff =
true;
78 float innerConeCos = 1.0f;
79 float outerConeCos = 1.0f;
83 float evaluateLocalAttenuation(
const LocalLightEvalInput& in)
85 const Vector3 lightDirW = in.lightPosition - in.worldPos;
86 const float lightDirLenSq = lightDirW.lengthSquared();
87 if (lightDirLenSq <= 1e-8f) {
88 return in.linearFalloff ? 1.0f : 16.0f;
92 float attenuation = in.linearFalloff ? getFalloffLinear(in.range, lightDirW) : getFalloffInvSquared(in.range, lightDirW);
94 attenuation *= getSpotEffect(in.spotDirection.normalized(), in.innerConeCos, in.outerConeCos, -dLightDirNormW);
102 static bool ran =
false;
103 static bool pass =
true;
109 auto check = [&](
const bool condition,
const char* message) {
112 spdlog::error(
"Lighting validation failed: {}", message);
117 check(nearlyEqual(1.0f, 1.0f),
"directional baseline attenuation must be 1.0");
121 LocalLightEvalInput in{};
122 in.lightPosition =
Vector3(0.0f, 0.0f, 0.0f);
124 in.linearFalloff =
true;
126 in.worldPos =
Vector3(0.0f, 0.0f, 0.0f);
127 check(nearlyEqual(evaluateLocalAttenuation(in), 1.0f),
"point linear attenuation at source must be 1.0");
129 in.worldPos =
Vector3(0.0f, 0.0f, 5.0f);
130 check(nearlyEqual(evaluateLocalAttenuation(in), 0.5f),
"point linear attenuation at half range must be 0.5");
132 in.worldPos =
Vector3(0.0f, 0.0f, 10.0f);
133 check(nearlyEqual(evaluateLocalAttenuation(in), 0.0f),
"point linear attenuation at range limit must be 0.0");
138 LocalLightEvalInput in{};
139 in.lightPosition =
Vector3(0.0f, 0.0f, 0.0f);
141 in.linearFalloff =
false;
143 in.worldPos =
Vector3(0.0f, 0.0f, 1.0f);
144 const float nearValue = evaluateLocalAttenuation(in);
145 in.worldPos =
Vector3(0.0f, 0.0f, 5.0f);
146 const float midValue = evaluateLocalAttenuation(in);
147 check(nearValue > midValue,
"point inverse-squared attenuation must decrease with distance");
149 in.worldPos =
Vector3(0.0f, 0.0f, 12.0f);
150 check(nearlyEqual(evaluateLocalAttenuation(in), 0.0f),
"point inverse-squared attenuation must clamp to 0 outside range window");
155 const float innerCone = std::cos(20.0f * std::numbers::pi_v<float> / 180.0f);
156 const float outerCone = std::cos(30.0f * std::numbers::pi_v<float> / 180.0f);
157 LocalLightEvalInput in{};
158 in.lightPosition =
Vector3(0.0f, 0.0f, 0.0f);
159 in.spotDirection =
Vector3(0.0f, 0.0f, -1.0f);
161 in.innerConeCos = innerCone;
162 in.outerConeCos = outerCone;
163 in.linearFalloff =
true;
166 in.worldPos =
Vector3(0.0f, 0.0f, -5.0f);
167 const float inside = evaluateLocalAttenuation(in);
168 check(inside >= 0.99f,
"spot attenuation should be near 1.0 inside inner cone");
170 in.worldPos =
Vector3(5.0f, 0.0f, -5.0f);
171 const float outside = evaluateLocalAttenuation(in);
172 check(outside <= 1e-3f,
"spot attenuation should be near 0.0 outside outer cone");
174 in.worldPos =
Vector3(2.0f, 0.0f, -5.0f);
175 const float between = evaluateLocalAttenuation(in);
176 check(between > 0.0f && between < 1.0f,
"spot attenuation should interpolate between cones");
180 assert(
false &&
"Lighting self-test failed");
182 spdlog::info(
"Lighting validation self-test passed.");
bool runLightingValidationSelfTest()
3D vector for positions, directions, and normals with multi-backend SIMD acceleration.
Vector3 normalized() const