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

This commit is contained in:
Wojtek Figat
2024-02-07 09:40:45 +01:00
64 changed files with 1445 additions and 350 deletions

View File

@@ -99,10 +99,7 @@ void BlendPoseBucketInit(AnimGraphInstanceData::Bucket& bucket)
void StateMachineBucketInit(AnimGraphInstanceData::Bucket& bucket)
{
bucket.StateMachine.LastUpdateFrame = 0;
bucket.StateMachine.CurrentState = nullptr;
bucket.StateMachine.ActiveTransition = nullptr;
bucket.StateMachine.TransitionPosition = 0.0f;
Platform::MemoryClear(&bucket.StateMachine, sizeof(bucket.StateMachine));
}
void SlotBucketInit(AnimGraphInstanceData::Bucket& bucket)

View File

@@ -39,6 +39,7 @@ void AnimGraphImpulse::SetNodeModelTransformation(SkeletonData& skeleton, int32
void AnimGraphInstanceData::Clear()
{
ClearState();
Slots.Clear();
Parameters.Resize(0);
}
@@ -55,7 +56,7 @@ void AnimGraphInstanceData::ClearState()
RootMotion = Transform::Identity;
State.Resize(0);
NodesPose.Resize(0);
Slots.Clear();
TraceEvents.Clear();
}
void AnimGraphInstanceData::Invalidate()
@@ -238,6 +239,7 @@ void AnimGraphExecutor::Update(AnimGraphInstanceData& data, float dt)
}
for (auto& e : data.ActiveEvents)
e.Hit = false;
data.TraceEvents.Clear();
// Init empty nodes data
context.EmptyNodes.RootMotion = Transform::Identity;

View File

@@ -129,6 +129,8 @@ public:
UseDefaultRule = 4,
InterruptionRuleRechecking = 8,
InterruptionInstant = 16,
InterruptionSourceState = 32,
InterruptionDestinationState = 64,
};
public:
@@ -200,6 +202,21 @@ struct FLAXENGINE_API AnimGraphSlot
bool Reset = false;
};
/// <summary>
/// The animation graph state container for a single node playback trace (eg. animation sample info or state transition). Can be used by Anim Graph debugging or custom scripting.
/// </summary>
API_STRUCT() struct FLAXENGINE_API AnimGraphTraceEvent
{
DECLARE_SCRIPTING_TYPE_MINIMAL(AnimGraphTraceEvent);
// Contextual asset used. For example, sampled animation.
API_FIELD() Asset* Asset = nullptr;
// Generic value contextual to playback type (eg. animation sample position).
API_FIELD() float Value = 0;
// Identifier of the node in the graph.
API_FIELD() uint32 NodeId = 0;
};
/// <summary>
/// The animation graph instance data storage. Required to update the animation graph.
/// </summary>
@@ -241,7 +258,10 @@ public:
uint64 LastUpdateFrame;
AnimGraphNode* CurrentState;
AnimGraphStateTransition* ActiveTransition;
AnimGraphStateTransition* BaseTransition;
AnimGraphNode* BaseTransitionState;
float TransitionPosition;
float BaseTransitionPosition;
};
struct SlotBucket
@@ -358,6 +378,12 @@ public:
/// </summary>
void InvokeAnimEvents();
public:
// Anim Graph logic tracing feature that allows to collect insights of animations sampling and skeleton poses operations.
bool EnableTracing = false;
// Trace events collected when using EnableTracing option.
Array<AnimGraphTraceEvent> TraceEvents;
private:
struct OutgoingEvent
{
@@ -837,7 +863,7 @@ public:
}
/// <summary>
/// Resets all the state bucket used by the given graph including sub-graphs (total). Can eb used to reset the animation state of the nested graph (including children).
/// Resets all the state bucket used by the given graph including sub-graphs (total). Can be used to reset the animation state of the nested graph (including children).
/// </summary>
void ResetBuckets(AnimGraphContext& context, AnimGraphBase* graph);
@@ -866,5 +892,7 @@ private:
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);
Variant Blend(AnimGraphNode* node, const Value& poseA, const Value& poseB, float alpha, AlphaBlendMode alphaMode);
Variant SampleState(AnimGraphNode* state);
void InitStateTransition(AnimGraphContext& context, AnimGraphInstanceData::StateMachineBucket& stateMachineBucket, AnimGraphStateTransition* transition = nullptr);
AnimGraphStateTransition* UpdateStateTransitions(AnimGraphContext& context, const AnimGraphNode::StateMachineData& stateMachineData, AnimGraphNode* state, AnimGraphNode* ignoreState = nullptr);
void UpdateStateTransitions(AnimGraphContext& context, const AnimGraphNode::StateMachineData& stateMachineData, AnimGraphInstanceData::StateMachineBucket& stateMachineBucket, const AnimGraphNode::StateBaseData& stateData);
};

View File

@@ -219,6 +219,16 @@ void AnimGraphExecutor::ProcessAnimation(AnimGraphImpulse* nodes, AnimGraphNode*
const float animPos = GetAnimSamplePos(length, anim, pos, speed);
const float animPrevPos = GetAnimSamplePos(length, anim, prevPos, speed);
// Add to trace
auto& context = Context.Get();
if (context.Data->EnableTracing)
{
auto& trace = context.Data->TraceEvents.AddOne();
trace.Asset = anim;
trace.Value = animPos;
trace.NodeId = node->ID;
}
// Evaluate nested animations
bool hasNested = false;
if (anim->NestedAnims.Count() != 0)
@@ -460,10 +470,12 @@ Variant AnimGraphExecutor::Blend(AnimGraphNode* node, const Value& poseA, const
{
ANIM_GRAPH_PROFILE_EVENT("Blend Pose");
if (isnan(alpha) || isinf(alpha))
alpha = 0;
alpha = Math::Saturate(alpha);
alpha = AlphaBlend::Process(alpha, alphaMode);
const auto nodes = node->GetNodes(this);
auto nodesA = static_cast<AnimGraphImpulse*>(poseA.AsPointer);
auto nodesB = static_cast<AnimGraphImpulse*>(poseB.AsPointer);
if (!ANIM_GRAPH_IS_VALID_PTR(poseA))
@@ -484,32 +496,40 @@ Variant AnimGraphExecutor::Blend(AnimGraphNode* node, const Value& poseA, const
Variant AnimGraphExecutor::SampleState(AnimGraphNode* state)
{
// Prepare
auto& data = state->Data.State;
if (data.Graph == nullptr || data.Graph->GetRootNode() == nullptr)
{
// Invalid state graph
return Value::Null;
}
ANIM_GRAPH_PROFILE_EVENT("Evaluate State");
// Evaluate state
auto rootNode = data.Graph->GetRootNode();
auto result = eatBox((Node*)rootNode, &rootNode->Boxes[0]);
return result;
return eatBox((Node*)rootNode, &rootNode->Boxes[0]);
}
void AnimGraphExecutor::UpdateStateTransitions(AnimGraphContext& context, const AnimGraphNode::StateMachineData& stateMachineData, AnimGraphInstanceData::StateMachineBucket& stateMachineBucket, const AnimGraphNode::StateBaseData& stateData)
void AnimGraphExecutor::InitStateTransition(AnimGraphContext& context, AnimGraphInstanceData::StateMachineBucket& stateMachineBucket, AnimGraphStateTransition* transition)
{
// Reset transiton
stateMachineBucket.ActiveTransition = transition;
stateMachineBucket.TransitionPosition = 0.0f;
// End base transition
if (stateMachineBucket.BaseTransition)
{
ResetBuckets(context, stateMachineBucket.BaseTransitionState->Data.State.Graph);
stateMachineBucket.BaseTransition = nullptr;
stateMachineBucket.BaseTransitionState = nullptr;
stateMachineBucket.BaseTransitionPosition = 0.0f;
}
}
AnimGraphStateTransition* AnimGraphExecutor::UpdateStateTransitions(AnimGraphContext& context, const AnimGraphNode::StateMachineData& stateMachineData, AnimGraphNode* state, AnimGraphNode* ignoreState)
{
int32 transitionIndex = 0;
const AnimGraphNode::StateBaseData& stateData = state->Data.State;
while (transitionIndex < ANIM_GRAPH_MAX_STATE_TRANSITIONS && stateData.Transitions[transitionIndex] != AnimGraphNode::StateData::InvalidTransitionIndex)
{
const uint16 idx = stateData.Transitions[transitionIndex];
ASSERT(idx < stateMachineData.Graph->StateTransitions.Count());
auto& transition = stateMachineData.Graph->StateTransitions[idx];
if (transition.Destination == stateMachineBucket.CurrentState)
if (transition.Destination == state || transition.Destination == ignoreState)
{
// Ignore transition to the current state
transitionIndex++;
@@ -517,7 +537,7 @@ void AnimGraphExecutor::UpdateStateTransitions(AnimGraphContext& context, const
}
// Evaluate source state transition data (position, length, etc.)
const Value sourceStatePtr = SampleState(stateMachineBucket.CurrentState);
const Value sourceStatePtr = SampleState(state);
auto& transitionData = context.TransitionData; // Note: this could support nested transitions but who uses state machine inside transition rule?
if (ANIM_GRAPH_IS_VALID_PTR(sourceStatePtr))
{
@@ -538,6 +558,7 @@ void AnimGraphExecutor::UpdateStateTransitions(AnimGraphContext& context, const
if (transition.RuleGraph && !useDefaultRule)
{
// Execute transition rule
ANIM_GRAPH_PROFILE_EVENT("Rule");
auto rootNode = transition.RuleGraph->GetRootNode();
ASSERT(rootNode);
if (!(bool)eatBox((Node*)rootNode, &rootNode->Boxes[0]))
@@ -560,10 +581,7 @@ void AnimGraphExecutor::UpdateStateTransitions(AnimGraphContext& context, const
canEnter = true;
if (canEnter)
{
// Start transition
stateMachineBucket.ActiveTransition = &transition;
stateMachineBucket.TransitionPosition = 0.0f;
break;
return &transition;
}
// Skip after Solo transition
@@ -573,6 +591,18 @@ void AnimGraphExecutor::UpdateStateTransitions(AnimGraphContext& context, const
transitionIndex++;
}
// No transition
return nullptr;
}
void AnimGraphExecutor::UpdateStateTransitions(AnimGraphContext& context, const AnimGraphNode::StateMachineData& stateMachineData, AnimGraphInstanceData::StateMachineBucket& stateMachineBucket, const AnimGraphNode::StateBaseData& stateData)
{
AnimGraphStateTransition* transition = UpdateStateTransitions(context, stateMachineData, stateMachineBucket.CurrentState);
if (transition)
{
InitStateTransition(context, stateMachineBucket, transition);
}
}
void ComputeMultiBlendLength(float& length, AnimGraphNode* node)
@@ -1101,6 +1131,17 @@ 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())
{
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;
}
// Only A or missing/invalid mask
if (Math::NearEqual(alpha, 0.0f, ANIM_GRAPH_BLEND_THRESHOLD) || mask == nullptr || mask->WaitForLoaded())
@@ -1501,10 +1542,9 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu
// Blend two animations
{
const float alpha = Math::Saturate(bucket.TransitionPosition / blendDuration);
const float alpha = bucket.TransitionPosition / blendDuration;
const auto valueA = tryGetValue(node->GetBox(FirstBlendPoseBoxIndex + bucket.PreviousBlendPoseIndex), Value::Null);
const auto valueB = tryGetValue(node->GetBox(FirstBlendPoseBoxIndex + poseIndex), Value::Null);
value = Blend(node, valueA, valueB, alpha, mode);
}
@@ -1610,22 +1650,21 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu
// Enter to the first state pointed by the Entry node (without transitions)
bucket.CurrentState = data.Graph->GetRootNode();
bucket.ActiveTransition = nullptr;
bucket.TransitionPosition = 0.0f;
InitStateTransition(context, bucket);
// Reset all state buckets pof the graphs and nodes included inside the state machine
// Reset all state buckets of the graphs and nodes included inside the state machine
ResetBuckets(context, data.Graph);
}
#define END_TRANSITION() \
ResetBuckets(context, bucket.CurrentState->Data.State.Graph); \
bucket.CurrentState = bucket.ActiveTransition->Destination; \
bucket.ActiveTransition = nullptr; \
bucket.TransitionPosition = 0.0f
InitStateTransition(context, bucket)
// Update the active transition
if (bucket.ActiveTransition)
{
bucket.TransitionPosition += context.DeltaTime;
ASSERT(bucket.CurrentState);
// Check for transition end
if (bucket.TransitionPosition >= bucket.ActiveTransition->BlendDuration)
@@ -1633,38 +1672,70 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu
END_TRANSITION();
}
// Check for transition interruption
else if (EnumHasAnyFlags(bucket.ActiveTransition->Flags, AnimGraphStateTransition::FlagTypes::InterruptionRuleRechecking))
else if (EnumHasAnyFlags(bucket.ActiveTransition->Flags, AnimGraphStateTransition::FlagTypes::InterruptionRuleRechecking) &&
EnumHasNoneFlags(bucket.ActiveTransition->Flags, AnimGraphStateTransition::FlagTypes::UseDefaultRule) &&
bucket.ActiveTransition->RuleGraph)
{
const bool useDefaultRule = EnumHasAnyFlags(bucket.ActiveTransition->Flags, AnimGraphStateTransition::FlagTypes::UseDefaultRule);
if (bucket.ActiveTransition->RuleGraph && !useDefaultRule)
// Execute transition rule
auto rootNode = bucket.ActiveTransition->RuleGraph->GetRootNode();
if (!(bool)eatBox((Node*)rootNode, &rootNode->Boxes[0]))
{
// Execute transition rule
auto rootNode = bucket.ActiveTransition->RuleGraph->GetRootNode();
if (!(bool)eatBox((Node*)rootNode, &rootNode->Boxes[0]))
bool cancelTransition = false;
if (EnumHasAnyFlags(bucket.ActiveTransition->Flags, AnimGraphStateTransition::FlagTypes::InterruptionInstant))
{
bool cancelTransition = false;
if (EnumHasAnyFlags(bucket.ActiveTransition->Flags, AnimGraphStateTransition::FlagTypes::InterruptionInstant))
cancelTransition = true;
}
else
{
// Blend back to the source state (remove currently applied delta and rewind transition)
bucket.TransitionPosition -= context.DeltaTime;
bucket.TransitionPosition -= context.DeltaTime;
if (bucket.TransitionPosition <= ZeroTolerance)
{
cancelTransition = true;
}
else
{
// Blend back to the source state (remove currently applied delta and rewind transition)
bucket.TransitionPosition -= context.DeltaTime;
bucket.TransitionPosition -= context.DeltaTime;
if (bucket.TransitionPosition <= ZeroTolerance)
{
cancelTransition = true;
}
}
if (cancelTransition)
{
// Go back to the source state
ResetBuckets(context, bucket.CurrentState->Data.State.Graph);
bucket.ActiveTransition = nullptr;
bucket.TransitionPosition = 0.0f;
}
}
if (cancelTransition)
{
// Go back to the source state
ResetBuckets(context, bucket.CurrentState->Data.State.Graph);
InitStateTransition(context, bucket);
}
}
}
if (bucket.ActiveTransition && !bucket.BaseTransition && EnumHasAnyFlags(bucket.ActiveTransition->Flags, AnimGraphStateTransition::FlagTypes::InterruptionSourceState))
{
// Try to interrupt with any other transition in the source state (except the current transition)
if (AnimGraphStateTransition* transition = UpdateStateTransitions(context, data, bucket.CurrentState, bucket.ActiveTransition->Destination))
{
// Change active transition to the interrupted one
if (EnumHasNoneFlags(bucket.ActiveTransition->Flags, AnimGraphStateTransition::FlagTypes::InterruptionInstant))
{
// Cache the current blending state to be used as a base when blending towards new destination state (seamless blending after interruption)
bucket.BaseTransition = bucket.ActiveTransition;
bucket.BaseTransitionState = bucket.CurrentState;
bucket.BaseTransitionPosition = bucket.TransitionPosition;
}
bucket.ActiveTransition = transition;
bucket.TransitionPosition = 0.0f;
}
}
if (bucket.ActiveTransition && !bucket.BaseTransition && EnumHasAnyFlags(bucket.ActiveTransition->Flags, AnimGraphStateTransition::FlagTypes::InterruptionDestinationState))
{
// Try to interrupt with any other transition in the destination state (except the transition back to the current state if exists)
if (AnimGraphStateTransition* transition = UpdateStateTransitions(context, data, bucket.ActiveTransition->Destination, bucket.CurrentState))
{
// Change active transition to the interrupted one
if (EnumHasNoneFlags(bucket.ActiveTransition->Flags, AnimGraphStateTransition::FlagTypes::InterruptionInstant))
{
// Cache the current blending state to be used as a base when blending towards new destination state (seamless blending after interruption)
bucket.BaseTransition = bucket.ActiveTransition;
bucket.BaseTransitionState = bucket.CurrentState;
bucket.BaseTransitionPosition = bucket.TransitionPosition;
}
bucket.CurrentState = bucket.ActiveTransition->Destination;
bucket.ActiveTransition = transition;
bucket.TransitionPosition = 0.0f;
}
}
}
@@ -1693,9 +1764,23 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu
}
}
// Sample the current state
const auto currentState = SampleState(bucket.CurrentState);
value = currentState;
if (bucket.BaseTransitionState)
{
// Sample the other state (eg. when blending from interrupted state to the another state from the old destination)
value = SampleState(bucket.BaseTransitionState);
if (bucket.BaseTransition)
{
// Evaluate the base pose from the time when transition was interrupted
const auto destinationState = SampleState(bucket.BaseTransition->Destination);
const float alpha = bucket.BaseTransitionPosition / bucket.BaseTransition->BlendDuration;
value = Blend(node, value, destinationState, alpha, bucket.BaseTransition->BlendMode);
}
}
else
{
// Sample the current state
value = SampleState(bucket.CurrentState);
}
// Handle active transition blending
if (bucket.ActiveTransition)
@@ -1704,14 +1789,12 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu
const auto destinationState = SampleState(bucket.ActiveTransition->Destination);
// Perform blending
const float alpha = Math::Saturate(bucket.TransitionPosition / bucket.ActiveTransition->BlendDuration);
value = Blend(node, currentState, destinationState, alpha, bucket.ActiveTransition->BlendMode);
const float alpha = bucket.TransitionPosition / bucket.ActiveTransition->BlendDuration;
value = Blend(node, value, destinationState, alpha, bucket.ActiveTransition->BlendMode);
}
// Update bucket
bucket.LastUpdateFrame = context.CurrentFrameIndex;
#undef END_TRANSITION
break;
}
// Entry
@@ -2132,7 +2215,7 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu
// Blend out
auto input = tryGetValue(node->GetBox(1), Value::Null);
bucket.BlendOutPosition += deltaTime;
const float alpha = Math::Saturate(bucket.BlendOutPosition / slot.BlendOutTime);
const float alpha = bucket.BlendOutPosition / slot.BlendOutTime;
value = Blend(node, value, input, alpha, AlphaBlendMode::HermiteCubic);
}
else if (bucket.LoopsDone == 0 && slot.BlendInTime > 0.0f && bucket.BlendInPosition < slot.BlendInTime)
@@ -2140,7 +2223,7 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu
// Blend in
auto input = tryGetValue(node->GetBox(1), Value::Null);
bucket.BlendInPosition += deltaTime;
const float alpha = Math::Saturate(bucket.BlendInPosition / slot.BlendInTime);
const float alpha = bucket.BlendInPosition / slot.BlendInTime;
value = Blend(node, input, value, alpha, AlphaBlendMode::HermiteCubic);
}
break;

View File

@@ -19,6 +19,7 @@ AudioSource::AudioSource(const SpawnParams& params)
, _minDistance(1000.0f)
, _loop(false)
, _playOnStart(false)
, _startTime(0.0f)
, _allowSpatialization(true)
{
Clip.Changed.Bind<AudioSource, &AudioSource::OnClipChanged>(this);
@@ -71,6 +72,11 @@ void AudioSource::SetPlayOnStart(bool value)
_playOnStart = value;
}
void AudioSource::SetStartTime(float value)
{
_startTime = value;
}
void AudioSource::SetMinDistance(float value)
{
value = Math::Max(0.0f, value);
@@ -361,6 +367,7 @@ void AudioSource::Serialize(SerializeStream& stream, const void* otherObj)
SERIALIZE_MEMBER(DopplerFactor, _dopplerFactor);
SERIALIZE_MEMBER(Loop, _loop);
SERIALIZE_MEMBER(PlayOnStart, _playOnStart);
SERIALIZE_MEMBER(StartTime, _startTime);
SERIALIZE_MEMBER(AllowSpatialization, _allowSpatialization);
}
@@ -377,6 +384,7 @@ void AudioSource::Deserialize(DeserializeStream& stream, ISerializeModifier* mod
DESERIALIZE_MEMBER(DopplerFactor, _dopplerFactor);
DESERIALIZE_MEMBER(Loop, _loop);
DESERIALIZE_MEMBER(PlayOnStart, _playOnStart);
DESERIALIZE_MEMBER(StartTime, _startTime);
DESERIALIZE_MEMBER(AllowSpatialization, _allowSpatialization);
DESERIALIZE(Clip);
}
@@ -540,5 +548,7 @@ void AudioSource::BeginPlay(SceneBeginData* data)
return;
#endif
Play();
if (GetStartTime() > 0)
SetTime(GetStartTime());
}
}

View File

@@ -52,6 +52,7 @@ private:
float _dopplerFactor = 1.0f;
bool _loop;
bool _playOnStart;
float _startTime;
bool _allowSpatialization;
bool _clipChanged = false;
@@ -148,11 +149,25 @@ public:
return _playOnStart;
}
/// <summary>
/// Determines the time (in seconds) at which the audio clip starts playing if Play On Start is enabled.
/// </summary>
API_PROPERTY(Attributes = "EditorOrder(51), DefaultValue(0.0f), Limit(0, float.MaxValue, 0.01f), EditorDisplay(\"Audio Source\", \"Start Time\"), VisibleIf(nameof(PlayOnStart))")
FORCE_INLINE float GetStartTime() const
{
return _startTime;
}
/// <summary>
/// Determines whether the audio clip should auto play on game start.
/// </summary>
API_PROPERTY() void SetPlayOnStart(bool value);
/// <summary>
/// Determines the time (in seconds) at which the audio clip starts playing if Play On Start is enabled.
/// </summary>
API_PROPERTY() void SetStartTime(float value);
/// <summary>
/// Gets the minimum distance at which audio attenuation starts. When the listener is closer to the source than this value, audio is heard at full volume. Once farther away the audio starts attenuating.
/// </summary>

View File

@@ -116,7 +116,7 @@ namespace ALC
{
AudioBackend::Listener::TransformChanged(listener);
const Vector3 velocity = listener->GetVelocity();
const Float3 velocity = listener->GetVelocity();
alListener3f(AL_VELOCITY, FLAX_VEL_TO_OAL(velocity));
alListenerf(AL_GAIN, Audio::GetVolume());
}
@@ -319,8 +319,6 @@ void AudioBackendOAL::Listener_OnAdd(AudioListener* listener)
ALC::RebuildContexts(false);
#else
AudioBackend::Listener::TransformChanged(listener);
const Vector3 velocity = listener->GetVelocity();
alListener3f(AL_VELOCITY, FLAX_VEL_TO_OAL(velocity));
alListenerf(AL_GAIN, Audio::GetVolume());
#endif
}
@@ -336,7 +334,7 @@ void AudioBackendOAL::Listener_VelocityChanged(AudioListener* listener)
{
ALC_GET_LISTENER_CONTEXT(listener)
const Vector3 velocity = listener->GetVelocity();
const Float3 velocity = listener->GetVelocity();
alListener3f(AL_VELOCITY, FLAX_VEL_TO_OAL(velocity));
}
@@ -344,15 +342,15 @@ void AudioBackendOAL::Listener_TransformChanged(AudioListener* listener)
{
ALC_GET_LISTENER_CONTEXT(listener)
const Vector3 position = listener->GetPosition();
const Float3 position = listener->GetPosition();
const Quaternion orientation = listener->GetOrientation();
const Vector3 flipX(-1, 1, 1);
const Vector3 alOrientation[2] =
const Float3 flipX(-1, 1, 1);
const Float3 alOrientation[2] =
{
// Forward
orientation * Vector3::Forward * flipX,
orientation * Float3::Forward * flipX,
// Up
orientation * Vector3::Up * flipX
orientation * Float3::Up * flipX
};
alListenerfv(AL_ORIENTATION, (float*)alOrientation);

View File

@@ -18,7 +18,7 @@ namespace FlaxEditor.Content.Settings
/// <summary>
/// The layers names.
/// </summary>
[EditorOrder(10), EditorDisplay("Layers", EditorDisplayAttribute.InlineStyle), Collection(ReadOnly = true)]
[EditorOrder(10), EditorDisplay("Layers", EditorDisplayAttribute.InlineStyle), Collection(ReadOnly = true, Display = CollectionAttribute.DisplayType.Inline)]
public string[] Layers = new string[32];
/// <summary>

View File

@@ -56,13 +56,7 @@ using System.Runtime.InteropServices;
namespace FlaxEngine
{
/// <summary>
/// Represents a 3x3 Matrix ( contains only Scale and Rotation ).
/// </summary>
[Serializable]
[StructLayout(LayoutKind.Sequential, Pack = 4)]
// ReSharper disable once InconsistentNaming
public struct Matrix3x3 : IEquatable<Matrix3x3>, IFormattable
partial struct Matrix3x3 : IEquatable<Matrix3x3>, IFormattable
{
/// <summary>
/// The size of the <see cref="Matrix3x3"/> type, in bytes.
@@ -135,9 +129,7 @@ namespace FlaxEngine
/// <param name="value">The value that will be assigned to all components.</param>
public Matrix3x3(float value)
{
M11 = M12 = M13 =
M21 = M22 = M23 =
M31 = M32 = M33 = value;
M11 = M12 = M13 = M21 = M22 = M23 = M31 = M32 = M33 = value;
}
/// <summary>

View File

@@ -9,8 +9,9 @@
/// <summary>
/// Represents a 3x3 mathematical matrix.
/// </summary>
API_STRUCT(InBuild) struct FLAXENGINE_API Matrix3x3
API_STRUCT() struct FLAXENGINE_API Matrix3x3
{
DECLARE_SCRIPTING_TYPE_MINIMAL(Matrix3x3);
public:
union
{

View File

@@ -139,6 +139,24 @@ VariantType::VariantType(const StringAnsiView& typeName)
return;
}
}
{
// Aliases
if (typeName == "FlaxEngine.Vector2")
{
new(this) VariantType(Vector2);
return;
}
if (typeName == "FlaxEngine.Vector3")
{
new(this) VariantType(Vector3);
return;
}
if (typeName == "FlaxEngine.Vector4")
{
new(this) VariantType(Vector4);
return;
}
}
// Check case for array
if (typeName.EndsWith(StringAnsiView("[]"), StringSearchCase::CaseSensitive))
@@ -3985,15 +4003,32 @@ void Variant::CopyStructure(void* src)
{
if (AsBlob.Data && src)
{
const ScriptingTypeHandle typeHandle = Scripting::FindScriptingType(StringAnsiView(Type.TypeName));
const StringAnsiView typeName(Type.TypeName);
const ScriptingTypeHandle typeHandle = Scripting::FindScriptingType(typeName);
if (typeHandle)
{
auto& type = typeHandle.GetType();
type.Struct.Copy(AsBlob.Data, src);
}
#if USE_CSHARP
else if (const auto mclass = Scripting::FindClass(typeName))
{
// Fallback to C#-only types
MCore::Thread::Attach();
if (MANAGED_GC_HANDLE && mclass->IsValueType())
{
MObject* instance = MCore::GCHandle::GetTarget(MANAGED_GC_HANDLE);
void* data = MCore::Object::Unbox(instance);
Platform::MemoryCopy(data, src, mclass->GetInstanceSize());
}
}
#endif
else
{
Platform::MemoryCopy(AsBlob.Data, src, AsBlob.Length);
if (typeName.Length() != 0)
{
LOG(Warning, "Missing scripting type \'{0}\'", String(typeName));
}
}
}
}

View File

@@ -1359,7 +1359,7 @@ bool Actor::IsPrefabRoot() const
Actor* Actor::FindActor(const StringView& name) const
{
Actor* result = nullptr;
if (StringUtils::Compare(*_name, *name) == 0)
if (_name == name)
{
result = const_cast<Actor*>(this);
}
@@ -1393,7 +1393,7 @@ Actor* Actor::FindActor(const MClass* type) const
Actor* Actor::FindActor(const MClass* type, const StringView& name) const
{
CHECK_RETURN(type, nullptr);
if (GetClass()->IsSubClassOf(type) && StringUtils::Compare(*_name, *name) == 0)
if (GetClass()->IsSubClassOf(type) && _name == name)
return const_cast<Actor*>(this);
for (auto child : Children)
{

View File

@@ -225,6 +225,17 @@ void AnimatedModel::SetMasterPoseModel(AnimatedModel* masterPose)
_masterPose->AnimationUpdated.Bind<AnimatedModel, &AnimatedModel::OnAnimationUpdated>(this);
}
const Array<AnimGraphTraceEvent>& AnimatedModel::GetTraceEvents() const
{
#if !BUILD_RELEASE
if (!GetEnableTracing())
{
LOG(Warning, "Accessing AnimatedModel.TraceEvents with tracing disabled.");
}
#endif
return GraphInstance.TraceEvents;
}
#define CHECK_ANIM_GRAPH_PARAM_ACCESS() \
if (!AnimationGraph) \
{ \

View File

@@ -259,6 +259,27 @@ public:
/// <param name="masterPose">The master pose actor to use.</param>
API_FUNCTION() void SetMasterPoseModel(AnimatedModel* masterPose);
/// <summary>
/// Enables extracting animation playback insights for debugging or custom scripting.
/// </summary>
API_PROPERTY(Attributes="HideInEditor, NoSerialize") bool GetEnableTracing() const
{
return GraphInstance.EnableTracing;
}
/// <summary>
/// Enables extracting animation playback insights for debugging or custom scripting.
/// </summary>
API_PROPERTY() void SetEnableTracing(bool value)
{
GraphInstance.EnableTracing = value;
}
/// <summary>
/// Gets the trace events from the last animation update. Valid only when EnableTracing is active.
/// </summary>
API_PROPERTY(Attributes="HideInEditor, NoSerialize") const Array<AnimGraphTraceEvent>& GetTraceEvents() const;
public:
/// <summary>
/// Gets the anim graph instance parameters collection.

View File

@@ -16,7 +16,7 @@ public:
/// <summary>
/// The list of the string localization tables used by the game.
/// </summary>
API_FIELD()
API_FIELD(Attributes="Collection(Display = CollectionAttribute.DisplayType.Inline)")
Array<AssetReference<LocalizedStringTable>> LocalizedStringTables;
/// <summary>

View File

@@ -300,6 +300,44 @@ void RigidBody::ClosestPoint(const Vector3& position, Vector3& result) const
}
}
void RigidBody::AddMovement(const Vector3& translation, const Quaternion& rotation)
{
// filter rotation according to constraints
Quaternion allowedRotation;
if (EnumHasAllFlags(GetConstraints(), RigidbodyConstraints::LockRotation))
allowedRotation = Quaternion::Identity;
else
{
Float3 euler = rotation.GetEuler();
if (EnumHasAnyFlags(GetConstraints(), RigidbodyConstraints::LockRotationX))
euler.X = 0;
if (EnumHasAnyFlags(GetConstraints(), RigidbodyConstraints::LockRotationY))
euler.Y = 0;
if (EnumHasAnyFlags(GetConstraints(), RigidbodyConstraints::LockRotationZ))
euler.Z = 0;
allowedRotation = Quaternion::Euler(euler);
}
// filter translation according to the constraints
auto allowedTranslation = translation;
if (EnumHasAllFlags(GetConstraints(), RigidbodyConstraints::LockPosition))
allowedTranslation = Vector3::Zero;
else
{
if (EnumHasAnyFlags(GetConstraints(), RigidbodyConstraints::LockPositionX))
allowedTranslation.X = 0;
if (EnumHasAnyFlags(GetConstraints(), RigidbodyConstraints::LockPositionY))
allowedTranslation.Y = 0;
if (EnumHasAnyFlags(GetConstraints(), RigidbodyConstraints::LockPositionZ))
allowedTranslation.Z = 0;
}
Transform t;
t.Translation = _transform.Translation + allowedTranslation;
t.Orientation = _transform.Orientation * allowedRotation;
t.Scale = _transform.Scale;
SetTransform(t);
}
void RigidBody::OnCollisionEnter(const Collision& c)
{
CollisionEnter(c);

View File

@@ -486,6 +486,7 @@ public:
// [Actor]
void Serialize(SerializeStream& stream, const void* otherObj) override;
void Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) override;
void AddMovement(const Vector3& translation, const Quaternion& rotation) override;
// [IPhysicsActor]
void* GetPhysicsActor() const override;

View File

@@ -3312,7 +3312,6 @@ void* PhysicsBackend::CreateVehicle(WheeledVehicle* actor)
// Create vehicle drive
auto drive4W = PxVehicleDrive4W::allocate(wheels.Count());
drive4W->setup(PhysX, actorPhysX, *wheelsSimData, driveSimData, Math::Max(wheels.Count() - 4, 0));
drive4W->setToRestState();
drive4W->mDriveDynData.forceGearChange(PxVehicleGearsData::eFIRST);
drive4W->mDriveDynData.setUseAutoGears(gearbox.AutoGear);
vehicle = drive4W;
@@ -3355,7 +3354,6 @@ void* PhysicsBackend::CreateVehicle(WheeledVehicle* actor)
// Create vehicle drive
auto driveNW = PxVehicleDriveNW::allocate(wheels.Count());
driveNW->setup(PhysX, actorPhysX, *wheelsSimData, driveSimData, wheels.Count());
driveNW->setToRestState();
driveNW->mDriveDynData.forceGearChange(PxVehicleGearsData::eFIRST);
driveNW->mDriveDynData.setUseAutoGears(gearbox.AutoGear);
vehicle = driveNW;
@@ -3366,7 +3364,6 @@ void* PhysicsBackend::CreateVehicle(WheeledVehicle* actor)
// Create vehicle drive
auto driveNo = PxVehicleNoDrive::allocate(wheels.Count());
driveNo->setup(PhysX, actorPhysX, *wheelsSimData);
driveNo->setToRestState();
vehicle = driveNo;
break;
}

View File

@@ -203,8 +203,11 @@ void Physics::Simulate(float dt)
void Physics::CollectResults()
{
if (DefaultScene)
DefaultScene->CollectResults();
for (PhysicsScene* scene : Scenes)
{
if (scene->GetAutoSimulation())
scene->CollectResults();
}
}
bool Physics::IsDuringSimulation()

View File

@@ -324,7 +324,19 @@ void WindowsWindow::SetBorderless(bool isBorderless, bool maximized)
lStyle |= WS_OVERLAPPED | WS_SYSMENU | WS_BORDER | WS_CAPTION;
SetWindowLong(_handle, GWL_STYLE, lStyle);
SetWindowPos(_handle, nullptr, 0, 0, (int)_settings.Size.X, (int)_settings.Size.Y, SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE);
const Float2 clientSize = GetClientSize();
const Float2 desktopSize = Platform::GetDesktopSize();
// Move window and half size if it is larger than desktop size
if (clientSize.X >= desktopSize.X && clientSize.Y >= desktopSize.Y)
{
const Float2 halfSize = desktopSize * 0.5f;
const Float2 middlePos = halfSize * 0.5f;
SetWindowPos(_handle, nullptr, (int)middlePos.X, (int)middlePos.Y, (int)halfSize.X, (int)halfSize.Y, SWP_FRAMECHANGED | SWP_NOZORDER | SWP_NOACTIVATE);
}
else
{
SetWindowPos(_handle, nullptr, 0, 0, (int)clientSize.X, (int)clientSize.Y, SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE);
}
if (maximized)
{

View File

@@ -10,6 +10,32 @@ namespace FlaxEngine
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Class)]
public sealed class CollectionAttribute : Attribute
{
/// <summary>
/// The display type for collections.
/// </summary>
public enum DisplayType
{
/// <summary>
/// Displays the default display type.
/// </summary>
Default,
/// <summary>
/// Displays a header.
/// </summary>
Header,
/// <summary>
/// Displays inline.
/// </summary>
Inline,
}
/// <summary>
/// Gets or sets the display type.
/// </summary>
public DisplayType Display;
/// <summary>
/// Gets or sets whether this collection is read-only. If <c>true</c>, applications using this collection should not allow to add or remove items.
/// </summary>

View File

@@ -606,14 +606,15 @@ namespace FlaxEngine.GUI
_popup.LostFocus += DestroyPopup;
// Show dropdown popup
var locationRootSpace = Location + new Float2(0, Height);
var locationRootSpace = Location + new Float2(0, Height - Height * (1 - Scale.Y) / 2);
var parent = Parent;
while (parent != null && parent != root)
{
locationRootSpace = parent.PointToParent(ref locationRootSpace);
parent = parent.Parent;
}
_popup.Location = locationRootSpace;
_popup.Scale = Scale;
_popup.Location = locationRootSpace - new Float2(0, _popup.Height * (1 - _popup.Scale.Y) / 2);
_popup.Parent = root;
_popup.Focus();
_popup.StartMouseCapture();

View File

@@ -132,6 +132,22 @@ namespace FlaxEngine.GUI
if (_alwaysShowScrollbars != value)
{
_alwaysShowScrollbars = value;
switch (_scrollBars)
{
case ScrollBars.None:
break;
case ScrollBars.Horizontal:
HScrollBar.Visible = value;
break;
case ScrollBars.Vertical:
VScrollBar.Visible = value;
break;
case ScrollBars.Both:
HScrollBar.Visible = value;
VScrollBar.Visible = value;
break;
default: break;
}
PerformLayout();
}
}

View File

@@ -685,9 +685,9 @@ void VisjectExecutor::ProcessGroupPacking(Box* box, Node* node, Value& value)
case 36:
{
// Get value with structure data
Variant structureValue = eatBox(node, node->GetBox(0)->FirstConnection());
if (!node->GetBox(0)->HasConnection())
return;
Variant structureValue = eatBox(node, node->GetBox(0)->FirstConnection());
// Find type
const StringView typeName(node->Values[0]);
@@ -741,6 +741,12 @@ void VisjectExecutor::ProcessGroupPacking(Box* box, Node* node, Value& value)
return;
}
const ScriptingType& type = typeHandle.GetType();
if (structureValue.Type.Type != VariantType::Structure) // If structureValue is eg. Float we can try to cast it to a required structure type
{
VariantType typeVariantType(typeNameAnsiView);
if (Variant::CanCast(structureValue, typeVariantType))
structureValue = Variant::Cast(structureValue, typeVariantType);
}
structureValue.InvertInline(); // Extract any Float3/Int32 into Structure type from inlined format
const ScriptingTypeHandle structureValueTypeHandle = Scripting::FindScriptingType(structureValue.Type.GetTypeName());
if (structureValue.Type.Type != VariantType::Structure || typeHandle != structureValueTypeHandle)