**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

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