Raycasted intersector lightmapping

This commit is contained in:
tovjemam 2025-08-14 21:40:50 +02:00
parent 0c32e21955
commit d0ecf09b06
4 changed files with 296 additions and 32 deletions

View File

@ -18,12 +18,15 @@ App::App()
size_t s1i = world_.AddSector(room001);
game::Sector& s1 = world_.GetSector(s1i);
s1.AddLight(glm::vec3(0.0f, 0.0f, 1.8f), glm::vec3(0.0f, 1.0f, 1.0f), 5.0f);
s1.AddLight(glm::vec3(0.0f, 2.0f, 1.8f), glm::vec3(0.0f, 1.0f, 1.0f), 5.0f);
s1.AddLight(glm::vec3(1.0f, 3.0f, 1.8f), glm::vec3(1.0f, 0.0f, 0.0f), 5.0f);
size_t s2i = world_.AddSector(room001);
game::Sector& s2 = world_.GetSector(s2i);
s2.AddLight(glm::vec3(0.0f, 0.0f, 1.8f), glm::vec3(1.0f, 1.0f, 0.9f), 6.0f);
s2.AddLight(glm::vec3(0.0f, 2.0f, 1.8f), glm::vec3(1.0f, 1.0f, 0.9f), 6.0f);
s2.AddLight(glm::vec3(2.0f, -0.0f, 1.0f), glm::vec3(0.0f, 1.0f, 0.0f), 3.0f);
world_.LinkPortals(s1i, "EDoor", s1i, "EDoor", 0);
world_.LinkPortals(s1i, "NDoor", s2i, "WDoor", 0);
world_.LinkPortals(s2i, "NDoor", s2i, "SDoor", 0);
world_.LinkPortals(s2i, "EDoor", s2i, "EDoor", game::LINK_ROTATE180);

View File

@ -4,6 +4,7 @@
#include <glm/gtc/quaternion.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <BulletCollision/NarrowPhaseCollision/btRaycastCallback.h>
game::Sector::Sector(World* world, size_t idx, std::shared_ptr<assets::SectorDef> def) :
world_(world),
@ -50,16 +51,20 @@ game::Sector::Sector(World* world, size_t idx, std::shared_ptr<assets::SectorDef
portal_map_[def_portal.name] = i;
glm::vec3 portal_normal = glm::normalize(glm::vec3(portal.plane));
const float half_portal_thickness = 0.1f;
glm::vec3 portal_position = def_portal.origin - portal_normal * half_portal_thickness;
// create portal collision object
portal.bt_col_shape = std::make_unique<btBoxShape>(
btVector3(portal.def->size.x * portal.scale * 0.5f, 0.1f, portal.def->size.y * portal.scale * 0.5f)
btVector3(portal.def->size.x * portal.scale * 0.5f, half_portal_thickness, portal.def->size.y * portal.scale * 0.5f)
);
portal.bt_col_obj = std::make_unique<btCollisionObject>();
portal.bt_col_obj->setCollisionShape(portal.bt_col_shape.get());
btQuaternion bt_rotation(angles_rad.x, angles_rad.y, angles_rad.z);
btVector3 bt_position(def_portal.origin.x, def_portal.origin.y, def_portal.origin.z);
btVector3 bt_position(portal_position.x, portal_position.y, portal_position.z);
btTransform bt_transform(bt_rotation, bt_position);
portal.bt_col_obj->setWorldTransform(bt_transform);
@ -84,6 +89,7 @@ void game::Sector::AddLight(const glm::vec3& position, const glm::vec3& color, f
light.position = position;
light.color = color;
light.radius = radius;
light.through_portal = nullptr; // The light is here
lights_.push_back(light);
}
@ -264,10 +270,161 @@ const game::Portal* game::Sector::TestPortalContact(btCapsuleShapeZ& capsule, co
return result_callback.portal;
}
static bool PointIsOnRight(const glm::vec2& p0, const glm::vec2& p1, const glm::vec2& p)
namespace game
{
// Check if point p is on the right side of the line segment p0 -> p1
return (p1.x - p0.x) * (p.y - p0.y) - (p1.y - p0.y) * (p.x - p0.x) < 0.0f;
struct SectorClosestRayResultCallback : public btCollisionWorld::ClosestRayResultCallback
{
using Super = btCollisionWorld::ClosestRayResultCallback;
glm::vec3 dir;
float min_fraction = 0.0f; // Minimum fraction to consider a hit valid
SectorClosestRayResultCallback(const btVector3& rayFromWorld, const btVector3& rayToWorld) :
Super(rayFromWorld, rayToWorld)
{
glm::vec3 start(rayFromWorld.x(), rayFromWorld.y(), rayFromWorld.z());
glm::vec3 end(rayToWorld.x(), rayToWorld.y(), rayToWorld.z());
dir = end - start;
}
virtual btScalar addSingleResult(btCollisionWorld::LocalRayResult& rayResult, bool normalInWorldSpace) override
{
void* ptr = rayResult.m_collisionObject->getUserPointer();
CollisionObjectData* data = static_cast<CollisionObjectData*>(ptr);
if (data && data->type == CO_PORTAL)
{
float dot = glm::dot(glm::vec3(data->portal->plane), dir);
if (dot > 0.0f)
{
// The ray is going in the opposite direction of the portal plane, ignore this hit
return m_closestHitFraction;
}
}
else if (rayResult.m_hitFraction < min_fraction)
{
// Ignore hits that are before the minimum fraction
return m_closestHitFraction;
}
return Super::addSingleResult(rayResult, normalInWorldSpace);
}
};
}
bool game::Sector::Raycast(const Sector*& sector, const glm::vec3& start, const glm::vec3& end, std::vector<const Portal*>* out_traversed_portals)
{
glm::vec3 start_to_end = end - start;
float distance = glm::length(start_to_end);
glm::vec3 dir = start_to_end / distance;
float start_offset = 0.1f; // Start the ray a bit earlier to avoid missing close portals
distance += start_offset;
glm::vec3 ray_start = start - dir * start_offset;
glm::vec3 ray_end = end;
float min_fraction = start_offset / distance;
const int MAX_PORTAL_TRAVERSALS = 4;
for (int i = 0; i < MAX_PORTAL_TRAVERSALS; ++i)
{
btVector3 bt_start(ray_start.x, ray_start.y, ray_start.z);
btVector3 bt_end(ray_end.x, ray_end.y, ray_end.z);
SectorClosestRayResultCallback cb(bt_start, bt_end);
cb.min_fraction = min_fraction;
cb.m_flags = btTriangleRaycastCallback::kF_FilterBackfaces;
sector->bt_world_.rayTest(bt_start, bt_end, cb);
if (!cb.hasHit())
{
// No hit, we reached the end of the ray
return false;
}
// We hit something, check if it's a portal
void* ptr = cb.m_collisionObject->getUserPointer();
CollisionObjectData* data = static_cast<CollisionObjectData*>(ptr);
if (data && data->type == CO_PORTAL)
{
const Portal* portal = data->portal;
if (out_traversed_portals)
{
out_traversed_portals->push_back(portal);
}
if (!portal->link)
{
return true;
}
// Transform the ray start and end to the portal's sector space
glm::vec3 hit_point(
cb.m_hitPointWorld.x(),
cb.m_hitPointWorld.y(),
cb.m_hitPointWorld.z()
);
ray_start = glm::vec3(portal->tr_position * glm::vec4(hit_point, 1.0f));
ray_end = glm::vec3(portal->tr_position * glm::vec4(ray_end, 1.0f));
min_fraction = 0.0f;
// Update the sector to the linked one
sector = portal->link->sector;
}
else
{
// We hit something that is not a portal, return success
return true;
}
}
return false;
}
void game::Sector::GetLightsAt(const glm::vec3& pos, std::vector<Light>& out_lights) const
{
for (const Light& light : all_lights_)
{
float dist = glm::distance(light.position, pos);
if (dist >= light.radius)
{
continue;
}
const Sector* sector = this;
static std::vector<const Portal*> traversed_portals;
traversed_portals.clear();
bool hit = Raycast(sector, pos, light.position, &traversed_portals);
if (hit)
{
continue; // The light ray is blocked
}
if (!light.through_portal && !traversed_portals.empty())
{
// The light is not through a portal, but we traversed some portals
// This means the light is not visible from this position
continue;
}
if (light.through_portal && !(traversed_portals.size() == 1 && traversed_portals[0] == light.through_portal))
{
// Ray didnt traverse through the portal that the light is visible through
continue;
}
out_lights.push_back(light);
}
}
static float SignedDistanceToLine(const glm::vec2& p0, const glm::vec2& p1, const glm::vec2& p)
@ -276,9 +433,47 @@ static float SignedDistanceToLine(const glm::vec2& p0, const glm::vec2& p1, cons
return (p1.x - p0.x) * (p.y - p0.y) - (p1.y - p0.y) * (p.x - p0.x);
}
void game::Sector::Bake()
{
GenerateAllLights();
BakeLightmap();
}
void game::Sector::GenerateAllLights()
{
all_lights_.clear();
// Add lights from this sector
for (const Light& light : lights_)
{
all_lights_.push_back(light);
}
// Add lights from linked sectors
for (const Portal& portal : portals_)
{
if (!portal.link)
{
continue; // No link, skip
}
const Sector* linked_sector = portal.link->sector;
for (const Light& ext_light : linked_sector->lights_)
{
Light light = ext_light;
light.position = glm::vec3(portal.link->tr_position * glm::vec4(ext_light.position, 1.0f)); // Transform light position to this sector's space
light.radius = ext_light.radius * portal.tr_scale; // Scale the radius
light.through_portal = &portal;
all_lights_.push_back(light);
}
}
}
void game::Sector::BakeLightmap()
{
const size_t lightmap_size = 256;
const size_t lightmap_size = 512;
const glm::vec3 ambient_light(0.2f); // Ambient light color
const float margin = 2.0f;
@ -287,21 +482,24 @@ void game::Sector::BakeLightmap()
struct LightmapTexel
{
bool used = false;
bool used = false; // Inside triangle or margin
bool inside = false; // Actually inside a triangle
glm::vec3 color = glm::vec3(0.0f);
};
std::vector<LightmapTexel> lightmap(lightmap_size * lightmap_size);
std::vector<Light> lights;
for (const assets::MeshTriangle& tri : mesh_tris)
{
//const assets::MeshVertex* verts[3];
glm::vec2 verts[3];
glm::vec3 vert_pos[3];
glm::vec3 vert_norm[3];
collision::AABB2 tri_aabb;
for (size_t i = 0; i < 3; ++i)
{
const assets::MeshVertex& vert = mesh_verts[tri.vert[i]];
@ -325,30 +523,25 @@ void game::Sector::BakeLightmap()
{
LightmapTexel& texel = lightmap[y * lightmap_size + x];
if (texel.used)
if (texel.inside)
{
continue;
continue; // Texel was already inside another triangle
}
glm::vec2 texel_pos((float)x + 0.5f, (float)y + 0.5f); // Center of the texel
//if (PointIsOnRight(verts[0], verts[1], texel_pos) ||
// PointIsOnRight(verts[1], verts[2], texel_pos) ||
// PointIsOnRight(verts[2], verts[0], texel_pos))
//{
// continue; // Texel is outside the triangle
//}
// Calculate signed distance to the triangle edges
float sd = SignedDistanceToLine(verts[0], verts[1], texel_pos);
sd = glm::min(sd, SignedDistanceToLine(verts[1], verts[2], texel_pos));
sd = glm::min(sd, SignedDistanceToLine(verts[2], verts[0], texel_pos));
if (sd > margin)
{
continue; // Texel is outside the triangle
continue; // Texel is outside the triangle even with margin
}
texel.used = sd < 0.0f;
texel.inside = sd < 0.0f;
texel.used = true; // Mark as used, even if its in the margin area
// Compute barycentric coordinates
glm::vec2 v0 = verts[1] - verts[0];
@ -360,6 +553,7 @@ void game::Sector::BakeLightmap()
float d20 = glm::dot(v2, v0);
float d21 = glm::dot(v2, v1);
float denom = d00 * d11 - d01 * d01;
if (denom == 0.0f)
{
continue; // Degenerate triangle, skip
@ -374,7 +568,10 @@ void game::Sector::BakeLightmap()
glm::vec3 light_color = ambient_light;
for (const Light& light : lights_)
lights.clear();
GetLightsAt(texel_pos_ws, lights);
for (const Light& light : lights)
{
glm::vec3 light_dir = glm::normalize(light.position - texel_pos_ws);
float dot = glm::dot(texel_norm_ws, light_dir);
@ -393,14 +590,65 @@ void game::Sector::BakeLightmap()
}
}
std::vector<uint8_t> lightmap_data(lightmap_size * lightmap_size * 3);
for (size_t i = 0; i < lightmap.size(); ++i)
std::vector<uint8_t> lightmap_data(lightmap_size * lightmap_size * 3, 0);
for (int y = 0; y < lightmap_size; ++y)
{
const LightmapTexel& texel = lightmap[i];
glm::vec3 color = glm::clamp(texel.color * 0.5f, glm::vec3(0.0f), glm::vec3(1.0f));
lightmap_data[i * 3 + 0] = static_cast<uint8_t>(color.r * 255.0f);
lightmap_data[i * 3 + 1] = static_cast<uint8_t>(color.g * 255.0f);
lightmap_data[i * 3 + 2] = static_cast<uint8_t>(color.b * 255.0f);
for (int x = 0; x < lightmap_size; ++x)
{
size_t i = y * lightmap_size + x;
glm::vec3 color(0.0f);
if (!lightmap[i].used)
{
// Not used texel, skip
continue;
}
float total_weight = 0.0f;
// Apply blur
for (int oy = -1; oy <= 1; ++oy)
{
for (int ox = -1; ox <= 1; ++ox)
{
int nx = x + ox;
int ny = y + oy;
if (nx < 0 || nx >= lightmap_size || ny < 0 || ny >= lightmap_size)
{
continue; // Out of bounds
}
size_t ni = ny * lightmap_size + nx;
const LightmapTexel& texel = lightmap[ni];
if (!texel.used)
{
continue; // Not used texel
}
float weight = 1.0f;
if (ox != 0 || oy != 0)
{
weight = (glm::abs(ox) + glm::abs(oy) == 1) ? 0.5f : 0.25f; // 0.5 for neighbors, 0.25 for corners
}
color += texel.color * weight;
total_weight += weight;
}
}
color /= total_weight;
// Multiply by 0.5 - shader will multiply by 2.0 to make it possible to store higher values
color = glm::clamp(color * 0.5f, glm::vec3(0.0f), glm::vec3(1.0f));
lightmap_data[i * 3 + 0] = static_cast<uint8_t>(color.r * 255.0f);
lightmap_data[i * 3 + 1] = static_cast<uint8_t>(color.g * 255.0f);
lightmap_data[i * 3 + 2] = static_cast<uint8_t>(color.b * 255.0f);
}
}

View File

@ -58,6 +58,8 @@ namespace game
glm::vec3 position;
glm::vec3 color;
float radius;
const Portal* through_portal;
};
class Sector
@ -68,7 +70,9 @@ namespace game
std::shared_ptr<assets::Mesh> mesh_;
std::vector<Light> lights_;
std::vector<Light> lights_; // Light in this sector
std::vector<Light> all_lights_; // Lights in this sector and linked sectors
std::shared_ptr<gfx::Texture> lightmap_;
std::vector<Portal> portals_;
@ -94,6 +98,7 @@ namespace game
size_t GetIndex() const { return idx_; }
const std::shared_ptr<assets::Mesh>& GetMesh() const { return mesh_; }
const std::shared_ptr<gfx::Texture>& GetLightmap() const { return lightmap_; }
btCollisionWorld& GetBtWorld() { return bt_world_; }
int GetPortalIndex(const std::string& name) const;
const Portal& GetPortal(size_t idx) const { return portals_[idx]; }
@ -111,6 +116,14 @@ namespace game
const Portal* TestPortalContact(btCapsuleShapeZ& capsule, const glm::mat3& basis, const glm::vec3& pos);
static bool Raycast(const Sector*& sector, const glm::vec3& start, const glm::vec3& end, std::vector<const Portal*>* out_traversed_portals = nullptr);
void GetLightsAt(const glm::vec3& pos, std::vector<Light>& out_lights) const;
void Bake();
private:
void GenerateAllLights();
void BakeLightmap();
};

View File

@ -32,6 +32,6 @@ void game::World::Bake()
{
for (auto& sector : sectors_)
{
sector->BakeLightmap();
sector->Bake();
}
}