Merge remote-tracking branch 'origin/master' into 1.9

# Conflicts:
#	Source/Engine/Renderer/RenderList.cpp
#	Source/Engine/Renderer/RenderList.h
This commit is contained in:
Wojtek Figat
2024-04-17 09:58:59 +02:00
99 changed files with 11337 additions and 1004 deletions

View File

@@ -3,6 +3,7 @@
#pragma once
#include "Engine/Core/Types/String.h"
#include "Engine/Core/Types/Pair.h"
#include "Engine/Core/Math/Transform.h"
#include "Engine/Animations/Curve.h"
@@ -68,6 +69,16 @@ public:
uint64 GetMemoryUsage() const;
};
/// <summary>
/// Single track with events.
/// </summary>
struct EventAnimationData
{
float Duration = 0.0f;
StringAnsi TypeName;
StringAnsi JsonData;
};
/// <summary>
/// Root Motion modes that can be applied by the animation. Used as flags for selective behavior.
/// </summary>
@@ -120,10 +131,15 @@ struct AnimationData
String RootNodeName;
/// <summary>
/// The per skeleton node animation channels.
/// The per-skeleton node animation channels.
/// </summary>
Array<NodeAnimationData> Channels;
/// <summary>
/// The animation event tracks.
/// </summary>
Array<Pair<String, StepCurve<EventAnimationData>>> Events;
public:
/// <summary>
/// Gets the length of the animation (in seconds).

View File

@@ -5,6 +5,7 @@
#include "Engine/Visject/VisjectGraph.h"
#include "Engine/Content/Assets/Animation.h"
#include "Engine/Core/Collections/ChunkedArray.h"
#include "Engine/Core/Collections/BitArray.h"
#include "Engine/Animations/AlphaBlend.h"
#include "Engine/Core/Math/Matrix.h"
#include "../Config.h"
@@ -892,7 +893,7 @@ private:
int32 GetRootNodeIndex(Animation* anim);
void ProcessAnimEvents(AnimGraphNode* node, bool loop, float length, float animPos, float animPrevPos, Animation* anim, float speed);
void ProcessAnimation(AnimGraphImpulse* nodes, AnimGraphNode* node, bool loop, float length, float pos, float prevPos, Animation* anim, float speed, float weight = 1.0f, ProcessAnimationMode mode = ProcessAnimationMode::Override);
void ProcessAnimation(AnimGraphImpulse* nodes, AnimGraphNode* node, bool loop, float length, float pos, float prevPos, Animation* anim, float speed, float weight = 1.0f, ProcessAnimationMode mode = ProcessAnimationMode::Override, BitArray<InlinedAllocation<8>>* usedNodes = nullptr);
Variant SampleAnimation(AnimGraphNode* node, bool loop, float length, float startTimePos, float prevTimePos, float& newTimePos, Animation* anim, float speed);
Variant SampleAnimationsWithBlend(AnimGraphNode* node, bool loop, float length, float startTimePos, float prevTimePos, float& newTimePos, Animation* animA, Animation* animB, float speedA, float speedB, float alpha);
Variant SampleAnimationsWithBlend(AnimGraphNode* node, bool loop, float length, float startTimePos, float prevTimePos, float& newTimePos, Animation* animA, Animation* animB, Animation* animC, float speedA, float speedB, float speedC, float alphaA, float alphaB, float alphaC);

View File

@@ -25,7 +25,7 @@ namespace
{
for (int32 i = 0; i < nodes->Nodes.Count(); i++)
{
nodes->Nodes[i].Orientation.Normalize();
nodes->Nodes.Get()[i].Orientation.Normalize();
}
if (rootMotionMode != RootMotionExtraction::NoExtraction)
{
@@ -222,7 +222,7 @@ FORCE_INLINE void GetAnimSamplePos(bool loop, float length, float startTimePos,
prevPos = GetAnimPos(prevTimePos, startTimePos, loop, length);
}
void AnimGraphExecutor::ProcessAnimation(AnimGraphImpulse* nodes, AnimGraphNode* node, bool loop, float length, float pos, float prevPos, Animation* anim, float speed, float weight, ProcessAnimationMode mode)
void AnimGraphExecutor::ProcessAnimation(AnimGraphImpulse* nodes, AnimGraphNode* node, bool loop, float length, float pos, float prevPos, Animation* anim, float speed, float weight, ProcessAnimationMode mode, BitArray<InlinedAllocation<8>>* usedNodes)
{
PROFILE_CPU_ASSET(anim);
@@ -240,9 +240,16 @@ void AnimGraphExecutor::ProcessAnimation(AnimGraphImpulse* nodes, AnimGraphNode*
}
// Evaluate nested animations
bool hasNested = false;
BitArray<InlinedAllocation<8>> usedNodesThis;
if (anim->NestedAnims.Count() != 0)
{
if (usedNodes == nullptr)
{
// Per-channel bit to indicate which channels were used by nested
usedNodesThis.Resize(nodes->Nodes.Count());
usedNodes = &usedNodesThis;
}
for (auto& e : anim->NestedAnims)
{
const auto& nestedAnim = e.Second;
@@ -262,8 +269,7 @@ void AnimGraphExecutor::ProcessAnimation(AnimGraphImpulse* nodes, AnimGraphNode*
nestedAnimPrevPos = nestedAnimPrevPos * frameRateMatchScale;
GetAnimSamplePos(nestedAnim.Loop, nestedAnimLength, nestedAnim.StartTime, nestedAnimPrevPos, nestedAnimPos, nestedAnimPos, nestedAnimPrevPos);
ProcessAnimation(nodes, node, true, nestedAnimLength, nestedAnimPos, nestedAnimPrevPos, nestedAnim.Anim, 1.0f, weight, mode);
hasNested = true;
ProcessAnimation(nodes, node, true, nestedAnimLength, nestedAnimPos, nestedAnimPrevPos, nestedAnim.Anim, 1.0f, weight, mode, usedNodes);
}
}
}
@@ -295,6 +301,15 @@ void AnimGraphExecutor::ProcessAnimation(AnimGraphImpulse* nodes, AnimGraphNode*
{
RetargetSkeletonNode(mapping.SourceSkeleton->Skeleton, mapping.TargetSkeleton->Skeleton, sourceMapping, srcNode, i);
}
// Mark node as used
if (usedNodes)
usedNodes->Set(i, true);
}
else if (usedNodes && usedNodes != &usedNodesThis)
{
// Skip for nested animations so other one or top-level anim will update remaining nodes
continue;
}
// Blend node
@@ -316,7 +331,7 @@ void AnimGraphExecutor::ProcessAnimation(AnimGraphImpulse* nodes, AnimGraphNode*
dstNode.Scale = srcNode.Scale * weight;
dstNode.Orientation = srcNode.Orientation * weight;
}
else if (!hasNested)
else
{
dstNode = srcNode;
}
@@ -1177,14 +1192,12 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu
{
const float alpha = Math::Saturate((float)tryGetValue(node->GetBox(3), node->Values[0]));
auto mask = node->Assets[0].As<SkeletonMask>();
auto maskAssetBox = node->GetBox(4); // 4 is the id of skeleton mask parameter node.
// Check if have some mask asset connected with the mask node
if (maskAssetBox->HasConnection())
// Use the mask connected with this node instead of default mask asset
auto maskAssetBox = node->TryGetBox(4);
if (maskAssetBox && maskAssetBox->HasConnection())
{
const Value assetBoxValue = tryGetValue(maskAssetBox, Value::Null);
// Use the mask connected with this node instead of default mask asset
if (assetBoxValue != Value::Null)
mask = (SkeletonMask*)assetBoxValue.AsAsset;
}

View File

@@ -10,79 +10,102 @@ void InverseKinematics::SolveAimIK(const Transform& node, const Vector3& target,
Quaternion::FindBetween(fromNode, toTarget, outNodeCorrection);
}
void InverseKinematics::SolveTwoBoneIK(Transform& rootNode, Transform& jointNode, Transform& targetNode, const Vector3& target, const Vector3& jointTarget, bool allowStretching, float maxStretchScale)
{
Real lowerLimbLength = (targetNode.Translation - jointNode.Translation).Length();
Real upperLimbLength = (jointNode.Translation - rootNode.Translation).Length();
Vector3 jointPos = jointNode.Translation;
Vector3 desiredDelta = target - rootNode.Translation;
Real desiredLength = desiredDelta.Length();
Real limbLengthLimit = lowerLimbLength + upperLimbLength;
Vector3 desiredDir;
if (desiredLength < ZeroTolerance)
void InverseKinematics::SolveTwoBoneIK(Transform& rootTransform, Transform& midJointTransform, Transform& endEffectorTransform, const Vector3& targetPosition, const Vector3& poleVector, bool allowStretching, float maxStretchScale)
{
// Calculate limb segment lengths
Real lowerLimbLength = (endEffectorTransform.Translation - midJointTransform.Translation).Length();
Real upperLimbLength = (midJointTransform.Translation - rootTransform.Translation).Length();
Vector3 midJointPos = midJointTransform.Translation;
// Calculate the direction and length towards the target
Vector3 toTargetVector = targetPosition - rootTransform.Translation;
Real toTargetLength = toTargetVector.Length();
Real totalLimbLength = lowerLimbLength + upperLimbLength;
// Normalize the direction vector or set a default direction if too small
Vector3 toTargetDir;
if (toTargetLength < ZeroTolerance)
{
desiredLength = ZeroTolerance;
desiredDir = Vector3(1, 0, 0);
toTargetLength = ZeroTolerance;
toTargetDir = Vector3(1, 0, 0);
}
else
{
desiredDir = desiredDelta.GetNormalized();
toTargetDir = toTargetVector.GetNormalized();
}
Vector3 jointTargetDelta = jointTarget - rootNode.Translation;
const Real jointTargetLengthSqr = jointTargetDelta.LengthSquared();
// Calculate the pole vector direction
Vector3 poleVectorDelta = poleVector - rootTransform.Translation;
const Real poleVectorLengthSqr = poleVectorDelta.LengthSquared();
Vector3 jointPlaneNormal, jointBendDir;
if (jointTargetLengthSqr < ZeroTolerance * ZeroTolerance)
Vector3 jointPlaneNormal, bendDirection;
if (poleVectorLengthSqr < ZeroTolerance * ZeroTolerance)
{
jointBendDir = Vector3::Forward;
bendDirection = Vector3::Forward;
jointPlaneNormal = Vector3::Up;
}
else
{
jointPlaneNormal = desiredDir ^ jointTargetDelta;
jointPlaneNormal = toTargetDir ^ poleVectorDelta;
if (jointPlaneNormal.LengthSquared() < ZeroTolerance * ZeroTolerance)
{
desiredDir.FindBestAxisVectors(jointPlaneNormal, jointBendDir);
toTargetDir.FindBestAxisVectors(jointPlaneNormal, bendDirection);
}
else
{
jointPlaneNormal.Normalize();
jointBendDir = jointTargetDelta - (jointTargetDelta | desiredDir) * desiredDir;
jointBendDir.Normalize();
bendDirection = poleVectorDelta - (poleVectorDelta | toTargetDir) * toTargetDir;
bendDirection.Normalize();
}
}
// Handle limb stretching if allowed
if (allowStretching)
{
const Real initialStretchRatio = 1.0f;
const Real range = maxStretchScale - initialStretchRatio;
if (range > ZeroTolerance && limbLengthLimit > ZeroTolerance)
const Real stretchRange = maxStretchScale - initialStretchRatio;
if (stretchRange > ZeroTolerance && totalLimbLength > ZeroTolerance)
{
const Real reachRatio = desiredLength / limbLengthLimit;
const Real scalingFactor = (maxStretchScale - 1.0f) * Math::Saturate((reachRatio - initialStretchRatio) / range);
const Real reachRatio = toTargetLength / totalLimbLength;
const Real scalingFactor = (maxStretchScale - 1.0f) * Math::Saturate((reachRatio - initialStretchRatio) / stretchRange);
if (scalingFactor > ZeroTolerance)
{
lowerLimbLength *= 1.0f + scalingFactor;
upperLimbLength *= 1.0f + scalingFactor;
limbLengthLimit *= 1.0f + scalingFactor;
totalLimbLength *= 1.0f + scalingFactor;
}
}
}
Vector3 resultEndPos = target;
Vector3 resultJointPos = jointPos;
// Calculate new positions for joint and end effector
Vector3 newEndEffectorPos = targetPosition;
Vector3 newMidJointPos = midJointPos;
if (desiredLength >= limbLengthLimit)
{
resultEndPos = rootNode.Translation + limbLengthLimit * desiredDir;
resultJointPos = rootNode.Translation + upperLimbLength * desiredDir;
if (toTargetLength >= totalLimbLength) {
// Target is beyond the reach of the limb
Vector3 rootToEnd = (targetPosition - rootTransform.Translation).GetNormalized();
// Calculate the slight offset towards the pole vector
Vector3 rootToPole = (poleVector - rootTransform.Translation).GetNormalized();
Vector3 slightBendDirection = Vector3::Cross(rootToEnd, rootToPole);
if (slightBendDirection.LengthSquared() < ZeroTolerance * ZeroTolerance) {
slightBendDirection = Vector3::Up;
}
else {
slightBendDirection.Normalize();
}
// Calculate the direction from root to mid joint with a slight offset towards the pole vector
Vector3 midJointDirection = Vector3::Cross(slightBendDirection, rootToEnd).GetNormalized();
Real slightOffset = upperLimbLength * 0.01f; // Small percentage of the limb length for slight offset
newMidJointPos = rootTransform.Translation + rootToEnd * (upperLimbLength - slightOffset) + midJointDirection * slightOffset;
}
else
{
const Real twoAb = 2.0f * upperLimbLength * desiredLength;
const Real cosAngle = twoAb > ZeroTolerance ? (upperLimbLength * upperLimbLength + desiredLength * desiredLength - lowerLimbLength * lowerLimbLength) / twoAb : 0.0f;
// Target is within reach, calculate joint position
const Real twoAb = 2.0f * upperLimbLength * toTargetLength;
const Real cosAngle = twoAb > ZeroTolerance ? (upperLimbLength * upperLimbLength + toTargetLength * toTargetLength - lowerLimbLength * lowerLimbLength) / twoAb : 0.0f;
const bool reverseUpperBone = cosAngle < 0.0f;
const Real angle = Math::Acos(cosAngle);
const Real jointLineDist = upperLimbLength * Math::Sin(angle);
@@ -90,23 +113,66 @@ void InverseKinematics::SolveTwoBoneIK(Transform& rootNode, Transform& jointNode
Real projJointDist = projJointDistSqr > 0.0f ? Math::Sqrt(projJointDistSqr) : 0.0f;
if (reverseUpperBone)
projJointDist *= -1.0f;
resultJointPos = rootNode.Translation + projJointDist * desiredDir + jointLineDist * jointBendDir;
newMidJointPos = rootTransform.Translation + projJointDist * toTargetDir + jointLineDist * bendDirection;
}
// Update root joint orientation
{
const Vector3 oldDir = (jointPos - rootNode.Translation).GetNormalized();
const Vector3 newDir = (resultJointPos - rootNode.Translation).GetNormalized();
const Quaternion deltaRotation = Quaternion::FindBetween(oldDir, newDir);
rootNode.Orientation = deltaRotation * rootNode.Orientation;
// Vector from root joint to mid joint (local Y-axis direction)
Vector3 localY = (newMidJointPos - rootTransform.Translation).GetNormalized();
// Vector from mid joint to end effector (used to calculate plane normal)
Vector3 midToEnd = (newEndEffectorPos - newMidJointPos).GetNormalized();
// Calculate the plane normal (local Z-axis direction)
Vector3 localZ = Vector3::Cross(localY, midToEnd).GetNormalized();
// Calculate the local X-axis direction, should be perpendicular to the Y and Z axes
Vector3 localX = Vector3::Cross(localY, localZ).GetNormalized();
// Correct the local Z-axis direction based on the cross product of X and Y to ensure orthogonality
localZ = Vector3::Cross(localX, localY).GetNormalized();
// Construct a rotation from the orthogonal basis vectors
Quaternion newRootJointOrientation = Quaternion::LookRotation(localZ, localY);
// Apply the new rotation to the root joint
rootTransform.Orientation = newRootJointOrientation;
}
// Update mid joint orientation to point Y-axis towards the end effector and Z-axis perpendicular to the IK plane
{
const Vector3 oldDir = (targetNode.Translation - jointPos).GetNormalized();
const Vector3 newDir = (resultEndPos - resultJointPos).GetNormalized();
const Quaternion deltaRotation = Quaternion::FindBetween(oldDir, newDir);
jointNode.Orientation = deltaRotation * jointNode.Orientation;
jointNode.Translation = resultJointPos;
// Vector from mid joint to end effector (local Y-axis direction after rotation)
Vector3 midToEnd = (newEndEffectorPos - newMidJointPos).GetNormalized();
// Calculate the plane normal using the root, mid joint, and end effector positions (will be the local Z-axis direction)
Vector3 rootToMid = (newMidJointPos - rootTransform.Translation).GetNormalized();
Vector3 planeNormal = Vector3::Cross(rootToMid, midToEnd).GetNormalized();
// Vector from mid joint to end effector (local Y-axis direction)
Vector3 localY = (newEndEffectorPos - newMidJointPos).GetNormalized();
// Calculate the plane normal using the root, mid joint, and end effector positions (local Z-axis direction)
Vector3 localZ = Vector3::Cross(rootToMid, localY).GetNormalized();
//// Calculate the local X-axis direction, should be perpendicular to the Y and Z axes
Vector3 localX = Vector3::Cross(localY, localZ).GetNormalized();
// Correct the local Z-axis direction based on the cross product of X and Y to ensure orthogonality
localZ = Vector3::Cross(localX, localY).GetNormalized();
// Construct a rotation from the orthogonal basis vectors
// The axes are used differently here than a standard LookRotation to align Z towards the end and Y perpendicular
Quaternion newMidJointOrientation = Quaternion::LookRotation(localZ, localY); // Assuming FromLookRotation creates a rotation with the first vector as forward and the second as up
// Apply the new rotation to the mid joint
midJointTransform.Orientation = newMidJointOrientation;
midJointTransform.Translation = newMidJointPos;
}
targetNode.Translation = resultEndPos;
// Update end effector transform
endEffectorTransform.Translation = newEndEffectorPos;
}

View File

@@ -152,12 +152,17 @@ void AudioSource::Play()
RequestStreamingBuffersUpdate();
}
}
else
else if (SourceIDs.HasItems())
{
// Play it right away
SetNonStreamingBuffer();
PlayInternal();
}
else
{
// Source was nt properly added to the Audio Backend
LOG(Warning, "Cannot play unitialized audio source.");
}
}
void AudioSource::Pause()

View File

@@ -184,9 +184,8 @@ void SoftAssetReferenceBase::OnUnloaded(Asset* asset)
Asset::Asset(const SpawnParams& params, const AssetInfo* info)
: ManagedScriptingObject(params)
, _refCount(0)
, _loadingTask(nullptr)
, _isLoaded(false)
, _loadFailed(false)
, _loadState(0)
, _loadingTask(0)
, _deleteFileOnUnload(false)
, _isVirtual(false)
{
@@ -225,10 +224,10 @@ void Asset::OnDeleteObject()
// Unload asset data (in a safe way to protect asset data)
Locker.Lock();
if (_isLoaded)
if (IsLoaded())
{
unload(false);
_isLoaded = false;
Platform::AtomicStore(&_loadState, (int64)LoadState::Unloaded);
}
Locker.Unlock();
@@ -319,11 +318,6 @@ void Asset::ChangeID(const Guid& newId)
Content::onAssetChangeId(this, oldId, newId);
}
bool Asset::LastLoadFailed() const
{
return _loadFailed != 0;
}
#if USE_EDITOR
bool Asset::ShouldDeleteFileOnUnload() const
@@ -337,7 +331,7 @@ uint64 Asset::GetMemoryUsage() const
{
uint64 result = sizeof(Asset);
Locker.Lock();
if (_loadingTask)
if (Platform::AtomicRead(&_loadingTask))
result += sizeof(ContentLoadTask);
result += (OnLoaded.Capacity() + OnReloading.Capacity() + OnUnloaded.Capacity()) * sizeof(EventType::FunctionType);
Locker.Unlock();
@@ -368,7 +362,7 @@ void Asset::Reload()
{
// Unload current data
unload(true);
_isLoaded = false;
Platform::AtomicStore(&_loadState, (int64)LoadState::Unloaded);
}
// Start reloading process
@@ -426,7 +420,7 @@ bool Asset::WaitForLoaded(double timeoutInMilliseconds) const
// Check if has missing loading task
Platform::MemoryBarrier();
const auto loadingTask = _loadingTask;
const auto loadingTask = (ContentLoadTask*)Platform::AtomicRead(&_loadingTask);
if (loadingTask == nullptr)
{
LOG(Warning, "WaitForLoaded asset \'{0}\' failed. No loading task attached and asset is not loaded.", ToString());
@@ -516,7 +510,7 @@ bool Asset::WaitForLoaded(double timeoutInMilliseconds) const
Content::tryCallOnLoaded((Asset*)this);
}
return _isLoaded == 0;
return !IsLoaded();
}
void Asset::InitAsVirtual()
@@ -525,14 +519,14 @@ void Asset::InitAsVirtual()
_isVirtual = true;
// Be a loaded thing
_isLoaded = true;
Platform::AtomicStore(&_loadState, (int64)LoadState::Loaded);
}
void Asset::CancelStreaming()
{
// Cancel loading task but go over asset locker to prevent case if other load threads still loads asset while it's reimported on other thread
Locker.Lock();
ContentLoadTask* loadTask = _loadingTask;
auto loadTask = (ContentLoadTask*)Platform::AtomicRead(&_loadingTask);
Locker.Unlock();
if (loadTask)
{
@@ -575,10 +569,11 @@ ContentLoadTask* Asset::createLoadingTask()
void Asset::startLoading()
{
ASSERT(!IsLoaded());
ASSERT(_loadingTask == nullptr);
_loadingTask = createLoadingTask();
ASSERT(_loadingTask != nullptr);
_loadingTask->Start();
ASSERT(Platform::AtomicRead(&_loadingTask) == 0);
auto loadingTask = createLoadingTask();
ASSERT(loadingTask != nullptr);
Platform::AtomicStore(&_loadingTask, (intptr)loadingTask);
loadingTask->Start();
}
void Asset::releaseStorage()
@@ -593,7 +588,7 @@ bool Asset::IsInternalType() const
bool Asset::onLoad(LoadAssetTask* task)
{
// It may fail when task is cancelled and new one is created later (don't crash but just end with an error)
if (task->Asset.Get() != this || _loadingTask == nullptr)
if (task->Asset.Get() != this || Platform::AtomicRead(&_loadingTask) == 0)
return true;
Locker.Lock();
@@ -606,15 +601,15 @@ bool Asset::onLoad(LoadAssetTask* task)
}
const bool isLoaded = result == LoadResult::Ok;
const bool failed = !isLoaded;
_loadFailed = failed;
_isLoaded = !failed;
LoadState state = LoadState::Loaded;
Platform::AtomicStore(&_loadState, (int64)(isLoaded ? LoadState::Loaded : LoadState::LoadFailed));
if (failed)
{
LOG(Error, "Loading asset \'{0}\' result: {1}.", ToString(), ToString(result));
}
// Unlink task
_loadingTask = nullptr;
Platform::AtomicStore(&_loadingTask, 0);
ASSERT(failed || IsLoaded() == isLoaded);
Locker.Unlock();
@@ -663,12 +658,12 @@ void Asset::onUnload_MainThread()
OnUnloaded(this);
// Check if is during loading
if (_loadingTask != nullptr)
auto loadingTask = (ContentLoadTask*)Platform::AtomicRead(&_loadingTask);
if (loadingTask != nullptr)
{
// Cancel loading
auto task = _loadingTask;
_loadingTask = nullptr;
Platform::AtomicStore(&_loadingTask, 0);
LOG(Warning, "Cancel loading task for \'{0}\'", ToString());
task->Cancel();
loadingTask->Cancel();
}
}

View File

@@ -34,11 +34,17 @@ public:
DECLARE_ENUM_7(LoadResult, Ok, Failed, MissingDataChunk, CannotLoadData, CannotLoadStorage, CannotLoadInitData, InvalidData);
protected:
volatile int64 _refCount;
ContentLoadTask* _loadingTask;
enum class LoadState : int64
{
Unloaded,
Loaded,
LoadFailed,
};
volatile int64 _refCount;
volatile int64 _loadState;
volatile intptr _loadingTask;
int8 _isLoaded : 1; // Indicates that asset is loaded
int8 _loadFailed : 1; // Indicates that last asset loading has failed
int8 _deleteFileOnUnload : 1; // Indicates that asset source file should be removed on asset unload
int8 _isVirtual : 1; // Indicates that asset is pure virtual (generated or temporary, has no storage so won't be saved)
@@ -111,13 +117,16 @@ public:
/// </summary>
API_PROPERTY() FORCE_INLINE bool IsLoaded() const
{
return _isLoaded != 0;
return Platform::AtomicRead(&_loadState) == (int64)LoadState::Loaded;
}
/// <summary>
/// Returns true if last asset loading failed, otherwise false.
/// </summary>
API_PROPERTY() bool LastLoadFailed() const;
API_PROPERTY() bool LastLoadFailed() const
{
return Platform::AtomicRead(&_loadState) == (int64)LoadState::LoadFailed;
}
/// <summary>
/// Determines whether this asset is virtual (generated or temporary, has no storage so it won't be saved).

View File

@@ -1434,8 +1434,7 @@ Asset::LoadResult VisualScript::load()
if (_instances.HasItems())
{
// Mark as already loaded so any WaitForLoaded checks during GetDefaultInstance bellow will handle this Visual Script as ready to use
_loadFailed = false;
_isLoaded = true;
Platform::AtomicStore(&_loadState, (int64)LoadState::Loaded);
// Setup scripting type
CacheScriptingType();

View File

@@ -14,6 +14,7 @@
#include "Engine/Engine/EngineService.h"
#include "Engine/Platform/FileSystem.h"
#include "Engine/Threading/Threading.h"
#include "Engine/Threading/MainThreadTask.h"
#include "Engine/Graphics/Graphics.h"
#include "Engine/Engine/Time.h"
#include "Engine/Engine/Globals.h"
@@ -688,101 +689,135 @@ bool Content::FastTmpAssetClone(const StringView& path, String& resultPath)
return false;
}
bool Content::CloneAssetFile(const StringView& dstPath, const StringView& srcPath, const Guid& dstId)
class CloneAssetFileTask : public MainThreadTask
{
PROFILE_CPU();
ASSERT(FileSystem::AreFilePathsEqual(srcPath, dstPath) == false && dstId.IsValid());
public:
StringView dstPath;
StringView srcPath;
Guid dstId;
bool* output;
LOG(Info, "Cloning asset \'{0}\' to \'{1}\'({2}).", srcPath, dstPath, dstId);
// Check source file
if (!FileSystem::FileExists(srcPath))
protected:
bool Run() override
{
LOG(Warning, "Missing source file.");
return true;
}
// Special case for json resources
if (JsonStorageProxy::IsValidExtension(FileSystem::GetExtension(srcPath).ToLower()))
{
if (FileSystem::CopyFile(dstPath, srcPath))
{
LOG(Warning, "Cannot copy file to destination.");
return true;
}
if (JsonStorageProxy::ChangeId(dstPath, dstId))
{
LOG(Warning, "Cannot change asset ID.");
return true;
}
*output = Content::CloneAssetFile(dstPath, srcPath, dstId);
return false;
}
};
// Check if destination file is missing
if (!FileSystem::FileExists(dstPath))
bool Content::CloneAssetFile(const StringView& dstPath, const StringView& srcPath, const Guid& dstId)
{
// Best to run this on the main thread to avoid clone conflicts.
if (IsInMainThread())
{
// Use quick file copy
if (FileSystem::CopyFile(dstPath, srcPath))
PROFILE_CPU();
ASSERT(FileSystem::AreFilePathsEqual(srcPath, dstPath) == false && dstId.IsValid());
LOG(Info, "Cloning asset \'{0}\' to \'{1}\'({2}).", srcPath, dstPath, dstId);
// Check source file
if (!FileSystem::FileExists(srcPath))
{
LOG(Warning, "Cannot copy file to destination.");
LOG(Warning, "Missing source file.");
return true;
}
// Change ID
auto storage = ContentStorageManager::GetStorage(dstPath);
FlaxStorage::Entry e;
storage->GetEntry(0, e);
if (storage == nullptr || storage->ChangeAssetID(e, dstId))
// Special case for json resources
if (JsonStorageProxy::IsValidExtension(FileSystem::GetExtension(srcPath).ToLower()))
{
LOG(Warning, "Cannot change asset ID.");
return true;
if (FileSystem::CopyFile(dstPath, srcPath))
{
LOG(Warning, "Cannot copy file to destination.");
return true;
}
if (JsonStorageProxy::ChangeId(dstPath, dstId))
{
LOG(Warning, "Cannot change asset ID.");
return true;
}
return false;
}
// Check if destination file is missing
if (!FileSystem::FileExists(dstPath))
{
// Use quick file copy
if (FileSystem::CopyFile(dstPath, srcPath))
{
LOG(Warning, "Cannot copy file to destination.");
return true;
}
// Change ID
auto storage = ContentStorageManager::GetStorage(dstPath);
FlaxStorage::Entry e;
storage->GetEntry(0, e);
if (storage == nullptr || storage->ChangeAssetID(e, dstId))
{
LOG(Warning, "Cannot change asset ID.");
return true;
}
}
else
{
// Use temporary file
String tmpPath = Globals::TemporaryFolder / Guid::New().ToString(Guid::FormatType::D);
if (FileSystem::CopyFile(tmpPath, srcPath))
{
LOG(Warning, "Cannot copy file.");
return true;
}
// Change asset ID
{
auto storage = ContentStorageManager::GetStorage(tmpPath);
if (!storage)
{
LOG(Warning, "Cannot change asset ID.");
return true;
}
FlaxStorage::Entry e;
storage->GetEntry(0, e);
if (storage->ChangeAssetID(e, dstId))
{
LOG(Warning, "Cannot change asset ID.");
return true;
}
}
// Unlock destination file
ContentStorageManager::EnsureAccess(dstPath);
// Copy temp file to the destination
if (FileSystem::CopyFile(dstPath, tmpPath))
{
LOG(Warning, "Cannot copy file to destination.");
return true;
}
// Cleanup
FileSystem::DeleteFile(tmpPath);
// Reload storage
if (auto storage = ContentStorageManager::GetStorage(dstPath))
{
storage->Reload();
}
}
}
else
{
// Use temporary file
String tmpPath = Globals::TemporaryFolder / Guid::New().ToString(Guid::FormatType::D);
if (FileSystem::CopyFile(tmpPath, srcPath))
{
LOG(Warning, "Cannot copy file.");
return true;
}
CloneAssetFileTask* task = New<CloneAssetFileTask>();
task->dstId = dstId;
task->dstPath = dstPath;
task->srcPath = srcPath;
// Change asset ID
{
auto storage = ContentStorageManager::GetStorage(tmpPath);
if (!storage)
{
LOG(Warning, "Cannot change asset ID.");
return true;
}
FlaxStorage::Entry e;
storage->GetEntry(0, e);
if (storage->ChangeAssetID(e, dstId))
{
LOG(Warning, "Cannot change asset ID.");
return true;
}
}
bool result = false;
task->output = &result;
task->Start();
task->Wait();
// Unlock destination file
ContentStorageManager::EnsureAccess(dstPath);
// Copy temp file to the destination
if (FileSystem::CopyFile(dstPath, tmpPath))
{
LOG(Warning, "Cannot copy file to destination.");
return true;
}
// Cleanup
FileSystem::DeleteFile(tmpPath);
// Reload storage
if (auto storage = ContentStorageManager::GetStorage(dstPath))
{
storage->Reload();
}
return result;
}
return false;

View File

@@ -31,12 +31,11 @@ public:
if (Asset)
{
Asset->Locker.Lock();
if (Asset->_loadingTask == this)
if (Platform::AtomicRead(&Asset->_loadingTask) == (intptr)this)
{
Asset->_loadFailed = true;
Asset->_isLoaded = false;
Platform::AtomicStore(&Asset->_loadState, (int64)Asset::LoadState::LoadFailed);
Platform::AtomicStore(&Asset->_loadingTask, 0);
LOG(Error, "Loading asset \'{0}\' result: {1}.", ToString(), ToString(Result::TaskFailed));
Asset->_loadingTask = nullptr;
}
Asset->Locker.Unlock();
}
@@ -77,8 +76,8 @@ protected:
if (Asset)
{
Asset->Locker.Lock();
if (Asset->_loadingTask == this)
Asset->_loadingTask = nullptr;
if (Platform::AtomicRead(&Asset->_loadingTask) == (intptr)this)
Platform::AtomicStore(&Asset->_loadingTask, 0);
Asset->Locker.Unlock();
Asset = nullptr;
}
@@ -91,8 +90,8 @@ protected:
if (Asset)
{
Asset->Locker.Lock();
if (Asset->_loadingTask == this)
Asset->_loadingTask = nullptr;
if (Platform::AtomicRead(&Asset->_loadingTask) == (intptr)this)
Platform::AtomicStore(&Asset->_loadingTask, 0);
Asset->Locker.Unlock();
Asset = nullptr;
}

View File

@@ -413,6 +413,7 @@ bool AssetsImportingManagerService::Init()
{ TEXT("jpg"), ASSET_FILES_EXTENSION, ImportTexture::Import },
{ TEXT("hdr"), ASSET_FILES_EXTENSION, ImportTexture::Import },
{ TEXT("raw"), ASSET_FILES_EXTENSION, ImportTexture::Import },
{ TEXT("exr"), ASSET_FILES_EXTENSION, ImportTexture::Import },
// IES Profiles
{ TEXT("ies"), ASSET_FILES_EXTENSION, ImportTexture::ImportIES },

View File

@@ -15,6 +15,7 @@
#include "Engine/Content/Storage/ContentStorageManager.h"
#include "Engine/Content/Assets/Animation.h"
#include "Engine/Content/Content.h"
#include "Engine/Animations/AnimEvent.h"
#include "Engine/Level/Actors/EmptyActor.h"
#include "Engine/Level/Actors/StaticModel.h"
#include "Engine/Level/Prefabs/Prefab.h"
@@ -141,48 +142,6 @@ void RepackMeshLightmapUVs(ModelData& data)
}
}
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;
@@ -458,10 +417,62 @@ CreateAssetResult ImportModel::Import(CreateAssetContext& context)
data = &dataThis;
}
// Check if restore materials on model reimport
if (options.RestoreMaterialsOnReimport && data->Materials.HasItems())
// Check if restore local changes on asset reimport
constexpr bool RestoreAnimEventsOnReimport = true;
const bool restoreMaterials = options.RestoreMaterialsOnReimport && data->Materials.HasItems();
const bool restoreAnimEvents = RestoreAnimEventsOnReimport && options.Type == ModelTool::ModelType::Animation && data->Animations.HasItems();
if ((restoreMaterials || restoreAnimEvents) && FileSystem::FileExists(context.TargetAssetPath))
{
TryRestoreMaterials(context, *data);
AssetReference<Asset> asset = Content::LoadAsync<Asset>(context.TargetAssetPath);
if (asset && !asset->WaitForLoaded())
{
auto* model = ScriptingObject::Cast<ModelBase>(asset);
auto* animation = ScriptingObject::Cast<Animation>(asset);
if (restoreMaterials && model)
{
// Copy material settings
for (int32 i = 0; i < data->Materials.Count(); i++)
{
auto& dstSlot = data->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();
}
}
}
if (restoreAnimEvents && animation)
{
// Copy anim event tracks
for (const auto& e : animation->Events)
{
auto& clone = data->Animations[0].Events.AddOne();
clone.First = e.First;
const auto& eKeys = e.Second.GetKeyframes();
auto& cloneKeys = clone.Second.GetKeyframes();
clone.Second.Resize(eKeys.Count());
for (int32 i = 0; i < eKeys.Count(); i++)
{
const auto& eKey = eKeys[i];
auto& cloneKey = cloneKeys[i];
cloneKey.Time = eKey.Time;
cloneKey.Value.Duration = eKey.Value.Duration;
if (eKey.Value.Instance)
{
cloneKey.Value.TypeName = eKey.Value.Instance->GetType().Fullname;
rapidjson_flax::StringBuffer buffer;
CompactJsonWriter writer(buffer);
writer.StartObject();
eKey.Value.Instance->Serialize(writer, nullptr);
writer.EndObject();
cloneKey.Value.JsonData.Set(buffer.GetString(), buffer.GetSize());
}
}
}
}
}
}
// 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

View File

@@ -44,6 +44,49 @@ public:
}
};
private:
template<typename T>
static void Merge(T* data, T* tmp, int32 start, int32 mid, int32 end)
{
int32 h = start;
int32 i = start;
int32 j = mid + 1;
while (h <= mid && j <= end)
{
if (data[h] < data[j])
tmp[i] = data[h++];
else
tmp[i] = data[j++];
i++;
}
if (h > mid)
{
for (int32 k = j; k <= end; k++)
tmp[i++] = data[k];
}
else
{
for (int32 k = h; k <= mid; k++)
tmp[i++] = data[k];
}
for (int32 k = start; k <= end; k++)
data[k] = tmp[k];
}
template<typename T>
static void MergeSort(T* data, T* tmp, int32 start, int32 end)
{
if (start >= end)
return;
const int32 mid = (start + end) / 2;
MergeSort(data, tmp, start, mid);
MergeSort(data, tmp, mid + 1, end);
Merge(data, tmp, start, mid, end);
}
public:
/// <summary>
/// Sorts the linear data array using Quick Sort algorithm (non recursive version, uses temporary stack collection).
@@ -263,6 +306,33 @@ public:
}
}
/// <summary>
/// Sorts the linear data array using Merge Sort algorithm (recursive version, uses temporary memory).
/// </summary>
/// <param name="data">The data pointer.</param>
/// <param name="count">The elements count.</param>
/// <param name="tmp">The additional temporary memory buffer for sorting data. If null then will be automatically allocated within this function call.</param>
template<typename T>
static void MergeSort(T* data, int32 count, T* tmp = nullptr)
{
if (count < 2)
return;
const bool alloc = tmp == nullptr;
if (alloc)
tmp = (T*)Platform::Allocate(sizeof(T) * count, 16);
MergeSort(data, tmp, 0, count - 1);
if (alloc)
Platform::Free(tmp);
}
template<typename T, typename AllocationType = HeapAllocation>
FORCE_INLINE static void MergeSort(Array<T, AllocationType>& data, Array<T, AllocationType>* tmp = nullptr)
{
if (tmp)
tmp->Resize(data.Count());
MergeSort(data.Get(), data.Count(), tmp ? tmp->Get() : nullptr);
}
/// <summary>
/// Sorts the linear data array using Radix Sort algorithm (uses temporary keys collection).
/// </summary>

View File

@@ -14,6 +14,8 @@
#define FORCE_INLINE inline
#define FORCE_NOINLINE __attribute__((noinline))
#define NO_RETURN __attribute__((noreturn))
#define NO_SANITIZE_ADDRESS __attribute__((no_sanitize_address))
#define NO_SANITIZE_THREAD __attribute__((no_sanitize_thread))
#define PACK_BEGIN()
#define PACK_END() __attribute__((__packed__))
#define ALIGN_BEGIN(_align)
@@ -44,6 +46,8 @@
#define FORCE_INLINE inline
#define FORCE_NOINLINE __attribute__((noinline))
#define NO_RETURN __attribute__((noreturn))
#define NO_SANITIZE_ADDRESS __attribute__((no_sanitize_address))
#define NO_SANITIZE_THREAD __attribute__((no_sanitize_thread))
#define PACK_BEGIN()
#define PACK_END() __attribute__((__packed__))
#define ALIGN_BEGIN(_align)
@@ -69,6 +73,8 @@
#define FORCE_INLINE __forceinline
#define FORCE_NOINLINE __declspec(noinline)
#define NO_RETURN __declspec(noreturn)
#define NO_SANITIZE_ADDRESS
#define NO_SANITIZE_THREAD
#define PACK_BEGIN() __pragma(pack(push, 1))
#define PACK_END() ; __pragma(pack(pop))
#define ALIGN_BEGIN(_align) __declspec(align(_align))

View File

@@ -94,4 +94,15 @@ namespace Utilities
return (x * 0x01010101) >> 24;
#endif
}
// Copy memory region but ignoring address sanatizer checks for memory regions.
NO_SANITIZE_ADDRESS static void UnsafeMemoryCopy(void* dst, const void* src, uint64 size)
{
#if BUILD_RELEASE
memcpy(dst, src, static_cast<size_t>(size));
#else
for (uint64 i = 0; i < size; i++)
((byte*)dst)[i] = ((byte*)src)[i];
#endif
}
}

View File

@@ -129,7 +129,7 @@ PACK_STRUCT(struct Data {
Matrix ViewProjection;
Float2 Padding;
float ClipPosZBias;
bool EnableDepthTest;
uint32 EnableDepthTest;
});
struct PsData

View File

@@ -2,6 +2,7 @@
#include "CommandLine.h"
#include "Engine/Core/Collections/Array.h"
#include "Engine/Core/Utilities.h"
#include <iostream>
CommandLine::OptionsData CommandLine::Options;
@@ -81,7 +82,7 @@ bool CommandLine::Parse(const Char* cmdLine)
if (pos) \
{ \
len = ARRAY_COUNT(text) - 1; \
Platform::MemoryCopy(pos, pos + len, (end - pos - len) * 2); \
Utilities::UnsafeMemoryCopy(pos, pos + len, (end - pos - len) * 2); \
*(end - len) = 0; \
end -= len; \
Options.field = true; \
@@ -98,7 +99,7 @@ bool CommandLine::Parse(const Char* cmdLine)
} \
Options.field = String(argStart, static_cast<int32>(argEnd - argStart)); \
len = static_cast<int32>((argEnd - pos) + 1); \
Platform::MemoryCopy(pos, pos + len, (end - pos - len) * 2); \
Utilities::UnsafeMemoryCopy(pos, pos + len, (end - pos - len) * 2); \
*(end - len) = 0; \
end -= len; \
}
@@ -114,7 +115,7 @@ bool CommandLine::Parse(const Char* cmdLine)
{ \
Options.field = String(argStart, static_cast<int32>(argEnd - argStart)); \
len = static_cast<int32>((argEnd - pos) + 1); \
Platform::MemoryCopy(pos, pos + len, (end - pos - len) * 2); \
Utilities::UnsafeMemoryCopy(pos, pos + len, (end - pos - len) * 2); \
*(end - len) = 0; \
end -= len; \
} \

View File

@@ -324,10 +324,12 @@ void Engine::OnUpdate()
// Call event
Update();
UpdateGraph->Execute();
// Update services
EngineService::OnUpdate();
// Run async
UpdateGraph->Execute();
}
void Engine::OnLateUpdate()

View File

@@ -221,9 +221,7 @@ Asset::LoadResult GameplayGlobals::load()
// Get data
const auto chunk = GetChunk(0);
if (!chunk || !chunk->IsLoaded())
{
return LoadResult::MissingDataChunk;
}
MemoryReadStream stream(chunk->Get(), chunk->Size());
// Load all variables
@@ -234,15 +232,16 @@ Asset::LoadResult GameplayGlobals::load()
for (int32 i = 0; i < count; i++)
{
stream.ReadString(&name, 71);
if (name.IsEmpty())
{
LOG(Warning, "Empty variable name");
return LoadResult::InvalidData;
}
auto& e = Variables[name];
stream.ReadVariant(&e.DefaultValue);
e.Value = e.DefaultValue;
}
if (stream.HasError())
{
// Failed to load data
Variables.Clear();
return LoadResult::InvalidData;
}
return LoadResult::Ok;
}

View File

@@ -115,7 +115,7 @@ public:
// Rollback state and cancel
_context = nullptr;
_state = TaskState::Queued;
SetState(TaskState::Queued);
Cancel();
}
@@ -148,8 +148,7 @@ protected:
ASSERT(_context != nullptr);
_context->OnCancelSync(this);
_context = nullptr;
_state = TaskState::Canceled;
SetState(TaskState::Canceled);
}
else
{

View File

@@ -9,9 +9,8 @@
void GPUTask::Execute(GPUTasksContext* context)
{
// Begin
ASSERT(IsQueued() && _context == nullptr);
_state = TaskState::Running;
SetState(TaskState::Running);
// Perform an operation
const auto result = run(context);
@@ -19,7 +18,7 @@ void GPUTask::Execute(GPUTasksContext* context)
// Process result
if (IsCancelRequested())
{
_state = TaskState::Canceled;
SetState(TaskState::Canceled);
}
else if (result != Result::Ok)
{

View File

@@ -941,7 +941,19 @@ bool ModelData::Pack2AnimationHeader(WriteStream* stream, int32 animIndex) const
}
// Animation events
stream->WriteInt32(0);
stream->WriteInt32(anim.Events.Count());
for (auto& e : anim.Events)
{
stream->WriteString(e.First, 172);
stream->WriteInt32(e.Second.GetKeyframes().Count());
for (const auto& k : e.Second.GetKeyframes())
{
stream->WriteFloat(k.Time);
stream->WriteFloat(k.Value.Duration);
stream->WriteStringAnsi(k.Value.TypeName, 17);
stream->WriteJson(k.Value.JsonData);
}
}
// Nested animations
stream->WriteInt32(0);

View File

@@ -13,4 +13,12 @@ namespace FlaxEngine
Bit = bit;
}
}
public partial struct AntiAliasingSettings
{
/// <summary>
/// Whether or not to show the TAA settings.
/// </summary>
public bool ShowTAASettings => (Mode == AntialiasingMode.TemporalAntialiasing);
}
}

View File

@@ -1888,25 +1888,25 @@ API_STRUCT() struct FLAXENGINE_API AntiAliasingSettings : ISerializable
/// <summary>
/// The diameter (in texels) inside which jitter samples are spread. Smaller values result in crisper but more aliased output, while larger values result in more stable but blurrier output.
/// </summary>
API_FIELD(Attributes="Limit(0.1f, 1f, 0.001f), EditorOrder(1), PostProcessSetting((int)AntiAliasingSettingsOverride.TAA_JitterSpread), EditorDisplay(null, \"TAA Jitter Spread\")")
API_FIELD(Attributes="Limit(0.1f, 1f, 0.001f), EditorOrder(1), PostProcessSetting((int)AntiAliasingSettingsOverride.TAA_JitterSpread), EditorDisplay(null, \"TAA Jitter Spread\"), VisibleIf(nameof(ShowTAASettings))")
float TAA_JitterSpread = 1.0f;
/// <summary>
/// Controls the amount of sharpening applied to the color buffer. TAA can induce a slight loss of details in high frequency regions. Sharpening alleviates this issue. High values may introduce dark-border artifacts.
/// </summary>
API_FIELD(Attributes="Limit(0, 3f, 0.001f), EditorOrder(2), PostProcessSetting((int)AntiAliasingSettingsOverride.TAA_Sharpness), EditorDisplay(null, \"TAA Sharpness\")")
API_FIELD(Attributes="Limit(0, 3f, 0.001f), EditorOrder(2), PostProcessSetting((int)AntiAliasingSettingsOverride.TAA_Sharpness), EditorDisplay(null, \"TAA Sharpness\"), VisibleIf(nameof(ShowTAASettings))")
float TAA_Sharpness = 0.1f;
/// <summary>
/// The blend coefficient for stationary fragments. Controls the percentage of history samples blended into the final color for fragments with minimal active motion.
/// </summary>
API_FIELD(Attributes="Limit(0, 0.99f, 0.001f), EditorOrder(3), PostProcessSetting((int)AntiAliasingSettingsOverride.TAA_StationaryBlending), EditorDisplay(null, \"TAA Stationary Blending\")")
API_FIELD(Attributes="Limit(0, 0.99f, 0.001f), EditorOrder(3), PostProcessSetting((int)AntiAliasingSettingsOverride.TAA_StationaryBlending), EditorDisplay(null, \"TAA Stationary Blending\"), VisibleIf(nameof(ShowTAASettings))")
float TAA_StationaryBlending = 0.95f;
/// <summary>
/// The blending coefficient for moving fragments. Controls the percentage of history samples blended into the final color for fragments with significant active motion.
/// </summary>
API_FIELD(Attributes="Limit(0, 0.99f, 0.001f), EditorOrder(4), PostProcessSetting((int)AntiAliasingSettingsOverride.TAA_MotionBlending), EditorDisplay(null, \"TAA Motion Blending\")")
API_FIELD(Attributes="Limit(0, 0.99f, 0.001f), EditorOrder(4), PostProcessSetting((int)AntiAliasingSettingsOverride.TAA_MotionBlending), EditorDisplay(null, \"TAA Motion Blending\"), VisibleIf(nameof(ShowTAASettings))")
float TAA_MotionBlending = 0.85f;
public:

View File

@@ -10,6 +10,7 @@
#include "GPUTimerQueryVulkan.h"
#endif
#include "DescriptorSetVulkan.h"
#include "Engine/Engine/Engine.h"
#include "Engine/Profiler/ProfilerCPU.h"
void CmdBufferVulkan::AddWaitSemaphore(VkPipelineStageFlags waitFlags, SemaphoreVulkan* waitSemaphore)
@@ -32,7 +33,7 @@ void CmdBufferVulkan::Begin()
// Acquire a descriptor pool set on
if (_descriptorPoolSetContainer == nullptr)
{
_descriptorPoolSetContainer = &_device->DescriptorPoolsManager->AcquirePoolSetContainer();
_descriptorPoolSetContainer = _device->DescriptorPoolsManager->AcquirePoolSetContainer();
}
_state = State::IsInsideBegin;
@@ -138,7 +139,7 @@ void CmdBufferVulkan::RefreshFenceStatus()
if (_descriptorPoolSetContainer)
{
_device->DescriptorPoolsManager->ReleasePoolSet(*_descriptorPoolSetContainer);
_descriptorPoolSetContainer->LastFrameUsed = Engine::FrameCount;
_descriptorPoolSetContainer = nullptr;
}
}
@@ -279,6 +280,7 @@ void CmdBufferManagerVulkan::WaitForCmdBuffer(CmdBufferVulkan* cmdBuffer, float
void CmdBufferManagerVulkan::PrepareForNewActiveCommandBuffer()
{
PROFILE_CPU();
ASSERT_LOW_LAYER(_activeCmdBuffer == nullptr)
for (int32 i = 0; i < _pool._cmdBuffers.Count(); i++)
{
auto cmdBuffer = _pool._cmdBuffers.Get()[i];
@@ -286,8 +288,7 @@ void CmdBufferManagerVulkan::PrepareForNewActiveCommandBuffer()
if (cmdBuffer->GetState() == CmdBufferVulkan::State::ReadyForBegin)
{
_activeCmdBuffer = cmdBuffer;
_activeCmdBuffer->Begin();
return;
break;
}
else
{
@@ -295,8 +296,12 @@ void CmdBufferManagerVulkan::PrepareForNewActiveCommandBuffer()
}
}
// Always begin fresh command buffer for rendering
_activeCmdBuffer = _pool.Create();
if (_activeCmdBuffer == nullptr)
{
// Always begin fresh command buffer for rendering
_activeCmdBuffer = _pool.Create();
}
_activeCmdBuffer->Begin();
#if VULKAN_USE_QUERIES

View File

@@ -247,8 +247,7 @@ void TypedDescriptorPoolSetVulkan::Reset()
DescriptorPoolSetContainerVulkan::DescriptorPoolSetContainerVulkan(GPUDeviceVulkan* device)
: _device(device)
, _lastFrameUsed(Engine::FrameCount)
, _used(true)
, LastFrameUsed(Engine::FrameCount)
{
}
@@ -278,12 +277,6 @@ void DescriptorPoolSetContainerVulkan::Reset()
}
}
void DescriptorPoolSetContainerVulkan::SetUsed(bool used)
{
_used = used;
_lastFrameUsed = used ? Engine::FrameCount : _lastFrameUsed;
}
DescriptorPoolsManagerVulkan::DescriptorPoolsManagerVulkan(GPUDeviceVulkan* device)
: _device(device)
{
@@ -294,26 +287,21 @@ DescriptorPoolsManagerVulkan::~DescriptorPoolsManagerVulkan()
_poolSets.ClearDelete();
}
DescriptorPoolSetContainerVulkan& DescriptorPoolsManagerVulkan::AcquirePoolSetContainer()
DescriptorPoolSetContainerVulkan* DescriptorPoolsManagerVulkan::AcquirePoolSetContainer()
{
ScopeLock lock(_locker);
for (auto* poolSet : _poolSets)
{
if (poolSet->IsUnused())
if (poolSet->Refs == 0)
{
poolSet->SetUsed(true);
poolSet->LastFrameUsed = Engine::FrameCount;
poolSet->Reset();
return *poolSet;
return poolSet;
}
}
const auto poolSet = New<DescriptorPoolSetContainerVulkan>(_device);
_poolSets.Add(poolSet);
return *poolSet;
}
void DescriptorPoolsManagerVulkan::ReleasePoolSet(DescriptorPoolSetContainerVulkan& poolSet)
{
poolSet.SetUsed(false);
return poolSet;
}
void DescriptorPoolsManagerVulkan::GC()
@@ -322,7 +310,7 @@ void DescriptorPoolsManagerVulkan::GC()
for (int32 i = _poolSets.Count() - 1; i >= 0; i--)
{
const auto poolSet = _poolSets[i];
if (poolSet->IsUnused() && Engine::FrameCount - poolSet->GetLastFrameUsed() > VULKAN_RESOURCE_DELETE_SAFE_FRAMES_COUNT)
if (poolSet->Refs == 0 && Engine::FrameCount - poolSet->LastFrameUsed > VULKAN_RESOURCE_DELETE_SAFE_FRAMES_COUNT)
{
_poolSets.RemoveAt(i);
Delete(poolSet);

View File

@@ -212,8 +212,6 @@ class DescriptorPoolSetContainerVulkan
private:
GPUDeviceVulkan* _device;
Dictionary<uint32, TypedDescriptorPoolSetVulkan*> _typedDescriptorPools;
uint64 _lastFrameUsed;
bool _used;
public:
DescriptorPoolSetContainerVulkan(GPUDeviceVulkan* device);
@@ -222,17 +220,9 @@ public:
public:
TypedDescriptorPoolSetVulkan* AcquireTypedPoolSet(const DescriptorSetLayoutVulkan& layout);
void Reset();
void SetUsed(bool used);
bool IsUnused() const
{
return !_used;
}
uint64 GetLastFrameUsed() const
{
return _lastFrameUsed;
}
mutable uint64 Refs = 0;
mutable uint64 LastFrameUsed;
};
class DescriptorPoolsManagerVulkan
@@ -246,8 +236,7 @@ public:
DescriptorPoolsManagerVulkan(GPUDeviceVulkan* device);
~DescriptorPoolsManagerVulkan();
DescriptorPoolSetContainerVulkan& AcquirePoolSetContainer();
void ReleasePoolSet(DescriptorPoolSetContainerVulkan& poolSet);
DescriptorPoolSetContainerVulkan* AcquirePoolSetContainer();
void GC();
};

View File

@@ -112,7 +112,11 @@ ComputePipelineStateVulkan::ComputePipelineStateVulkan(GPUDeviceVulkan* device,
ComputePipelineStateVulkan::~ComputePipelineStateVulkan()
{
DSWriteContainer.Release();
CurrentTypedDescriptorPoolSet = nullptr;
if (CurrentTypedDescriptorPoolSet)
{
CurrentTypedDescriptorPoolSet->GetOwner()->Refs--;
CurrentTypedDescriptorPoolSet = nullptr;
}
DescriptorSetsLayout = nullptr;
DescriptorSetHandles.Resize(0);
DynamicOffsets.Resize(0);
@@ -206,7 +210,11 @@ VkPipeline GPUPipelineStateVulkan::GetState(RenderPassVulkan* renderPass)
void GPUPipelineStateVulkan::OnReleaseGPU()
{
DSWriteContainer.Release();
CurrentTypedDescriptorPoolSet = nullptr;
if (CurrentTypedDescriptorPoolSet)
{
CurrentTypedDescriptorPoolSet->GetOwner()->Refs--;
CurrentTypedDescriptorPoolSet = nullptr;
}
DescriptorSetsLayout = nullptr;
DescriptorSetHandles.Resize(0);
DynamicOffsets.Resize(0);

View File

@@ -41,17 +41,17 @@ public:
DescriptorPoolSetContainerVulkan* cmdBufferPoolSet = cmdBuffer->GetDescriptorPoolSet();
if (CurrentTypedDescriptorPoolSet == nullptr || CurrentTypedDescriptorPoolSet->GetOwner() != cmdBufferPoolSet)
{
ASSERT(cmdBufferPoolSet);
if (CurrentTypedDescriptorPoolSet)
CurrentTypedDescriptorPoolSet->GetOwner()->Refs--;
CurrentTypedDescriptorPoolSet = cmdBufferPoolSet->AcquireTypedPoolSet(*DescriptorSetsLayout);
CurrentTypedDescriptorPoolSet->GetOwner()->Refs++;
return true;
}
return false;
}
inline bool AllocateDescriptorSets()
{
ASSERT(CurrentTypedDescriptorPoolSet);
return CurrentTypedDescriptorPoolSet->AllocateDescriptorSets(*DescriptorSetsLayout, DescriptorSetHandles.Get());
}
@@ -165,7 +165,10 @@ public:
DescriptorPoolSetContainerVulkan* cmdBufferPoolSet = cmdBuffer->GetDescriptorPoolSet();
if (CurrentTypedDescriptorPoolSet == nullptr || CurrentTypedDescriptorPoolSet->GetOwner() != cmdBufferPoolSet)
{
if (CurrentTypedDescriptorPoolSet)
CurrentTypedDescriptorPoolSet->GetOwner()->Refs--;
CurrentTypedDescriptorPoolSet = cmdBufferPoolSet->AcquireTypedPoolSet(*DescriptorSetsLayout);
CurrentTypedDescriptorPoolSet->GetOwner()->Refs++;
return true;
}
return false;

View File

@@ -7,6 +7,8 @@
#include "Engine/Animations/Animations.h"
#include "Engine/Engine/Engine.h"
#if USE_EDITOR
#include "Engine/Core/Math/OrientedBoundingBox.h"
#include "Engine/Core/Math/Matrix3x3.h"
#include "Editor/Editor.h"
#endif
#include "Engine/Graphics/GPUContext.h"
@@ -1018,6 +1020,45 @@ void AnimatedModel::OnDebugDrawSelected()
ModelInstanceActor::OnDebugDrawSelected();
}
void AnimatedModel::OnDebugDraw()
{
if (ShowDebugDrawSkeleton && SkinnedModel && AnimationGraph)
{
if (GraphInstance.NodesPose.IsEmpty())
PreInitSkinningData();
Matrix world;
GetLocalToWorldMatrix(world);
// Draw bounding box at the node locations
const float boxSize = Math::Min(1.0f, (float)_sphere.Radius / 100.0f);
OrientedBoundingBox localBox(Vector3(-boxSize), Vector3(boxSize));
for (int32 nodeIndex = 0; nodeIndex < GraphInstance.NodesPose.Count(); nodeIndex++)
{
Matrix transform = GraphInstance.NodesPose[nodeIndex] * world;
Float3 scale, translation;
Matrix3x3 rotation;
transform.Decompose(scale, rotation, translation);
transform = Matrix::Invert(Matrix::Scaling(scale)) * transform;
OrientedBoundingBox box = localBox * transform;
DEBUG_DRAW_WIRE_BOX(box, Color::Green, 0, false);
}
// Nodes connections
for (int32 nodeIndex = 0; nodeIndex < SkinnedModel->Skeleton.Nodes.Count(); nodeIndex++)
{
int32 parentIndex = SkinnedModel->Skeleton.Nodes[nodeIndex].ParentIndex;
if (parentIndex != -1)
{
Float3 parentPos = (GraphInstance.NodesPose[parentIndex] * world).GetTranslation();
Float3 bonePos = (GraphInstance.NodesPose[nodeIndex] * world).GetTranslation();
DEBUG_DRAW_LINE(parentPos, bonePos, Color::Green, 0, false);
}
}
}
ModelInstanceActor::OnDebugDraw();
}
BoundingBox AnimatedModel::GetEditorBox() const
{
if (SkinnedModel)

View File

@@ -168,6 +168,13 @@ public:
API_FIELD(Attributes="EditorOrder(120), DefaultValue(null), EditorDisplay(\"Skinned Model\")")
ScriptingObjectReference<Actor> RootMotionTarget;
#if USE_EDITOR
/// <summary>
/// If checked, the skeleton pose will be shawn during debug shapes drawing.
/// </summary>
API_FIELD(Attributes="EditorOrder(200), EditorDisplay(\"Skinned Model\")") bool ShowDebugDrawSkeleton = false;
#endif
public:
/// <summary>
/// The graph instance data container. For dynamic usage only at runtime, not serialized.
@@ -416,6 +423,7 @@ public:
void Draw(RenderContextBatch& renderContextBatch) override;
#if USE_EDITOR
void OnDebugDrawSelected() override;
void OnDebugDraw() override;
BoundingBox GetEditorBox() const override;
#endif
bool IntersectsItself(const Ray& ray, Real& distance, Vector3& normal) override;

View File

@@ -105,10 +105,8 @@ void NavMesh::OnDataAssetLoaded()
if (Data.Tiles.HasItems())
return;
const bool isEnabled = IsDuringPlay() && IsActiveInHierarchy();
// Remove added tiles
if (isEnabled)
if (_navMeshActive)
{
RemoveTiles();
}
@@ -120,7 +118,7 @@ void NavMesh::OnDataAssetLoaded()
IsDataDirty = false;
// Add loaded tiles
if (isEnabled)
if (_navMeshActive)
{
AddTiles();
}
@@ -156,15 +154,36 @@ void NavMesh::OnEnable()
// Base
Actor::OnEnable();
GetScene()->Navigation.Meshes.Add(this);
AddTiles();
if (!_navMeshActive)
{
GetScene()->Navigation.Meshes.Add(this);
AddTiles();
_navMeshActive = true;
}
}
void NavMesh::OnDisable()
{
RemoveTiles();
GetScene()->Navigation.Meshes.Remove(this);
if (_navMeshActive)
{
RemoveTiles();
GetScene()->Navigation.Meshes.Remove(this);
_navMeshActive = false;
}
// Base
Actor::OnDisable();
}
void NavMesh::Initialize()
{
// Base
Actor::Initialize();
if (!_navMeshActive && IsActiveInHierarchy())
{
GetScene()->Navigation.Meshes.Add(this);
AddTiles();
_navMeshActive = true;
}
}

View File

@@ -68,6 +68,9 @@ private:
void RemoveTiles();
void OnDataAssetLoaded();
private:
bool _navMeshActive = false;
public:
// [Actor]
void Serialize(SerializeStream& stream, const void* otherObj) override;
@@ -77,4 +80,5 @@ protected:
// [Actor]
void OnEnable() override;
void OnDisable() override;
void Initialize() override;
};

View File

@@ -55,13 +55,13 @@ public:
{
return __sync_fetch_and_add(dst, value);
}
FORCE_INLINE static int32 AtomicRead(int32 volatile* dst)
FORCE_INLINE static int32 AtomicRead(int32 const volatile* dst)
{
int32 result;
__atomic_load(dst, &result, __ATOMIC_RELAXED);
return result;
}
FORCE_INLINE static int64 AtomicRead(int64 volatile* dst)
FORCE_INLINE static int64 AtomicRead(int64 const volatile* dst)
{
int64 result;
__atomic_load(dst, &result, __ATOMIC_RELAXED);

View File

@@ -45,21 +45,21 @@ public:
{
return __sync_fetch_and_add(dst, value);
}
FORCE_INLINE static int32 AtomicRead(int32 volatile* dst)
FORCE_INLINE static int32 AtomicRead(int32 const volatile* dst)
{
return __atomic_load_n(dst, __ATOMIC_RELAXED);
}
FORCE_INLINE static int64 AtomicRead(int64 volatile* dst)
FORCE_INLINE static int64 AtomicRead(int64 const volatile* dst)
{
return __atomic_load_n(dst, __ATOMIC_RELAXED);
}
FORCE_INLINE static void AtomicStore(int32 volatile* dst, int32 value)
{
__atomic_store(dst, &value, __ATOMIC_SEQ_CST);
__atomic_store_n((volatile int32*)dst, value, __ATOMIC_RELAXED);
}
FORCE_INLINE static void AtomicStore(int64 volatile* dst, int64 value)
{
__atomic_store(dst, &value, __ATOMIC_SEQ_CST);
__atomic_store_n((volatile int64*)dst, value, __ATOMIC_RELAXED);
}
FORCE_INLINE static void Prefetch(void const* ptr)
{

View File

@@ -270,14 +270,14 @@ public:
/// </summary>
/// <param name="dst">A pointer to the destination value.</param>
/// <returns>The function returns the value of the destination parameter.</returns>
static int32 AtomicRead(int32 volatile* dst) = delete;
static int32 AtomicRead(int32 const volatile* dst) = delete;
/// <summary>
/// Performs an atomic 64-bit variable read operation on the specified values.
/// </summary>
/// <param name="dst">A pointer to the destination value.</param>
/// <returns>The function returns the value of the destination parameter.</returns>
static int64 AtomicRead(int64 volatile* dst) = delete;
static int64 AtomicRead(int64 const volatile* dst) = delete;
/// <summary>
/// Sets a 32-bit variable to the specified value as an atomic operation.

View File

@@ -69,13 +69,13 @@ public:
{
return __sync_fetch_and_add(dst, value);
}
FORCE_INLINE static int32 AtomicRead(int32 volatile* dst)
FORCE_INLINE static int32 AtomicRead(int32 const volatile* dst)
{
int32 result;
__atomic_load(dst, &result, __ATOMIC_SEQ_CST);
return result;
}
FORCE_INLINE static int64 AtomicRead(int64 volatile* dst)
FORCE_INLINE static int64 AtomicRead(int64 const volatile* dst)
{
int64 result;
__atomic_load(dst, &result, __ATOMIC_SEQ_CST);

View File

@@ -57,7 +57,7 @@ public:
/// <summary>
/// Locks the critical section.
/// </summary>
void Lock() const
NO_SANITIZE_THREAD void Lock() const
{
pthread_mutex_lock(_mutexPtr);
#if BUILD_DEBUG
@@ -69,7 +69,7 @@ public:
/// Attempts to enter a critical section without blocking. If the call is successful, the calling thread takes ownership of the critical section.
/// </summary>
/// <returns>True if calling thread took ownership of the critical section.</returns>
bool TryLock() const
NO_SANITIZE_THREAD bool TryLock() const
{
return pthread_mutex_trylock(_mutexPtr) == 0;
}
@@ -77,7 +77,7 @@ public:
/// <summary>
/// Releases the lock on the critical section.
/// </summary>
void Unlock() const
NO_SANITIZE_THREAD void Unlock() const
{
#if BUILD_DEBUG
((UnixCriticalSection*)this)->_owningThreadId = 0;

View File

@@ -63,13 +63,13 @@ public:
return _interlockedexchangeadd64(dst, value);
#endif
}
static int32 AtomicRead(int32 volatile* dst)
static int32 AtomicRead(int32 const volatile* dst)
{
return (int32)_InterlockedCompareExchange((long volatile*)dst, 0, 0);
}
static int64 AtomicRead(int64 volatile* dst)
static int64 AtomicRead(int64 const volatile* dst)
{
return _InterlockedCompareExchange64(dst, 0, 0);
return _InterlockedCompareExchange64((int64 volatile*)dst, 0, 0);
}
static void AtomicStore(int32 volatile* dst, int32 value)
{

View File

@@ -27,6 +27,7 @@ namespace
// Cached data for the draw calls sorting
Array<uint64> SortingKeys[2];
Array<int32> SortingIndices;
Array<DrawBatch> SortingBatches;
Array<RenderList*> FreeRenderList;
struct MemPoolEntry
@@ -594,12 +595,13 @@ namespace
}
}
void RenderList::SortDrawCalls(const RenderContext& renderContext, bool reverseDistance, DrawCallsList& list, const RenderListBuffer<DrawCall>& drawCalls, DrawPass pass)
void RenderList::SortDrawCalls(const RenderContext& renderContext, bool reverseDistance, DrawCallsList& list, const RenderListBuffer<DrawCall>& drawCalls, DrawPass pass, bool stable)
{
PROFILE_CPU();
const auto* drawCallsData = drawCalls.Get();
const auto* listData = list.Indices.Get();
const int32 listSize = list.Indices.Count();
ZoneValue(listSize);
// Peek shared memory
#define PREPARE_CACHE(list) (list).Clear(); (list).Resize(listSize)
@@ -656,6 +658,7 @@ void RenderList::SortDrawCalls(const RenderContext& renderContext, bool reverseD
}
DrawBatch batch;
static_assert(sizeof(DrawBatch) == sizeof(uint64) * 2, "Fix the size of draw batch to optimize memory access.");
batch.SortKey = sortedKeys[i];
batch.StartIndex = i;
batch.BatchSize = batchSize;
@@ -665,8 +668,12 @@ void RenderList::SortDrawCalls(const RenderContext& renderContext, bool reverseD
i += batchSize;
}
// Sort draw calls batches by depth
Sorting::QuickSort(list.Batches);
// When using depth buffer draw calls are already almost ideally sorted by Radix Sort but transparency needs more stability to prevent flickering
if (stable)
{
// Sort draw calls batches by depth
Sorting::MergeSort(list.Batches, &SortingBatches);
}
}
FORCE_INLINE bool CanUseInstancing(DrawPass pass)

View File

@@ -217,17 +217,17 @@ struct DrawBatch
/// <summary>
/// The first draw call index.
/// </summary>
int32 StartIndex;
uint16 StartIndex;
/// <summary>
/// A number of draw calls to be submitted at once.
/// </summary>
int32 BatchSize;
uint16 BatchSize;
/// <summary>
/// The total amount of instances (sum from all draw calls in this batch).
/// </summary>
int32 InstanceCount;
uint32 InstanceCount;
bool operator<(const DrawBatch& other) const
{
@@ -525,7 +525,8 @@ public:
/// <param name="pass">The draw pass (optional).</param>
API_FUNCTION() FORCE_INLINE void SortDrawCalls(API_PARAM(Ref) const RenderContext& renderContext, bool reverseDistance, DrawCallsListType listType, DrawPass pass = DrawPass::All)
{
SortDrawCalls(renderContext, reverseDistance, DrawCallsLists[(int32)listType], DrawCalls, pass);
const bool stable = listType == DrawCallsListType::Forward;
SortDrawCalls(renderContext, reverseDistance, DrawCallsLists[(int32)listType], DrawCalls, pass, stable);
}
/// <summary>
@@ -536,7 +537,8 @@ public:
/// <param name="list">The collected draw calls indices list.</param>
/// <param name="drawCalls">The collected draw calls list.</param>
/// <param name="pass">The draw pass (optional).</param>
void SortDrawCalls(const RenderContext& renderContext, bool reverseDistance, DrawCallsList& list, const RenderListBuffer<DrawCall>& drawCalls, DrawPass pass = DrawPass::All);
/// <param name="stable">If set to <c>true</c> draw batches will be additionally sorted to prevent any flickering, otherwise Depth Buffer will smooth out any non-stability in sorting.</param>
void SortDrawCalls(const RenderContext& renderContext, bool reverseDistance, DrawCallsList& list, const RenderListBuffer<DrawCall>& drawCalls, DrawPass pass = DrawPass::All, bool stable = false);
/// <summary>
/// Executes the collected draw calls.

View File

@@ -3,6 +3,7 @@
#include "BinaryModule.h"
#include "ScriptingObject.h"
#include "Engine/Core/Log.h"
#include "Engine/Core/Utilities.h"
#include "Engine/Threading/Threading.h"
#include "Engine/Profiler/ProfilerCPU.h"
#include "ManagedCLR/MAssembly.h"
@@ -436,6 +437,7 @@ void ScriptingType::SetupScriptVTable(ScriptingTypeHandle baseTypeHandle)
}
}
NO_SANITIZE_ADDRESS
void ScriptingType::SetupScriptObjectVTable(void* object, ScriptingTypeHandle baseTypeHandle, int32 wrapperIndex)
{
// Analyze vtable size
@@ -475,7 +477,7 @@ void ScriptingType::SetupScriptObjectVTable(void* object, ScriptingTypeHandle ba
// Duplicate vtable
Script.VTable = (void**)((byte*)Platform::Allocate(totalSize, 16) + prefixSize);
Platform::MemoryCopy((byte*)Script.VTable - prefixSize, (byte*)vtable - prefixSize, prefixSize + size);
Utilities::UnsafeMemoryCopy((byte*)Script.VTable - prefixSize, (byte*)vtable - prefixSize, prefixSize + size);
// Override vtable entries
if (interfacesCount)
@@ -508,7 +510,7 @@ void ScriptingType::SetupScriptObjectVTable(void* object, ScriptingTypeHandle ba
const int32 interfaceSize = interfaceCount * sizeof(void*);
// Duplicate interface vtable
Platform::MemoryCopy((byte*)Script.VTable + interfaceOffset, (byte*)vtableInterface - prefixSize, prefixSize + interfaceSize);
Utilities::UnsafeMemoryCopy((byte*)Script.VTable + interfaceOffset, (byte*)vtableInterface - prefixSize, prefixSize + interfaceSize);
// Override interface vtable entries
const auto scriptOffset = interfaces->ScriptVTableOffset;

View File

@@ -497,7 +497,9 @@ void ReadStream::Read(Variant& data)
break;
}
default:
CRASH;
_hasError = true;
LOG(Error, "Invalid Variant type. Corrupted data.");
break;
}
}
@@ -946,6 +948,13 @@ void WriteStream::WriteJson(ISerializable* obj, const void* otherObj)
WriteInt32(0);
}
void WriteStream::WriteJson(const StringAnsiView& json)
{
WriteInt32(FLAXENGINE_VERSION_BUILD);
WriteInt32((int32)json.Length());
WriteBytes((byte*)json.Get(), (int32)json.Length());
}
void WriteStream::WriteString(const StringView& data)
{
Write(data);

View File

@@ -233,6 +233,7 @@ public:
/// <param name="obj">The object to serialize.</param>
/// <param name="otherObj">The instance of the object to compare with and serialize only the modified properties. If null, then serialize all properties.</param>
void WriteJson(ISerializable* obj, const void* otherObj = nullptr);
void WriteJson(const StringAnsiView& json);
public:
// Writes String to the stream

View File

@@ -11,13 +11,13 @@
void Task::Start()
{
if (_state != TaskState::Created)
if (GetState() != TaskState::Created)
return;
OnStart();
// Change state
_state = TaskState::Queued;
SetState(TaskState::Queued);
// Add task to the execution queue
Enqueue();
@@ -110,7 +110,6 @@ Task* Task::ContinueWith(const Function<bool()>& action, Object* target)
Task* Task::StartNew(Task* task)
{
ASSERT(task);
task->Start();
return task;
}
@@ -137,11 +136,10 @@ Task* Task::StartNew(Function<bool()>::Signature& action, Object* target)
void Task::Execute()
{
// Begin
if (IsCanceled())
return;
ASSERT(IsQueued());
_state = TaskState::Running;
SetState(TaskState::Running);
// Perform an operation
bool failed = Run();
@@ -149,7 +147,7 @@ void Task::Execute()
// Process result
if (IsCancelRequested())
{
_state = TaskState::Canceled;
SetState(TaskState::Canceled);
}
else if (failed)
{
@@ -167,10 +165,8 @@ void Task::OnStart()
void Task::OnFinish()
{
ASSERT(IsRunning());
ASSERT(!IsCancelRequested());
_state = TaskState::Finished;
ASSERT(IsRunning() && !IsCancelRequested());
SetState(TaskState::Finished);
// Send event further
if (_continueWith)
@@ -181,7 +177,7 @@ void Task::OnFinish()
void Task::OnFail()
{
_state = TaskState::Failed;
SetState(TaskState::Failed);
// Send event further
if (_continueWith)
@@ -209,8 +205,7 @@ void Task::OnCancel()
const auto state = GetState();
if (state != TaskState::Finished && state != TaskState::Failed)
{
_state = TaskState::Canceled;
SetState(TaskState::Canceled);
OnEnd();
}
}

View File

@@ -49,7 +49,6 @@ class FLAXENGINE_API Task : public Object, public NonCopyable
//
protected:
/// <summary>
/// The cancel flag used to indicate that there is request to cancel task operation.
/// </summary>
@@ -65,14 +64,18 @@ protected:
/// </summary>
Task* _continueWith = nullptr;
public:
void SetState(TaskState state)
{
Platform::AtomicStore((int64 volatile*)&_state, (uint64)state);
}
public:
/// <summary>
/// Gets the task state.
/// </summary>
FORCE_INLINE TaskState GetState() const
{
return static_cast<TaskState>(Platform::AtomicRead((int64 volatile*)&_state));
return (TaskState)Platform::AtomicRead((int64 const volatile*)&_state);
}
/// <summary>
@@ -94,7 +97,6 @@ public:
}
public:
/// <summary>
/// Checks if operation failed.
/// </summary>
@@ -153,7 +155,6 @@ public:
}
public:
/// <summary>
/// Starts this task execution (and will continue with all children).
/// </summary>
@@ -199,7 +200,6 @@ public:
}
public:
/// <summary>
/// Continues that task execution with a given task (will call Start on given task after finishing that one).
/// </summary>
@@ -232,7 +232,6 @@ public:
Task* ContinueWith(const Function<bool()>& action, Object* target = nullptr);
public:
/// <summary>
/// Starts the new task.
/// </summary>
@@ -312,7 +311,6 @@ public:
}
protected:
/// <summary>
/// Executes this task.
/// It should be called by the task consumer (thread pool or other executor of this task type).
@@ -328,7 +326,6 @@ protected:
virtual bool Run() = 0;
protected:
virtual void Enqueue() = 0;
virtual void OnStart();
virtual void OnFinish();

View File

@@ -9,6 +9,7 @@
#include "Engine/Platform/FileSystem.h"
#include "Engine/Platform/File.h"
#include "Engine/Tools/TextureTool/TextureTool.h"
#include "Engine/Utilities/AnsiPathTempFile.h"
// Import Assimp library
// Source: https://github.com/assimp/assimp
@@ -157,7 +158,7 @@ struct AssimpImporterData
Array<AssimpBone> Bones;
Dictionary<int32, Array<int32>> MeshIndexToNodeIndex;
AssimpImporterData(const char* path, const ModelTool::Options& options)
AssimpImporterData(const StringView& path, const ModelTool::Options& options)
: Path(path)
, Options(options)
{
@@ -735,7 +736,7 @@ void ImportAnimation(int32 index, ModelData& data, AssimpImporterData& importerD
}
}
bool ModelTool::ImportDataAssimp(const char* path, ModelData& data, Options& options, String& errorMsg)
bool ModelTool::ImportDataAssimp(const String& path, ModelData& data, Options& options, String& errorMsg)
{
static bool AssimpInited = false;
if (!AssimpInited)
@@ -784,7 +785,10 @@ bool ModelTool::ImportDataAssimp(const char* path, ModelData& data, Options& opt
context.AssimpImporter.SetPropertyBool(AI_CONFIG_IMPORT_FBX_OPTIMIZE_EMPTY_ANIMATION_CURVES, true);
// Import file
context.Scene = context.AssimpImporter.ReadFile(path, flags);
{
AnsiPathTempFile tempFile(path);
context.Scene = context.AssimpImporter.ReadFile(tempFile.Path.Get(), flags);
}
if (context.Scene == nullptr)
{
LOG_STR(Warning, String(context.AssimpImporter.GetErrorString()));

View File

@@ -816,7 +816,7 @@ void BakeTransforms(FbxScene* scene)
scene->GetRootNode()->ConvertPivotAnimationRecursive(nullptr, FbxNode::eDestinationPivot, frameRate, false);
}
bool ModelTool::ImportDataAutodeskFbxSdk(const char* path, ImportedModelData& data, Options& options, String& errorMsg)
bool ModelTool::ImportDataAutodeskFbxSdk(const String& path, ImportedModelData& data, Options& options, String& errorMsg)
{
ScopeLock lock(FbxSdkManager::Locker);
@@ -836,7 +836,7 @@ bool ModelTool::ImportDataAutodeskFbxSdk(const char* path, ImportedModelData& da
auto ios = FbxSdkManager::Manager->GetIOSettings();
ios->SetBoolProp(IMP_FBX_MODEL, importMeshes);
ios->SetBoolProp(IMP_FBX_ANIMATION, importAnimations);
if (!importer->Initialize(path, -1, ios))
if (!importer->Initialize(StringAnsi(path), -1, ios))
{
errorMsg = String::Format(TEXT("Failed to initialize FBX importer. {0}"), String(importer->GetStatus().GetErrorString()));
return false;

View File

@@ -103,7 +103,7 @@ struct OpenFbxImporterData
Array<const ofbx::Material*> Materials;
Array<MaterialSlotEntry> ImportedMaterials;
OpenFbxImporterData(const char* path, const ModelTool::Options& options, ofbx::IScene* scene)
OpenFbxImporterData(const String& path, const ModelTool::Options& options, ofbx::IScene* scene)
: Scene(scene)
, ScenePtr(scene)
, Path(path)
@@ -1114,11 +1114,11 @@ static Float3 FbxVectorFromAxisAndSign(int axis, int sign)
return { 0.f, 0.f, 0.f };
}
bool ModelTool::ImportDataOpenFBX(const char* path, ModelData& data, Options& options, String& errorMsg)
bool ModelTool::ImportDataOpenFBX(const String& path, ModelData& data, Options& options, String& errorMsg)
{
// Import file
Array<byte> fileData;
if (File::ReadAllBytes(String(path), fileData))
if (File::ReadAllBytes(path, fileData))
{
errorMsg = TEXT("Cannot load file.");
return true;

View File

@@ -471,64 +471,37 @@ bool ModelTool::ImportData(const String& path, ModelData& data, Options& options
options.MergeMeshes = false; // Meshes merging doesn't make sense when we want to import each mesh individually
// TODO: maybe we could update meshes merger to collapse meshes within the same name if splitting is enabled?
// Validate path
// Note: Assimp/Autodesk supports only ANSI characters in imported file path
StringAnsi importPath;
String tmpPath;
if (path.IsANSI() == false)
{
// Use temporary file
LOG(Warning, "Model Tool doesn't support importing files from paths using non ASNI characters. Using temporary file.");
FileSystem::GetTempFilePath(tmpPath);
if (tmpPath.IsANSI() == false || FileSystem::CopyFile(tmpPath, path))
{
errorMsg = TEXT("Path with non ANSI characters is invalid.");
return true;
}
importPath = tmpPath.ToStringAnsi();
}
else
{
importPath = path.ToStringAnsi();
}
// Call importing backend
#if (USE_AUTODESK_FBX_SDK || USE_OPEN_FBX) && USE_ASSIMP
if (path.EndsWith(TEXT(".fbx"), StringSearchCase::IgnoreCase))
{
#if USE_AUTODESK_FBX_SDK
if (ImportDataAutodeskFbxSdk(importPath.Get(), data, options, errorMsg))
if (ImportDataAutodeskFbxSdk(path, data, options, errorMsg))
return true;
#elif USE_OPEN_FBX
if (ImportDataOpenFBX(importPath.Get(), data, options, errorMsg))
if (ImportDataOpenFBX(path, data, options, errorMsg))
return true;
#endif
}
else
{
if (ImportDataAssimp(importPath.Get(), data, options, errorMsg))
if (ImportDataAssimp(path, data, options, errorMsg))
return true;
}
#elif USE_ASSIMP
if (ImportDataAssimp(importPath.Get(), data, options, errorMsg))
if (ImportDataAssimp(path, data, options, errorMsg))
return true;
#elif USE_AUTODESK_FBX_SDK
if (ImportDataAutodeskFbxSdk(importPath.Get(), data, options, errorMsg))
if (ImportDataAutodeskFbxSdk(path, data, options, errorMsg))
return true;
#elif USE_OPEN_FBX
if (ImportDataOpenFBX(importPath.Get(), data, options, errorMsg))
if (ImportDataOpenFBX(path, data, options, errorMsg))
return true;
#else
LOG(Error, "Compiled without model importing backend.");
return true;
#endif
// Remove temporary file
if (tmpPath.HasChars() && FileSystem::FileExists(tmpPath))
{
FileSystem::DeleteFile(tmpPath);
}
// Remove namespace prefixes from the nodes names
{
for (auto& node : data.Nodes)
@@ -1248,7 +1221,7 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option
}
// Apply the import transformation
if (!importTransform.IsIdentity())
if (!importTransform.IsIdentity() && data.Nodes.HasItems())
{
if (options.Type == ModelType::SkinnedModel)
{

View File

@@ -386,13 +386,13 @@ public:
private:
static void CalculateBoneOffsetMatrix(const Array<SkeletonNode>& nodes, Matrix& offsetMatrix, int32 nodeIndex);
#if USE_ASSIMP
static bool ImportDataAssimp(const char* path, ModelData& data, Options& options, String& errorMsg);
static bool ImportDataAssimp(const String& path, ModelData& data, Options& options, String& errorMsg);
#endif
#if USE_AUTODESK_FBX_SDK
static bool ImportDataAutodeskFbxSdk(const char* path, ModelData& data, Options& options, String& errorMsg);
static bool ImportDataAutodeskFbxSdk(const String& path, ModelData& data, Options& options, String& errorMsg);
#endif
#if USE_OPEN_FBX
static bool ImportDataOpenFBX(const char* path, ModelData& data, Options& options, String& errorMsg);
static bool ImportDataOpenFBX(const String& path, ModelData& data, Options& options, String& errorMsg);
#endif
#endif
};

View File

@@ -21,6 +21,7 @@ public class TextureTool : EngineModule
bool useDirectXTex = false;
bool useStb = false;
bool useExr = options.Target.IsEditor;
switch (options.Platform.Target)
{
@@ -58,6 +59,10 @@ public class TextureTool : EngineModule
options.PrivateDependencies.Add("bc7enc16");
}
}
if (useExr)
{
options.PrivateDependencies.Add("tinyexr");
}
if (options.Target.IsEditor && astc.IsSupported(options))
{
// ASTC for mobile (iOS and Android)

View File

@@ -15,6 +15,7 @@
#if USE_EDITOR
#include "Engine/Graphics/GPUDevice.h"
#endif
#include "Engine/Utilities/AnsiPathTempFile.h"
// Import DirectXTex library
// Source: https://github.com/Microsoft/DirectXTex
@@ -24,6 +25,19 @@ DECLARE_HANDLE(HMONITOR);
#endif
#include <ThirdParty/DirectXTex/DirectXTex.h>
#if USE_EDITOR
// Import tinyexr library
// Source: https://github.com/syoyo/tinyexr
#define TINYEXR_IMPLEMENTATION
#define TINYEXR_USE_MINIZ 1
#define TINYEXR_USE_STB_ZLIB 0
#define TINYEXR_USE_THREAD 0
#define TINYEXR_USE_OPENMP 0
#undef min
#undef max
#include <ThirdParty/tinyexr/tinyexr.h>
#endif
namespace
{
FORCE_INLINE PixelFormat ToPixelFormat(const DXGI_FORMAT format)
@@ -36,7 +50,7 @@ namespace
return static_cast<DXGI_FORMAT>(format);
}
HRESULT Compress(const DirectX::Image* srcImages, size_t nimages, const DirectX::TexMetadata& metadata, DXGI_FORMAT format, DWORD compress, float threshold, DirectX::ScratchImage& cImages)
HRESULT Compress(const DirectX::Image* srcImages, size_t nimages, const DirectX::TexMetadata& metadata, DXGI_FORMAT format, DirectX::TEX_COMPRESS_FLAGS compress, float threshold, DirectX::ScratchImage& cImages)
{
#if USE_EDITOR
if ((format == DXGI_FORMAT_BC7_UNORM || format == DXGI_FORMAT_BC7_UNORM_SRGB || format == DXGI_FORMAT_BC6H_UF16 || format == DXGI_FORMAT_BC6H_SF16) &&
@@ -60,12 +74,12 @@ namespace
size_t _nimages;
const DirectX::TexMetadata& _metadata;
DXGI_FORMAT _format;
DWORD _compress;
DirectX::TEX_COMPRESS_FLAGS _compress;
DirectX::ScratchImage& _cImages;
public:
HRESULT CompressResult = E_FAIL;
GPUCompressTask(ConditionVariable& signal, const DirectX::Image* srcImages, size_t nimages, const DirectX::TexMetadata& metadata, DXGI_FORMAT format, DWORD compress, DirectX::ScratchImage& cImages)
GPUCompressTask(ConditionVariable& signal, const DirectX::Image* srcImages, size_t nimages, const DirectX::TexMetadata& metadata, DXGI_FORMAT format, DirectX::TEX_COMPRESS_FLAGS compress, DirectX::ScratchImage& cImages)
: GPUTask(Type::Custom)
, _signal(&signal)
, _srcImages(srcImages)
@@ -276,6 +290,46 @@ HRESULT LoadFromRAWFile(const StringView& path, DirectX::ScratchImage& image)
return image.InitializeFromImage(img);
}
HRESULT LoadFromEXRFile(const StringView& path, DirectX::ScratchImage& image)
{
#if USE_EDITOR
// Load exr file
AnsiPathTempFile tempFile(path);
float* pixels;
int width, height;
const char* err = nullptr;
int ret = LoadEXR(&pixels, &width, &height, tempFile.Path.Get(), &err);
if (ret != TINYEXR_SUCCESS)
{
if (err)
{
LOG_STR(Warning, String(err));
FreeEXRErrorMessage(err);
}
return S_FALSE;
}
// Setup image
DirectX::Image img;
img.format = DXGI_FORMAT_R32G32B32A32_FLOAT;
img.width = width;
img.height = height;
img.rowPitch = width * sizeof(Float4);
img.slicePitch = img.rowPitch * height;
// Link data
img.pixels = (uint8_t*)pixels;
// Init
HRESULT result = image.InitializeFromImage(img);
free(pixels);
return result;
#else
LOG(Warning, "EXR format is not supported.");
return S_FALSE;
#endif
}
bool TextureTool::ImportTextureDirectXTex(ImageType type, const StringView& path, TextureData& textureData, bool& hasAlpha)
{
// Load image data
@@ -302,6 +356,9 @@ bool TextureTool::ImportTextureDirectXTex(ImageType type, const StringView& path
case ImageType::RAW:
result = LoadFromRAWFile(path, image);
break;
case ImageType::EXR:
result = LoadFromEXRFile(path, image);
break;
default:
result = DXGI_ERROR_INVALID_CALL;
break;
@@ -518,6 +575,9 @@ bool TextureTool::ImportTextureDirectXTex(ImageType type, const StringView& path
case ImageType::RAW:
result = LoadFromRAWFile(path, image1);
break;
case ImageType::EXR:
result = LoadFromEXRFile(path, image1);
break;
case ImageType::Internal:
{
if (options.InternalLoad.IsBinded())
@@ -688,7 +748,7 @@ bool TextureTool::ImportTextureDirectXTex(ImageType type, const StringView& path
if (!keepAsIs && options.FlipY)
{
auto& tmpImg = GET_TMP_IMG();
DWORD flags = DirectX::TEX_FR_FLIP_VERTICAL;
DirectX::TEX_FR_FLAGS flags = DirectX::TEX_FR_FLIP_VERTICAL;
result = FlipRotate(currentImage->GetImages(), currentImage->GetImageCount(), currentImage->GetMetadata(), flags, tmpImg);
if (FAILED(result))
{
@@ -698,7 +758,7 @@ bool TextureTool::ImportTextureDirectXTex(ImageType type, const StringView& path
SET_CURRENT_IMG(tmpImg);
}
// Check if it invert green channel
// Check if invert green channel
if (!keepAsIs && options.InvertGreenChannel)
{
auto& timage = GET_TMP_IMG();

View File

@@ -767,6 +767,10 @@ bool TextureTool::GetImageType(const StringView& path, ImageType& type)
{
type = ImageType::RAW;
}
else if (extension == TEXT("exr"))
{
type = ImageType::EXR;
}
else
{
LOG(Warning, "Unknown file type.");

View File

@@ -252,6 +252,7 @@ private:
JPEG,
HDR,
RAW,
EXR,
Internal,
};

View File

@@ -10,6 +10,7 @@
#include "Engine/Graphics/RenderTools.h"
#include "Engine/Graphics/Textures/TextureData.h"
#include "Engine/Graphics/PixelFormatExtensions.h"
#include "Engine/Utilities/AnsiPathTempFile.h"
#include "Engine/Platform/File.h"
#define STBI_ASSERT(x) ASSERT(x)
@@ -48,6 +49,18 @@
// Compression libs for Editor
#include <ThirdParty/detex/detex.h>
#include <ThirdParty/bc7enc16/bc7enc16.h>
// Import tinyexr library
// Source: https://github.com/syoyo/tinyexr
#define TINYEXR_IMPLEMENTATION
#define TINYEXR_USE_MINIZ 1
#define TINYEXR_USE_STB_ZLIB 0
#define TINYEXR_USE_THREAD 0
#define TINYEXR_USE_OPENMP 0
#undef min
#undef max
#include <ThirdParty/tinyexr/tinyexr.h>
#endif
static void stbWrite(void* context, void* data, int size)
@@ -173,7 +186,7 @@ bool TextureTool::ExportTextureStb(ImageType type, const StringView& path, const
{
if (textureData.GetArraySize() != 1)
{
LOG(Warning, "Exporting texture arrays and cubemaps is not supported by stb library.");
LOG(Warning, "Exporting texture arrays and cubemaps is not supported.");
}
TextureData const* texture = &textureData;
@@ -189,7 +202,7 @@ bool TextureTool::ExportTextureStb(ImageType type, const StringView& path, const
const auto sampler = GetSampler(texture->Format);
if (sampler == nullptr)
{
LOG(Warning, "Texture data format {0} is not supported by stb library.", (int32)textureData.Format);
LOG(Warning, "Texture data format {0} is not supported.", (int32)textureData.Format);
return true;
}
const auto srcData = texture->GetData(0, 0);
@@ -272,16 +285,19 @@ bool TextureTool::ExportTextureStb(ImageType type, const StringView& path, const
break;
}
case ImageType::GIF:
LOG(Warning, "GIF format is not supported by stb library.");
LOG(Warning, "GIF format is not supported.");
break;
case ImageType::TIFF:
LOG(Warning, "GIF format is not supported by stb library.");
LOG(Warning, "GIF format is not supported.");
break;
case ImageType::DDS:
LOG(Warning, "DDS format is not supported by stb library.");
LOG(Warning, "DDS format is not supported.");
break;
case ImageType::RAW:
LOG(Warning, "RAW format is not supported by stb library.");
LOG(Warning, "RAW format is not supported.");
break;
case ImageType::EXR:
LOG(Warning, "EXR format is not supported.");
break;
default:
LOG(Warning, "Unknown format.");
@@ -383,11 +399,49 @@ bool TextureTool::ImportTextureStb(ImageType type, const StringView& path, Textu
break;
}
case ImageType::EXR:
{
#if USE_EDITOR
// Load exr file
AnsiPathTempFile tempFile(path);
float* pixels;
int width, height;
const char* err = nullptr;
int ret = LoadEXR(&pixels, &width, &height, tempFile.Path.Get(), &err);
if (ret != TINYEXR_SUCCESS)
{
if (err)
{
LOG_STR(Warning, String(err));
FreeEXRErrorMessage(err);
}
return true;
}
// Setup texture data
textureData.Width = width;
textureData.Height = height;
textureData.Depth = 1;
textureData.Format = PixelFormat::R32G32B32A32_Float;
textureData.Items.Resize(1);
textureData.Items[0].Mips.Resize(1);
auto& mip = textureData.Items[0].Mips[0];
mip.RowPitch = width * sizeof(Float4);
mip.DepthPitch = mip.RowPitch * height;
mip.Lines = height;
mip.Data.Copy((const byte*)pixels, mip.DepthPitch);
free(pixels);
#else
LOG(Warning, "EXR format is not supported.");
#endif
break;
}
case ImageType::DDS:
LOG(Warning, "DDS format is not supported by stb library.");
LOG(Warning, "DDS format is not supported.");
break;
case ImageType::TIFF:
LOG(Warning, "TIFF format is not supported by stb library.");
LOG(Warning, "TIFF format is not supported.");
break;
default:
LOG(Warning, "Unknown format.");

View File

@@ -290,7 +290,7 @@ namespace FlaxEngine.GUI
{
var leftEdge = selection.StartIndex <= textBlock.Range.StartIndex ? textBlock.Bounds.UpperLeft : font.GetCharPosition(_text, selection.StartIndex);
var rightEdge = selection.EndIndex >= textBlock.Range.EndIndex ? textBlock.Bounds.UpperRight : font.GetCharPosition(_text, selection.EndIndex);
float height = font.Height / DpiScale;
float height = font.Height;
float alpha = Mathf.Min(1.0f, Mathf.Cos(_animateTime * BackgroundSelectedFlashSpeed) * 0.5f + 1.3f);
alpha *= alpha;
Color selectionColor = Color.White * alpha;
@@ -340,7 +340,7 @@ namespace FlaxEngine.GUI
if (textBlock.Style.UnderlineBrush != null)
{
var underLineHeight = 2.0f;
var height = font.Height / DpiScale;
var height = font.Height;
var underlineRect = new Rectangle(textBlock.Bounds.Location.X, textBlock.Bounds.Location.Y + height - underLineHeight * 0.5f, textBlock.Bounds.Width, underLineHeight);
textBlock.Style.UnderlineBrush.Draw(underlineRect, textBlock.Style.Color);
}

View File

@@ -0,0 +1,37 @@
// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved.
#pragma once
#include "Engine/Core/Types/String.h"
#include "Engine/Platform/FileSystem.h"
// Small utility that uses temporary file to properly handle non-ANSI paths for 3rd party libs.
struct AnsiPathTempFile
{
StringAnsi Path;
String TempPath;
bool Temp;
AnsiPathTempFile(const String& path)
{
if (path.IsANSI() == false)
{
// Use temporary file
FileSystem::GetTempFilePath(TempPath);
if (TempPath.IsANSI() && !FileSystem::CopyFile(TempPath, path))
{
Path = TempPath.ToStringAnsi();
return;
}
TempPath.Clear();
}
Path = path.ToStringAnsi();
}
~AnsiPathTempFile()
{
// Cleanup temporary file after use
if (TempPath.HasChars())
FileSystem::DeleteFile(TempPath);
}
};

View File

@@ -47,6 +47,12 @@ namespace FlaxEngine.Utilities
/// True if this tag contained a leading or trailing forward slash.
/// </summary>
public bool IsSlash => IsLeadingSlash || IsEndingSlash;
/// <inheritdoc />
public override string ToString()
{
return Name;
}
};
/// <summary>
@@ -231,6 +237,9 @@ namespace FlaxEngine.Utilities
tag.Attributes[s] = value;
}
}
if (EOF)
return false;
}
// Skip over closing '>'
@@ -264,8 +273,13 @@ namespace FlaxEngine.Utilities
private string ParseAttributeName()
{
int start = _pos;
while (!EOF && char.IsLetterOrDigit(Peek()))
while (!EOF)
{
var c = Peek();
if (!char.IsLetterOrDigit(c) && c != '-')
break;
Move();
}
return _html.Substring(start, _pos - start);
}