// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. #include "SkinnedModel.h" #include "Animation.h" #include "Engine/Core/Log.h" #include "Engine/Engine/Engine.h" #include "Engine/Serialization/MemoryReadStream.h" #include "Engine/Streaming/StreamingGroup.h" #include "Engine/Threading/ThreadPoolTask.h" #include "Engine/Threading/Threading.h" #include "Engine/Graphics/RenderTools.h" #include "Engine/Graphics/RenderTask.h" #include "Engine/Graphics/Models/ModelInstanceEntry.h" #include "Engine/Graphics/Models/Config.h" #include "Engine/Content/Content.h" #include "Engine/Content/WeakAssetReference.h" #include "Engine/Content/Factories/BinaryAssetFactory.h" #include "Engine/Content/Upgraders/SkinnedModelAssetUpgrader.h" #include "Engine/Debug/Exceptions/ArgumentOutOfRangeException.h" #include "Engine/Profiler/ProfilerCPU.h" #include "Engine/Renderer/DrawCall.h" #define CHECK_INVALID_BUFFER(model, buffer) \ if (buffer->IsValidFor(model) == false) \ { \ LOG(Warning, "Invalid Skinned Model Instance Buffer size {0} for Skinned Model {1}. It should be {2}. Manual update to proper size.", buffer->Count(), model->ToString(), model->MaterialSlots.Count()); \ buffer->Setup(model); \ } /// /// Skinned model data streaming task /// class StreamSkinnedModelLODTask : public ThreadPoolTask { private: WeakAssetReference _asset; int32 _lodIndex; FlaxStorage::LockData _dataLock; public: /// /// Init /// /// Parent model /// LOD to stream index StreamSkinnedModelLODTask(SkinnedModel* model, int32 lodIndex) : _asset(model) , _lodIndex(lodIndex) , _dataLock(model->Storage->Lock()) { } public: // [ThreadPoolTask] bool HasReference(Object* resource) const override { return _asset == resource; } protected: // [ThreadPoolTask] bool Run() override { AssetReference model = _asset.Get(); if (model == nullptr) { return true; } // Get data BytesContainer data; model->GetLODData(_lodIndex, data); if (data.IsInvalid()) { LOG(Warning, "Missing data chunk"); return true; } 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) if (model->LODs[_lodIndex].Load(stream)) { LOG(Warning, "Cannot load LOD{1} for model \'{0}\'", model->ToString(), _lodIndex); return true; } // Update residency level model->_loadedLODs++; model->ResidencyChanged(); return false; } void OnEnd() override { // Unlink if (_asset) { ASSERT(_asset->_streamingTask == this); _asset->_streamingTask = nullptr; _asset = nullptr; } _dataLock.Release(); // Base ThreadPoolTask::OnEnd(); } }; REGISTER_BINARY_ASSET_WITH_UPGRADER(SkinnedModel, "FlaxEngine.SkinnedModel", SkinnedModelAssetUpgrader, true); SkinnedModel::SkinnedModel(const SpawnParams& params, const AssetInfo* info) : ModelBase(params, info, StreamingGroups::Instance()->SkinnedModels()) { } SkinnedModel::~SkinnedModel() { ASSERT(_streamingTask == nullptr); ASSERT(_skeletonMappingCache.Count() == 0); } bool SkinnedModel::HasAnyLODInitialized() const { return LODs.HasItems() && LODs.Last().HasAnyMeshInitialized(); } Array SkinnedModel::GetBlendShapes() { Array result; if (LODs.HasItems()) { for (auto& mesh : LODs[0].Meshes) { for (auto& blendShape : mesh.BlendShapes) { if (!result.Contains(blendShape.Name)) result.Add(blendShape.Name); } } } return result; } ContentLoadTask* SkinnedModel::RequestLODDataAsync(int32 lodIndex) { const int32 chunkIndex = MODEL_LOD_TO_CHUNK_INDEX(lodIndex); return RequestChunkDataAsync(chunkIndex); } void SkinnedModel::GetLODData(int32 lodIndex, BytesContainer& data) const { const int32 chunkIndex = MODEL_LOD_TO_CHUNK_INDEX(lodIndex); GetChunkData(chunkIndex, data); } SkinnedModel::SkeletonMapping SkinnedModel::GetSkeletonMapping(Asset* source) { SkeletonMapping mapping; mapping.TargetSkeleton = this; if (WaitForLoaded() || !source || source->WaitForLoaded()) return mapping; ScopeLock lock(Locker); SkeletonMappingData mappingData; if (!_skeletonMappingCache.TryGet(source, mappingData)) { PROFILE_CPU(); // Initialize the mapping const int32 nodesCount = Skeleton.Nodes.Count(); mappingData.NodesMapping = Span((int32*)Allocator::Allocate(nodesCount * sizeof(int32)), nodesCount); for (int32 i = 0; i < nodesCount; i++) mappingData.NodesMapping[i] = -1; SkeletonRetarget* retarget = nullptr; const Guid sourceId = source->GetID(); for (auto& e : _skeletonRetargets) { if (e.SourceAsset == sourceId) { retarget = &e; break; } } if (const auto* sourceAnim = Cast(source)) { const auto& channels = sourceAnim->Data.Channels; if (retarget && retarget->SkeletonAsset) { // Map retarget skeleton nodes from animation channels if (auto* skeleton = Content::Load(retarget->SkeletonAsset)) { const SkeletonMapping skeletonMapping = GetSkeletonMapping(skeleton); mappingData.SourceSkeleton = skeleton; if (skeletonMapping.NodesMapping.Length() == nodesCount) { const auto& nodes = skeleton->Skeleton.Nodes; for (int32 j = 0; j < nodesCount; j++) { if (skeletonMapping.NodesMapping[j] != -1) { const Char* nodeName = nodes[skeletonMapping.NodesMapping[j]].Name.GetText(); for (int32 i = 0; i < channels.Count(); i++) { if (StringUtils::CompareIgnoreCase(nodeName, channels[i].NodeName.GetText()) == 0) { mappingData.NodesMapping[j] = i; break; } } } } } } else { #if !BUILD_RELEASE LOG(Error, "Missing asset {0} to use for skeleton mapping of {1}", retarget->SkeletonAsset, ToString()); #endif return mapping; } } else { // Map animation channels to the skeleton nodes (by name) for (int32 i = 0; i < channels.Count(); i++) { const NodeAnimationData& nodeAnim = channels[i]; for (int32 j = 0; j < nodesCount; j++) { if (StringUtils::CompareIgnoreCase(Skeleton.Nodes[j].Name.GetText(), nodeAnim.NodeName.GetText()) == 0) { mappingData.NodesMapping[j] = i; break; } } } } } else if (const auto* sourceModel = Cast(source)) { if (retarget) { // Use nodes retargeting for (const auto& e : retarget->NodesMapping) { const int32 dstIndex = Skeleton.FindNode(e.Key); const int32 srcIndex = sourceModel->Skeleton.FindNode(e.Value); if (dstIndex != -1 && srcIndex != -1) { mappingData.NodesMapping[dstIndex] = srcIndex; } } } else { // Map source skeleton nodes to the target skeleton nodes (by name) const auto& nodes = sourceModel->Skeleton.Nodes; for (int32 i = 0; i < nodes.Count(); i++) { const SkeletonNode& node = nodes[i]; for (int32 j = 0; j < nodesCount; j++) { if (StringUtils::CompareIgnoreCase(Skeleton.Nodes[j].Name.GetText(), node.Name.GetText()) == 0) { mappingData.NodesMapping[j] = i; break; } } } } } else { #if !BUILD_RELEASE LOG(Error, "Invalid asset type {0} to use for skeleton mapping of {1}", source->GetTypeName(), ToString()); #endif } // Add to cache _skeletonMappingCache.Add(source, mappingData); source->OnUnloaded.Bind(this); #if USE_EDITOR source->OnReloading.Bind(this); #endif } mapping.SourceSkeleton = mappingData.SourceSkeleton; mapping.NodesMapping = mappingData.NodesMapping; return mapping; } bool SkinnedModel::Intersects(const Ray& ray, const Matrix& world, Real& distance, Vector3& normal, SkinnedMesh** mesh, int32 lodIndex) { if (LODs.Count() == 0) return false; return LODs[lodIndex].Intersects(ray, world, distance, normal, mesh); } bool SkinnedModel::Intersects(const Ray& ray, const Transform& transform, Real& distance, Vector3& normal, SkinnedMesh** mesh, int32 lodIndex) { if (LODs.Count() == 0) return false; return LODs[lodIndex].Intersects(ray, transform, distance, normal, mesh); } BoundingBox SkinnedModel::GetBox(const Matrix& world, int32 lodIndex) const { if (LODs.Count() == 0) return BoundingBox::Zero; return LODs[lodIndex].GetBox(world); } BoundingBox SkinnedModel::GetBox(int32 lodIndex) const { if (LODs.Count() == 0) return BoundingBox::Zero; return LODs[lodIndex].GetBox(); } template FORCE_INLINE void SkinnedModelDraw(SkinnedModel* model, const RenderContext& renderContext, const ContextType& context, const SkinnedMesh::DrawInfo& info) { ASSERT(info.Buffer); if (!model->CanBeRendered()) return; const auto frame = Engine::FrameCount; const auto modelFrame = info.DrawState->PrevFrame + 1; CHECK_INVALID_BUFFER(model, info.Buffer); // Select a proper LOD index (model may be culled) int32 lodIndex; if (info.ForcedLOD != -1) { lodIndex = info.ForcedLOD; } else { lodIndex = RenderTools::ComputeSkinnedModelLOD(model, info.Bounds.Center, (float)info.Bounds.Radius, renderContext); if (lodIndex == -1) { // Handling model fade-out transition if (modelFrame == frame && info.DrawState->PrevLOD != -1 && !renderContext.View.IsSingleFrame) { // Check if start transition if (info.DrawState->LODTransition == 255) { info.DrawState->LODTransition = 0; } RenderTools::UpdateModelLODTransition(info.DrawState->LODTransition); // Check if end transition if (info.DrawState->LODTransition == 255) { info.DrawState->PrevLOD = lodIndex; } else { const auto prevLOD = model->ClampLODIndex(info.DrawState->PrevLOD); const float normalizedProgress = static_cast(info.DrawState->LODTransition) * (1.0f / 255.0f); model->LODs.Get()[prevLOD].Draw(renderContext, info, normalizedProgress); } } return; } } lodIndex += info.LODBias + renderContext.View.ModelLODBias; lodIndex = model->ClampLODIndex(lodIndex); if (renderContext.View.IsSingleFrame) { } // Check if it's the new frame and could update the drawing state (note: model instance could be rendered many times per frame to different viewports) else if (modelFrame == frame) { // Check if start transition if (info.DrawState->PrevLOD != lodIndex && info.DrawState->LODTransition == 255) { info.DrawState->LODTransition = 0; } RenderTools::UpdateModelLODTransition(info.DrawState->LODTransition); // Check if end transition if (info.DrawState->LODTransition == 255) { info.DrawState->PrevLOD = lodIndex; } } // Check if there was a gap between frames in drawing this model instance else if (modelFrame < frame || info.DrawState->PrevLOD == -1) { // Reset state info.DrawState->PrevLOD = lodIndex; info.DrawState->LODTransition = 255; } // Draw if (info.DrawState->PrevLOD == lodIndex || renderContext.View.IsSingleFrame) { model->LODs.Get()[lodIndex].Draw(context, info, 0.0f); } else if (info.DrawState->PrevLOD == -1) { const float normalizedProgress = static_cast(info.DrawState->LODTransition) * (1.0f / 255.0f); model->LODs.Get()[lodIndex].Draw(context, info, 1.0f - normalizedProgress); } else { const auto prevLOD = model->ClampLODIndex(info.DrawState->PrevLOD); const float normalizedProgress = static_cast(info.DrawState->LODTransition) * (1.0f / 255.0f); model->LODs.Get()[prevLOD].Draw(context, info, normalizedProgress); model->LODs.Get()[lodIndex].Draw(context, info, normalizedProgress - 1.0f); } } void SkinnedModel::Draw(const RenderContext& renderContext, const SkinnedMesh::DrawInfo& info) { SkinnedModelDraw(this, renderContext, renderContext, info); } void SkinnedModel::Draw(const RenderContextBatch& renderContextBatch, const SkinnedMesh::DrawInfo& info) { SkinnedModelDraw(this, renderContextBatch.GetMainContext(), renderContextBatch, info); } bool SkinnedModel::SetupLODs(const Span& meshesCountPerLod) { ScopeLock lock(Locker); // Validate input and state if (!IsVirtual()) { LOG(Error, "Only virtual models can be updated at runtime."); return true; } return Init(meshesCountPerLod);; } bool SkinnedModel::SetupSkeleton(const Array& nodes) { // Validate input if (nodes.Count() <= 0 || nodes.Count() > MAX_uint16) return true; auto model = this; if (!model->IsVirtual()) return true; ScopeLock lock(model->Locker); // Setup model->Skeleton.Nodes = nodes; model->Skeleton.Bones.Resize(nodes.Count()); for (int32 i = 0; i < nodes.Count(); i++) { auto& node = model->Skeleton.Nodes[i]; model->Skeleton.Bones[i].ParentIndex = node.ParentIndex; model->Skeleton.Bones[i].LocalTransform = node.LocalTransform; model->Skeleton.Bones[i].NodeIndex = i; } ClearSkeletonMapping(); // Calculate offset matrix (inverse bind pose transform) for every bone manually for (int32 i = 0; i < model->Skeleton.Bones.Count(); i++) { Matrix t = Matrix::Identity; int32 idx = model->Skeleton.Bones[i].NodeIndex; do { t *= model->Skeleton.Nodes[idx].LocalTransform.GetWorld(); idx = model->Skeleton.Nodes[idx].ParentIndex; } while (idx != -1); t.Invert(); model->Skeleton.Bones[i].OffsetMatrix = t; } return false; } bool SkinnedModel::SetupSkeleton(const Array& nodes, const Array& bones, bool autoCalculateOffsetMatrix) { // Validate input if (nodes.Count() <= 0 || nodes.Count() > MAX_uint16) return true; if (bones.Count() <= 0 || bones.Count() > MAX_BONES_PER_MODEL) return true; auto model = this; if (!model->IsVirtual()) return true; ScopeLock lock(model->Locker); // Setup model->Skeleton.Nodes = nodes; model->Skeleton.Bones = bones; ClearSkeletonMapping(); // Calculate offset matrix (inverse bind pose transform) for every bone manually if (autoCalculateOffsetMatrix) { for (int32 i = 0; i < model->Skeleton.Bones.Count(); i++) { Matrix t = Matrix::Identity; int32 idx = model->Skeleton.Bones[i].NodeIndex; do { t *= model->Skeleton.Nodes[idx].LocalTransform.GetWorld(); idx = model->Skeleton.Nodes[idx].ParentIndex; } while (idx != -1); t.Invert(); model->Skeleton.Bones[i].OffsetMatrix = t; } } 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(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 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 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 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(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& meshesCountPerLod) { if (meshesCountPerLod.IsInvalid() || meshesCountPerLod.Length() > MODEL_MAX_LODS) { Log::ArgumentOutOfRangeException(); return true; } // Dispose previous data and disable streaming (will start data uploading tasks manually) StopStreaming(); // Setup MaterialSlots.Resize(1); MinScreenSize = 0.0f; for (int32 lodIndex = 0; lodIndex < LODs.Count(); lodIndex++) LODs[lodIndex].Dispose(); LODs.Resize(meshesCountPerLod.Length()); _initialized = true; // Setup meshes for (int32 lodIndex = 0; lodIndex < meshesCountPerLod.Length(); lodIndex++) { auto& lod = LODs[lodIndex]; lod._model = this; lod._lodIndex = lodIndex; lod.ScreenSize = 1.0f; const int32 meshesCount = meshesCountPerLod[lodIndex]; if (meshesCount <= 0 || meshesCount > MODEL_MAX_MESHES) return true; lod.Meshes.Resize(meshesCount); for (int32 meshIndex = 0; meshIndex < meshesCount; meshIndex++) { lod.Meshes[meshIndex].Init(this, lodIndex, meshIndex, 0, BoundingBox::Zero, BoundingSphere::Empty); } } // Update resource residency _loadedLODs = meshesCountPerLod.Length(); ResidencyChanged(); return false; } void SkinnedModel::ClearSkeletonMapping() { for (auto& e : _skeletonMappingCache) { e.Key->OnUnloaded.Unbind(this); #if USE_EDITOR e.Key->OnReloading.Unbind(this); #endif Allocator::Free(e.Value.NodesMapping.Get()); } _skeletonMappingCache.Clear(); } void SkinnedModel::OnSkeletonMappingSourceAssetUnloaded(Asset* obj) { ScopeLock lock(Locker); auto i = _skeletonMappingCache.Find(obj); ASSERT(i != _skeletonMappingCache.End()); // Unlink event obj->OnUnloaded.Unbind(this); #if USE_EDITOR obj->OnReloading.Unbind(this); #endif // Clear cache Allocator::Free(i->Value.NodesMapping.Get()); _skeletonMappingCache.Remove(i); } uint64 SkinnedModel::GetMemoryUsage() const { Locker.Lock(); uint64 result = BinaryAsset::GetMemoryUsage(); result += sizeof(SkinnedModel) - sizeof(BinaryAsset); result += Skeleton.GetMemoryUsage(); result += _skeletonMappingCache.Capacity() * sizeof(Dictionary>::Bucket); for (const auto& e : _skeletonMappingCache) result += e.Value.NodesMapping.Length(); Locker.Unlock(); return result; } void SkinnedModel::SetupMaterialSlots(int32 slotsCount) { ModelBase::SetupMaterialSlots(slotsCount); // Adjust meshes indices for slots for (int32 lodIndex = 0; lodIndex < LODs.Count(); lodIndex++) { for (int32 meshIndex = 0; meshIndex < LODs[lodIndex].Meshes.Count(); meshIndex++) { auto& mesh = LODs[lodIndex].Meshes[meshIndex]; if (mesh.GetMaterialSlotIndex() >= slotsCount) mesh.SetMaterialSlotIndex(slotsCount - 1); } } } int32 SkinnedModel::GetLODsCount() const { return LODs.Count(); } void SkinnedModel::GetMeshes(Array& meshes, int32 lodIndex) { 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::InitAsVirtual() { // Init with one mesh and single bone int32 meshesCount = 1; Init(ToSpan(&meshesCount, 1)); ClearSkeletonMapping(); Skeleton.Dispose(); // Skeleton.Nodes.Resize(1); Skeleton.Nodes[0].Name = TEXT("Root"); Skeleton.Nodes[0].LocalTransform = Transform::Identity; Skeleton.Nodes[0].ParentIndex = -1; // Skeleton.Bones.Resize(1); Skeleton.Bones[0].NodeIndex = 0; Skeleton.Bones[0].OffsetMatrix = Matrix::Identity; Skeleton.Bones[0].LocalTransform = Transform::Identity; Skeleton.Bones[0].ParentIndex = -1; // Base BinaryAsset::InitAsVirtual(); } void SkinnedModel::CancelStreaming() { CancelStreamingTasks(); } #if USE_EDITOR void SkinnedModel::GetReferences(Array& output) const { // Base BinaryAsset::GetReferences(output); for (int32 i = 0; i < MaterialSlots.Count(); i++) { output.Add(MaterialSlots[i].Material.GetID()); } } #endif int32 SkinnedModel::GetMaxResidency() const { return LODs.Count(); } int32 SkinnedModel::GetCurrentResidency() const { return _loadedLODs; } int32 SkinnedModel::GetAllocatedResidency() const { return LODs.Count(); } bool SkinnedModel::CanBeUpdated() const { // Check if is ready and has no streaming tasks running return IsInitialized() && _streamingTask == nullptr; } Task* SkinnedModel::UpdateAllocation(int32 residency) { // SkinnedModels are not using dynamic allocation feature return nullptr; } Task* SkinnedModel::CreateStreamingTask(int32 residency) { ScopeLock lock(Locker); ASSERT(IsInitialized() && Math::IsInRange(residency, 0, LODs.Count()) && _streamingTask == nullptr); Task* result = nullptr; const int32 lodCount = residency - GetCurrentResidency(); // Switch if go up or down with residency if (lodCount > 0) { // Allow only to change LODs count by 1 ASSERT(Math::Abs(lodCount) == 1); int32 lodIndex = HighestResidentLODIndex() - 1; // Request LOD data result = (Task*)RequestLODDataAsync(lodIndex); // Add upload data task _streamingTask = New(this, lodIndex); if (result) result->ContinueWith(_streamingTask); else result = _streamingTask; } else { // Do the quick data release for (int32 i = HighestResidentLODIndex(); i < LODs.Count() - residency; i++) LODs[i].Unload(); _loadedLODs = residency; ResidencyChanged(); } return result; } void SkinnedModel::CancelStreamingTasks() { if (_streamingTask) { _streamingTask->Cancel(); ASSERT_LOW_LAYER(_streamingTask == nullptr); } } Asset::LoadResult SkinnedModel::load() { // Get header chunk auto chunk0 = GetChunk(0); 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) 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(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++) { auto& mesh = lod.Meshes[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; } // Box BoundingBox box; stream->ReadBoundingBox(&box); // Sphere BoundingSphere sphere; stream->ReadBoundingSphere(&sphere); // Create mesh object mesh.Init(this, lodIndex, meshIndex, materialSlotIndex, 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); return LoadResult::Ok; } void SkinnedModel::unload(bool isReloading) { // End streaming (if still active) if (_streamingTask != nullptr) { // Cancel streaming task _streamingTask->Cancel(); _streamingTask = nullptr; } // Cleanup MaterialSlots.Resize(0); for (int32 i = 0; i < LODs.Count(); i++) LODs[i].Dispose(); LODs.Clear(); Skeleton.Dispose(); _initialized = false; _loadedLODs = 0; _skeletonRetargets.Clear(); ClearSkeletonMapping(); } bool SkinnedModel::init(AssetInitData& initData) { // Validate if (initData.SerializedVersion != SerializedVersion) { LOG(Error, "Invalid serialized model version."); return true; } return false; } AssetChunksFlag SkinnedModel::getChunksToPreload() const { // Note: we don't preload any meshes here because it's done by the Streaming Manager return GET_CHUNK_FLAG(0); }