diff --git a/Flax.sln.DotSettings b/Flax.sln.DotSettings index 4c0ea9564..007b8e5a2 100644 --- a/Flax.sln.DotSettings +++ b/Flax.sln.DotSettings @@ -360,6 +360,7 @@ True True True + True True True True diff --git a/Source/Engine/Content/Assets/Model.cpp b/Source/Engine/Content/Assets/Model.cpp index d41d92f16..b237f8a0e 100644 --- a/Source/Engine/Content/Assets/Model.cpp +++ b/Source/Engine/Content/Assets/Model.cpp @@ -2,18 +2,27 @@ #include "Model.h" #include "Engine/Core/Log.h" +#include "Engine/Core/Math/Int3.h" +#include "Engine/Core/RandomStream.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/Core/Math/Int2.h" +#include "Engine/Debug/DebugDraw.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/Async/GPUTask.h" +#include "Engine/Graphics/Textures/GPUTexture.h" +#include "Engine/Profiler/ProfilerCPU.h" #include "Engine/Renderer/DrawCall.h" +#include "Engine/Threading/JobSystem.h" #include "Engine/Threading/Threading.h" +#include "Engine/Tools/ModelTool/MeshAccelerationStructure.h" #if GPU_ENABLE_ASYNC_RESOURCES_CREATION #include "Engine/Threading/ThreadPoolTask.h" #define STREAM_TASK_BASE ThreadPoolTask @@ -37,13 +46,11 @@ REGISTER_BINARY_ASSET_ABSTRACT(ModelBase, "FlaxEngine.ModelBase"); class StreamModelLODTask : public STREAM_TASK_BASE { private: - WeakAssetReference _asset; int32 _lodIndex; FlaxStorage::LockData _dataLock; public: - /// /// Init /// @@ -57,7 +64,6 @@ public: } public: - // [ThreadPoolTask] bool HasReference(Object* resource) const override { @@ -65,7 +71,6 @@ public: } protected: - // [ThreadPoolTask] bool Run() override { @@ -229,7 +234,7 @@ void Model::Draw(const RenderContext& renderContext, const Mesh::DrawInfo& info) info.DrawState->PrevLOD = lodIndex; } } - // Check if there was a gap between frames in drawing this model instance + // Check if there was a gap between frames in drawing this model instance else if (modelFrame < frame || info.DrawState->PrevLOD == -1) { // Reset state @@ -560,6 +565,192 @@ bool Model::Save(bool withMeshDataFromGpu, const StringView& path) #endif +bool Model::GenerateSDF(float resolutionScale, int32 lodIndex) +{ + if (!HasAnyLODInitialized()) + return true; + PROFILE_CPU(); + auto startTime = Platform::GetTimeSeconds(); + ScopeLock lock(Locker); + + // Setup SDF texture properties + lodIndex = Math::Clamp(lodIndex, HighestResidentLODIndex(), LODs.Count() - 1); + auto& lod = LODs[lodIndex]; + BoundingBox bounds = lod.GetBox(); + Vector3 size = bounds.GetSize(); + SDF.WorldUnitsPerVoxel = 10 / Math::Max(resolutionScale, 0.0001f); + Int3 resolution(Vector3::Ceil(Vector3::Clamp(size / SDF.WorldUnitsPerVoxel, 4, 256))); + Vector3 uvwToLocalMul = size; + Vector3 uvwToLocalAdd = bounds.Minimum; + SDF.LocalToUVWMul = Vector3::One / uvwToLocalMul; + SDF.LocalToUVWAdd = -uvwToLocalAdd / uvwToLocalMul; + SDF.MaxDistance = size.MaxValue(); + SDF.LocalBoundsMin = bounds.Minimum; + SDF.LocalBoundsMax = bounds.Maximum; + // TODO: maybe apply 1 voxel margin around the geometry? + const int32 maxMips = 3; + const int32 mipCount = Math::Min(MipLevelsCount(resolution.X, resolution.Y, resolution.Z, true), maxMips); + if (!SDF.Texture) + SDF.Texture = GPUTexture::New(); + // TODO: use 8bit format for smaller SDF textures (eg. res<100) + if (SDF.Texture->Init(GPUTextureDescription::New3D(resolution.X, resolution.Y, resolution.Z, PixelFormat::R16_Float, GPUTextureFlags::ShaderResource | GPUTextureFlags::UnorderedAccess, mipCount))) + { + SAFE_DELETE_GPU_RESOURCE(SDF.Texture); + return true; + } + + // TODO: support GPU to generate model SDF on-the-fly (if called during rendering) + + // Setup acceleration structure for fast ray tracing the mesh triangles + MeshAccelerationStructure scene; + scene.Add(this, lodIndex); + scene.BuildBVH(); + + // Allocate memory for the distant field + Array voxels; + voxels.Resize(resolution.X * resolution.Y * resolution.Z); + Vector3 xyzToLocalMul = uvwToLocalMul / Vector3(resolution); + Vector3 xyzToLocalAdd = uvwToLocalAdd; + + // TODO: use optimized sparse storage for SDF data as hierarchical bricks + // https://graphics.pixar.com/library/IrradianceAtlas/paper.pdf + // http://maverick.inria.fr/Membres/Cyril.Crassin/thesis/CCrassinThesis_EN_Web.pdf + // http://ramakarl.com/pdfs/2016_Hoetzlein_GVDB.pdf + // https://www.cse.chalmers.se/~uffe/HighResolutionSparseVoxelDAGs.pdf + // then use R8 format and brick size of 8x8x8 + + // Brute-force for each voxel to calculate distance to the closest triangle with point query and distance sign by raycasting around the voxel + const int32 sampleCount = 12; + Array sampleDirections; + sampleDirections.Resize(sampleCount); + { + RandomStream rand; + sampleDirections.Get()[0] = Vector3::Up; + sampleDirections.Get()[1] = Vector3::Down; + sampleDirections.Get()[2] = Vector3::Left; + sampleDirections.Get()[3] = Vector3::Right; + sampleDirections.Get()[4] = Vector3::Forward; + sampleDirections.Get()[5] = Vector3::Backward; + for (int32 i = 6; i < sampleCount; i++) + sampleDirections.Get()[i] = rand.GetUnitVector(); + } + Function sdfJob = [this, &resolution, &sampleDirections, &scene, &voxels, &xyzToLocalMul, &xyzToLocalAdd](int32 z) + { + PROFILE_CPU_NAMED("Model SDF Job"); + const float encodeScale = 1.0f / SDF.MaxDistance; + float hitDistance; + Vector3 hitNormal, hitPoint; + Triangle hitTriangle; + const int32 zAddress = resolution.Y * resolution.X * z; + for (int32 y = 0; y < resolution.Y; y++) + { + const int32 yAddress = resolution.X * y + zAddress; + for (int32 x = 0; x < resolution.X; x++) + { + float minDistance = SDF.MaxDistance; + Vector3 voxelPos = Vector3((float)x, (float)y, (float)z) * xyzToLocalMul + xyzToLocalAdd; + + // Point query to find the distance to the closest surface + scene.PointQuery(voxelPos, minDistance, hitPoint, hitTriangle); + + // Raycast samples around voxel to count triangle backfaces hit + int32 hitBackCount = 0, hitCount = 0; + for (int32 sample = 0; sample < sampleDirections.Count(); sample++) + { + Ray sampleRay(voxelPos, sampleDirections[sample]); + if (scene.RayCast(sampleRay, hitDistance, hitNormal, hitTriangle)) + { + hitCount++; + const bool backHit = Vector3::Dot(sampleRay.Direction, hitTriangle.GetNormal()) > 0; + if (backHit) + hitBackCount++; + } + } + + float distance = minDistance; + // TODO: surface thickness threshold? shift reduce distance for all voxels by something like 0.01 to enlarge thin geometry + //if ((float)hitBackCount > )hitCount * 0.3f && hitCount != 0) + if ((float)hitBackCount > (float)sampleDirections.Count() * 0.6f && hitCount != 0) + { + // Voxel is inside the geometry so turn it into negative distance to the surface + distance *= -1; + } + const int32 xAddress = x + yAddress; + voxels.Get()[xAddress] = Float16Compressor::Compress(distance * encodeScale); + } + } + }; + JobSystem::Execute(sdfJob, resolution.Z); + + // Upload data to the GPU + BytesContainer data; + data.Link((byte*)voxels.Get(), voxels.Count() * sizeof(Half)); + auto task = SDF.Texture->UploadMipMapAsync(data, 0, resolution.X * sizeof(Half), data.Length(), true); + if (task) + task->Start(); + + // Generate mip maps + Array voxelsMip; + for (int32 mipLevel = 1; mipLevel < mipCount; mipLevel++) + { + Int3 resolutionMip = Int3::Max(resolution / 2, Int3::One); + voxelsMip.Resize(resolutionMip.X * resolutionMip.Y * resolutionMip.Z); + + // Downscale mip + Function mipJob = [this, &voxelsMip, &voxels, &resolution, &resolutionMip](int32 z) + { + PROFILE_CPU_NAMED("Model SDF Mip Job"); + const float encodeScale = 1.0f / SDF.MaxDistance; + const float decodeScale = SDF.MaxDistance; + const int32 zAddress = resolutionMip.Y * resolutionMip.X * z; + for (int32 y = 0; y < resolutionMip.Y; y++) + { + const int32 yAddress = resolutionMip.X * y + zAddress; + for (int32 x = 0; x < resolutionMip.X; x++) + { + // Linear box filter around the voxel + float distance = 0; + for (int32 dz = 0; dz < 2; dz++) + { + const int32 dzAddress = (z * 2 + dz) * (resolution.Y * resolution.X); + for (int32 dy = 0; dy < 2; dy++) + { + const int32 dyAddress = (y * 2 + dy) * (resolution.X) + dzAddress; + for (int32 dx = 0; dx < 2; dx++) + { + const int32 dxAddress = (x * 2 + dx) + dyAddress; + const float d = Float16Compressor::Decompress(voxels.Get()[dxAddress]) * decodeScale; + distance += d; + } + } + } + distance *= 1.0f / 8.0f; + + const int32 xAddress = x + yAddress; + voxelsMip.Get()[xAddress] = Float16Compressor::Compress(distance * encodeScale); + } + } + }; + JobSystem::Execute(mipJob, resolutionMip.Z); + + // Upload to the GPU + data.Link((byte*)voxelsMip.Get(), voxelsMip.Count() * sizeof(Half)); + task = SDF.Texture->UploadMipMapAsync(data, mipLevel, resolutionMip.X * sizeof(Half), data.Length(), true); + if (task) + task->Start(); + + // Go down + voxelsMip.Swap(voxels); + resolution = resolutionMip; + } + +#if !BUILD_RELEASE + auto endTime = Platform::GetTimeSeconds(); + LOG(Info, "Generated SDF {}x{}x{} ({} kB) in {}ms for {}", resolution.X, resolution.Y, resolution.Z, SDF.Texture->GetMemoryUsage() / 1024, (int32)((endTime - startTime) * 1000.0), GetPath()); +#endif + return false; +} + bool Model::Init(const Span& meshesCountPerLod) { if (meshesCountPerLod.IsInvalid() || meshesCountPerLod.Length() > MODEL_MAX_LODS) @@ -574,6 +765,7 @@ bool Model::Init(const Span& meshesCountPerLod) // Setup MaterialSlots.Resize(1); MinScreenSize = 0.0f; + SAFE_DELETE_GPU_RESOURCE(SDF.Texture); // Setup LODs for (int32 lodIndex = 0; lodIndex < LODs.Count(); lodIndex++) @@ -840,6 +1032,7 @@ void Model::unload(bool isReloading) } // Cleanup + SAFE_DELETE_GPU_RESOURCE(SDF.Texture); MaterialSlots.Resize(0); for (int32 i = 0; i < LODs.Count(); i++) LODs[i].Dispose(); diff --git a/Source/Engine/Content/Assets/Model.h b/Source/Engine/Content/Assets/Model.h index 6232147ba..a4d6711eb 100644 --- a/Source/Engine/Content/Assets/Model.h +++ b/Source/Engine/Content/Assets/Model.h @@ -28,6 +28,11 @@ public: /// API_FIELD(ReadOnly) Array> LODs; + /// + /// The generated Sign Distant Field (SDF) for this model (merged all meshes). Use GenerateSDF to update it. + /// + API_FIELD(ReadOnly) SDFData SDF; + public: /// @@ -200,6 +205,15 @@ public: API_FUNCTION() bool Save(bool withMeshDataFromGpu = false, const StringView& path = StringView::Empty); #endif + + /// + /// Generates the Sign Distant Field for this model. + /// + /// Can be called in async in case of SDF generation on a CPU (assuming model is not during rendering). + /// The SDF texture resolution scale. Use higher values for more precise data but with significant performance and memory overhead. + /// The index of the LOD to use for the SDF building. + /// True if failed, otherwise false. + API_FUNCTION() bool GenerateSDF(float resolutionScale = 1.0f, int32 lodIndex = 6); private: diff --git a/Source/Engine/Content/Assets/ModelBase.h b/Source/Engine/Content/Assets/ModelBase.h index c194fd38e..6f2065abf 100644 --- a/Source/Engine/Content/Assets/ModelBase.h +++ b/Source/Engine/Content/Assets/ModelBase.h @@ -24,9 +24,52 @@ class MeshBase; /// API_CLASS(Abstract, NoSpawn) class FLAXENGINE_API ModelBase : public BinaryAsset, public StreamableResource { -DECLARE_ASSET_HEADER(ModelBase); -protected: + DECLARE_ASSET_HEADER(ModelBase); +public: + /// + /// The Sign Distant Field (SDF) data for the model. + /// + API_STRUCT() struct SDFData + { + DECLARE_SCRIPTING_TYPE_MINIMAL(SDFData); + /// + /// The SDF volume texture (merged all meshes). + /// + API_FIELD() GPUTexture* Texture = nullptr; + + /// + /// The transformation scale from model local-space to the generated SDF texture space (local-space -> uv). + /// + API_FIELD() Vector3 LocalToUVWMul; + + /// + /// Amount of world-units per SDF texture voxel. + /// + API_FIELD() float WorldUnitsPerVoxel; + + /// + /// The transformation offset from model local-space to the generated SDF texture space (local-space -> uv). + /// + API_FIELD() Vector3 LocalToUVWAdd; + + /// + /// The maximum distance stored in the SDF texture. Used to rescale normalized SDF into world-units (in model local space). + /// + API_FIELD() float MaxDistance; + + /// + /// The bounding box of the SDF texture in the model local-space. + /// + API_FIELD() Vector3 LocalBoundsMin; + + /// + /// The bounding box of the SDF texture in the model local-space. + /// + API_FIELD() Vector3 LocalBoundsMax; + }; + +protected: explicit ModelBase(const SpawnParams& params, const AssetInfo* info, StreamingGroup* group) : BinaryAsset(params, info) , StreamableResource(group) @@ -34,7 +77,6 @@ protected: } public: - /// /// The minimum screen size to draw this model (the bottom limit). Used to cull small models. Set to 0 to disable this feature. /// diff --git a/Source/Engine/Content/Content.Build.cs b/Source/Engine/Content/Content.Build.cs index 36608e3a8..501856f02 100644 --- a/Source/Engine/Content/Content.Build.cs +++ b/Source/Engine/Content/Content.Build.cs @@ -18,6 +18,7 @@ public class Content : EngineModule options.PrivateDependencies.Add("lz4"); options.PrivateDependencies.Add("AudioTool"); options.PrivateDependencies.Add("TextureTool"); + options.PrivateDependencies.Add("ModelTool"); options.PrivateDependencies.Add("Particles"); if (options.Target.IsEditor)