Terrain partial invalidation for spline component (#1309)

This commit is contained in:
Turánszki János
2025-11-18 08:59:23 +01:00
committed by GitHub
parent f0467c987d
commit 1df7a11904
13 changed files with 208 additions and 39 deletions
+1 -1
View File
@@ -33,7 +33,7 @@ template <typename T>
constexpr T sqr(T x) { return x * x; }
template <typename T>
constexpr T pow4(T x) { return x * x * x * x; }
constexpr T pow4(T x) { return sqr(sqr(x)); }
template <typename T>
constexpr T clamp(T x, T a, T b)
+20 -19
View File
@@ -16,29 +16,26 @@ namespace wi
uint32_t count = 0;
constexpr bool isLeaf() const { return count > 0; }
};
wi::vector<uint8_t> allocation;
Node* nodes = nullptr;
wi::vector<Node> nodes;
wi::vector<uint32_t> leaf_indices;
uint32_t node_count = 0;
uint32_t* leaf_indices = nullptr;
uint32_t leaf_count = 0;
constexpr bool IsValid() const { return nodes != nullptr; }
constexpr bool IsValid() const { return node_count > 0; }
// Completely rebuilds tree from scratch
void Build(const wi::primitive::AABB* aabbs, uint32_t aabb_count)
{
node_count = 0;
if (aabb_count == 0)
return;
const uint32_t node_capacity = aabb_count * 2 - 1;
allocation.reserve(
sizeof(Node) * node_capacity +
sizeof(uint32_t) * aabb_count
);
nodes = (Node*)allocation.data();
leaf_indices = (uint32_t*)(nodes + node_capacity);
leaf_count = aabb_count;
if (aabb_count == 0)
{
nodes.clear();
leaf_indices.clear();
return;
}
nodes.resize(aabb_count * 2 - 1);
leaf_indices.resize(aabb_count);
Node& node = nodes[node_count++];
node = {};
@@ -58,7 +55,7 @@ namespace wi
return;
if (aabb_count == 0)
return;
if (aabb_count != leaf_count)
if (aabb_count != (uint32_t)leaf_indices.size())
return;
for (uint32_t i = node_count - 1; i > 0; --i)
@@ -88,7 +85,9 @@ namespace wi
const std::function<void(uint32_t index)>& callback
) const
{
Node& node = nodes[nodeIndex];
if (node_count == 0)
return;
const Node& node = nodes[nodeIndex];
if (!node.aabb.intersects(primitive))
return;
if (node.isLeaf())
@@ -112,13 +111,15 @@ namespace wi
const std::function<bool(uint32_t index)>& callback
) const
{
if (node_count == 0)
return false;
uint32_t stack[64];
uint32_t count = 0;
stack[count++] = 0; // push node 0
while (count > 0)
while (count > 0 && (count < (arraysize(stack) - 1)))
{
const uint32_t nodeIndex = stack[--count];
Node& node = nodes[nodeIndex];
const Node& node = nodes[nodeIndex];
if (!node.aabb.intersects(primitive))
continue;
if (node.isLeaf())
+10
View File
@@ -195,6 +195,16 @@ namespace wi::primitive
bool intersection = frustum.Intersects(bb);
return intersection;
}
bool AABB::intersects(const BoundingBox& other) const
{
BoundingBox bb = BoundingBox(getCenter(), getHalfWidth());
return bb.Intersects(other);
}
bool AABB::intersects(const BoundingOrientedBox& other) const
{
BoundingBox bb = BoundingBox(getCenter(), getHalfWidth());
return bb.Intersects(other);
}
AABB AABB::operator* (float a)
{
XMFLOAT3 min = getMin();
+2
View File
@@ -49,6 +49,8 @@ namespace wi::primitive
bool intersects(const Ray& ray) const;
bool intersects(const Sphere& sphere) const;
bool intersects(const BoundingFrustum& frustum) const;
bool intersects(const BoundingBox& other) const;
bool intersects(const BoundingOrientedBox& other) const;
AABB operator* (float a);
static AABB Merge(const AABB& a, const AABB& b);
void AddPoint(const XMFLOAT3& pos);
+4
View File
@@ -18865,6 +18865,10 @@ void DrawBox(const XMFLOAT4X4& boxMatrix, const XMFLOAT4& color, bool depth)
else
renderableBoxes.push_back(std::make_pair(boxMatrix,color));
}
void DrawBox(const BoundingOrientedBox& obb, const XMFLOAT4& color, bool depth)
{
DrawBox(XMMatrixScalingFromVector(XMLoadFloat3(&obb.Extents)) * XMMatrixRotationQuaternion(XMLoadFloat4(&obb.Orientation)) * XMMatrixTranslationFromVector(XMLoadFloat3(&obb.Center)), color, depth);
}
void DrawSphere(const Sphere& sphere, const XMFLOAT4& color, bool depth)
{
if(depth)
+2 -1
View File
@@ -1233,7 +1233,8 @@ namespace wi::renderer
// Add box to render in next frame. It will be rendered in DrawDebugWorld()
void DrawBox(const wi::primitive::AABB& aabb, const XMFLOAT4& color = XMFLOAT4(1, 1, 1, 1), bool depth = true);
void DrawBox(const XMMATRIX& boxMatrix, const XMFLOAT4& color = XMFLOAT4(1, 1, 1, 1), bool depth = true);
void DrawBox(const XMFLOAT4X4& boxMatrix, const XMFLOAT4& color = XMFLOAT4(1,1,1,1), bool depth = true);
void DrawBox(const XMFLOAT4X4& boxMatrix, const XMFLOAT4& color = XMFLOAT4(1, 1, 1, 1), bool depth = true);
void DrawBox(const BoundingOrientedBox& obb, const XMFLOAT4& color = XMFLOAT4(1,1,1,1), bool depth = true);
// Add sphere to render in next frame. It will be rendered in DrawDebugWorld()
void DrawSphere(const wi::primitive::Sphere& sphere, const XMFLOAT4& color = XMFLOAT4(1, 1, 1, 1), bool depth = true);
// Add capsule to render in next frame. It will be rendered in DrawDebugWorld()
+4 -1
View File
@@ -5943,6 +5943,8 @@ namespace wi::scene
}
void Scene::RunSplineUpdateSystem(wi::jobsystem::context& ctx)
{
ScopedCPUProfiling("Spline Update");
// On the main thread, check if any of them require mesh component, etc:
for (size_t i = 0; i < splines.GetCount(); ++i)
{
@@ -6229,8 +6231,9 @@ namespace wi::scene
// Compute AABB:
if (dirty || (spline.dirty_terrain && spline.terrain_modifier_amount > 0))
{
spline.aabb = spline.ComputeAABB();
spline.PrecomputeSplineBounds(4);
}
});
wi::jobsystem::Wait(ctx);
}
+1 -1
View File
@@ -327,7 +327,7 @@ namespace wi::scene
// The contents of the other scene will be lost (and moved to this)!
// Any references to entities or components from the other scene will now reference them in this scene.
virtual void Merge(Scene& other);
// Similar to merge but skipping some things that are safe to skip within the Update look
// Similar to merge but skipping some things that are safe to skip within the Update loop
void MergeFastInternal(Scene& other);
// Create a copy of prefab and merge it into this.
// prefab : source scene to be copied from
+67 -1
View File
@@ -2010,7 +2010,8 @@ namespace wi::scene
size_t MeshComponent::GetMemoryUsageBVH() const
{
return
bvh.allocation.capacity() +
bvh.nodes.size() * sizeof(BVH::Node) +
bvh.leaf_indices.size() * sizeof(uint32_t) +
bvh_leaf_aabbs.size() * sizeof(wi::primitive::AABB);
}
size_t MeshComponent::GetClusterCount() const
@@ -3227,4 +3228,69 @@ namespace wi::scene
precomputed_node_distances[i] = distance;
}
}
void SplineComponent::PrecomputeSplineBounds(int subdivision)
{
if (spline_node_entities.size() < 2)
{
precomputed_obbs.clear();
precomputed_aabbs.clear();
}
else
{
float rangemod = width;
if (terrain_modifier_amount > 0)
{
rangemod /= sqr(terrain_modifier_amount); // sqr is used to match with distance falloff used in terrain generation
}
const size_t count = (spline_node_entities.size() - 1) * subdivision;
precomputed_obbs.resize(count);
precomputed_aabbs.resize(count);
const XMVECTOR XAXIS = XMVectorSet(1, 0, 0, 0);
for (size_t i = 0; i < count; ++i)
{
const float t0 = float(i) / count;
const float t1 = float(i + 1) / count;
const XMMATRIX M0 = EvaluateSplineAt(t0);
const XMMATRIX M1 = EvaluateSplineAt(t1);
const XMVECTOR P0 = wi::math::GetPosition(M0);
const XMVECTOR P1 = wi::math::GetPosition(M1);
const XMVECTOR C = XMVectorLerp(P0, P1, 0.5f);
const XMVECTOR T = XMVector3Normalize(P1 - P0);
const XMVECTOR A = XMVector3Normalize(XMVector3Cross(XAXIS, T));
const float angle = XMScalarACos(XMVectorGetX(XMVector3Dot(XAXIS, T)));
const XMVECTOR Q = XMQuaternionNormalize(XMQuaternionRotationNormal(A, angle));
BoundingOrientedBox& obb = precomputed_obbs[i];
XMStoreFloat3(&obb.Center, C);
obb.Extents = XMFLOAT3(wi::math::Distance(P0, P1) * 0.5f + rangemod, rangemod, rangemod);
XMStoreFloat4(&obb.Orientation, Q);
#if 0
// DEBUG OBB:
static wi::SpinLock locker;
locker.lock();
wi::renderer::DrawBox(precomputed_obbs[i]);
locker.unlock();
#endif
XMFLOAT3 corners[BoundingOrientedBox::CORNER_COUNT];
obb.GetCorners(corners);
AABB& aabb = precomputed_aabbs[i];
aabb = {};
for (auto& x : corners)
{
aabb.AddPoint(x);
}
#if 0
// DEBUG AABB:
static wi::SpinLock locker;
locker.lock();
wi::renderer::DrawBox(precomputed_aabbs[i]);
locker.unlock();
#endif
}
}
bvh.Build(precomputed_aabbs.data(), (uint32_t)precomputed_aabbs.size());
}
}
+9 -2
View File
@@ -2647,9 +2647,11 @@ namespace wi::scene
mutable int prev_terrain_generation_nodes = 0;
mutable bool dirty_terrain = false;
bool prev_looped = false;
wi::primitive::AABB aabb;
wi::ecs::Entity materialEntity = wi::ecs::INVALID_ENTITY; // temp for terrain usage
mutable wi::ecs::Entity materialEntity_terrainPrev = wi::ecs::INVALID_ENTITY; // temp for terrain usage
wi::vector<BoundingOrientedBox> precomputed_obbs; // an array of OBBs that approximate the spline's volume
wi::vector<wi::primitive::AABB> precomputed_aabbs; // an array of AABBs that approximate the spline's volume
wi::BVH bvh; // BVH fitted onto the precomputed_aabbs for accelerated intersection checking. The leaf nodes can be used to index aabbs and obbs alike
// Evaluate an interpolated location on the spline at t which in range [0,1] on the spline
// the result matrix is oriented to look towards the spline direction and face upwards along the spline normal
@@ -2661,12 +2663,17 @@ namespace wi::scene
// Trace a point on the spline's plane:
XMVECTOR TraceSplinePlane(const XMVECTOR& ORIGIN, const XMVECTOR& DIRECTION, int steps = 10) const;
// Compute the boounding box of the spline iteratively
// Compute the bounding box of the spline iteratively
wi::primitive::AABB ComputeAABB(int steps = 10) const;
// Precompute the spline node distances that will be used at spline evaluation calls
void PrecomputeSplineNodeDistances();
// Compute the oriented and axis aligned bounding boxes of the spline iteratively that approximates the spline volume
// Will write into the precomputed_aabbs and precomputed_obbs array, subdivision mean how many boxes will be used per-segment
// Will also build the bvh
void PrecomputeSplineBounds(int subdivision = 10);
// By default the spline is drawn as camera facing, this can be used to set it to be drawn aligned to segment rotations:
bool IsDrawAligned() const { return _flags & DRAW_ALIGNED; }
void SetDrawAligned(bool value = true) { if (value) { _flags |= DRAW_ALIGNED; } else { _flags &= ~DRAW_ALIGNED; } }
+86 -12
View File
@@ -13,6 +13,7 @@
#include <mutex>
#include <string>
#include <atomic>
#include <deque>
using namespace wi::ecs;
using namespace wi::scene;
@@ -183,6 +184,8 @@ namespace wi::terrain
wi::jobsystem::context workload;
std::atomic_bool cancelled{ false };
wi::vector<SplineComponent> splines;
wi::vector<Chunk> removable_chunks; // chunks that were invalidated are regenerated on the generator thread. Before merging them with the scene, the previous version of them will need to be removed from the destination scene
std::deque<Chunk> priority_invalidation; // to not let invalidation stuck at same chunks every frame while editing splines, for more appealing visual feedback
};
wi::jobsystem::context virtual_texture_ctx;
@@ -605,23 +608,40 @@ namespace wi::terrain
const SplineComponent& spline = scene->splines[i];
if (spline.terrain_modifier_amount > 0 || spline.prev_terrain_modifier_amount > 0)
{
restart_generation |= spline.dirty_terrain;
if (restart_generation)
bool spline_terrain_invalidation = false;
if (spline.dirty_terrain)
{
spline.dirty_terrain = false;
spline_terrain_invalidation = true;
spline.prev_terrain_modifier_amount = spline.terrain_modifier_amount;
spline.prev_terrain_pushdown = spline.terrain_pushdown;
spline.prev_terrain_texture_falloff = spline.terrain_texture_falloff;
spline.prev_terrain_generation_nodes = (int)spline.spline_node_entities.size();
}
restart_generation |= spline.materialEntity != spline.materialEntity_terrainPrev;
spline_terrain_invalidation |= spline.materialEntity != spline.materialEntity_terrainPrev;
const MaterialComponent* splineMaterial = scene->materials.GetComponent(spline.materialEntity);
if (splineMaterial != nullptr)
{
restart_generation |= splineMaterial->IsDirty();
spline_terrain_invalidation |= splineMaterial->IsDirty();
splineMaterialEntities.push_back(spline.materialEntity);
}
spline.materialEntity_terrainPrev = spline.materialEntity;
if (spline_terrain_invalidation)
{
for (auto it = chunks.begin(); it != chunks.end(); it++)
{
ChunkData& chunk_data = it->second;
if (chunk_data.invalidated)
continue;
BoundingBox bb(chunk_data.sphere.center, XMFLOAT3(chunk_data.sphere.radius, 1000000, chunk_data.sphere.radius));
if (spline.bvh.IntersectsFirst(bb, [&](uint32_t index) { return spline.precomputed_obbs[index].Intersects(bb); }))
{
chunk_data.invalidated = true;
generator->priority_invalidation.push_back(it->first);
}
}
}
}
}
@@ -656,6 +676,23 @@ namespace wi::terrain
weather = *weather_component; // feedback default weather
}
// Invalidated chunks replacements, originals are removed before merging updated ones:
for (Chunk chunk : generator->removable_chunks)
{
auto it = chunks.find(chunk);
if (it != chunks.end())
{
ChunkData& chunk_data = it->second;
scene->Entity_Remove(chunk_data.entity);
chunk_data.props_entity = INVALID_ENTITY;
if (chunk_data.vt != nullptr)
{
chunk_data.vt->invalidate();
}
}
}
generator->removable_chunks.clear();
// What was generated, will be merged in to the main scene
scene->MergeFastInternal(generator->scene);
@@ -900,9 +937,6 @@ namespace wi::terrain
if (spline.terrain_modifier_amount > 0)
{
generator->splines.push_back(spline);
// extrude spline aabb for heightfield check:
generator->splines.back().aabb._min.y = -FLT_MAX;
generator->splines.back().aabb._max.y = FLT_MAX;
}
}
wi::jobsystem::Execute(generator->workload, [=](wi::jobsystem::JobArgs a) {
@@ -916,12 +950,25 @@ namespace wi::terrain
chunk.x += offset_x;
chunk.z += offset_z;
auto it = chunks.find(chunk);
if (it == chunks.end() || it->second.entity == INVALID_ENTITY)
if (it == chunks.end() || it->second.entity == INVALID_ENTITY || it->second.invalidated)
{
// Generate a new chunk:
ChunkData& chunk_data = chunks[chunk];
chunk_data.entity = generator->scene.Entity_CreateObject("chunk_" + std::to_string(chunk.x) + "_" + std::to_string(chunk.z));
std::string chunk_name = "chunk_" + std::to_string(chunk.x) + "_" + std::to_string(chunk.z);
if (chunk_data.entity == INVALID_ENTITY)
{
chunk_data.entity = generator->scene.Entity_CreateObject(chunk_name);
}
else
{
// replacement will be made instead of simple merge, entity ID can be reused:
generator->scene.names.Create(chunk_data.entity) = std::move(chunk_name);
generator->scene.layers.Create(chunk_data.entity);
generator->scene.transforms.Create(chunk_data.entity);
generator->scene.objects.Create(chunk_data.entity);
generator->removable_chunks.push_back(chunk);
}
ObjectComponent& object = *generator->scene.objects.GetComponent(chunk_data.entity);
object.lod_bias = lod_bias;
object.filterMask |= wi::enums::FILTER_NAVIGATION_MESH;
@@ -1012,13 +1059,14 @@ namespace wi::terrain
// Apply splines to height only:
const XMVECTOR P = XMVectorSet(world_pos.x, -100000, world_pos.y, 0);
const wi::primitive::Ray ray(P, UP);
int splinematerialcnt = -1;
for (size_t j = 0; j < generator->splines.size(); ++j)
{
const SplineComponent& spline = generator->splines[j];
if (spline.materialEntity != INVALID_ENTITY)
splinematerialcnt++;
if (!spline.aabb.intersects(P))
if (!spline.bvh.IntersectsFirst(ray, [&](uint32_t index) { return spline.precomputed_aabbs[index].intersects(ray); }))
continue;
XMVECTOR S = spline.TraceSplinePlane(P, UP, 4);
S = spline.ClosestPointOnSpline(S, 4);
@@ -1134,6 +1182,8 @@ namespace wi::terrain
}
// Create the textures for virtual texture update:
chunk_data.heightmap = {};
chunk_data.blendmap = {};
CreateChunkRegionTexture(chunk_data);
if (IsPhysicsEnabled())
@@ -1162,10 +1212,13 @@ namespace wi::terrain
if (it != chunks.end() && it->second.entity != INVALID_ENTITY)
{
ChunkData& chunk_data = it->second;
if (chunk_data.grass_entity == INVALID_ENTITY && chunk_data.grass.meshID != INVALID_ENTITY)
if ((chunk_data.grass_entity == INVALID_ENTITY || chunk_data.invalidated) && chunk_data.grass.meshID != INVALID_ENTITY)
{
// add patch for this chunk
chunk_data.grass_entity = CreateEntity();
if (chunk_data.grass_entity == INVALID_ENTITY)
{
chunk_data.grass_entity = CreateEntity();
}
wi::HairParticleSystem& grass = generator->scene.hairs.Create(chunk_data.grass_entity);
grass = chunk_data.grass;
chunk_data.grass_density_current = grass_density;
@@ -1297,6 +1350,13 @@ namespace wi::terrain
}
}
it = chunks.find(chunk); // re-query!
if (it != chunks.end() && it->second.entity != INVALID_ENTITY)
{
ChunkData& chunk_data = it->second;
chunk_data.invalidated = false;
}
if (generated_something && timer.elapsed_milliseconds() > generation_time_budget_milliseconds)
{
generator->cancelled.store(true);
@@ -1304,6 +1364,20 @@ namespace wi::terrain
};
// priority invalidation queue:
// This doesn't necessarily finish every frame, that's why it's a queue, next frame will pick up earlier requests before newer ones
while (!generator->priority_invalidation.empty())
{
Chunk chunk = generator->priority_invalidation.front();
generator->priority_invalidation.pop_front();
auto it = chunks.find(chunk);
if (it != chunks.end() && it->second.invalidated) // Check here too in this special case, because multiple of the same entries can easily exist on the queue. Already refreshes chunks will not be refreshed again
{
request_chunk(chunk.x, chunk.z);
if (generator->cancelled.load()) return;
}
}
// generate center chunk first:
request_chunk(0, 0);
if (generator->cancelled.load()) return;
+1
View File
@@ -230,6 +230,7 @@ namespace wi::terrain
wi::primitive::Sphere sphere;
XMFLOAT3 position = XMFLOAT3(0, 0, 0);
bool visible = true;
bool invalidated = false;
std::shared_ptr<VirtualTexture> vt;
wi::vector<uint16_t> heightmap_data;
wi::graphics::Texture heightmap;
+1 -1
View File
@@ -9,7 +9,7 @@ namespace wi::version
// minor features, major updates, breaking compatibility changes
const int minor = 71;
// minor bug fixes, alterations, refactors, updates
const int revision = 855;
const int revision = 856;
const std::string version_string = std::to_string(major) + "." + std::to_string(minor) + "." + std::to_string(revision);