Add bounded point and spot shadows with fallback diagnostics

This commit is contained in:
Emil
2026-09-24 02:51:11 +03:00
parent 252f7e2e91
commit a5fb2167d6
9 changed files with 384 additions and 23 deletions
+15
View File
@@ -29,6 +29,21 @@ with tempfile.TemporaryDirectory(prefix="faset-player-diagnostics-") as temporar
report = json.loads(profile.read_text(encoding="utf-8"))
assert report["completed_frames"] == 1 and len(report["samples"]) == 1, report
assert report["samples"][0]["tick"] == 1, report["samples"]
lighting = report["samples"][0]
assert lighting["effective_lighting_path"] == "forward", lighting
for field in ["submitted_local_lights", "omitted_local_lights",
"requested_sun_cascades", "effective_sun_cascades",
"requested_local_shadow_faces", "local_shadow_faces",
"local_shadow_tiles", "dropped_shadow_faces",
"dropped_point_shadow_faces", "shadow_atlas_full_drops",
"shadow_caster_budget_drops", "shadow_unavailable_drops",
"shadow_caster_draws", "sun_shadow_atlas_bytes",
"local_shadow_atlas_bytes", "gpu_main_raster_ms",
"gpu_sun_shadow_ms", "gpu_local_shadow_ms"]:
assert field in lighting, (field, lighting)
assert lighting["submitted_local_lights"] == 0 and \
lighting["effective_sun_cascades"] == 0 and \
lighting["local_shadow_faces"] == 0, lighting
# The same linked v2 schema must validate without registering or invoking behavior.
validated = subprocess.run([sys.argv[1], "--scene", str(scene), "--validate"],
+150 -4
View File
@@ -145,13 +145,159 @@ void sun() {
two_d.stats.gpu_sun_shadow_ms == 0,
"Sprite-only rendering spends no sun shadow GPU work");
}
Snapshot local_scene(LocalLight::Kind kind, bool caster_shadow) {
Snapshot result;
result.view_id = "p3-local-shadow";
result.eye = {0, 5, 8};
const auto view = look_at(result.eye, {0, -1, 0});
const auto projection = orthographic(-3, 3, -2.25f, 2.25f, .1f, 50);
result.projection = projection;
result.view_projection = multiply(projection, view);
result.camera_frustum = CameraFrustum{view, projection, .1f, 50, false};
result.authored_lights_present = true;
DrawItem floor;
floor.mesh = cube_mesh();
floor.model = transform({0, -1, 0}, {}, {8, .1f, 8});
floor.color = {.8f, .8f, .8f, 1};
floor.instance_key = "floor";
result.draws.push_back(floor);
DrawItem caster;
caster.mesh = cube_mesh();
caster.model = transform({0, .7f, 0}, {}, {.8f, .8f, .8f});
caster.color = {.4f, .4f, .4f, 1};
caster.cast_shadow = caster_shadow;
caster.instance_key = "caster";
result.draws.push_back(caster);
LocalLight light;
light.kind = kind;
light.stable_id = "local";
light.position = {0, 3, 0};
light.direction = {0, -1, 0};
light.color = {1, .85f, .65f, 1};
light.intensity = 80;
light.range = 8;
light.inner_angle = .3f;
light.outer_angle = .7f;
result.local_lights.push_back(light);
return result;
}
std::size_t darker_pixels(const Frame& shadowed, const Frame& unshadowed) {
std::size_t count{};
for (std::size_t i = 0; i < shadowed.pixels.size(); i += 4)
count += int(unshadowed.pixels[i]) > int(shadowed.pixels[i]) + 12;
return count;
}
Snapshot point_face_scene(Vec3 axis, bool caster_shadow) {
Snapshot result;
result.view_id = "point-six-faces";
const Vec3 lateral = std::abs(axis[1]) > .9f ? Vec3{0, 0, 1} : Vec3{0, 1, 0};
result.eye = {-axis[0] * .4f + lateral[0] * 2,
-axis[1] * .4f + lateral[1] * 2,
-axis[2] * .4f + lateral[2] * 2};
const Vec3 target{axis[0] * 3, axis[1] * 3, axis[2] * 3};
const auto view = look_at(result.eye, target);
const auto projection = orthographic(-2, 2, -2, 2, .1f, 20);
result.projection = projection;
result.view_projection = multiply(projection, view);
result.camera_frustum = CameraFrustum{view, projection, .1f, 20, false};
result.authored_lights_present = true;
DrawItem receiver;
receiver.mesh = cube_mesh();
receiver.model = transform(target, {}, {1.5f, 1.5f, 1.5f});
receiver.color = {.8f, .8f, .8f, 1};
receiver.instance_key = "point-receiver";
result.draws.push_back(receiver);
DrawItem caster;
caster.mesh = cube_mesh();
caster.model = transform({axis[0] * 1.5f, axis[1] * 1.5f, axis[2] * 1.5f},
{}, {.5f, .5f, .5f});
caster.cast_shadow = caster_shadow;
caster.instance_key = "point-caster";
result.draws.push_back(caster);
LocalLight light;
light.stable_id = "point-face";
light.position = {0, 0, 0};
light.range = 8;
light.intensity = 90;
result.local_lights.push_back(light);
return result;
}
void local() {
auto direct = make_renderer(VisibilityMode::Direct);
auto gpu = make_renderer(VisibilityMode::GpuFrustum);
auto occlusion = make_renderer(VisibilityMode::GpuOcclusion);
for (auto kind : {LocalLight::Kind::Point, LocalLight::Kind::Spot}) {
const auto scene_with_shadow = local_scene(kind, true);
const auto shadowed = capture(direct, scene_with_shadow);
const auto gpu_shadowed = capture(gpu, scene_with_shadow);
const auto occlusion_shadowed = capture(occlusion, scene_with_shadow);
const auto unshadowed = capture(direct, local_scene(kind, false));
const auto faces = kind == LocalLight::Kind::Point ? 6u : 1u;
require(shadowed.stats.local_shadow_faces == faces &&
shadowed.stats.requested_local_shadow_faces == faces &&
shadowed.stats.shadow_caster_draws <= 4096 &&
shadowed.stats.local_shadow_atlas_bytes > 0 &&
shadowed.stats.gpu_local_shadow_ms > 0,
"Point/spot views render within atlas and caster budgets");
require(darker_pixels(shadowed, unshadowed) > 20,
"Caster darkens point/spot-lit receiver (count=" +
std::to_string(darker_pixels(shadowed, unshadowed)) + ")");
require(shadowed.stats.validation_errors == 0 &&
gpu_shadowed.stats.validation_errors == 0 &&
occlusion_shadowed.stats.validation_errors == 0,
"Local shadow rendering passes Vulkan validation");
compare_frames(shadowed, gpu_shadowed);
compare_frames(shadowed, occlusion_shadowed);
}
for (const Vec3 axis : {Vec3{1, 0, 0}, Vec3{-1, 0, 0}, Vec3{0, 1, 0},
Vec3{0, -1, 0}, Vec3{0, 0, 1}, Vec3{0, 0, -1},
Vec3{.7071068f, .7071068f, 0}}) {
const auto shadowed = capture(direct, point_face_scene(axis, true));
const auto unshadowed = capture(direct, point_face_scene(axis, false));
require(shadowed.stats.local_shadow_faces == 6 &&
darker_pixels(shadowed, unshadowed) > 5,
"A point light shadows each face direction and the adjacent-face seam");
}
auto crowded = local_scene(LocalLight::Kind::Point, true);
const auto point = crowded.local_lights.front();
crowded.local_lights.clear();
for (int i = 0; i < 15; ++i) {
LocalLight filler;
filler.kind = LocalLight::Kind::Spot;
filler.stable_id = "filler-" + std::to_string(i);
filler.position = {100, 100, 100};
filler.direction = {0, -1, 0};
filler.range = 8;
filler.intensity = 1;
filler.shadow_priority = 10;
crowded.local_lights.push_back(filler);
}
const auto without_point = capture(direct, crowded);
crowded.local_lights.push_back(point);
const auto overflow = capture(direct, crowded);
require(overflow.stats.requested_local_shadow_faces == 21 &&
overflow.stats.dropped_point_shadow_faces == 6 &&
overflow.stats.shadow_atlas_full_drops == 6 &&
overflow.stats.local_shadow_tiles <= 16,
"Fifteen occupied tiles drop the complete six-face point shadow");
std::size_t brightened{};
for (std::size_t i = 0; i < overflow.pixels.size(); i += 4)
brightened += int(overflow.pixels[i]) > int(without_point.pixels[i]) + 12;
require(brightened > 20,
"Point light with dropped atlas faces still illuminates unshadowed");
}
} // namespace
int main(int argc, char** argv) {
try {
if (argc != 2 || std::string(argv[1]) != "--sun")
throw std::invalid_argument("Expected --sun");
sun();
std::cout << "Sun cascade atlas and Direct/GPU lighting parity passed\n";
if (argc != 2)
throw std::invalid_argument("Expected --sun or --local");
if (std::string(argv[1]) == "--sun")
sun();
else if (std::string(argv[1]) == "--local")
local();
else
throw std::invalid_argument("Expected --sun or --local");
std::cout << "Shadow atlas and Direct/GPU lighting parity passed\n";
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;