Code for building a lightprobe AABB tree

This commit is contained in:
Magnus Norddahl 2025-07-20 07:54:33 +02:00
commit 3e6943fb4f
4 changed files with 365 additions and 1 deletions

View file

@ -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<CollisionNode> Nodes;
int RootNode = 0;
// Light probe AABB binary tree
TArray<ProbeNode> 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<CPUAccelStruct> Collision;
// For finding light probes
std::unique_ptr<LightProbeAABBTree> LightProbeAABB;
// Lightmap tiles and their locations in the texture atlas
struct
{

View file

@ -61,3 +61,203 @@ void LightProbeIncrementalBuilder::Full(const TArray<LightProbe>& 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<LightProbe>& 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<LightProbe>& 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;
}

View file

@ -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<LightProbe>& probes);
int Subdivide(int* instances, int numInstances, const FVector3* centroids, int* workBuffer, const TArray<LightProbe>& 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<Node> Nodes;
int Root = 0;
struct
{
std::vector<int> leafs;
std::vector<FVector3> centroids;
std::vector<int> workbuffer;
} Scratch;
static bool OverlapAABB(const FVector2& center, float extent, const Node& node);
};

View file

@ -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;