Files
FlaxEngine/Source/Engine/Content/Assets/Model.cpp
2025-01-14 23:26:26 +01:00

752 lines
22 KiB
C++

// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved.
#include "Model.h"
#include "Engine/Core/Log.h"
#include "Engine/Engine/Engine.h"
#include "Engine/Serialization/MemoryReadStream.h"
#include "Engine/Content/WeakAssetReference.h"
#include "Engine/Content/Upgraders/ModelAssetUpgrader.h"
#include "Engine/Content/Factories/BinaryAssetFactory.h"
#include "Engine/Graphics/RenderTools.h"
#include "Engine/Graphics/RenderTask.h"
#include "Engine/Graphics/Models/ModelInstanceEntry.h"
#include "Engine/Streaming/StreamingGroup.h"
#include "Engine/Debug/Exceptions/ArgumentOutOfRangeException.h"
#include "Engine/Graphics/GPUDevice.h"
#include "Engine/Graphics/Async/GPUTask.h"
#include "Engine/Graphics/Async/Tasks/GPUUploadTextureMipTask.h"
#include "Engine/Graphics/Models/MeshDeformation.h"
#include "Engine/Graphics/Textures/GPUTexture.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");
class StreamModelSDFTask : public GPUUploadTextureMipTask
{
private:
WeakAssetReference<Model> _asset;
FlaxStorage::LockData _dataLock;
public:
StreamModelSDFTask(Model* model, GPUTexture* texture, const Span<byte>& data, int32 mipIndex, int32 rowPitch, int32 slicePitch)
: GPUUploadTextureMipTask(texture, mipIndex, data, rowPitch, slicePitch, false)
, _asset(model)
, _dataLock(model->Storage->Lock())
{
}
bool HasReference(Object* resource) const override
{
return _asset == resource;
}
Result run(GPUTasksContext* context) override
{
AssetReference<Model> model = _asset.Get();
if (model == nullptr)
return Result::MissingResources;
return GPUUploadTextureMipTask::run(context);
}
void OnEnd() override
{
_dataLock.Release();
// Base
GPUUploadTextureMipTask::OnEnd();
}
};
REGISTER_BINARY_ASSET_WITH_UPGRADER(Model, "FlaxEngine.Model", ModelAssetUpgrader, true);
static byte EnableModelSDF = 0;
Model::Model(const SpawnParams& params, const AssetInfo* info)
: ModelBase(params, info, StreamingGroups::Instance()->Models())
{
if (EnableModelSDF == 0 && GPUDevice::Instance)
{
const bool enable = GPUDevice::Instance->GetFeatureLevel() >= FeatureLevel::SM5;
EnableModelSDF = enable ? 1 : 2;
}
}
bool Model::HasAnyLODInitialized() const
{
return LODs.HasItems() && LODs.Last().HasAnyMeshInitialized();
}
bool Model::Intersects(const Ray& ray, const Matrix& world, Real& distance, Vector3& normal, Mesh** mesh, int32 lodIndex)
{
return LODs[lodIndex].Intersects(ray, world, distance, normal, mesh);
}
bool Model::Intersects(const Ray& ray, const Transform& transform, Real& distance, Vector3& normal, Mesh** mesh, int32 lodIndex)
{
return LODs[lodIndex].Intersects(ray, transform, distance, normal, mesh);
}
BoundingBox Model::GetBox(const Matrix& world, int32 lodIndex) const
{
return LODs[lodIndex].GetBox(world);
}
BoundingBox Model::GetBox(int32 lodIndex) const
{
return LODs[lodIndex].GetBox();
}
void Model::Draw(const RenderContext& renderContext, MaterialBase* material, const Matrix& world, StaticFlags flags, bool receiveDecals, int8 sortOrder) const
{
if (!CanBeRendered())
return;
// Select a proper LOD index (model may be culled)
const BoundingBox box = GetBox(world);
BoundingSphere sphere;
BoundingSphere::FromBox(box, sphere);
int32 lodIndex = RenderTools::ComputeModelLOD(this, sphere.Center - renderContext.View.Origin, (float)sphere.Radius, renderContext);
if (lodIndex == -1)
return;
lodIndex += renderContext.View.ModelLODBias;
lodIndex = ClampLODIndex(lodIndex);
// Draw
LODs[lodIndex].Draw(renderContext, material, world, flags, receiveDecals, DrawPass::Default, 0, sortOrder);
}
template<typename ContextType>
FORCE_INLINE void ModelDraw(Model* model, const RenderContext& renderContext, const ContextType& context, const Mesh::DrawInfo& info)
{
ASSERT(info.Buffer);
if (!model->CanBeRendered())
return;
if (!info.Buffer->IsValidFor(model))
info.Buffer->Setup(model);
const auto frame = Engine::FrameCount;
const auto modelFrame = info.DrawState->PrevFrame + 1;
// Select a proper LOD index (model may be culled)
int32 lodIndex;
if (info.ForcedLOD != -1)
{
lodIndex = info.ForcedLOD;
}
else
{
lodIndex = RenderTools::ComputeModelLOD(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<float>(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<float>(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<float>(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 Model::Draw(const RenderContext& renderContext, const Mesh::DrawInfo& info)
{
ModelDraw(this, renderContext, renderContext, info);
}
void Model::Draw(const RenderContextBatch& renderContextBatch, const Mesh::DrawInfo& info)
{
ModelDraw(this, renderContextBatch.GetMainContext(), renderContextBatch, info);
}
bool Model::SetupLODs(const Span<int32>& 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 Model::GenerateSDF(float resolutionScale, int32 lodIndex, bool cacheData, float backfacesThreshold, bool useGPU)
{
if (EnableModelSDF == 2)
return true; // Not supported
ScopeLock lock(Locker);
if (!HasAnyLODInitialized())
return true;
if (IsInMainThread() && IsVirtual())
{
// TODO: could be supported if algorithm could run on a GPU and called during rendering
LOG(Warning, "Cannot generate SDF for virtual models on a main thread.");
return true;
}
lodIndex = Math::Clamp(lodIndex, HighestResidentLODIndex(), LODs.Count() - 1);
// Generate SDF
#if USE_EDITOR
cacheData &= Storage != nullptr; // Cache only if has storage linked
MemoryWriteStream sdfStream;
MemoryWriteStream* outputStream = cacheData ? &sdfStream : nullptr;
#else
class MemoryWriteStream* outputStream = nullptr;
#endif
Locker.Unlock();
const bool failed = ModelTool::GenerateModelSDF(this, nullptr, resolutionScale, lodIndex, &SDF, outputStream, GetPath(), backfacesThreshold, useGPU);
Locker.Lock();
if (failed)
return true;
#if USE_EDITOR
// Set asset data
if (cacheData)
{
auto chunk = GetOrCreateChunk(15);
chunk->Data.Copy(sdfStream.GetHandle(), sdfStream.GetPosition());
chunk->Flags |= FlaxChunkFlags::KeepInMemory; // Prevent GC-ing chunk data so it will be properly saved
}
#endif
return false;
}
void Model::SetSDF(const SDFData& sdf)
{
ScopeLock lock(Locker);
if (SDF.Texture == sdf.Texture)
return;
SAFE_DELETE_GPU_RESOURCE(SDF.Texture);
SDF = sdf;
ReleaseChunk(15);
}
bool Model::Init(const Span<int32>& 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;
SAFE_DELETE_GPU_RESOURCE(SDF.Texture);
// Setup LODs
LODs.Resize(meshesCountPerLod.Length());
_initialized = true;
// Setup meshes
for (int32 lodIndex = 0; lodIndex < meshesCountPerLod.Length(); lodIndex++)
{
auto& lod = LODs[lodIndex];
lod.Link(this, 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].Link(this, lodIndex, meshIndex);
}
}
// Update resource residency
_loadedLODs = meshesCountPerLod.Length();
ResidencyChanged();
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(MODEL_HEADER_VERSION == 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(MODEL_HEADER_VERSION == 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);
// 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 Model::GetLODsCount() const
{
return LODs.Count();
}
const ModelLODBase* Model::GetLOD(int32 lodIndex) const
{
CHECK_RETURN(LODs.IsValidIndex(lodIndex), nullptr);
return &LODs.Get()[lodIndex];
}
ModelLODBase* Model::GetLOD(int32 lodIndex)
{
CHECK_RETURN(LODs.IsValidIndex(lodIndex), nullptr);
return &LODs.Get()[lodIndex];
}
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
{
LODs[lodIndex].GetMeshes(meshes);
}
void Model::GetMeshes(Array<MeshBase*>& meshes, int32 lodIndex)
{
LODs[lodIndex].GetMeshes(meshes);
}
void Model::InitAsVirtual()
{
// Init with a single LOD and one mesh
int32 meshesCount = 1;
Init(ToSpan(&meshesCount, 1));
// Base
BinaryAsset::InitAsVirtual();
}
int32 Model::GetMaxResidency() const
{
return LODs.Count();
}
int32 Model::GetAllocatedResidency() const
{
return LODs.Count();
}
Asset::LoadResult Model::load()
{
// Get header chunk
auto chunk0 = GetChunk(0);
if (chunk0 == nullptr || chunk0->IsMissing())
return LoadResult::MissingDataChunk;
MemoryReadStream headerStream(chunk0->Get(), chunk0->Size());
// Load asset data (anything but mesh contents that use streaming)
byte headerVersion;
if (LoadHeader(headerStream, headerVersion))
return LoadResult::InvalidData;
// Load SDF
auto chunk15 = GetChunk(15);
if (chunk15 && chunk15->IsLoaded() && EnableModelSDF == 1)
{
PROFILE_CPU_NAMED("SDF");
MemoryReadStream sdfStream(chunk15->Get(), chunk15->Size());
int32 version;
sdfStream.Read(version);
switch (version)
{
case 1:
{
ModelSDFHeader data;
sdfStream.ReadBytes(&data, sizeof(data));
if (!SDF.Texture)
{
String name;
#if !BUILD_RELEASE
name = GetPath() + TEXT(".SDF");
#endif
SDF.Texture = GPUDevice::Instance->CreateTexture(name);
}
if (SDF.Texture->Init(GPUTextureDescription::New3D(data.Width, data.Height, data.Depth, data.Format, GPUTextureFlags::ShaderResource, data.MipLevels)))
return LoadResult::Failed;
SDF.LocalToUVWMul = data.LocalToUVWMul;
SDF.LocalToUVWAdd = data.LocalToUVWAdd;
SDF.WorldUnitsPerVoxel = data.WorldUnitsPerVoxel;
SDF.MaxDistance = data.MaxDistance;
SDF.LocalBoundsMin = data.LocalBoundsMin;
SDF.LocalBoundsMax = data.LocalBoundsMax;
SDF.ResolutionScale = data.ResolutionScale;
SDF.LOD = data.LOD;
for (int32 mipLevel = 0; mipLevel < data.MipLevels; mipLevel++)
{
ModelSDFMip mipData;
sdfStream.ReadBytes(&mipData, sizeof(mipData));
void* mipBytes = sdfStream.Move(mipData.SlicePitch);
auto task = ::New<StreamModelSDFTask>(this, SDF.Texture, Span<byte>((byte*)mipBytes, mipData.SlicePitch), mipData.MipIndex, mipData.RowPitch, mipData.SlicePitch);
task->Start();
}
break;
}
default:
LOG(Warning, "Unknown SDF data version {0} in {1}", version, ToString());
break;
}
}
#if !BUILD_RELEASE
// Validate LODs
for (int32 lodIndex = 1; lodIndex < LODs.Count(); lodIndex++)
{
const auto prevSS = LODs[lodIndex - 1].ScreenSize;
const auto thisSS = LODs[lodIndex].ScreenSize;
if (prevSS <= thisSS)
{
LOG(Warning, "Model LOD {0} has invalid screen size compared to LOD {1} (asset: {2})", lodIndex, lodIndex - 1, ToString());
}
}
#endif
// Request resource streaming
StartStreaming(true);
return LoadResult::Ok;
}
void Model::unload(bool isReloading)
{
ModelBase::unload(isReloading);
// Cleanup
SAFE_DELETE_GPU_RESOURCE(SDF.Texture);
LODs.Clear();
}
AssetChunksFlag Model::getChunksToPreload() const
{
// Note: we don't preload any LODs here because it's done by the Streaming Manager
return GET_CHUNK_FLAG(0) | GET_CHUNK_FLAG(15);
}
bool ModelLOD::Intersects(const Ray& ray, const Matrix& world, Real& distance, Vector3& normal, Mesh** mesh)
{
bool result = false;
Real closest = MAX_Real;
Vector3 closestNormal = Vector3::Up;
for (int32 i = 0; i < Meshes.Count(); i++)
{
Real dst;
Vector3 nrm;
if (Meshes[i].Intersects(ray, world, dst, nrm) && dst < closest)
{
result = true;
*mesh = &Meshes[i];
closest = dst;
closestNormal = nrm;
}
}
distance = closest;
normal = closestNormal;
return result;
}
bool ModelLOD::Intersects(const Ray& ray, const Transform& transform, Real& distance, Vector3& normal, Mesh** mesh)
{
bool result = false;
Real closest = MAX_Real;
Vector3 closestNormal = Vector3::Up;
for (int32 i = 0; i < Meshes.Count(); i++)
{
Real dst;
Vector3 nrm;
if (Meshes[i].Intersects(ray, transform, dst, nrm) && dst < closest)
{
result = true;
*mesh = &Meshes[i];
closest = dst;
closestNormal = nrm;
}
}
distance = closest;
normal = closestNormal;
return result;
}
int32 ModelLOD::GetMeshesCount() const
{
return Meshes.Count();
}
const MeshBase* ModelLOD::GetMesh(int32 index) const
{
return Meshes.Get() + index;
}
MeshBase* ModelLOD::GetMesh(int32 index)
{
return Meshes.Get() + index;
}
void ModelLOD::GetMeshes(Array<MeshBase*>& meshes)
{
meshes.Resize(Meshes.Count());
for (int32 meshIndex = 0; meshIndex < Meshes.Count(); meshIndex++)
meshes[meshIndex] = &Meshes.Get()[meshIndex];
}
void ModelLOD::GetMeshes(Array<const MeshBase*>& meshes) const
{
meshes.Resize(Meshes.Count());
for (int32 meshIndex = 0; meshIndex < Meshes.Count(); meshIndex++)
meshes[meshIndex] = &Meshes.Get()[meshIndex];
}