Raycasted intersector lightmapping
This commit is contained in:
parent
0c32e21955
commit
d0ecf09b06
@ -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);
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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<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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -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();
|
||||
};
|
||||
|
||||
|
||||
@ -32,6 +32,6 @@ void game::World::Bake()
|
||||
{
|
||||
for (auto& sector : sectors_)
|
||||
{
|
||||
sector->BakeLightmap();
|
||||
sector->Bake();
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user