PortalGame/src/game/entity.cpp
2025-09-05 20:00:19 +02:00

263 lines
7.0 KiB
C++

#include "entity.hpp"
#include "world.hpp"
#include <algorithm>
#define GLM_ENABLE_EXPERIMENTAL
#include <glm/gtx/norm.hpp>
game::Entity::Entity(World* world, size_t sector_idx, const CapsuleShape& capsule_shape, const glm::vec3& position) :
world_(world),
capsule_(capsule_shape),
velocity_(0.0f),
touching_portal_(nullptr),
light_trace_offset_(0.0f, 0.0f, capsule_shape.height * 0.5f),
lights_valid_(false)
{
CreateOccurrenceParams occu_params;
occu_params.sector = &world->GetSector(sector_idx);
occu_params.position = position;
occu_params.scale = 1.0f; // Default scale
occu_params.basis = glm::mat3(1.0f); // Identity basis
// Create initial occurence in given sector
occu_ = CreateOccurrence(occu_params);
}
void game::Entity::Move(glm::vec3& velocity, float dt)
{
glm::vec3 u = velocity * dt; // Calculate the movement vector
const int MAX_ITERS = 16;
for (size_t i = 0; i < MAX_ITERS && glm::dot(u, u) > 0.0f; ++i)
{
//printf("Entity::Move: Iteration %zu, u = (%f, %f, %f)\n", i, u.x, u.y, u.z);
// offset in occu's sector space
glm::vec3 occu_offset = occu_->basis_ * u;
glm::vec3 to = occu_->position_ + occu_offset;
float occu_hit_fraction = 1.0f;
glm::vec3 occu_hit_normal;
const Portal* hit_portal = nullptr;
bool hit = occu_->Sweep(to, occu_hit_fraction, occu_hit_normal, &hit_portal);
if (hit_portal != touching_portal_)
{
touching_portal_ = hit_portal;
if (hit_portal)
{
// Beginning to touch a portal, create other occurrence in the linked sector
CreateOtherOccurence(*hit_portal);
}
else
{
// Not touching the portal anymore, destroy the other occurrence
other_occu_.reset();
}
}
float other_hit_fraction = 1.0f;
glm::vec3 other_hit_normal;
bool other_hit = false;
if (other_occu_)
{
glm::vec3 other_offset = other_occu_->basis_ * u;
glm::vec3 other_to = other_occu_->position_ + other_offset;
// Sweep the other occurrence if it exists
other_hit = other_occu_->Sweep(other_to, other_hit_fraction, other_hit_normal, nullptr);
}
bool any_hit = hit || other_hit;
float hit_fraction = 1.0f; // Default to no hit
glm::vec3 hit_normal;
if (hit && (!other_hit || occu_hit_fraction <= other_hit_fraction))
{
// Primary occurrence hit firstly
hit_fraction = occu_hit_fraction;
hit_normal = occu_->inv_normal_basis_ * occu_hit_normal;
}
else if (other_hit)
{
// Other occurrence hit firstly
hit_fraction = other_hit_fraction;
hit_normal = other_occu_->inv_normal_basis_ * other_hit_normal;
}
// Update the position based on the hit fraction
occu_->position_ += hit_fraction * occu_offset;
//if (any_hit)
//{
// occu_->position_ += hit_normal * 0.00001f;
//}
if (other_occu_ && touching_portal_)
{
other_occu_->position_ = touching_portal_->tr_position * glm::vec4(occu_->position_, 1.0f);
// TELEPORT
float sd = glm::dot(glm::vec3(touching_portal_->plane), occu_->position_) + touching_portal_->plane.w;
if (sd < 0.0f)
{
std::swap(occu_, other_occu_); // Swap occurrences
touching_portal_ = touching_portal_->link; // Switch to the linked portal
}
}
if (!any_hit)
{
break;
}
//hit_normal *= -1.0f; // Invert the normal to point outwards
//printf("Entity::Move: Hit detected, hit_fraction = %f, hit_normal = (%f, %f, %f)\n", hit_fraction, hit_normal.x, hit_normal.y, hit_normal.z);
u -= hit_fraction * u; // Reduce the movement vector by the hit fraction
u -= glm::dot(u, hit_normal) * hit_normal; // Reflect the velocity along the hit normal
velocity -= glm::dot(velocity, hit_normal) * hit_normal; // Adjust the velocity
}
}
void game::Entity::Update(float dt)
{
Move(velocity_, dt);
lights_valid_ = false;
}
void game::Entity::UpdateLights()
{
if (lights_valid_)
{
return; // Already updated since last update
}
const SectorEnvironment& env = occu_->sector_->GetEnvironment();
glm::vec3 trace_pos = occu_->position_ + occu_->basis_ * light_trace_offset_;
static std::vector<Light> lights;
lights.clear();
occu_->sector_->GetLightsAt(trace_pos, lights);
// select only closest SD_MAX_LIGHTS lights
if (lights.size() > SD_MAX_LIGHTS)
{
std::partial_sort(
lights.begin(),
lights.begin() + SD_MAX_LIGHTS,
lights.end(),
[&](const Light& a, const Light& b)
{
float aa = glm::distance2(a.position, trace_pos) / (a.radius * a.radius);
float ab = glm::distance2(b.position, trace_pos) / (b.radius * b.radius);
return aa < ab;
}
);
lights.resize(SD_MAX_LIGHTS);
}
// Primary occurence
occu_->lights_.ambient = env.ambient;
occu_->lights_.num_lights = lights.size();
for (size_t i = 0; i < lights.size(); ++i)
{
const Light& light = lights[i];
occu_->lights_.colors_rs[i] = glm::vec4(light.color, light.radius);
occu_->lights_.positions[i] = light.position;
}
// Other occurence
if (other_occu_ && touching_portal_)
{
other_occu_->lights_.ambient = env.ambient;
other_occu_->lights_.num_lights = lights.size();
for (size_t i = 0; i < lights.size(); ++i)
{
const Light& light = lights[i];
// transform pos through portal
glm::vec3 new_pos = touching_portal_->tr_position * glm::vec4(light.position, 1.0f);
float new_radius = light.radius * touching_portal_->tr_scale;
other_occu_->lights_.colors_rs[i] = glm::vec4(light.color, new_radius);
other_occu_->lights_.positions[i] = new_pos;
}
}
lights_valid_ = true;
}
void game::Entity::CreateOtherOccurence(const Portal& portal)
{
other_occu_.reset();
if (!portal.link)
{
// No linked sector, no occurence :(
return;
}
CreateOccurrenceParams other_occu_params;
other_occu_params.sector = portal.link->sector;
other_occu_params.position = portal.tr_position * glm::vec4(occu_->position_, 1.0f);
other_occu_params.scale = occu_->scale_ * portal.tr_scale;
other_occu_params.basis = portal.tr_basis * occu_->basis_;
other_occu_ = CreateOccurrence(other_occu_params);
}
game::EntityOccurrence::EntityOccurrence(Entity* entity, const CreateOccurrenceParams& params) :
entity_(entity),
sector_(params.sector),
position_(params.position),
scale_(params.scale),
basis_(params.basis),
inv_basis_(glm::inverse(params.basis)),
bt_capsule_(entity->GetCapsuleShape().radius * params.scale, entity->GetCapsuleShape().height * params.scale)
{
// compute normalized basis
for (size_t i = 0; i < 3; ++i)
{
normal_basis_[i] = glm::normalize(basis_[i]);
}
// compute inverse normalized basis
inv_normal_basis_ = glm::inverse(normal_basis_);
}
bool game::EntityOccurrence::Sweep(const glm::vec3& target_position, float& hit_fraction, glm::vec3& hit_normal, const Portal** hit_portal)
{
if (hit_portal)
{
*hit_portal = sector_->TestPortalContact(bt_capsule_, inv_normal_basis_, position_);
if (!*hit_portal)
{
*hit_portal = sector_->TestPortalContact(bt_capsule_, inv_normal_basis_, target_position);
}
}
return sector_->SweepCapsule(
bt_capsule_,
inv_normal_basis_,
position_,
target_position,
hit_fraction,
hit_normal
);
}
const game::LightInfluences& game::EntityOccurrence::GetLights()
{
entity_->UpdateLights();
return lights_;
}