From 3e6943fb4f573c077f1f70660b84724d82dee8e3 Mon Sep 17 00:00:00 2001 From: Magnus Norddahl Date: Sun, 20 Jul 2025 07:54:33 +0200 Subject: [PATCH] Code for building a lightprobe AABB tree --- .../rendering/hwrenderer/data/hw_levelmesh.h | 8 + .../hwrenderer/data/hw_lightprobe.cpp | 200 ++++++++++++++++++ .../rendering/hwrenderer/data/hw_lightprobe.h | 79 +++++++ wadsrc/static/shaders/lightmap/frag_copy.glsl | 79 ++++++- 4 files changed, 365 insertions(+), 1 deletion(-) diff --git a/src/common/rendering/hwrenderer/data/hw_levelmesh.h b/src/common/rendering/hwrenderer/data/hw_levelmesh.h index 85041ed1e..ea6873d2d 100644 --- a/src/common/rendering/hwrenderer/data/hw_levelmesh.h +++ b/src/common/rendering/hwrenderer/data/hw_levelmesh.h @@ -4,6 +4,7 @@ #include "tarray.h" #include "vectors.h" #include "hw_collision.h" +#include "hw_lightprobe.h" #include "flatvertices.h" #include "hw_levelmeshlight.h" #include "hw_levelmeshportal.h" @@ -164,6 +165,10 @@ public: // Acceleration structure nodes for when the GPU doesn't support rayquery TArray Nodes; int RootNode = 0; + + // Light probe AABB binary tree + TArray ProbeNodes; + int ProbeRootNode = 0; } Mesh; // Ranges in mesh that have changed since last upload @@ -197,6 +202,9 @@ public: // Data structure for doing mesh traces on the CPU std::unique_ptr Collision; + // For finding light probes + std::unique_ptr LightProbeAABB; + // Lightmap tiles and their locations in the texture atlas struct { diff --git a/src/common/rendering/hwrenderer/data/hw_lightprobe.cpp b/src/common/rendering/hwrenderer/data/hw_lightprobe.cpp index 71a0fe87c..dd1a3cef6 100644 --- a/src/common/rendering/hwrenderer/data/hw_lightprobe.cpp +++ b/src/common/rendering/hwrenderer/data/hw_lightprobe.cpp @@ -61,3 +61,203 @@ void LightProbeIncrementalBuilder::Full(const TArray& probes, std::f Step(probes, renderScene); } } + +///////////////////////////////////////////////////////////////////////////// + +LightProbeAABBTree::LightProbeAABBTree(LevelMesh* mesh) : Mesh(mesh) +{ +} + +LightProbeAABBTree::~LightProbeAABBTree() +{ +} + +int LightProbeAABBTree::FindClosestProbe(FVector3 pos, float extent) +{ + if (Root == -1) + return 0; + + float probeDistSqr = 0.0; + int probeIndex = 0; + Node* stack[64]; + int stackIndex = 0; + stack[stackIndex++] = &Nodes[Root]; + do + { + Node* a = stack[--stackIndex]; + if (OverlapAABB(pos.XY(), extent, *a)) + { + if (a->IsLeaf()) + { + FVector3 probePos = a->probePos; + FVector3 d = probePos - pos; + float distSqr = d | d; + if (probeIndex == 0 || probeDistSqr > distSqr) + { + probeIndex = a->probeIndex; + probeDistSqr = distSqr; + } + } + else + { + stack[stackIndex++] = &Nodes[a->right]; + stack[stackIndex++] = &Nodes[a->left]; + } + } + } while (stackIndex > 0); + return probeIndex; +} + +bool LightProbeAABBTree::OverlapAABB(const FVector2& center, float extent, const Node& node) +{ + float dx = center.X - node.aabb.Center.X; + float px = extent + node.aabb.Extents.X - std::abs(dx); + if (px < 0.0f) + return false; + float dy = center.Y - node.aabb.Center.Y; + float py = extent + node.aabb.Extents.Y - std::abs(dy); + if (py < 0.0f) + return false; + return true; +} + +void LightProbeAABBTree::Update() +{ + //Create(Mesh->LightProbes); + //Upload(); +} + +void LightProbeAABBTree::Upload() +{ +} + +void LightProbeAABBTree::Create(const TArray& probes) +{ + Scratch.leafs.clear(); + Scratch.leafs.reserve(probes.size()); + Scratch.centroids.clear(); + Scratch.centroids.reserve(probes.size()); + for (int i = 0; i < probes.size(); i++) + { + Scratch.leafs.push_back(i); + Scratch.centroids.push_back(FVector3(probes[i].position.XY(), 1.0f)); + } + + size_t neededbuffersize = probes.size() * 2; + if (Scratch.workbuffer.size() < neededbuffersize) + Scratch.workbuffer.resize(neededbuffersize); + + Nodes.clear(); + Root = Subdivide(Scratch.leafs.data(), (int)Scratch.leafs.size(), Scratch.centroids.data(), Scratch.workbuffer.data(), probes); +} + +int LightProbeAABBTree::Subdivide(int* instances, int numInstances, const FVector3* centroids, int* workBuffer, const TArray& probes) +{ + if (numInstances == 0) + return -1; + + // Find bounding box and median of the instance centroids + FVector2 median(0.0f, 0.0f); + FVector2 min = probes[instances[0]].position.XY() - 1.0f; + FVector2 max = probes[instances[0]].position.XY() + 1.0f; + for (int i = 0; i < numInstances; i++) + { + FVector2 bboxmin = probes[instances[i]].position.XY() - 1.0f; + FVector2 bboxmax = probes[instances[i]].position.XY() + 1.0f; + + min.X = std::min(min.X, bboxmin.X); + min.Y = std::min(min.Y, bboxmin.Y); + + max.X = std::max(max.X, bboxmax.X); + max.Y = std::max(max.Y, bboxmax.Y); + + median += centroids[instances[i]].XY(); + } + median /= (float)numInstances; + + // For numerical stability + min.X -= 0.1f; + min.Y -= 0.1f; + max.X += 0.1f; + max.Y += 0.1f; + + if (numInstances == 1) // Leaf node + { + Nodes.push_back(Node(min, max, instances[0], probes[instances[0]].position)); + return (int)Nodes.size() - 1; + } + + // Find the longest axis + float axis_lengths[3] = + { + max.X - min.X, + max.Y - min.Y + }; + + int axis_order[2] = { 0, 1 }; + std::sort(axis_order, axis_order + 2, [&](int a, int b) { return axis_lengths[a] > axis_lengths[b]; }); + + // Try split at longest axis, then if that fails the next longest, and then the remaining one + int left_count, right_count; + FVector2 axis; + for (int attempt = 0; attempt < 2; attempt++) + { + // Find the split plane for axis + switch (axis_order[attempt]) + { + default: + case 0: axis = FVector2(1.0f, 0.0f); break; + case 1: axis = FVector2(0.0f, 1.0f); break; + } + FVector3 plane(axis, -(median | axis)); // plane(axis, -dot(median, axis)); + + // Split instances into two + left_count = 0; + right_count = 0; + for (int i = 0; i < numInstances; i++) + { + int instance = instances[i]; + + float side = centroids[instance] | plane; + if (side >= 0.0f) + { + workBuffer[left_count] = instance; + left_count++; + } + else + { + workBuffer[numInstances + right_count] = instance; + right_count++; + } + } + + if (left_count != 0 && right_count != 0) + break; + } + + // Check if something went wrong when splitting and do a random split instead + if (left_count == 0 || right_count == 0) + { + left_count = numInstances / 2; + right_count = numInstances - left_count; + } + else + { + // Move result back into instances list: + for (int i = 0; i < left_count; i++) + instances[i] = workBuffer[i]; + for (int i = 0; i < right_count; i++) + instances[i + left_count] = workBuffer[numInstances + i]; + } + + // Create child nodes: + int left_index = -1; + int right_index = -1; + if (left_count > 0) + left_index = Subdivide(instances, left_count, centroids, workBuffer, probes); + if (right_count > 0) + right_index = Subdivide(instances + left_count, right_count, centroids, workBuffer, probes); + + Nodes.push_back(Node(min, max, left_index, right_index)); + return (int)Nodes.size() - 1; +} diff --git a/src/common/rendering/hwrenderer/data/hw_lightprobe.h b/src/common/rendering/hwrenderer/data/hw_lightprobe.h index 66886581d..1b8006314 100644 --- a/src/common/rendering/hwrenderer/data/hw_lightprobe.h +++ b/src/common/rendering/hwrenderer/data/hw_lightprobe.h @@ -2,6 +2,8 @@ #include "vectors.h" +class LevelMesh; + struct LightProbe { FVector3 position; @@ -27,3 +29,80 @@ private: int cubemapsAllocated = 0; int iterations = 0; }; + +struct ProbeNode +{ + FVector2 center; + FVector2 extents; + int left; + int right; + int probeIndex; + int padding0; + FVector3 probePos; + float padding1; +}; + +class LightProbeAABBTree +{ +public: + LightProbeAABBTree(LevelMesh* mesh); + ~LightProbeAABBTree(); + + void Update(); + + int FindClosestProbe(FVector3 pos, float extent); + +private: + void Create(const TArray& probes); + int Subdivide(int* instances, int numInstances, const FVector3* centroids, int* workBuffer, const TArray& probes); + void Upload(); + + LevelMesh* Mesh = nullptr; + + struct BBox + { + BBox() = default; + + BBox(const FVector2& aabb_min, const FVector2& aabb_max) + { + min = aabb_min; + max = aabb_max; + auto halfmin = aabb_min * 0.5f; + auto halfmax = aabb_max * 0.5f; + Center = halfmax + halfmin; + Extents = halfmax - halfmin; + } + + FVector2 min; + FVector2 max; + FVector2 Center; + FVector2 Extents; + }; + + struct Node + { + Node() = default; + Node(const FVector2& aabb_min, const FVector2& aabb_max, int probeIndex, FVector3& probePos) : aabb(aabb_min, aabb_max), probeIndex(probeIndex), probePos(probePos) {} + Node(const FVector2& aabb_min, const FVector2& aabb_max, int left, int right) : aabb(aabb_min, aabb_max), left(left), right(right) {} + + bool IsLeaf() const { return probeIndex == 0; } + + BBox aabb; + int left = -1; + int right = -1; + int probeIndex = 0; + FVector3 probePos; + }; + + std::vector Nodes; + int Root = 0; + + struct + { + std::vector leafs; + std::vector centroids; + std::vector workbuffer; + } Scratch; + + static bool OverlapAABB(const FVector2& center, float extent, const Node& node); +}; diff --git a/wadsrc/static/shaders/lightmap/frag_copy.glsl b/wadsrc/static/shaders/lightmap/frag_copy.glsl index 53f7834b3..41cfc50be 100644 --- a/wadsrc/static/shaders/lightmap/frag_copy.glsl +++ b/wadsrc/static/shaders/lightmap/frag_copy.glsl @@ -7,9 +7,86 @@ layout(location = 1) in vec3 WorldPos; layout(location = 0) out vec4 FragColor; layout(location = 1) out uvec4 FragProbe; +#if 1 + +uint findClosestProbe(vec3 pos, float size) +{ + return 0; +} + +#else + +struct ProbeNode +{ + vec2 center; + vec2 extents; + int left; + int right; + uint probeIndex; + int padding0; + vec3 probePos; + float padding1; +}; + +layout(std430, set = 0, binding = 1) buffer readonly ProbeBuffer +{ + int probeNodeRoot; + int probebufferPadding1; + int probebufferPadding2; + int probebufferPadding3; + ProbeNode probeNodes[]; +}; + +bool isProbeNodeLeaf(int nodeIndex) +{ + return probeNodes[nodeIndex].probeIndex != 0; +} + +bool overlapAABB(vec2 center, vec2 extents, int nodeIndex) +{ + vec2 d = center - probeNodes[nodeIndex].center; + vec2 p = extents + probeNodes[nodeIndex].extents - abs(d); + return p.x >= 0.0 && p.y >= 0.0; +} + +uint findClosestProbe(vec3 pos, float size) +{ + float probeDistSqr = 0.0; + uint probeIndex = 0; + int stack[64]; + int stackIndex = 0; + stack[stackIndex++] = probeNodeRoot; + do + { + int a = stack[--stackIndex]; + if (overlapAABB(pos, vec2(size, size), a)) + { + if (isProbeNodeLeaf(a)) + { + vec3 probePos = probeNodes[a].probePos; + vec3 d = probePos - pos; + float distSqr = dot(d, d); + if (probeIndex == 0 || probeDistSqr > distSqr) + { + probeIndex = probeNodes[a].probeIndex; + probeDistSqr = distSqr; + } + } + else + { + stack[stackIndex++] = nodes[a].right; + stack[stackIndex++] = nodes[a].left; + } + } + } while (stackIndex > 0); + return probeIndex; +} + +#endif + void main() { - uint probeIndex = 0; + uint probeIndex = findClosestProbe(WorldPos, 512.0); FragColor = texture(Tex, TexCoord); FragProbe.x = probeIndex;