diff --git a/src/app.cpp b/src/app.cpp index 2adfecb..f9cd33d 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -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); diff --git a/src/game/sector.cpp b/src/game/sector.cpp index ebb748b..554503a 100644 --- a/src/game/sector.cpp +++ b/src/game/sector.cpp @@ -4,6 +4,7 @@ #include #include +#include game::Sector::Sector(World* world, size_t idx, std::shared_ptr def) : world_(world), @@ -50,16 +51,20 @@ game::Sector::Sector(World* world, size_t idx, std::shared_ptr( - 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(); 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); } @@ -164,7 +170,7 @@ namespace game // Ignore portal hits return m_closestHitFraction; } - + return Super::addSingleResult(convexResult, normalInWorldSpace); } }; @@ -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(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* 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(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& 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 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 lightmap(lightmap_size * lightmap_size); + std::vector 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 lightmap_data(lightmap_size * lightmap_size * 3); - for (size_t i = 0; i < lightmap.size(); ++i) + std::vector 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(color.r * 255.0f); - lightmap_data[i * 3 + 1] = static_cast(color.g * 255.0f); - lightmap_data[i * 3 + 2] = static_cast(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(color.r * 255.0f); + lightmap_data[i * 3 + 1] = static_cast(color.g * 255.0f); + lightmap_data[i * 3 + 2] = static_cast(color.b * 255.0f); + } } diff --git a/src/game/sector.hpp b/src/game/sector.hpp index 723efeb..2ed26ec 100644 --- a/src/game/sector.hpp +++ b/src/game/sector.hpp @@ -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 mesh_; - std::vector lights_; + std::vector lights_; // Light in this sector + std::vector all_lights_; // Lights in this sector and linked sectors + std::shared_ptr lightmap_; std::vector portals_; @@ -94,6 +98,7 @@ namespace game size_t GetIndex() const { return idx_; } const std::shared_ptr& GetMesh() const { return mesh_; } const std::shared_ptr& 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* out_traversed_portals = nullptr); + + void GetLightsAt(const glm::vec3& pos, std::vector& out_lights) const; + + void Bake(); + + private: + void GenerateAllLights(); void BakeLightmap(); }; diff --git a/src/game/world.cpp b/src/game/world.cpp index ee996c8..a01ee91 100644 --- a/src/game/world.cpp +++ b/src/game/world.cpp @@ -32,6 +32,6 @@ void game::World::Bake() { for (auto& sector : sectors_) { - sector->BakeLightmap(); + sector->Bake(); } }