#include "state.hpp"
/**
* @file
* @brief Mainly heuristics to reduce the number of objects and lights that need to be traced.
*/
/** @brief An object is fully occluded by another one from the given position. */
static bool is_occluded(const Object* obj, const vertex_t& pos, const scene_t& visible_scene) {
return std::any_of(visible_scene.begin(), visible_scene.end(), [obj, &pos](const auto& other) {
return other.get() != obj && other->intersect_full(pos, obj->box());
});
}
/** @brief A box volume is fully occluded by an object from the given position. */
static bool is_occluded(const box_t& box, const vertex_t& pos, const scene_t& visible_scene) {
return std::any_of(visible_scene.begin(), visible_scene.end(),
[&box, &pos](const auto& other) { return other->intersect_full(pos, box); });
}
/** @brief An object can possibly be reached by any active light. */
static bool can_lit(const Object* obj, const std::list<lightbox_t>& effective_lights) {
return std::any_of(effective_lights.begin(), effective_lights.end(), [obj](const auto& light) {
return light.box.intersects_xy(obj->box()) && obj->can_intersect(light.rad.pos);
});
}
/** @brief An object is not occluded and can in turn occlude any active light. */
static bool can_occlude(const Object* obj, const std::list<lightbox_t>& effective_lights, const box_t& viewport,
const scene_t& visible_scene) {
for (const auto& light : effective_lights) {
box_t effective_box{vertex_t::minof(viewport.lo, light.rad.pos), vertex_t::maxof(viewport.hi, light.rad.pos)};
if (obj->box().intersects_xy(effective_box) && light.box.intersects_xy(obj->box()) &&
!is_occluded(obj, light.rad.pos, visible_scene)) {
return true;
}
}
return false;
}
/** @brief An object might not be seen but can still obstruct view. */
static bool can_occlude(const Object* obj, const vertex_t& pos, const box_t& viewport, const scene_t& visible_scene) {
return viewport.intersects_xy(obj->box()) && obj->can_intersect(pos) && !is_occluded(obj, pos, visible_scene);
}
/** @brief A lightbox can be cut down if an object fully divides it. */
static void cut_lightbox(lightbox_t& lightbox, const scene_t& full_scene) {
#if USE_LIGHTBOX > 0
for (const auto& obj : full_scene) {
lightbox.box.cut_down(lightbox.rad.pos, obj->box());
}
#if USE_LIGHTBOX > 1
// try again, a some smaller box might allow a new cut
for (const auto& obj : full_scene) {
lightbox.box.cut_down(lightbox.rad.pos, obj->box());
}
#endif
#endif
}
/** @brief Lightbox factory, depending on the max light radius. */
static lightbox_t make_lightbox(const PointLight* light, const vertex_t& pos) {
const disc_t rad{pos.with_z(0.5F), vertex_t::setted(light->get_max_dist()).with_z(0.5F)};
return lightbox_t{light, rad, rad.get_box()};
}
/** @brief Lightbox, possibly reduced if an object splits through. */
static lightbox_t make_lightbox(const PointLight* light, const vertex_t& pos, const scene_t& full_scene) {
lightbox_t lightbox = make_lightbox(light, pos);
cut_lightbox(lightbox, full_scene);
return lightbox;
}
LightStack::LightStack(const scene_t& scene, const lights_t& lights) {
for (const auto& light : lights) {
push(light.second, light.first, scene);
}
}
void LightStack::push(const PointLight* light, const vertex_t& pos, const scene_t& scene) {
lightboxes.push_back(make_lightbox(light, pos, scene));
}
bool LightStack::push(const vertex_t& pos, const scene_t& scene) {
if (!stack.empty()) {
const PointLight* light = stack.back();
stack.pop_back();
lightboxes.push_back(make_lightbox(light, pos, scene));
return true;
}
return false;
}
const PointLight* LightStack::pop_nearest(const vertex_t& pos, fcoord_t max) {
fcoord_t min_dist = max * max;
std::list<lightbox_t>::iterator nearest = lightboxes.end();
std::list<lightbox_t>::iterator it = lightboxes.begin();
for (; it != lightboxes.end(); ++it) {
const fcoord_t dist = it->rad.pos.from(pos).sqlen();
if (dist <= min_dist) {
min_dist = dist;
nearest = it;
}
}
if (nearest != lightboxes.end()) {
const PointLight* light = nearest->light;
stack.push_back(light);
lightboxes.erase(nearest);
return light;
}
return nullptr;
}
const std::list<lightbox_t>& LightStack::get() const {
return lightboxes;
}
size_t LightStack::size() const {
return stack.size();
}
vertex_t State::clip(const vertex_t& pos, const vertex_t& off) const {
ray_t movement{pos.with_z(0.5F), off.with_z(), pos.with_z(0.5F) + off.with_z()};
box_t movement_box{movement};
bool intersects = false;
bool intersects_box = false;
vertex_t hit{}, nrm{};
for (const auto& obj : scene) {
if (obj->box().intersects_xy(movement_box)) {
intersects_box = true;
if (obj->intersect(movement, hit, nrm)) {
intersects = true;
movement = {movement.pos, hit - movement.pos, hit};
#if USE_LOG_STATS > 0
LOG("Clip to %.3f/%.3f", movement.dst.x, movement.dst.y);
#endif
}
}
}
if (!intersects_box) { // sanity check, ends up in void
return pos;
} else if (intersects) {
return hit.with_z(pos.z) + nrm;
} else {
return pos + off;
}
}
State::State(const scene_t& scene, const lights_t& lights, vertex_t raster, vertex_t viewport, vertex_t camera,
res_t resolution, bool subsample, fcoord_t sensitivity)
: sampler(resolution, subsample),
contexts(ZViewport::viewport_spec_t{raster, viewport, camera.z, sampler.resolution_in()}),
scene(scene),
lights(lights),
lightboxes(scene, lights),
camera(camera),
sensitivity(sensitivity),
player(viewport.x / 2.0F, {1.0F, 1.0F, 1.0F}),
renderer(contexts, sampler),
light_tracer(renderer.queue()),
tracer(light_tracer.queue()) {}
bool State::tick(const Renderer::input_t& in) {
Timer timer;
auto ctx = contexts.pop();
ctx->timer.reset();
// collect or drop a light upon key press
#if USE_LIGHT_PICKUP > 0
const fcoord_t pickup_dist = ctx->viewport.get_config().raster.x / 2.0F; // half a tile
if (in.action.action) {
const PointLight* nearest = lightboxes.pop_nearest(camera, pickup_dist);
if (nearest != nullptr) {
if (nearest->get_color() > ones || nearest->get_color() < zeroes) {
return false; // win/stop when picking up end light
}
} else {
lightboxes.push(camera, scene);
}
}
#if USE_LIGHT_PICKUP > 1
player =
PointLight(std::min(ctx->viewport.get_config().viewport.x * 0.5F,
ctx->viewport.get_config().viewport.x * ((fcoord_t)lightboxes.size() / 20.0F + 0.125F)),
{1.0F, 1.0F, 1.0F});
#endif
#endif
// move camera, viewport, player position
if (in.action.move) {
#if NO_CLIP
camera += in.offset;
#else
camera = clip(camera, in.offset);
#endif
}
if (in.action.beam) {
camera = in.offset.with_z(camera.z);
}
ctx->viewport.update(camera);
#if USE_LOG_STATS > 0
LOG("Move to %.3f/%.3f", camera.x, camera.y);
#endif
// preprocess scene for each corner separately
lightbox_t player_box = make_lightbox(&player, ctx->viewport.get_camera(), scene);
for (size_t n = 0; n < 4; ++n) {
sub_frame_t& corner = ctx->corners.at(n);
const UNUSED box_t bounding_box = ctx->viewport.get_viewbox(corner.viewport);
// collect lights that might contribute
corner.effective_lights.clear();
corner.effective_lights.push_back(player_box); // NB: copy to begin
for (const auto& light : lightboxes.get()) {
#if TRACE_ALL
ctx->effective_lights.push_back(light.get());
#else
if (light.box.intersects_xy(bounding_box)) { // could shine into scene (pos + max dist in viewport)
if (!is_occluded(light.box, player_box.rad.pos, scene)) {
corner.effective_lights.push_back(light);
}
}
#endif
}
// collect objects that might be seen or interact with the lights
corner.visible_scene.clear();
corner.effective_scene.clear();
for (const auto& obj : scene) {
#if TRACE_ALL
ctx->visible_scene.push_back(obj.get());
ctx->effective_scene.push_back(obj.get());
#else
if (obj->box().intersects_xy(bounding_box) && obj->can_intersect(camera) &&
can_lit(obj.get(), corner.effective_lights) && !is_occluded(obj.get(), player_box.rad.pos, scene)) {
corner.visible_scene.push_back(obj.get());
}
if (can_occlude(obj.get(), corner.effective_lights, bounding_box, scene) ||
can_occlude(obj.get(), player_box.rad.pos, bounding_box, scene)) {
corner.effective_scene.push_back(obj.get());
}
#endif
}
}
// start frame rendering by enqueueing to the object tracer as first processor
tracer.queue().submit(std::move(ctx));
#if USE_LOG_STATS > 1
LOG("Update in %4lu ms", timer.measure());
#endif
return true;
}
Renderer::input_t State::get_input() {
return renderer.get_input(sensitivity);
}
unsigned State::congestion_hint() {
const unsigned curr_congestion = tracer.queue().congestion_count() + light_tracer.queue().congestion_count() +
renderer.queue().congestion_count();
return curr_congestion - std::exchange(congestion_count, curr_congestion);
}
const SlidingAverage<msec_t>& State::duration_hint() {
return renderer.duration_hint();
}