Merge remote-tracking branch 'upstream/master'

This commit is contained in:
ExMatics HydrogenC
2023-12-19 12:33:13 +08:00
183 changed files with 6019 additions and 3547 deletions

View File

@@ -144,7 +144,10 @@ BehaviorKnowledge::~BehaviorKnowledge()
void BehaviorKnowledge::InitMemory(BehaviorTree* tree)
{
ASSERT_LOW_LAYER(!Tree && tree);
if (Tree)
FreeMemory();
if (!tree)
return;
Tree = tree;
Blackboard = Variant::NewValue(tree->Graph.Root->BlackboardType);
RelevantNodes.Resize(tree->Graph.NodesCount, false);

View File

@@ -624,8 +624,10 @@ void BehaviorTreeLoopDecorator::PostUpdate(const BehaviorUpdateContext& context,
if (result == BehaviorUpdateResult::Success)
{
auto state = GetState<State>(context.Memory);
state->Loops--;
if (state->Loops > 0)
if (!InfiniteLoop)
state->Loops--;
if (state->Loops > 0 || InfiniteLoop)
{
// Keep running in a loop but reset node's state (preserve self state)
result = BehaviorUpdateResult::Running;

View File

@@ -305,12 +305,16 @@ API_CLASS(Sealed) class FLAXENGINE_API BehaviorTreeLoopDecorator : public Behavi
DECLARE_SCRIPTING_TYPE_WITH_CONSTRUCTOR_IMPL(BehaviorTreeLoopDecorator, BehaviorTreeDecorator);
API_AUTO_SERIALIZATION();
// Amount of times to execute the node. Unused if LoopCountSelector is used.
API_FIELD(Attributes="EditorOrder(10), Limit(0)")
// Is the loop infinite (until failed)?
API_FIELD(Attributes = "EditorOrder(10)")
bool InfiniteLoop = false;
// Amount of times to execute the node. Unused if LoopCountSelector is used or if InfiniteLoop is used.
API_FIELD(Attributes="EditorOrder(20), Limit(0), VisibleIf(nameof(InfiniteLoop), true)")
int32 LoopCount = 3;
// Amount of times to execute the node from behavior's knowledge (blackboard, goal or sensor). If set, overrides LoopCount.
API_FIELD(Attributes="EditorOrder(20)")
// Amount of times to execute the node from behavior's knowledge (blackboard, goal or sensor). If set, overrides LoopCount. Unused if InfiniteLoop is used.
API_FIELD(Attributes="EditorOrder(30), VisibleIf(nameof(InfiniteLoop), true)")
BehaviorKnowledgeSelector<int32> LoopCountSelector;
public:

View File

@@ -17,6 +17,11 @@ API_CLASS(Abstract) class FLAXENGINE_API AnimEvent : public SerializableScriptin
{
DECLARE_SCRIPTING_TYPE(AnimEvent);
/// <summary>
/// Indicates whether the event can be executed in async from a thread that updates the animated model. Otherwise, event execution will be delayed until the sync point of the animated model and called from the main thread. Async events need to precisely handle data access, especially when it comes to editing scene objects with multi-threading.
/// </summary>
API_FIELD(Attributes="HideInEditor, NoSerialize") bool Async = false;
#if USE_EDITOR
/// <summary>
/// Event display color in the Editor.

View File

@@ -98,7 +98,6 @@ public:
/// </summary>
struct AnimationData
{
public:
/// <summary>
/// The duration of the animation (in frames).
/// </summary>
@@ -114,6 +113,11 @@ public:
/// </summary>
bool EnableRootMotion = false;
/// <summary>
/// The animation name.
/// </summary>
String Name;
/// <summary>
/// The custom node name to be used as a root motion source. If not specified the actual root node will be used.
/// </summary>
@@ -131,14 +135,14 @@ public:
FORCE_INLINE float GetLength() const
{
#if BUILD_DEBUG
ASSERT(FramesPerSecond != 0);
ASSERT(FramesPerSecond > ZeroTolerance);
#endif
return static_cast<float>(Duration / FramesPerSecond);
}
uint64 GetMemoryUsage() const
{
uint64 result = RootNodeName.Length() * sizeof(Char) + Channels.Capacity() * sizeof(NodeAnimationData);
uint64 result = (Name.Length() + RootNodeName.Length()) * sizeof(Char) + Channels.Capacity() * sizeof(NodeAnimationData);
for (const auto& e : Channels)
result += e.GetMemoryUsage();
return result;
@@ -151,9 +155,7 @@ public:
{
int32 result = 0;
for (int32 i = 0; i < Channels.Count(); i++)
{
result += Channels[i].GetKeyframesCount();
}
return result;
}
@@ -166,6 +168,7 @@ public:
::Swap(Duration, other.Duration);
::Swap(FramesPerSecond, other.FramesPerSecond);
::Swap(EnableRootMotion, other.EnableRootMotion);
::Swap(Name, other.Name);
::Swap(RootNodeName, other.RootNodeName);
Channels.Swap(other.Channels);
}
@@ -175,6 +178,7 @@ public:
/// </summary>
void Dispose()
{
Name.Clear();
Duration = 0.0;
FramesPerSecond = 0.0;
RootNodeName.Clear();

View File

@@ -146,6 +146,7 @@ void AnimationsSystem::PostExecute(TaskGraph* graph)
auto animatedModel = AnimationManagerInstance.UpdateList[index];
if (CanUpdateModel(animatedModel))
{
animatedModel->GraphInstance.InvokeAnimEvents();
animatedModel->OnAnimationUpdated_Sync();
}
}

View File

@@ -38,22 +38,16 @@ void AnimGraphImpulse::SetNodeModelTransformation(SkeletonData& skeleton, int32
void AnimGraphInstanceData::Clear()
{
Version = 0;
LastUpdateTime = -1;
CurrentFrame = 0;
RootTransform = Transform::Identity;
RootMotion = Transform::Identity;
ClearState();
Parameters.Resize(0);
State.Resize(0);
NodesPose.Resize(0);
Slots.Resize(0);
for (const auto& e : Events)
((AnimContinuousEvent*)e.Instance)->OnEnd((AnimatedModel*)Object, e.Anim, 0.0f, 0.0f);
Events.Resize(0);
}
void AnimGraphInstanceData::ClearState()
{
for (const auto& e : ActiveEvents)
OutgoingEvents.Add(e.End((AnimatedModel*)Object));
ActiveEvents.Clear();
InvokeAnimEvents();
Version = 0;
LastUpdateTime = -1;
CurrentFrame = 0;
@@ -62,9 +56,6 @@ void AnimGraphInstanceData::ClearState()
State.Resize(0);
NodesPose.Resize(0);
Slots.Clear();
for (const auto& e : Events)
((AnimContinuousEvent*)e.Instance)->OnEnd((AnimatedModel*)Object, e.Anim, 0.0f, 0.0f);
Events.Clear();
}
void AnimGraphInstanceData::Invalidate()
@@ -73,6 +64,43 @@ void AnimGraphInstanceData::Invalidate()
CurrentFrame = 0;
}
void AnimGraphInstanceData::InvokeAnimEvents()
{
const bool all = IsInMainThread();
for (int32 i = 0; i < OutgoingEvents.Count(); i++)
{
const OutgoingEvent e = OutgoingEvents[i];
if (all || e.Instance->Async)
{
OutgoingEvents.RemoveAtKeepOrder(i);
switch (e.Type)
{
case OutgoingEvent::OnEvent:
e.Instance->OnEvent(e.Actor, e.Anim, e.Time, e.DeltaTime);
break;
case OutgoingEvent::OnBegin:
((AnimContinuousEvent*)e.Instance)->OnBegin(e.Actor, e.Anim, e.Time, e.DeltaTime);
break;
case OutgoingEvent::OnEnd:
((AnimContinuousEvent*)e.Instance)->OnEnd(e.Actor, e.Anim, e.Time, e.DeltaTime);
break;
}
}
}
}
AnimGraphInstanceData::OutgoingEvent AnimGraphInstanceData::ActiveEvent::End(AnimatedModel* actor) const
{
OutgoingEvent out;
out.Instance = Instance;
out.Actor = actor;
out.Anim = Anim;
out.Time = 0.0f;
out.DeltaTime = 0.0f;
out.Type = OutgoingEvent::OnEnd;
return out;
}
AnimGraphImpulse* AnimGraphNode::GetNodes(AnimGraphExecutor* executor)
{
auto& context = AnimGraphExecutor::Context.Get();
@@ -208,7 +236,7 @@ void AnimGraphExecutor::Update(AnimGraphInstanceData& data, float dt)
// Initialize buckets
ResetBuckets(context, &_graph);
}
for (auto& e : data.Events)
for (auto& e : data.ActiveEvents)
e.Hit = false;
// Init empty nodes data
@@ -240,16 +268,17 @@ void AnimGraphExecutor::Update(AnimGraphInstanceData& data, float dt)
if (animResult == nullptr)
animResult = GetEmptyNodes();
}
if (data.Events.Count() != 0)
if (data.ActiveEvents.Count() != 0)
{
ANIM_GRAPH_PROFILE_EVENT("Events");
for (int32 i = data.Events.Count() - 1; i >= 0; i--)
for (int32 i = data.ActiveEvents.Count() - 1; i >= 0; i--)
{
const auto& e = data.Events[i];
const auto& e = data.ActiveEvents[i];
if (!e.Hit)
{
((AnimContinuousEvent*)e.Instance)->OnEnd((AnimatedModel*)context.Data->Object, e.Anim, 0.0f, 0.0f);
data.Events.RemoveAt(i);
// Remove active event that was not hit during this frame (eg. animation using it was not used in blending)
data.OutgoingEvents.Add(e.End((AnimatedModel*)context.Data->Object));
data.ActiveEvents.RemoveAt(i);
}
}
}
@@ -284,7 +313,6 @@ void AnimGraphExecutor::Update(AnimGraphInstanceData& data, float dt)
RetargetSkeletonNode(sourceSkeleton, targetSkeleton, mapping, node, i);
targetNodes[i] = node;
}
}
}
@@ -319,6 +347,9 @@ void AnimGraphExecutor::Update(AnimGraphInstanceData& data, float dt)
data.RootMotion = animResult->RootMotion;
}
// Invoke any async anim events
context.Data->InvokeAnimEvents();
// Cleanup
context.Data = nullptr;
}

View File

@@ -23,6 +23,9 @@ class AnimSubGraph;
class AnimGraphBase;
class AnimGraphNode;
class AnimGraphExecutor;
class AnimatedModel;
class AnimEvent;
class AnimContinuousEvent;
class SkinnedModel;
class SkeletonData;
@@ -349,16 +352,40 @@ public:
/// </summary>
void Invalidate();
/// <summary>
/// Invokes any outgoing AnimEvent and AnimContinuousEvent collected during the last animation update. When called from non-main thread only Async events will be invoked.
/// </summary>
void InvokeAnimEvents();
private:
struct Event
struct OutgoingEvent
{
enum Types
{
OnEvent,
OnBegin,
OnEnd,
};
AnimEvent* Instance;
AnimatedModel* Actor;
Animation* Anim;
float Time, DeltaTime;
Types Type;
};
struct ActiveEvent
{
AnimContinuousEvent* Instance;
Animation* Anim;
AnimGraphNode* Node;
bool Hit;
OutgoingEvent End(AnimatedModel* actor) const;
};
Array<Event, InlinedAllocation<8>> Events;
Array<ActiveEvent, InlinedAllocation<8>> ActiveEvents;
Array<OutgoingEvent, InlinedAllocation<8>> OutgoingEvents;
};
/// <summary>
@@ -441,7 +468,7 @@ public:
/// The invalid transition valid used in Transitions to indicate invalid transition linkage.
/// </summary>
const static uint16 InvalidTransitionIndex = MAX_uint16;
/// <summary>
/// The outgoing transitions from this state to the other states. Each array item contains index of the transition data from the state node graph transitions cache. Value InvalidTransitionIndex is used for last transition to indicate the transitions amount.
/// </summary>

View File

@@ -100,48 +100,50 @@ void AnimGraphExecutor::ProcessAnimEvents(AnimGraphNode* node, bool loop, float
if (!k.Value.Instance)
continue;
const float duration = k.Value.Duration > 1 ? k.Value.Duration : 0.0f;
#define ADD_OUTGOING_EVENT(type) context.Data->OutgoingEvents.Add({ k.Value.Instance, (AnimatedModel*)context.Data->Object, anim, eventTime, eventDeltaTime, AnimGraphInstanceData::OutgoingEvent::type })
if (k.Time <= eventTimeMax && eventTimeMin <= k.Time + duration)
{
int32 stateIndex = -1;
if (duration > 1)
{
// Begin for continuous event
for (stateIndex = 0; stateIndex < context.Data->Events.Count(); stateIndex++)
for (stateIndex = 0; stateIndex < context.Data->ActiveEvents.Count(); stateIndex++)
{
const auto& e = context.Data->Events[stateIndex];
const auto& e = context.Data->ActiveEvents[stateIndex];
if (e.Instance == k.Value.Instance && e.Node == node)
break;
}
if (stateIndex == context.Data->Events.Count())
if (stateIndex == context.Data->ActiveEvents.Count())
{
auto& e = context.Data->Events.AddOne();
e.Instance = k.Value.Instance;
ASSERT(k.Value.Instance->Is<AnimContinuousEvent>());
auto& e = context.Data->ActiveEvents.AddOne();
e.Instance = (AnimContinuousEvent*)k.Value.Instance;
e.Anim = anim;
e.Node = node;
ASSERT(k.Value.Instance->Is<AnimContinuousEvent>());
((AnimContinuousEvent*)k.Value.Instance)->OnBegin((AnimatedModel*)context.Data->Object, anim, eventTime, eventDeltaTime);
ADD_OUTGOING_EVENT(OnBegin);
}
}
// Event
k.Value.Instance->OnEvent((AnimatedModel*)context.Data->Object, anim, eventTime, eventDeltaTime);
ADD_OUTGOING_EVENT(OnEvent);
if (stateIndex != -1)
context.Data->Events[stateIndex].Hit = true;
context.Data->ActiveEvents[stateIndex].Hit = true;
}
else if (duration > 1)
{
// End for continuous event
for (int32 i = 0; i < context.Data->Events.Count(); i++)
for (int32 i = 0; i < context.Data->ActiveEvents.Count(); i++)
{
const auto& e = context.Data->Events[i];
const auto& e = context.Data->ActiveEvents[i];
if (e.Instance == k.Value.Instance && e.Node == node)
{
((AnimContinuousEvent*)k.Value.Instance)->OnEnd((AnimatedModel*)context.Data->Object, anim, eventTime, eventDeltaTime);
context.Data->Events.RemoveAt(i);
ADD_OUTGOING_EVENT(OnEnd);
context.Data->ActiveEvents.RemoveAt(i);
break;
}
}
}
#undef ADD_OUTGOING_EVENT
}
}
}
@@ -1068,20 +1070,26 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu
else
{
const auto nodes = node->GetNodes(this);
const auto nodesA = static_cast<AnimGraphImpulse*>(valueA.AsPointer);
const auto nodesB = static_cast<AnimGraphImpulse*>(valueB.AsPointer);
Transform t, tA, tB;
const auto basePoseNodes = static_cast<AnimGraphImpulse*>(valueA.AsPointer);
const auto blendPoseNodes = static_cast<AnimGraphImpulse*>(valueB.AsPointer);
const auto& refrenceNodes = _graph.BaseModel.Get()->GetNodes();
Transform t, basePoseTransform, blendPoseTransform, refrenceTransform;
for (int32 i = 0; i < nodes->Nodes.Count(); i++)
{
tA = nodesA->Nodes[i];
tB = nodesB->Nodes[i];
t.Translation = tA.Translation + tB.Translation;
t.Orientation = tA.Orientation * tB.Orientation;
t.Scale = tA.Scale * tB.Scale;
t.Orientation.Normalize();
Transform::Lerp(tA, t, alpha, nodes->Nodes[i]);
basePoseTransform = basePoseNodes->Nodes[i];
blendPoseTransform = blendPoseNodes->Nodes[i];
refrenceTransform = refrenceNodes[i].LocalTransform;
// base + (blend - refrence) = transform
t.Translation = basePoseTransform.Translation + (blendPoseTransform.Translation - refrenceTransform.Translation);
auto diff = Quaternion::Invert(refrenceTransform.Orientation) * blendPoseTransform.Orientation;
t.Orientation = basePoseTransform.Orientation * diff;
t.Scale = basePoseTransform.Scale + (blendPoseTransform.Scale - refrenceTransform.Scale);
//lerp base and transform
Transform::Lerp(basePoseTransform, t, alpha, nodes->Nodes[i]);
}
Transform::Lerp(nodesA->RootMotion, nodesA->RootMotion + nodesB->RootMotion, alpha, nodes->RootMotion);
Transform::Lerp(basePoseNodes->RootMotion, basePoseNodes->RootMotion + blendPoseNodes->RootMotion, alpha, nodes->RootMotion);
value = nodes;
}
}

View File

@@ -310,6 +310,10 @@ void Asset::ChangeID(const Guid& newId)
if (!IsVirtual())
CRASH;
// ID has to be unique
if (Content::GetAsset(newId) != nullptr)
CRASH;
const Guid oldId = _id;
ManagedScriptingObject::ChangeID(newId);
Content::onAssetChangeId(this, oldId, newId);
@@ -438,12 +442,15 @@ bool Asset::WaitForLoaded(double timeoutInMilliseconds) const
// Note: to reproduce this case just include material into material (use layering).
// So during loading first material it will wait for child materials loaded calling this function
const double timeoutInSeconds = timeoutInMilliseconds * 0.001;
const double startTime = Platform::GetTimeSeconds();
Task* task = loadingTask;
Array<ContentLoadTask*, InlinedAllocation<64>> localQueue;
while (!Engine::ShouldExit())
#define CHECK_CONDITIONS() (!Engine::ShouldExit() && (timeoutInSeconds <= 0.0 || Platform::GetTimeSeconds() - startTime < timeoutInSeconds))
do
{
// Try to execute content tasks
while (task->IsQueued() && !Engine::ShouldExit())
while (task->IsQueued() && CHECK_CONDITIONS())
{
// Dequeue task from the loading queue
ContentLoadTask* tmp;
@@ -494,7 +501,8 @@ bool Asset::WaitForLoaded(double timeoutInMilliseconds) const
break;
}
}
}
} while (CHECK_CONDITIONS());
#undef CHECK_CONDITIONS
}
else
{

View File

@@ -34,7 +34,6 @@
#define CHECK_INVALID_BUFFER(model, buffer) \
if (buffer->IsValidFor(model) == false) \
{ \
LOG(Warning, "Invalid Model Instance Buffer size {0} for Model {1}. It should be {2}. Manual update to proper size.", buffer->Count(), model->ToString(), model->MaterialSlots.Count()); \
buffer->Setup(model); \
}

View File

@@ -23,7 +23,6 @@
#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); \
}

View File

@@ -892,6 +892,11 @@ void VisualScriptExecutor::ProcessGroupFunction(Box* boxBase, Node* node, Value&
PrintStack(LogType::Error);
break;
}
if (boxBase->ID == 1)
{
value = instance;
break;
}
// TODO: check if instance is of event type (including inheritance)
// Add Visual Script method to the event bindings table

View File

@@ -230,6 +230,7 @@ bool Content::GetAssetInfo(const Guid& id, AssetInfo& info)
// Find asset in registry
if (Cache.FindAsset(id, info))
return true;
PROFILE_CPU();
// Locking injects some stalls but we need to make it safe (only one thread can pass though it at once)
ScopeLock lock(WorkspaceDiscoveryLocker);
@@ -276,6 +277,7 @@ bool Content::GetAssetInfo(const StringView& path, AssetInfo& info)
// Find asset in registry
if (Cache.FindAsset(path, info))
return true;
PROFILE_CPU();
const auto extension = FileSystem::GetExtension(path).ToLower();
@@ -538,6 +540,8 @@ void Content::DeleteAsset(Asset* asset)
void Content::DeleteAsset(const StringView& path)
{
PROFILE_CPU();
// Try to delete already loaded asset
Asset* asset = GetAsset(path);
if (asset != nullptr)
@@ -566,12 +570,12 @@ void Content::DeleteAsset(const StringView& path)
void Content::deleteFileSafety(const StringView& path, const Guid& id)
{
// Check if given id is invalid
if (!id.IsValid())
{
LOG(Warning, "Cannot remove file \'{0}\'. Given ID is invalid.", path);
return;
}
PROFILE_CPU();
// Ensure that file has the same ID (prevent from deleting different assets)
auto storage = ContentStorageManager::TryGetStorage(path);
@@ -678,6 +682,7 @@ bool Content::FastTmpAssetClone(const StringView& path, String& resultPath)
bool Content::CloneAssetFile(const StringView& dstPath, const StringView& srcPath, const Guid& dstId)
{
PROFILE_CPU();
ASSERT(FileSystem::AreFilePathsEqual(srcPath, dstPath) == false && dstId.IsValid());
LOG(Info, "Cloning asset \'{0}\' to \'{1}\'({2}).", srcPath, dstPath, dstId);
@@ -796,6 +801,7 @@ Asset* Content::CreateVirtualAsset(MClass* type)
Asset* Content::CreateVirtualAsset(const ScriptingTypeHandle& type)
{
PROFILE_CPU();
auto& assetType = type.GetType();
// Init mock asset info

View File

@@ -562,6 +562,7 @@ bool FlaxStorage::Reload()
{
if (!IsLoaded())
return false;
PROFILE_CPU();
OnReloading(this);
@@ -728,7 +729,11 @@ bool FlaxStorage::ChangeAssetID(Entry& e, const Guid& newId)
}
// Close file
CloseFileHandles();
if (CloseFileHandles())
{
LOG(Error, "Cannot close file access for '{}'", _path);
return true;
}
// Change ID
// TODO: here we could extend it and load assets from the storage and call asset ID change event to change references
@@ -776,6 +781,8 @@ FlaxChunk* FlaxStorage::AllocateChunk()
bool FlaxStorage::Create(const StringView& path, const AssetInitData* data, int32 dataCount, bool silentMode, const CustomData* customData)
{
PROFILE_CPU();
ZoneText(*path, path.Length());
LOG(Info, "Creating package at \'{0}\'. Silent Mode: {1}", path, silentMode);
// Prepare to have access to the file
@@ -1296,8 +1303,10 @@ FileReadStream* FlaxStorage::OpenFile()
return stream;
}
void FlaxStorage::CloseFileHandles()
bool FlaxStorage::CloseFileHandles()
{
PROFILE_CPU();
// Note: this is usually called by the content manager when this file is not used or on exit
// In those situations all the async tasks using this storage should be cancelled externally
@@ -1323,10 +1332,12 @@ void FlaxStorage::CloseFileHandles()
waitTime = 100;
while (Platform::AtomicRead(&_chunksLock) != 0 && waitTime-- > 0)
Platform::Sleep(1);
ASSERT(_chunksLock == 0);
if (Platform::AtomicRead(&_chunksLock) != 0)
return true; // Failed, someone is still accessing the file
// Close file handles (from all threads)
_file.DeleteAll();
return false;
}
void FlaxStorage::Dispose()
@@ -1335,7 +1346,10 @@ void FlaxStorage::Dispose()
return;
// Close file
CloseFileHandles();
if (CloseFileHandles())
{
LOG(Error, "Cannot close file access for '{}'", _path);
}
// Release data
_chunks.ClearDelete();

View File

@@ -405,7 +405,7 @@ public:
/// <summary>
/// Closes the file handles (it can be modified from the outside).
/// </summary>
void CloseFileHandles();
bool CloseFileHandles();
/// <summary>
/// Releases storage resources and closes handle to the file.

View File

@@ -15,7 +15,7 @@
#include "Engine/Platform/Platform.h"
#include "Engine/Engine/Globals.h"
#include "ImportTexture.h"
#include "ImportModelFile.h"
#include "ImportModel.h"
#include "ImportAudio.h"
#include "ImportShader.h"
#include "ImportFont.h"
@@ -165,20 +165,7 @@ bool CreateAssetContext::AllocateChunk(int32 index)
void CreateAssetContext::AddMeta(JsonWriter& writer) const
{
writer.JKEY("ImportPath");
if (AssetsImportingManager::UseImportPathRelative && !FileSystem::IsRelative(InputPath)
#if PLATFORM_WINDOWS
// Import path from other drive should be stored as absolute on Windows to prevent issues
&& InputPath.Length() > 2 && Globals::ProjectFolder.Length() > 2 && InputPath[0] == Globals::ProjectFolder[0]
#endif
)
{
const String relativePath = FileSystem::ConvertAbsolutePathToRelative(Globals::ProjectFolder, InputPath);
writer.String(relativePath);
}
else
{
writer.String(InputPath);
}
writer.String(AssetsImportingManager::GetImportPath(InputPath));
writer.JKEY("ImportUsername");
writer.String(Platform::GetUserName());
}
@@ -189,7 +176,12 @@ void CreateAssetContext::ApplyChanges()
auto storage = ContentStorageManager::TryGetStorage(TargetAssetPath);
if (storage && storage->IsLoaded())
{
storage->CloseFileHandles();
if (storage->CloseFileHandles())
{
LOG(Error, "Cannot close file access for '{}'", TargetAssetPath);
_applyChangesResult = CreateAssetResult::CannotSaveFile;
return;
}
}
// Move file
@@ -304,8 +296,24 @@ bool AssetsImportingManager::ImportIfEdited(const StringView& inputPath, const S
return false;
}
String AssetsImportingManager::GetImportPath(const String& path)
{
if (UseImportPathRelative && !FileSystem::IsRelative(path)
#if PLATFORM_WINDOWS
// Import path from other drive should be stored as absolute on Windows to prevent issues
&& path.Length() > 2 && Globals::ProjectFolder.Length() > 2 && path[0] == Globals::ProjectFolder[0]
#endif
)
{
return FileSystem::ConvertAbsolutePathToRelative(Globals::ProjectFolder, path);
}
return path;
}
bool AssetsImportingManager::Create(const Function<CreateAssetResult(CreateAssetContext&)>& callback, const StringView& inputPath, const StringView& outputPath, Guid& assetId, void* arg)
{
PROFILE_CPU();
ZoneText(*outputPath, outputPath.Length());
const auto startTime = Platform::GetTimeSeconds();
// Pick ID if not specified
@@ -369,7 +377,7 @@ bool AssetsImportingManager::Create(const Function<CreateAssetResult(CreateAsset
if (result == CreateAssetResult::Ok)
{
// Register asset
Content::GetRegistry()->RegisterAsset(context.Data.Header, outputPath);
Content::GetRegistry()->RegisterAsset(context.Data.Header, context.TargetAssetPath);
// Done
const auto endTime = Platform::GetTimeSeconds();
@@ -380,7 +388,7 @@ bool AssetsImportingManager::Create(const Function<CreateAssetResult(CreateAsset
// Do nothing
return true;
}
else
else if (result != CreateAssetResult::Skip)
{
LOG(Error, "Cannot import file '{0}'! Result: {1}", inputPath, ::ToString(result));
return true;
@@ -425,37 +433,37 @@ bool AssetsImportingManagerService::Init()
{ TEXT("otf"), ASSET_FILES_EXTENSION, ImportFont::Import },
// Models
{ TEXT("obj"), ASSET_FILES_EXTENSION, ImportModelFile::Import },
{ TEXT("fbx"), ASSET_FILES_EXTENSION, ImportModelFile::Import },
{ TEXT("x"), ASSET_FILES_EXTENSION, ImportModelFile::Import },
{ TEXT("dae"), ASSET_FILES_EXTENSION, ImportModelFile::Import },
{ TEXT("gltf"), ASSET_FILES_EXTENSION, ImportModelFile::Import },
{ TEXT("glb"), ASSET_FILES_EXTENSION, ImportModelFile::Import },
{ TEXT("obj"), ASSET_FILES_EXTENSION, ImportModel::Import },
{ TEXT("fbx"), ASSET_FILES_EXTENSION, ImportModel::Import },
{ TEXT("x"), ASSET_FILES_EXTENSION, ImportModel::Import },
{ TEXT("dae"), ASSET_FILES_EXTENSION, ImportModel::Import },
{ TEXT("gltf"), ASSET_FILES_EXTENSION, ImportModel::Import },
{ TEXT("glb"), ASSET_FILES_EXTENSION, ImportModel::Import },
// gettext PO files
{ TEXT("po"), TEXT("json"), CreateJson::ImportPo },
// Models (untested formats - may fail :/)
{ TEXT("blend"), ASSET_FILES_EXTENSION, ImportModelFile::Import },
{ TEXT("bvh"), ASSET_FILES_EXTENSION, ImportModelFile::Import },
{ TEXT("ase"), ASSET_FILES_EXTENSION, ImportModelFile::Import },
{ TEXT("ply"), ASSET_FILES_EXTENSION, ImportModelFile::Import },
{ TEXT("dxf"), ASSET_FILES_EXTENSION, ImportModelFile::Import },
{ TEXT("ifc"), ASSET_FILES_EXTENSION, ImportModelFile::Import },
{ TEXT("nff"), ASSET_FILES_EXTENSION, ImportModelFile::Import },
{ TEXT("smd"), ASSET_FILES_EXTENSION, ImportModelFile::Import },
{ TEXT("vta"), ASSET_FILES_EXTENSION, ImportModelFile::Import },
{ TEXT("mdl"), ASSET_FILES_EXTENSION, ImportModelFile::Import },
{ TEXT("md2"), ASSET_FILES_EXTENSION, ImportModelFile::Import },
{ TEXT("md3"), ASSET_FILES_EXTENSION, ImportModelFile::Import },
{ TEXT("md5mesh"), ASSET_FILES_EXTENSION, ImportModelFile::Import },
{ TEXT("q3o"), ASSET_FILES_EXTENSION, ImportModelFile::Import },
{ TEXT("q3s"), ASSET_FILES_EXTENSION, ImportModelFile::Import },
{ TEXT("ac"), ASSET_FILES_EXTENSION, ImportModelFile::Import },
{ TEXT("stl"), ASSET_FILES_EXTENSION, ImportModelFile::Import },
{ TEXT("lwo"), ASSET_FILES_EXTENSION, ImportModelFile::Import },
{ TEXT("lws"), ASSET_FILES_EXTENSION, ImportModelFile::Import },
{ TEXT("lxo"), ASSET_FILES_EXTENSION, ImportModelFile::Import },
{ TEXT("blend"), ASSET_FILES_EXTENSION, ImportModel::Import },
{ TEXT("bvh"), ASSET_FILES_EXTENSION, ImportModel::Import },
{ TEXT("ase"), ASSET_FILES_EXTENSION, ImportModel::Import },
{ TEXT("ply"), ASSET_FILES_EXTENSION, ImportModel::Import },
{ TEXT("dxf"), ASSET_FILES_EXTENSION, ImportModel::Import },
{ TEXT("ifc"), ASSET_FILES_EXTENSION, ImportModel::Import },
{ TEXT("nff"), ASSET_FILES_EXTENSION, ImportModel::Import },
{ TEXT("smd"), ASSET_FILES_EXTENSION, ImportModel::Import },
{ TEXT("vta"), ASSET_FILES_EXTENSION, ImportModel::Import },
{ TEXT("mdl"), ASSET_FILES_EXTENSION, ImportModel::Import },
{ TEXT("md2"), ASSET_FILES_EXTENSION, ImportModel::Import },
{ TEXT("md3"), ASSET_FILES_EXTENSION, ImportModel::Import },
{ TEXT("md5mesh"), ASSET_FILES_EXTENSION, ImportModel::Import },
{ TEXT("q3o"), ASSET_FILES_EXTENSION, ImportModel::Import },
{ TEXT("q3s"), ASSET_FILES_EXTENSION, ImportModel::Import },
{ TEXT("ac"), ASSET_FILES_EXTENSION, ImportModel::Import },
{ TEXT("stl"), ASSET_FILES_EXTENSION, ImportModel::Import },
{ TEXT("lwo"), ASSET_FILES_EXTENSION, ImportModel::Import },
{ TEXT("lws"), ASSET_FILES_EXTENSION, ImportModel::Import },
{ TEXT("lxo"), ASSET_FILES_EXTENSION, ImportModel::Import },
};
AssetsImportingManager::Importers.Add(InBuildImporters, ARRAY_COUNT(InBuildImporters));
@@ -473,7 +481,7 @@ bool AssetsImportingManagerService::Init()
{ AssetsImportingManager::CreateMaterialInstanceTag, CreateMaterialInstance::Create },
// Models
{ AssetsImportingManager::CreateModelTag, ImportModelFile::Create },
{ AssetsImportingManager::CreateModelTag, ImportModel::Create },
// Other
{ AssetsImportingManager::CreateRawDataTag, CreateRawData::Create },

View File

@@ -236,6 +236,9 @@ public:
return ImportIfEdited(inputPath, outputPath, id, arg);
}
// Converts source files path into the relative format if enabled by the project settings. Result path can be stored in asset for reimports.
static String GetImportPath(const String& path);
private:
static bool Create(const CreateAssetFunction& callback, const StringView& inputPath, const StringView& outputPath, Guid& assetId, void* arg);
};

View File

@@ -53,7 +53,7 @@ bool CreateJson::Create(const StringView& path, const StringAnsiView& data, cons
{
if (FileSystem::CreateDirectory(directory))
{
LOG(Warning, "Failed to create directory");
LOG(Warning, "Failed to create directory '{}'", directory);
return true;
}
}

View File

@@ -0,0 +1,743 @@
// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
#include "ImportModel.h"
#if COMPILE_WITH_ASSETS_IMPORTER
#include "Engine/Core/Log.h"
#include "Engine/Core/Collections/Sorting.h"
#include "Engine/Core/Collections/ArrayExtensions.h"
#include "Engine/Serialization/MemoryWriteStream.h"
#include "Engine/Serialization/JsonWriters.h"
#include "Engine/Graphics/Models/ModelData.h"
#include "Engine/Content/Assets/Model.h"
#include "Engine/Content/Assets/SkinnedModel.h"
#include "Engine/Content/Storage/ContentStorageManager.h"
#include "Engine/Content/Assets/Animation.h"
#include "Engine/Content/Content.h"
#include "Engine/Level/Actors/EmptyActor.h"
#include "Engine/Level/Actors/StaticModel.h"
#include "Engine/Level/Prefabs/Prefab.h"
#include "Engine/Level/Prefabs/PrefabManager.h"
#include "Engine/Level/Scripts/ModelPrefab.h"
#include "Engine/Platform/FileSystem.h"
#include "Engine/Utilities/RectPack.h"
#include "Engine/Profiler/ProfilerCPU.h"
#include "AssetsImportingManager.h"
bool ImportModel::TryGetImportOptions(const StringView& path, Options& options)
{
if (FileSystem::FileExists(path))
{
// Try to load asset file and asset info
auto tmpFile = ContentStorageManager::GetStorage(path);
AssetInitData data;
if (tmpFile
&& tmpFile->GetEntriesCount() == 1
&& (
(tmpFile->GetEntry(0).TypeName == Model::TypeName && !tmpFile->LoadAssetHeader(0, data) && data.SerializedVersion >= 4)
||
(tmpFile->GetEntry(0).TypeName == SkinnedModel::TypeName && !tmpFile->LoadAssetHeader(0, data) && data.SerializedVersion >= 1)
||
(tmpFile->GetEntry(0).TypeName == Animation::TypeName && !tmpFile->LoadAssetHeader(0, data) && data.SerializedVersion >= 1)
))
{
// Check import meta
rapidjson_flax::Document metadata;
metadata.Parse((const char*)data.Metadata.Get(), data.Metadata.Length());
if (metadata.HasParseError() == false)
{
options.Deserialize(metadata, nullptr);
return true;
}
}
}
return false;
}
struct PrefabObject
{
int32 NodeIndex;
String Name;
String AssetPath;
};
void RepackMeshLightmapUVs(ModelData& data)
{
// Use weight-based coordinates space placement and rect-pack to allocate more space for bigger meshes in the model lightmap chart
int32 lodIndex = 0;
auto& lod = data.LODs[lodIndex];
// Build list of meshes with their area
struct LightmapUVsPack : RectPack<LightmapUVsPack, float>
{
LightmapUVsPack(float x, float y, float width, float height)
: RectPack<LightmapUVsPack, float>(x, y, width, height)
{
}
void OnInsert()
{
}
};
struct MeshEntry
{
MeshData* Mesh;
float Area;
float Size;
LightmapUVsPack* Slot;
};
Array<MeshEntry> entries;
entries.Resize(lod.Meshes.Count());
float areaSum = 0;
for (int32 meshIndex = 0; meshIndex < lod.Meshes.Count(); meshIndex++)
{
auto& entry = entries[meshIndex];
entry.Mesh = lod.Meshes[meshIndex];
entry.Area = entry.Mesh->CalculateTrianglesArea();
entry.Size = Math::Sqrt(entry.Area);
areaSum += entry.Area;
}
if (areaSum > ZeroTolerance)
{
// Pack all surfaces into atlas
float atlasSize = Math::Sqrt(areaSum) * 1.02f;
int32 triesLeft = 10;
while (triesLeft--)
{
bool failed = false;
const float chartsPadding = (4.0f / 256.0f) * atlasSize;
LightmapUVsPack root(chartsPadding, chartsPadding, atlasSize - chartsPadding, atlasSize - chartsPadding);
for (auto& entry : entries)
{
entry.Slot = root.Insert(entry.Size, entry.Size, chartsPadding);
if (entry.Slot == nullptr)
{
// Failed to insert surface, increase atlas size and try again
atlasSize *= 1.5f;
failed = true;
break;
}
}
if (!failed)
{
// Transform meshes lightmap UVs into the slots in the whole atlas
const float atlasSizeInv = 1.0f / atlasSize;
for (const auto& entry : entries)
{
Float2 uvOffset(entry.Slot->X * atlasSizeInv, entry.Slot->Y * atlasSizeInv);
Float2 uvScale((entry.Slot->Width - chartsPadding) * atlasSizeInv, (entry.Slot->Height - chartsPadding) * atlasSizeInv);
// TODO: SIMD
for (auto& uv : entry.Mesh->LightmapUVs)
{
uv = uv * uvScale + uvOffset;
}
}
break;
}
}
}
}
void TryRestoreMaterials(CreateAssetContext& context, ModelData& modelData)
{
// Skip if file is missing
if (!FileSystem::FileExists(context.TargetAssetPath))
return;
// Try to load asset that gets reimported
AssetReference<Asset> asset = Content::LoadAsync<Asset>(context.TargetAssetPath);
if (asset == nullptr)
return;
if (asset->WaitForLoaded())
return;
// Get model object
ModelBase* model = nullptr;
if (asset.Get()->GetTypeName() == Model::TypeName)
{
model = ((Model*)asset.Get());
}
else if (asset.Get()->GetTypeName() == SkinnedModel::TypeName)
{
model = ((SkinnedModel*)asset.Get());
}
if (!model)
return;
// Peek materials
for (int32 i = 0; i < modelData.Materials.Count(); i++)
{
auto& dstSlot = modelData.Materials[i];
if (model->MaterialSlots.Count() > i)
{
auto& srcSlot = model->MaterialSlots[i];
dstSlot.Name = srcSlot.Name;
dstSlot.ShadowsMode = srcSlot.ShadowsMode;
dstSlot.AssetID = srcSlot.Material.GetID();
}
}
}
void SetupMaterialSlots(ModelData& data, const Array<MaterialSlotEntry>& materials)
{
Array<int32> materialSlotsTable;
materialSlotsTable.Resize(materials.Count());
materialSlotsTable.SetAll(-1);
for (auto& lod : data.LODs)
{
for (MeshData* mesh : lod.Meshes)
{
int32 newSlotIndex = materialSlotsTable[mesh->MaterialSlotIndex];
if (newSlotIndex == -1)
{
newSlotIndex = data.Materials.Count();
data.Materials.AddOne() = materials[mesh->MaterialSlotIndex];
}
mesh->MaterialSlotIndex = newSlotIndex;
}
}
}
bool SortMeshGroups(IGrouping<StringView, MeshData*> const& i1, IGrouping<StringView, MeshData*> const& i2)
{
return i1.GetKey().Compare(i2.GetKey()) < 0;
}
CreateAssetResult ImportModel::Import(CreateAssetContext& context)
{
// Get import options
Options options;
if (context.CustomArg != nullptr)
{
// Copy import options from argument
options = *static_cast<Options*>(context.CustomArg);
}
else
{
// Restore the previous settings or use default ones
if (!TryGetImportOptions(context.TargetAssetPath, options))
{
LOG(Warning, "Missing model import options. Using default values.");
}
}
// Import model file
ModelData* data = options.Cached ? options.Cached->Data : nullptr;
ModelData dataThis;
Array<IGrouping<StringView, MeshData*>>* meshesByNamePtr = options.Cached ? (Array<IGrouping<StringView, MeshData*>>*)options.Cached->MeshesByName : nullptr;
Array<IGrouping<StringView, MeshData*>> meshesByNameThis;
String autoImportOutput;
if (!data)
{
String errorMsg;
autoImportOutput = StringUtils::GetDirectoryName(context.TargetAssetPath);
autoImportOutput /= options.SubAssetFolder.HasChars() ? options.SubAssetFolder.TrimTrailing() : String(StringUtils::GetFileNameWithoutExtension(context.InputPath));
if (ModelTool::ImportModel(context.InputPath, dataThis, options, errorMsg, autoImportOutput))
{
LOG(Error, "Cannot import model file. {0}", errorMsg);
return CreateAssetResult::Error;
}
data = &dataThis;
// Group meshes by the name (the same mesh name can be used by multiple meshes that use different materials)
if (data->LODs.Count() != 0)
{
const Function<StringView(MeshData* const&)> f = [](MeshData* const& x) -> StringView
{
return x->Name;
};
ArrayExtensions::GroupBy(data->LODs[0].Meshes, f, meshesByNameThis);
Sorting::QuickSort(meshesByNameThis.Get(), meshesByNameThis.Count(), &SortMeshGroups);
}
meshesByNamePtr = &meshesByNameThis;
}
Array<IGrouping<StringView, MeshData*>>& meshesByName = *meshesByNamePtr;
// Import objects from file separately
ModelTool::Options::CachedData cached = { data, (void*)meshesByNamePtr };
Array<PrefabObject> prefabObjects;
if (options.Type == ModelTool::ModelType::Prefab)
{
// Normalize options
options.SplitObjects = false;
options.ObjectIndex = -1;
// Import all of the objects recursive but use current model data to skip loading file again
options.Cached = &cached;
Function<bool(Options& splitOptions, const StringView& objectName, String& outputPath)> splitImport = [&context, &autoImportOutput](Options& splitOptions, const StringView& objectName, String& outputPath)
{
// Recursive importing of the split object
String postFix = objectName;
const int32 splitPos = postFix.FindLast(TEXT('|'));
if (splitPos != -1)
postFix = postFix.Substring(splitPos + 1);
// TODO: check for name collisions with material/texture assets
outputPath = autoImportOutput / String(StringUtils::GetFileNameWithoutExtension(context.TargetAssetPath)) + TEXT(" ") + postFix + TEXT(".flax");
splitOptions.SubAssetFolder = TEXT(" "); // Use the same folder as asset as they all are imported to the subdir for the prefab (see SubAssetFolder usage above)
return AssetsImportingManager::Import(context.InputPath, outputPath, &splitOptions);
};
auto splitOptions = options;
LOG(Info, "Splitting imported {0} meshes", meshesByName.Count());
PrefabObject prefabObject;
for (int32 groupIndex = 0; groupIndex < meshesByName.Count(); groupIndex++)
{
auto& group = meshesByName[groupIndex];
// Cache object options (nested sub-object import removes the meshes)
prefabObject.NodeIndex = group.First()->NodeIndex;
prefabObject.Name = group.First()->Name;
splitOptions.Type = ModelTool::ModelType::Model;
splitOptions.ObjectIndex = groupIndex;
if (!splitImport(splitOptions, group.GetKey(), prefabObject.AssetPath))
{
prefabObjects.Add(prefabObject);
}
}
LOG(Info, "Splitting imported {0} animations", data->Animations.Count());
for (int32 i = 0; i < data->Animations.Count(); i++)
{
auto& animation = data->Animations[i];
splitOptions.Type = ModelTool::ModelType::Animation;
splitOptions.ObjectIndex = i;
splitImport(splitOptions, animation.Name, prefabObject.AssetPath);
}
}
else if (options.SplitObjects)
{
// Import the first object within this call
options.SplitObjects = false;
options.ObjectIndex = 0;
// Import rest of the objects recursive but use current model data to skip loading file again
options.Cached = &cached;
Function<bool(Options& splitOptions, const StringView& objectName)> splitImport;
splitImport.Bind([&context](Options& splitOptions, const StringView& objectName)
{
// Recursive importing of the split object
String postFix = objectName;
const int32 splitPos = postFix.FindLast(TEXT('|'));
if (splitPos != -1)
postFix = postFix.Substring(splitPos + 1);
const String outputPath = String(StringUtils::GetPathWithoutExtension(context.TargetAssetPath)) + TEXT(" ") + postFix + TEXT(".flax");
return AssetsImportingManager::Import(context.InputPath, outputPath, &splitOptions);
});
auto splitOptions = options;
switch (options.Type)
{
case ModelTool::ModelType::Model:
case ModelTool::ModelType::SkinnedModel:
LOG(Info, "Splitting imported {0} meshes", meshesByName.Count());
for (int32 groupIndex = 1; groupIndex < meshesByName.Count(); groupIndex++)
{
auto& group = meshesByName[groupIndex];
splitOptions.ObjectIndex = groupIndex;
splitImport(splitOptions, group.GetKey());
}
break;
case ModelTool::ModelType::Animation:
LOG(Info, "Splitting imported {0} animations", data->Animations.Count());
for (int32 i = 1; i < data->Animations.Count(); i++)
{
auto& animation = data->Animations[i];
splitOptions.ObjectIndex = i;
splitImport(splitOptions, animation.Name);
}
break;
}
}
// When importing a single object as model asset then select a specific mesh group
Array<MeshData*> meshesToDelete;
if (options.ObjectIndex >= 0 &&
options.ObjectIndex < meshesByName.Count() &&
(options.Type == ModelTool::ModelType::Model || options.Type == ModelTool::ModelType::SkinnedModel))
{
auto& group = meshesByName[options.ObjectIndex];
if (&dataThis == data)
{
// Use meshes only from the the grouping (others will be removed manually)
{
auto& lod = dataThis.LODs[0];
meshesToDelete.Add(lod.Meshes);
lod.Meshes.Clear();
for (MeshData* mesh : group)
{
lod.Meshes.Add(mesh);
meshesToDelete.Remove(mesh);
}
}
for (int32 lodIndex = 1; lodIndex < dataThis.LODs.Count(); lodIndex++)
{
auto& lod = dataThis.LODs[lodIndex];
Array<MeshData*> lodMeshes = lod.Meshes;
lod.Meshes.Clear();
for (MeshData* lodMesh : lodMeshes)
{
if (lodMesh->Name == group.GetKey())
lod.Meshes.Add(lodMesh);
else
meshesToDelete.Add(lodMesh);
}
}
// Use only materials references by meshes from the first grouping
{
auto materials = dataThis.Materials;
dataThis.Materials.Clear();
SetupMaterialSlots(dataThis, materials);
}
}
else
{
// Copy data from others data
dataThis.Skeleton = data->Skeleton;
dataThis.Nodes = data->Nodes;
// Move meshes from this group (including any LODs of them)
{
auto& lod = dataThis.LODs.AddOne();
lod.ScreenSize = data->LODs[0].ScreenSize;
lod.Meshes.Add(group);
for (MeshData* mesh : group)
data->LODs[0].Meshes.Remove(mesh);
}
for (int32 lodIndex = 1; lodIndex < data->LODs.Count(); lodIndex++)
{
Array<MeshData*> lodMeshes = data->LODs[lodIndex].Meshes;
for (int32 i = lodMeshes.Count() - 1; i >= 0; i--)
{
MeshData* lodMesh = lodMeshes[i];
if (lodMesh->Name == group.GetKey())
data->LODs[lodIndex].Meshes.Remove(lodMesh);
else
lodMeshes.RemoveAtKeepOrder(i);
}
if (lodMeshes.Count() == 0)
break; // No meshes of that name in this LOD so skip further ones
auto& lod = dataThis.LODs.AddOne();
lod.ScreenSize = data->LODs[lodIndex].ScreenSize;
lod.Meshes.Add(lodMeshes);
}
// Copy materials used by the meshes
SetupMaterialSlots(dataThis, data->Materials);
}
data = &dataThis;
}
// Check if restore materials on model reimport
if (options.RestoreMaterialsOnReimport && data->Materials.HasItems())
{
TryRestoreMaterials(context, *data);
}
// When using generated lightmap UVs those coordinates needs to be moved so all meshes are in unique locations in [0-1]x[0-1] coordinates space
// Model importer generates UVs in [0-1] space for each mesh so now we need to pack them inside the whole model (only when using multiple meshes)
if (options.Type == ModelTool::ModelType::Model && options.LightmapUVsSource == ModelLightmapUVsSource::Generate && data->LODs.HasItems() && data->LODs[0].Meshes.Count() > 1)
{
RepackMeshLightmapUVs(*data);
}
// Create destination asset type
CreateAssetResult result = CreateAssetResult::InvalidTypeID;
switch (options.Type)
{
case ModelTool::ModelType::Model:
result = CreateModel(context, *data, &options);
break;
case ModelTool::ModelType::SkinnedModel:
result = CreateSkinnedModel(context, *data, &options);
break;
case ModelTool::ModelType::Animation:
result = CreateAnimation(context, *data, &options);
break;
case ModelTool::ModelType::Prefab:
result = CreatePrefab(context, *data, options, prefabObjects);
break;
}
for (auto mesh : meshesToDelete)
Delete(mesh);
if (result != CreateAssetResult::Ok)
return result;
// Create json with import context
rapidjson_flax::StringBuffer importOptionsMetaBuffer;
importOptionsMetaBuffer.Reserve(256);
CompactJsonWriter importOptionsMetaObj(importOptionsMetaBuffer);
JsonWriter& importOptionsMeta = importOptionsMetaObj;
importOptionsMeta.StartObject();
{
context.AddMeta(importOptionsMeta);
options.Serialize(importOptionsMeta, nullptr);
}
importOptionsMeta.EndObject();
context.Data.Metadata.Copy((const byte*)importOptionsMetaBuffer.GetString(), (uint32)importOptionsMetaBuffer.GetSize());
return CreateAssetResult::Ok;
}
CreateAssetResult ImportModel::Create(CreateAssetContext& context)
{
ASSERT(context.CustomArg != nullptr);
auto& modelData = *(ModelData*)context.CustomArg;
// Ensure model has any meshes
if ((modelData.LODs.IsEmpty() || modelData.LODs[0].Meshes.IsEmpty()))
{
LOG(Warning, "Models has no valid meshes");
return CreateAssetResult::Error;
}
// Auto calculate LODs transition settings
modelData.CalculateLODsScreenSizes();
return CreateModel(context, modelData);
}
CreateAssetResult ImportModel::CreateModel(CreateAssetContext& context, ModelData& modelData, const Options* options)
{
PROFILE_CPU();
IMPORT_SETUP(Model, Model::SerializedVersion);
// Save model header
MemoryWriteStream stream(4096);
if (modelData.Pack2ModelHeader(&stream))
return CreateAssetResult::Error;
if (context.AllocateChunk(0))
return CreateAssetResult::CannotAllocateChunk;
context.Data.Header.Chunks[0]->Data.Copy(stream.GetHandle(), stream.GetPosition());
// Pack model LODs data
const auto lodCount = modelData.GetLODsCount();
for (int32 lodIndex = 0; lodIndex < lodCount; lodIndex++)
{
stream.SetPosition(0);
// Pack meshes
auto& meshes = modelData.LODs[lodIndex].Meshes;
for (int32 meshIndex = 0; meshIndex < meshes.Count(); meshIndex++)
{
if (meshes[meshIndex]->Pack2Model(&stream))
{
LOG(Warning, "Cannot pack mesh.");
return CreateAssetResult::Error;
}
}
const int32 chunkIndex = lodIndex + 1;
if (context.AllocateChunk(chunkIndex))
return CreateAssetResult::CannotAllocateChunk;
context.Data.Header.Chunks[chunkIndex]->Data.Copy(stream.GetHandle(), stream.GetPosition());
}
// Generate SDF
if (options && options->GenerateSDF)
{
stream.SetPosition(0);
if (!ModelTool::GenerateModelSDF(nullptr, &modelData, options->SDFResolution, lodCount - 1, nullptr, &stream, context.TargetAssetPath))
{
if (context.AllocateChunk(15))
return CreateAssetResult::CannotAllocateChunk;
context.Data.Header.Chunks[15]->Data.Copy(stream.GetHandle(), stream.GetPosition());
}
}
return CreateAssetResult::Ok;
}
CreateAssetResult ImportModel::CreateSkinnedModel(CreateAssetContext& context, ModelData& modelData, const Options* options)
{
PROFILE_CPU();
IMPORT_SETUP(SkinnedModel, SkinnedModel::SerializedVersion);
// Save skinned model header
MemoryWriteStream stream(4096);
if (modelData.Pack2SkinnedModelHeader(&stream))
return CreateAssetResult::Error;
if (context.AllocateChunk(0))
return CreateAssetResult::CannotAllocateChunk;
context.Data.Header.Chunks[0]->Data.Copy(stream.GetHandle(), stream.GetPosition());
// Pack model LODs data
const auto lodCount = modelData.GetLODsCount();
for (int32 lodIndex = 0; lodIndex < lodCount; lodIndex++)
{
stream.SetPosition(0);
// Mesh Data Version
stream.WriteByte(1);
// Pack meshes
auto& meshes = modelData.LODs[lodIndex].Meshes;
for (int32 meshIndex = 0; meshIndex < meshes.Count(); meshIndex++)
{
if (meshes[meshIndex]->Pack2SkinnedModel(&stream))
{
LOG(Warning, "Cannot pack mesh.");
return CreateAssetResult::Error;
}
}
const int32 chunkIndex = lodIndex + 1;
if (context.AllocateChunk(chunkIndex))
return CreateAssetResult::CannotAllocateChunk;
context.Data.Header.Chunks[chunkIndex]->Data.Copy(stream.GetHandle(), stream.GetPosition());
}
return CreateAssetResult::Ok;
}
CreateAssetResult ImportModel::CreateAnimation(CreateAssetContext& context, ModelData& modelData, const Options* options)
{
PROFILE_CPU();
IMPORT_SETUP(Animation, Animation::SerializedVersion);
// Save animation data
MemoryWriteStream stream(8182);
const int32 animIndex = options && options->ObjectIndex != -1 ? options->ObjectIndex : 0; // Single animation per asset
if (modelData.Pack2AnimationHeader(&stream, animIndex))
return CreateAssetResult::Error;
if (context.AllocateChunk(0))
return CreateAssetResult::CannotAllocateChunk;
context.Data.Header.Chunks[0]->Data.Copy(stream.GetHandle(), stream.GetPosition());
return CreateAssetResult::Ok;
}
CreateAssetResult ImportModel::CreatePrefab(CreateAssetContext& context, ModelData& data, const Options& options, const Array<PrefabObject>& prefabObjects)
{
PROFILE_CPU();
if (data.Nodes.Count() == 0)
return CreateAssetResult::Error;
// If that prefab already exists then we need to use it as base to preserve object IDs and local changes applied by user
const String outputPath = String(StringUtils::GetPathWithoutExtension(context.TargetAssetPath)) + DEFAULT_PREFAB_EXTENSION_DOT;
auto* prefab = FileSystem::FileExists(outputPath) ? Content::Load<Prefab>(outputPath) : nullptr;
if (prefab)
{
// Ensure that prefab has Default Instance so ObjectsCache is valid (used below)
prefab->GetDefaultInstance();
}
// Create prefab structure
Dictionary<int32, Actor*> nodeToActor;
Array<Actor*> nodeActors;
Actor* rootActor = nullptr;
for (int32 nodeIndex = 0; nodeIndex < data.Nodes.Count(); nodeIndex++)
{
const auto& node = data.Nodes[nodeIndex];
// Create actor(s) for this node
nodeActors.Clear();
for (const PrefabObject& e : prefabObjects)
{
if (e.NodeIndex == nodeIndex)
{
auto* actor = New<StaticModel>();
actor->SetName(e.Name);
if (auto* model = Content::LoadAsync<Model>(e.AssetPath))
{
actor->Model = model;
}
nodeActors.Add(actor);
}
}
Actor* nodeActor = nodeActors.Count() == 1 ? nodeActors[0] : New<EmptyActor>();
if (nodeActors.Count() > 1)
{
for (Actor* e : nodeActors)
{
e->SetParent(nodeActor);
}
}
if (nodeActors.Count() != 1)
{
// Include default actor to iterate over it properly in code below
nodeActors.Add(nodeActor);
}
// Setup node in hierarchy
nodeToActor.Add(nodeIndex, nodeActor);
nodeActor->SetName(node.Name);
nodeActor->SetLocalTransform(node.LocalTransform);
if (nodeIndex == 0)
{
// Special case for root actor to link any unlinked nodes
nodeToActor.Add(-1, nodeActor);
rootActor = nodeActor;
}
else
{
Actor* parentActor;
if (nodeToActor.TryGet(node.ParentIndex, parentActor))
nodeActor->SetParent(parentActor);
}
// Link with object from prefab (if reimporting)
if (prefab)
{
for (Actor* a : nodeActors)
{
for (const auto& i : prefab->ObjectsCache)
{
if (i.Value->GetTypeHandle() != a->GetTypeHandle()) // Type match
continue;
auto* o = (Actor*)i.Value;
if (o->GetName() != a->GetName()) // Name match
continue;
// Mark as this object already exists in prefab so will be preserved when updating it
a->LinkPrefab(o->GetPrefabID(), o->GetPrefabObjectID());
break;
}
}
}
}
ASSERT_LOW_LAYER(rootActor);
{
// Add script with import options
auto* modelPrefabScript = New<ModelPrefab>();
modelPrefabScript->SetParent(rootActor);
modelPrefabScript->ImportPath = AssetsImportingManager::GetImportPath(context.InputPath);
modelPrefabScript->ImportOptions = options;
// Link with existing prefab instance
if (prefab)
{
for (const auto& i : prefab->ObjectsCache)
{
if (i.Value->GetTypeHandle() == modelPrefabScript->GetTypeHandle())
{
modelPrefabScript->LinkPrefab(i.Value->GetPrefabID(), i.Value->GetPrefabObjectID());
break;
}
}
}
}
// Create prefab instead of native asset
bool failed;
if (prefab)
{
failed = prefab->ApplyAll(rootActor);
}
else
{
failed = PrefabManager::CreatePrefab(rootActor, outputPath, false);
}
// Cleanup objects from memory
rootActor->DeleteObjectNow();
if (failed)
return CreateAssetResult::Error;
return CreateAssetResult::Skip;
}
#endif

View File

@@ -8,15 +8,10 @@
#include "Engine/Tools/ModelTool/ModelTool.h"
/// <summary>
/// Enable/disable caching model import options
/// </summary>
#define IMPORT_MODEL_CACHE_OPTIONS 1
/// <summary>
/// Importing models utility
/// </summary>
class ImportModelFile
class ImportModel
{
public:
typedef ModelTool::Options Options;
@@ -45,9 +40,10 @@ public:
static CreateAssetResult Create(CreateAssetContext& context);
private:
static CreateAssetResult ImportModel(CreateAssetContext& context, ModelData& modelData, const Options* options = nullptr);
static CreateAssetResult ImportSkinnedModel(CreateAssetContext& context, ModelData& modelData, const Options* options = nullptr);
static CreateAssetResult ImportAnimation(CreateAssetContext& context, ModelData& modelData, const Options* options = nullptr);
static CreateAssetResult CreateModel(CreateAssetContext& context, ModelData& data, const Options* options = nullptr);
static CreateAssetResult CreateSkinnedModel(CreateAssetContext& context, ModelData& data, const Options* options = nullptr);
static CreateAssetResult CreateAnimation(CreateAssetContext& context, ModelData& data, const Options* options = nullptr);
static CreateAssetResult CreatePrefab(CreateAssetContext& context, ModelData& data, const Options& options, const Array<struct PrefabObject>& prefabObjects);
};
#endif

View File

@@ -1,307 +0,0 @@
// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
#include "ImportModel.h"
#if COMPILE_WITH_ASSETS_IMPORTER
#include "Engine/Core/Log.h"
#include "Engine/Serialization/MemoryWriteStream.h"
#include "Engine/Serialization/JsonWriters.h"
#include "Engine/Graphics/Models/ModelData.h"
#include "Engine/Content/Assets/Model.h"
#include "Engine/Content/Assets/SkinnedModel.h"
#include "Engine/Content/Storage/ContentStorageManager.h"
#include "Engine/Content/Assets/Animation.h"
#include "Engine/Content/Content.h"
#include "Engine/Platform/FileSystem.h"
#include "AssetsImportingManager.h"
bool ImportModelFile::TryGetImportOptions(const StringView& path, Options& options)
{
#if IMPORT_MODEL_CACHE_OPTIONS
if (FileSystem::FileExists(path))
{
// Try to load asset file and asset info
auto tmpFile = ContentStorageManager::GetStorage(path);
AssetInitData data;
if (tmpFile
&& tmpFile->GetEntriesCount() == 1
&& (
(tmpFile->GetEntry(0).TypeName == Model::TypeName && !tmpFile->LoadAssetHeader(0, data) && data.SerializedVersion >= 4)
||
(tmpFile->GetEntry(0).TypeName == SkinnedModel::TypeName && !tmpFile->LoadAssetHeader(0, data) && data.SerializedVersion >= 1)
||
(tmpFile->GetEntry(0).TypeName == Animation::TypeName && !tmpFile->LoadAssetHeader(0, data) && data.SerializedVersion >= 1)
))
{
// Check import meta
rapidjson_flax::Document metadata;
metadata.Parse((const char*)data.Metadata.Get(), data.Metadata.Length());
if (metadata.HasParseError() == false)
{
options.Deserialize(metadata, nullptr);
return true;
}
}
}
#endif
return false;
}
void TryRestoreMaterials(CreateAssetContext& context, ModelData& modelData)
{
// Skip if file is missing
if (!FileSystem::FileExists(context.TargetAssetPath))
return;
// Try to load asset that gets reimported
AssetReference<Asset> asset = Content::LoadAsync<Asset>(context.TargetAssetPath);
if (asset == nullptr)
return;
if (asset->WaitForLoaded())
return;
// Get model object
ModelBase* model = nullptr;
if (asset.Get()->GetTypeName() == Model::TypeName)
{
model = ((Model*)asset.Get());
}
else if (asset.Get()->GetTypeName() == SkinnedModel::TypeName)
{
model = ((SkinnedModel*)asset.Get());
}
if (!model)
return;
// Peek materials
for (int32 i = 0; i < modelData.Materials.Count(); i++)
{
auto& dstSlot = modelData.Materials[i];
if (model->MaterialSlots.Count() > i)
{
auto& srcSlot = model->MaterialSlots[i];
dstSlot.Name = srcSlot.Name;
dstSlot.ShadowsMode = srcSlot.ShadowsMode;
dstSlot.AssetID = srcSlot.Material.GetID();
}
}
}
CreateAssetResult ImportModelFile::Import(CreateAssetContext& context)
{
// Get import options
Options options;
if (context.CustomArg != nullptr)
{
// Copy import options from argument
options = *static_cast<Options*>(context.CustomArg);
}
else
{
// Restore the previous settings or use default ones
if (!TryGetImportOptions(context.TargetAssetPath, options))
{
LOG(Warning, "Missing model import options. Using default values.");
}
}
if (options.SplitObjects)
{
options.OnSplitImport.Bind([&context](Options& splitOptions, const String& objectName)
{
// Recursive importing of the split object
String postFix = objectName;
const int32 splitPos = postFix.FindLast(TEXT('|'));
if (splitPos != -1)
postFix = postFix.Substring(splitPos + 1);
const String outputPath = String(StringUtils::GetPathWithoutExtension(context.TargetAssetPath)) + TEXT(" ") + postFix + TEXT(".flax");
return AssetsImportingManager::Import(context.InputPath, outputPath, &splitOptions);
});
}
// Import model file
ModelData modelData;
String errorMsg;
String autoImportOutput(StringUtils::GetDirectoryName(context.TargetAssetPath));
autoImportOutput /= options.SubAssetFolder.HasChars() ? options.SubAssetFolder.TrimTrailing() : String(StringUtils::GetFileNameWithoutExtension(context.InputPath));
if (ModelTool::ImportModel(context.InputPath, modelData, options, errorMsg, autoImportOutput))
{
LOG(Error, "Cannot import model file. {0}", errorMsg);
return CreateAssetResult::Error;
}
// Check if restore materials on model reimport
if (options.RestoreMaterialsOnReimport && modelData.Materials.HasItems())
{
TryRestoreMaterials(context, modelData);
}
// Auto calculate LODs transition settings
modelData.CalculateLODsScreenSizes();
// Create destination asset type
CreateAssetResult result = CreateAssetResult::InvalidTypeID;
switch (options.Type)
{
case ModelTool::ModelType::Model:
result = ImportModel(context, modelData, &options);
break;
case ModelTool::ModelType::SkinnedModel:
result = ImportSkinnedModel(context, modelData, &options);
break;
case ModelTool::ModelType::Animation:
result = ImportAnimation(context, modelData, &options);
break;
}
if (result != CreateAssetResult::Ok)
return result;
#if IMPORT_MODEL_CACHE_OPTIONS
// Create json with import context
rapidjson_flax::StringBuffer importOptionsMetaBuffer;
importOptionsMetaBuffer.Reserve(256);
CompactJsonWriter importOptionsMetaObj(importOptionsMetaBuffer);
JsonWriter& importOptionsMeta = importOptionsMetaObj;
importOptionsMeta.StartObject();
{
context.AddMeta(importOptionsMeta);
options.Serialize(importOptionsMeta, nullptr);
}
importOptionsMeta.EndObject();
context.Data.Metadata.Copy((const byte*)importOptionsMetaBuffer.GetString(), (uint32)importOptionsMetaBuffer.GetSize());
#endif
return CreateAssetResult::Ok;
}
CreateAssetResult ImportModelFile::Create(CreateAssetContext& context)
{
ASSERT(context.CustomArg != nullptr);
auto& modelData = *(ModelData*)context.CustomArg;
// Ensure model has any meshes
if ((modelData.LODs.IsEmpty() || modelData.LODs[0].Meshes.IsEmpty()))
{
LOG(Warning, "Models has no valid meshes");
return CreateAssetResult::Error;
}
// Auto calculate LODs transition settings
modelData.CalculateLODsScreenSizes();
// Import
return ImportModel(context, modelData);
}
CreateAssetResult ImportModelFile::ImportModel(CreateAssetContext& context, ModelData& modelData, const Options* options)
{
// Base
IMPORT_SETUP(Model, Model::SerializedVersion);
// Save model header
MemoryWriteStream stream(4096);
if (modelData.Pack2ModelHeader(&stream))
return CreateAssetResult::Error;
if (context.AllocateChunk(0))
return CreateAssetResult::CannotAllocateChunk;
context.Data.Header.Chunks[0]->Data.Copy(stream.GetHandle(), stream.GetPosition());
// Pack model LODs data
const auto lodCount = modelData.GetLODsCount();
for (int32 lodIndex = 0; lodIndex < lodCount; lodIndex++)
{
stream.SetPosition(0);
// Pack meshes
auto& meshes = modelData.LODs[lodIndex].Meshes;
for (int32 meshIndex = 0; meshIndex < meshes.Count(); meshIndex++)
{
if (meshes[meshIndex]->Pack2Model(&stream))
{
LOG(Warning, "Cannot pack mesh.");
return CreateAssetResult::Error;
}
}
const int32 chunkIndex = lodIndex + 1;
if (context.AllocateChunk(chunkIndex))
return CreateAssetResult::CannotAllocateChunk;
context.Data.Header.Chunks[chunkIndex]->Data.Copy(stream.GetHandle(), stream.GetPosition());
}
// Generate SDF
if (options && options->GenerateSDF)
{
stream.SetPosition(0);
if (!ModelTool::GenerateModelSDF(nullptr, &modelData, options->SDFResolution, lodCount - 1, nullptr, &stream, context.TargetAssetPath))
{
if (context.AllocateChunk(15))
return CreateAssetResult::CannotAllocateChunk;
context.Data.Header.Chunks[15]->Data.Copy(stream.GetHandle(), stream.GetPosition());
}
}
return CreateAssetResult::Ok;
}
CreateAssetResult ImportModelFile::ImportSkinnedModel(CreateAssetContext& context, ModelData& modelData, const Options* options)
{
// Base
IMPORT_SETUP(SkinnedModel, SkinnedModel::SerializedVersion);
// Save skinned model header
MemoryWriteStream stream(4096);
if (modelData.Pack2SkinnedModelHeader(&stream))
return CreateAssetResult::Error;
if (context.AllocateChunk(0))
return CreateAssetResult::CannotAllocateChunk;
context.Data.Header.Chunks[0]->Data.Copy(stream.GetHandle(), stream.GetPosition());
// Pack model LODs data
const auto lodCount = modelData.GetLODsCount();
for (int32 lodIndex = 0; lodIndex < lodCount; lodIndex++)
{
stream.SetPosition(0);
// Mesh Data Version
stream.WriteByte(1);
// Pack meshes
auto& meshes = modelData.LODs[lodIndex].Meshes;
for (int32 meshIndex = 0; meshIndex < meshes.Count(); meshIndex++)
{
if (meshes[meshIndex]->Pack2SkinnedModel(&stream))
{
LOG(Warning, "Cannot pack mesh.");
return CreateAssetResult::Error;
}
}
const int32 chunkIndex = lodIndex + 1;
if (context.AllocateChunk(chunkIndex))
return CreateAssetResult::CannotAllocateChunk;
context.Data.Header.Chunks[chunkIndex]->Data.Copy(stream.GetHandle(), stream.GetPosition());
}
return CreateAssetResult::Ok;
}
CreateAssetResult ImportModelFile::ImportAnimation(CreateAssetContext& context, ModelData& modelData, const Options* options)
{
// Base
IMPORT_SETUP(Animation, Animation::SerializedVersion);
// Save animation data
MemoryWriteStream stream(8182);
if (modelData.Pack2AnimationHeader(&stream))
return CreateAssetResult::Error;
if (context.AllocateChunk(0))
return CreateAssetResult::CannotAllocateChunk;
context.Data.Header.Chunks[0]->Data.Copy(stream.GetHandle(), stream.GetPosition());
return CreateAssetResult::Ok;
}
#endif

View File

@@ -1,54 +0,0 @@
// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
#pragma once
#include "Types.h"
#if COMPILE_WITH_ASSETS_IMPORTER
#include "Engine/Content/Assets/Model.h"
#include "Engine/Tools/ModelTool/ModelTool.h"
/// <summary>
/// Enable/disable caching model import options
/// </summary>
#define IMPORT_MODEL_CACHE_OPTIONS 1
/// <summary>
/// Importing models utility
/// </summary>
class ImportModelFile
{
public:
typedef ModelTool::Options Options;
public:
/// <summary>
/// Tries the get model import options from the target location asset.
/// </summary>
/// <param name="path">The asset path.</param>
/// <param name="options">The options.</param>
/// <returns>True if success, otherwise false.</returns>
static bool TryGetImportOptions(String path, Options& options);
/// <summary>
/// Imports the model file.
/// </summary>
/// <param name="context">The importing context.</param>
/// <returns>Result.</returns>
static CreateAssetResult Import(CreateAssetContext& context);
/// <summary>
/// Creates the model asset from the ModelData storage (input argument should be pointer to ModelData).
/// </summary>
/// <param name="context">The importing context.</param>
/// <returns>Result.</returns>
static CreateAssetResult Create(CreateAssetContext& context);
private:
static CreateAssetResult ImportModel(CreateAssetContext& context, ModelData& modelData);
static CreateAssetResult ImportSkinnedModel(CreateAssetContext& context, ModelData& modelData);
static CreateAssetResult ImportAnimation(CreateAssetContext& context, ModelData& modelData);
};
#endif

View File

@@ -8,7 +8,6 @@
#include "Engine/Serialization/MemoryWriteStream.h"
#include "Engine/Serialization/MemoryReadStream.h"
#include "Engine/Graphics/Textures/TextureData.h"
#include "Engine/Graphics/Textures/TextureUtils.h"
#include "Engine/Graphics/PixelFormatExtensions.h"
#include "Engine/Content/Storage/ContentStorageManager.h"
#include "Engine/ContentImporters/ImportIES.h"

View File

@@ -18,7 +18,7 @@ class CreateAssetContext;
/// <summary>
/// Create/Import new asset callback result
/// </summary>
DECLARE_ENUM_7(CreateAssetResult, Ok, Abort, Error, CannotSaveFile, InvalidPath, CannotAllocateChunk, InvalidTypeID);
DECLARE_ENUM_8(CreateAssetResult, Ok, Abort, Error, CannotSaveFile, InvalidPath, CannotAllocateChunk, InvalidTypeID, Skip);
/// <summary>
/// Create/Import new asset callback function

View File

@@ -734,9 +734,7 @@ public:
}
else
{
Array tmp = MoveTemp(other);
other = *this;
*this = MoveTemp(tmp);
::Swap(other, *this);
}
}

View File

@@ -55,9 +55,7 @@ public:
for (int32 i = 0; i < obj.Count(); i++)
{
if (predicate(obj[i]))
{
return i;
}
}
return INVALID_INDEX;
}
@@ -74,9 +72,7 @@ public:
for (int32 i = 0; i < obj.Count(); i++)
{
if (predicate(obj[i]))
{
return true;
}
}
return false;
}
@@ -93,13 +89,101 @@ public:
for (int32 i = 0; i < obj.Count(); i++)
{
if (!predicate(obj[i]))
{
return false;
}
}
return true;
}
/// <summary>
/// Filters a sequence of values based on a predicate.
/// </summary>
/// <param name="obj">The target collection.</param>
/// <param name="predicate">The prediction function. Return true for elements that should be included in result list.</param>
/// <param name="result">The result list with items that passed the predicate.</param>
template<typename T, typename AllocationType>
static void Where(const Array<T, AllocationType>& obj, const Function<bool(const T&)>& predicate, Array<T, AllocationType>& result)
{
for (const T& i : obj)
{
if (predicate(i))
result.Add(i);
}
}
/// <summary>
/// Filters a sequence of values based on a predicate.
/// </summary>
/// <param name="obj">The target collection.</param>
/// <param name="predicate">The prediction function. Return true for elements that should be included in result list.</param>
/// <returns>The result list with items that passed the predicate.</returns>
template<typename T, typename AllocationType>
static Array<T, AllocationType> Where(const Array<T, AllocationType>& obj, const Function<bool(const T&)>& predicate)
{
Array<T, AllocationType> result;
Where(obj, predicate, result);
return result;
}
/// <summary>
/// Projects each element of a sequence into a new form.
/// </summary>
/// <param name="obj">The target collection.</param>
/// <param name="selector">A transform function to apply to each source element; the second parameter of the function represents the index of the source element.</param>
/// <param name="result">The result list whose elements are the result of invoking the transform function on each element of source.</param>
template<typename TResult, typename TSource, typename AllocationType>
static void Select(const Array<TSource, AllocationType>& obj, const Function<TResult(const TSource&)>& selector, Array<TResult, AllocationType>& result)
{
for (const TSource& i : obj)
result.Add(MoveTemp(selector(i)));
}
/// <summary>
/// Projects each element of a sequence into a new form.
/// </summary>
/// <param name="obj">The target collection.</param>
/// <param name="selector">A transform function to apply to each source element; the second parameter of the function represents the index of the source element.</param>
/// <returns>The result list whose elements are the result of invoking the transform function on each element of source.</returns>
template<typename TResult, typename TSource, typename AllocationType>
static Array<TResult, AllocationType> Select(const Array<TSource, AllocationType>& obj, const Function<TResult(const TSource&)>& selector)
{
Array<TResult, AllocationType> result;
Select(obj, selector, result);
return result;
}
/// <summary>
/// Removes all the elements that match the conditions defined by the specified predicate.
/// </summary>
/// <param name="obj">The target collection to modify.</param>
/// <param name="predicate">A transform function that defines the conditions of the elements to remove.</param>
template<typename T, typename AllocationType>
static void RemoveAll(Array<T, AllocationType>& obj, const Function<bool(const T&)>& predicate)
{
for (int32 i = obj.Count() - 1; i >= 0; i--)
{
if (predicate(obj[i]))
obj.RemoveAtKeepOrder(i);
}
}
/// <summary>
/// Removes all the elements that match the conditions defined by the specified predicate.
/// </summary>
/// <param name="obj">The target collection to process.</param>
/// <param name="predicate">A transform function that defines the conditions of the elements to remove.</param>
/// <returns>The result list whose elements are the result of invoking the transform function on each element of source.</returns>
template<typename T, typename AllocationType>
static Array<T, AllocationType> RemoveAll(const Array<T, AllocationType>& obj, const Function<bool(const T&)>& predicate)
{
Array<T, AllocationType> result;
for (const T& i : obj)
{
if (!predicate(i))
result.Ass(i);
}
return result;
}
/// <summary>
/// Groups the elements of a sequence according to a specified key selector function.
/// </summary>
@@ -109,7 +193,7 @@ public:
template<typename TSource, typename TKey, typename AllocationType>
static void GroupBy(const Array<TSource, AllocationType>& obj, const Function<TKey(TSource const&)>& keySelector, Array<IGrouping<TKey, TSource>, AllocationType>& result)
{
Dictionary<TKey, IGrouping<TKey, TSource>> data(static_cast<int32>(obj.Count() * 3.0f));
Dictionary<TKey, IGrouping<TKey, TSource>> data;
for (int32 i = 0; i < obj.Count(); i++)
{
const TKey key = keySelector(obj[i]);

View File

@@ -616,9 +616,7 @@ public:
}
else
{
Dictionary tmp = MoveTemp(other);
other = *this;
*this = MoveTemp(tmp);
::Swap(other, *this);
}
}

View File

@@ -45,6 +45,16 @@ public:
};
public:
/// <summary>
/// Sorts the linear data array using Quick Sort algorithm (non recursive version, uses temporary stack collection).
/// </summary>
/// <param name="data">The data container.</param>
template<typename T, typename AllocationType = HeapAllocation>
FORCE_INLINE static void QuickSort(Array<T, AllocationType>& data)
{
QuickSort(data.Get(), data.Count());
}
/// <summary>
/// Sorts the linear data array using Quick Sort algorithm (non recursive version, uses temporary stack collection).
/// </summary>

View File

@@ -58,7 +58,7 @@ bool Log::Logger::Init()
int32 remaining = oldLogs.Count() - maxLogFiles + 1;
if (remaining > 0)
{
Sorting::QuickSort(oldLogs.Get(), oldLogs.Count());
Sorting::QuickSort(oldLogs);
// Delete the oldest logs
int32 i = 0;

View File

@@ -15,6 +15,8 @@ const Float2 Float2::Zero(0.0f);
template<>
const Float2 Float2::One(1.0f);
template<>
const Float2 Float2::Half(0.5f);
template<>
const Float2 Float2::UnitX(1.0f, 0.0f);
template<>
const Float2 Float2::UnitY(0.0f, 1.0f);

View File

@@ -44,6 +44,9 @@ public:
// Vector with all components equal 1
static FLAXENGINE_API const Vector2Base<T> One;
// Vector with all components equal 0.5
static FLAXENGINE_API const Vector2Base<T> Half;
// Vector X=1, Y=0
static FLAXENGINE_API const Vector2Base<T> UnitX;

View File

@@ -17,6 +17,8 @@ const Float4 Float4::Zero(0.0f);
template<>
const Float4 Float4::One(1.0f);
template<>
const Float4 Float4::Half(0.5f);
template<>
const Float4 Float4::UnitX(1.0f, 0.0f, 0.0f, 0.0f);
template<>
const Float4 Float4::UnitY(0.0f, 1.0f, 0.0f, 0.0f);

View File

@@ -54,6 +54,9 @@ public:
// Vector with all components equal 1
static FLAXENGINE_API const Vector4Base<T> One;
// Vector with all components equal 0.5
static FLAXENGINE_API const Vector4Base<T> Half;
// Vector X=1, Y=0, Z=0, W=0
static FLAXENGINE_API const Vector4Base<T> UnitX;

View File

@@ -304,7 +304,7 @@ template<typename T>
inline void Swap(T& a, T& b) noexcept
{
T tmp = MoveTemp(a);
a = b;
a = MoveTemp(b);
b = MoveTemp(tmp);
}

View File

@@ -1331,39 +1331,50 @@ namespace FlaxEngine.Interop
// Skip using in-built delegate for value types (eg. Transform) to properly handle instance value passing to method
if (invokeDelegate == null && !method.DeclaringType.IsValueType)
{
List<Type> methodTypes = new List<Type>();
if (!method.IsStatic)
methodTypes.Add(method.DeclaringType);
if (returnType != typeof(void))
methodTypes.Add(returnType);
methodTypes.AddRange(parameterTypes);
List<Type> genericParamTypes = new List<Type>();
foreach (var type in methodTypes)
// Thread-safe creation
lock (typeCache)
{
if (type.IsByRef)
genericParamTypes.Add(type.GetElementType());
else if (type.IsPointer)
genericParamTypes.Add(typeof(IntPtr));
else
genericParamTypes.Add(type);
}
string invokerTypeName = $"{typeof(Invoker).FullName}+Invoker{(method.IsStatic ? "Static" : "")}{(returnType != typeof(void) ? "Ret" : "NoRet")}{parameterTypes.Length}{(genericParamTypes.Count > 0 ? "`" + genericParamTypes.Count : "")}";
Type invokerType = Type.GetType(invokerTypeName);
if (invokerType != null)
{
if (genericParamTypes.Count != 0)
invokerType = invokerType.MakeGenericType(genericParamTypes.ToArray());
invokeDelegate = invokerType.GetMethod(nameof(Invoker.InvokerStaticNoRet0.MarshalAndInvoke), BindingFlags.Static | BindingFlags.NonPublic).CreateDelegate<Invoker.MarshalAndInvokeDelegate>();
delegInvoke = invokerType.GetMethod(nameof(Invoker.InvokerStaticNoRet0.CreateDelegate), BindingFlags.Static | BindingFlags.NonPublic).Invoke(null, new object[] { method });
if (invokeDelegate == null)
{
TryCreateDelegate();
}
}
}
outDeleg = invokeDelegate;
outDelegInvoke = delegInvoke;
return outDeleg != null;
}
private void TryCreateDelegate()
{
var methodTypes = new List<Type>();
if (!method.IsStatic)
methodTypes.Add(method.DeclaringType);
if (returnType != typeof(void))
methodTypes.Add(returnType);
methodTypes.AddRange(parameterTypes);
var genericParamTypes = new List<Type>();
foreach (var type in methodTypes)
{
if (type.IsByRef)
genericParamTypes.Add(type.GetElementType());
else if (type.IsPointer)
genericParamTypes.Add(typeof(IntPtr));
else
genericParamTypes.Add(type);
}
string invokerTypeName = $"{typeof(Invoker).FullName}+Invoker{(method.IsStatic ? "Static" : "")}{(returnType != typeof(void) ? "Ret" : "NoRet")}{parameterTypes.Length}{(genericParamTypes.Count > 0 ? "`" + genericParamTypes.Count : "")}";
Type invokerType = Type.GetType(invokerTypeName);
if (invokerType != null)
{
if (genericParamTypes.Count != 0)
invokerType = invokerType.MakeGenericType(genericParamTypes.ToArray());
delegInvoke = invokerType.GetMethod(nameof(Invoker.InvokerStaticNoRet0.CreateDelegate), BindingFlags.Static | BindingFlags.NonPublic).Invoke(null, new object[] { method });
invokeDelegate = invokerType.GetMethod(nameof(Invoker.InvokerStaticNoRet0.MarshalAndInvoke), BindingFlags.Static | BindingFlags.NonPublic).CreateDelegate<Invoker.MarshalAndInvokeDelegate>();
}
}
#endif
}

View File

@@ -104,7 +104,7 @@ bool Time::TickData::OnTickBegin(float targetFps, float maxDeltaTime)
if (targetFps > ZeroTolerance)
{
int skip = (int)(1 + (time - NextBegin) / (1.0 / targetFps));
int skip = (int)(1 + (time - NextBegin) * targetFps);
NextBegin += (1.0 / targetFps) * skip;
}
}
@@ -160,7 +160,7 @@ bool Time::FixedStepTickData::OnTickBegin(float targetFps, float maxDeltaTime)
if (targetFps > ZeroTolerance)
{
int skip = (int)(1 + (time - NextBegin) / (1.0 / targetFps));
int skip = (int)(1 + (time - NextBegin) * targetFps);
NextBegin += (1.0 / targetFps) * skip;
}
}

View File

@@ -10,6 +10,7 @@
#include "Engine/Core/Collections/BitArray.h"
#include "Engine/Tools/ModelTool/ModelTool.h"
#include "Engine/Tools/ModelTool/VertexTriangleAdjacency.h"
#include "Engine/Profiler/ProfilerCPU.h"
#include "Engine/Platform/Platform.h"
#define USE_MIKKTSPACE 1
#include "ThirdParty/MikkTSpace/mikktspace.h"
@@ -78,6 +79,7 @@ void RemapArrayHelper(Array<T>& target, const std::vector<uint32_t>& remap)
bool MeshData::GenerateLightmapUVs()
{
PROFILE_CPU();
#if PLATFORM_WINDOWS
// Prepare
HRESULT hr;
@@ -235,6 +237,7 @@ void RemapBuffer(Array<T>& src, Array<T>& dst, const Array<int32>& mapping, int3
void MeshData::BuildIndexBuffer()
{
PROFILE_CPU();
const auto startTime = Platform::GetTimeSeconds();
const int32 vertexCount = Positions.Count();
@@ -341,6 +344,7 @@ bool MeshData::GenerateNormals(float smoothingAngle)
LOG(Warning, "Missing vertex or index data to generate normals.");
return true;
}
PROFILE_CPU();
const auto startTime = Platform::GetTimeSeconds();
@@ -520,6 +524,7 @@ bool MeshData::GenerateTangents(float smoothingAngle)
LOG(Warning, "Missing normals or texcoors data to generate tangents.");
return true;
}
PROFILE_CPU();
const auto startTime = Platform::GetTimeSeconds();
const int32 vertexCount = Positions.Count();
@@ -706,6 +711,7 @@ void MeshData::ImproveCacheLocality()
if (Positions.IsEmpty() || Indices.IsEmpty() || Positions.Count() <= VertexCacheSize)
return;
PROFILE_CPU();
const auto startTime = Platform::GetTimeSeconds();
@@ -886,6 +892,7 @@ void MeshData::ImproveCacheLocality()
float MeshData::CalculateTrianglesArea() const
{
PROFILE_CPU();
float sum = 0;
// TODO: use SIMD
for (int32 i = 0; i + 2 < Indices.Count(); i += 3)

View File

@@ -625,6 +625,11 @@ bool MaterialSlotEntry::UsesProperties() const
Normals.TextureIndex != -1;
}
ModelLodData::~ModelLodData()
{
Meshes.ClearDelete();
}
BoundingBox ModelLodData::GetBox() const
{
if (Meshes.IsEmpty())
@@ -644,11 +649,9 @@ void ModelData::CalculateLODsScreenSizes()
{
const float autoComputeLodPowerBase = 0.5f;
const int32 lodCount = LODs.Count();
for (int32 lodIndex = 0; lodIndex < lodCount; lodIndex++)
{
auto& lod = LODs[lodIndex];
if (lodIndex == 0)
{
lod.ScreenSize = 1.0f;
@@ -675,6 +678,8 @@ void ModelData::TransformBuffer(const Matrix& matrix)
}
}
#if USE_EDITOR
bool ModelData::Pack2ModelHeader(WriteStream* stream) const
{
// Validate input
@@ -724,7 +729,12 @@ bool ModelData::Pack2ModelHeader(WriteStream* stream) const
// Amount of meshes
const int32 meshes = lod.Meshes.Count();
if (meshes == 0 || meshes > MODEL_MAX_MESHES)
if (meshes == 0)
{
LOG(Warning, "Empty LOD.");
return true;
}
if (meshes > MODEL_MAX_MESHES)
{
LOG(Warning, "Too many meshes per LOD.");
return true;
@@ -880,20 +890,21 @@ bool ModelData::Pack2SkinnedModelHeader(WriteStream* stream) const
return false;
}
bool ModelData::Pack2AnimationHeader(WriteStream* stream) const
bool ModelData::Pack2AnimationHeader(WriteStream* stream, int32 animIndex) const
{
// Validate input
if (stream == nullptr)
if (stream == nullptr || animIndex < 0 || animIndex >= Animations.Count())
{
Log::ArgumentNullException();
return true;
}
if (Animation.Duration <= ZeroTolerance || Animation.FramesPerSecond <= ZeroTolerance)
auto& anim = Animations.Get()[animIndex];
if (anim.Duration <= ZeroTolerance || anim.FramesPerSecond <= ZeroTolerance)
{
Log::InvalidOperationException(TEXT("Invalid animation duration."));
return true;
}
if (Animation.Channels.IsEmpty())
if (anim.Channels.IsEmpty())
{
Log::ArgumentOutOfRangeException(TEXT("Channels"), TEXT("Animation channels collection cannot be empty."));
return true;
@@ -901,22 +912,23 @@ bool ModelData::Pack2AnimationHeader(WriteStream* stream) const
// Info
stream->WriteInt32(100); // Header version (for fast version upgrades without serialization format change)
stream->WriteDouble(Animation.Duration);
stream->WriteDouble(Animation.FramesPerSecond);
stream->WriteBool(Animation.EnableRootMotion);
stream->WriteString(Animation.RootNodeName, 13);
stream->WriteDouble(anim.Duration);
stream->WriteDouble(anim.FramesPerSecond);
stream->WriteBool(anim.EnableRootMotion);
stream->WriteString(anim.RootNodeName, 13);
// Animation channels
stream->WriteInt32(Animation.Channels.Count());
for (int32 i = 0; i < Animation.Channels.Count(); i++)
stream->WriteInt32(anim.Channels.Count());
for (int32 i = 0; i < anim.Channels.Count(); i++)
{
auto& anim = Animation.Channels[i];
stream->WriteString(anim.NodeName, 172);
Serialization::Serialize(*stream, anim.Position);
Serialization::Serialize(*stream, anim.Rotation);
Serialization::Serialize(*stream, anim.Scale);
auto& channel = anim.Channels[i];
stream->WriteString(channel.NodeName, 172);
Serialization::Serialize(*stream, channel.Position);
Serialization::Serialize(*stream, channel.Rotation);
Serialization::Serialize(*stream, channel.Scale);
}
return false;
}
#endif

View File

@@ -366,12 +366,32 @@ struct FLAXENGINE_API MaterialSlotEntry
bool UsesProperties() const;
};
/// <summary>
/// Data container for model hierarchy node.
/// </summary>
struct FLAXENGINE_API ModelDataNode
{
/// <summary>
/// The parent node index. The root node uses value -1.
/// </summary>
int32 ParentIndex;
/// <summary>
/// The local transformation of the node, relative to the parent node.
/// </summary>
Transform LocalTransform;
/// <summary>
/// The name of this node.
/// </summary>
String Name;
};
/// <summary>
/// Data container for LOD metadata and sub meshes.
/// </summary>
class FLAXENGINE_API ModelLodData
struct FLAXENGINE_API ModelLodData
{
public:
/// <summary>
/// The screen size to switch LODs. Bottom limit of the model screen size to render this LOD.
/// </summary>
@@ -382,21 +402,10 @@ public:
/// </summary>
Array<MeshData*> Meshes;
public:
/// <summary>
/// Initializes a new instance of the <see cref="ModelLodData"/> class.
/// </summary>
ModelLodData()
{
}
/// <summary>
/// Finalizes an instance of the <see cref="ModelLodData"/> class.
/// </summary>
~ModelLodData()
{
Meshes.ClearDelete();
}
~ModelLodData();
/// <summary>
/// Gets the bounding box combined for all meshes in this model LOD.
@@ -426,7 +435,7 @@ public:
Array<MaterialSlotEntry> Materials;
/// <summary>
/// Array with all LODs. The first element is the top most LOD0 followed by the LOD1, LOD2, etc.
/// Array with all Level Of Details that contain meshes. The first element is the top most LOD0 followed by the LOD1, LOD2, etc.
/// </summary>
Array<ModelLodData> LODs;
@@ -435,24 +444,20 @@ public:
/// </summary>
SkeletonData Skeleton;
/// <summary>
/// The scene nodes (in hierarchy).
/// </summary>
Array<ModelDataNode> Nodes;
/// <summary>
/// The node animations.
/// </summary>
AnimationData Animation;
public:
/// <summary>
/// Initializes a new instance of the <see cref="ModelData"/> class.
/// </summary>
ModelData()
{
}
Array<AnimationData> Animations;
public:
/// <summary>
/// Gets the valid level of details count.
/// </summary>
/// <returns>The LOD count.</returns>
FORCE_INLINE int32 GetLODsCount() const
{
return LODs.Count();
@@ -461,7 +466,6 @@ public:
/// <summary>
/// Determines whether this instance has valid skeleton structure.
/// </summary>
/// <returns>True if has skeleton, otherwise false.</returns>
FORCE_INLINE bool HasSkeleton() const
{
return Skeleton.Bones.HasItems();
@@ -479,6 +483,7 @@ public:
/// <param name="matrix">The matrix to use for the transformation.</param>
void TransformBuffer(const Matrix& matrix);
#if USE_EDITOR
public:
/// <summary>
/// Pack mesh data to the header stream
@@ -498,6 +503,8 @@ public:
/// Pack animation data to the header stream
/// </summary>
/// <param name="stream">Output stream</param>
/// <param name="animIndex">Index of animation.</param>
/// <returns>True if cannot save data, otherwise false</returns>
bool Pack2AnimationHeader(WriteStream* stream) const;
bool Pack2AnimationHeader(WriteStream* stream, int32 animIndex = 0) const;
#endif
};

View File

@@ -36,7 +36,7 @@ public:
/// </summary>
/// <param name="sourceSkeleton">The source model skeleton.</param>
/// <param name="targetSkeleton">The target skeleton. May be null to disable nodes mapping.</param>
SkeletonMapping(Items& sourceSkeleton, Items* targetSkeleton)
SkeletonMapping(const Items& sourceSkeleton, const Items* targetSkeleton)
{
Size = sourceSkeleton.Count();
SourceToTarget.Resize(Size); // model => skeleton mapping

View File

@@ -660,6 +660,37 @@ int PixelFormatExtensions::ComputeComponentsCount(const PixelFormat format)
}
}
int32 PixelFormatExtensions::ComputeBlockSize(PixelFormat format)
{
switch (format)
{
case PixelFormat::BC1_Typeless:
case PixelFormat::BC1_UNorm:
case PixelFormat::BC1_UNorm_sRGB:
case PixelFormat::BC2_Typeless:
case PixelFormat::BC2_UNorm:
case PixelFormat::BC2_UNorm_sRGB:
case PixelFormat::BC3_Typeless:
case PixelFormat::BC3_UNorm:
case PixelFormat::BC3_UNorm_sRGB:
case PixelFormat::BC4_Typeless:
case PixelFormat::BC4_UNorm:
case PixelFormat::BC4_SNorm:
case PixelFormat::BC5_Typeless:
case PixelFormat::BC5_UNorm:
case PixelFormat::BC5_SNorm:
case PixelFormat::BC6H_Typeless:
case PixelFormat::BC6H_Uf16:
case PixelFormat::BC6H_Sf16:
case PixelFormat::BC7_Typeless:
case PixelFormat::BC7_UNorm:
case PixelFormat::BC7_UNorm_sRGB:
return 4;
default:
return 1;
}
}
PixelFormat PixelFormatExtensions::TosRGB(const PixelFormat format)
{
switch (format)

View File

@@ -173,6 +173,13 @@ public:
/// <returns>The components count.</returns>
API_FUNCTION() static int ComputeComponentsCount(PixelFormat format);
/// <summary>
/// Computes the amount of pixels per-axis stored in the a single block of the format (eg. 4 for BC-family). Returns 1 for uncompressed formats.
/// </summary>
/// <param name="format">The <see cref="PixelFormat"/>.</param>
/// <returns>The block pixels count.</returns>
API_FUNCTION() static int32 ComputeBlockSize(PixelFormat format);
/// <summary>
/// Finds the equivalent sRGB format to the provided format.
/// </summary>

View File

@@ -13,7 +13,7 @@ struct RenderContext;
/// Custom PostFx which can modify final image by processing it with material based filters. The base class for all post process effects used by the graphics pipeline. Allows to extend frame rendering logic and apply custom effects such as outline, night vision, contrast etc.
/// </summary>
/// <remarks>
/// Override this class and implement custom post fx logic. Use <b>MainRenderTask.Instance.CustomPostFx.Add(myPostFx)</b> to attach your script to rendering or add script to camera actor.
/// Override this class and implement custom post fx logic. Use <b>MainRenderTask.Instance.AddCustomPostFx(myPostFx)</b> to attach your script to rendering or add script to camera actor.
/// </remarks>
API_CLASS(Abstract) class FLAXENGINE_API PostProcessEffect : public Script
{

View File

@@ -192,6 +192,9 @@ bool RenderBuffers::Init(int32 width, int32 height)
_viewport = Viewport(0, 0, static_cast<float>(width), static_cast<float>(height));
LastEyeAdaptationTime = 0;
// Flush any pool render targets to prevent over-allocating GPU memory when resizing game viewport
RenderTargetPool::Flush(false, 4);
return result;
}

View File

@@ -7,35 +7,31 @@
struct Entry
{
bool IsOccupied;
GPUTexture* RT;
uint64 LastFrameTaken;
uint64 LastFrameReleased;
uint32 DescriptionHash;
bool IsOccupied;
};
namespace
{
Array<Entry> TemporaryRTs(64);
Array<Entry> TemporaryRTs;
}
void RenderTargetPool::Flush(bool force)
void RenderTargetPool::Flush(bool force, int32 framesOffset)
{
const uint64 framesOffset = 3 * 60;
if (framesOffset < 0)
framesOffset = 3 * 60; // For how many frames RTs should be cached (by default)
const uint64 maxReleaseFrame = Engine::FrameCount - framesOffset;
force |= Engine::ShouldExit();
for (int32 i = 0; i < TemporaryRTs.Count(); i++)
{
auto& tmp = TemporaryRTs[i];
if (!tmp.IsOccupied && (force || (tmp.LastFrameReleased < maxReleaseFrame)))
const auto& e = TemporaryRTs[i];
if (!e.IsOccupied && (force || e.LastFrameReleased < maxReleaseFrame))
{
// Release
tmp.RT->DeleteObjectNow();
TemporaryRTs.RemoveAt(i);
i--;
e.RT->DeleteObjectNow();
TemporaryRTs.RemoveAt(i--);
if (TemporaryRTs.IsEmpty())
break;
}
@@ -48,19 +44,14 @@ GPUTexture* RenderTargetPool::Get(const GPUTextureDescription& desc)
const uint32 descHash = GetHash(desc);
for (int32 i = 0; i < TemporaryRTs.Count(); i++)
{
auto& tmp = TemporaryRTs[i];
if (!tmp.IsOccupied && tmp.DescriptionHash == descHash)
auto& e = TemporaryRTs[i];
if (!e.IsOccupied && e.DescriptionHash == descHash)
{
ASSERT(tmp.RT);
// Mark as used
tmp.IsOccupied = true;
tmp.LastFrameTaken = Engine::FrameCount;
return tmp.RT;
e.IsOccupied = true;
return e.RT;
}
}
#if !BUILD_RELEASE
if (TemporaryRTs.Count() > 2000)
{
@@ -71,24 +62,23 @@ GPUTexture* RenderTargetPool::Get(const GPUTextureDescription& desc)
// Create new rt
const String name = TEXT("TemporaryRT_") + StringUtils::ToString(TemporaryRTs.Count());
auto newRenderTarget = GPUDevice::Instance->CreateTexture(name);
if (newRenderTarget->Init(desc))
GPUTexture* rt = GPUDevice::Instance->CreateTexture(name);
if (rt->Init(desc))
{
Delete(newRenderTarget);
Delete(rt);
LOG(Error, "Cannot create temporary render target. Description: {0}", desc.ToString());
return nullptr;
}
// Create temporary rt entry
Entry entry;
entry.IsOccupied = true;
entry.LastFrameReleased = 0;
entry.LastFrameTaken = Engine::FrameCount;
entry.RT = newRenderTarget;
entry.DescriptionHash = descHash;
TemporaryRTs.Add(entry);
Entry e;
e.IsOccupied = true;
e.LastFrameReleased = 0;
e.RT = rt;
e.DescriptionHash = descHash;
TemporaryRTs.Add(e);
return newRenderTarget;
return rt;
}
void RenderTargetPool::Release(GPUTexture* rt)
@@ -98,14 +88,13 @@ void RenderTargetPool::Release(GPUTexture* rt)
for (int32 i = 0; i < TemporaryRTs.Count(); i++)
{
auto& tmp = TemporaryRTs[i];
if (tmp.RT == rt)
auto& e = TemporaryRTs[i];
if (e.RT == rt)
{
// Mark as free
ASSERT(tmp.IsOccupied);
tmp.IsOccupied = false;
tmp.LastFrameReleased = Engine::FrameCount;
ASSERT(e.IsOccupied);
e.IsOccupied = false;
e.LastFrameReleased = Engine::FrameCount;
return;
}
}

View File

@@ -15,7 +15,8 @@ public:
/// Flushes the temporary render targets.
/// </summary>
/// <param name="force">True if release unused render targets by force, otherwise will use a few frames of delay.</param>
static void Flush(bool force = false);
/// <param name="framesOffset">Amount of previous frames that should persist in the pool after flush. Resources used more than given value wil be freed. Use value of -1 to auto pick default duration.</param>
static void Flush(bool force = false, int32 framesOffset = -1);
/// <summary>
/// Gets a temporary render target.

View File

@@ -115,27 +115,20 @@ bool ShaderAssetBase::Save()
bool IsValidShaderCache(DataContainer<byte>& shaderCache, Array<String>& includes)
{
if (shaderCache.Length() == 0)
{
return false;
}
MemoryReadStream stream(shaderCache.Get(), shaderCache.Length());
// Read cache format version
int32 version;
stream.ReadInt32(&version);
if (version != GPU_SHADER_CACHE_VERSION)
{
return false;
}
// Read the location of additional data that contains list of included source files
int32 additionalDataStart;
stream.ReadInt32(&additionalDataStart);
stream.SetPosition(additionalDataStart);
bool result = true;
// Read all includes
int32 includesCount;
stream.ReadInt32(&includesCount);
@@ -144,28 +137,16 @@ bool IsValidShaderCache(DataContainer<byte>& shaderCache, Array<String>& include
{
String& include = includes.AddOne();
stream.ReadString(&include, 11);
include = ShadersCompilation::ResolveShaderPath(include);
DateTime lastEditTime;
stream.Read(lastEditTime);
// Check if included file exists locally and has been modified since last compilation
if (FileSystem::FileExists(include) && FileSystem::GetFileLastEditTime(include) > lastEditTime)
{
result = false;
}
return false;
}
#if 0
// Check duplicates
for (int32 i = 0; i < includes.Count(); i++)
{
if (includes.FindLast(includes[i]) != i)
{
CRASH;
}
}
#endif
return result;
return true;
}
#endif

View File

@@ -480,6 +480,16 @@ bool GPUTexture::Init(const GPUTextureDescription& desc)
break;
}
}
const bool isCompressed = PixelFormatExtensions::IsCompressed(desc.Format);
if (isCompressed)
{
const int32 blockSize = PixelFormatExtensions::ComputeBlockSize(desc.Format);
if (desc.Width < blockSize || desc.Height < blockSize)
{
LOG(Warning, "Cannot create texture. Invalid dimensions. Description: {0}", desc.ToString());
return true;
}
}
// Release previous data
ReleaseGPU();
@@ -487,7 +497,7 @@ bool GPUTexture::Init(const GPUTextureDescription& desc)
// Initialize
_desc = desc;
_sRGB = PixelFormatExtensions::IsSRGB(desc.Format);
_isBlockCompressed = PixelFormatExtensions::IsCompressed(desc.Format);
_isBlockCompressed = isCompressed;
if (OnInit())
{
ReleaseGPU();

View File

@@ -114,9 +114,9 @@ bool StreamingTexture::Create(const TextureHeader& header)
{
// Ensure that streaming doesn't go too low because the hardware expects the texture to be min in size of compressed texture block
int32 lastMip = header.MipLevels - 1;
while ((header.Width >> lastMip) < 4 && (header.Height >> lastMip) < 4)
while ((header.Width >> lastMip) < 4 && (header.Height >> lastMip) < 4 && lastMip > 0)
lastMip--;
_minMipCountBlockCompressed = header.MipLevels - lastMip + 1;
_minMipCountBlockCompressed = Math::Min(header.MipLevels - lastMip + 1, header.MipLevels);
}
// Request resource streaming
@@ -296,6 +296,7 @@ Task* StreamingTexture::UpdateAllocation(int32 residency)
// Setup texture
if (texture->Init(desc))
{
Streaming.Error = true;
LOG(Error, "Cannot allocate texture {0}.", ToString());
}
if (allocatedResidency != 0)

View File

@@ -223,6 +223,11 @@ void TextureBase::SetTextureGroup(int32 textureGroup)
}
}
bool TextureBase::HasStreamingError() const
{
return _texture.Streaming.Error;
}
BytesContainer TextureBase::GetMipData(int32 mipIndex, int32& rowPitch, int32& slicePitch)
{
BytesContainer result;

View File

@@ -148,6 +148,11 @@ public:
/// </summary>
API_PROPERTY() void SetTextureGroup(int32 textureGroup);
/// <summary>
/// Returns true if texture streaming failed (eg. pixel format is unsupported or texture data cannot be uploaded to GPU due to memory limit).
/// </summary>
API_PROPERTY() bool HasStreamingError() const;
public:
/// <summary>
/// Gets the mip data.

View File

@@ -1,61 +0,0 @@
// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
#pragma once
#include "Engine/Core/Types/BaseTypes.h"
#include "Types.h"
/// <summary>
/// Texture utilities class
/// </summary>
class TextureUtils
{
public:
static PixelFormat ToPixelFormat(const TextureFormatType format, int32 width, int32 height, bool canCompress)
{
const bool canUseBlockCompression = width % 4 == 0 && height % 4 == 0;
if (canCompress && canUseBlockCompression)
{
switch (format)
{
case TextureFormatType::ColorRGB:
return PixelFormat::BC1_UNorm;
case TextureFormatType::ColorRGBA:
return PixelFormat::BC3_UNorm;
case TextureFormatType::NormalMap:
return PixelFormat::BC5_UNorm;
case TextureFormatType::GrayScale:
return PixelFormat::BC4_UNorm;
case TextureFormatType::HdrRGBA:
return PixelFormat::BC7_UNorm;
case TextureFormatType::HdrRGB:
#if PLATFORM_LINUX
// TODO: support BC6H compression for Linux Editor
return PixelFormat::BC7_UNorm;
#else
return PixelFormat::BC6H_Uf16;
#endif
default:
return PixelFormat::Unknown;
}
}
switch (format)
{
case TextureFormatType::ColorRGB:
return PixelFormat::R8G8B8A8_UNorm;
case TextureFormatType::ColorRGBA:
return PixelFormat::R8G8B8A8_UNorm;
case TextureFormatType::NormalMap:
return PixelFormat::R16G16_UNorm;
case TextureFormatType::GrayScale:
return PixelFormat::R8_UNorm;
case TextureFormatType::HdrRGBA:
return PixelFormat::R16G16B16A16_Float;
case TextureFormatType::HdrRGB:
return PixelFormat::R11G11B10_Float;
default:
return PixelFormat::Unknown;
}
}
};

View File

@@ -247,7 +247,7 @@ void GPUDeviceVulkan::GetInstanceLayersAndExtensions(Array<const char*>& outInst
if (foundUniqueLayers.HasItems())
{
LOG(Info, "Found instance layers:");
Sorting::QuickSort(foundUniqueLayers.Get(), foundUniqueLayers.Count());
Sorting::QuickSort(foundUniqueLayers);
for (const StringAnsi& name : foundUniqueLayers)
{
LOG(Info, "- {0}", String(name));
@@ -257,7 +257,7 @@ void GPUDeviceVulkan::GetInstanceLayersAndExtensions(Array<const char*>& outInst
if (foundUniqueExtensions.HasItems())
{
LOG(Info, "Found instance extensions:");
Sorting::QuickSort(foundUniqueExtensions.Get(), foundUniqueExtensions.Count());
Sorting::QuickSort(foundUniqueExtensions);
for (const StringAnsi& name : foundUniqueExtensions)
{
LOG(Info, "- {0}", String(name));
@@ -455,7 +455,7 @@ void GPUDeviceVulkan::GetDeviceExtensionsAndLayers(VkPhysicalDevice gpu, Array<c
if (foundUniqueLayers.HasItems())
{
LOG(Info, "Found device layers:");
Sorting::QuickSort(foundUniqueLayers.Get(), foundUniqueLayers.Count());
Sorting::QuickSort(foundUniqueLayers);
for (const StringAnsi& name : foundUniqueLayers)
{
LOG(Info, "- {0}", String(name));
@@ -465,7 +465,7 @@ void GPUDeviceVulkan::GetDeviceExtensionsAndLayers(VkPhysicalDevice gpu, Array<c
if (foundUniqueExtensions.HasItems())
{
LOG(Info, "Found device extensions:");
Sorting::QuickSort(foundUniqueExtensions.Get(), foundUniqueExtensions.Count());
Sorting::QuickSort(foundUniqueExtensions);
for (const StringAnsi& name : foundUniqueExtensions)
{
LOG(Info, "- {0}", String(name));

View File

@@ -206,10 +206,18 @@ void Actor::OnDeleteObject()
#endif
for (int32 i = 0; i < Scripts.Count(); i++)
{
auto e = Scripts[i];
ASSERT(e->_parent == this);
e->_parent = nullptr;
e->DeleteObject();
auto script = Scripts[i];
ASSERT(script->_parent == this);
if (script->_wasAwakeCalled)
{
script->_wasAwakeCalled = false;
CHECK_EXECUTE_IN_EDITOR
{
script->OnDestroy();
}
}
script->_parent = nullptr;
script->DeleteObject();
}
#if BUILD_DEBUG
ASSERT(callsCheck == Scripts.Count());
@@ -239,14 +247,8 @@ const Guid& Actor::GetSceneObjectId() const
void Actor::SetParent(Actor* value, bool worldPositionsStays, bool canBreakPrefabLink)
{
// Check if value won't change
if (_parent == value)
return;
if (IsDuringPlay() && !IsInMainThread())
{
LOG(Error, "Editing scene hierarchy is only allowed on a main thread.");
return;
}
#if USE_EDITOR || !BUILD_RELEASE
if (Is<Scene>())
{
@@ -265,6 +267,13 @@ void Actor::SetParent(Actor* value, bool worldPositionsStays, bool canBreakPrefa
// Detect it actor is not in a game but new parent is already in a game (we should spawn it)
const bool isBeingSpawned = !IsDuringPlay() && newScene && value->IsDuringPlay();
// Actors system doesn't support editing scene hierarchy from multiple threads
if (!IsInMainThread() && (IsDuringPlay() || isBeingSpawned))
{
LOG(Error, "Editing scene hierarchy is only allowed on a main thread.");
return;
}
// Handle changing scene (unregister from it)
const bool isSceneChanging = prevScene != newScene;
if (prevScene && isSceneChanging && wasActiveInTree)
@@ -888,9 +897,13 @@ void Actor::EndPlay()
for (auto* script : Scripts)
{
CHECK_EXECUTE_IN_EDITOR
if (script->_wasAwakeCalled)
{
script->OnDestroy();
script->_wasAwakeCalled = false;
CHECK_EXECUTE_IN_EDITOR
{
script->OnDestroy();
}
}
}
@@ -1033,7 +1046,10 @@ void Actor::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier)
}
else if (!parent && parentId.IsValid())
{
LOG(Warning, "Missing parent actor {0} for \'{1}\'", parentId, ToString());
if (_prefabObjectID.IsValid())
LOG(Warning, "Missing parent actor {0} for \'{1}\', prefab object {2}", parentId, ToString(), _prefabObjectID);
else
LOG(Warning, "Missing parent actor {0} for \'{1}\'", parentId, ToString());
}
}
}

View File

@@ -99,6 +99,8 @@ void SkyLight::UpdateBounds()
{
_sphere = BoundingSphere(GetPosition(), GetScaledRadius());
BoundingBox::FromSphere(_sphere, _box);
if (_sceneRenderingKey != -1)
GetSceneRendering()->UpdateActor(this, _sceneRenderingKey);
}
void SkyLight::Draw(RenderContext& renderContext)

View File

@@ -439,7 +439,7 @@ public:
bool Do() const override
{
auto scene = Scripting::FindObject<Scene>(TargetScene);
auto scene = Level::FindScene(TargetScene);
if (!scene)
return true;
return unloadScene(scene);
@@ -934,13 +934,13 @@ bool Level::loadScene(rapidjson_flax::Value& data, int32 engineBuild, Scene** ou
// Loaded scene objects list
CollectionPoolCache<ActorsCache::SceneObjectsListType>::ScopeCache sceneObjects = ActorsCache::SceneObjectsListCache.Get();
const int32 objectsCount = (int32)data.Size();
sceneObjects->Resize(objectsCount);
const int32 dataCount = (int32)data.Size();
sceneObjects->Resize(dataCount);
sceneObjects->At(0) = scene;
// Spawn all scene objects
SceneObjectsFactory::Context context(modifier.Value);
context.Async = JobSystem::GetThreadsCount() > 1 && objectsCount > 10;
context.Async = JobSystem::GetThreadsCount() > 1 && dataCount > 10;
{
PROFILE_CPU_NAMED("Spawn");
SceneObject** objects = sceneObjects->Get();
@@ -963,12 +963,12 @@ bool Level::loadScene(rapidjson_flax::Value& data, int32 engineBuild, Scene** ou
}
else
SceneObjectsFactory::HandleObjectDeserializationError(stream);
}, objectsCount - 1);
}, dataCount - 1);
ScenesLock.Lock();
}
else
{
for (int32 i = 1; i < objectsCount; i++) // start from 1. at index [0] was scene
for (int32 i = 1; i < dataCount; i++) // start from 1. at index [0] was scene
{
auto& stream = data[i];
auto obj = SceneObjectsFactory::Spawn(context, stream);
@@ -1012,13 +1012,13 @@ bool Level::loadScene(rapidjson_flax::Value& data, int32 engineBuild, Scene** ou
SceneObjectsFactory::Deserialize(context, obj, data[i]);
idMapping = nullptr;
}
}, objectsCount - 1);
}, dataCount - 1);
ScenesLock.Lock();
}
else
{
Scripting::ObjectsLookupIdMapping.Set(&modifier.Value->IdsMapping);
for (int32 i = 1; i < objectsCount; i++) // start from 1. at index [0] was scene
for (int32 i = 1; i < dataCount; i++) // start from 1. at index [0] was scene
{
auto& objData = data[i];
auto obj = objects[i];
@@ -1049,7 +1049,7 @@ bool Level::loadScene(rapidjson_flax::Value& data, int32 engineBuild, Scene** ou
PROFILE_CPU_NAMED("Initialize");
SceneObject** objects = sceneObjects->Get();
for (int32 i = 0; i < objectsCount; i++)
for (int32 i = 0; i < dataCount; i++)
{
SceneObject* obj = objects[i];
if (obj)

View File

@@ -22,6 +22,7 @@
#include "Engine/ContentImporters/CreateJson.h"
#include "Engine/Debug/Exceptions/ArgumentNullException.h"
#include "Engine/Profiler/ProfilerCPU.h"
#include "Engine/Threading/MainThreadTask.h"
#include "Editor/Editor.h"
// Apply flow:
@@ -174,6 +175,12 @@ public:
/// <param name="newObjectIds">Collection with ids of the objects (actors and scripts) from the prefab after changes apply. Used to find new objects or old objects and use this information during changes sync (eg. generate ids for the new objects to prevent ids collisions).</param>
/// <returns>True if failed, otherwise false.</returns>
static bool SynchronizePrefabInstances(PrefabInstancesData& prefabInstancesData, Actor* defaultInstance, SceneObjectsListCacheType& sceneObjects, const Guid& prefabId, rapidjson_flax::StringBuffer& tmpBuffer, const Array<Guid>& oldObjectsIds, const Array<Guid>& newObjectIds);
static void DeletePrefabObject(SceneObject* obj)
{
obj->SetParent(nullptr);
obj->DeleteObject();
}
};
void PrefabInstanceData::CollectPrefabInstances(PrefabInstancesData& prefabInstancesData, const Guid& prefabId, Actor* defaultInstance, Actor* targetActor)
@@ -302,14 +309,10 @@ bool PrefabInstanceData::SynchronizePrefabInstances(PrefabInstancesData& prefabI
{
// Remove object
LOG(Info, "Removing object {0} from instance {1} (prefab: {2})", obj->GetSceneObjectId(), instance.TargetActor->ToString(), prefabId);
obj->DeleteObject();
obj->SetParent(nullptr);
DeletePrefabObject(obj);
sceneObjects.Value->RemoveAtKeepOrder(i);
existingObjectsCount--;
i--;
continue;
}
@@ -358,10 +361,7 @@ bool PrefabInstanceData::SynchronizePrefabInstances(PrefabInstancesData& prefabI
{
// Remove object removed from the prefab
LOG(Info, "Removing prefab instance object {0} from instance {1} (prefab object: {2}, prefab: {3})", obj->GetSceneObjectId(), instance.TargetActor->ToString(), obj->GetPrefabObjectID(), prefabId);
obj->DeleteObject();
obj->SetParent(nullptr);
DeletePrefabObject(obj);
sceneObjects.Value->RemoveAtKeepOrder(i);
deserializeSceneObjectIndex--;
existingObjectsCount--;
@@ -633,6 +633,19 @@ bool Prefab::ApplyAll(Actor* targetActor)
}
}
}
if (!IsInMainThread())
{
// Prefabs cannot be updated on async thread so sync it with a Main Thread
bool result = true;
Function<void()> action = [&]
{
result = ApplyAll(targetActor);
};
const auto task = Task::StartNew(New<MainThreadActionTask>(action));
if (task->Wait(TimeSpan::FromSeconds(10)))
result = true;
return result;
}
// Prevent cyclic references
{
@@ -921,9 +934,7 @@ bool Prefab::ApplyAllInternal(Actor* targetActor, bool linkTargetActorObjectToPr
{
// Remove object removed from the prefab
LOG(Info, "Removing object {0} from prefab default instance", obj->GetSceneObjectId());
obj->DeleteObject();
obj->SetParent(nullptr);
PrefabInstanceData::DeletePrefabObject(obj);
sceneObjects->At(i) = nullptr;
}
}
@@ -1219,14 +1230,14 @@ bool Prefab::SyncChangesInternal(PrefabInstancesData& prefabInstancesData)
{
ScopeLock lock(Locker);
_isCreatingDefaultInstance = true;
_defaultInstance = PrefabManager::SpawnPrefab(this, Transform::Identity, nullptr, &ObjectsCache, true);
_defaultInstance = PrefabManager::SpawnPrefab(this, nullptr, &ObjectsCache, true);
_isCreatingDefaultInstance = false;
}
// Instantiate prefab instance from prefab (default spawning logic)
// Note: it will get any added or removed objects from the nested prefabs
// TODO: try to optimize by using recreated default instance to ApplyAllInternal (will need special path there if apply is done with default instance to unlink it instead of destroying)
const auto targetActor = PrefabManager::SpawnPrefab(this, Transform::Identity, nullptr, nullptr, true);
const auto targetActor = PrefabManager::SpawnPrefab(this, nullptr, nullptr, true);
if (targetActor == nullptr)
{
LOG(Warning, "Failed to instantiate default prefab instance from changes synchronization.");

View File

@@ -76,7 +76,7 @@ Actor* Prefab::GetDefaultInstance()
_isCreatingDefaultInstance = true;
// Instantiate objects from prefab (default spawning logic)
_defaultInstance = PrefabManager::SpawnPrefab(this, Transform::Identity, nullptr, &ObjectsCache);
_defaultInstance = PrefabManager::SpawnPrefab(this, nullptr, &ObjectsCache);
_isCreatingDefaultInstance = false;
return _defaultInstance;
@@ -87,17 +87,12 @@ SceneObject* Prefab::GetDefaultInstance(const Guid& objectId)
const auto result = GetDefaultInstance();
if (!result)
return nullptr;
if (objectId.IsValid())
{
const void* object;
SceneObject* object;
if (ObjectsCache.TryGet(objectId, object))
{
// Actor or Script
return (SceneObject*)object;
}
return object;
}
return result;
}

View File

@@ -44,7 +44,7 @@ public:
/// <summary>
/// The objects cache maps the id of the object contained in the prefab asset (actor or script) to the default instance deserialized from prefab data. Valid only if asset is loaded and GetDefaultInstance was called.
/// </summary>
Dictionary<Guid, const void*> ObjectsCache;
Dictionary<Guid, SceneObject*> ObjectsCache;
public:
/// <summary>

View File

@@ -76,12 +76,12 @@ Actor* PrefabManager::SpawnPrefab(Prefab* prefab, Actor* parent)
return SpawnPrefab(prefab, Transform(Vector3::Minimum), parent, nullptr);
}
Actor* PrefabManager::SpawnPrefab(Prefab* prefab, Actor* parent, Dictionary<Guid, const void*>* objectsCache, bool withSynchronization)
Actor* PrefabManager::SpawnPrefab(Prefab* prefab, Actor* parent, Dictionary<Guid, SceneObject*>* objectsCache, bool withSynchronization)
{
return SpawnPrefab(prefab, Transform(Vector3::Minimum), parent, objectsCache, withSynchronization);
}
Actor* PrefabManager::SpawnPrefab(Prefab* prefab, const Transform& transform, Actor* parent, Dictionary<Guid, const void*>* objectsCache, bool withSynchronization)
Actor* PrefabManager::SpawnPrefab(Prefab* prefab, const Transform& transform, Actor* parent, Dictionary<Guid, SceneObject*>* objectsCache, bool withSynchronization)
{
PROFILE_CPU_NAMED("Prefab.Spawn");
if (prefab == nullptr)
@@ -94,8 +94,8 @@ Actor* PrefabManager::SpawnPrefab(Prefab* prefab, const Transform& transform, Ac
LOG(Warning, "Waiting for prefab asset be loaded failed. {0}", prefab->ToString());
return nullptr;
}
const int32 objectsCount = prefab->ObjectsCount;
if (objectsCount == 0)
const int32 dataCount = prefab->ObjectsCount;
if (dataCount == 0)
{
LOG(Warning, "Prefab has no objects. {0}", prefab->ToString());
return nullptr;
@@ -107,7 +107,7 @@ Actor* PrefabManager::SpawnPrefab(Prefab* prefab, const Transform& transform, Ac
// Prepare
CollectionPoolCache<ActorsCache::SceneObjectsListType>::ScopeCache sceneObjects = ActorsCache::SceneObjectsListCache.Get();
sceneObjects->Resize(objectsCount);
sceneObjects->Resize(dataCount);
CollectionPoolCache<ISerializeModifier, Cache::ISerializeModifierClearCallback>::ScopeCache modifier = Cache::ISerializeModifier.Get();
modifier->EngineBuild = prefab->DataEngineBuild;
modifier->IdsMapping.EnsureCapacity(prefab->ObjectsIds.Count() * 4);
@@ -126,7 +126,7 @@ Actor* PrefabManager::SpawnPrefab(Prefab* prefab, const Transform& transform, Ac
// Deserialize prefab objects
auto prevIdMapping = Scripting::ObjectsLookupIdMapping.Get();
Scripting::ObjectsLookupIdMapping.Set(&modifier.Value->IdsMapping);
for (int32 i = 0; i < objectsCount; i++)
for (int32 i = 0; i < dataCount; i++)
{
auto& stream = data[i];
SceneObject* obj = SceneObjectsFactory::Spawn(context, stream);
@@ -145,7 +145,7 @@ Actor* PrefabManager::SpawnPrefab(Prefab* prefab, const Transform& transform, Ac
SceneObjectsFactory::SynchronizeNewPrefabInstances(context, prefabSyncData);
Scripting::ObjectsLookupIdMapping.Set(&modifier.Value->IdsMapping);
}
for (int32 i = 0; i < objectsCount; i++)
for (int32 i = 0; i < dataCount; i++)
{
auto& stream = data[i];
SceneObject* obj = sceneObjects->At(i);
@@ -154,28 +154,6 @@ Actor* PrefabManager::SpawnPrefab(Prefab* prefab, const Transform& transform, Ac
}
Scripting::ObjectsLookupIdMapping.Set(prevIdMapping);
// Pick prefab root object
if (sceneObjects->IsEmpty())
{
LOG(Warning, "No valid objects in prefab.");
return nullptr;
}
Actor* root = nullptr;
const Guid prefabRootObjectId = prefab->GetRootObjectId();
for (int32 i = 0; i < objectsCount; i++)
{
if (JsonTools::GetGuid(data[i], "ID") == prefabRootObjectId)
{
root = dynamic_cast<Actor*>(sceneObjects->At(i));
break;
}
}
if (!root)
{
LOG(Warning, "Missing prefab root object.");
return nullptr;
}
// Synchronize prefab instances (prefab may have new objects added or some removed so deserialized instances need to synchronize with it)
if (withSynchronization)
{
@@ -183,6 +161,30 @@ Actor* PrefabManager::SpawnPrefab(Prefab* prefab, const Transform& transform, Ac
SceneObjectsFactory::SynchronizePrefabInstances(context, prefabSyncData);
}
// Pick prefab root object
Actor* root = nullptr;
const Guid prefabRootObjectId = prefab->GetRootObjectId();
for (int32 i = 0; i < dataCount && !root; i++)
{
if (JsonTools::GetGuid(data[i], "ID") == prefabRootObjectId)
root = dynamic_cast<Actor*>(sceneObjects->At(i));
}
if (!root)
{
// Fallback to the first actor that has no parent
for (int32 i = 0; i < sceneObjects->Count() && !root; i++)
{
SceneObject* obj = sceneObjects->At(i);
if (obj && !obj->GetParent())
root = dynamic_cast<Actor*>(obj);
}
}
if (!root)
{
LOG(Warning, "Missing prefab root object. {0}", prefab->ToString());
return nullptr;
}
// Prepare parent linkage for prefab root actor
if (root->_parent)
root->_parent->Children.Remove(root);
@@ -264,7 +266,7 @@ Actor* PrefabManager::SpawnPrefab(Prefab* prefab, const Transform& transform, Ac
}
// Link objects to prefab (only deserialized from prefab data)
for (int32 i = 0; i < objectsCount; i++)
for (int32 i = 0; i < dataCount; i++)
{
auto& stream = data[i];
SceneObject* obj = sceneObjects->At(i);

View File

@@ -89,7 +89,7 @@ API_CLASS(Static) class FLAXENGINE_API PrefabManager
/// <param name="objectsCache">The options output objects cache that can be filled with prefab object id mapping to deserialized object (actor or script).</param>
/// <param name="withSynchronization">True if perform prefab changes synchronization for the spawned objects. It will check if need to add new objects due to nested prefab modifications.</param>
/// <returns>The created actor (root) or null if failed.</returns>
static Actor* SpawnPrefab(Prefab* prefab, Actor* parent, Dictionary<Guid, const void*, HeapAllocation>* objectsCache, bool withSynchronization = false);
static Actor* SpawnPrefab(Prefab* prefab, Actor* parent, Dictionary<Guid, SceneObject*, HeapAllocation>* objectsCache, bool withSynchronization = true);
/// <summary>
/// Spawns the instance of the prefab objects. If parent actor is specified then created actors are fully initialized (OnLoad event and BeginPlay is called if parent actor is already during gameplay).
@@ -100,7 +100,7 @@ API_CLASS(Static) class FLAXENGINE_API PrefabManager
/// <param name="objectsCache">The options output objects cache that can be filled with prefab object id mapping to deserialized object (actor or script).</param>
/// <param name="withSynchronization">True if perform prefab changes synchronization for the spawned objects. It will check if need to add new objects due to nested prefab modifications.</param>
/// <returns>The created actor (root) or null if failed.</returns>
static Actor* SpawnPrefab(Prefab* prefab, const Transform& transform, Actor* parent, Dictionary<Guid, const void*, HeapAllocation>* objectsCache, bool withSynchronization = false);
static Actor* SpawnPrefab(Prefab* prefab, const Transform& transform, Actor* parent, Dictionary<Guid, SceneObject*, HeapAllocation>* objectsCache, bool withSynchronization = true);
#if USE_EDITOR

View File

@@ -11,29 +11,6 @@ String SceneInfo::ToString() const
return TEXT("SceneInfo");
}
const int32 lightmapAtlasSizes[] =
{
32,
64,
128,
256,
512,
1024,
2048,
4096
};
DECLARE_ENUM_8(LightmapAtlasSize, _32, _64, _128, _256, _512, _1024, _2048, _4096);
LightmapAtlasSize getLightmapAtlasSize(int32 size)
{
for (int32 i = 0; i < LightmapAtlasSize_Count; i++)
{
if (lightmapAtlasSizes[i] == size)
return (LightmapAtlasSize)i;
}
return LightmapAtlasSize::_1024;
}
void SceneInfo::Serialize(SerializeStream& stream, const void* otherObj)
{
SERIALIZE_GET_OTHER_OBJ(SceneInfo);

View File

@@ -16,8 +16,9 @@
#if !BUILD_RELEASE || USE_EDITOR
#include "Engine/Level/Level.h"
#include "Engine/Threading/Threading.h"
#include "Engine/Level/Components/MissingScript.h"
#include "Engine/Level/Scripts/MissingScript.h"
#endif
#include "Engine/Level/Scripts/ModelPrefab.h"
#if USE_EDITOR
@@ -46,6 +47,11 @@ void MissingScript::SetReferenceScript(const ScriptingObjectReference<Script>& v
#endif
ModelPrefab::ModelPrefab(const SpawnParams& params)
: Script(params)
{
}
SceneObjectsFactory::Context::Context(ISerializeModifier* modifier)
: Modifier(modifier)
{
@@ -132,7 +138,7 @@ SceneObject* SceneObjectsFactory::Spawn(Context& context, const ISerializable::D
auto prefab = Content::LoadAsync<Prefab>(prefabId);
if (prefab == nullptr)
{
LOG(Warning, "Missing prefab with id={0}.", prefabId);
LOG(Warning, "Missing prefab {0}.", prefabId);
return nullptr;
}
if (prefab->WaitForLoaded())
@@ -258,7 +264,7 @@ void SceneObjectsFactory::Deserialize(Context& context, SceneObject* obj, ISeria
auto prefab = Content::LoadAsync<Prefab>(prefabId);
if (prefab == nullptr)
{
LOG(Warning, "Missing prefab with id={0}.", prefabId);
LOG(Warning, "Missing prefab {0}.", prefabId);
return;
}
if (prefab->WaitForLoaded())
@@ -311,11 +317,13 @@ void SceneObjectsFactory::HandleObjectDeserializationError(const ISerializable::
{
#if USE_EDITOR
// Add dummy script
auto* dummyScript = parent->AddScript<MissingScript>();
const auto typeNameMember = value.FindMember("TypeName");
if (typeNameMember != value.MemberEnd() && typeNameMember->value.IsString())
{
auto* dummyScript = parent->AddScript<MissingScript>();
dummyScript->MissingTypeName = typeNameMember->value.GetString();
dummyScript->Data = MoveTemp(bufferStr);
dummyScript->Data = MoveTemp(bufferStr);
}
#endif
LOG(Warning, "Parent actor of the missing object: {0}", parent->GetName());
}
@@ -418,9 +426,6 @@ void SceneObjectsFactory::SetupPrefabInstances(Context& context, const PrefabSyn
ASSERT(count <= data.SceneObjects.Count());
for (int32 i = 0; i < count; i++)
{
const SceneObject* obj = data.SceneObjects[i];
if (!obj)
continue;
const auto& stream = data.Data[i];
Guid prefabObjectId, prefabId;
if (!JsonTools::GetGuidIfValid(prefabObjectId, stream, "PrefabObjectID"))
@@ -430,15 +435,18 @@ void SceneObjectsFactory::SetupPrefabInstances(Context& context, const PrefabSyn
Guid parentId = JsonTools::GetGuid(stream, "ParentID");
for (int32 j = i - 1; j >= 0; j--)
{
// Find instance ID of the parent to this object (use data in json for relationship)
// Find ID of the parent to this object (use data in json for relationship)
if (parentId == JsonTools::GetGuid(data.Data[j], "ID") && data.SceneObjects[j])
{
parentId = data.SceneObjects[j]->GetID();
break;
}
}
const Guid id = obj->GetID();
const SceneObject* obj = data.SceneObjects[i];
const Guid id = obj ? obj->GetID() : JsonTools::GetGuid(stream, "ID");
auto prefab = Content::LoadAsync<Prefab>(prefabId);
if (!prefab)
continue;
// Check if it's parent is in the same prefab
int32 index;
@@ -453,6 +461,8 @@ void SceneObjectsFactory::SetupPrefabInstances(Context& context, const PrefabSyn
auto& e = context.Instances.AddOne();
e.Prefab = prefab;
e.RootId = id;
e.RootIndex = i;
e.StatIndex = i;
}
context.ObjectToInstance[id] = index;
@@ -484,6 +494,85 @@ void SceneObjectsFactory::SynchronizeNewPrefabInstances(Context& context, Prefab
Scripting::ObjectsLookupIdMapping.Set(&data.Modifier->IdsMapping);
data.InitialCount = data.SceneObjects.Count();
// Recreate any missing prefab root objects that were deleted (eg. spawned prefab got its root changed and deleted so old prefab instance needs to respawn it)
for (int32 instanceIndex = 0; instanceIndex < context.Instances.Count(); instanceIndex++)
{
PrefabInstance& instance = context.Instances[instanceIndex];
SceneObject* root = data.SceneObjects[instance.RootIndex];
if (!root && instance.Prefab)
{
instance.FixRootParent = true;
// Check if current prefab root existed in the deserialized data
const auto& oldRootData = data.Data[instance.RootIndex];
const Guid oldRootId = JsonTools::GetGuid(oldRootData, "ID");
const Guid prefabObjectId = JsonTools::GetGuid(oldRootData, "PrefabObjectID");
const Guid prefabRootId = instance.Prefab->GetRootObjectId();
Guid id;
int32 idInstance = -1;
bool syncNewRoot = false;
if (instance.IdsMapping.TryGet(prefabRootId, id) && context.ObjectToInstance.TryGet(id, idInstance) && idInstance == instanceIndex)
{
// Update the missing root with the valid object from this prefab instance
LOG(Warning, "Changed prefab instance root from ID={0}, PrefabObjectID={1} to ID={2}, PrefabObjectID={3} ({4})", instance.RootId, prefabObjectId, id, prefabRootId, instance.Prefab->ToString());
}
else
{
LOG(Warning, "Missing prefab instance root (ID={0}, PrefabObjectID={1}, {2})", instance.RootId, prefabObjectId, instance.Prefab->ToString());
// Get prefab object data from the prefab
const ISerializable::DeserializeStream* prefabData;
if (!instance.Prefab->ObjectsDataCache.TryGet(prefabRootId, prefabData))
{
LOG(Warning, "Missing object {1} data in prefab {0}.", instance.Prefab->ToString(), prefabObjectId);
continue;
}
// Map prefab object ID to the new prefab object instance
id = Guid::New();
data.Modifier->IdsMapping[prefabRootId] = id;
// Create prefab instance (recursive prefab loading to support nested prefabs)
root = Spawn(context, *prefabData);
if (!root)
{
LOG(Warning, "Failed to create object {1} from prefab {0}.", instance.Prefab->ToString(), prefabRootId);
continue;
}
// Register object
root->RegisterObject();
data.SceneObjects.Add(root);
auto& newObj = data.NewObjects.AddOne();
newObj.Prefab = instance.Prefab;
newObj.PrefabData = prefabData;
newObj.PrefabObjectId = prefabRootId;
newObj.Id = id;
context.ObjectToInstance[id] = instanceIndex;
syncNewRoot = true;
}
// Update prefab root info
instance.RootId = id;
instance.RootIndex = data.SceneObjects.Find(Scripting::FindObject<SceneObject>(id));
CHECK(instance.RootIndex != -1);
// Remap removed prefab root into the current root (can be different type but is needed for proper hierarchy linkage)
instance.IdsMapping[prefabObjectId] = id;
instance.IdsMapping[prefabRootId] = id;
instance.IdsMapping[oldRootId] = id;
data.Modifier->IdsMapping[prefabObjectId] = id;
data.Modifier->IdsMapping[prefabRootId] = id;
data.Modifier->IdsMapping[oldRootId] = id;
// Add any sub-objects that are missing (in case new root was created)
if (syncNewRoot)
{
SynchronizeNewPrefabInstances(context, data, instance.Prefab, (Actor*)root, prefabRootId, instance.RootIndex, oldRootData);
}
}
}
// Check all actors with prefab linkage for adding missing objects
for (int32 i = 0; i < data.InitialCount; i++)
{
@@ -498,13 +587,12 @@ void SceneObjectsFactory::SynchronizeNewPrefabInstances(Context& context, Prefab
continue;
if (!JsonTools::GetGuidIfValid(actorId, stream, "ID"))
continue;
const Guid actorParentId = JsonTools::GetGuid(stream, "ParentID");
// Load prefab
auto prefab = Content::LoadAsync<Prefab>(prefabId);
if (prefab == nullptr)
{
LOG(Warning, "Missing prefab with id={0}.", prefabId);
LOG(Warning, "Missing prefab {0}.", prefabId);
continue;
}
if (prefab->WaitForLoaded())
@@ -513,66 +601,7 @@ void SceneObjectsFactory::SynchronizeNewPrefabInstances(Context& context, Prefab
continue;
}
// Check for RemovedObjects list
const auto removedObjects = SERIALIZE_FIND_MEMBER(stream, "RemovedObjects");
// Check if the given actor has new children or scripts added (inside the prefab that it uses)
// TODO: consider caching prefab objects structure maybe to boost this logic?
for (auto it = prefab->ObjectsDataCache.Begin(); it.IsNotEnd(); ++it)
{
// Use only objects that are linked to the current actor
const Guid parentId = JsonTools::GetGuid(*it->Value, "ParentID");
if (parentId != actorPrefabObjectId)
continue;
// Skip if object was marked to be removed per instance
const Guid prefabObjectId = JsonTools::GetGuid(*it->Value, "ID");
if (removedObjects != stream.MemberEnd())
{
auto& list = removedObjects->value;
const int32 size = static_cast<int32>(list.Size());
bool removed = false;
for (int32 j = 0; j < size; j++)
{
if (JsonTools::GetGuid(list[j]) == prefabObjectId)
{
removed = true;
break;
}
}
if (removed)
continue;
}
// Use only objects that are missing
bool spawned = false;
for (int32 j = i + 1; j < data.InitialCount; j++)
{
const auto& jData = data.Data[j];
const Guid jParentId = JsonTools::GetGuid(jData, "ParentID");
//if (jParentId == actorParentId)
// break;
//if (jParentId != actorId)
// continue;
const Guid jPrefabObjectId = JsonTools::GetGuid(jData, "PrefabObjectID");
if (jPrefabObjectId != prefabObjectId)
continue;
// This object exists in the saved scene objects list
spawned = true;
break;
}
if (spawned)
continue;
// Map prefab object id to this actor's prefab instance so the new objects gets added to it
context.SetupIdsMapping(actor, data.Modifier);
data.Modifier->IdsMapping[actorPrefabObjectId] = actor->GetID();
Scripting::ObjectsLookupIdMapping.Set(&data.Modifier->IdsMapping);
// Create instance (including all children)
SynchronizeNewPrefabInstance(context, data, prefab, actor, prefabObjectId);
}
SynchronizeNewPrefabInstances(context, data, prefab, actor, actorPrefabObjectId, i, stream);
}
Scripting::ObjectsLookupIdMapping.Set(nullptr);
@@ -599,7 +628,7 @@ void SceneObjectsFactory::SynchronizePrefabInstances(Context& context, PrefabSyn
auto prefab = Content::LoadAsync<Prefab>(prefabId);
if (prefab == nullptr)
{
LOG(Warning, "Missing prefab with id={0}.", prefabId);
LOG(Warning, "Missing prefab {0}.", prefabId);
continue;
}
if (prefab->WaitForLoaded())
@@ -620,7 +649,7 @@ void SceneObjectsFactory::SynchronizePrefabInstances(Context& context, PrefabSyn
// Invalid connection object found!
LOG(Info, "Object {0} has invalid parent object {4} -> {5} (PrefabObjectID: {1}, PrefabID: {2}, Path: {3})", obj->GetSceneObjectId(), prefabObjectId, prefab->GetID(), prefab->GetPath(), parentPrefabObjectId, actualParentPrefabId);
// Map actual prefab object id to the current scene objects collection
// Map actual prefab object ID to the current scene objects collection
context.SetupIdsMapping(obj, data.Modifier);
data.Modifier->IdsMapping.TryGet(actualParentPrefabId, actualParentPrefabId);
@@ -640,15 +669,12 @@ void SceneObjectsFactory::SynchronizePrefabInstances(Context& context, PrefabSyn
if (i != 0)
{
const auto defaultInstance = prefab->GetDefaultInstance(obj->GetPrefabObjectID());
if (defaultInstance)
{
obj->SetOrderInParent(defaultInstance->GetOrderInParent());
}
const int32 order = defaultInstance ? defaultInstance->GetOrderInParent() : -1;
if (order != -1)
obj->SetOrderInParent(order);
}
}
Scripting::ObjectsLookupIdMapping.Set(&data.Modifier->IdsMapping);
// Synchronize new prefab objects
for (int32 i = 0; i < data.NewObjects.Count(); i++)
{
@@ -656,20 +682,97 @@ void SceneObjectsFactory::SynchronizePrefabInstances(Context& context, PrefabSyn
auto& newObj = data.NewObjects[i];
// Deserialize object with prefab data
Scripting::ObjectsLookupIdMapping.Set(&data.Modifier->IdsMapping);
Deserialize(context, obj, *(ISerializable::DeserializeStream*)newObj.PrefabData);
obj->LinkPrefab(newObj.Prefab->GetID(), newObj.PrefabObjectId);
// Preserve order in parent (values from prefab are used)
const auto defaultInstance = newObj.Prefab->GetDefaultInstance(newObj.PrefabObjectId);
if (defaultInstance)
const int32 order = defaultInstance ? defaultInstance->GetOrderInParent() : -1;
if (order != -1)
obj->SetOrderInParent(order);
}
// Setup hierarchy for the prefab instances (ensure any new objects are connected)
for (const auto& instance : context.Instances)
{
const auto& prefabStartData = data.Data[instance.StatIndex];
Guid prefabStartParentId;
if (instance.FixRootParent && JsonTools::GetGuidIfValid(prefabStartParentId, prefabStartData, "ParentID"))
{
obj->SetOrderInParent(defaultInstance->GetOrderInParent());
auto* root = data.SceneObjects[instance.RootIndex];
const auto rootParent = Scripting::FindObject<Actor>(prefabStartParentId);
root->SetParent(rootParent, false);
}
}
Scripting::ObjectsLookupIdMapping.Set(nullptr);
}
void SceneObjectsFactory::SynchronizeNewPrefabInstances(Context& context, PrefabSyncData& data, Prefab* prefab, Actor* actor, const Guid& actorPrefabObjectId, int32 i, const ISerializable::DeserializeStream& stream)
{
// Check for RemovedObjects list
const auto removedObjects = SERIALIZE_FIND_MEMBER(stream, "RemovedObjects");
// Check if the given actor has new children or scripts added (inside the prefab that it uses)
// TODO: consider caching prefab objects structure maybe to boost this logic?
for (auto it = prefab->ObjectsDataCache.Begin(); it.IsNotEnd(); ++it)
{
// Use only objects that are linked to the current actor
const Guid parentId = JsonTools::GetGuid(*it->Value, "ParentID");
if (parentId != actorPrefabObjectId)
continue;
// Skip if object was marked to be removed per instance
const Guid prefabObjectId = JsonTools::GetGuid(*it->Value, "ID");
if (removedObjects != stream.MemberEnd())
{
auto& list = removedObjects->value;
const int32 size = static_cast<int32>(list.Size());
bool removed = false;
for (int32 j = 0; j < size; j++)
{
if (JsonTools::GetGuid(list[j]) == prefabObjectId)
{
removed = true;
break;
}
}
if (removed)
continue;
}
// Use only objects that are missing
bool spawned = false;
int32 childSearchStart = i + 1; // Objects are serialized with parent followed by its children
int32 instanceIndex = -1;
if (context.ObjectToInstance.TryGet(actor->GetID(), instanceIndex) && context.Instances[instanceIndex].Prefab == prefab)
{
// Start searching from the beginning of that prefab instance (eg. in case prefab objects were reordered)
childSearchStart = Math::Min(childSearchStart, context.Instances[instanceIndex].StatIndex);
}
for (int32 j = childSearchStart; j < data.InitialCount; j++)
{
if (JsonTools::GetGuid(data.Data[j], "PrefabObjectID") == prefabObjectId)
{
// This object exists in the saved scene objects list
spawned = true;
break;
}
}
if (spawned)
continue;
// Map prefab object ID to this actor's prefab instance so the new objects gets added to it
context.SetupIdsMapping(actor, data.Modifier);
data.Modifier->IdsMapping[actorPrefabObjectId] = actor->GetID();
Scripting::ObjectsLookupIdMapping.Set(&data.Modifier->IdsMapping);
// Create instance (including all children)
SynchronizeNewPrefabInstance(context, data, prefab, actor, prefabObjectId);
}
}
void SceneObjectsFactory::SynchronizeNewPrefabInstance(Context& context, PrefabSyncData& data, Prefab* prefab, Actor* actor, const Guid& prefabObjectId)
{
PROFILE_CPU_NAMED("SynchronizeNewPrefabInstance");

View File

@@ -15,8 +15,11 @@ class FLAXENGINE_API SceneObjectsFactory
public:
struct PrefabInstance
{
int32 StatIndex;
int32 RootIndex;
Guid RootId;
Prefab* Prefab;
bool FixRootParent = false;
Dictionary<Guid, Guid> IdsMapping;
};
@@ -70,6 +73,8 @@ public:
struct PrefabSyncData
{
friend SceneObjectsFactory;
friend class PrefabManager;
// The created scene objects. Collection can be modified (eg. for spawning missing objects).
Array<SceneObject*>& SceneObjects;
// The scene objects data.
@@ -124,5 +129,6 @@ public:
static void SynchronizePrefabInstances(Context& context, PrefabSyncData& data);
private:
static void SynchronizeNewPrefabInstances(Context& context, PrefabSyncData& data, Prefab* prefab, Actor* actor, const Guid& actorPrefabObjectId, int32 i, const ISerializable::DeserializeStream& stream);
static void SynchronizeNewPrefabInstance(Context& context, PrefabSyncData& data, Prefab* prefab, Actor* actor, const Guid& prefabObjectId);
};

View File

@@ -4,10 +4,8 @@
#if USE_EDITOR
#include "Engine/Core/Cache.h"
#include "Engine/Scripting/Script.h"
#include "Engine/Scripting/ScriptingObjectReference.h"
#include "Engine/Serialization/JsonWriters.h"
/// <summary>
/// Actor script component that represents missing script.

View File

@@ -0,0 +1,29 @@
// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
#pragma once
#include "Engine/Scripting/Script.h"
#if USE_EDITOR
#include "Engine/Tools/ModelTool/ModelTool.h"
#endif
/// <summary>
/// Actor script component that handled model prefabs importing and setup.
/// </summary>
API_CLASS(Attributes="HideInEditor") class FLAXENGINE_API ModelPrefab : public Script
{
API_AUTO_SERIALIZATION();
DECLARE_SCRIPTING_TYPE(ModelPrefab);
#if USE_EDITOR
/// <summary>
/// Source model file path (absolute or relative to the project).
/// </summary>
API_FIELD(Attributes="ReadOnly") String ImportPath;
/// <summary>
/// Model file import settings.
/// </summary>
API_FIELD() ModelTool::Options ImportOptions;
#endif
};

View File

@@ -45,7 +45,7 @@ namespace FlaxEngine
/// <returns>True if both values are not equal, otherwise false.</returns>
public static bool operator !=(Tag left, Tag right)
{
return left.Index == right.Index;
return left.Index != right.Index;
}
/// <summary>
@@ -213,11 +213,11 @@ namespace FlaxEngine
}
/// <summary>
/// Checks if the list of tags contains all of the given tags (including parent tags check). For example, HasAll({"A.B", "C"}, {"A", "C"}) returns true, for exact check use HasAllExact.
/// Checks if the list of tags contains all the given tags (including parent tags check). For example, HasAll({"A.B", "C"}, {"A", "C"}) returns true, for exact check use HasAllExact.
/// </summary>
/// <param name="list">The tags list to use.</param>
/// <param name="tags">The tags to check.</param>
/// <returns>True if all of the given tags are contained by the list of tags. Returns true for empty list.</returns>
/// <returns>True if all the given tags are contained by the list of tags. Returns true for empty list.</returns>
public static bool HasAll(this Tag[] list, Tag[] tags)
{
if (tags == null || tags.Length == 0)
@@ -233,11 +233,11 @@ namespace FlaxEngine
}
/// <summary>
/// Checks if the list of tags contains all of the given tags (exact match). For example, HasAllExact({"A.B", "C"}, {"A", "C"}) returns false, for parents check use HasAll.
/// Checks if the list of tags contains all the given tags (exact match). For example, HasAllExact({"A.B", "C"}, {"A", "C"}) returns false, for parents check use HasAll.
/// </summary>
/// <param name="list">The tags list to use.</param>
/// <param name="tags">The tags to check.</param>
/// <returns>True if all of the given tags are contained by the list of tags. Returns true for empty list.</returns>
/// <returns>True if all the given tags are contained by the list of tags. Returns true for empty list.</returns>
public static bool HasAllExact(this Tag[] list, Tag[] tags)
{
if (tags == null || tags.Length == 0)

View File

@@ -36,7 +36,7 @@ NavMeshRuntime::NavMeshRuntime(const NavMeshProperties& properties)
NavMeshRuntime::~NavMeshRuntime()
{
dtFreeNavMesh(_navMesh);
Dispose();
dtFreeNavMeshQuery(_navMeshQuery);
}
@@ -336,9 +336,7 @@ void NavMeshRuntime::SetTileSize(float tileSize)
// Dispose the existing mesh (its invalid)
if (_navMesh)
{
dtFreeNavMesh(_navMesh);
_navMesh = nullptr;
_tiles.Clear();
Dispose();
}
_tileSize = tileSize;
@@ -363,14 +361,9 @@ void NavMeshRuntime::EnsureCapacity(int32 tilesToAddCount)
// Ensure to have size assigned
ASSERT(_tileSize != 0);
// Fre previous data (if any)
if (_navMesh)
{
dtFreeNavMesh(_navMesh);
}
// Allocate new navmesh
_navMesh = dtAllocNavMesh();
// Allocate navmesh and initialize the default query
if (!_navMesh)
_navMesh = dtAllocNavMesh();
if (dtStatusFailed(_navMeshQuery->init(_navMesh, MAX_NODES)))
{
LOG(Error, "Failed to initialize navmesh {0}.", Properties.Name);
@@ -517,7 +510,7 @@ void NavMeshRuntime::RemoveTile(int32 x, int32 y, int32 layer)
}
}
void NavMeshRuntime::RemoveTiles(bool (* prediction)(const NavMeshRuntime* navMesh, const NavMeshTile& tile, void* customData), void* userData)
void NavMeshRuntime::RemoveTiles(bool (*prediction)(const NavMeshRuntime* navMesh, const NavMeshTile& tile, void* customData), void* userData)
{
ScopeLock lock(Locker);
ASSERT(prediction);

View File

@@ -817,7 +817,7 @@ void InvokeObjectSpawn(const NetworkMessageObjectSpawn& msgData, const NetworkMe
NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to find prefab {}", msgData.PrefabId.ToString());
return;
}
prefabInstance = PrefabManager::SpawnPrefab(prefab, Transform::Identity, nullptr, nullptr);
prefabInstance = PrefabManager::SpawnPrefab(prefab, nullptr, nullptr);
if (!prefabInstance)
{
NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to spawn object type {}", msgData.PrefabId.ToString());

View File

@@ -1,8 +1,8 @@
// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
#include "BoxCollider.h"
#include "Engine/Serialization/Serialization.h"
#include "Engine/Physics/PhysicsBackend.h"
#include "Engine/Level/Scene/Scene.h"
BoxCollider::BoxCollider(const SpawnParams& params)
: Collider(params)
@@ -20,6 +20,32 @@ void BoxCollider::SetSize(const Float3& value)
UpdateBounds();
}
void BoxCollider::AutoResize()
{
Actor* parent = GetParent();
if (Cast<Scene>(parent))
return;
// Get bounds of all siblings (excluding itself)
const Vector3 parentScale = parent->GetScale();
if (parentScale.IsAnyZero())
return; // Avoid division by zero
BoundingBox parentBox = parent->GetBox();
for (const Actor* sibling : parent->Children)
{
if (sibling != this)
BoundingBox::Merge(parentBox, sibling->GetBoxWithChildren(), parentBox);
}
const Vector3 parentSize = parentBox.GetSize();
const Vector3 parentCenter = parentBox.GetCenter() - parent->GetPosition();
// Update bounds
SetLocalPosition(Vector3::Zero);
SetSize(parentSize / parentScale);
SetCenter(parentCenter / parentScale);
SetOrientation(GetOrientation() * Quaternion::Invert(GetOrientation()));
}
#if USE_EDITOR
#include "Engine/Debug/DebugDraw.h"
@@ -116,24 +142,6 @@ bool BoxCollider::IntersectsItself(const Ray& ray, Real& distance, Vector3& norm
return _bounds.Intersects(ray, distance, normal);
}
void BoxCollider::Serialize(SerializeStream& stream, const void* otherObj)
{
// Base
Collider::Serialize(stream, otherObj);
SERIALIZE_GET_OTHER_OBJ(BoxCollider);
SERIALIZE_MEMBER(Size, _size);
}
void BoxCollider::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier)
{
// Base
Collider::Deserialize(stream, modifier);
DESERIALIZE_MEMBER(Size, _size);
}
void BoxCollider::UpdateBounds()
{
// Cache bounds

View File

@@ -12,6 +12,7 @@
API_CLASS(Attributes="ActorContextMenu(\"New/Physics/Colliders/Box Collider\"), ActorToolbox(\"Physics\")")
class FLAXENGINE_API BoxCollider : public Collider
{
API_AUTO_SERIALIZATION();
DECLARE_SCENE_OBJECT(BoxCollider);
private:
Float3 _size;
@@ -42,6 +43,11 @@ public:
return _bounds;
}
/// <summary>
/// Resizes the collider based on the bounds of it's parent to contain it whole (including any siblings).
/// </summary>
API_FUNCTION() void AutoResize();
public:
// [Collider]
#if USE_EDITOR
@@ -49,8 +55,6 @@ public:
void OnDebugDrawSelected() override;
#endif
bool IntersectsItself(const Ray& ray, Real& distance, Vector3& normal) override;
void Serialize(SerializeStream& stream, const void* otherObj) override;
void Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) override;
protected:
// [Collider]

View File

@@ -1,7 +1,6 @@
// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
#include "CapsuleCollider.h"
#include "Engine/Serialization/Serialization.h"
CapsuleCollider::CapsuleCollider(const SpawnParams& params)
: Collider(params)
@@ -81,26 +80,6 @@ bool CapsuleCollider::IntersectsItself(const Ray& ray, Real& distance, Vector3&
return _orientedBox.Intersects(ray, distance, normal);
}
void CapsuleCollider::Serialize(SerializeStream& stream, const void* otherObj)
{
// Base
Collider::Serialize(stream, otherObj);
SERIALIZE_GET_OTHER_OBJ(CapsuleCollider);
SERIALIZE_MEMBER(Radius, _radius);
SERIALIZE_MEMBER(Height, _height);
}
void CapsuleCollider::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier)
{
// Base
Collider::Deserialize(stream, modifier);
DESERIALIZE_MEMBER(Radius, _radius);
DESERIALIZE_MEMBER(Height, _height);
}
void CapsuleCollider::UpdateBounds()
{
// Cache bounds

View File

@@ -13,6 +13,7 @@
API_CLASS(Attributes="ActorContextMenu(\"New/Physics/Colliders/Capsule Collider\"), ActorToolbox(\"Physics\")")
class FLAXENGINE_API CapsuleCollider : public Collider
{
API_AUTO_SERIALIZATION();
DECLARE_SCENE_OBJECT(CapsuleCollider);
private:
float _radius;
@@ -58,8 +59,6 @@ public:
void OnDebugDrawSelected() override;
#endif
bool IntersectsItself(const Ray& ray, Real& distance, Vector3& normal) override;
void Serialize(SerializeStream& stream, const void* otherObj) override;
void Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) override;
protected:
// [Collider]

View File

@@ -5,7 +5,6 @@
#include "Engine/Physics/Physics.h"
#include "Engine/Physics/PhysicsBackend.h"
#include "Engine/Physics/PhysicsScene.h"
#include "Engine/Serialization/Serialization.h"
#include "Engine/Engine/Time.h"
#define CC_MIN_SIZE 0.001f
@@ -387,33 +386,3 @@ void CharacterController::OnPhysicsSceneChanged(PhysicsScene* previous)
DeleteController();
CreateController();
}
void CharacterController::Serialize(SerializeStream& stream, const void* otherObj)
{
// Base
Collider::Serialize(stream, otherObj);
SERIALIZE_GET_OTHER_OBJ(CharacterController);
SERIALIZE_MEMBER(StepOffset, _stepOffset);
SERIALIZE_MEMBER(SlopeLimit, _slopeLimit);
SERIALIZE_MEMBER(NonWalkableMode, _nonWalkableMode);
SERIALIZE_MEMBER(Radius, _radius);
SERIALIZE_MEMBER(Height, _height);
SERIALIZE_MEMBER(MinMoveDistance, _minMoveDistance);
SERIALIZE_MEMBER(UpDirection, _upDirection);
}
void CharacterController::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier)
{
// Base
Collider::Deserialize(stream, modifier);
DESERIALIZE_MEMBER(StepOffset, _stepOffset);
DESERIALIZE_MEMBER(SlopeLimit, _slopeLimit);
DESERIALIZE_MEMBER(NonWalkableMode, _nonWalkableMode);
DESERIALIZE_MEMBER(Radius, _radius);
DESERIALIZE_MEMBER(Height, _height);
DESERIALIZE_MEMBER(MinMoveDistance, _minMoveDistance);
DESERIALIZE_MEMBER(UpDirection, _upDirection);
}

View File

@@ -12,6 +12,7 @@
API_CLASS(Attributes="ActorContextMenu(\"New/Physics/Character Controller\"), ActorToolbox(\"Physics\")")
class FLAXENGINE_API CharacterController : public Collider, public IPhysicsActor
{
API_AUTO_SERIALIZATION();
DECLARE_SCENE_OBJECT(CharacterController);
public:
/// <summary>
@@ -198,8 +199,6 @@ public:
#if USE_EDITOR
void OnDebugDrawSelected() override;
#endif
void Serialize(SerializeStream& stream, const void* otherObj) override;
void Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) override;
void CreateShape() override;
void UpdateBounds() override;
void AddMovement(const Vector3& translation, const Quaternion& rotation) override;

View File

@@ -5,7 +5,6 @@
#if USE_EDITOR
#include "Engine/Level/Scene/SceneRendering.h"
#endif
#include "Engine/Serialization/Serialization.h"
#include "Engine/Physics/PhysicsSettings.h"
#include "Engine/Physics/Physics.h"
#include "Engine/Physics/PhysicsBackend.h"
@@ -292,30 +291,6 @@ void Collider::OnMaterialChanged()
PhysicsBackend::SetShapeMaterial(_shape, Material.Get());
}
void Collider::Serialize(SerializeStream& stream, const void* otherObj)
{
// Base
PhysicsColliderActor::Serialize(stream, otherObj);
SERIALIZE_GET_OTHER_OBJ(Collider);
SERIALIZE_MEMBER(IsTrigger, _isTrigger);
SERIALIZE_MEMBER(Center, _center);
SERIALIZE_MEMBER(ContactOffset, _contactOffset);
SERIALIZE(Material);
}
void Collider::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier)
{
// Base
PhysicsColliderActor::Deserialize(stream, modifier);
DESERIALIZE_MEMBER(IsTrigger, _isTrigger);
DESERIALIZE_MEMBER(Center, _center);
DESERIALIZE_MEMBER(ContactOffset, _contactOffset);
DESERIALIZE(Material);
}
void Collider::BeginPlay(SceneBeginData* data)
{
// Check if has no shape created (it means no rigidbody requested it but also collider may be spawned at runtime)

View File

@@ -17,6 +17,7 @@ class RigidBody;
/// <seealso cref="PhysicsColliderActor" />
API_CLASS(Abstract) class FLAXENGINE_API Collider : public PhysicsColliderActor
{
API_AUTO_SERIALIZATION();
DECLARE_SCENE_OBJECT_ABSTRACT(Collider);
protected:
Vector3 _center;
@@ -196,8 +197,6 @@ private:
public:
// [PhysicsColliderActor]
void Serialize(SerializeStream& stream, const void* otherObj) override;
void Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) override;
RigidBody* GetAttachedRigidBody() const override;
protected:

View File

@@ -3,7 +3,6 @@
#include "MeshCollider.h"
#include "Engine/Core/Math/Matrix.h"
#include "Engine/Core/Math/Ray.h"
#include "Engine/Serialization/Serialization.h"
#include "Engine/Physics/Physics.h"
#include "Engine/Physics/PhysicsScene.h"
#if USE_EDITOR || !BUILD_RELEASE
@@ -117,24 +116,6 @@ bool MeshCollider::IntersectsItself(const Ray& ray, Real& distance, Vector3& nor
return _box.Intersects(ray, distance, normal);
}
void MeshCollider::Serialize(SerializeStream& stream, const void* otherObj)
{
// Base
Collider::Serialize(stream, otherObj);
SERIALIZE_GET_OTHER_OBJ(MeshCollider);
SERIALIZE(CollisionData);
}
void MeshCollider::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier)
{
// Base
Collider::Deserialize(stream, modifier);
DESERIALIZE(CollisionData);
}
void MeshCollider::UpdateBounds()
{
// Cache bounds

View File

@@ -13,6 +13,7 @@
API_CLASS(Attributes="ActorContextMenu(\"New/Physics/Colliders/Mesh Collider\"), ActorToolbox(\"Physics\")")
class FLAXENGINE_API MeshCollider : public Collider
{
API_AUTO_SERIALIZATION();
DECLARE_SCENE_OBJECT(MeshCollider);
public:
/// <summary>
@@ -33,8 +34,6 @@ public:
void OnDebugDrawSelected() override;
#endif
bool IntersectsItself(const Ray& ray, Real& distance, Vector3& normal) override;
void Serialize(SerializeStream& stream, const void* otherObj) override;
void Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) override;
protected:
// [Collider]

View File

@@ -1,7 +1,6 @@
// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
#include "SphereCollider.h"
#include "Engine/Serialization/Serialization.h"
SphereCollider::SphereCollider(const SpawnParams& params)
: Collider(params)
@@ -58,24 +57,6 @@ bool SphereCollider::IntersectsItself(const Ray& ray, Real& distance, Vector3& n
return _sphere.Intersects(ray, distance, normal);
}
void SphereCollider::Serialize(SerializeStream& stream, const void* otherObj)
{
// Base
Collider::Serialize(stream, otherObj);
SERIALIZE_GET_OTHER_OBJ(SphereCollider);
SERIALIZE_MEMBER(Radius, _radius);
}
void SphereCollider::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier)
{
// Base
Collider::Deserialize(stream, modifier);
DESERIALIZE_MEMBER(Radius, _radius);
}
void SphereCollider::UpdateBounds()
{
// Cache bounds

View File

@@ -11,6 +11,7 @@
API_CLASS(Attributes="ActorContextMenu(\"New/Physics/Colliders/Sphere Collider\"), ActorToolbox(\"Physics\")")
class FLAXENGINE_API SphereCollider : public Collider
{
API_AUTO_SERIALIZATION();
DECLARE_SCENE_OBJECT(SphereCollider);
private:
float _radius;
@@ -38,8 +39,6 @@ public:
void OnDebugDrawSelected() override;
#endif
bool IntersectsItself(const Ray& ray, Real& distance, Vector3& normal) override;
void Serialize(SerializeStream& stream, const void* otherObj) override;
void Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) override;
protected:
// [Collider]

View File

@@ -5,7 +5,6 @@
#include "Engine/Core/Math/Matrix.h"
#include "Engine/Core/Math/Ray.h"
#include "Engine/Level/Actors/Spline.h"
#include "Engine/Serialization/Serialization.h"
#include "Engine/Physics/Physics.h"
#include "Engine/Physics/PhysicsBackend.h"
#include "Engine/Physics/PhysicsScene.h"
@@ -124,26 +123,6 @@ bool SplineCollider::IntersectsItself(const Ray& ray, Real& distance, Vector3& n
return _box.Intersects(ray, distance, normal);
}
void SplineCollider::Serialize(SerializeStream& stream, const void* otherObj)
{
// Base
Collider::Serialize(stream, otherObj);
SERIALIZE_GET_OTHER_OBJ(SplineCollider);
SERIALIZE(CollisionData);
SERIALIZE_MEMBER(PreTransform, _preTransform)
}
void SplineCollider::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier)
{
// Base
Collider::Deserialize(stream, modifier);
DESERIALIZE(CollisionData);
DESERIALIZE_MEMBER(PreTransform, _preTransform);
}
void SplineCollider::OnParentChanged()
{
if (_spline)

View File

@@ -15,6 +15,7 @@ class Spline;
/// <seealso cref="Spline" />
API_CLASS() class FLAXENGINE_API SplineCollider : public Collider
{
API_AUTO_SERIALIZATION();
DECLARE_SCENE_OBJECT(SplineCollider);
private:
Spline* _spline = nullptr;
@@ -61,8 +62,6 @@ public:
void OnDebugDrawSelected() override;
#endif
bool IntersectsItself(const Ray& ray, Real& distance, Vector3& normal) override;
void Serialize(SerializeStream& stream, const void* otherObj) override;
void Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) override;
void OnParentChanged() override;
void EndPlay() override;

View File

@@ -7,10 +7,12 @@
#include "Engine/Graphics/Async/GPUTask.h"
#include "Engine/Graphics/Models/MeshBase.h"
#include "Engine/Threading/Threading.h"
#include "Engine/Profiler/ProfilerCPU.h"
#include "Engine/Core/Log.h"
bool CollisionCooking::CookCollision(const Argument& arg, CollisionData::SerializedOptions& outputOptions, BytesContainer& outputData)
{
PROFILE_CPU();
int32 convexVertexLimit = Math::Clamp(arg.ConvexVertexLimit, CONVEX_VERTEX_MIN, CONVEX_VERTEX_MAX);
if (arg.ConvexVertexLimit == 0)
convexVertexLimit = CONVEX_VERTEX_MAX;

View File

@@ -961,6 +961,7 @@ void PhysicalMaterial::UpdatePhysicsMaterial()
bool CollisionCooking::CookConvexMesh(CookingInput& input, BytesContainer& output)
{
PROFILE_CPU();
ENSURE_CAN_COOK;
if (input.VertexCount == 0)
LOG(Warning, "Empty mesh data for collision cooking.");
@@ -1004,6 +1005,7 @@ bool CollisionCooking::CookConvexMesh(CookingInput& input, BytesContainer& outpu
bool CollisionCooking::CookTriangleMesh(CookingInput& input, BytesContainer& output)
{
PROFILE_CPU();
ENSURE_CAN_COOK;
if (input.VertexCount == 0 || input.IndexCount == 0)
LOG(Warning, "Empty mesh data for collision cooking.");
@@ -1038,6 +1040,7 @@ bool CollisionCooking::CookTriangleMesh(CookingInput& input, BytesContainer& out
bool CollisionCooking::CookHeightField(int32 cols, int32 rows, const PhysicsBackend::HeightFieldSample* data, WriteStream& stream)
{
PROFILE_CPU();
ENSURE_CAN_COOK;
PxHeightFieldDesc heightFieldDesc;
@@ -2800,7 +2803,7 @@ void PhysicsBackend::SetHingeJointLimit(void* joint, const LimitAngularRange& va
void PhysicsBackend::SetHingeJointDrive(void* joint, const HingeJointDrive& value)
{
auto jointPhysX = (PxRevoluteJoint*)joint;
jointPhysX->setDriveVelocity(Math::Max(value.Velocity, 0.0f));
jointPhysX->setDriveVelocity(value.Velocity);
jointPhysX->setDriveForceLimit(Math::Max(value.ForceLimit, 0.0f));
jointPhysX->setDriveGearRatio(Math::Max(value.GearRatio, 0.0f));
jointPhysX->setRevoluteJointFlag(PxRevoluteJointFlag::eDRIVE_FREESPIN, value.FreeSpin);

View File

@@ -17,6 +17,37 @@ API_CLASS(sealed, Namespace="FlaxEditor.Content.Settings") class FLAXENGINE_API
DECLARE_SCRIPTING_TYPE_MINIMAL(AndroidPlatformSettings);
API_AUTO_SERIALIZATION();
/// <summary>
/// Android screen orientation options.
/// </summary>
API_ENUM() enum class FLAXENGINE_API ScreenOrientation
{
/// <summary>
/// "portrait" mode
/// </summary>
Portrait,
/// <summary>
/// "reversePortrait" mode
/// </summary>
PortraitReverse,
/// <summary>
/// "landscape" mode
/// </summary>
LandscapeRight,
/// <summary>
/// "reverseLandscape" mode
/// </summary>
LandscapeLeft,
/// <summary>
/// "fullSensor" mode
/// </summary>
AutoRotation,
};
/// <summary>
/// The application package name (eg. com.company.product). Custom tokens: ${PROJECT_NAME}, ${COMPANY_NAME}.
/// </summary>
@@ -29,6 +60,12 @@ API_CLASS(sealed, Namespace="FlaxEditor.Content.Settings") class FLAXENGINE_API
API_FIELD(Attributes="EditorOrder(100), EditorDisplay(\"General\")")
Array<String> Permissions;
/// <summary>
/// The default screen orientation.
/// </summary>
API_FIELD(Attributes = "EditorOrder(110), EditorDisplay(\"General\")")
ScreenOrientation DefaultOrientation = ScreenOrientation::AutoRotation;
/// <summary>
/// Custom icon texture to use for the application (overrides the default one).
/// </summary>

View File

@@ -94,6 +94,7 @@ X11::XcursorImage* CursorsImg[(int32)CursorType::MAX];
Dictionary<StringAnsi, X11::KeyCode> KeyNameMap;
Array<KeyboardKeys> KeyCodeMap;
Delegate<void*> LinuxPlatform::xEventRecieved;
Window* MouseTrackingWindow = nullptr;
// Message boxes configuration
#define LINUX_DIALOG_MIN_BUTTON_WIDTH 64
@@ -1916,7 +1917,6 @@ bool LinuxPlatform::Init()
{
if (PlatformBase::Init())
return true;
char fileNameBuffer[1024];
// Init timing
@@ -2398,7 +2398,7 @@ void LinuxPlatform::Tick()
// Update input context focus
X11::XSetICFocus(IC);
window = WindowsManager::GetByNativePtr((void*)event.xfocus.window);
if (window)
if (window && MouseTrackingWindow == nullptr)
{
window->OnGotFocus();
}
@@ -2407,7 +2407,7 @@ void LinuxPlatform::Tick()
// Update input context focus
X11::XUnsetICFocus(IC);
window = WindowsManager::GetByNativePtr((void*)event.xfocus.window);
if (window)
if (window && MouseTrackingWindow == nullptr)
{
window->OnLostFocus();
}
@@ -2514,23 +2514,32 @@ void LinuxPlatform::Tick()
break;
case ButtonPress:
window = WindowsManager::GetByNativePtr((void*)event.xbutton.window);
if (window)
if (MouseTrackingWindow)
MouseTrackingWindow->OnButtonPress(&event.xbutton);
else if (window)
window->OnButtonPress(&event.xbutton);
break;
case ButtonRelease:
window = WindowsManager::GetByNativePtr((void*)event.xbutton.window);
if (window)
if (MouseTrackingWindow)
MouseTrackingWindow->OnButtonRelease(&event.xbutton);
else if (window)
window->OnButtonRelease(&event.xbutton);
break;
case MotionNotify:
window = WindowsManager::GetByNativePtr((void*)event.xmotion.window);
if (window)
if (MouseTrackingWindow)
MouseTrackingWindow->OnMotionNotify(&event.xmotion);
else if (window)
window->OnMotionNotify(&event.xmotion);
break;
case EnterNotify:
// nothing?
break;
case LeaveNotify:
window = WindowsManager::GetByNativePtr((void*)event.xcrossing.window);
if (MouseTrackingWindow)
MouseTrackingWindow->OnLeaveNotify(&event.xcrossing);
if (window)
window->OnLeaveNotify(&event.xcrossing);
break;

View File

@@ -40,6 +40,7 @@ extern X11::Atom xAtomWmName;
extern Dictionary<StringAnsi, X11::KeyCode> KeyNameMap;
extern Array<KeyboardKeys> KeyCodeMap;
extern X11::Cursor Cursors[(int32)CursorType::MAX];
extern Window* MouseTrackingWindow;
static constexpr uint32 MouseDoubleClickTime = 500;
static constexpr uint32 MaxDoubleClickDistanceSquared = 10;
@@ -54,7 +55,7 @@ LinuxWindow::LinuxWindow(const CreateWindowSettings& settings)
return;
auto screen = XDefaultScreen(display);
// Cache data
// Cache data
int32 width = Math::TruncToInt(settings.Size.X);
int32 height = Math::TruncToInt(settings.Size.Y);
_clientSize = Float2((float)width, (float)height);
@@ -111,6 +112,12 @@ LinuxWindow::LinuxWindow(const CreateWindowSettings& settings)
windowAttributes.border_pixel = XBlackPixel(display, screen);
windowAttributes.event_mask = KeyPressMask | KeyReleaseMask | StructureNotifyMask | ExposureMask;
if (!settings.IsRegularWindow)
{
windowAttributes.save_under = true;
windowAttributes.override_redirect = true;
}
// TODO: implement all window settings
/*
bool Fullscreen;
@@ -118,11 +125,16 @@ LinuxWindow::LinuxWindow(const CreateWindowSettings& settings)
bool AllowMaximize;
*/
unsigned long valueMask = CWBackPixel | CWBorderPixel | CWEventMask | CWColormap;
if (!settings.IsRegularWindow)
{
valueMask |= CWOverrideRedirect | CWSaveUnder;
}
const X11::Window window = X11::XCreateWindow(
display, X11::XRootWindow(display, screen), x, y,
width, height, 0, visualInfo->depth, InputOutput,
visualInfo->visual,
CWBackPixel | CWBorderPixel | CWEventMask | CWColormap, &windowAttributes);
valueMask, &windowAttributes);
_window = window;
LinuxWindow::SetTitle(settings.Title);
@@ -811,12 +823,13 @@ void LinuxWindow::SetTitle(const StringView& title)
void LinuxWindow::StartTrackingMouse(bool useMouseScreenOffset)
{
// TODO: impl this
MouseTrackingWindow = this;
}
void LinuxWindow::EndTrackingMouse()
{
// TODO: impl this
if (MouseTrackingWindow == this)
MouseTrackingWindow = nullptr;
}
void LinuxWindow::SetCursor(CursorType type)

View File

@@ -434,6 +434,10 @@ void PostProcessingPass::Render(RenderContext& renderContext, GPUTexture* input,
// Set lens flares output
context->BindSR(3, bloomTmp2->View(0, 1));
}
else
{
context->BindSR(3, (GPUResourceView*)nullptr);
}
////////////////////////////////////////////////////////////////////////////////////
// Final composite

View File

@@ -194,7 +194,7 @@ void RenderList::AddSettingsBlend(IPostFxSettingsProvider* provider, float weigh
void RenderList::BlendSettings()
{
PROFILE_CPU();
Sorting::QuickSort(Blendable.Get(), Blendable.Count());
Sorting::QuickSort(Blendable);
Settings = Graphics::PostProcessSettings;
for (auto& b : Blendable)
{
@@ -634,7 +634,7 @@ void RenderList::SortDrawCalls(const RenderContext& renderContext, bool reverseD
}
// Sort draw calls batches by depth
Sorting::QuickSort(list.Batches.Get(), list.Batches.Count());
Sorting::QuickSort(list.Batches);
}
FORCE_INLINE bool CanUseInstancing(DrawPass pass)

View File

@@ -1235,17 +1235,17 @@ bool ManagedBinaryModule::InvokeMethod(void* method, const Variant& instance, Sp
const bool withInterfaces = !mMethod->IsStatic() && mMethod->GetParentClass()->IsInterface();
if (!mMethod->IsStatic())
{
// Box instance into C# object
// Box instance into C# object (and validate the type)
MObject* instanceObject = MUtils::BoxVariant(instance);
const MClass* instanceObjectClass = MCore::Object::GetClass(instanceObject);
// Validate instance
if (!instanceObject || !instanceObjectClass->IsSubClassOf(mMethod->GetParentClass(), withInterfaces))
if (!instanceObject)
{
if (!instanceObject)
LOG(Error, "Failed to call method '{0}.{1}' (args count: {2}) without object instance", String(mMethod->GetParentClass()->GetFullName()), String(mMethod->GetName()), parametersCount);
else
LOG(Error, "Failed to call method '{0}.{1}' (args count: {2}) with invalid object instance of type '{3}'", String(mMethod->GetParentClass()->GetFullName()), String(mMethod->GetName()), parametersCount, String(MUtils::GetClassFullname(instanceObject)));
LOG(Error, "Failed to call method '{0}.{1}' (args count: {2}) without object instance", String(mMethod->GetParentClass()->GetFullName()), String(mMethod->GetName()), parametersCount);
return true;
}
const MClass* instanceObjectClass = MCore::Object::GetClass(instanceObject);
if (!instanceObjectClass->IsSubClassOf(mMethod->GetParentClass(), withInterfaces))
{
LOG(Error, "Failed to call method '{0}.{1}' (args count: {2}) with invalid object instance of type '{3}'", String(mMethod->GetParentClass()->GetFullName()), String(mMethod->GetName()), parametersCount, String(MUtils::GetClassFullname(instanceObject)));
return true;
}

View File

@@ -108,6 +108,7 @@ namespace
};
ChunkedArray<Location, 256> ManagedSourceLocations;
ThreadLocal<uint32> ManagedEventsCount;
#endif
#endif
}
@@ -145,7 +146,9 @@ DEFINE_INTERNAL_CALL(void) ProfilerInternal_BeginEvent(MString* nameObj)
srcLoc->color = 0;
}
//static constexpr tracy::SourceLocationData tracySrcLoc{ nullptr, __FUNCTION__, __FILE__, (uint32_t)__LINE__, 0 };
tracy::ScopedZone::Begin(srcLoc);
const bool tracyActive = tracy::ScopedZone::Begin(srcLoc);
if (tracyActive)
ManagedEventsCount.Get()++;
#endif
#endif
#endif
@@ -155,7 +158,12 @@ DEFINE_INTERNAL_CALL(void) ProfilerInternal_EndEvent()
{
#if COMPILE_WITH_PROFILER
#if TRACY_ENABLE
tracy::ScopedZone::End();
uint32& tracyActive = ManagedEventsCount.Get();
if (tracyActive > 0)
{
tracyActive--;
tracy::ScopedZone::End();
}
#endif
ProfilerCPU::EndEvent();
#endif

View File

@@ -1657,7 +1657,7 @@ bool InitHostfxr()
// Get path to hostfxr library
get_hostfxr_parameters get_hostfxr_params;
get_hostfxr_params.size = sizeof(hostfxr_initialize_parameters);
get_hostfxr_params.size = sizeof(get_hostfxr_parameters);
get_hostfxr_params.assembly_path = libraryPath.Get();
#if PLATFORM_MAC
::String macOSDotnetRoot = TEXT("/usr/local/share/dotnet");

View File

@@ -27,6 +27,7 @@ Script::Script(const SpawnParams& params)
, _tickUpdate(false)
, _tickLateUpdate(false)
, _tickLateFixedUpdate(false)
, _wasAwakeCalled(false)
, _wasStartCalled(false)
, _wasEnableCalled(false)
{
@@ -86,7 +87,7 @@ void Script::SetParent(Actor* value, bool canBreakPrefabLink)
// Unlink from the old one
if (_parent)
{
if (!value && _parent->IsDuringPlay() && _parent->IsActiveInHierarchy() && GetEnabled())
if (!value && _parent->IsDuringPlay() && _parent->IsActiveInHierarchy() && GetEnabled() && _wasEnableCalled)
{
// Call disable when script is removed from actor (new actor is null)
Disable();
@@ -241,19 +242,31 @@ String Script::ToString() const
void Script::OnDeleteObject()
{
// Ensure to unlink from the parent (it will call Disable event if required)
SetParent(nullptr);
// Check if remove object from game
if (IsDuringPlay())
// Call OnDisable
if (_wasEnableCalled)
{
Disable();
}
// Call OnDestroy
if (_wasAwakeCalled)
{
_wasAwakeCalled = false;
CHECK_EXECUTE_IN_EDITOR
{
OnDestroy();
}
}
// End play
if (IsDuringPlay())
{
EndPlay();
}
// Unlink from parent
SetParent(nullptr);
// Base
SceneObject::OnDeleteObject();
}
@@ -274,6 +287,9 @@ void Script::Initialize()
if (!IsRegistered())
RegisterObject();
// Call OnAwake
ASSERT(!_wasAwakeCalled);
_wasAwakeCalled = true;
CHECK_EXECUTE_IN_EDITOR
{
OnAwake();

View File

@@ -20,6 +20,7 @@ protected:
uint16 _tickUpdate : 1;
uint16 _tickLateUpdate : 1;
uint16 _tickLateFixedUpdate : 1;
uint16 _wasAwakeCalled : 1;
uint16 _wasStartCalled : 1;
uint16 _wasEnableCalled : 1;
#if USE_EDITOR

View File

@@ -466,7 +466,7 @@ void JsonWriter::SceneObject(::SceneObject* obj)
prefab->GetDefaultInstance();
// Get prefab object instance from the prefab
const void* prefabObject;
::SceneObject* prefabObject;
if (prefab->ObjectsCache.TryGet(obj->GetPrefabObjectID(), prefabObject))
{
// Serialize modified properties compared with the default object from prefab
@@ -481,7 +481,7 @@ void JsonWriter::SceneObject(::SceneObject* obj)
}
else
{
LOG(Warning, "Missing prefab with id={0}.", obj->GetPrefabID());
LOG(Warning, "Missing prefab {0}.", obj->GetPrefabID());
}
}

View File

@@ -111,8 +111,9 @@ public:
struct StreamingCache
{
int64 LastUpdate = 0;
int32 TargetResidency = 0;
int64 TargetResidencyChange = 0;
int32 TargetResidency = 0;
bool Error = false;
SamplesBuffer<float, 5> QualitySamples;
};
@@ -131,7 +132,8 @@ public:
/// <summary>
/// Stops the streaming (eg. on streaming fail).
/// </summary>
void ResetStreaming();
/// <param name="error">True if streaming failed.</param>
void ResetStreaming(bool error = true);
protected:

View File

@@ -84,8 +84,9 @@ void StreamableResource::RequestStreamingUpdate()
Streaming.LastUpdate = 0;
}
void StreamableResource::ResetStreaming()
void StreamableResource::ResetStreaming(bool error)
{
Streaming.Error = error;
Streaming.TargetResidency = 0;
Streaming.LastUpdate = DateTime::MaxValue().Ticks;
}

View File

@@ -84,6 +84,8 @@ void TerrainChunk::Draw(const RenderContext& renderContext) const
DrawCall drawCall;
if (TerrainManager::GetChunkGeometry(drawCall, chunkSize, lod))
return;
if (!_neighbors[0])
const_cast<TerrainChunk*>(this)->CacheNeighbors();
drawCall.InstanceCount = 1;
drawCall.Material = _cachedDrawMaterial;
renderContext.View.GetWorldMatrix(_transform, drawCall.World);

Some files were not shown because too many files have changed in this diff Show More