diff --git a/src/framework/tarray.h b/src/framework/tarray.h index bc84bb0..37f299c 100644 --- a/src/framework/tarray.h +++ b/src/framework/tarray.h @@ -274,8 +274,14 @@ public: // returns address of first element T* Data() const { - return &Array[0]; + return Array; } + + T* begin() { return Array; } + T* end() { return Array + Count; } + const T* begin() const { return Array; } + const T* end() const { return Array + Count; } + private: T *Array; unsigned int Most; diff --git a/src/framework/templates.h b/src/framework/templates.h index 950eda4..27e77a3 100644 --- a/src/framework/templates.h +++ b/src/framework/templates.h @@ -179,3 +179,15 @@ T clamp (const T in, const T min, const T max) { return in <= min ? min : in >= max ? max : in; } + +template +T smoothstep(const T edge0, const T edge1, const T x) +{ + auto t = clamp((x - edge0) / (edge1 - edge0), T(0.0), T(1.0)); + return t * t * (T(3.0) - T(2.0) * t); +} + +inline float radians(float degrees) +{ + return degrees * 3.14159265359f / 180.0f; +} diff --git a/src/level/doomdata.h b/src/level/doomdata.h index 17ac8df..a527b63 100644 --- a/src/level/doomdata.h +++ b/src/level/doomdata.h @@ -2,10 +2,14 @@ #pragma once #include "framework/tarray.h" +#include "framework/templates.h" #include "math/mathlib.h" #include +#include #undef MIN #undef MAX +#undef min +#undef max enum { @@ -271,6 +275,44 @@ struct ThingLight bool bCeiling; IntSector *sector; MapSubsectorEx *ssect; + + Vec3 LightOrigin() const + { + float originZ; + if (!bCeiling) + originZ = sector->floorplane.zAt(origin.x, origin.y) + height; + else + originZ = sector->ceilingplane.zAt(origin.x, origin.y) - height; + return Vec3(origin.x, origin.y, originZ); + } + + float LightRadius() const + { + return radius + radius; // 2.0 because gzdoom's dynlights do this and we want them to match + } + + float SpotAttenuation(const Vec3& dir) const + { + float spotAttenuation = 1.0f; + if (outerAngleCos > -1.0f) + { + float negPitch = -radians(mapThing->pitch); + float xyLen = std::cos(negPitch); + Vec3 spotDir; + spotDir.x = -std::cos(radians(mapThing->angle)) * xyLen; + spotDir.y = -std::sin(radians(mapThing->angle)) * xyLen; + spotDir.z = -std::sin(negPitch); + float cosDir = Vec3::Dot(dir, spotDir); + spotAttenuation = smoothstep(outerAngleCos, innerAngleCos, cosDir); + spotAttenuation = std::max(spotAttenuation, 0.0f); + } + return spotAttenuation; + } + + float DistAttenuation(float distance) const + { + return std::max(1.0f - (distance / LightRadius()), 0.0f); + } }; struct SurfaceLightDef diff --git a/src/level/level.cpp b/src/level/level.cpp index fceb048..e68e24d 100644 --- a/src/level/level.cpp +++ b/src/level/level.cpp @@ -20,6 +20,7 @@ #include "level/level.h" #include "lightmap/lightmap.h" +#include "lightmap/raytracer.h" //#include "rejectbuilder.h" #include @@ -690,8 +691,13 @@ void FProcessor::BuildLightmaps() { Level.SetupLights(); LightmapMesh = std::make_unique(Level, Samples, LMDims); +#if 1 DLightRaytracer raytracer; raytracer.Raytrace(LightmapMesh.get()); +#else + Raytracer raytracer; + raytracer.Raytrace(LightmapMesh.get()); +#endif LightmapMesh->CreateTextures(); } diff --git a/src/lightmap/lightmap.cpp b/src/lightmap/lightmap.cpp index 2bed0c6..05e50db 100644 --- a/src/lightmap/lightmap.cpp +++ b/src/lightmap/lightmap.cpp @@ -121,18 +121,6 @@ bool DLightRaytracer::EmitFromCeiling(const Surface *surface, const Vec3 &origin return true; } -template -T smoothstep(const T edge0, const T edge1, const T x) -{ - auto t = clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0); - return t * t * (3.0 - 2.0 * t); -} - -static float radians(float degrees) -{ - return degrees * 3.14159265359f / 180.0f; -} - // Traces a line from the texel's origin to the sunlight direction and against all nearby thing lights Vec3 DLightRaytracer::LightTexelSample(const Vec3 &origin, Surface *surface) { @@ -147,13 +135,7 @@ Vec3 DLightRaytracer::LightTexelSample(const Vec3 &origin, Surface *surface) { ThingLight *tl = &mesh->map->ThingLights[i]; - float originZ; - if (!tl->bCeiling) - originZ = tl->sector->floorplane.zAt(tl->origin.x, tl->origin.y) + tl->height; - else - originZ = tl->sector->ceilingplane.zAt(tl->origin.x, tl->origin.y) - tl->height; - - Vec3 lightOrigin(tl->origin.x, tl->origin.y, originZ); + Vec3 lightOrigin = tl->LightOrigin(); if (surface && plane.Distance(lightOrigin) - plane.d < 0) { @@ -161,8 +143,7 @@ Vec3 DLightRaytracer::LightTexelSample(const Vec3 &origin, Surface *surface) continue; } - float radius = tl->radius * 2.0f; // 2.0 because gzdoom's dynlights do this and we want them to match - float intensity = tl->intensity; + float radius = tl->LightRadius(); if (origin.DistanceSq(lightOrigin) > (radius*radius)) { @@ -174,23 +155,9 @@ Vec3 DLightRaytracer::LightTexelSample(const Vec3 &origin, Surface *surface) float dist = dir.Unit(); dir.Normalize(); - float spotAttenuation = 1.0f; - if (tl->outerAngleCos > -1.0f) - { - float negPitch = -radians(tl->mapThing->pitch); - float xyLen = std::cos(negPitch); - Vec3 spotDir; - spotDir.x = -std::cos(radians(tl->mapThing->angle)) * xyLen; - spotDir.y = -std::sin(radians(tl->mapThing->angle)) * xyLen; - spotDir.z = -std::sin(negPitch); - float cosDir = Vec3::Dot(dir, spotDir); - spotAttenuation = smoothstep(tl->outerAngleCos, tl->innerAngleCos, cosDir); - if (spotAttenuation <= 0.0f) - { - // outside spot light - continue; - } - } + float spotAttenuation = tl->SpotAttenuation(dir); + if (spotAttenuation == 0.0f) + continue; if (mesh->TraceAnyHit(lightOrigin, origin)) { @@ -202,7 +169,7 @@ Vec3 DLightRaytracer::LightTexelSample(const Vec3 &origin, Surface *surface) attenuation *= spotAttenuation; if (surface) attenuation *= plane.Normal().Dot(dir); - attenuation *= intensity; + attenuation *= tl->intensity; // accumulate results color += tl->rgb * attenuation; diff --git a/src/lightmap/raytracer.cpp b/src/lightmap/raytracer.cpp index 4a4edb7..6f2b137 100644 --- a/src/lightmap/raytracer.cpp +++ b/src/lightmap/raytracer.cpp @@ -28,4 +28,231 @@ Raytracer::~Raytracer() void Raytracer::Raytrace(LevelMesh* level) { mesh = level; + + printf("Tracing light probes\n"); + + Worker::RunJob((int)mesh->lightProbes.size(), [=](int id) { + RaytraceProbeSample(&mesh->lightProbes[id]); + }); + + printf("Tracing surfaces (%d bounces)\n", LightBounce); + + struct SurfaceTask + { + int surf, x, y; + }; + std::vector tasks; + + for (size_t i = 0; i < mesh->surfaces.size(); i++) + { + Surface* surface = mesh->surfaces[i].get(); + int sampleWidth = surface->lightmapDims[0]; + int sampleHeight = surface->lightmapDims[1]; + for (int y = 0; y < sampleHeight; y++) + { + for (int x = 0; x < sampleWidth; x++) + { + SurfaceTask task; + task.surf = (int)i; + task.x = x; + task.y = y; + tasks.push_back(task); + } + } + } + + Worker::RunJob((int)tasks.size(), [=](int id) { + const SurfaceTask& task = tasks[id]; + RaytraceSurfaceSample(mesh->surfaces[task.surf].get(), task.x, task.y); + }); + + printf("Raytrace complete\n"); +} + +void Raytracer::RaytraceProbeSample(LightProbeSample* probe) +{ + Vec3 color(0.0f, 0.0f, 0.0f); + + for (ThingLight& light : mesh->map->ThingLights) + { + Vec3 lightOrigin = light.LightOrigin(); + float lightRadius = light.LightRadius(); + + if (probe->Position.DistanceSq(lightOrigin) > (lightRadius * lightRadius)) + continue; + + if (mesh->TraceAnyHit(lightOrigin, probe->Position)) + continue; // this light is occluded by something + + Vec3 dir = (lightOrigin - probe->Position); + float dist = dir.Unit(); + dir.Normalize(); + + color += light.rgb * (light.SpotAttenuation(dir) * light.DistAttenuation(dist) * light.intensity); + } + + probe->Color = color; +} + +void Raytracer::RaytraceSurfaceSample(Surface* surface, int x, int y) +{ + Vec3 normal = surface->plane.Normal(); + Vec3 pos = surface->lightmapOrigin + normal + surface->lightmapSteps[0] * (float)x + surface->lightmapSteps[1] * (float)y; + + Vec3 incoming(0.0f, 0.0f, 0.0f); + if (LightBounce > 0) + { + float totalWeight = 0.0f; + for (int i = 0; i < SAMPLE_COUNT; i++) + { + Vec2 Xi = Hammersley(i, SAMPLE_COUNT); + Vec3 H = ImportanceSampleGGX(Xi, normal, 1.0f); + Vec3 L = Vec3::Normalize(H * (2.0f * Vec3::Dot(normal, H)) - normal); + float NdotL = std::max(Vec3::Dot(normal, L), 0.0f); + if (NdotL > 0.0f) + { + incoming += TracePath(pos, L, i) * NdotL; + totalWeight += NdotL; + } + } + incoming = incoming / totalWeight / (float)LightBounce; + } + + incoming = incoming + GetSurfaceEmittance(surface, 0.0f) + GetLightEmittance(surface, pos); + + size_t sampleWidth = surface->lightmapDims[0]; + surface->samples[x + y * sampleWidth] = incoming; +} + +Vec3 Raytracer::GetLightEmittance(Surface* surface, const Vec3& pos) +{ + Vec3 emittance = Vec3(0.0f); + for (ThingLight& light : mesh->map->ThingLights) + { + Vec3 lightOrigin = light.LightOrigin(); + float lightRadius = light.LightRadius(); + + if (surface->plane.Distance(lightOrigin) - surface->plane.d < 0) + continue; // completely behind the plane + + if (pos.DistanceSq(lightOrigin) > (lightRadius * lightRadius)) + continue; // light too far away + + Vec3 dir = (lightOrigin - pos); + float dist = dir.Unit(); + dir.Normalize(); + + float attenuation = light.SpotAttenuation(dir) * light.DistAttenuation(dist) * surface->plane.Normal().Dot(dir); + if (attenuation <= 0.0f) + continue; + + if (mesh->TraceAnyHit(lightOrigin, pos)) + continue; // this light is occluded by something + + emittance += light.rgb * (attenuation * light.intensity); + } + return emittance; +} + +Vec3 Raytracer::TracePath(const Vec3& pos, const Vec3& dir, int sampleIndex, int depth) +{ + if (depth >= LightBounce) + return Vec3(0.0f); + + LevelTraceHit hit = mesh->Trace(pos, pos + dir * 1000.0f); + if (hit.fraction == 1.0f) + return Vec3(0.0f); + + Vec3 normal = hit.hitSurface->plane.Normal(); + Vec3 hitpos = hit.start * (1.0f - hit.fraction) + hit.end * hit.fraction + normal * 0.1f; + + Vec3 emittance = GetSurfaceEmittance(hit.hitSurface, pos.Distance(hitpos)) + GetLightEmittance(hit.hitSurface, hitpos) * 0.5f; + + Vec2 Xi = Hammersley(sampleIndex, SAMPLE_COUNT); + Vec3 H = ImportanceSampleGGX(Xi, normal, 1.0f); + Vec3 L = Vec3::Normalize(H * (2.0f * Vec3::Dot(normal, H)) - normal); + + float NdotL = std::max(Vec3::Dot(normal, L), 0.0f); + if (NdotL <= 0.0f) + return emittance; + + const float p = 1 / (2 * M_PI); + Vec3 incoming = TracePath(hitpos, normal, (sampleIndex + depth + 1) % SAMPLE_COUNT, depth + 1); + + return emittance + incoming * NdotL / p; +} + +float Raytracer::RadicalInverse_VdC(uint32_t bits) +{ + bits = (bits << 16u) | (bits >> 16u); + bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u); + bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u); + bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u); + bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u); + return float(bits) * 2.3283064365386963e-10f; // / 0x100000000 +} + +Vec2 Raytracer::Hammersley(uint32_t i, uint32_t N) +{ + return Vec2(float(i) / float(N), RadicalInverse_VdC(i)); +} + +Vec3 Raytracer::ImportanceSampleGGX(Vec2 Xi, Vec3 N, float roughness) +{ + float a = roughness * roughness; + + float phi = 2.0f * M_PI * Xi.x; + float cosTheta = sqrt((1.0f - Xi.y) / (1.0f + (a * a - 1.0f) * Xi.y)); + float sinTheta = sqrt(1.0f - cosTheta * cosTheta); + + // from spherical coordinates to cartesian coordinates + Vec3 H(std::cos(phi) * sinTheta, std::sin(phi) * sinTheta, cosTheta); + + // from tangent-space vector to world-space sample vector + Vec3 up = std::abs(N.z) < 0.999f ? Vec3(0.0f, 0.0f, 1.0f) : Vec3(1.0f, 0.0f, 0.0f); + Vec3 tangent = Vec3::Normalize(Vec3::Cross(up, N)); + Vec3 bitangent = Vec3::Cross(N, tangent); + + Vec3 sampleVec = tangent * H.x + bitangent * H.y + N * H.z; + return Vec3::Normalize(sampleVec); +} + +Vec3 Raytracer::GetSurfaceEmittance(Surface* surface, float distance) +{ + SurfaceLightDef* def = nullptr; + if (surface->type >= ST_MIDDLESIDE && surface->type <= ST_LOWERSIDE) + { + int lightdefidx = mesh->map->Sides[surface->typeIndex].lightdef; + if (lightdefidx != -1) + { + def = &mesh->map->SurfaceLights[lightdefidx]; + } + } + else if (surface->type == ST_FLOOR || surface->type == ST_CEILING) + { + MapSubsectorEx* sub = &mesh->map->GLSubsectors[surface->typeIndex]; + IntSector* sector = mesh->map->GetSectorFromSubSector(sub); + + if (sector && surface->numVerts > 0) + { + if (sector->floorlightdef != -1 && surface->type == ST_FLOOR) + { + def = &mesh->map->SurfaceLights[sector->floorlightdef]; + } + else if (sector->ceilinglightdef != -1 && surface->type == ST_CEILING) + { + def = &mesh->map->SurfaceLights[sector->ceilinglightdef]; + } + } + } + + if (def) + { + float attenuation = std::max(1.0f - (distance / def->distance), 0.0f); + return def->rgb * (attenuation * def->intensity); + } + else + { + return Vec3(0.0f); + } } diff --git a/src/lightmap/raytracer.h b/src/lightmap/raytracer.h index ce91895..0ffefda 100644 --- a/src/lightmap/raytracer.h +++ b/src/lightmap/raytracer.h @@ -12,5 +12,18 @@ public: void Raytrace(LevelMesh* level); private: + void RaytraceProbeSample(LightProbeSample* probe); + void RaytraceSurfaceSample(Surface* surface, int x, int y); + Vec3 TracePath(const Vec3& pos, const Vec3& dir, int sampleIndex, int depth = 0); + + Vec3 GetLightEmittance(Surface* surface, const Vec3& pos); + Vec3 GetSurfaceEmittance(Surface* surface, float distance); + + static float RadicalInverse_VdC(uint32_t bits); + static Vec2 Hammersley(uint32_t i, uint32_t N); + static Vec3 ImportanceSampleGGX(Vec2 Xi, Vec3 N, float roughness); + + int SAMPLE_COUNT = 1024;// 128;// 1024; + LevelMesh* mesh = nullptr; }; diff --git a/src/main.cpp b/src/main.cpp index 1abf9ab..571cf63 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -457,7 +457,7 @@ static void ParseArgs(int argc, char **argv) case 'B': LightBounce = atoi(optarg); if (LightBounce < 0) LightBounce = 0; - if (LightBounce > 1) LightBounce = 1; + if (LightBounce > 8) LightBounce = 8; break; case 'i': GridSize = std::stof(optarg); @@ -510,7 +510,7 @@ static void ShowUsage() " -S, --size=NNN lightmap texture dimensions for width and height\n" " must be in powers of two (1, 2, 4, 8, 16, etc)\n" " -M, --multisample=NNN Number of samples to use per texel (default %d)\n" - " -B, --bounce=NNN Number of indirect light bounces (default %d, max 1)\n" + " -B, --bounce=NNN Number of indirect light bounces (default %d, max 8)\n" " -i, --gridsize=NNN Automatic light probe grid size, floating point\n" " Lower values increase granularity at the expense of performance\n" " Recommended: 32.0, 64.0, 128.0, etc (default %.1f)\n"