diff --git a/Source/Editor/Tools/Terrain/CreateTerrainDialog.cs b/Source/Editor/Tools/Terrain/CreateTerrainDialog.cs index 252891d44..fd28c302b 100644 --- a/Source/Editor/Tools/Terrain/CreateTerrainDialog.cs +++ b/Source/Editor/Tools/Terrain/CreateTerrainDialog.cs @@ -45,9 +45,6 @@ namespace FlaxEditor.Tools.Terrain [EditorOrder(130), EditorDisplay("Layout"), DefaultValue(null), Tooltip("The default material used for terrain rendering (chunks can override this). It must have Domain set to terrain.")] public MaterialBase Material; - [EditorOrder(200), EditorDisplay("Collision"), DefaultValue(null), AssetReference(typeof(PhysicalMaterial), true), Tooltip("Terrain default physical material used to define the collider physical properties.")] - public JsonAsset PhysicalMaterial; - [EditorOrder(210), EditorDisplay("Collision", "Collision LOD"), DefaultValue(-1), Limit(-1, 100, 0.1f), Tooltip("Terrain geometry LOD index used for collision.")] public int CollisionLOD = -1; @@ -152,7 +149,6 @@ namespace FlaxEditor.Tools.Terrain terrain.Setup(_options.LODCount, (int)_options.ChunkSize); terrain.Transform = new Transform(_options.Position, _options.Orientation, _options.Scale); terrain.Material = _options.Material; - terrain.PhysicalMaterial = _options.PhysicalMaterial; terrain.CollisionLOD = _options.CollisionLOD; if (_options.Heightmap) terrain.Position -= new Vector3(0, _options.HeightmapScale * 0.5f, 0); diff --git a/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp b/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp index f3e7baed1..cb2f3e736 100644 --- a/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp +++ b/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp @@ -644,6 +644,19 @@ void GetShapeGeometry(const CollisionShape& shape, PxGeometryHolder& geometry) } } +void GetShapeMaterials(Array>& materialsPhysX, Span materials) +{ + materialsPhysX.Resize(materials.Length()); + for (int32 i = 0; i < materials.Length(); i++) + { + PxMaterial* materialPhysX = DefaultMaterial; + const JsonAsset* material = materials.Get()[i]; + if (material && !material->WaitForLoaded() && material->Instance) + materialPhysX = (PxMaterial*)((PhysicalMaterial*)material->Instance)->GetPhysicsMaterial(); + materialsPhysX.Get()[i] = materialPhysX; + } +} + PxFilterFlags FilterShader( PxFilterObjectAttributes attributes0, PxFilterData filterData0, PxFilterObjectAttributes attributes1, PxFilterData filterData1, @@ -2449,17 +2462,14 @@ void PhysicsBackend::AddRigidDynamicActorTorque(void* actor, const Vector3& torq actorPhysX->addTorque(C2P(torque), static_cast(mode)); } -void* PhysicsBackend::CreateShape(PhysicsColliderActor* collider, const CollisionShape& geometry, JsonAsset* material, bool enabled, bool trigger) +void* PhysicsBackend::CreateShape(PhysicsColliderActor* collider, const CollisionShape& geometry, Span materials, bool enabled, bool trigger) { const PxShapeFlags shapeFlags = GetShapeFlags(trigger, enabled); - PxMaterial* materialPhysX = DefaultMaterial; - if (material && !material->WaitForLoaded() && material->Instance) - { - materialPhysX = (PxMaterial*)((PhysicalMaterial*)material->Instance)->GetPhysicsMaterial(); - } + Array> materialsPhysX; + GetShapeMaterials(materialsPhysX, materials); PxGeometryHolder geometryPhysX; GetShapeGeometry(geometry, geometryPhysX); - PxShape* shapePhysX = PhysX->createShape(geometryPhysX.any(), *materialPhysX, true, shapeFlags); + PxShape* shapePhysX = PhysX->createShape(geometryPhysX.any(), materialsPhysX.Get(), materialsPhysX.Count(), true, shapeFlags); shapePhysX->userData = collider; #if PHYSX_DEBUG_NAMING shapePhysX->setName("Shape"); @@ -2549,15 +2559,12 @@ void PhysicsBackend::SetShapeContactOffset(void* shape, float value) shapePhysX->setContactOffset(Math::Max(shapePhysX->getRestOffset() + ZeroTolerance, value)); } -void PhysicsBackend::SetShapeMaterial(void* shape, JsonAsset* material) +void PhysicsBackend::SetShapeMaterials(void* shape, Span materials) { auto shapePhysX = (PxShape*)shape; - PxMaterial* materialPhysX = DefaultMaterial; - if (material && !material->WaitForLoaded() && material->Instance) - { - materialPhysX = (PxMaterial*)((PhysicalMaterial*)material->Instance)->GetPhysicsMaterial(); - } - shapePhysX->setMaterials(&materialPhysX, 1); + Array> materialsPhysX; + GetShapeMaterials(materialsPhysX, materials); + shapePhysX->setMaterials(materialsPhysX.Get(), materialsPhysX.Count()); } void PhysicsBackend::SetShapeGeometry(void* shape, const CollisionShape& geometry) @@ -4077,10 +4084,17 @@ void PhysicsBackend::GetHeightFieldSize(void* heightField, int32& rows, int32& c columns = (int32)heightFieldPhysX->getNbColumns(); } -float PhysicsBackend::GetHeightFieldHeight(void* heightField, float x, float z) +float PhysicsBackend::GetHeightFieldHeight(void* heightField, int32 x, int32 z) { auto heightFieldPhysX = (PxHeightField*)heightField; - return heightFieldPhysX->getHeight(x, z); + return heightFieldPhysX->getHeight((float)x, (float)z); +} + +PhysicsBackend::HeightFieldSample PhysicsBackend::GetHeightFieldSample(void* heightField, int32 x, int32 z) +{ + auto heightFieldPhysX = (PxHeightField*)heightField; + auto sample = heightFieldPhysX->getSample(x, z); + return { sample.height, sample.materialIndex0, sample.materialIndex1 }; } bool PhysicsBackend::ModifyHeightField(void* heightField, int32 startCol, int32 startRow, int32 cols, int32 rows, const HeightFieldSample* data) diff --git a/Source/Engine/Physics/PhysicsBackend.h b/Source/Engine/Physics/PhysicsBackend.h index f20683e31..d7393df2c 100644 --- a/Source/Engine/Physics/PhysicsBackend.h +++ b/Source/Engine/Physics/PhysicsBackend.h @@ -4,6 +4,7 @@ #include "Physics.h" #include "PhysicsSettings.h" +#include "Engine/Core/Types/Span.h" struct HingeJointDrive; struct SpringParameters; @@ -182,7 +183,7 @@ public: static void AddRigidDynamicActorTorque(void* actor, const Vector3& torque, ForceMode mode); // Shapes - static void* CreateShape(PhysicsColliderActor* collider, const CollisionShape& geometry, JsonAsset* material, bool enabled, bool trigger); + static void* CreateShape(PhysicsColliderActor* collider, const CollisionShape& geometry, Span materials, bool enabled, bool trigger); static void SetShapeState(void* shape, bool enabled, bool trigger); static void SetShapeFilterMask(void* shape, uint32 mask0, uint32 mask1); static void* GetShapeActor(void* shape); @@ -191,7 +192,7 @@ public: static void GetShapeLocalPose(void* shape, Vector3& position, Quaternion& orientation); static void SetShapeLocalPose(void* shape, const Vector3& position, const Quaternion& orientation); static void SetShapeContactOffset(void* shape, float value); - static void SetShapeMaterial(void* shape, JsonAsset* material); + static void SetShapeMaterials(void* shape, Span materials); static void SetShapeGeometry(void* shape, const CollisionShape& geometry); static void AttachShape(void* shape, void* actor); static void DetachShape(void* shape, void* actor); @@ -303,7 +304,8 @@ public: static void GetTriangleMeshTriangles(void* triangleMesh, Array& vertexBuffer, Array& indexBuffer); static const uint32* GetTriangleMeshRemap(void* triangleMesh, uint32& count); static void GetHeightFieldSize(void* heightField, int32& rows, int32& columns); - static float GetHeightFieldHeight(void* heightField, float x, float z); + static float GetHeightFieldHeight(void* heightField, int32 x, int32 z); + static HeightFieldSample GetHeightFieldSample(void* heightField, int32 x, int32 z); static bool ModifyHeightField(void* heightField, int32 startCol, int32 startRow, int32 cols, int32 rows, const HeightFieldSample* data); static void FlushRequests(); static void FlushRequests(void* scene); @@ -330,6 +332,14 @@ public: flags = (RigidDynamicFlags)(((uint32)flags & ~(uint32)flag) | (value ? (uint32)flag : 0)); SetRigidDynamicActorFlags(actor, flags); } + FORCE_INLINE static void* CreateShape(PhysicsColliderActor* collider, const CollisionShape& geometry, JsonAsset* material, bool enabled, bool trigger) + { + return CreateShape(collider, geometry, Span(&material, 1), enabled, trigger); + } + FORCE_INLINE static void SetShapeMaterial(void* shape, JsonAsset* material) + { + SetShapeMaterials(shape, Span(&material, 1)); + } }; DECLARE_ENUM_OPERATORS(PhysicsBackend::ActorFlags); diff --git a/Source/Engine/Physics/PhysicsBackendEmpty.cpp b/Source/Engine/Physics/PhysicsBackendEmpty.cpp index 1a418af0c..e26898494 100644 --- a/Source/Engine/Physics/PhysicsBackendEmpty.cpp +++ b/Source/Engine/Physics/PhysicsBackendEmpty.cpp @@ -408,7 +408,7 @@ void PhysicsBackend::AddRigidDynamicActorTorque(void* actor, const Vector3& torq { } -void* PhysicsBackend::CreateShape(PhysicsColliderActor* collider, const CollisionShape& geometry, JsonAsset* material, bool enabled, bool trigger) +void* PhysicsBackend::CreateShape(PhysicsColliderActor* collider, const CollisionShape& geometry, Span materials, bool enabled, bool trigger) { return DUMY_HANDLE; } @@ -447,7 +447,7 @@ void PhysicsBackend::SetShapeContactOffset(void* shape, float value) { } -void PhysicsBackend::SetShapeMaterial(void* shape, JsonAsset* material) +void PhysicsBackend::SetShapeMaterials(void* shape, Span materials) { } @@ -826,11 +826,16 @@ void PhysicsBackend::GetHeightFieldSize(void* heightField, int32& rows, int32& c columns = 0; } -float PhysicsBackend::GetHeightFieldHeight(void* heightField, float x, float z) +float PhysicsBackend::GetHeightFieldHeight(void* heightField, int32 x, int32 z) { return 0.0f; } +PhysicsBackend::HeightFieldSample PhysicsBackend::GetHeightFieldSample(void* heightField, int32 x, int32 z) +{ + return HeightFieldSample(); +} + bool PhysicsBackend::ModifyHeightField(void* heightField, int32 startCol, int32 startRow, int32 cols, int32 rows, const HeightFieldSample* data) { return true; diff --git a/Source/Engine/Terrain/Terrain.cpp b/Source/Engine/Terrain/Terrain.cpp index 772abf4d3..3f71760c3 100644 --- a/Source/Engine/Terrain/Terrain.cpp +++ b/Source/Engine/Terrain/Terrain.cpp @@ -29,7 +29,7 @@ Terrain::Terrain(const SpawnParams& params) , _cachedScale(1.0f) { _drawCategory = SceneRendering::SceneDrawAsync; - PhysicalMaterial.Changed.Bind(this); + _physicalMaterials.Resize(8); } Terrain::~Terrain() @@ -228,22 +228,6 @@ void Terrain::DrawChunk(const RenderContext& renderContext, const Int2& patchCoo } } -void Terrain::OnPhysicalMaterialChanged() -{ - if (_patches.IsEmpty()) - return; - - // Update the shapes material - for (int32 pathIndex = 0; pathIndex < _patches.Count(); pathIndex++) - { - const auto patch = _patches[pathIndex]; - if (patch->HasCollision()) - { - PhysicsBackend::SetShapeMaterial(patch->_physicsShape, PhysicalMaterial); - } - } -} - #if TERRAIN_USE_PHYSICS_DEBUG void Terrain::DrawPhysicsDebug(RenderView& view) @@ -295,6 +279,21 @@ void Terrain::SetCollisionLOD(int32 value) #endif } +void Terrain::SetPhysicalMaterials(const Array, FixedAllocation<8>>& value) +{ + _physicalMaterials = value; + _physicalMaterials.Resize(8); + JsonAsset* materials[8]; + for (int32 i = 0;i<8;i++) + materials[i] = _physicalMaterials[i]; + for (int32 pathIndex = 0; pathIndex < _patches.Count(); pathIndex++) + { + const auto patch = _patches.Get()[pathIndex]; + if (patch->HasCollision()) + PhysicsBackend::SetShapeMaterials(patch->_physicsShape, ToSpan(materials, 8)); + } +} + TerrainPatch* Terrain::GetPatch(const Int2& patchCoord) const { return GetPatch(patchCoord.X, patchCoord.Y); @@ -667,8 +666,8 @@ void Terrain::Serialize(SerializeStream& stream, const void* otherObj) SERIALIZE_MEMBER(ScaleInLightmap, _scaleInLightmap); SERIALIZE_MEMBER(BoundsExtent, _boundsExtent); SERIALIZE_MEMBER(CollisionLOD, _collisionLod); + SERIALIZE_MEMBER(PhysicalMaterials, _physicalMaterials); SERIALIZE(Material); - SERIALIZE(PhysicalMaterial); SERIALIZE(DrawModes); SERIALIZE_MEMBER(LODCount, _lodCount); @@ -714,8 +713,8 @@ void Terrain::Deserialize(DeserializeStream& stream, ISerializeModifier* modifie DESERIALIZE_MEMBER(LODDistribution, _lodDistribution); DESERIALIZE_MEMBER(ScaleInLightmap, _scaleInLightmap); DESERIALIZE_MEMBER(BoundsExtent, _boundsExtent); + DESERIALIZE_MEMBER(PhysicalMaterials, _physicalMaterials); DESERIALIZE(Material); - DESERIALIZE(PhysicalMaterial); DESERIALIZE(DrawModes); member = stream.FindMember("LODCount"); @@ -780,6 +779,15 @@ void Terrain::Deserialize(DeserializeStream& stream, ISerializeModifier* modifie // [Deprecated on 27.04.2022, expires on 27.04.2024] if (modifier->EngineBuild <= 6331) DrawModes |= DrawPass::GlobalSurfaceAtlas; + + // [Deprecated on 15.02.2024, expires on 15.02.2026] + JsonAssetReference PhysicalMaterial; + DESERIALIZE(PhysicalMaterial); + if (PhysicalMaterial) + { + for (auto& e : _physicalMaterials) + e = PhysicalMaterial; + } } RigidBody* Terrain::GetAttachedRigidBody() const diff --git a/Source/Engine/Terrain/Terrain.h b/Source/Engine/Terrain/Terrain.h index 1f495c5ab..aad0e4e1b 100644 --- a/Source/Engine/Terrain/Terrain.h +++ b/Source/Engine/Terrain/Terrain.h @@ -41,13 +41,12 @@ struct RenderView; /// API_CLASS(Sealed) class FLAXENGINE_API Terrain : public PhysicsColliderActor { -DECLARE_SCENE_OBJECT(Terrain); + DECLARE_SCENE_OBJECT(Terrain); friend Terrain; friend TerrainPatch; friend TerrainChunk; private: - char _lodBias; char _forcedLod; char _collisionLod; @@ -60,28 +59,21 @@ private: Float3 _cachedScale; Array> _patches; Array _drawChunks; + Array, FixedAllocation<8>> _physicalMaterials; public: - /// /// Finalizes an instance of the class. /// ~Terrain(); public: - /// /// The default material used for terrain rendering (chunks can override this). /// API_FIELD(Attributes="EditorOrder(100), DefaultValue(null), EditorDisplay(\"Terrain\")") AssetReference Material; - /// - /// The physical material used to define the terrain collider physical properties. - /// - API_FIELD(Attributes="EditorOrder(520), DefaultValue(null), Limit(-1, 100, 0.1f), EditorDisplay(\"Collision\")") - JsonAssetReference<::PhysicalMaterial> PhysicalMaterial; - /// /// The draw passes to use for rendering this object. /// @@ -89,7 +81,6 @@ public: DrawPass DrawModes = DrawPass::Default; public: - /// /// Gets the terrain Level Of Detail bias value. Allows to increase or decrease rendered terrain quality. /// @@ -180,6 +171,21 @@ public: /// API_PROPERTY() void SetCollisionLOD(int32 value); + /// + /// Gets the list with physical materials used to define the terrain collider physical properties - each for terrain layer (layer index matches index in this array). + /// + API_PROPERTY(Attributes="EditorOrder(520), EditorDisplay(\"Collision\"), Collection(MinCount = 8, MaxCount = 8)") + FORCE_INLINE const Array, FixedAllocation<8>>& GetPhysicalMaterials() const + { + return _physicalMaterials; + } + + /// + /// Sets the list with physical materials used to define the terrain collider physical properties - each for terrain layer (layer index matches index in this array). + /// + API_PROPERTY() + void SetPhysicalMaterials(const Array, FixedAllocation<8>>& value); + /// /// Gets the terrain Level Of Detail count. /// @@ -311,7 +317,6 @@ public: #endif public: - #if TERRAIN_EDITING /// @@ -362,7 +367,6 @@ public: void RemoveLightmap(); public: - /// /// Performs a raycast against this terrain collision shape. /// @@ -432,14 +436,11 @@ public: API_FUNCTION() void DrawChunk(API_PARAM(Ref) const RenderContext& renderContext, API_PARAM(Ref) const Int2& patchCoord, API_PARAM(Ref) const Int2& chunkCoord, MaterialBase* material, int32 lodIndex = 0) const; private: - - void OnPhysicalMaterialChanged(); #if TERRAIN_USE_PHYSICS_DEBUG - void DrawPhysicsDebug(RenderView& view); + void DrawPhysicsDebug(RenderView& view); #endif public: - // [PhysicsColliderActor] void Draw(RenderContext& renderContext) override; #if USE_EDITOR @@ -452,7 +453,6 @@ public: RigidBody* GetAttachedRigidBody() const override; protected: - // [PhysicsColliderActor] void OnEnable() override; void OnDisable() override; diff --git a/Source/Engine/Terrain/TerrainPatch.cpp b/Source/Engine/Terrain/TerrainPatch.cpp index 7fa449601..d30f65ec7 100644 --- a/Source/Engine/Terrain/TerrainPatch.cpp +++ b/Source/Engine/Terrain/TerrainPatch.cpp @@ -27,6 +27,9 @@ #include "Engine/ContentImporters/AssetsImportingManager.h" #endif #endif +#if TERRAIN_UPDATING +#include "Engine/Core/Collections/ArrayExtensions.h" +#endif #if USE_EDITOR #include "Engine/Debug/DebugDraw.h" #endif @@ -105,6 +108,7 @@ void TerrainPatch::RemoveLightmap() void TerrainPatch::UpdateBounds() { + PROFILE_CPU(); Chunks[0].UpdateBounds(); _bounds = Chunks[0]._bounds; for (int32 i = 1; i < CHUNKS_COUNT; i++) @@ -116,6 +120,8 @@ void TerrainPatch::UpdateBounds() void TerrainPatch::UpdateTransform() { + PROFILE_CPU(); + // Update physics if (_physicsActor) { @@ -138,8 +144,14 @@ void TerrainPatch::UpdateTransform() #if TERRAIN_EDITING || TERRAIN_UPDATING +bool IsValidMaterial(const JsonAssetReference& e) +{ + return e; +} + struct TerrainDataUpdateInfo { + TerrainPatch* Patch; int32 ChunkSize; int32 VertexCountEdge; int32 HeightmapSize; @@ -147,6 +159,36 @@ struct TerrainDataUpdateInfo int32 TextureSize; float PatchOffset; float PatchHeight; + Color32* SplatMaps[TERRAIN_MAX_SPLATMAPS_COUNT] = {}; + + TerrainDataUpdateInfo(TerrainPatch* patch, float patchOffset = 0.0f, float patchHeight = 1.0f) + : Patch(patch) + , PatchOffset(patchOffset) + , PatchHeight(patchHeight) + { + ChunkSize = patch->GetTerrain()->GetChunkSize(); + VertexCountEdge = ChunkSize + 1; + HeightmapSize = ChunkSize * TerrainPatch::CHUNKS_COUNT_EDGE + 1; + HeightmapLength = HeightmapSize * HeightmapSize; + TextureSize = VertexCountEdge * TerrainPatch::CHUNKS_COUNT_EDGE; + } + + bool UsePhysicalMaterials() const + { + return ArrayExtensions::Any>(Patch->GetTerrain()->GetPhysicalMaterials(), IsValidMaterial); + } + + // When using physical materials, then get splatmaps data required for per-triangle material indices + void GetSplatMaps() + { + if (SplatMaps[0]) + return; + if (UsePhysicalMaterials()) + { + for (int32 i = 0; i < TERRAIN_MAX_SPLATMAPS_COUNT; i++) + SplatMaps[i] = Patch->GetSplatMapData(i); + } + } }; // Shared data container for the terrain data updating shared by the normals and collision generation logic @@ -321,10 +363,9 @@ void UpdateNormalsAndHoles(const TerrainDataUpdateInfo& info, const float* heigh PROFILE_CPU_NAMED("Terrain.CalculateNormals"); // Expand the area for the normals to prevent issues on the edges (for the averaged normals) - const int32 heightMapSize = info.HeightmapSize; const Int2 modifiedEnd = modifiedOffset + modifiedSize; const Int2 normalsStart = Int2::Max(Int2::Zero, modifiedOffset - 1); - const Int2 normalsEnd = Int2::Min(heightMapSize, modifiedEnd + 1); + const Int2 normalsEnd = Int2::Min(info.HeightmapSize, modifiedEnd + 1); const Int2 normalsSize = normalsEnd - normalsStart; // Prepare memory @@ -342,7 +383,7 @@ void UpdateNormalsAndHoles(const TerrainDataUpdateInfo& info, const float* heigh // Get four vertices from the quad #define GET_VERTEX(a, b) \ int32 i##a##b = (z + (b) - normalsStart.Y) * normalsSize.X + (x + (a) - normalsStart.X); \ - int32 h##a##b = (z + (b)) * heightMapSize + (x + (a)); \ + int32 h##a##b = (z + (b)) * info.HeightmapSize + (x + (a)); \ Float3 v##a##b; v##a##b.X = (x + (a)) * TERRAIN_UNITS_PER_VERTEX; \ v##a##b.Y = heightmap[h##a##b]; \ v##a##b.Z = (z + (b)) * TERRAIN_UNITS_PER_VERTEX @@ -428,7 +469,7 @@ void UpdateNormalsAndHoles(const TerrainDataUpdateInfo& info, const float* heigh const int32 dz = chunkHeightmapZ + z - modifiedOffset.Y; if (dz < 0 || dz >= modifiedSize.Y) continue; - const int32 hz = (chunkHeightmapZ + z) * heightMapSize; + const int32 hz = (chunkHeightmapZ + z) * info.HeightmapSize; const int32 sz = (chunkHeightmapZ + z - normalsStart.Y) * normalsSize.X; const int32 tz = (chunkTextureZ + z) * info.TextureSize; @@ -546,13 +587,52 @@ void FixMips(const TerrainDataUpdateInfo& info, TextureBase::InitData* initData, } } -bool CookCollision(const TerrainDataUpdateInfo& info, TextureBase::InitData* initData, int32 collisionLod, Array* collisionData) +FORCE_INLINE byte GetPhysicalMaterial(const Color32& raw, const TerrainDataUpdateInfo& info, int32 chunkZ, int32 chunkX, int32 z, int32 x) +{ + byte result = 0; + if (ReadIsHole(raw)) + { + // Hole + result = (uint8)PhysicsBackend::HeightFieldMaterial::Hole; + } + else if (info.SplatMaps[0]) + { + // Use the layer with the highest influence (splatmap data is Mip0 so convert x/z coords back to LOD0) + uint8 layer = 0; + uint8 layerWeight = 0; + const int32 splatmapTextureIndex = (chunkZ * info.ChunkSize + z) * info.HeightmapSize + chunkX * info.ChunkSize + x; + for (int32 splatIndex = 0; splatIndex < TERRAIN_MAX_SPLATMAPS_COUNT; splatIndex++) + { + for (int32 channelIndex = 0; channelIndex < 4; channelIndex++) + { + // Assume splatmap data pitch matches the row size and shift by channel index to simply sample at R chanel + const Color32* splatmap = (const Color32*)((const byte*)info.SplatMaps[splatIndex] + channelIndex); + const uint8 splat = splatmap[splatmapTextureIndex].R; + if (splat > layerWeight) + { + layer = splatIndex * 4 + channelIndex; + layerWeight = splat; + if (layerWeight == MAX_uint8) + break; + } + } + if (layerWeight == MAX_uint8) + break; + } + result = layer; + } + return result; +} + +bool CookCollision(TerrainDataUpdateInfo& info, TextureBase::InitData* initData, int32 collisionLod, Array* collisionData) { #if COMPILE_WITH_PHYSICS_COOKING + info.GetSplatMaps(); PROFILE_CPU_NAMED("Terrain.CookCollision"); // Prepare data const int32 collisionLOD = Math::Clamp(collisionLod, 0, initData->Mips.Count() - 1); + const int32 collisionLODInv = (int32)Math::Pow(2.0f, (float)collisionLOD); const int32 heightFieldChunkSize = ((info.ChunkSize + 1) >> collisionLOD) - 1; const int32 heightFieldSize = heightFieldChunkSize * TerrainPatch::CHUNKS_COUNT_EDGE + 1; const int32 heightFieldLength = heightFieldSize * heightFieldSize; @@ -562,36 +642,30 @@ bool CookCollision(const TerrainDataUpdateInfo& info, TextureBase::InitData* ini Platform::MemoryClear(heightFieldData, sizeof(PhysicsBackend::HeightFieldSample) * heightFieldLength); // Setup terrain collision information - auto& mip = initData->Mips[collisionLOD]; + const auto& mip = initData->Mips[collisionLOD]; const int32 vertexCountEdgeMip = info.VertexCountEdge >> collisionLOD; const int32 textureSizeMip = info.TextureSize >> collisionLOD; for (int32 chunkX = 0; chunkX < TerrainPatch::CHUNKS_COUNT_EDGE; chunkX++) { const int32 chunkTextureX = chunkX * vertexCountEdgeMip; const int32 chunkStartX = chunkX * heightFieldChunkSize; - for (int32 chunkZ = 0; chunkZ < TerrainPatch::CHUNKS_COUNT_EDGE; chunkZ++) { const int32 chunkTextureZ = chunkZ * vertexCountEdgeMip; const int32 chunkStartZ = chunkZ * heightFieldChunkSize; - for (int32 z = 0; z < vertexCountEdgeMip; z++) { + const int32 heightmapZ = chunkStartZ + z; for (int32 x = 0; x < vertexCountEdgeMip; x++) { - const int32 textureIndex = (chunkTextureZ + z) * textureSizeMip + chunkTextureX + x; - - const Color32 raw = mip.Data.Get()[textureIndex]; - const float normalizedHeight = ReadNormalizedHeight(raw); - const bool isHole = ReadIsHole(raw); - const int32 heightmapX = chunkStartX + x; - const int32 heightmapZ = chunkStartZ + z; + + const int32 textureIndex = (chunkTextureZ + z) * textureSizeMip + chunkTextureX + x; + const Color32 raw = mip.Data.Get()[textureIndex]; + sample.Height = int16(TERRAIN_PATCH_COLLISION_QUANTIZATION * ReadNormalizedHeight(raw)); + sample.MaterialIndex0 = sample.MaterialIndex1 = GetPhysicalMaterial(raw, info, chunkZ, chunkX, z * collisionLODInv, x * collisionLODInv); + const int32 dstIndex = (heightmapX * heightFieldSize) + heightmapZ; - - sample.Height = int16(TERRAIN_PATCH_COLLISION_QUANTIZATION * normalizedHeight); - sample.MaterialIndex0 = sample.MaterialIndex1 = isHole ? (uint8)PhysicsBackend::HeightFieldMaterial::Hole : 0; - heightFieldData[dstIndex] = sample; } } @@ -620,14 +694,16 @@ bool CookCollision(const TerrainDataUpdateInfo& info, TextureBase::InitData* ini #endif } -bool ModifyCollision(const TerrainDataUpdateInfo& info, TextureBase::InitData* initData, int32 collisionLod, const Int2& modifiedOffset, const Int2& modifiedSize, void* heightField) +bool ModifyCollision(TerrainDataUpdateInfo& info, TextureBase::InitData* initData, int32 collisionLod, const Int2& modifiedOffset, const Int2& modifiedSize, void* heightField) { + info.GetSplatMaps(); PROFILE_CPU_NAMED("Terrain.ModifyCollision"); // Prepare data const Vector2 modifiedOffsetRatio((float)modifiedOffset.X / info.HeightmapSize, (float)modifiedOffset.Y / info.HeightmapSize); const Vector2 modifiedSizeRatio((float)modifiedSize.X / info.HeightmapSize, (float)modifiedSize.Y / info.HeightmapSize); const int32 collisionLOD = Math::Clamp(collisionLod, 0, initData->Mips.Count() - 1); + const int32 collisionLODInv = (int32)Math::Pow(2.0f, (float)collisionLOD); const int32 heightFieldChunkSize = ((info.ChunkSize + 1) >> collisionLOD) - 1; const int32 heightFieldSize = heightFieldChunkSize * TerrainPatch::CHUNKS_COUNT_EDGE + 1; const Int2 samplesOffset(Vector2::Floor(modifiedOffsetRatio * (float)heightFieldSize)); @@ -646,56 +722,45 @@ bool ModifyCollision(const TerrainDataUpdateInfo& info, TextureBase::InitData* i Platform::MemoryClear(heightFieldData, sizeof(PhysicsBackend::HeightFieldSample) * heightFieldDataLength); // Setup terrain collision information - auto& mip = initData->Mips[collisionLOD]; + const auto& mip = initData->Mips[collisionLOD]; const int32 vertexCountEdgeMip = info.VertexCountEdge >> collisionLOD; const int32 textureSizeMip = info.TextureSize >> collisionLOD; for (int32 chunkX = 0; chunkX < TerrainPatch::CHUNKS_COUNT_EDGE; chunkX++) { const int32 chunkTextureX = chunkX * vertexCountEdgeMip; const int32 chunkStartX = chunkX * heightFieldChunkSize; - - // Skip unmodified chunks if (chunkStartX >= samplesEnd.X || chunkStartX + vertexCountEdgeMip < samplesOffset.X) - continue; + continue; // Skip unmodified chunks for (int32 chunkZ = 0; chunkZ < TerrainPatch::CHUNKS_COUNT_EDGE; chunkZ++) { const int32 chunkTextureZ = chunkZ * vertexCountEdgeMip; const int32 chunkStartZ = chunkZ * heightFieldChunkSize; - - // Skip unmodified chunks if (chunkStartZ >= samplesEnd.Y || chunkStartZ + vertexCountEdgeMip < samplesOffset.Y) - continue; + continue; // Skip unmodified chunks // TODO: adjust loop range to reduce iterations count for edge cases (skip checking unmodified samples) for (int32 z = 0; z < vertexCountEdgeMip; z++) { - // Skip unmodified columns const int32 heightmapZ = chunkStartZ + z; const int32 heightmapLocalZ = heightmapZ - samplesOffset.Y; if (heightmapLocalZ < 0 || heightmapLocalZ >= samplesSize.Y) - continue; + continue; // Skip unmodified columns // TODO: adjust loop range to reduce iterations count for edge cases (skip checking unmodified samples) for (int32 x = 0; x < vertexCountEdgeMip; x++) { - // Skip unmodified rows const int32 heightmapX = chunkStartX + x; const int32 heightmapLocalX = heightmapX - samplesOffset.X; if (heightmapLocalX < 0 || heightmapLocalX >= samplesSize.X) - continue; + continue; // Skip unmodified rows const int32 textureIndex = (chunkTextureZ + z) * textureSizeMip + chunkTextureX + x; - const Color32 raw = mip.Data.Get()[textureIndex]; - const float normalizedHeight = ReadNormalizedHeight(raw); - const bool isHole = ReadIsHole(raw); + sample.Height = int16(TERRAIN_PATCH_COLLISION_QUANTIZATION * ReadNormalizedHeight(raw)); + sample.MaterialIndex0 = sample.MaterialIndex1 = GetPhysicalMaterial(raw, info, chunkZ, chunkX, z * collisionLODInv, x * collisionLODInv); const int32 dstIndex = (heightmapLocalX * samplesSize.Y) + heightmapLocalZ; - - sample.Height = int16(TERRAIN_PATCH_COLLISION_QUANTIZATION * normalizedHeight); - sample.MaterialIndex0 = sample.MaterialIndex1 = isHole ? (uint8)PhysicsBackend::HeightFieldMaterial::Hole : 0; - heightFieldData[dstIndex] = sample; } } @@ -718,38 +783,23 @@ bool ModifyCollision(const TerrainDataUpdateInfo& info, TextureBase::InitData* i bool TerrainPatch::SetupHeightMap(int32 heightMapLength, const float* heightMap, const byte* holesMask, bool forceUseVirtualStorage) { - // Validate input + PROFILE_CPU_NAMED("Terrain.Setup"); if (heightMap == nullptr) { LOG(Warning, "Cannot create terrain without a heightmap specified."); return true; } - const int32 chunkSize = _terrain->_chunkSize; - const int32 vertexCountEdge = chunkSize + 1; - const int32 heightMapSize = chunkSize * CHUNKS_COUNT_EDGE + 1; - if (heightMapLength != heightMapSize * heightMapSize) + TerrainDataUpdateInfo info(this); + if (heightMapLength != info.HeightmapLength) { - LOG(Warning, "Invalid heightmap length. Terrain of chunk size equal {0} uses heightmap of size {1}x{1} (heightmap array length must be {2}). Input heightmap has length {3}.", chunkSize, heightMapSize, heightMapSize * heightMapSize, heightMapLength); + LOG(Warning, "Invalid heightmap length. Terrain of chunk size equal {0} uses heightmap of size {1}x{1} (heightmap array length must be {2}). Input heightmap has length {3}.", info.ChunkSize, info.HeightmapSize, info.HeightmapLength, heightMapLength); return true; } const PixelFormat pixelFormat = PixelFormat::R8G8B8A8_UNorm; - PROFILE_CPU_NAMED("Terrain.Setup"); - // Input heightmap data overlaps on chunk edges but it needs to be duplicated for chunks (each chunk has own scale-bias for height values normalization) - const int32 textureSize = vertexCountEdge * CHUNKS_COUNT_EDGE; const int32 pixelStride = PixelFormatExtensions::SizeInBytes(pixelFormat); - const int32 lodCount = Math::Min(_terrain->_lodCount, MipLevelsCount(vertexCountEdge) - 2); - - // Setup patch data info - TerrainDataUpdateInfo info; - info.ChunkSize = chunkSize; - info.VertexCountEdge = vertexCountEdge; - info.HeightmapSize = heightMapSize; - info.HeightmapLength = heightMapLength; - info.TextureSize = textureSize; - info.PatchOffset = 0.0f; - info.PatchHeight = 1.0f; + const int32 lodCount = Math::Min(_terrain->_lodCount, MipLevelsCount(info.VertexCountEdge) - 2); // Process heightmap to get per-patch height normalization values float chunkOffsets[CHUNKS_COUNT]; @@ -782,18 +832,17 @@ bool TerrainPatch::SetupHeightMap(int32 heightMapLength, const float* heightMap, // Create heightmap texture data source container auto initData = New(); initData->Format = pixelFormat; - initData->Width = textureSize; - initData->Height = textureSize; + initData->Width = info.TextureSize; + initData->Height = info.TextureSize; initData->ArraySize = 1; initData->Mips.Resize(lodCount); // Allocate top mip data { PROFILE_CPU_NAMED("Terrain.AllocateHeightmap"); - auto& mip = initData->Mips[0]; - mip.RowPitch = textureSize * pixelStride; - mip.SlicePitch = mip.RowPitch * textureSize; + mip.RowPitch = info.TextureSize * pixelStride; + mip.SlicePitch = mip.RowPitch * info.TextureSize; mip.Data.Allocate(mip.SlicePitch); } @@ -945,21 +994,17 @@ bool TerrainPatch::SetupHeightMap(int32 heightMapLength, const float* heightMap, bool TerrainPatch::SetupSplatMap(int32 index, int32 splatMapLength, const Color32* splatMap, bool forceUseVirtualStorage) { + PROFILE_CPU_NAMED("Terrain.SetupSplatMap"); CHECK_RETURN(index >= 0 && index < TERRAIN_MAX_SPLATMAPS_COUNT, true); - - // Validate input if (splatMap == nullptr) { LOG(Warning, "Cannot create terrain without any splatmap specified."); return true; } - const int32 chunkSize = _terrain->_chunkSize; - const int32 vertexCountEdge = chunkSize + 1; - const int32 heightMapSize = chunkSize * CHUNKS_COUNT_EDGE + 1; - const int32 heightMapLength = heightMapSize * heightMapSize; - if (splatMapLength != heightMapLength) + TerrainDataUpdateInfo info(this, _yOffset, _yHeight); + if (splatMapLength != info.HeightmapLength) { - LOG(Warning, "Invalid splatmap length. Terrain of chunk size equal {0} uses heightmap of size {1}x{1} (heightmap array length must be {2}). Input heightmap has length {3}.", chunkSize, heightMapSize, heightMapLength, splatMapLength); + LOG(Warning, "Invalid splatmap length. Terrain of chunk size equal {0} uses heightmap of size {1}x{1} (heightmap array length must be {2}). Input heightmap has length {3}.", info.ChunkSize, info.HeightmapSize, info.HeightmapLength, splatMapLength); return true; } const PixelFormat pixelFormat = PixelFormat::R8G8B8A8_UNorm; @@ -974,22 +1019,9 @@ bool TerrainPatch::SetupSplatMap(int32 index, int32 splatMapLength, const Color3 } } - PROFILE_CPU_NAMED("Terrain.SetupSplatMap"); - // Input splatmap data overlaps on chunk edges but it needs to be duplicated for chunks - const int32 textureSize = vertexCountEdge * CHUNKS_COUNT_EDGE; const int32 pixelStride = PixelFormatExtensions::SizeInBytes(pixelFormat); - const int32 lodCount = Math::Min(_terrain->_lodCount, MipLevelsCount(vertexCountEdge) - 2); - - // Setup patch data info - TerrainDataUpdateInfo info; - info.ChunkSize = chunkSize; - info.VertexCountEdge = vertexCountEdge; - info.HeightmapSize = heightMapSize; - info.HeightmapLength = heightMapLength; - info.TextureSize = textureSize; - info.PatchOffset = _yOffset; - info.PatchHeight = _yHeight; + const int32 lodCount = Math::Min(_terrain->_lodCount, MipLevelsCount(info.VertexCountEdge) - 2); // Prepare #if USE_EDITOR @@ -1016,18 +1048,17 @@ bool TerrainPatch::SetupSplatMap(int32 index, int32 splatMapLength, const Color3 // Create heightmap texture data source container auto initData = New(); initData->Format = pixelFormat; - initData->Width = textureSize; - initData->Height = textureSize; + initData->Width = info.TextureSize; + initData->Height = info.TextureSize; initData->ArraySize = 1; initData->Mips.Resize(lodCount); // Allocate top mip data { PROFILE_CPU_NAMED("Terrain.AllocateSplatmap"); - auto& mip = initData->Mips[0]; - mip.RowPitch = textureSize * pixelStride; - mip.SlicePitch = mip.RowPitch * textureSize; + mip.RowPitch = info.TextureSize * pixelStride; + mip.SlicePitch = mip.RowPitch * info.TextureSize; mip.Data.Allocate(mip.SlicePitch); } @@ -1112,8 +1143,6 @@ bool TerrainPatch::SetupSplatMap(int32 index, int32 splatMapLength, const Color3 bool TerrainPatch::InitializeHeightMap() { PROFILE_CPU_NAMED("Terrain.InitializeHeightMap"); - - // Initialize with flat heightmap data const auto heightmapSize = _terrain->GetChunkSize() * TerrainPatch::CHUNKS_COUNT_EDGE + 1; Array heightmap; heightmap.Resize(heightmapSize * heightmapSize); @@ -1179,6 +1208,7 @@ void TerrainPatch::ClearCache() void TerrainPatch::CacheHeightData() { PROFILE_CPU_NAMED("Terrain.CacheHeightData"); + const TerrainDataUpdateInfo info(this); // Ensure that heightmap data is all loaded // TODO: disable streaming for heightmap texture if it's being modified by the editor @@ -1198,16 +1228,9 @@ void TerrainPatch::CacheHeightData() return; } - // Get texture input (note: this must match Setup method) - const int32 chunkSize = _terrain->_chunkSize; - const int32 vertexCountEdge = chunkSize + 1; - const int32 textureSize = vertexCountEdge * CHUNKS_COUNT_EDGE; - const int32 heightMapSize = chunkSize * CHUNKS_COUNT_EDGE + 1; - // Allocate data - const int32 heightMapLength = heightMapSize * heightMapSize; - _cachedHeightMap.Resize(heightMapLength); - _cachedHolesMask.Resize(heightMapLength); + _cachedHeightMap.Resize(info.HeightmapLength); + _cachedHolesMask.Resize(info.HeightmapLength); _wasHeightModified = false; // Extract heightmap data and denormalize it to get the pure height field @@ -1217,18 +1240,18 @@ void TerrainPatch::CacheHeightData() const auto holesMaskPtr = _cachedHolesMask.Get(); for (int32 chunkIndex = 0; chunkIndex < CHUNKS_COUNT; chunkIndex++) { - const int32 chunkTextureX = Chunks[chunkIndex]._x * vertexCountEdge; - const int32 chunkTextureZ = Chunks[chunkIndex]._z * vertexCountEdge; + const int32 chunkTextureX = Chunks[chunkIndex]._x * info.VertexCountEdge; + const int32 chunkTextureZ = Chunks[chunkIndex]._z * info.VertexCountEdge; - const int32 chunkHeightmapX = Chunks[chunkIndex]._x * chunkSize; - const int32 chunkHeightmapZ = Chunks[chunkIndex]._z * chunkSize; + const int32 chunkHeightmapX = Chunks[chunkIndex]._x * info.ChunkSize; + const int32 chunkHeightmapZ = Chunks[chunkIndex]._z * info.ChunkSize; - for (int32 z = 0; z < vertexCountEdge; z++) + for (int32 z = 0; z < info.VertexCountEdge; z++) { - const int32 tz = (chunkTextureZ + z) * textureSize; - const int32 sz = (chunkHeightmapZ + z) * heightMapSize; + const int32 tz = (chunkTextureZ + z) * info.TextureSize; + const int32 sz = (chunkHeightmapZ + z) * info.HeightmapSize; - for (int32 x = 0; x < vertexCountEdge; x++) + for (int32 x = 0; x < info.VertexCountEdge; x++) { const int32 tx = chunkTextureX + x; const int32 sx = chunkHeightmapX + x; @@ -1249,18 +1272,14 @@ void TerrainPatch::CacheHeightData() void TerrainPatch::CacheSplatData() { - // Prepare - const int32 chunkSize = _terrain->_chunkSize; - const int32 vertexCountEdge = chunkSize + 1; - const int32 textureSize = vertexCountEdge * CHUNKS_COUNT_EDGE; - const int32 heightMapSize = chunkSize * CHUNKS_COUNT_EDGE + 1; - const int32 heightMapLength = heightMapSize * heightMapSize; + PROFILE_CPU_NAMED("Terrain.CacheSplatData"); + const TerrainDataUpdateInfo info(this); // Cache all the splatmaps for (int32 index = 0; index < TERRAIN_MAX_SPLATMAPS_COUNT; index++) { // Allocate data - _cachedSplatMap[index].Resize(heightMapLength); + _cachedSplatMap[index].Resize(info.HeightmapLength); _wasSplatmapModified[index] = false; // Skip if has missing splatmap asset @@ -1272,8 +1291,6 @@ void TerrainPatch::CacheSplatData() continue; } - PROFILE_CPU_NAMED("Terrain.CacheSplatData"); - // Ensure that splatmap data is all loaded // TODO: disable streaming for heightmap texture if it's being modified by the editor if (Splatmap[index]->WaitForLoaded()) @@ -1296,18 +1313,18 @@ void TerrainPatch::CacheSplatData() const auto splatMapPtr = static_cast(_cachedSplatMap[index].Get()); for (int32 chunkIndex = 0; chunkIndex < CHUNKS_COUNT; chunkIndex++) { - const int32 chunkTextureX = Chunks[chunkIndex]._x * vertexCountEdge; - const int32 chunkTextureZ = Chunks[chunkIndex]._z * vertexCountEdge; + const int32 chunkTextureX = Chunks[chunkIndex]._x * info.VertexCountEdge; + const int32 chunkTextureZ = Chunks[chunkIndex]._z * info.VertexCountEdge; - const int32 chunkHeightmapX = Chunks[chunkIndex]._x * chunkSize; - const int32 chunkHeightmapZ = Chunks[chunkIndex]._z * chunkSize; + const int32 chunkHeightmapX = Chunks[chunkIndex]._x * info.ChunkSize; + const int32 chunkHeightmapZ = Chunks[chunkIndex]._z * info.ChunkSize; - for (int32 z = 0; z < vertexCountEdge; z++) + for (int32 z = 0; z < info.VertexCountEdge; z++) { - const int32 tz = (chunkTextureZ + z) * textureSize; - const int32 sz = (chunkHeightmapZ + z) * heightMapSize; + const int32 tz = (chunkTextureZ + z) * info.TextureSize; + const int32 sz = (chunkHeightmapZ + z) * info.HeightmapSize; - for (int32 x = 0; x < vertexCountEdge; x++) + for (int32 x = 0; x < info.VertexCountEdge; x++) { const int32 tx = chunkTextureX + x; const int32 sx = chunkHeightmapX + x; @@ -1324,9 +1341,7 @@ void TerrainPatch::CacheSplatData() bool TerrainPatch::ModifyHeightMap(const float* samples, const Int2& modifiedOffset, const Int2& modifiedSize) { // Validate input samples range - const int32 chunkSize = _terrain->_chunkSize; - const int32 vertexCountEdge = chunkSize + 1; - const int32 heightMapSize = chunkSize * CHUNKS_COUNT_EDGE + 1; + TerrainDataUpdateInfo info(this); if (samples == nullptr) { LOG(Warning, "Missing heightmap samples data."); @@ -1334,13 +1349,12 @@ bool TerrainPatch::ModifyHeightMap(const float* samples, const Int2& modifiedOff } if (modifiedOffset.X < 0 || modifiedOffset.Y < 0 || modifiedSize.X <= 0 || modifiedSize.Y <= 0 || - modifiedOffset.X + modifiedSize.X > heightMapSize || - modifiedOffset.Y + modifiedSize.Y > heightMapSize) + modifiedOffset.X + modifiedSize.X > info.HeightmapSize || + modifiedOffset.Y + modifiedSize.Y > info.HeightmapSize) { LOG(Warning, "Invalid heightmap samples range."); return true; } - PROFILE_CPU_NAMED("Terrain.ModifyHeightMap"); // Check if has no heightmap @@ -1364,28 +1378,17 @@ bool TerrainPatch::ModifyHeightMap(const float* samples, const Int2& modifiedOff // Modify heightmap data { PROFILE_CPU_NAMED("Terrain.WrtieCache"); - for (int32 z = 0; z < modifiedSize.Y; z++) { // TODO: use batches row mem copy for (int32 x = 0; x < modifiedSize.X; x++) { - heightMap[(z + modifiedOffset.Y) * heightMapSize + (x + modifiedOffset.X)] = samples[z * modifiedSize.X + x]; + heightMap[(z + modifiedOffset.Y) * info.HeightmapSize + (x + modifiedOffset.X)] = samples[z * modifiedSize.X + x]; } } } - // Setup patch data info - TerrainDataUpdateInfo info; - info.ChunkSize = chunkSize; - info.VertexCountEdge = vertexCountEdge; - info.HeightmapSize = heightMapSize; - info.HeightmapLength = heightMapSize * heightMapSize; - info.TextureSize = vertexCountEdge * CHUNKS_COUNT_EDGE; - info.PatchOffset = 0.0f; - info.PatchHeight = 1.0f; - // Process heightmap to get per-patch height normalization values float chunkOffsets[CHUNKS_COUNT]; float chunkHeights[CHUNKS_COUNT]; @@ -1426,15 +1429,13 @@ bool TerrainPatch::ModifyHeightMap(const float* samples, const Int2& modifiedOff chunk.UpdateTransform(); } _terrain->UpdateBounds(); - return UpdateHeightData(info, modifiedOffset, modifiedSize, wasHeightRangeChanged); + return UpdateHeightData(info, modifiedOffset, modifiedSize, wasHeightRangeChanged, true); } bool TerrainPatch::ModifyHolesMask(const byte* samples, const Int2& modifiedOffset, const Int2& modifiedSize) { // Validate input samples range - const int32 chunkSize = _terrain->_chunkSize; - const int32 vertexCountEdge = chunkSize + 1; - const int32 heightMapSize = chunkSize * CHUNKS_COUNT_EDGE + 1; + TerrainDataUpdateInfo info(this, _yOffset, _yHeight); if (samples == nullptr) { LOG(Warning, "Missing holes mask samples data."); @@ -1442,13 +1443,12 @@ bool TerrainPatch::ModifyHolesMask(const byte* samples, const Int2& modifiedOffs } if (modifiedOffset.X < 0 || modifiedOffset.Y < 0 || modifiedSize.X <= 0 || modifiedSize.Y <= 0 || - modifiedOffset.X + modifiedSize.X > heightMapSize || - modifiedOffset.Y + modifiedSize.Y > heightMapSize) + modifiedOffset.X + modifiedSize.X > info.HeightmapSize || + modifiedOffset.Y + modifiedSize.Y > info.HeightmapSize) { LOG(Warning, "Invalid holes mask samples range."); return true; } - PROFILE_CPU_NAMED("Terrain.ModifyHolesMask"); // Check if has no heightmap @@ -1472,28 +1472,17 @@ bool TerrainPatch::ModifyHolesMask(const byte* samples, const Int2& modifiedOffs // Modify holes mask data { PROFILE_CPU_NAMED("Terrain.WrtieCache"); - for (int32 z = 0; z < modifiedSize.Y; z++) { // TODO: use batches row mem copy for (int32 x = 0; x < modifiedSize.X; x++) { - holesMask[(z + modifiedOffset.Y) * heightMapSize + (x + modifiedOffset.X)] = samples[z * modifiedSize.X + x]; + holesMask[(z + modifiedOffset.Y) * info.HeightmapSize + (x + modifiedOffset.X)] = samples[z * modifiedSize.X + x]; } } } - // Setup patch data info - TerrainDataUpdateInfo info; - info.ChunkSize = chunkSize; - info.VertexCountEdge = vertexCountEdge; - info.HeightmapSize = heightMapSize; - info.HeightmapLength = heightMapSize * heightMapSize; - info.TextureSize = vertexCountEdge * CHUNKS_COUNT_EDGE; - info.PatchOffset = _yOffset; - info.PatchHeight = _yHeight; - // Check if has allocated texture if (_dataHeightmap) { @@ -1505,7 +1494,7 @@ bool TerrainPatch::ModifyHolesMask(const byte* samples, const Int2& modifiedOffs } // Update all the stuff - return UpdateHeightData(info, modifiedOffset, modifiedSize, false); + return UpdateHeightData(info, modifiedOffset, modifiedSize, false, true); } bool TerrainPatch::ModifySplatMap(int32 index, const Color32* samples, const Int2& modifiedOffset, const Int2& modifiedSize) @@ -1523,9 +1512,7 @@ bool TerrainPatch::ModifySplatMap(int32 index, const Color32* samples, const Int } // Validate input samples range - const int32 chunkSize = _terrain->_chunkSize; - const int32 vertexCountEdge = chunkSize + 1; - const int32 heightMapSize = chunkSize * CHUNKS_COUNT_EDGE + 1; + TerrainDataUpdateInfo info(this, _yOffset, _yHeight); if (samples == nullptr) { LOG(Warning, "Missing splatmap samples data."); @@ -1533,13 +1520,12 @@ bool TerrainPatch::ModifySplatMap(int32 index, const Color32* samples, const Int } if (modifiedOffset.X < 0 || modifiedOffset.Y < 0 || modifiedSize.X <= 0 || modifiedSize.Y <= 0 || - modifiedOffset.X + modifiedSize.X > heightMapSize || - modifiedOffset.Y + modifiedSize.Y > heightMapSize) + modifiedOffset.X + modifiedSize.X > info.HeightmapSize || + modifiedOffset.Y + modifiedSize.Y > info.HeightmapSize) { LOG(Warning, "Invalid heightmap samples range."); return true; } - PROFILE_CPU_NAMED("Terrain.ModifySplatMap"); // Get the current data to modify it @@ -1552,14 +1538,13 @@ bool TerrainPatch::ModifySplatMap(int32 index, const Color32* samples, const Int // Modify splat map data { PROFILE_CPU_NAMED("Terrain.WrtieCache"); - for (int32 z = 0; z < modifiedSize.Y; z++) { // TODO: use batches row mem copy for (int32 x = 0; x < modifiedSize.X; x++) { - splatMap[(z + modifiedOffset.Y) * heightMapSize + (x + modifiedOffset.X)] = samples[z * modifiedSize.X + x]; + splatMap[(z + modifiedOffset.Y) * info.HeightmapSize + (x + modifiedOffset.X)] = samples[z * modifiedSize.X + x]; } } } @@ -1570,7 +1555,6 @@ bool TerrainPatch::ModifySplatMap(int32 index, const Color32* samples, const Int if (dataSplatmap == nullptr) { PROFILE_CPU_NAMED("Terrain.InitDataStorage"); - if (Heightmap->WaitForLoaded()) { LOG(Error, "Failed to load heightmap."); @@ -1597,16 +1581,6 @@ bool TerrainPatch::ModifySplatMap(int32 index, const Color32* samples, const Int mip.Data.Allocate(mip.SlicePitch); } - // Setup patch data info - TerrainDataUpdateInfo info; - info.ChunkSize = chunkSize; - info.VertexCountEdge = vertexCountEdge; - info.HeightmapSize = heightMapSize; - info.HeightmapLength = heightMapSize * heightMapSize; - info.TextureSize = vertexCountEdge * CHUNKS_COUNT_EDGE; - info.PatchOffset = _yOffset; - info.PatchHeight = _yHeight; - // Update splat map storage data const bool hasSplatmap = splatmap; const auto splatmapData = dataSplatmap->Mips[0].Data.Get(); @@ -1712,12 +1686,18 @@ bool TerrainPatch::ModifySplatMap(int32 index, const Color32* samples, const Int // TODO: disable splatmap dynamic streaming - data on a GPU was modified and we don't want to override it with the old data stored in the asset container + // Update heightfield to reflect physical materials layering + if (info.UsePhysicalMaterials() && HasCollision()) + { + UpdateHeightData(info, modifiedOffset, modifiedSize, false, false); + } + return false; } -bool TerrainPatch::UpdateHeightData(const TerrainDataUpdateInfo& info, const Int2& modifiedOffset, const Int2& modifiedSize, bool wasHeightRangeChanged) +bool TerrainPatch::UpdateHeightData(TerrainDataUpdateInfo& info, const Int2& modifiedOffset, const Int2& modifiedSize, bool wasHeightRangeChanged, bool wasHeightChanged) { - // Prepare + PROFILE_CPU(); float* heightMap = GetHeightmapData(); byte* holesMask = GetHolesMaskData(); ASSERT(heightMap && holesMask); @@ -1753,9 +1733,7 @@ bool TerrainPatch::UpdateHeightData(const TerrainDataUpdateInfo& info, const Int // Downscale mip data for all lower LODs if (GenerateMips(_dataHeightmap)) - { return true; - } // Fix generated mip maps to keep the same values for chunk edges (reduce cracks on continuous LOD transitions) FixMips(info, _dataHeightmap, pixelStride); @@ -1779,9 +1757,7 @@ bool TerrainPatch::UpdateHeightData(const TerrainDataUpdateInfo& info, const Int } const auto collisionData = &_heightfield->Data; if (CookCollision(info, _dataHeightmap, _terrain->_collisionLod, collisionData)) - { return true; - } UpdateCollision(); } else @@ -1789,7 +1765,8 @@ bool TerrainPatch::UpdateHeightData(const TerrainDataUpdateInfo& info, const Int ScopeLock lock(_collisionLocker); if (ModifyCollision(info, _dataHeightmap, _terrain->_collisionLod, modifiedOffset, modifiedSize, _physicsHeightField)) return true; - UpdateCollisionScale(); + if (wasHeightChanged) + UpdateCollisionScale(); } #else // Modify heightfield samples (without cooking collision which is done on a separate async task) @@ -1811,6 +1788,9 @@ bool TerrainPatch::UpdateHeightData(const TerrainDataUpdateInfo& info, const Int } #endif + if (!wasHeightChanged) + return false; + // Invalidate cache #if TERRAIN_USE_PHYSICS_DEBUG _debugLines.Resize(0); @@ -1843,18 +1823,8 @@ void TerrainPatch::SaveHeightData() { return; } - PROFILE_CPU_NAMED("Terrain.Save"); - - // Setup patch data info - TerrainDataUpdateInfo info; - info.ChunkSize = _terrain->_chunkSize; - info.VertexCountEdge = info.ChunkSize + 1; - info.HeightmapSize = info.ChunkSize * CHUNKS_COUNT_EDGE + 1; - info.HeightmapLength = info.HeightmapSize * info.HeightmapSize; - info.TextureSize = info.VertexCountEdge * CHUNKS_COUNT_EDGE; - info.PatchOffset = _yOffset; - info.PatchHeight = _yHeight; + TerrainDataUpdateInfo info(this, _yOffset, _yHeight); // Save heightmap to asset if (Heightmap->WaitForLoaded()) @@ -1913,7 +1883,6 @@ void TerrainPatch::SaveSplatData(int32 index) { return; } - PROFILE_CPU_NAMED("Terrain.Save"); // Save splatmap to asset @@ -1937,6 +1906,7 @@ void TerrainPatch::SaveSplatData(int32 index) bool TerrainPatch::UpdateCollision() { + PROFILE_CPU(); ScopeLock lock(_collisionLocker); // Update collision @@ -2111,6 +2081,7 @@ void TerrainPatch::UpdatePostManualDeserialization() void TerrainPatch::CreateCollision() { + PROFILE_CPU(); ASSERT(!HasCollision()); if (CreateHeightField()) return; @@ -2125,7 +2096,10 @@ void TerrainPatch::CreateCollision() shape.SetHeightField(_physicsHeightField, heightScale, rowScale, columnScale); // Create shape - _physicsShape = PhysicsBackend::CreateShape(_terrain, shape, _terrain->PhysicalMaterial, _terrain->IsActiveInHierarchy(), false); + JsonAsset* materials[8]; + for (int32 i = 0;i<8;i++) + materials[i] = _terrain->GetPhysicalMaterials()[i]; + _physicsShape = PhysicsBackend::CreateShape(_terrain, shape, ToSpan(materials, 8), _terrain->IsActiveInHierarchy(), false); PhysicsBackend::SetShapeLocalPose(_physicsShape, Vector3(0, _yOffset * terrainTransform.Scale.Y, 0), Quaternion::Identity); // Create static actor @@ -2137,6 +2111,7 @@ void TerrainPatch::CreateCollision() bool TerrainPatch::CreateHeightField() { + PROFILE_CPU(); ASSERT(_physicsHeightField == nullptr); // Skip if height field data is missing but warn on loading failed @@ -2162,6 +2137,7 @@ bool TerrainPatch::CreateHeightField() void TerrainPatch::UpdateCollisionScale() const { + PROFILE_CPU(); ASSERT(HasCollision()); // Create geometry @@ -2179,6 +2155,7 @@ void TerrainPatch::UpdateCollisionScale() const void TerrainPatch::DestroyCollision() { + PROFILE_CPU(); ScopeLock lock(_collisionLocker); ASSERT(HasCollision()); @@ -2205,6 +2182,7 @@ void TerrainPatch::DestroyCollision() void TerrainPatch::CacheDebugLines() { + PROFILE_CPU(); ASSERT(_debugLines.IsEmpty() && _physicsHeightField); int32 rows, cols; @@ -2213,12 +2191,21 @@ void TerrainPatch::CacheDebugLines() _debugLines.Resize((rows - 1) * (cols - 1) * 6 + (cols + rows - 2) * 2); Vector3* data = _debugLines.Get(); -#define GET_VERTEX(x, y) const Vector3 v##x##y((float)(row + (x)), PhysicsBackend::GetHeightFieldHeight(_physicsHeightField, (float)(row + (x)), (float)(col + (y))) / TERRAIN_PATCH_COLLISION_QUANTIZATION, (float)(col + (y))) +#define GET_VERTEX(x, y) const Vector3 v##x##y((float)(row + (x)), PhysicsBackend::GetHeightFieldHeight(_physicsHeightField, row + (x), col + (y)) / TERRAIN_PATCH_COLLISION_QUANTIZATION, (float)(col + (y))) for (int32 row = 0; row < rows - 1; row++) { for (int32 col = 0; col < cols - 1; col++) { + // Skip holes + const auto sample = PhysicsBackend::GetHeightFieldSample(_physicsHeightField, row, col); + if (sample.MaterialIndex0 == (uint8)PhysicsBackend::HeightFieldMaterial::Hole) + { + for (int32 i = 0; i < 6; i++) + *data++ = Vector3::Zero; + continue; + } + GET_VERTEX(0, 0); GET_VERTEX(0, 1); GET_VERTEX(1, 0); @@ -2294,6 +2281,7 @@ const Array& TerrainPatch::GetCollisionTriangles() ScopeLock lock(_collisionLocker); if (!_physicsShape || _collisionTriangles.HasItems()) return _collisionTriangles; + PROFILE_CPU(); int32 rows, cols; PhysicsBackend::GetHeightFieldSize(_physicsHeightField, rows, cols); @@ -2301,7 +2289,7 @@ const Array& TerrainPatch::GetCollisionTriangles() _collisionTriangles.Resize((rows - 1) * (cols - 1) * 6); Vector3* data = _collisionTriangles.Get(); -#define GET_VERTEX(x, y) Vector3 v##x##y((float)(row + (x)), PhysicsBackend::GetHeightFieldHeight(_physicsHeightField, (float)(row + (x)), (float)(col + (y))) / TERRAIN_PATCH_COLLISION_QUANTIZATION, (float)(col + (y))); Vector3::Transform(v##x##y, world, v##x##y) +#define GET_VERTEX(x, y) Vector3 v##x##y((float)(row + (x)), PhysicsBackend::GetHeightFieldHeight(_physicsHeightField, row + (x), col + (y)) / TERRAIN_PATCH_COLLISION_QUANTIZATION, (float)(col + (y))); Vector3::Transform(v##x##y, world, v##x##y) const float size = _terrain->_chunkSize * TERRAIN_UNITS_PER_VERTEX * CHUNKS_COUNT_EDGE; const Transform terrainTransform = _terrain->_transform; @@ -2312,6 +2300,15 @@ const Array& TerrainPatch::GetCollisionTriangles() { for (int32 col = 0; col < cols - 1; col++) { + // Skip holes + const auto sample = PhysicsBackend::GetHeightFieldSample(_physicsHeightField, row, col); + if (sample.MaterialIndex0 == (uint8)PhysicsBackend::HeightFieldMaterial::Hole) + { + for (int32 i = 0; i < 6; i++) + *data++ = Vector3::Zero; + continue; + } + GET_VERTEX(0, 0); GET_VERTEX(0, 1); GET_VERTEX(1, 0); @@ -2334,6 +2331,7 @@ const Array& TerrainPatch::GetCollisionTriangles() void TerrainPatch::GetCollisionTriangles(const BoundingSphere& bounds, Array& result) { + PROFILE_CPU(); result.Clear(); // Skip if no intersection with patch @@ -2430,6 +2428,7 @@ void TerrainPatch::GetCollisionTriangles(const BoundingSphere& bounds, Array& vertexBuffer, Array& indexBuffer) { + PROFILE_CPU(); vertexBuffer.Clear(); indexBuffer.Clear(); @@ -2459,7 +2458,7 @@ void TerrainPatch::ExtractCollisionGeometry(Array& vertexBuffer, Array _debugLines; // TODO: large-worlds + Array _debugLines; // TODO: large-worlds #endif #if USE_EDITOR Array _collisionTriangles; // TODO: large-worlds @@ -62,14 +60,12 @@ private: void Init(Terrain* terrain, int16 x, int16 z); public: - /// /// Finalizes an instance of the class. /// ~TerrainPatch(); public: - /// /// The chunks contained within the patch. Organized in 4x4 square. /// @@ -86,11 +82,9 @@ public: AssetReference Splatmap[TERRAIN_MAX_SPLATMAPS_COUNT]; public: - /// /// Gets the Y axis heightmap offset from terrain origin. /// - /// The offset. FORCE_INLINE float GetOffsetY() const { return _yOffset; @@ -99,7 +93,6 @@ public: /// /// Gets the Y axis heightmap height. /// - /// The height. FORCE_INLINE float GetHeightY() const { return _yHeight; @@ -108,7 +101,6 @@ public: /// /// Gets the x coordinate. /// - /// The x position. FORCE_INLINE int32 GetX() const { return _x; @@ -117,7 +109,6 @@ public: /// /// Gets the z coordinate. /// - /// The z position. FORCE_INLINE int32 GetZ() const { return _z; @@ -126,7 +117,6 @@ public: /// /// Gets the terrain. /// - /// The terrain, FORCE_INLINE Terrain* GetTerrain() const { return _terrain; @@ -168,14 +158,12 @@ public: /// /// Gets the patch world bounds. /// - /// The bounding box. FORCE_INLINE const BoundingBox& GetBounds() const { return _bounds; } public: - /// /// Removes the lightmap data from the terrain patch. /// @@ -192,7 +180,6 @@ public: void UpdateTransform(); #if TERRAIN_EDITING - /// /// Initializes the patch heightmap and collision to the default flat level. /// @@ -218,11 +205,9 @@ public: /// If set to true patch will use virtual storage by force. Otherwise it can use normal texture asset storage on drive (valid only during Editor). Runtime-created terrain can only use virtual storage (in RAM). /// True if failed, otherwise false. bool SetupSplatMap(int32 index, int32 splatMapLength, const Color32* splatMap, bool forceUseVirtualStorage = false); - #endif #if TERRAIN_UPDATING - /// /// Gets the raw pointer to the heightmap data. /// @@ -291,18 +276,15 @@ public: bool ModifySplatMap(int32 index, const Color32* samples, const Int2& modifiedOffset, const Int2& modifiedSize); private: - - bool UpdateHeightData(const struct TerrainDataUpdateInfo& info, const Int2& modifiedOffset, const Int2& modifiedSize, bool wasHeightRangeChanged); + bool UpdateHeightData(struct TerrainDataUpdateInfo& info, const Int2& modifiedOffset, const Int2& modifiedSize, bool wasHeightRangeChanged, bool wasHeightChanged); void SaveHeightData(); void CacheHeightData(); void SaveSplatData(); void SaveSplatData(int32 index); void CacheSplatData(); - #endif public: - /// /// Performs a raycast against this terrain collision shape. /// @@ -353,18 +335,14 @@ public: void ClosestPoint(const Vector3& position, Vector3& result) const; #if USE_EDITOR - /// /// Updates the patch data after manual deserialization called at runtime (eg. by editor undo). /// void UpdatePostManualDeserialization(); - #endif public: - #if USE_EDITOR - /// /// Gets the collision mesh triangles array (3 vertices per triangle in linear list). Cached internally to reuse data. /// @@ -377,7 +355,6 @@ public: /// The world-space bounds to find terrain triangles that intersect with it. /// The result triangles that intersect with the given bounds (in world-space). void GetCollisionTriangles(const BoundingSphere& bounds, Array& result); - #endif /// @@ -388,7 +365,6 @@ public: void ExtractCollisionGeometry(Array& vertexBuffer, Array& indexBuffer); private: - /// /// Determines whether this patch has created collision representation. /// @@ -419,8 +395,8 @@ private: void DestroyCollision(); #if TERRAIN_USE_PHYSICS_DEBUG - void CacheDebugLines(); - void DrawPhysicsDebug(RenderView& view); + void CacheDebugLines(); + void DrawPhysicsDebug(RenderView& view); #endif /// @@ -430,8 +406,8 @@ private: bool UpdateCollision(); void OnPhysicsSceneChanged(PhysicsScene* previous); -public: +public: // [ISerializable] void Serialize(SerializeStream& stream, const void* otherObj) override; void Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) override;