Add support for multiple physical materials in terrain - one for each painted layer

#1112 #2159
This commit is contained in:
Wojtek Figat
2024-02-16 17:11:40 +01:00
parent f04f1cc90e
commit 42b4443e14
8 changed files with 306 additions and 298 deletions

View File

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

View File

@@ -644,6 +644,19 @@ void GetShapeGeometry(const CollisionShape& shape, PxGeometryHolder& geometry)
}
}
void GetShapeMaterials(Array<PxMaterial*, InlinedAllocation<1>>& materialsPhysX, Span<JsonAsset*> 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<PxForceMode::Enum>(mode));
}
void* PhysicsBackend::CreateShape(PhysicsColliderActor* collider, const CollisionShape& geometry, JsonAsset* material, bool enabled, bool trigger)
void* PhysicsBackend::CreateShape(PhysicsColliderActor* collider, const CollisionShape& geometry, Span<JsonAsset*> 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<PxMaterial*, InlinedAllocation<1>> 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<JsonAsset*> 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<PxMaterial*, InlinedAllocation<1>> 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)

View File

@@ -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<JsonAsset*> 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<JsonAsset*> 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<Float3, HeapAllocation>& vertexBuffer, Array<int32, HeapAllocation>& 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<JsonAsset*>(&material, 1), enabled, trigger);
}
FORCE_INLINE static void SetShapeMaterial(void* shape, JsonAsset* material)
{
SetShapeMaterials(shape, Span<JsonAsset*>(&material, 1));
}
};
DECLARE_ENUM_OPERATORS(PhysicsBackend::ActorFlags);

View File

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

View File

@@ -29,7 +29,7 @@ Terrain::Terrain(const SpawnParams& params)
, _cachedScale(1.0f)
{
_drawCategory = SceneRendering::SceneDrawAsync;
PhysicalMaterial.Changed.Bind<Terrain, &Terrain::OnPhysicalMaterialChanged>(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<JsonAssetReference<PhysicalMaterial>, 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> PhysicalMaterial;
DESERIALIZE(PhysicalMaterial);
if (PhysicalMaterial)
{
for (auto& e : _physicalMaterials)
e = PhysicalMaterial;
}
}
RigidBody* Terrain::GetAttachedRigidBody() const

View File

@@ -41,13 +41,12 @@ struct RenderView;
/// <seealso cref="PhysicsColliderActor" />
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<TerrainPatch*, InlinedAllocation<64>> _patches;
Array<TerrainChunk*> _drawChunks;
Array<JsonAssetReference<PhysicalMaterial>, FixedAllocation<8>> _physicalMaterials;
public:
/// <summary>
/// Finalizes an instance of the <see cref="Terrain"/> class.
/// </summary>
~Terrain();
public:
/// <summary>
/// The default material used for terrain rendering (chunks can override this).
/// </summary>
API_FIELD(Attributes="EditorOrder(100), DefaultValue(null), EditorDisplay(\"Terrain\")")
AssetReference<MaterialBase> Material;
/// <summary>
/// The physical material used to define the terrain collider physical properties.
/// </summary>
API_FIELD(Attributes="EditorOrder(520), DefaultValue(null), Limit(-1, 100, 0.1f), EditorDisplay(\"Collision\")")
JsonAssetReference<::PhysicalMaterial> PhysicalMaterial;
/// <summary>
/// The draw passes to use for rendering this object.
/// </summary>
@@ -89,7 +81,6 @@ public:
DrawPass DrawModes = DrawPass::Default;
public:
/// <summary>
/// Gets the terrain Level Of Detail bias value. Allows to increase or decrease rendered terrain quality.
/// </summary>
@@ -180,6 +171,21 @@ public:
/// </summary>
API_PROPERTY() void SetCollisionLOD(int32 value);
/// <summary>
/// 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).
/// </summary>
API_PROPERTY(Attributes="EditorOrder(520), EditorDisplay(\"Collision\"), Collection(MinCount = 8, MaxCount = 8)")
FORCE_INLINE const Array<JsonAssetReference<PhysicalMaterial>, FixedAllocation<8>>& GetPhysicalMaterials() const
{
return _physicalMaterials;
}
/// <summary>
/// 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).
/// </summary>
API_PROPERTY()
void SetPhysicalMaterials(const Array<JsonAssetReference<PhysicalMaterial>, FixedAllocation<8>>& value);
/// <summary>
/// Gets the terrain Level Of Detail count.
/// </summary>
@@ -311,7 +317,6 @@ public:
#endif
public:
#if TERRAIN_EDITING
/// <summary>
@@ -362,7 +367,6 @@ public:
void RemoveLightmap();
public:
/// <summary>
/// Performs a raycast against this terrain collision shape.
/// </summary>
@@ -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;

View File

@@ -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<PhysicalMaterial>& 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<JsonAssetReference<PhysicalMaterial>>(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<byte>* 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<byte>* collisionData)
{
#if COMPILE_WITH_PHYSICS_COOKING
info.GetSplatMaps();
PROFILE_CPU_NAMED("Terrain.CookCollision");
// Prepare data
const int32 collisionLOD = Math::Clamp<int32>(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<Color32>()[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<Color32>()[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<int32>(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<Color32>()[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<int32>(_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<int32>(_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<TextureBase::InitData>();
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<int32>(_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<int32>(_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<TextureBase::InitData>();
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<float> 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<Color32*>(_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<Vector3>& 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<Vector3>& 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<Vector3>& 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<Vector3>& TerrainPatch::GetCollisionTriangles()
void TerrainPatch::GetCollisionTriangles(const BoundingSphere& bounds, Array<Vector3>& result)
{
PROFILE_CPU();
result.Clear();
// Skip if no intersection with patch
@@ -2430,6 +2428,7 @@ void TerrainPatch::GetCollisionTriangles(const BoundingSphere& bounds, Array<Vec
void TerrainPatch::ExtractCollisionGeometry(Array<Float3>& vertexBuffer, Array<int32>& indexBuffer)
{
PROFILE_CPU();
vertexBuffer.Clear();
indexBuffer.Clear();
@@ -2459,7 +2458,7 @@ void TerrainPatch::ExtractCollisionGeometry(Array<Float3>& vertexBuffer, Array<i
{
for (int32 col = 0; col < cols; col++)
{
Float3 v((float)row, PhysicsBackend::GetHeightFieldHeight(_physicsHeightField, (float)row, (float)col) / TERRAIN_PATCH_COLLISION_QUANTIZATION, (float)col);
Float3 v((float)row, PhysicsBackend::GetHeightFieldHeight(_physicsHeightField, row, col) / TERRAIN_PATCH_COLLISION_QUANTIZATION, (float)col);
Float3::Transform(v, world, v);
*vb++ = v;
}

View File

@@ -22,7 +22,6 @@ class FLAXENGINE_API TerrainPatch : public ISerializable
friend TerrainChunk;
public:
enum
{
CHUNKS_COUNT = 16,
@@ -30,7 +29,6 @@ public:
};
private:
Terrain* _terrain;
int16 _x, _z;
float _yOffset, _yHeight;
@@ -52,7 +50,7 @@ private:
TextureBase::InitData* _dataSplatmap[TERRAIN_MAX_SPLATMAPS_COUNT] = {};
#endif
#if TERRAIN_USE_PHYSICS_DEBUG
Array<Vector3> _debugLines; // TODO: large-worlds
Array<Vector3> _debugLines; // TODO: large-worlds
#endif
#if USE_EDITOR
Array<Vector3> _collisionTriangles; // TODO: large-worlds
@@ -62,14 +60,12 @@ private:
void Init(Terrain* terrain, int16 x, int16 z);
public:
/// <summary>
/// Finalizes an instance of the <see cref="TerrainPatch"/> class.
/// </summary>
~TerrainPatch();
public:
/// <summary>
/// The chunks contained within the patch. Organized in 4x4 square.
/// </summary>
@@ -86,11 +82,9 @@ public:
AssetReference<Texture> Splatmap[TERRAIN_MAX_SPLATMAPS_COUNT];
public:
/// <summary>
/// Gets the Y axis heightmap offset from terrain origin.
/// </summary>
/// <returns>The offset.</returns>
FORCE_INLINE float GetOffsetY() const
{
return _yOffset;
@@ -99,7 +93,6 @@ public:
/// <summary>
/// Gets the Y axis heightmap height.
/// </summary>
/// <returns>The height.</returns>
FORCE_INLINE float GetHeightY() const
{
return _yHeight;
@@ -108,7 +101,6 @@ public:
/// <summary>
/// Gets the x coordinate.
/// </summary>
/// <returns>The x position.</returns>
FORCE_INLINE int32 GetX() const
{
return _x;
@@ -117,7 +109,6 @@ public:
/// <summary>
/// Gets the z coordinate.
/// </summary>
/// <returns>The z position.</returns>
FORCE_INLINE int32 GetZ() const
{
return _z;
@@ -126,7 +117,6 @@ public:
/// <summary>
/// Gets the terrain.
/// </summary>
/// <returns>The terrain,</returns>
FORCE_INLINE Terrain* GetTerrain() const
{
return _terrain;
@@ -168,14 +158,12 @@ public:
/// <summary>
/// Gets the patch world bounds.
/// </summary>
/// <returns>The bounding box.</returns>
FORCE_INLINE const BoundingBox& GetBounds() const
{
return _bounds;
}
public:
/// <summary>
/// Removes the lightmap data from the terrain patch.
/// </summary>
@@ -192,7 +180,6 @@ public:
void UpdateTransform();
#if TERRAIN_EDITING
/// <summary>
/// Initializes the patch heightmap and collision to the default flat level.
/// </summary>
@@ -218,11 +205,9 @@ public:
/// <param name="forceUseVirtualStorage">If set to <c>true</c> 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).</param>
/// <returns>True if failed, otherwise false.</returns>
bool SetupSplatMap(int32 index, int32 splatMapLength, const Color32* splatMap, bool forceUseVirtualStorage = false);
#endif
#if TERRAIN_UPDATING
/// <summary>
/// Gets the raw pointer to the heightmap data.
/// </summary>
@@ -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:
/// <summary>
/// Performs a raycast against this terrain collision shape.
/// </summary>
@@ -353,18 +335,14 @@ public:
void ClosestPoint(const Vector3& position, Vector3& result) const;
#if USE_EDITOR
/// <summary>
/// Updates the patch data after manual deserialization called at runtime (eg. by editor undo).
/// </summary>
void UpdatePostManualDeserialization();
#endif
public:
#if USE_EDITOR
/// <summary>
/// Gets the collision mesh triangles array (3 vertices per triangle in linear list). Cached internally to reuse data.
/// </summary>
@@ -377,7 +355,6 @@ public:
/// <param name="bounds">The world-space bounds to find terrain triangles that intersect with it.</param>
/// <param name="result">The result triangles that intersect with the given bounds (in world-space).</param>
void GetCollisionTriangles(const BoundingSphere& bounds, Array<Vector3>& result);
#endif
/// <summary>
@@ -388,7 +365,6 @@ public:
void ExtractCollisionGeometry(Array<Float3>& vertexBuffer, Array<int32>& indexBuffer);
private:
/// <summary>
/// Determines whether this patch has created collision representation.
/// </summary>
@@ -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
/// <summary>
@@ -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;