diff --git a/WickedEngine/CommonInclude.h b/WickedEngine/CommonInclude.h index 4841af8e8..20119d376 100644 --- a/WickedEngine/CommonInclude.h +++ b/WickedEngine/CommonInclude.h @@ -33,7 +33,7 @@ template constexpr T sqr(T x) { return x * x; } template -constexpr T pow4(T x) { return x * x * x * x; } +constexpr T pow4(T x) { return sqr(sqr(x)); } template constexpr T clamp(T x, T a, T b) diff --git a/WickedEngine/wiBVH.h b/WickedEngine/wiBVH.h index 15ed4f365..a4a5bb54c 100644 --- a/WickedEngine/wiBVH.h +++ b/WickedEngine/wiBVH.h @@ -16,29 +16,26 @@ namespace wi uint32_t count = 0; constexpr bool isLeaf() const { return count > 0; } }; - wi::vector allocation; - Node* nodes = nullptr; + wi::vector nodes; + wi::vector 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& 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& 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()) diff --git a/WickedEngine/wiPrimitive.cpp b/WickedEngine/wiPrimitive.cpp index 51281bf7d..fe60f4c34 100644 --- a/WickedEngine/wiPrimitive.cpp +++ b/WickedEngine/wiPrimitive.cpp @@ -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(); diff --git a/WickedEngine/wiPrimitive.h b/WickedEngine/wiPrimitive.h index 73a798883..3c315cd60 100644 --- a/WickedEngine/wiPrimitive.h +++ b/WickedEngine/wiPrimitive.h @@ -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); diff --git a/WickedEngine/wiRenderer.cpp b/WickedEngine/wiRenderer.cpp index bd8d57dac..3c88d868e 100644 --- a/WickedEngine/wiRenderer.cpp +++ b/WickedEngine/wiRenderer.cpp @@ -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) diff --git a/WickedEngine/wiRenderer.h b/WickedEngine/wiRenderer.h index cad62a037..bc0037a79 100644 --- a/WickedEngine/wiRenderer.h +++ b/WickedEngine/wiRenderer.h @@ -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() diff --git a/WickedEngine/wiScene.cpp b/WickedEngine/wiScene.cpp index 569e284a5..a86623a77 100644 --- a/WickedEngine/wiScene.cpp +++ b/WickedEngine/wiScene.cpp @@ -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); } diff --git a/WickedEngine/wiScene.h b/WickedEngine/wiScene.h index b3c7048bc..589bcfbc0 100644 --- a/WickedEngine/wiScene.h +++ b/WickedEngine/wiScene.h @@ -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 diff --git a/WickedEngine/wiScene_Components.cpp b/WickedEngine/wiScene_Components.cpp index 65e60704c..79336b69a 100644 --- a/WickedEngine/wiScene_Components.cpp +++ b/WickedEngine/wiScene_Components.cpp @@ -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()); + } } diff --git a/WickedEngine/wiScene_Components.h b/WickedEngine/wiScene_Components.h index ff8d9595f..c9b9db776 100644 --- a/WickedEngine/wiScene_Components.h +++ b/WickedEngine/wiScene_Components.h @@ -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 precomputed_obbs; // an array of OBBs that approximate the spline's volume + wi::vector 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; } } diff --git a/WickedEngine/wiTerrain.cpp b/WickedEngine/wiTerrain.cpp index 0dc1ef406..75790d9ea 100644 --- a/WickedEngine/wiTerrain.cpp +++ b/WickedEngine/wiTerrain.cpp @@ -13,6 +13,7 @@ #include #include #include +#include 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 splines; + wi::vector 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 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; diff --git a/WickedEngine/wiTerrain.h b/WickedEngine/wiTerrain.h index d70a29d42..9990bd375 100644 --- a/WickedEngine/wiTerrain.h +++ b/WickedEngine/wiTerrain.h @@ -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 vt; wi::vector heightmap_data; wi::graphics::Texture heightmap; diff --git a/WickedEngine/wiVersion.cpp b/WickedEngine/wiVersion.cpp index 911729922..25ec88946 100644 --- a/WickedEngine/wiVersion.cpp +++ b/WickedEngine/wiVersion.cpp @@ -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);