**Refactor meshes format to support custom vertex layouts and new flexible api to access mesh data**

#3044 #2667
This commit is contained in:
Wojtek Figat
2025-01-06 22:47:19 +01:00
parent 29bfef677f
commit db4d7d2a05
65 changed files with 4428 additions and 3106 deletions

View File

@@ -13,8 +13,10 @@
#if USE_EDITOR
#include "Engine/Serialization/MemoryWriteStream.h"
#include "Engine/Serialization/JsonWriters.h"
#include "Engine/Graphics/Models/ModelData.h"
#include "Engine/Content/JsonAsset.h"
#include "Engine/Level/Level.h"
#include "Engine/Debug/Exceptions/ArgumentOutOfRangeException.h"
#endif
REGISTER_BINARY_ASSET(Animation, "FlaxEngine.Animation", false);
@@ -490,6 +492,64 @@ bool Animation::Save(const StringView& path)
return false;
}
bool Animation::SaveHeader(const ModelData& modelData, WriteStream& stream, int32 animIndex)
{
// Validate input
if (animIndex < 0 || animIndex >= modelData.Animations.Count())
{
Log::ArgumentOutOfRangeException(TEXT("animIndex"));
return true;
}
auto& anim = modelData.Animations.Get()[animIndex];
if (anim.Duration <= ZeroTolerance || anim.FramesPerSecond <= ZeroTolerance)
{
Log::ArgumentOutOfRangeException(TEXT("Invalid animation duration."));
return true;
}
if (anim.Channels.IsEmpty())
{
Log::ArgumentOutOfRangeException(TEXT("Channels"), TEXT("Animation channels collection cannot be empty."));
return true;
}
// Info
stream.Write(103); // Header version (for fast version upgrades without serialization format change)
stream.Write(anim.Duration);
stream.Write(anim.FramesPerSecond);
stream.Write((byte)anim.RootMotionFlags);
stream.Write(anim.RootNodeName, 13);
// Animation channels
stream.WriteInt32(anim.Channels.Count());
for (const auto& channel : anim.Channels)
{
stream.Write(channel.NodeName, 172);
Serialization::Serialize(stream, channel.Position);
Serialization::Serialize(stream, channel.Rotation);
Serialization::Serialize(stream, channel.Scale);
}
// Animation events
stream.WriteInt32(anim.Events.Count());
for (auto& e : anim.Events)
{
stream.Write(e.First, 172);
stream.Write(e.Second.GetKeyframes().Count());
for (const auto& k : e.Second.GetKeyframes())
{
stream.Write(k.Time);
stream.Write(k.Value.Duration);
stream.Write(k.Value.TypeName, 17);
stream.WriteJson(k.Value.JsonData);
}
}
// Nested animations
stream.WriteInt32(0); // Empty list
return false;
}
void Animation::GetReferences(Array<Guid>& assets, Array<String>& files) const
{
BinaryAsset::GetReferences(assets, files);

View File

@@ -151,6 +151,12 @@ public:
bool Save(const StringView& path = StringView::Empty);
#endif
private:
#if USE_EDITOR
friend class ImportModel;
static bool SaveHeader(const class ModelData& modelData, WriteStream& stream, int32 animIndex);
#endif
public:
// [BinaryAsset]
#if USE_EDITOR

View File

@@ -7,7 +7,7 @@
#include "Engine/Content/WeakAssetReference.h"
#include "Engine/Content/Upgraders/ModelAssetUpgrader.h"
#include "Engine/Content/Factories/BinaryAssetFactory.h"
#include "Engine/Debug/DebugDraw.h"
#include "Engine/Core/Math/Transform.h"
#include "Engine/Graphics/RenderTools.h"
#include "Engine/Graphics/RenderTask.h"
#include "Engine/Graphics/Models/ModelInstanceEntry.h"
@@ -18,13 +18,13 @@
#include "Engine/Graphics/Async/Tasks/GPUUploadTextureMipTask.h"
#include "Engine/Graphics/Models/MeshDeformation.h"
#include "Engine/Graphics/Textures/GPUTexture.h"
#include "Engine/Graphics/Textures/TextureData.h"
#include "Engine/Profiler/ProfilerCPU.h"
#include "Engine/Renderer/DrawCall.h"
#include "Engine/Threading/Threading.h"
#include "Engine/Tools/ModelTool/ModelTool.h"
#if USE_EDITOR
#include "Engine/Serialization/MemoryWriteStream.h"
#include "Engine/Graphics/Textures/TextureData.h"
#endif
REGISTER_BINARY_ASSET_ABSTRACT(ModelBase, "FlaxEngine.ModelBase");
@@ -246,330 +246,6 @@ bool Model::SetupLODs(const Span<int32>& meshesCountPerLod)
return Init(meshesCountPerLod);
}
#if USE_EDITOR
bool Model::Save(bool withMeshDataFromGpu, const StringView& path)
{
// Validate state
if (WaitForLoaded())
{
LOG(Error, "Asset loading failed. Cannot save it.");
return true;
}
if (IsVirtual() && path.IsEmpty())
{
LOG(Error, "To save virtual asset asset you need to specify the target asset path location.");
return true;
}
if (withMeshDataFromGpu && IsInMainThread())
{
LOG(Error, "To save model with GPU mesh buffers it needs to be called from the other thread (not the main thread).");
return true;
}
if (IsVirtual() && !withMeshDataFromGpu)
{
LOG(Error, "To save virtual model asset you need to specify 'withMeshDataFromGpu' (it has no other storage container to get data).");
return true;
}
ScopeLock lock(Locker);
// Create model data header
MemoryWriteStream headerStream(1024);
MemoryWriteStream* stream = &headerStream;
{
// Min Screen Size
stream->WriteFloat(MinScreenSize);
// Amount of material slots
stream->WriteInt32(MaterialSlots.Count());
// For each material slot
for (int32 materialSlotIndex = 0; materialSlotIndex < MaterialSlots.Count(); materialSlotIndex++)
{
auto& slot = MaterialSlots[materialSlotIndex];
const auto id = slot.Material.GetID();
stream->Write(id);
stream->WriteByte(static_cast<byte>(slot.ShadowsMode));
stream->WriteString(slot.Name, 11);
}
// Amount of LODs
const int32 lods = LODs.Count();
stream->WriteByte(lods);
// For each LOD
for (int32 lodIndex = 0; lodIndex < lods; lodIndex++)
{
auto& lod = LODs[lodIndex];
// Screen Size
stream->WriteFloat(lod.ScreenSize);
// Amount of meshes
const int32 meshes = lod.Meshes.Count();
stream->WriteUint16(meshes);
// For each mesh
for (int32 meshIndex = 0; meshIndex < meshes; meshIndex++)
{
const auto& mesh = lod.Meshes[meshIndex];
// Material Slot index
stream->WriteInt32(mesh.GetMaterialSlotIndex());
// Box
const auto box = mesh.GetBox();
stream->WriteBoundingBox(box);
// Sphere
const auto sphere = mesh.GetSphere();
stream->WriteBoundingSphere(sphere);
// Has Lightmap UVs
stream->WriteBool(mesh.HasLightmapUVs());
}
}
}
// Use a temporary chunks for data storage for virtual assets
FlaxChunk* tmpChunks[ASSET_FILE_DATA_CHUNKS];
Platform::MemoryClear(tmpChunks, sizeof(tmpChunks));
Array<FlaxChunk> chunks;
if (IsVirtual())
chunks.Resize(ASSET_FILE_DATA_CHUNKS);
#define GET_CHUNK(index) (IsVirtual() ? tmpChunks[index] = &chunks[index] : GetOrCreateChunk(index))
// Check if use data from drive or from GPU
if (withMeshDataFromGpu)
{
// Download all meshes buffers
Array<Task*> tasks;
for (int32 lodIndex = 0; lodIndex < LODs.Count(); lodIndex++)
{
auto& lod = LODs[lodIndex];
const int32 meshesCount = lod.Meshes.Count();
struct MeshData
{
BytesContainer VB0;
BytesContainer VB1;
BytesContainer VB2;
BytesContainer IB;
uint32 DataSize() const
{
return VB0.Length() + VB1.Length() + VB2.Length() + IB.Length();
}
};
Array<MeshData> meshesData;
meshesData.Resize(meshesCount);
tasks.EnsureCapacity(meshesCount * 4);
for (int32 meshIndex = 0; meshIndex < meshesCount; meshIndex++)
{
const auto& mesh = lod.Meshes[meshIndex];
auto& meshData = meshesData[meshIndex];
// Vertex Buffer 0 (required)
auto task = mesh.DownloadDataGPUAsync(MeshBufferType::Vertex0, meshData.VB0);
if (task == nullptr)
return true;
task->Start();
tasks.Add(task);
// Vertex Buffer 1 (required)
task = mesh.DownloadDataGPUAsync(MeshBufferType::Vertex1, meshData.VB1);
if (task == nullptr)
return true;
task->Start();
tasks.Add(task);
// Vertex Buffer 2 (optional)
task = mesh.DownloadDataGPUAsync(MeshBufferType::Vertex2, meshData.VB2);
if (task)
{
task->Start();
tasks.Add(task);
}
// Index Buffer (required)
task = mesh.DownloadDataGPUAsync(MeshBufferType::Index, meshData.IB);
if (task == nullptr)
return true;
task->Start();
tasks.Add(task);
}
// Wait for all
if (Task::WaitAll(tasks))
return true;
tasks.Clear();
// Create meshes data
{
int32 dataSize = meshesCount * (2 * sizeof(uint32) + sizeof(bool));
for (int32 meshIndex = 0; meshIndex < meshesCount; meshIndex++)
{
dataSize += meshesData[meshIndex].DataSize();
}
MemoryWriteStream meshesStream(dataSize);
for (int32 meshIndex = 0; meshIndex < meshesCount; meshIndex++)
{
const auto& mesh = lod.Meshes[meshIndex];
const auto& meshData = meshesData[meshIndex];
uint32 vertices = mesh.GetVertexCount();
uint32 triangles = mesh.GetTriangleCount();
bool hasColors = meshData.VB2.IsValid();
uint32 vb0Size = vertices * sizeof(VB0ElementType);
uint32 vb1Size = vertices * sizeof(VB1ElementType);
uint32 vb2Size = vertices * sizeof(VB2ElementType);
uint32 indicesCount = triangles * 3;
bool shouldUse16BitIndexBuffer = indicesCount <= MAX_uint16;
bool use16BitIndexBuffer = mesh.Use16BitIndexBuffer();
uint32 ibSize = indicesCount * (use16BitIndexBuffer ? sizeof(uint16) : sizeof(uint32));
if (vertices == 0 || triangles == 0)
{
LOG(Warning, "Cannot save model with empty meshes.");
return true;
}
if ((uint32)meshData.VB0.Length() < vb0Size)
{
LOG(Warning, "Invalid vertex buffer 0 size.");
return true;
}
if ((uint32)meshData.VB1.Length() < vb1Size)
{
LOG(Warning, "Invalid vertex buffer 1 size.");
return true;
}
if (hasColors && (uint32)meshData.VB2.Length() < vb2Size)
{
LOG(Warning, "Invalid vertex buffer 2 size.");
return true;
}
if ((uint32)meshData.IB.Length() < ibSize)
{
LOG(Warning, "Invalid index buffer size.");
return true;
}
// #MODEL_DATA_FORMAT_USAGE
meshesStream.WriteUint32(vertices);
meshesStream.WriteUint32(triangles);
meshesStream.WriteBytes(meshData.VB0.Get(), vb0Size);
meshesStream.WriteBytes(meshData.VB1.Get(), vb1Size);
meshesStream.WriteBool(hasColors);
if (hasColors)
{
meshesStream.WriteBytes(meshData.VB2.Get(), vb2Size);
}
if (shouldUse16BitIndexBuffer == use16BitIndexBuffer)
{
meshesStream.WriteBytes(meshData.IB.Get(), ibSize);
}
else if (shouldUse16BitIndexBuffer)
{
auto ib = (const int32*)meshData.IB.Get();
for (uint32 i = 0; i < indicesCount; i++)
{
meshesStream.WriteUint16(ib[i]);
}
}
else
{
CRASH;
}
}
// Override LOD data chunk with the fetched GPU meshes memory
auto lodChunk = GET_CHUNK(MODEL_LOD_TO_CHUNK_INDEX(lodIndex));
if (lodChunk == nullptr)
return true;
lodChunk->Data.Copy(meshesStream.GetHandle(), meshesStream.GetPosition());
}
}
// Download SDF data
if (SDF.Texture)
{
auto sdfChunk = GET_CHUNK(15);
if (sdfChunk == nullptr)
return true;
MemoryWriteStream sdfStream;
sdfStream.WriteInt32(1); // Version
ModelSDFHeader data(SDF, SDF.Texture->GetDescription());
sdfStream.WriteBytes(&data, sizeof(data));
TextureData sdfTextureData;
if (SDF.Texture->DownloadData(sdfTextureData))
return true;
for (int32 mipLevel = 0; mipLevel < sdfTextureData.Items[0].Mips.Count(); mipLevel++)
{
auto& mip = sdfTextureData.Items[0].Mips[mipLevel];
ModelSDFMip mipData(mipLevel, mip);
sdfStream.WriteBytes(&mipData, sizeof(mipData));
sdfStream.WriteBytes(mip.Data.Get(), mip.Data.Length());
}
sdfChunk->Data.Copy(sdfStream.GetHandle(), sdfStream.GetPosition());
}
}
else
{
// Load all chunks with a mesh data
for (int32 lodIndex = 0; lodIndex < LODs.Count(); lodIndex++)
{
if (LoadChunk(MODEL_LOD_TO_CHUNK_INDEX(lodIndex)))
return true;
}
if (SDF.Texture)
{
// SDF data from file (only if has no cached texture data)
if (LoadChunk(15))
return true;
}
else
{
// No SDF texture
ReleaseChunk(15);
}
}
// Set mesh header data
auto headerChunk = GET_CHUNK(0);
ASSERT(headerChunk != nullptr);
headerChunk->Data.Copy(headerStream.GetHandle(), headerStream.GetPosition());
#undef GET_CHUNK
// Save
AssetInitData data;
data.SerializedVersion = SerializedVersion;
if (IsVirtual())
Platform::MemoryCopy(_header.Chunks, tmpChunks, sizeof(_header.Chunks));
const bool saveResult = path.HasChars() ? SaveAsset(path, data) : SaveAsset(data, true);
if (IsVirtual())
Platform::MemoryClear(_header.Chunks, sizeof(_header.Chunks));
if (saveResult)
{
LOG(Error, "Cannot save \'{0}\'", ToString());
return true;
}
return false;
}
#endif
bool Model::GenerateSDF(float resolutionScale, int32 lodIndex, bool cacheData, float backfacesThreshold, bool useGPU)
{
if (EnableModelSDF == 2)
@@ -666,6 +342,172 @@ bool Model::Init(const Span<int32>& meshesCountPerLod)
return false;
}
bool Model::LoadHeader(ReadStream& stream, byte& headerVersion)
{
if (ModelBase::LoadHeader(stream, headerVersion))
return true;
// LODs
byte lods = stream.ReadByte();
if (lods == 0 || lods > MODEL_MAX_LODS)
return true;
LODs.Resize(lods);
_initialized = true;
for (int32 lodIndex = 0; lodIndex < lods; lodIndex++)
{
auto& lod = LODs[lodIndex];
lod._model = this;
lod._lodIndex = lodIndex;
stream.ReadFloat(&lod.ScreenSize);
// Meshes
uint16 meshesCount;
stream.ReadUint16(&meshesCount);
if (meshesCount == 0 || meshesCount > MODEL_MAX_MESHES)
return true;
ASSERT(lodIndex == 0 || LODs[0].Meshes.Count() >= meshesCount);
lod.Meshes.Resize(meshesCount, false);
for (uint16 meshIndex = 0; meshIndex < meshesCount; meshIndex++)
{
Mesh& mesh = lod.Meshes[meshIndex];
mesh.Link(this, lodIndex, meshIndex);
// Material Slot index
int32 materialSlotIndex;
stream.ReadInt32(&materialSlotIndex);
if (materialSlotIndex < 0 || materialSlotIndex >= MaterialSlots.Count())
{
LOG(Warning, "Invalid material slot index {0} for mesh {1}. Slots count: {2}.", materialSlotIndex, meshIndex, MaterialSlots.Count());
return true;
}
mesh.SetMaterialSlotIndex(materialSlotIndex);
// Bounds
BoundingBox box;
stream.Read(box);
BoundingSphere sphere;
stream.Read(sphere);
mesh.SetBounds(box, sphere);
// Lightmap UVs channel
int8 lightmapUVs;
stream.ReadInt8(&lightmapUVs);
mesh.LightmapUVsIndex = (int32)lightmapUVs;
}
}
return false;
}
#if USE_EDITOR
bool Model::SaveHeader(WriteStream& stream)
{
if (ModelBase::SaveHeader(stream))
return true;
static_assert(HeaderVersion == 2, "Update code");
// LODs
stream.Write((byte)LODs.Count());
for (int32 lodIndex = 0; lodIndex < LODs.Count(); lodIndex++)
{
auto& lod = LODs[lodIndex];
stream.Write(lod.ScreenSize);
// Meshes
stream.Write((uint16)lod.Meshes.Count());
for (const auto& mesh : lod.Meshes)
{
stream.Write(mesh.GetMaterialSlotIndex());
stream.Write(mesh.GetBox());
stream.Write(mesh.GetSphere());
stream.Write((int8)mesh.LightmapUVsIndex);
}
}
return false;
}
bool Model::SaveHeader(WriteStream& stream, const ModelData& modelData)
{
if (ModelBase::SaveHeader(stream, modelData))
return true;
static_assert(HeaderVersion == 2, "Update code");
// LODs
stream.Write((byte)modelData.LODs.Count());
for (int32 lodIndex = 0; lodIndex < modelData.LODs.Count(); lodIndex++)
{
auto& lod = modelData.LODs[lodIndex];
stream.Write(lod.ScreenSize);
// Meshes
stream.Write((uint16)lod.Meshes.Count());
for (const auto& mesh : lod.Meshes)
{
BoundingBox box;
BoundingSphere sphere;
mesh->CalculateBounds(box, sphere);
stream.Write(mesh->MaterialSlotIndex);
stream.Write(box);
stream.Write(sphere);
stream.Write((int8)mesh->LightmapUVsIndex);
}
}
return false;
}
bool Model::Save(bool withMeshDataFromGpu, Function<FlaxChunk*(int32)>& getChunk)
{
if (ModelBase::Save(withMeshDataFromGpu, getChunk))
return true;
if (withMeshDataFromGpu)
{
// Download SDF data
if (SDF.Texture)
{
auto sdfChunk = getChunk(15);
if (sdfChunk == nullptr)
return true;
MemoryWriteStream sdfStream;
sdfStream.WriteInt32(1); // Version
ModelSDFHeader data(SDF, SDF.Texture->GetDescription());
sdfStream.WriteBytes(&data, sizeof(data));
TextureData sdfTextureData;
if (SDF.Texture->DownloadData(sdfTextureData))
return true;
for (int32 mipLevel = 0; mipLevel < sdfTextureData.Items[0].Mips.Count(); mipLevel++)
{
auto& mip = sdfTextureData.Items[0].Mips[mipLevel];
ModelSDFMip mipData(mipLevel, mip);
sdfStream.WriteBytes(&mipData, sizeof(mipData));
sdfStream.WriteBytes(mip.Data.Get(), mip.Data.Length());
}
sdfChunk->Data.Copy(sdfStream.GetHandle(), sdfStream.GetPosition());
}
}
else
{
if (SDF.Texture)
{
// SDF data from file (only if has no cached texture data)
if (LoadChunk(15))
return true;
}
else
{
// No SDF texture
ReleaseChunk(15);
}
}
return false;
}
#endif
void Model::SetupMaterialSlots(int32 slotsCount)
{
ModelBase::SetupMaterialSlots(slotsCount);
@@ -687,6 +529,26 @@ int32 Model::GetLODsCount() const
return LODs.Count();
}
const MeshBase* Model::GetMesh(int32 meshIndex, int32 lodIndex) const
{
auto& lod = LODs[lodIndex];
return &lod.Meshes[meshIndex];
}
MeshBase* Model::GetMesh(int32 meshIndex, int32 lodIndex)
{
auto& lod = LODs[lodIndex];
return &lod.Meshes[meshIndex];
}
void Model::GetMeshes(Array<const MeshBase*>& meshes, int32 lodIndex) const
{
auto& lod = LODs[lodIndex];
meshes.Resize(lod.Meshes.Count());
for (int32 meshIndex = 0; meshIndex < lod.Meshes.Count(); meshIndex++)
meshes[meshIndex] = &lod.Meshes[meshIndex];
}
void Model::GetMeshes(Array<MeshBase*>& meshes, int32 lodIndex)
{
auto& lod = LODs[lodIndex];
@@ -722,91 +584,11 @@ Asset::LoadResult Model::load()
if (chunk0 == nullptr || chunk0->IsMissing())
return LoadResult::MissingDataChunk;
MemoryReadStream headerStream(chunk0->Get(), chunk0->Size());
ReadStream* stream = &headerStream;
// Min Screen Size
stream->ReadFloat(&MinScreenSize);
// Amount of material slots
int32 materialSlotsCount;
stream->ReadInt32(&materialSlotsCount);
if (materialSlotsCount <= 0 || materialSlotsCount > 4096)
// Load asset data (anything but mesh contents that use streaming)
byte headerVersion;
if (LoadHeader(headerStream, headerVersion))
return LoadResult::InvalidData;
MaterialSlots.Resize(materialSlotsCount, false);
// For each material slot
for (int32 materialSlotIndex = 0; materialSlotIndex < materialSlotsCount; materialSlotIndex++)
{
auto& slot = MaterialSlots[materialSlotIndex];
// Material
Guid materialId;
stream->Read(materialId);
slot.Material = materialId;
// Shadows Mode
slot.ShadowsMode = static_cast<ShadowsCastingMode>(stream->ReadByte());
// Name
stream->ReadString(&slot.Name, 11);
}
// Amount of LODs
byte lods;
stream->ReadByte(&lods);
if (lods == 0 || lods > MODEL_MAX_LODS)
return LoadResult::InvalidData;
LODs.Resize(lods);
_initialized = true;
// For each LOD
for (int32 lodIndex = 0; lodIndex < lods; lodIndex++)
{
auto& lod = LODs[lodIndex];
lod._model = this;
lod._lodIndex = lodIndex;
// Screen Size
stream->ReadFloat(&lod.ScreenSize);
// Amount of meshes
uint16 meshesCount;
stream->ReadUint16(&meshesCount);
if (meshesCount == 0 || meshesCount > MODEL_MAX_MESHES)
return LoadResult::InvalidData;
ASSERT(lodIndex == 0 || LODs[0].Meshes.Count() >= meshesCount);
// Allocate memory
lod.Meshes.Resize(meshesCount, false);
// For each mesh
for (uint16 meshIndex = 0; meshIndex < meshesCount; meshIndex++)
{
Mesh& mesh = lod.Meshes[meshIndex];
mesh.Link(this, lodIndex, meshIndex);
// Material Slot index
int32 materialSlotIndex;
stream->ReadInt32(&materialSlotIndex);
if (materialSlotIndex < 0 || materialSlotIndex >= materialSlotsCount)
{
LOG(Warning, "Invalid material slot index {0} for mesh {1}. Slots count: {2}.", materialSlotIndex, meshIndex, materialSlotsCount);
return LoadResult::InvalidData;
}
mesh.SetMaterialSlotIndex(materialSlotIndex);
// Bounds
BoundingBox box;
stream->ReadBoundingBox(&box);
BoundingSphere sphere;
stream->ReadBoundingSphere(&sphere);
mesh.SetBounds(box, sphere);
// Has Lightmap UVs
bool hasLightmapUVs = stream->ReadBool();
mesh.LightmapUVsIndex = hasLightmapUVs ? 1 : -1;
}
}
// Load SDF
auto chunk15 = GetChunk(15);
@@ -815,7 +597,7 @@ Asset::LoadResult Model::load()
PROFILE_CPU_NAMED("SDF");
MemoryReadStream sdfStream(chunk15->Get(), chunk15->Size());
int32 version;
sdfStream.ReadInt32(&version);
sdfStream.Read(version);
switch (version)
{
case 1:

View File

@@ -159,7 +159,7 @@ public:
/// </summary>
API_CLASS(NoSpawn) class FLAXENGINE_API Model : public ModelBase
{
DECLARE_BINARY_ASSET_HEADER(Model, 25);
DECLARE_BINARY_ASSET_HEADER(Model, 30);
friend Mesh;
public:
@@ -274,19 +274,6 @@ public:
/// <returns>True if failed, otherwise false.</returns>
API_FUNCTION() bool SetupLODs(const Span<int32>& meshesCountPerLod);
#if USE_EDITOR
/// <summary>
/// Saves this asset to the file. Supported only in Editor.
/// </summary>
/// <remarks>If you use saving with the GPU mesh data then the call has to be provided from the thread other than the main game thread.</remarks>
/// <param name="withMeshDataFromGpu">True if save also GPU mesh buffers, otherwise will keep data in storage unmodified. Valid only if saving the same asset to the same location and it's loaded.</param>
/// <param name="path">The custom asset path to use for the saving. Use empty value to save this asset to its own storage location. Can be used to duplicate asset. Must be specified when saving virtual asset.</param>
/// <returns>True if cannot save data, otherwise false.</returns>
API_FUNCTION() bool Save(bool withMeshDataFromGpu = false, const StringView& path = StringView::Empty);
#endif
/// <summary>
/// Generates the Sign Distant Field for this model.
/// </summary>
@@ -312,10 +299,22 @@ private:
/// <returns>True if failed, otherwise false.</returns>
bool Init(const Span<int32>& meshesCountPerLod);
// [ModelBase]
bool LoadHeader(ReadStream& stream, byte& headerVersion);
#if USE_EDITOR
friend class ImportModel;
bool SaveHeader(WriteStream& stream) override;
static bool SaveHeader(WriteStream& stream, const ModelData& modelData);
bool Save(bool withMeshDataFromGpu, Function<FlaxChunk*(int32)>& getChunk) override;
#endif
public:
// [ModelBase]
void SetupMaterialSlots(int32 slotsCount) override;
int32 GetLODsCount() const override;
const MeshBase* GetMesh(int32 meshIndex, int32 lodIndex = 0) const override;
MeshBase* GetMesh(int32 meshIndex, int32 lodIndex = 0) override;
void GetMeshes(Array<const MeshBase*>& meshes, int32 lodIndex = 0) const override;
void GetMeshes(Array<MeshBase*>& meshes, int32 lodIndex = 0) override;
void InitAsVirtual() override;

View File

@@ -5,6 +5,9 @@
#include "Engine/Content/WeakAssetReference.h"
#include "Engine/Serialization/MemoryReadStream.h"
#include "Engine/Graphics/Config.h"
#include "Engine/Graphics/GPUBuffer.h"
#include "Engine/Graphics/Models/MeshBase.h"
#include "Engine/Graphics/Shaders/GPUVertexLayout.h"
#if GPU_ENABLE_ASYNC_RESOURCES_CREATION
#include "Engine/Threading/ThreadPoolTask.h"
#define STREAM_TASK_BASE ThreadPoolTask
@@ -12,8 +15,11 @@
#include "Engine/Threading/MainThreadTask.h"
#define STREAM_TASK_BASE MainThreadTask
#endif
#include "SkinnedModel.h" // TODO: remove this
#include "Model.h" // TODO: remove this
#if USE_EDITOR
#include "Engine/Serialization/MemoryWriteStream.h"
#include "Engine/Graphics/Models/ModelData.h"
#include "Engine/Debug/Exceptions/ArgumentOutOfRangeException.h"
#endif
/// <summary>
/// Model LOD streaming task.
@@ -54,93 +60,26 @@ public:
}
MemoryReadStream stream(data.Get(), data.Length());
// Note: this is running on thread pool task so we must be sure that updated LOD is not used at all (for rendering)
// Load model LOD (initialize vertex and index buffers)
// TODO: reformat model data storage to be the same for both assets (only custom per-mesh type data like BlendShapes)
// Load meshes data and pass data to the GPU buffers
Array<MeshBase*> meshes;
model->GetMeshes(meshes, _lodIndex);
if (model->Is<SkinnedModel>())
byte meshVersion = stream.ReadByte();
if (meshVersion < 2 || meshVersion > ModelBase::MeshVersion)
{
byte version = stream.ReadByte();
for (int32 i = 0; i < meshes.Count(); i++)
{
auto& mesh = (SkinnedMesh&)*meshes[i];
// #MODEL_DATA_FORMAT_USAGE
uint32 vertices;
stream.ReadUint32(&vertices);
uint32 triangles;
stream.ReadUint32(&triangles);
uint16 blendShapesCount;
stream.ReadUint16(&blendShapesCount);
if (blendShapesCount != mesh.BlendShapes.Count())
{
LOG(Warning, "Cannot initialize mesh {} in LOD{} for model \'{}\'. Incorrect blend shapes amount: {} (expected: {})", i, _lodIndex, model->ToString(), blendShapesCount, mesh.BlendShapes.Count());
return true;
}
for (auto& blendShape : mesh.BlendShapes)
{
blendShape.UseNormals = stream.ReadBool();
stream.ReadUint32(&blendShape.MinVertexIndex);
stream.ReadUint32(&blendShape.MaxVertexIndex);
uint32 blendShapeVertices;
stream.ReadUint32(&blendShapeVertices);
blendShape.Vertices.Resize(blendShapeVertices);
stream.ReadBytes(blendShape.Vertices.Get(), blendShape.Vertices.Count() * sizeof(BlendShapeVertex));
}
const uint32 indicesCount = triangles * 3;
const bool use16BitIndexBuffer = indicesCount <= MAX_uint16;
const uint32 ibStride = use16BitIndexBuffer ? sizeof(uint16) : sizeof(uint32);
if (vertices == 0 || triangles == 0)
return true;
const auto vb0 = stream.Move<VB0SkinnedElementType>(vertices);
const auto ib = stream.Move<byte>(indicesCount * ibStride);
// Setup GPU resources
if (mesh.Load(vertices, triangles, vb0, ib, use16BitIndexBuffer))
{
LOG(Warning, "Cannot initialize mesh {} in LOD{} for model \'{}\'. Vertices: {}, triangles: {}", i, _lodIndex, model->ToString(), vertices, triangles);
return true;
}
}
LOG(Warning, "Unsupported mesh version {}", meshVersion);
return true;
}
else
for (int32 meshIndex = 0; meshIndex < meshes.Count(); meshIndex++)
{
for (int32 i = 0; i < meshes.Count(); i++)
if (model->LoadMesh(stream, meshVersion, meshes[meshIndex]))
{
auto& mesh = (Mesh&)*meshes[i];
// #MODEL_DATA_FORMAT_USAGE
uint32 vertices;
stream.ReadUint32(&vertices);
uint32 triangles;
stream.ReadUint32(&triangles);
uint32 indicesCount = triangles * 3;
bool use16BitIndexBuffer = indicesCount <= MAX_uint16;
uint32 ibStride = use16BitIndexBuffer ? sizeof(uint16) : sizeof(uint32);
if (vertices == 0 || triangles == 0)
return true;
auto vb0 = stream.Move<VB0ElementType>(vertices);
auto vb1 = stream.Move<VB1ElementType>(vertices);
bool hasColors = stream.ReadBool();
VB2ElementType18* vb2 = nullptr;
if (hasColors)
{
vb2 = stream.Move<VB2ElementType18>(vertices);
}
auto ib = stream.Move<byte>(indicesCount * ibStride);
// Setup GPU resources
if (mesh.Load(vertices, triangles, vb0, vb1, vb2, ib, use16BitIndexBuffer))
{
LOG(Warning, "Cannot initialize mesh {} in LOD{} for model \'{}\'. Vertices: {}, triangles: {}", i, _lodIndex, model->ToString(), vertices, triangles);
return true;
}
LOG(Warning, "Cannot initialize mesh {} in LOD{} for model \'{}\'", meshIndex, _lodIndex, model->ToString());
return true;
}
}
// Update residency level
// Note: this is running on thread pool task so we must be sure that updated LOD is not used at all (for rendering)
model->_loadedLODs++;
model->ResidencyChanged();
@@ -209,6 +148,638 @@ void ModelBase::GetLODData(int32 lodIndex, BytesContainer& data) const
GetChunkData(chunkIndex, data);
}
#if USE_EDITOR
bool ModelBase::Save(bool withMeshDataFromGpu, const StringView& path)
{
// Validate state
if (WaitForLoaded())
{
LOG(Error, "Asset loading failed. Cannot save it.");
return true;
}
if (IsVirtual() && path.IsEmpty())
{
LOG(Error, "To save virtual asset asset you need to specify the target asset path location.");
return true;
}
if (withMeshDataFromGpu && IsInMainThread())
{
LOG(Error, "To save model with GPU mesh buffers it needs to be called from the other thread (not the main thread).");
return true;
}
if (IsVirtual() && !withMeshDataFromGpu)
{
LOG(Error, "To save virtual model asset you need to specify 'withMeshDataFromGpu' (it has no other storage container to get data).");
return true;
}
ScopeLock lock(Locker);
// Use a temporary chunks for data storage for virtual assets
FlaxChunk* tmpChunks[ASSET_FILE_DATA_CHUNKS];
Platform::MemoryClear(tmpChunks, sizeof(tmpChunks));
Array<FlaxChunk> chunks;
if (IsVirtual())
chunks.Resize(ASSET_FILE_DATA_CHUNKS);
Function<FlaxChunk*(int32)> getChunk = [&](int32 index) -> FlaxChunk* {
return IsVirtual() ? tmpChunks[index] = &chunks[index] : GetOrCreateChunk(index);
};
// Save LODs data
const int32 lodsCount = GetLODsCount();
if (withMeshDataFromGpu)
{
// Fetch runtime mesh data (from GPU)
MemoryWriteStream meshesStream;
for (int32 lodIndex = 0; lodIndex < lodsCount; lodIndex++)
{
meshesStream.SetPosition(0);
if (SaveLOD(meshesStream, lodIndex))
return true;
auto lodChunk = getChunk(MODEL_LOD_TO_CHUNK_INDEX(lodIndex));
if (lodChunk == nullptr)
return true;
lodChunk->Data.Copy(meshesStream.GetHandle(), meshesStream.GetPosition());
}
}
else if (!IsVirtual())
{
// Load all chunks with a mesh data
for (int32 lodIndex = 0; lodIndex < lodsCount; lodIndex++)
{
if (LoadChunk(MODEL_LOD_TO_CHUNK_INDEX(lodIndex)))
return true;
}
}
// Save custom data
if (Save(withMeshDataFromGpu, getChunk))
return true;
// Save header data
{
MemoryWriteStream headerStream(1024);
if (SaveHeader(headerStream))
return true;
auto headerChunk = getChunk(0);
ASSERT(headerChunk != nullptr);
headerChunk->Data.Copy(headerStream.GetHandle(), headerStream.GetPosition());
}
// Save file
AssetInitData data;
data.SerializedVersion = GetSerializedVersion();
if (IsVirtual())
Platform::MemoryCopy(_header.Chunks, tmpChunks, sizeof(_header.Chunks));
const bool saveResult = path.HasChars() ? SaveAsset(path, data) : SaveAsset(data, true);
if (IsVirtual())
Platform::MemoryClear(_header.Chunks, sizeof(_header.Chunks));
if (saveResult)
{
LOG(Error, "Cannot save \'{0}\'", ToString());
return true;
}
return false;
}
#endif
bool ModelBase::LoadHeader(ReadStream& stream, byte& headerVersion)
{
// Basic info
stream.Read(headerVersion);
if (headerVersion < 2 || headerVersion > HeaderVersion)
{
LOG(Warning, "Unsupported model asset header version {}", headerVersion);
return true;
}
static_assert(HeaderVersion == 2, "Update code");
stream.Read(MinScreenSize);
// Materials
int32 materialsCount;
stream.Read(materialsCount);
if (materialsCount < 0 || materialsCount > 4096)
return true;
MaterialSlots.Resize(materialsCount, false);
Guid materialId;
for (auto& slot : MaterialSlots)
{
stream.Read(materialId);
slot.Material = materialId;
slot.ShadowsMode = (ShadowsCastingMode)stream.ReadByte();
stream.Read(slot.Name, 11);
}
return false;
}
bool ModelBase::LoadMesh(MemoryReadStream& stream, byte meshVersion, MeshBase* mesh, MeshData* dataIfReadOnly)
{
// Load descriptor
static_assert(MeshVersion == 2, "Update code");
uint32 vertices, triangles;
stream.Read(vertices);
stream.Read(triangles);
const uint32 indicesCount = triangles * 3;
const bool use16BitIndexBuffer = indicesCount <= MAX_uint16;
const uint32 ibStride = use16BitIndexBuffer ? sizeof(uint16) : sizeof(uint32);
if (vertices == 0 || triangles == 0)
return true;
Array<const void*, FixedAllocation<MODEL_MAX_VB>> vbData;
Array<GPUVertexLayout*, FixedAllocation<MODEL_MAX_VB>> vbLayout;
byte vbCount;
stream.Read(vbCount);
if (vbCount > MODEL_MAX_VB)
return true;
vbData.Resize(vbCount);
vbLayout.Resize(vbCount);
for (int32 i = 0; i < vbCount; i++)
{
GPUVertexLayout::Elements elements;
stream.Read(elements);
vbLayout[i] = GPUVertexLayout::Get(elements);
}
// Move over actual mesh data
for (int32 i = 0; i < vbCount; i++)
{
auto layout = vbLayout[i];
if (!layout)
{
LOG(Warning, "Failed to get vertex layout for buffer {}", i);
return true;
}
vbData[i] = stream.Move(vertices * layout->GetStride());
}
const auto ibData = stream.Move(indicesCount * ibStride);
// Pass results if read-only
if (dataIfReadOnly)
{
dataIfReadOnly->Vertices = vertices;
dataIfReadOnly->Triangles = triangles;
dataIfReadOnly->IBStride = ibStride;
dataIfReadOnly->VBData = vbData;
dataIfReadOnly->VBLayout = vbLayout;
dataIfReadOnly->IBData = ibData;
return false;
}
// Setup GPU resources
return mesh->Init(vertices, triangles, vbData, ibData, use16BitIndexBuffer, vbLayout);
}
#if USE_EDITOR
bool ModelBase::SaveHeader(WriteStream& stream)
{
// Basic info
static_assert(HeaderVersion == 2, "Update code");
stream.Write(HeaderVersion);
stream.Write(MinScreenSize);
// Materials
stream.Write(MaterialSlots.Count());
for (const auto& slot : MaterialSlots)
{
stream.Write(slot.Material.GetID());
stream.Write((byte)slot.ShadowsMode);
stream.Write(slot.Name, 11);
}
return false;
}
bool ModelBase::SaveHeader(WriteStream& stream, const ModelData& modelData)
{
// Validate data
if (modelData.LODs.Count() > MODEL_MAX_LODS)
{
Log::ArgumentOutOfRangeException(TEXT("LODs"), TEXT("Too many LODs."));
return true;
}
for (int32 lodIndex = 0; lodIndex < modelData.LODs.Count(); lodIndex++)
{
auto& lod = modelData.LODs[lodIndex];
if (lod.Meshes.Count() > MODEL_MAX_MESHES)
{
Log::ArgumentOutOfRangeException(TEXT("LOD.Meshes"), TEXT("Too many meshes."));
return true;
}
for (int32 meshIndex = 0; meshIndex < lod.Meshes.Count(); meshIndex++)
{
auto& mesh = *lod.Meshes[meshIndex];
if (mesh.MaterialSlotIndex < 0 || mesh.MaterialSlotIndex >= modelData.Materials.Count())
{
Log::ArgumentOutOfRangeException(TEXT("MaterialSlotIndex"), TEXT("Incorrect material index used by the mesh."));
return true;
}
}
}
// Basic info
static_assert(HeaderVersion == 2, "Update code");
stream.Write(HeaderVersion);
stream.Write(modelData.MinScreenSize);
// Materials
stream.WriteInt32(modelData.Materials.Count());
for (const auto& slot : modelData.Materials)
{
stream.Write(slot.AssetID);
stream.Write((byte)slot.ShadowsMode);
stream.Write(slot.Name, 11);
}
return false;
}
bool ModelBase::SaveLOD(WriteStream& stream, int32 lodIndex) const
{
// Download all meshes buffers from the GPU
Array<Task*> tasks;
Array<const MeshBase*> meshes;
GetMeshes(meshes, lodIndex);
const int32 meshesCount = meshes.Count();
struct MeshData
{
BytesContainer VB[3];
BytesContainer IB;
};
Array<MeshData> meshesData;
meshesData.Resize(meshesCount);
tasks.EnsureCapacity(meshesCount * 4);
for (int32 meshIndex = 0; meshIndex < meshesCount; meshIndex++)
{
const auto& mesh = *meshes[meshIndex];
auto& meshData = meshesData[meshIndex];
// Vertex Buffer 0 (required)
auto task = mesh.DownloadDataGPUAsync(MeshBufferType::Vertex0, meshData.VB[0]);
if (task == nullptr)
return true;
task->Start();
tasks.Add(task);
// Vertex Buffer 1 (optional)
task = mesh.DownloadDataGPUAsync(MeshBufferType::Vertex1, meshData.VB[1]);
if (task)
{
task->Start();
tasks.Add(task);
}
// Vertex Buffer 2 (optional)
task = mesh.DownloadDataGPUAsync(MeshBufferType::Vertex2, meshData.VB[2]);
if (task)
{
task->Start();
tasks.Add(task);
}
// Index Buffer (required)
task = mesh.DownloadDataGPUAsync(MeshBufferType::Index, meshData.IB);
if (task == nullptr)
return true;
task->Start();
tasks.Add(task);
}
// Wait for async tasks
if (Task::WaitAll(tasks))
return true;
// Create meshes data
static_assert(MeshVersion == 2, "Update code");
stream.Write(MeshVersion);
for (int32 meshIndex = 0; meshIndex < meshesCount; meshIndex++)
{
const auto& mesh = *meshes[meshIndex];
const auto& meshData = meshesData[meshIndex];
uint32 vertices = mesh.GetVertexCount();
uint32 triangles = mesh.GetTriangleCount();
uint32 indicesCount = triangles * 3;
bool shouldUse16BitIndexBuffer = indicesCount <= MAX_uint16;
bool use16BitIndexBuffer = mesh.Use16BitIndexBuffer();
uint32 ibStride = use16BitIndexBuffer ? sizeof(uint16) : sizeof(uint32);
uint32 ibSize = indicesCount * ibStride;
// Validate data
if (vertices == 0 || triangles == 0)
{
LOG(Warning, "Cannot save model with empty meshes.");
return true;
}
Array<const GPUVertexLayout*, FixedAllocation<MODEL_MAX_VB>> vbLayout;
for (int32 vbIndex = 0; vbIndex < MODEL_MAX_VB; vbIndex++)
{
if (vbIndex != 0 && meshData.VB[vbIndex].IsInvalid()) // VB0 is always required
continue;
const GPUBuffer* vb = mesh.GetVertexBuffer(vbIndex);
if (!vb)
break;
const GPUVertexLayout* layout = vb->GetVertexLayout();
if (!layout)
{
LOG(Warning, "Invalid vertex buffer {}. Missing vertex layout.", vbIndex);
return true;
}
const uint32 vbSize = vb->GetSize();
vbLayout.Add(layout);
if ((uint32)meshData.VB[vbIndex].Length() != vbSize)
{
LOG(Warning, "Invalid vertex buffer {} size. Got {} bytes but expected {} bytes. Stride: {}. Layout:\n{}", vbIndex, meshData.VB[vbIndex].Length(), vbSize, vb->GetStride(), layout->GetElementsString());
return true;
}
}
if ((uint32)meshData.IB.Length() < ibSize)
{
LOG(Warning, "Invalid index buffer size. Got {} bytes bytes expected {} bytes. Stride: {}", meshData.IB.Length(), ibSize, ibStride);
return true;
}
// Write descriptor
stream.Write(vertices);
stream.Write(triangles);
byte vbCount = (byte)vbLayout.Count();
stream.Write(vbCount);
for (const GPUVertexLayout* layout : vbLayout)
stream.Write(layout->GetElements());
// Write actual mesh data
for (int32 vbIndex = 0; vbIndex < vbCount; vbIndex++)
{
const auto& vb = meshData.VB[vbIndex];
stream.WriteBytes(vb.Get(), vb.Length());
}
if (shouldUse16BitIndexBuffer == use16BitIndexBuffer)
{
stream.WriteBytes(meshData.IB.Get(), ibSize);
}
else if (shouldUse16BitIndexBuffer)
{
// Convert 32-bit indices to 16-bit
auto ib = (const int32*)meshData.IB.Get();
for (uint32 i = 0; i < indicesCount; i++)
{
stream.WriteUint16(ib[i]);
}
}
else
{
CRASH;
}
// Write custom data
if (SaveMesh(stream, &mesh))
return true;
}
return false;
}
bool ModelBase::SaveLOD(WriteStream& stream, const ModelData& modelData, int32 lodIndex, bool saveMesh(WriteStream& stream, const ModelData& modelData, int32 lodIndex, int32 meshIndex))
{
// Create meshes data
static_assert(MeshVersion == 2, "Update code");
stream.Write(MeshVersion);
const auto& lod = modelData.LODs[lodIndex];
for (int32 meshIndex = 0; meshIndex < lod.Meshes.Count(); meshIndex++)
{
const auto& mesh = *lod.Meshes[meshIndex];
uint32 vertices = (uint32)mesh.Positions.Count();
uint32 indicesCount = (uint32)mesh.Indices.Count();
uint32 triangles = indicesCount / 3;
bool use16BitIndexBuffer = indicesCount <= MAX_uint16;
const bool isSkinned = mesh.BlendIndices.HasItems() && mesh.BlendWeights.HasItems();
// Validate data
if (vertices == 0 || triangles == 0 || indicesCount % 3 != 0)
{
LOG(Warning, "Cannot save model with empty meshes.");
return true;
}
bool hasUVs = mesh.UVs.HasItems();
if (hasUVs && (uint32)mesh.UVs.Count() != vertices)
{
LOG(Error, "Invalid size of {0} stream.", TEXT("UVs"));
return true;
}
bool hasNormals = mesh.Normals.HasItems();
if (hasNormals && (uint32)mesh.Normals.Count() != vertices)
{
LOG(Error, "Invalid size of {0} stream.", TEXT("Normals"));
return true;
}
bool hasTangents = mesh.Tangents.HasItems();
if (hasTangents && (uint32)mesh.Tangents.Count() != vertices)
{
LOG(Error, "Invalid size of {0} stream.", TEXT("Tangents"));
return true;
}
bool hasBitangentSigns = mesh.BitangentSigns.HasItems();
if (hasBitangentSigns && (uint32)mesh.BitangentSigns.Count() != vertices)
{
LOG(Error, "Invalid size of {0} stream.", TEXT("BitangentSigns"));
return true;
}
bool hasLightmapUVs = mesh.LightmapUVs.HasItems();
if (hasLightmapUVs && (uint32)mesh.LightmapUVs.Count() != vertices)
{
LOG(Error, "Invalid size of {0} stream.", TEXT("LightmapUVs"));
return true;
}
bool hasColors = mesh.Colors.HasItems();
if (hasColors && (uint32)mesh.Colors.Count() != vertices)
{
LOG(Error, "Invalid size of {0} stream.", TEXT("Colors"));
return true;
}
if (isSkinned && (uint32)mesh.BlendIndices.Count() != vertices)
{
LOG(Error, "Invalid size of {0} stream.", TEXT("BlendIndices"));
return true;
}
if (isSkinned && (uint32)mesh.BlendWeights.Count() != vertices)
{
LOG(Error, "Invalid size of {0} stream.", TEXT("BlendWeights"));
return true;
}
// Define vertex buffers layout and packing
Array<GPUVertexLayout::Elements, FixedAllocation<MODEL_MAX_VB>> vbElements;
const bool useSeparatePositions = !isSkinned;
const bool useSeparateColors = !isSkinned;
{
byte vbIndex = 0;
// TODO: add option to quantize vertex positions (eg. 16-bit)
// TODO: add option to quantize vertex attributes (eg. 8-bit blend weights, 8-bit texcoords)
// TODO: add support for 16-bit blend indices (up to 65535 bones)
// Position
if (useSeparatePositions)
{
auto& vb0 = vbElements.AddOne();
vb0.Add({ VertexElement::Types::Position, vbIndex, 0, 0, PixelFormat::R32G32B32_Float });
vbIndex++;
}
// General purpose components
{
auto& vb = vbElements.AddOne();
if (!useSeparatePositions)
vb.Add({ VertexElement::Types::Position, vbIndex, 0, 0, PixelFormat::R32G32B32_Float });
if (hasUVs)
vb.Add({ VertexElement::Types::TexCoord, vbIndex, 0, 0, PixelFormat::R16G16_Float });
if (hasLightmapUVs)
vb.Add({ VertexElement::Types::TexCoord1, vbIndex, 0, 0, PixelFormat::R16G16_Float });
vb.Add({ VertexElement::Types::Normal, vbIndex, 0, 0, PixelFormat::R10G10B10A2_UNorm });
vb.Add({ VertexElement::Types::Tangent, vbIndex, 0, 0, PixelFormat::R10G10B10A2_UNorm });
if (isSkinned)
{
vb.Add({ VertexElement::Types::BlendIndices, vbIndex, 0, 0, PixelFormat::R8G8B8A8_UInt });
vb.Add({ VertexElement::Types::BlendWeights, vbIndex, 0, 0, PixelFormat::R16G16B16A16_Float });
}
if (!useSeparateColors && hasColors)
vb.Add({ VertexElement::Types::Color, vbIndex, 0, 0, PixelFormat::R8G8B8A8_UNorm });
vbIndex++;
}
// Colors
if (useSeparateColors && hasColors)
{
auto& vb = vbElements.AddOne();
vb.Add({ VertexElement::Types::Color, vbIndex, 0, 0, PixelFormat::R8G8B8A8_UNorm });
vbIndex++;
}
}
// Write descriptor
stream.Write(vertices);
stream.Write(triangles);
byte vbCount = (byte)vbElements.Count();
stream.Write(vbCount);
for (const auto& elements : vbElements)
stream.Write(elements);
// Write vertex buffers
for (int32 vbIndex = 0; vbIndex < vbCount; vbIndex++)
{
if (useSeparatePositions && vbIndex == 0)
{
// Fast path for vertex positions of static models using the first buffer
stream.WriteBytes(mesh.Positions.Get(), sizeof(Float3) * vertices);
continue;
}
// Write vertex components interleaved
const GPUVertexLayout::Elements& layout = vbElements[vbIndex];
for (uint32 vertex = 0; vertex < vertices; vertex++)
{
for (const VertexElement& element : layout)
{
switch (element.Type)
{
case VertexElement::Types::Position:
{
const Float3 position = mesh.Positions.Get()[vertex];
stream.Write(position);
break;
}
case VertexElement::Types::Color:
{
const Color32 color(mesh.Colors.Get()[vertex]);
stream.Write(color);
break;
}
case VertexElement::Types::Normal:
{
const Float3 normal = hasNormals ? mesh.Normals.Get()[vertex] : Float3::UnitZ;
const FloatR10G10B10A2 normalEnc(normal * 0.5f + 0.5f, 0);
stream.Write(normalEnc.Value);
break;
}
case VertexElement::Types::Tangent:
{
const Float3 normal = hasNormals ? mesh.Normals.Get()[vertex] : Float3::UnitZ;
const Float3 tangent = hasTangents ? mesh.Tangents.Get()[vertex] : Float3::UnitX;
const float bitangentSign = hasBitangentSigns ? mesh.BitangentSigns.Get()[vertex] : Float3::Dot(Float3::Cross(Float3::Normalize(Float3::Cross(normal, tangent)), normal), tangent);
const Float1010102 tangentEnc(tangent * 0.5f + 0.5f, (byte)(bitangentSign < 0 ? 1 : 0));
stream.Write(tangentEnc.Value);
break;
}
case VertexElement::Types::BlendIndices:
{
const Int4 blendIndices = mesh.BlendIndices.Get()[vertex];
const Color32 blendIndicesEnc(blendIndices.X, blendIndices.Y, blendIndices.Z, blendIndices.W);
stream.Write(blendIndicesEnc);
break;
}
case VertexElement::Types::BlendWeights:
{
const Float4 blendWeights = mesh.BlendWeights.Get()[vertex];
const Half4 blendWeightsEnc(blendWeights);
stream.Write(blendWeightsEnc);
break;
}
case VertexElement::Types::TexCoord0:
{
const Float2 uv = hasUVs ? mesh.UVs.Get()[vertex] : Float2::Zero;
const Half2 uvEnc(uv);
stream.Write(uvEnc);
break;
}
case VertexElement::Types::TexCoord1:
{
// TODO: refactor LightmapUVs into a generic TexCoord channel and support up to 4 UVs
const Float2 lightmapUV = hasLightmapUVs ? mesh.LightmapUVs.Get()[vertex] : Float2::Zero;
const Half2 lightmapUVEnc(lightmapUV);
stream.Write(lightmapUVEnc);
break;
}
default:
LOG(Error, "Unsupported vertex element: {}", element.ToString());
return true;
}
}
}
}
// Write index buffer
const uint32* meshIndices = mesh.Indices.Get();
if (use16BitIndexBuffer)
{
for (uint32 i = 0; i < indicesCount; i++)
stream.Write((uint16)meshIndices[i]);
}
else
{
stream.WriteBytes(meshIndices, sizeof(uint32) * indicesCount);
}
// Write custom data
if (saveMesh && saveMesh(stream, modelData, lodIndex, meshIndex))
return true;
}
return false;
}
bool ModelBase::SaveMesh(WriteStream& stream, const MeshBase* mesh) const
{
return false;
}
bool ModelBase::Save(bool withMeshDataFromGpu, Function<FlaxChunk*(int32)>& getChunk)
{
return false;
}
#endif
void ModelBase::CancelStreaming()
{
BinaryAsset::CancelStreaming();

View File

@@ -19,6 +19,7 @@
#define MODEL_LOD_TO_CHUNK_INDEX(lod) (lod + 1)
class MeshBase;
class StreamModelLODTask;
struct RenderContextBatch;
/// <summary>
@@ -28,9 +29,12 @@ API_CLASS(Abstract, NoSpawn) class FLAXENGINE_API ModelBase : public BinaryAsset
{
DECLARE_ASSET_HEADER(ModelBase);
friend MeshBase;
friend class StreamModelLODTask;
friend StreamModelLODTask;
protected:
static constexpr byte HeaderVersion = 2;
static constexpr byte MeshVersion = 2;
bool _initialized = false;
int32 _loadedLODs = 0;
StreamModelLODTask* _streamingTask = nullptr;
@@ -174,12 +178,27 @@ public:
/// <summary>
/// Gets amount of the level of details in the model.
/// </summary>
virtual int32 GetLODsCount() const = 0;
API_PROPERTY(Sealed) virtual int32 GetLODsCount() const = 0;
/// <summary>
/// Gets the mesh for a particular LOD index.
/// </summary>
virtual const MeshBase* GetMesh(int32 meshIndex, int32 lodIndex = 0) const = 0;
/// <summary>
/// Gets the mesh for a particular LOD index.
/// </summary>
API_FUNCTION(Sealed) virtual MeshBase* GetMesh(int32 meshIndex, int32 lodIndex = 0) = 0;
/// <summary>
/// Gets the meshes for a particular LOD index.
/// </summary>
virtual void GetMeshes(Array<MeshBase*>& meshes, int32 lodIndex = 0) = 0;
virtual void GetMeshes(Array<const MeshBase*>& meshes, int32 lodIndex = 0) const = 0;
/// <summary>
/// Gets the meshes for a particular LOD index.
/// </summary>
API_FUNCTION(Sealed) virtual void GetMeshes(API_PARAM(Out) Array<MeshBase*>& meshes, int32 lodIndex = 0) = 0;
public:
/// <summary>
@@ -196,6 +215,38 @@ public:
/// <param name="data">The data (it may be missing if failed to get it).</param>
void GetLODData(int32 lodIndex, BytesContainer& data) const;
#if USE_EDITOR
/// <summary>
/// Saves this asset to the file. Supported only in Editor.
/// </summary>
/// <remarks>If you use saving with the GPU mesh data then the call has to be provided from the thread other than the main game thread.</remarks>
/// <param name="withMeshDataFromGpu">True if save also GPU mesh buffers, otherwise will keep data in storage unmodified. Valid only if saving the same asset to the same location, and it's loaded.</param>
/// <param name="path">The custom asset path to use for the saving. Use empty value to save this asset to its own storage location. Can be used to duplicate asset. Must be specified when saving virtual asset.</param>
/// <returns>True when cannot save data, otherwise false.</returns>
API_FUNCTION() bool Save(bool withMeshDataFromGpu = false, const StringView& path = StringView::Empty);
#endif
protected:
friend class AssetExporters;
friend class MeshAccessor;
struct MeshData
{
uint32 Triangles, Vertices, IBStride;
Array<const void*, FixedAllocation<MODEL_MAX_VB>> VBData;
Array<class GPUVertexLayout*, FixedAllocation<MODEL_MAX_VB>> VBLayout;
const void* IBData;
};
virtual bool LoadMesh(class MemoryReadStream& stream, byte meshVersion, MeshBase* mesh, MeshData* dataIfReadOnly = nullptr);
bool LoadHeader(ReadStream& stream, byte& headerVersion);
#if USE_EDITOR
virtual bool SaveHeader(WriteStream& stream);
static bool SaveHeader(WriteStream& stream, const class ModelData& modelData);
bool SaveLOD(WriteStream& stream, int32 lodIndex) const;
static bool SaveLOD(WriteStream& stream, const ModelData& modelData, int32 lodIndex, bool(saveMesh)(WriteStream& stream, const ModelData& modelData, int32 lodIndex, int32 meshIndex) = nullptr);
virtual bool SaveMesh(WriteStream& stream, const MeshBase* mesh) const;
virtual bool Save(bool withMeshDataFromGpu, Function<FlaxChunk*(int32)>& getChunk);
#endif
public:
// [BinaryAsset]
void CancelStreaming() override;

View File

@@ -20,6 +20,7 @@
#include "Engine/Profiler/ProfilerCPU.h"
#include "Engine/Renderer/DrawCall.h"
#if USE_EDITOR
#include "Engine/Graphics/Models/ModelData.h"
#include "Engine/Serialization/MemoryWriteStream.h"
#endif
@@ -121,9 +122,9 @@ SkinnedModel::SkeletonMapping SkinnedModel::GetSkeletonMapping(Asset* source, bo
}
else
{
#if !BUILD_RELEASE
#if !BUILD_RELEASE
LOG(Error, "Missing asset {0} to use for skeleton mapping of {1}", retarget->SkeletonAsset, ToString());
#endif
#endif
return mapping;
}
}
@@ -231,7 +232,7 @@ FORCE_INLINE void SkinnedModelDraw(SkinnedModel* model, const RenderContext& ren
if (!model->CanBeRendered())
return;
if (!info.Buffer->IsValidFor(model))
info.Buffer->Setup(model);
info.Buffer->Setup(model);
const auto frame = Engine::FrameCount;
const auto modelFrame = info.DrawState->PrevFrame + 1;
@@ -395,7 +396,7 @@ bool SkinnedModel::SetupSkeleton(const Array<SkeletonNode>& nodes, const Array<S
// Validate input
if (nodes.Count() <= 0 || nodes.Count() > MAX_uint16)
return true;
if (bones.Count() <= 0 || bones.Count() > MAX_BONES_PER_MODEL)
if (bones.Count() <= 0 || bones.Count() > MODEL_MAX_BONES_PER_MODEL)
return true;
auto model = this;
if (!model->IsVirtual())
@@ -431,310 +432,6 @@ bool SkinnedModel::SetupSkeleton(const Array<SkeletonNode>& nodes, const Array<S
return false;
}
#if USE_EDITOR
bool SkinnedModel::Save(bool withMeshDataFromGpu, const StringView& path)
{
// Validate state
if (WaitForLoaded())
{
LOG(Error, "Asset loading failed. Cannot save it.");
return true;
}
if (IsVirtual() && path.IsEmpty())
{
LOG(Error, "To save virtual asset asset you need to specify the target asset path location.");
return true;
}
if (withMeshDataFromGpu && IsInMainThread())
{
LOG(Error, "To save model with GPU mesh buffers it needs to be called from the other thread (not the main thread).");
return true;
}
if (IsVirtual() && !withMeshDataFromGpu)
{
LOG(Error, "To save virtual model asset you need to specify 'withMeshDataFromGpu' (it has no other storage container to get data).");
return true;
}
ScopeLock lock(Locker);
// Create model data header
MemoryWriteStream headerStream(1024);
MemoryWriteStream* stream = &headerStream;
{
// Header Version
stream->WriteByte(1);
// Min Screen Size
stream->WriteFloat(MinScreenSize);
// Amount of material slots
stream->WriteInt32(MaterialSlots.Count());
// For each material slot
for (int32 materialSlotIndex = 0; materialSlotIndex < MaterialSlots.Count(); materialSlotIndex++)
{
auto& slot = MaterialSlots[materialSlotIndex];
const auto id = slot.Material.GetID();
stream->Write(id);
stream->WriteByte(static_cast<byte>(slot.ShadowsMode));
stream->WriteString(slot.Name, 11);
}
// Amount of LODs
const int32 lods = LODs.Count();
stream->WriteByte(lods);
// For each LOD
for (int32 lodIndex = 0; lodIndex < lods; lodIndex++)
{
auto& lod = LODs[lodIndex];
// Screen Size
stream->WriteFloat(lod.ScreenSize);
// Amount of meshes
const int32 meshes = lod.Meshes.Count();
stream->WriteUint16(meshes);
// For each mesh
for (int32 meshIndex = 0; meshIndex < meshes; meshIndex++)
{
const auto& mesh = lod.Meshes[meshIndex];
// Material Slot index
stream->WriteInt32(mesh.GetMaterialSlotIndex());
// Box
const auto box = mesh.GetBox();
stream->WriteBoundingBox(box);
// Sphere
const auto sphere = mesh.GetSphere();
stream->WriteBoundingSphere(sphere);
// Blend Shapes
stream->WriteUint16(mesh.BlendShapes.Count());
for (int32 blendShapeIndex = 0; blendShapeIndex < mesh.BlendShapes.Count(); blendShapeIndex++)
{
auto& blendShape = mesh.BlendShapes[blendShapeIndex];
stream->WriteString(blendShape.Name, 13);
stream->WriteFloat(blendShape.Weight);
}
}
}
// Skeleton
{
stream->WriteInt32(Skeleton.Nodes.Count());
// For each node
for (int32 nodeIndex = 0; nodeIndex < Skeleton.Nodes.Count(); nodeIndex++)
{
auto& node = Skeleton.Nodes[nodeIndex];
stream->Write(node.ParentIndex);
stream->WriteTransform(node.LocalTransform);
stream->WriteString(node.Name, 71);
}
stream->WriteInt32(Skeleton.Bones.Count());
// For each bone
for (int32 boneIndex = 0; boneIndex < Skeleton.Bones.Count(); boneIndex++)
{
auto& bone = Skeleton.Bones[boneIndex];
stream->Write(bone.ParentIndex);
stream->Write(bone.NodeIndex);
stream->WriteTransform(bone.LocalTransform);
stream->Write(bone.OffsetMatrix);
}
}
// Retargeting
{
stream->WriteInt32(_skeletonRetargets.Count());
for (const auto& retarget : _skeletonRetargets)
{
stream->Write(retarget.SourceAsset);
stream->Write(retarget.SkeletonAsset);
stream->Write(retarget.NodesMapping);
}
}
}
// Use a temporary chunks for data storage for virtual assets
FlaxChunk* tmpChunks[ASSET_FILE_DATA_CHUNKS];
Platform::MemoryClear(tmpChunks, sizeof(tmpChunks));
Array<FlaxChunk> chunks;
if (IsVirtual())
chunks.Resize(ASSET_FILE_DATA_CHUNKS);
#define GET_CHUNK(index) (IsVirtual() ? tmpChunks[index] = &chunks[index] : GetOrCreateChunk(index))
// Check if use data from drive or from GPU
if (withMeshDataFromGpu)
{
// Download all meshes buffers
Array<Task*> tasks;
for (int32 lodIndex = 0; lodIndex < LODs.Count(); lodIndex++)
{
auto& lod = LODs[lodIndex];
const int32 meshesCount = lod.Meshes.Count();
struct MeshData
{
BytesContainer VB0;
BytesContainer IB;
uint32 DataSize() const
{
return VB0.Length() + IB.Length();
}
};
Array<MeshData> meshesData;
meshesData.Resize(meshesCount);
tasks.EnsureCapacity(meshesCount * 4);
for (int32 meshIndex = 0; meshIndex < meshesCount; meshIndex++)
{
const auto& mesh = lod.Meshes[meshIndex];
auto& meshData = meshesData[meshIndex];
// Vertex Buffer 0 (required)
auto task = mesh.DownloadDataGPUAsync(MeshBufferType::Vertex0, meshData.VB0);
if (task == nullptr)
return true;
task->Start();
tasks.Add(task);
// Index Buffer (required)
task = mesh.DownloadDataGPUAsync(MeshBufferType::Index, meshData.IB);
if (task == nullptr)
return true;
task->Start();
tasks.Add(task);
}
if (Task::WaitAll(tasks))
return true;
tasks.Clear();
// Create meshes data
{
int32 dataSize = meshesCount * (2 * sizeof(uint32) + sizeof(bool));
for (int32 meshIndex = 0; meshIndex < meshesCount; meshIndex++)
dataSize += meshesData[meshIndex].DataSize();
MemoryWriteStream meshesStream(Math::RoundUpToPowerOf2(dataSize));
meshesStream.WriteByte(1);
for (int32 meshIndex = 0; meshIndex < meshesCount; meshIndex++)
{
const auto& mesh = lod.Meshes[meshIndex];
const auto& meshData = meshesData[meshIndex];
const uint32 vertices = mesh.GetVertexCount();
const uint32 triangles = mesh.GetTriangleCount();
const uint32 vb0Size = vertices * sizeof(VB0SkinnedElementType);
const uint32 indicesCount = triangles * 3;
const bool shouldUse16BitIndexBuffer = indicesCount <= MAX_uint16;
const bool use16BitIndexBuffer = mesh.Use16BitIndexBuffer();
const uint32 ibSize = indicesCount * (use16BitIndexBuffer ? sizeof(uint16) : sizeof(uint32));
if (vertices == 0 || triangles == 0)
{
LOG(Warning, "Cannot save model with empty meshes.");
return true;
}
if ((uint32)meshData.VB0.Length() < vb0Size)
{
LOG(Warning, "Invalid vertex buffer 0 size.");
return true;
}
if ((uint32)meshData.IB.Length() < ibSize)
{
LOG(Warning, "Invalid index buffer size.");
return true;
}
// #MODEL_DATA_FORMAT_USAGE
meshesStream.WriteUint32(vertices);
meshesStream.WriteUint32(triangles);
meshesStream.WriteUint16(mesh.BlendShapes.Count());
for (const auto& blendShape : mesh.BlendShapes)
{
meshesStream.WriteBool(blendShape.UseNormals);
meshesStream.WriteUint32(blendShape.MinVertexIndex);
meshesStream.WriteUint32(blendShape.MaxVertexIndex);
meshesStream.WriteUint32(blendShape.Vertices.Count());
meshesStream.WriteBytes(blendShape.Vertices.Get(), blendShape.Vertices.Count() * sizeof(BlendShapeVertex));
}
meshesStream.WriteBytes(meshData.VB0.Get(), vb0Size);
if (shouldUse16BitIndexBuffer == use16BitIndexBuffer)
{
meshesStream.WriteBytes(meshData.IB.Get(), ibSize);
}
else if (shouldUse16BitIndexBuffer)
{
const auto ib = reinterpret_cast<const int32*>(meshData.IB.Get());
for (uint32 i = 0; i < indicesCount; i++)
{
meshesStream.WriteUint16(ib[i]);
}
}
else
{
CRASH;
}
}
// Override meshes data chunk with the fetched GPU meshes memory
auto lodChunk = GET_CHUNK(MODEL_LOD_TO_CHUNK_INDEX(lodIndex));
if (lodChunk == nullptr)
return true;
lodChunk->Data.Copy(meshesStream.GetHandle(), meshesStream.GetPosition());
}
}
}
else
{
ASSERT(!IsVirtual());
// Load all chunks with a mesh data
for (int32 lodIndex = 0; lodIndex < LODs.Count(); lodIndex++)
{
if (LoadChunk(MODEL_LOD_TO_CHUNK_INDEX(lodIndex)))
return true;
}
}
// Set mesh header data
auto headerChunk = GET_CHUNK(0);
ASSERT(headerChunk != nullptr);
headerChunk->Data.Copy(headerStream.GetHandle(), headerStream.GetPosition());
#undef GET_CHUNK
// Save
AssetInitData data;
data.SerializedVersion = SerializedVersion;
if (IsVirtual())
Platform::MemoryCopy(_header.Chunks, tmpChunks, sizeof(_header.Chunks));
const bool saveResult = path.HasChars() ? SaveAsset(path, data) : SaveAsset(data, true);
if (IsVirtual())
Platform::MemoryClear(_header.Chunks, sizeof(_header.Chunks));
if (saveResult)
{
LOG(Error, "Cannot save \'{0}\'", ToString());
return true;
}
return false;
}
#endif
bool SkinnedModel::Init(const Span<int32>& meshesCountPerLod)
{
if (meshesCountPerLod.IsInvalid() || meshesCountPerLod.Length() > MODEL_MAX_LODS)
@@ -777,6 +474,329 @@ bool SkinnedModel::Init(const Span<int32>& meshesCountPerLod)
return false;
}
void BlendShape::LoadHeader(ReadStream& stream, byte headerVersion)
{
stream.Read(Name, 13);
stream.Read(Weight);
}
void BlendShape::Load(ReadStream& stream, byte meshVersion)
{
UseNormals = stream.ReadBool();
stream.ReadUint32(&MinVertexIndex);
stream.ReadUint32(&MaxVertexIndex);
uint32 blendShapeVertices;
stream.ReadUint32(&blendShapeVertices);
Vertices.Resize(blendShapeVertices);
stream.ReadBytes(Vertices.Get(), Vertices.Count() * sizeof(BlendShapeVertex));
}
#if USE_EDITOR
void BlendShape::SaveHeader(WriteStream& stream) const
{
stream.Write(Name, 13);
stream.Write(Weight);
}
void BlendShape::Save(WriteStream& stream) const
{
stream.WriteBool(UseNormals);
stream.WriteUint32(MinVertexIndex);
stream.WriteUint32(MaxVertexIndex);
stream.WriteUint32(Vertices.Count());
stream.WriteBytes(Vertices.Get(), Vertices.Count() * sizeof(BlendShapeVertex));
}
#endif
bool SkinnedModel::LoadMesh(MemoryReadStream& stream, byte meshVersion, MeshBase* mesh, MeshData* dataIfReadOnly)
{
if (ModelBase::LoadMesh(stream, meshVersion, mesh, dataIfReadOnly))
return true;
static_assert(MeshVersion == 2, "Update code");
auto skinnedMesh = (SkinnedMesh*)mesh;
// Blend Shapes
uint16 blendShapesCount;
stream.Read(blendShapesCount);
if (dataIfReadOnly)
{
// Skip blend shapes
BlendShape tmp;
while (blendShapesCount-- != 0)
tmp.Load(stream, meshVersion);
return false;
}
if (blendShapesCount != skinnedMesh->BlendShapes.Count())
{
LOG(Warning, "Incorrect blend shapes amount: {} (expected: {})", blendShapesCount, skinnedMesh->BlendShapes.Count());
return true;
}
for (auto& blendShape : skinnedMesh->BlendShapes)
blendShape.Load(stream, meshVersion);
return false;
}
bool SkinnedModel::LoadHeader(ReadStream& stream, byte& headerVersion)
{
if (ModelBase::LoadHeader(stream, headerVersion))
return true;
static_assert(HeaderVersion == 2, "Update code");
// LODs
byte lods = stream.ReadByte();
if (lods > MODEL_MAX_LODS)
return true;
LODs.Resize(lods);
_initialized = true;
for (int32 lodIndex = 0; lodIndex < lods; lodIndex++)
{
auto& lod = LODs[lodIndex];
lod._model = this;
lod._lodIndex = lodIndex;
stream.Read(lod.ScreenSize);
// Meshes
uint16 meshesCount;
stream.Read(meshesCount);
if (meshesCount > MODEL_MAX_MESHES)
return true;
ASSERT(lodIndex == 0 || LODs[0].Meshes.Count() >= meshesCount);
lod.Meshes.Resize(meshesCount, false);
for (uint16 meshIndex = 0; meshIndex < meshesCount; meshIndex++)
{
SkinnedMesh& mesh = lod.Meshes[meshIndex];
mesh.Link(this, lodIndex, meshIndex);
// Material Slot index
int32 materialSlotIndex;
stream.Read(materialSlotIndex);
if (materialSlotIndex < 0 || materialSlotIndex >= MaterialSlots.Count())
{
LOG(Warning, "Invalid material slot index {0} for mesh {1}. Slots count: {2}.", materialSlotIndex, meshIndex, MaterialSlots.Count());
return true;
}
mesh.SetMaterialSlotIndex(materialSlotIndex);
// Bounds
BoundingBox box;
stream.Read(box);
BoundingSphere sphere;
stream.Read(sphere);
mesh.SetBounds(box, sphere);
// Blend Shapes
uint16 blendShapes;
stream.Read(blendShapes);
mesh.BlendShapes.Resize(blendShapes);
for (auto& blendShape : mesh.BlendShapes)
blendShape.LoadHeader(stream, headerVersion);
}
}
// Skeleton
{
int32 nodesCount;
stream.Read(nodesCount);
if (nodesCount < 0)
return true;
Skeleton.Nodes.Resize(nodesCount, false);
for (auto& node : Skeleton.Nodes)
{
stream.Read(node.ParentIndex);
stream.Read(node.LocalTransform);
stream.Read(node.Name, 71);
}
int32 bonesCount;
stream.Read(bonesCount);
if (bonesCount < 0)
return true;
Skeleton.Bones.Resize(bonesCount, false);
for (auto& bone : Skeleton.Bones)
{
stream.Read(bone.ParentIndex);
stream.Read(bone.NodeIndex);
stream.Read(bone.LocalTransform);
stream.Read(bone.OffsetMatrix);
}
}
// Retargeting
{
int32 entriesCount;
stream.Read(entriesCount);
_skeletonRetargets.Resize(entriesCount);
for (auto& retarget : _skeletonRetargets)
{
stream.Read(retarget.SourceAsset);
stream.Read(retarget.SkeletonAsset);
stream.Read(retarget.NodesMapping);
}
}
return false;
}
#if USE_EDITOR
bool SkinnedModel::SaveHeader(WriteStream& stream)
{
if (ModelBase::SaveHeader(stream))
return true;
static_assert(HeaderVersion == 2, "Update code");
// LODs
stream.Write((byte)LODs.Count());
for (int32 lodIndex = 0; lodIndex < LODs.Count(); lodIndex++)
{
auto& lod = LODs[lodIndex];
stream.Write(lod.ScreenSize);
// Meshes
stream.Write((uint16)lod.Meshes.Count());
for (const auto& mesh : lod.Meshes)
{
stream.Write(mesh.GetMaterialSlotIndex());
stream.Write(mesh.GetBox());
stream.Write(mesh.GetSphere());
// Blend Shapes
const int32 blendShapes = mesh.BlendShapes.Count();
stream.Write((uint16)blendShapes);
for (const auto& blendShape : mesh.BlendShapes)
blendShape.Save(stream);
}
}
// Skeleton nodes
const auto& skeletonNodes = Skeleton.Nodes;
stream.Write(skeletonNodes.Count());
for (const auto& node : skeletonNodes)
{
stream.Write(node.ParentIndex);
stream.Write(node.LocalTransform);
stream.Write(node.Name, 71);
}
// Skeleton bones
const auto& skeletonBones = Skeleton.Bones;
stream.WriteInt32(skeletonBones.Count());
for (const auto& bone : skeletonBones)
{
stream.Write(bone.ParentIndex);
stream.Write(bone.NodeIndex);
stream.Write(bone.LocalTransform);
stream.Write(bone.OffsetMatrix);
}
// Retargeting
stream.WriteInt32(_skeletonRetargets.Count());
for (const auto& retarget : _skeletonRetargets)
{
stream.Write(retarget.SourceAsset);
stream.Write(retarget.SkeletonAsset);
stream.Write(retarget.NodesMapping);
}
return false;
}
bool SkinnedModel::SaveHeader(WriteStream& stream, const ModelData& modelData)
{
if (ModelBase::SaveHeader(stream, modelData))
return true;
static_assert(HeaderVersion == 2, "Update code");
// LODs
stream.Write((byte)modelData.LODs.Count());
for (int32 lodIndex = 0; lodIndex < modelData.LODs.Count(); lodIndex++)
{
auto& lod = modelData.LODs[lodIndex];
// Screen Size
stream.Write(lod.ScreenSize);
// Meshes
stream.Write((uint16)lod.Meshes.Count());
for (const auto& mesh : lod.Meshes)
{
BoundingBox box;
BoundingSphere sphere;
mesh->CalculateBounds(box, sphere);
stream.Write(mesh->MaterialSlotIndex);
stream.Write(box);
stream.Write(sphere);
// Blend Shapes
const int32 blendShapes = mesh->BlendShapes.Count();
stream.WriteUint16((uint16)blendShapes);
for (const auto& blendShape : mesh->BlendShapes)
blendShape.SaveHeader(stream);
}
}
// Skeleton nodes
const auto& skeletonNodes = modelData.Skeleton.Nodes;
stream.WriteInt32(skeletonNodes.Count());
for (const auto& node : skeletonNodes)
{
stream.Write(node.ParentIndex);
stream.Write(node.LocalTransform);
stream.Write(node.Name, 71);
}
// Skeleton bones
const auto& skeletonBones = modelData.Skeleton.Bones;
stream.WriteInt32(skeletonBones.Count());
for (const auto& bone : skeletonBones)
{
stream.Write(bone.ParentIndex);
stream.Write(bone.NodeIndex);
stream.Write(bone.LocalTransform);
stream.Write(bone.OffsetMatrix);
}
// Retargeting
stream.Write(0); // Empty list
return false;
}
bool SkinnedModel::SaveMesh(WriteStream& stream, const MeshBase* mesh) const
{
if (ModelBase::SaveMesh(stream, mesh))
return true;
static_assert(MeshVersion == 2, "Update code");
auto skinnedMesh = (const SkinnedMesh*)mesh;
// Blend Shapes
uint16 blendShapesCount = skinnedMesh->BlendShapes.Count();
stream.Write(blendShapesCount);
for (auto& blendShape : skinnedMesh->BlendShapes)
blendShape.Save(stream);
return false;
}
bool SkinnedModel::SaveMesh(WriteStream& stream, const ModelData& modelData, int32 lodIndex, int32 meshIndex)
{
static_assert(MeshVersion == 2, "Update code");
auto& mesh = modelData.LODs[lodIndex].Meshes[meshIndex];
// Blend Shapes
uint16 blendShapesCount = (uint16)mesh->BlendShapes.Count();
stream.Write(blendShapesCount);
for (auto& blendShape : mesh->BlendShapes)
blendShape.Save(stream);
return false;
}
#endif
void SkinnedModel::ClearSkeletonMapping()
{
for (auto& e : _skeletonMappingCache)
@@ -841,6 +861,26 @@ int32 SkinnedModel::GetLODsCount() const
return LODs.Count();
}
const MeshBase* SkinnedModel::GetMesh(int32 meshIndex, int32 lodIndex) const
{
auto& lod = LODs[lodIndex];
return &lod.Meshes[meshIndex];
}
MeshBase* SkinnedModel::GetMesh(int32 meshIndex, int32 lodIndex)
{
auto& lod = LODs[lodIndex];
return &lod.Meshes[meshIndex];
}
void SkinnedModel::GetMeshes(Array<const MeshBase*>& meshes, int32 lodIndex) const
{
auto& lod = LODs[lodIndex];
meshes.Resize(lod.Meshes.Count());
for (int32 meshIndex = 0; meshIndex < lod.Meshes.Count(); meshIndex++)
meshes[meshIndex] = &lod.Meshes[meshIndex];
}
void SkinnedModel::GetMeshes(Array<MeshBase*>& meshes, int32 lodIndex)
{
auto& lod = LODs[lodIndex];
@@ -889,145 +929,11 @@ Asset::LoadResult SkinnedModel::load()
if (chunk0 == nullptr || chunk0->IsMissing())
return LoadResult::MissingDataChunk;
MemoryReadStream headerStream(chunk0->Get(), chunk0->Size());
ReadStream* stream = &headerStream;
// Header Version
byte version = stream->ReadByte();
// Min Screen Size
stream->ReadFloat(&MinScreenSize);
// Amount of material slots
int32 materialSlotsCount;
stream->ReadInt32(&materialSlotsCount);
if (materialSlotsCount < 0 || materialSlotsCount > 4096)
// Load asset data (anything but mesh contents that use streaming)
byte headerVersion;
if (LoadHeader(headerStream, headerVersion))
return LoadResult::InvalidData;
MaterialSlots.Resize(materialSlotsCount, false);
// For each material slot
for (int32 materialSlotIndex = 0; materialSlotIndex < materialSlotsCount; materialSlotIndex++)
{
auto& slot = MaterialSlots[materialSlotIndex];
// Material
Guid materialId;
stream->Read(materialId);
slot.Material = materialId;
// Shadows Mode
slot.ShadowsMode = static_cast<ShadowsCastingMode>(stream->ReadByte());
// Name
stream->ReadString(&slot.Name, 11);
}
// Amount of LODs
byte lods;
stream->ReadByte(&lods);
if (lods > MODEL_MAX_LODS)
return LoadResult::InvalidData;
LODs.Resize(lods);
_initialized = true;
// For each LOD
for (int32 lodIndex = 0; lodIndex < lods; lodIndex++)
{
auto& lod = LODs[lodIndex];
lod._model = this;
lod._lodIndex = lodIndex;
// Screen Size
stream->ReadFloat(&lod.ScreenSize);
// Amount of meshes
uint16 meshesCount;
stream->ReadUint16(&meshesCount);
if (meshesCount == 0 || meshesCount > MODEL_MAX_MESHES)
return LoadResult::InvalidData;
ASSERT(lodIndex == 0 || LODs[0].Meshes.Count() >= meshesCount);
// Allocate memory
lod.Meshes.Resize(meshesCount, false);
// For each mesh
for (uint16 meshIndex = 0; meshIndex < meshesCount; meshIndex++)
{
SkinnedMesh& mesh = lod.Meshes[meshIndex];
mesh.Link(this, lodIndex, meshIndex);
// Material Slot index
int32 materialSlotIndex;
stream->ReadInt32(&materialSlotIndex);
if (materialSlotIndex < 0 || materialSlotIndex >= materialSlotsCount)
{
LOG(Warning, "Invalid material slot index {0} for mesh {1}. Slots count: {2}.", materialSlotIndex, meshIndex, materialSlotsCount);
return LoadResult::InvalidData;
}
mesh.SetMaterialSlotIndex(materialSlotIndex);
// Bounds
BoundingBox box;
stream->ReadBoundingBox(&box);
BoundingSphere sphere;
stream->ReadBoundingSphere(&sphere);
mesh.SetBounds(box, sphere);
// Blend Shapes
uint16 blendShapes;
stream->ReadUint16(&blendShapes);
mesh.BlendShapes.Resize(blendShapes);
for (int32 blendShapeIndex = 0; blendShapeIndex < blendShapes; blendShapeIndex++)
{
auto& blendShape = mesh.BlendShapes[blendShapeIndex];
stream->ReadString(&blendShape.Name, 13);
stream->ReadFloat(&blendShape.Weight);
}
}
}
// Skeleton
{
int32 nodesCount;
stream->ReadInt32(&nodesCount);
if (nodesCount <= 0)
return LoadResult::InvalidData;
Skeleton.Nodes.Resize(nodesCount, false);
for (int32 nodeIndex = 0; nodeIndex < nodesCount; nodeIndex++)
{
auto& node = Skeleton.Nodes.Get()[nodeIndex];
stream->Read(node.ParentIndex);
stream->ReadTransform(&node.LocalTransform);
stream->ReadString(&node.Name, 71);
}
int32 bonesCount;
stream->ReadInt32(&bonesCount);
if (bonesCount <= 0)
return LoadResult::InvalidData;
Skeleton.Bones.Resize(bonesCount, false);
for (int32 boneIndex = 0; boneIndex < bonesCount; boneIndex++)
{
auto& bone = Skeleton.Bones.Get()[boneIndex];
stream->Read(bone.ParentIndex);
stream->Read(bone.NodeIndex);
stream->ReadTransform(&bone.LocalTransform);
stream->Read(bone.OffsetMatrix);
}
}
// Retargeting
{
int32 entriesCount;
stream->ReadInt32(&entriesCount);
_skeletonRetargets.Resize(entriesCount);
for (int32 entryIndex = 0; entryIndex < entriesCount; entryIndex++)
{
auto& retarget = _skeletonRetargets[entryIndex];
stream->Read(retarget.SourceAsset);
stream->Read(retarget.SkeletonAsset);
stream->Read(retarget.NodesMapping);
}
}
// Request resource streaming
StartStreaming(true);

View File

@@ -139,7 +139,7 @@ public:
/// </summary>
API_CLASS(NoSpawn) class FLAXENGINE_API SkinnedModel : public ModelBase
{
DECLARE_BINARY_ASSET_HEADER(SkinnedModel, 5);
DECLARE_BINARY_ASSET_HEADER(SkinnedModel, 30);
friend SkinnedMesh;
public:
// Skeleton mapping descriptor.
@@ -331,17 +331,6 @@ public:
/// <returns>True if failed, otherwise false.</returns>
API_FUNCTION() bool SetupSkeleton(const Array<SkeletonNode>& nodes, const Array<SkeletonBone>& bones, bool autoCalculateOffsetMatrix);
#if USE_EDITOR
/// <summary>
/// Saves this asset to the file. Supported only in Editor.
/// </summary>
/// <remarks>If you use saving with the GPU mesh data then the call has to be provided from the thread other than the main game thread.</remarks>
/// <param name="withMeshDataFromGpu">True if save also GPU mesh buffers, otherwise will keep data in storage unmodified. Valid only if saving the same asset to the same location and it's loaded.</param>
/// <param name="path">The custom asset path to use for the saving. Use empty value to save this asset to its own storage location. Can be used to duplicate asset. Must be specified when saving virtual asset.</param>
/// <returns>True if cannot save data, otherwise false.</returns>
API_FUNCTION() bool Save(bool withMeshDataFromGpu = false, const StringView& path = StringView::Empty);
#endif
private:
/// <summary>
/// Initializes this skinned model to an empty collection of meshes. Ensure to init SkeletonData manually after the call.
@@ -350,6 +339,17 @@ private:
/// <returns>True if failed, otherwise false.</returns>
bool Init(const Span<int32>& meshesCountPerLod);
// [ModelBase]
bool LoadMesh(MemoryReadStream& stream, byte meshVersion, MeshBase* mesh, MeshData* dataIfReadOnly) override;
bool LoadHeader(ReadStream& stream, byte& headerVersion);
#if USE_EDITOR
friend class ImportModel;
bool SaveHeader(WriteStream& stream) override;
static bool SaveHeader(WriteStream& stream, const ModelData& modelData);
bool SaveMesh(WriteStream& stream, const MeshBase* mesh) const override;
static bool SaveMesh(WriteStream& stream, const ModelData& modelData, int32 lodIndex, int32 meshIndex);
#endif
void ClearSkeletonMapping();
void OnSkeletonMappingSourceAssetUnloaded(Asset* obj);
@@ -384,6 +384,9 @@ public:
uint64 GetMemoryUsage() const override;
void SetupMaterialSlots(int32 slotsCount) override;
int32 GetLODsCount() const override;
const MeshBase* GetMesh(int32 meshIndex, int32 lodIndex = 0) const override;
MeshBase* GetMesh(int32 meshIndex, int32 lodIndex = 0) override;
void GetMeshes(Array<const MeshBase*>& meshes, int32 lodIndex = 0) const override;
void GetMeshes(Array<MeshBase*>& meshes, int32 lodIndex = 0) override;
void InitAsVirtual() override;