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

# Conflicts:
#	Content/Shaders/GI/DDGI.flax
#	Content/Shaders/GUI.flax
#	Flax.flaxproj
#	Source/Editor/Windows/AboutDialog.cs
#	Source/Engine/Serialization/Stream.cpp
#	Source/Shaders/GUICommon.hlsl
This commit is contained in:
Wojtek Figat
2026-03-13 08:09:16 +01:00
152 changed files with 3784 additions and 434 deletions

View File

@@ -109,7 +109,14 @@ void Behavior::UpdateAsync()
const BehaviorUpdateResult result = tree->Graph.Root->InvokeUpdate(context);
if (result != BehaviorUpdateResult::Running)
_result = result;
if (_result != BehaviorUpdateResult::Running)
if (_result != BehaviorUpdateResult::Running && tree->Graph.Root->Loop)
{
// Reset State
_result = BehaviorUpdateResult::Running;
_accumulatedTime = 0.0f;
_totalTime = 0;
}
else if (_result != BehaviorUpdateResult::Running)
{
Finished();
}

View File

@@ -96,6 +96,10 @@ API_CLASS(Sealed) class FLAXENGINE_API BehaviorTreeRootNode : public BehaviorTre
// The target amount of the behavior logic updates per second.
API_FIELD(Attributes="EditorOrder(100)")
float UpdateFPS = 10.0f;
// Whether to loop the root node.
API_FIELD(Attributes="EditorOrder(200)")
bool Loop = true;
};
/// <summary>

View File

@@ -3,7 +3,6 @@
#include "Audio.h"
#include "AudioBackend.h"
#include "AudioSettings.h"
#include "FlaxEngine.Gen.h"
#include "Engine/Scripting/ScriptingType.h"
#include "Engine/Scripting/BinaryModule.h"
#include "Engine/Level/Level.h"

View File

@@ -5,6 +5,7 @@
#if USE_EDITOR
#include "BinaryAssetUpgrader.h"
#include "Engine/Render2D/FontAsset.h"
/// <summary>
/// Font Asset Upgrader
@@ -17,10 +18,33 @@ public:
{
const Upgrader upgraders[] =
{
{},
{ 3, 4, &Upgrade_3_To_4 },
};
setup(upgraders, ARRAY_COUNT(upgraders));
}
private:
struct FontOptionsOld
{
FontHinting Hinting;
FontFlags Flags;
};
static bool Upgrade_3_To_4(AssetMigrationContext& context)
{
ASSERT(context.Input.SerializedVersion == 3 && context.Output.SerializedVersion == 4);
FontOptionsOld optionsOld;
Platform::MemoryCopy(&optionsOld, context.Input.CustomData.Get(), sizeof(FontOptionsOld));
FontOptions options;
options.Hinting = optionsOld.Hinting;
options.Flags = optionsOld.Flags;
options.RasterMode = FontRasterMode::Bitmap;
context.Output.CustomData.Copy(&options);
return CopyChunk(context, 0);
}
};
#endif

View File

@@ -36,6 +36,30 @@
#include "CreateAnimation.h"
#include "CreateBehaviorTree.h"
#include "CreateJson.h"
#include "Engine/Content/Assets/Model.h"
namespace
{
bool IsAssetTypeNameTextureFile(const String& typeName)
{
return typeName == Texture::TypeName || typeName == SpriteAtlas::TypeName;
}
bool IsAssetTypeNameModelFile(const String& typeName)
{
return typeName == Model::TypeName || typeName == SkinnedModel::TypeName || typeName == Animation::TypeName;
}
bool IsAssetTypeNameMatch(const String& a, const String& b)
{
// Special case when reimporting model/texture but different type
if (IsAssetTypeNameTextureFile(a) && IsAssetTypeNameTextureFile(b))
return true;
if (IsAssetTypeNameModelFile(a) && IsAssetTypeNameModelFile(b))
return true;
return a == b;
}
}
// Tags used to detect asset creation mode
const String AssetsImportingManager::CreateTextureTag(TEXT("Texture"));
@@ -84,8 +108,6 @@ CreateAssetContext::CreateAssetContext(const StringView& inputPath, const String
CustomArg = arg;
Data.Header.ID = id;
SkipMetadata = false;
// TODO: we should use ASNI only chars path (Assimp can use only that kind)
OutputPath = Content::CreateTemporaryAssetPath();
}
@@ -122,6 +144,24 @@ CreateAssetResult CreateAssetContext::Run(const CreateAssetFunction& callback)
Data.Metadata.Copy((const byte*)buffer.GetString(), (uint32)buffer.GetSize());
}
// Check if target asset already exists but has different type
AssetInfo targetAssetInfo;
if (Content::GetAssetInfo(TargetAssetPath, targetAssetInfo) && !IsAssetTypeNameMatch(targetAssetInfo.TypeName, Data.Header.TypeName))
{
// Change path
int32 index = 0;
String newTargetAssetPath;
do
{
newTargetAssetPath = StringUtils::GetDirectoryName(TargetAssetPath);
newTargetAssetPath /= StringUtils::GetFileNameWithoutExtension(TargetAssetPath) + String::Format(TEXT(" ({})."), index++) + FileSystem::GetExtension(TargetAssetPath);
} while (index < 100 && FileSystem::FileExists(newTargetAssetPath));
TargetAssetPath = newTargetAssetPath;
// Change id
Data.Header.ID = Guid::New();
}
// Save file
result = FlaxStorage::Create(OutputPath, Data) ? CreateAssetResult::CannotSaveFile : CreateAssetResult::Ok;
if (result == CreateAssetResult::Ok)

View File

@@ -12,12 +12,13 @@
CreateAssetResult ImportFont::Import(CreateAssetContext& context)
{
// Base
IMPORT_SETUP(FontAsset, 3);
IMPORT_SETUP(FontAsset, 4);
// Setup header
FontOptions options;
options.Hinting = FontHinting::Default;
options.Flags = FontFlags::AntiAliasing;
options.RasterMode = FontRasterMode::Bitmap;
context.Data.CustomData.Copy(&options);
// Open the file

View File

@@ -82,6 +82,11 @@ namespace FlaxEngine
}
}
/// <summary>
/// Gets the brightness of the color
/// </summary>
public float Brightness => R * 0.299f + G * 0.587f + B * 0.114f;
/// <summary>
/// Returns the minimum color component value: Min(r,g,b).
/// </summary>

View File

@@ -166,6 +166,7 @@ bool CommandLine::Parse(const Char* cmdLine)
PARSE_BOOL_SWITCH("-clearcache ", ClearCache);
PARSE_BOOL_SWITCH("-clearcooker ", ClearCookerCache);
PARSE_ARG_SWITCH("-project ", Project);
PARSE_BOOL_SWITCH("-lastproject ", LastProject);
PARSE_BOOL_SWITCH("-new ", NewProject);
PARSE_BOOL_SWITCH("-genprojectfiles ", GenProjectFiles);
PARSE_ARG_SWITCH("-build ", Build);

View File

@@ -148,6 +148,11 @@ public:
/// </summary>
String Project;
/// <summary>
/// -lastproject (Opens the last project)
/// </summary>
Nullable<bool> LastProject;
/// <summary>
/// -new (generates the project files inside the specified project folder or uses current workspace folder)
/// </summary>

View File

@@ -581,7 +581,11 @@ void EngineImpl::InitLog()
#if COMPILE_WITH_DEV_ENV
LOG(Info, "Compiled for Dev Environment");
#endif
#if defined(FLAXENGINE_BRANCH) && defined(FLAXENGINE_COMMIT)
LOG(Info, "Version " FLAXENGINE_VERSION_TEXT ", {}, {}", StringAsUTF16<>(FLAXENGINE_BRANCH).Get(), StringAsUTF16<>(FLAXENGINE_COMMIT).Get());
#else
LOG(Info, "Version " FLAXENGINE_VERSION_TEXT);
#endif
const Char* cpp = TEXT("?");
if (__cplusplus == 202101L) cpp = TEXT("C++23");
else if (__cplusplus == 202002L) cpp = TEXT("C++20");

View File

@@ -22,7 +22,7 @@
#include "Engine/Serialization/Serialization.h"
#include "Engine/Utilities/Encryption.h"
#define FOLIAGE_GET_DRAW_MODES(renderContext, type) (type.DrawModes & renderContext.View.Pass & renderContext.View.GetShadowsDrawPassMask(type.ShadowsMode))
#define FOLIAGE_GET_DRAW_MODES(renderContext, type) (type._drawModes & renderContext.View.Pass & renderContext.View.GetShadowsDrawPassMask(type.ShadowsMode))
#define FOLIAGE_CAN_DRAW(renderContext, type) (type.IsReady() && FOLIAGE_GET_DRAW_MODES(renderContext, type) != DrawPass::None && type.Model->CanBeRendered())
Foliage::Foliage(const SpawnParams& params)
@@ -360,7 +360,7 @@ void Foliage::DrawCluster(DrawContext& context, FoliageCluster* cluster, Mesh::D
draw.DrawState = &instance.DrawState;
draw.Bounds = sphere;
draw.PerInstanceRandom = instance.Random;
draw.DrawModes = type.DrawModes;
draw.DrawModes = type._drawModes;
draw.SetStencilValue(_layer);
type.Model->Draw(context.RenderContext, draw);
@@ -597,14 +597,22 @@ void Foliage::DrawType(RenderContext& renderContext, const FoliageType& type, Me
void Foliage::InitType(const RenderView& view, FoliageType& type)
{
const DrawPass drawModes = type.DrawModes & view.Pass & view.GetShadowsDrawPassMask(type.ShadowsMode);
const DrawPass drawModes = type._drawModes & view.Pass & view.GetShadowsDrawPassMask(type.ShadowsMode);
type._canDraw = type.IsReady() && drawModes != DrawPass::None && type.Model && type.Model->CanBeRendered();
bool drawModesDirty = false;
for (int32 j = 0; j < type.Entries.Count(); j++)
{
auto& e = type.Entries[j];
e.ReceiveDecals = type.ReceiveDecals != 0;
e.ShadowsMode = type.ShadowsMode;
if (type._drawModesDirty)
{
type._drawModesDirty = 0;
drawModesDirty = true;
}
}
if (drawModesDirty)
GetSceneRendering()->UpdateActor(this, _sceneRenderingKey, ISceneRenderingListener::DrawModes);
}
int32 Foliage::GetInstancesCount() const
@@ -1250,7 +1258,7 @@ void Foliage::Draw(RenderContext& renderContext)
draw.Deformation = nullptr;
draw.Bounds = instance.Bounds;
draw.PerInstanceRandom = instance.Random;
draw.DrawModes = type.DrawModes & view.Pass & view.GetShadowsDrawPassMask(type.ShadowsMode);
draw.DrawModes = type._drawModes & view.Pass & view.GetShadowsDrawPassMask(type.ShadowsMode);
draw.SetStencilValue(_layer);
type.Model->Draw(renderContext, draw);
return;

View File

@@ -13,6 +13,7 @@ FoliageType::FoliageType()
, Index(-1)
{
_isReady = 0;
_drawModesDirty = 0;
ReceiveDecals = true;
UseDensityScaling = false;
@@ -32,7 +33,7 @@ FoliageType& FoliageType::operator=(const FoliageType& other)
CullDistance = other.CullDistance;
CullDistanceRandomRange = other.CullDistanceRandomRange;
ScaleInLightmap = other.ScaleInLightmap;
DrawModes = other.DrawModes;
SetDrawModes(other._drawModes);
ShadowsMode = other.ShadowsMode;
PaintDensity = other.PaintDensity;
PaintRadius = other.PaintRadius;
@@ -69,6 +70,19 @@ void FoliageType::SetMaterials(const Array<MaterialBase*>& value)
Entries[i].Material = value[i];
}
DrawPass FoliageType::GetDrawModes() const
{
return _drawModes;
}
void FoliageType::SetDrawModes(DrawPass value)
{
if (_drawModes == value)
return;
_drawModes = value;
_drawModesDirty = 1;
}
Float3 FoliageType::GetRandomScale() const
{
Float3 result;
@@ -150,7 +164,7 @@ void FoliageType::Serialize(SerializeStream& stream, const void* otherObj)
SERIALIZE(CullDistance);
SERIALIZE(CullDistanceRandomRange);
SERIALIZE(ScaleInLightmap);
SERIALIZE(DrawModes);
SERIALIZE_MEMBER(DrawModes, _drawModes);
SERIALIZE(ShadowsMode);
SERIALIZE_BIT(ReceiveDecals);
SERIALIZE_BIT(UseDensityScaling);
@@ -191,7 +205,7 @@ void FoliageType::Deserialize(DeserializeStream& stream, ISerializeModifier* mod
DESERIALIZE(CullDistance);
DESERIALIZE(CullDistanceRandomRange);
DESERIALIZE(ScaleInLightmap);
DESERIALIZE(DrawModes);
DESERIALIZE_MEMBER(DrawModes, _drawModes);
DESERIALIZE(ShadowsMode);
DESERIALIZE_BIT(ReceiveDecals);
DESERIALIZE_BIT(UseDensityScaling);

View File

@@ -48,6 +48,8 @@ API_CLASS(Sealed, NoSpawn) class FLAXENGINE_API FoliageType : public ScriptingOb
private:
uint8 _isReady : 1;
uint8 _canDraw : 1;
uint8 _drawModesDirty : 1;
DrawPass _drawModes = DrawPass::Depth | DrawPass::GBuffer | DrawPass::Forward;
public:
/// <summary>
@@ -124,9 +126,15 @@ public:
API_FIELD() float ScaleInLightmap = 1.0f;
/// <summary>
/// The draw passes to use for rendering this foliage type.
/// Gets the draw passes to use for rendering this foliage type.
/// </summary>
API_FIELD() DrawPass DrawModes = DrawPass::Depth | DrawPass::GBuffer | DrawPass::Forward;
API_PROPERTY(Attributes="DefaultValue(DrawPass.Depth | DrawPass.GBuffer | DrawPass.Forward)")
DrawPass GetDrawModes() const;
/// <summary>
/// Sets the draw passes to use for rendering this foliage type.
/// </summary>
API_PROPERTY() void SetDrawModes(DrawPass value);
/// <summary>
/// The shadows casting mode.
@@ -184,7 +192,7 @@ public:
API_FIELD() float PlacementRandomRollAngle = 0.0f;
/// <summary>
/// The density scaling scale applied to the global scale for the foliage instances of this type. Can be used to boost or reduce density scaling effect on this foliage type. Default is 1.
/// The density scale factor applied to the global scale for the foliage instances of this type. Can be used to boost or reduce density scaling effect on this foliage type. Default is 1. Lower to reduce density scaling effect when downscaling foliage via global quality/scalability.
/// </summary>
API_FIELD() float DensityScalingScale = 1.0f;

View File

@@ -158,6 +158,14 @@ void MeshAccessor::Stream::CopyTo(Span<Float3> dst) const
{
Platform::MemoryCopy(dst.Get(), _data.Get(), _data.Length());
}
else if (IsLinear(PixelFormat::R16G16B16A16_Float))
{
for (int32 i = 0; i < count; i++)
{
auto v = *(Half4*)(_data.Get() + i * _stride);
dst.Get()[i] = Float3(Float16Compressor::Decompress(v.X), Float16Compressor::Decompress(v.Y), Float16Compressor::Decompress(v.Z));
}
}
else
{
for (int32 i = 0; i < count; i++)

View File

@@ -2,6 +2,71 @@
#include "Gamepad.h"
namespace
{
GamepadAxis GetButtonAxis(GamepadButton button, bool& positive)
{
positive = true;
switch (button)
{
case GamepadButton::LeftTrigger:
return GamepadAxis::LeftTrigger;
case GamepadButton::RightTrigger:
return GamepadAxis::RightTrigger;
case GamepadButton::LeftStickUp:
return GamepadAxis::LeftStickY;
case GamepadButton::LeftStickDown:
positive = false;
return GamepadAxis::LeftStickY;
case GamepadButton::LeftStickLeft:
positive = false;
return GamepadAxis::LeftStickX;
case GamepadButton::LeftStickRight:
return GamepadAxis::LeftStickX;
case GamepadButton::RightStickUp:
return GamepadAxis::RightStickY;
case GamepadButton::RightStickDown:
positive = false;
return GamepadAxis::RightStickY;
case GamepadButton::RightStickLeft:
positive = false;
return GamepadAxis::RightStickX;
case GamepadButton::RightStickRight:
return GamepadAxis::RightStickX;
default:
return GamepadAxis::None;
}
}
bool GetButtonState(const Gamepad::State& state, GamepadButton button, float deadZone)
{
if (deadZone > 0.01f)
{
switch (button)
{
case GamepadButton::LeftTrigger:
case GamepadButton::RightTrigger:
case GamepadButton::LeftStickUp:
case GamepadButton::LeftStickDown:
case GamepadButton::LeftStickLeft:
case GamepadButton::LeftStickRight:
case GamepadButton::RightStickUp:
case GamepadButton::RightStickDown:
case GamepadButton::RightStickLeft:
case GamepadButton::RightStickRight:
{
bool positive;
float axis = state.Axis[(int32)GetButtonAxis(button, positive)];
return positive ? axis >= deadZone : axis <= -deadZone;
}
default:
break;
}
}
return state.Buttons[(int32)button];
}
}
void GamepadLayout::Init()
{
for (int32 i = 0; i < (int32)GamepadButton::MAX; i++)
@@ -31,6 +96,21 @@ void Gamepad::ResetState()
_mappedPrevState.Clear();
}
bool Gamepad::GetButton(GamepadButton button, float deadZone) const
{
return GetButtonState(_mappedState, button, deadZone);
}
bool Gamepad::GetButtonDown(GamepadButton button, float deadZone) const
{
return GetButtonState(_mappedState, button, deadZone) && !GetButtonState(_mappedPrevState, button, deadZone);
}
bool Gamepad::GetButtonUp(GamepadButton button, float deadZone) const
{
return !GetButtonState(_mappedState, button, deadZone) && GetButtonState(_mappedPrevState, button, deadZone);
}
bool Gamepad::IsAnyButtonDown() const
{
// TODO: optimize with SIMD

View File

@@ -148,36 +148,30 @@ public:
/// Gets the gamepad button state (true if being pressed during the current frame).
/// </summary>
/// <param name="button">Gamepad button to check</param>
/// <param name="deadZone">Custom dead-zone value to detect gamepad button usage for non-binary buttons such as left/right thumbs that can move freely. By default, any movement is registered.</param>
/// <returns>True if user holds down the button, otherwise false.</returns>
API_FUNCTION() FORCE_INLINE bool GetButton(const GamepadButton button) const
{
return _mappedState.Buttons[static_cast<int32>(button)];
}
API_FUNCTION() bool GetButton(GamepadButton button, float deadZone = 0.0f) const;
/// <summary>
/// Gets the gamepad button down state (true if was pressed during the current frame).
/// </summary>
/// <param name="button">Gamepad button to check</param>
/// <param name="deadZone">Custom dead-zone value to detect gamepad button usage for non-binary buttons such as left/right thumbs that can move freely. By default, any movement is registered.</param>
/// <returns>True if user starts pressing down the button, otherwise false.</returns>
API_FUNCTION() FORCE_INLINE bool GetButtonDown(const GamepadButton button) const
{
return _mappedState.Buttons[static_cast<int32>(button)] && !_mappedPrevState.Buttons[static_cast<int32>(button)];
}
/// <summary>
/// Checks if any gamepad button is currently pressed.
/// </summary>
API_PROPERTY() bool IsAnyButtonDown() const;
API_FUNCTION() bool GetButtonDown(GamepadButton button, float deadZone = 0.0f) const;
/// <summary>
/// Gets the gamepad button up state (true if was released during the current frame).
/// </summary>
/// <param name="button">Gamepad button to check</param>
/// <param name="deadZone">Custom dead-zone value to detect gamepad button usage for non-binary buttons such as left/right thumbs that can move freely. By default, any movement is registered.</param>
/// <returns>True if user releases the button, otherwise false.</returns>
API_FUNCTION() FORCE_INLINE bool GetButtonUp(const GamepadButton button) const
{
return !_mappedState.Buttons[static_cast<int32>(button)] && _mappedPrevState.Buttons[static_cast<int32>(button)];
}
API_FUNCTION() bool GetButtonUp(GamepadButton button, float deadZone = 0.0f) const;
/// <summary>
/// Checks if any gamepad button is currently pressed.
/// </summary>
API_PROPERTY() bool IsAnyButtonDown() const;
public:
/// <summary>

View File

@@ -121,6 +121,7 @@ void InputSettings::Deserialize(DeserializeStream& stream, ISerializeModifier* m
config.MouseButton = JsonTools::GetEnum(v, "MouseButton", MouseButton::None);
config.GamepadButton = JsonTools::GetEnum(v, "GamepadButton", GamepadButton::None);
config.Gamepad = JsonTools::GetEnum(v, "Gamepad", InputGamepadIndex::All);
config.DeadZone = JsonTools::GetFloat(v, "DeadZone", 0.5f);
}
}
else
@@ -513,24 +514,24 @@ float Input::GetGamepadAxis(int32 gamepadIndex, GamepadAxis axis)
return 0.0f;
}
bool Input::GetGamepadButton(int32 gamepadIndex, GamepadButton button)
bool Input::GetGamepadButton(int32 gamepadIndex, GamepadButton button, float deadZone)
{
if (gamepadIndex >= 0 && gamepadIndex < Gamepads.Count())
return Gamepads[gamepadIndex]->GetButton(button);
return Gamepads[gamepadIndex]->GetButton(button, deadZone);
return false;
}
bool Input::GetGamepadButtonDown(int32 gamepadIndex, GamepadButton button)
bool Input::GetGamepadButtonDown(int32 gamepadIndex, GamepadButton button, float deadZone)
{
if (gamepadIndex >= 0 && gamepadIndex < Gamepads.Count())
return Gamepads[gamepadIndex]->GetButtonDown(button);
return Gamepads[gamepadIndex]->GetButtonDown(button, deadZone);
return false;
}
bool Input::GetGamepadButtonUp(int32 gamepadIndex, GamepadButton button)
bool Input::GetGamepadButtonUp(int32 gamepadIndex, GamepadButton button, float deadZone)
{
if (gamepadIndex >= 0 && gamepadIndex < Gamepads.Count())
return Gamepads[gamepadIndex]->GetButtonUp(button);
return Gamepads[gamepadIndex]->GetButtonUp(button, deadZone);
return false;
}
@@ -556,13 +557,13 @@ float Input::GetGamepadAxis(InputGamepadIndex gamepad, GamepadAxis axis)
return false;
}
bool Input::GetGamepadButton(InputGamepadIndex gamepad, GamepadButton button)
bool Input::GetGamepadButton(InputGamepadIndex gamepad, GamepadButton button, float deadZone)
{
if (gamepad == InputGamepadIndex::All)
{
for (auto g : Gamepads)
{
if (g->GetButton(button))
if (g->GetButton(button, deadZone))
return true;
}
}
@@ -570,18 +571,18 @@ bool Input::GetGamepadButton(InputGamepadIndex gamepad, GamepadButton button)
{
const auto index = static_cast<int32>(gamepad);
if (index < Gamepads.Count())
return Gamepads[index]->GetButton(button);
return Gamepads[index]->GetButton(button, deadZone);
}
return false;
}
bool Input::GetGamepadButtonDown(InputGamepadIndex gamepad, GamepadButton button)
bool Input::GetGamepadButtonDown(InputGamepadIndex gamepad, GamepadButton button, float deadZone)
{
if (gamepad == InputGamepadIndex::All)
{
for (auto g : Gamepads)
{
if (g->GetButtonDown(button))
if (g->GetButtonDown(button, deadZone))
return true;
}
}
@@ -589,18 +590,18 @@ bool Input::GetGamepadButtonDown(InputGamepadIndex gamepad, GamepadButton button
{
const auto index = static_cast<int32>(gamepad);
if (index < Gamepads.Count())
return Gamepads[index]->GetButtonDown(button);
return Gamepads[index]->GetButtonDown(button, deadZone);
}
return false;
}
bool Input::GetGamepadButtonUp(InputGamepadIndex gamepad, GamepadButton button)
bool Input::GetGamepadButtonUp(InputGamepadIndex gamepad, GamepadButton button, float deadZone)
{
if (gamepad == InputGamepadIndex::All)
{
for (auto g : Gamepads)
{
if (g->GetButtonUp(button))
if (g->GetButtonUp(button, deadZone))
return true;
}
}
@@ -608,7 +609,7 @@ bool Input::GetGamepadButtonUp(InputGamepadIndex gamepad, GamepadButton button)
{
const auto index = static_cast<int32>(gamepad);
if (index < Gamepads.Count())
return Gamepads[index]->GetButtonUp(button);
return Gamepads[index]->GetButtonUp(button, deadZone);
}
return false;
}
@@ -1083,26 +1084,26 @@ void InputService::Update()
bool isActive;
if (config.Mode == InputActionMode::Pressing)
{
isActive = Input::GetKey(config.Key) || Input::GetMouseButton(config.MouseButton) || Input::GetGamepadButton(config.Gamepad, config.GamepadButton);
isActive = Input::GetKey(config.Key) || Input::GetMouseButton(config.MouseButton) || Input::GetGamepadButton(config.Gamepad, config.GamepadButton, config.DeadZone);
}
else if (config.Mode == InputActionMode::Press)
{
isActive = Input::GetKeyDown(config.Key) || Input::GetMouseButtonDown(config.MouseButton) || Input::GetGamepadButtonDown(config.Gamepad, config.GamepadButton);
isActive = Input::GetKeyDown(config.Key) || Input::GetMouseButtonDown(config.MouseButton) || Input::GetGamepadButtonDown(config.Gamepad, config.GamepadButton, config.DeadZone);
}
else
{
isActive = Input::GetKeyUp(config.Key) || Input::GetMouseButtonUp(config.MouseButton) || Input::GetGamepadButtonUp(config.Gamepad, config.GamepadButton);
isActive = Input::GetKeyUp(config.Key) || Input::GetMouseButtonUp(config.MouseButton) || Input::GetGamepadButtonUp(config.Gamepad, config.GamepadButton, config.DeadZone);
}
if (Input::GetKeyDown(config.Key) || Input::GetMouseButtonDown(config.MouseButton) || Input::GetGamepadButtonDown(config.Gamepad, config.GamepadButton))
if (Input::GetKeyDown(config.Key) || Input::GetMouseButtonDown(config.MouseButton) || Input::GetGamepadButtonDown(config.Gamepad, config.GamepadButton, config.DeadZone))
{
data.State = InputActionState::Press;
}
else if (Input::GetKey(config.Key) || Input::GetMouseButton(config.MouseButton) || Input::GetGamepadButton(config.Gamepad, config.GamepadButton))
else if (Input::GetKey(config.Key) || Input::GetMouseButton(config.MouseButton) || Input::GetGamepadButton(config.Gamepad, config.GamepadButton, config.DeadZone))
{
data.State = InputActionState::Pressing;
}
else if (Input::GetKeyUp(config.Key) || Input::GetMouseButtonUp(config.MouseButton) || Input::GetGamepadButtonUp(config.Gamepad, config.GamepadButton))
else if (Input::GetKeyUp(config.Key) || Input::GetMouseButtonUp(config.MouseButton) || Input::GetGamepadButtonUp(config.Gamepad, config.GamepadButton, config.DeadZone))
{
data.State = InputActionState::Release;
}

View File

@@ -243,24 +243,27 @@ public:
/// </summary>
/// <param name="gamepadIndex">The gamepad index</param>
/// <param name="button">Gamepad button to check</param>
/// <param name="deadZone">Custom dead-zone value to detect gamepad button usage for non-binary buttons such as left/right thumbs that can move freely. By default, any movement is registered.</param>
/// <returns>True if user holds down the button, otherwise false.</returns>
API_FUNCTION() static bool GetGamepadButton(int32 gamepadIndex, GamepadButton button);
API_FUNCTION() static bool GetGamepadButton(int32 gamepadIndex, GamepadButton button, float deadZone = 0.0f);
/// <summary>
/// Gets the gamepad button down state (true if was pressed during the current frame).
/// </summary>
/// <param name="gamepadIndex">The gamepad index</param>
/// <param name="button">Gamepad button to check</param>
/// <param name="deadZone">Custom dead-zone value to detect gamepad button usage for non-binary buttons such as left/right thumbs that can move freely. By default, any movement is registered.</param>
/// <returns>True if user starts pressing down the button, otherwise false.</returns>
API_FUNCTION() static bool GetGamepadButtonDown(int32 gamepadIndex, GamepadButton button);
API_FUNCTION() static bool GetGamepadButtonDown(int32 gamepadIndex, GamepadButton button, float deadZone = 0.0f);
/// <summary>
/// Gets the gamepad button up state (true if was released during the current frame).
/// </summary>
/// <param name="gamepadIndex">The gamepad index</param>
/// <param name="button">Gamepad button to check</param>
/// <param name="deadZone">Custom dead-zone value to detect gamepad button usage for non-binary buttons such as left/right thumbs that can move freely. By default, any movement is registered.</param>
/// <returns>True if user releases the button, otherwise false.</returns>
API_FUNCTION() static bool GetGamepadButtonUp(int32 gamepadIndex, GamepadButton button);
API_FUNCTION() static bool GetGamepadButtonUp(int32 gamepadIndex, GamepadButton button, float deadZone = 0.0f);
/// <summary>
/// Gets the gamepad axis value.
@@ -275,24 +278,27 @@ public:
/// </summary>
/// <param name="gamepad">The gamepad</param>
/// <param name="button">Gamepad button to check</param>
/// <param name="deadZone">Custom dead-zone value to detect gamepad button usage for non-binary buttons such as left/right thumbs that can move freely. By default, any movement is registered.</param>
/// <returns>True if user holds down the button, otherwise false.</returns>
API_FUNCTION() static bool GetGamepadButton(InputGamepadIndex gamepad, GamepadButton button);
API_FUNCTION() static bool GetGamepadButton(InputGamepadIndex gamepad, GamepadButton button, float deadZone = 0.0f);
/// <summary>
/// Gets the gamepad button down state (true if was pressed during the current frame).
/// </summary>
/// <param name="gamepad">The gamepad</param>
/// <param name="button">Gamepad button to check</param>
/// <param name="deadZone">Custom dead-zone value to detect gamepad button usage for non-binary buttons such as left/right thumbs that can move freely. By default, any movement is registered.</param>
/// <returns>True if user starts pressing down the button, otherwise false.</returns>
API_FUNCTION() static bool GetGamepadButtonDown(InputGamepadIndex gamepad, GamepadButton button);
API_FUNCTION() static bool GetGamepadButtonDown(InputGamepadIndex gamepad, GamepadButton button, float deadZone = 0.0f);
/// <summary>
/// Gets the gamepad button up state (true if was released during the current frame).
/// </summary>
/// <param name="gamepad">The gamepad</param>
/// <param name="button">Gamepad button to check</param>
/// <param name="deadZone">Custom dead-zone value to detect gamepad button usage for non-binary buttons such as left/right thumbs that can move freely. By default, any movement is registered.</param>
/// <returns>True if user releases the button, otherwise false.</returns>
API_FUNCTION() static bool GetGamepadButtonUp(InputGamepadIndex gamepad, GamepadButton button);
API_FUNCTION() static bool GetGamepadButtonUp(InputGamepadIndex gamepad, GamepadButton button, float deadZone = 0.0f);
public:
/// <summary>

View File

@@ -48,6 +48,12 @@ API_STRUCT() struct ActionConfig
/// </summary>
API_FIELD(Attributes="EditorOrder(40)")
InputGamepadIndex Gamepad;
/// <summary>
/// Threshold for non-binary value inputs such as gamepad stick position to decide if action was triggered. Can be sued to activate action only if input value is higher than specified number.
/// </summary>
API_FIELD(Attributes = "EditorOrder(50)")
float DeadZone = 0.5f;
};
/// <summary>

View File

@@ -5,6 +5,7 @@
#include "Level.h"
#include "SceneQuery.h"
#include "SceneObjectsFactory.h"
#include "FlaxEngine.Gen.h"
#include "Scene/Scene.h"
#include "Prefabs/Prefab.h"
#include "Prefabs/PrefabManager.h"
@@ -178,21 +179,20 @@ void Actor::OnDeleteObject()
_scene = nullptr;
}
}
else if (_parent)
else
{
// Unlink from the parent
_parent->Children.RemoveKeepOrder(this);
_parent->_isHierarchyDirty = true;
_parent = nullptr;
_scene = nullptr;
if (_isEnabled)
OnDisable();
if (_parent)
{
// Unlink from the parent
_parent->Children.RemoveKeepOrder(this);
_parent->_isHierarchyDirty = true;
_parent = nullptr;
_scene = nullptr;
}
}
// Ensure to exit gameplay in a valid way
ASSERT(!IsDuringPlay());
#if BUILD_DEBUG || BUILD_DEVELOPMENT
ASSERT(!_isEnabled);
#endif
// Fire event
Deleted(this);

View File

@@ -1,6 +1,7 @@
// Copyright (c) Wojciech Figat. All rights reserved.
#include "PointLight.h"
#include "Engine/Content/Deprecated.h"
#include "Engine/Graphics/RenderTask.h"
#include "Engine/Graphics/RenderTools.h"
#include "Engine/Graphics/RenderView.h"
@@ -196,6 +197,14 @@ void PointLight::Deserialize(DeserializeStream& stream, ISerializeModifier* modi
DESERIALIZE(UseInverseSquaredFalloff);
DESERIALIZE(UseIESBrightness);
DESERIALIZE(IESBrightnessScale);
// [Deprecated on 12.03.2026, expires on 12.03.2028]
if (modifier->EngineBuild <= 6807 && SERIALIZE_FIND_MEMBER(stream, "UseInverseSquaredFalloff") != stream.MemberEnd() && UseInverseSquaredFalloff)
{
// Convert old non-physical brightness value that was used for Inverse Squared Falloff which wasn't based on proper cm/m units calculations
MARK_CONTENT_DEPRECATED();
Brightness = Math::Sqrt(Brightness * 0.01f);
}
}
bool PointLight::IntersectsItself(const Ray& ray, Real& distance, Vector3& normal)

View File

@@ -1,6 +1,8 @@
// Copyright (c) Wojciech Figat. All rights reserved.
#include "SpotLight.h"
#include "Engine/Content/Deprecated.h"
#include "Engine/Graphics/RenderView.h"
#include "Engine/Renderer/RenderList.h"
#include "Engine/Content/Assets/IESProfile.h"
@@ -282,6 +284,14 @@ void SpotLight::Deserialize(DeserializeStream& stream, ISerializeModifier* modif
DESERIALIZE(UseInverseSquaredFalloff);
DESERIALIZE(UseIESBrightness);
DESERIALIZE(IESBrightnessScale);
// [Deprecated on 12.03.2026, expires on 12.03.2028]
if (modifier->EngineBuild <= 6807 && SERIALIZE_FIND_MEMBER(stream, "UseInverseSquaredFalloff") != stream.MemberEnd() && UseInverseSquaredFalloff)
{
// Convert old non-physical brightness value that was used for Inverse Squared Falloff which wasn't based on proper cm/m units calculations
MARK_CONTENT_DEPRECATED();
Brightness = Math::Sqrt(Brightness * 0.01f);
}
}
bool SpotLight::IntersectsItself(const Ray& ray, Real& distance, Vector3& normal)

View File

@@ -66,6 +66,20 @@ void StaticModel::SetBoundsScale(float value)
UpdateBounds();
}
DrawPass StaticModel::GetDrawModes() const
{
return _drawModes;
}
void StaticModel::SetDrawModes(DrawPass value)
{
if (_drawModes == value)
return;
_drawModes = value;
if (_sceneRenderingKey != -1)
GetSceneRendering()->UpdateActor(this, _sceneRenderingKey, ISceneRenderingListener::DrawModes);
}
int32 StaticModel::GetLODBias() const
{
return _lodBias;
@@ -330,13 +344,13 @@ void StaticModel::Draw(RenderContext& renderContext)
return;
if (renderContext.View.Pass == DrawPass::GlobalSDF)
{
if (EnumHasAnyFlags(DrawModes, DrawPass::GlobalSDF) && Model->SDF.Texture)
if (EnumHasAnyFlags(_drawModes, DrawPass::GlobalSDF) && Model->SDF.Texture)
GlobalSignDistanceFieldPass::Instance()->RasterizeModelSDF(this, Model->SDF, _transform, _box);
return;
}
if (renderContext.View.Pass == DrawPass::GlobalSurfaceAtlas)
{
if (EnumHasAnyFlags(DrawModes, DrawPass::GlobalSurfaceAtlas) && Model->SDF.Texture)
if (EnumHasAnyFlags(_drawModes, DrawPass::GlobalSurfaceAtlas) && Model->SDF.Texture)
GlobalSurfaceAtlasPass::Instance()->RasterizeActor(this, this, _sphere, _transform, Model->LODs.Last().GetBox());
return;
}
@@ -353,7 +367,7 @@ void StaticModel::Draw(RenderContext& renderContext)
draw.Lightmap = _scene ? _scene->LightmapsData.GetReadyLightmap(Lightmap.TextureIndex) : nullptr;
draw.LightmapUVs = &Lightmap.UVsArea;
draw.Flags = _staticFlags;
draw.DrawModes = DrawModes;
draw.DrawModes = _drawModes;
draw.Bounds = _sphere;
draw.Bounds.Center -= renderContext.View.Origin;
draw.PerInstanceRandom = GetPerInstanceRandom();
@@ -390,7 +404,7 @@ void StaticModel::Draw(RenderContextBatch& renderContextBatch)
draw.Lightmap = _scene ? _scene->LightmapsData.GetReadyLightmap(Lightmap.TextureIndex) : nullptr;
draw.LightmapUVs = &Lightmap.UVsArea;
draw.Flags = _staticFlags;
draw.DrawModes = DrawModes;
draw.DrawModes = _drawModes;
draw.Bounds = _sphere;
draw.Bounds.Center -= renderContext.View.Origin;
draw.PerInstanceRandom = GetPerInstanceRandom();
@@ -435,7 +449,7 @@ void StaticModel::Serialize(SerializeStream& stream, const void* otherObj)
SERIALIZE_MEMBER(LODBias, _lodBias);
SERIALIZE_MEMBER(ForcedLOD, _forcedLod);
SERIALIZE_MEMBER(SortOrder, _sortOrder);
SERIALIZE(DrawModes);
SERIALIZE_MEMBER(DrawModes, _drawModes);
if (HasLightmap()
#if USE_EDITOR
@@ -487,7 +501,7 @@ void StaticModel::Deserialize(DeserializeStream& stream, ISerializeModifier* mod
DESERIALIZE_MEMBER(LODBias, _lodBias);
DESERIALIZE_MEMBER(ForcedLOD, _forcedLod);
DESERIALIZE_MEMBER(SortOrder, _sortOrder);
DESERIALIZE(DrawModes);
DESERIALIZE_MEMBER(DrawModes, _drawModes);
DESERIALIZE_MEMBER(LightmapIndex, Lightmap.TextureIndex);
DESERIALIZE_MEMBER(LightmapArea, Lightmap.UVsArea);
@@ -537,27 +551,27 @@ void StaticModel::Deserialize(DeserializeStream& stream, ISerializeModifier* mod
if (member != stream.MemberEnd() && member->value.IsBool() && member->value.GetBool())
{
MARK_CONTENT_DEPRECATED();
DrawModes = DrawPass::Depth;
_drawModes = DrawPass::Depth;
}
}
// [Deprecated on 07.02.2022, expires on 07.02.2024]
if (modifier->EngineBuild <= 6330)
{
MARK_CONTENT_DEPRECATED();
DrawModes |= DrawPass::GlobalSDF;
_drawModes |= DrawPass::GlobalSDF;
}
// [Deprecated on 27.04.2022, expires on 27.04.2024]
if (modifier->EngineBuild <= 6331)
{
MARK_CONTENT_DEPRECATED();
DrawModes |= DrawPass::GlobalSurfaceAtlas;
_drawModes |= DrawPass::GlobalSurfaceAtlas;
}
{
const auto member = stream.FindMember("RenderPasses");
if (member != stream.MemberEnd() && member->value.IsInt())
{
DrawModes = (DrawPass)member->value.GetInt();
_drawModes = (DrawPass)member->value.GetInt();
}
}
}

View File

@@ -23,6 +23,7 @@ private:
bool _vertexColorsDirty;
byte _vertexColorsCount;
int8 _sortOrder;
DrawPass _drawModes = DrawPass::Default;
Array<Color32> _vertexColorsData[MODEL_MAX_LODS];
GPUBuffer* _vertexColorsBuffer[MODEL_MAX_LODS];
Model* _residencyChangedModel = nullptr;
@@ -40,12 +41,6 @@ public:
API_FIELD(Attributes="EditorOrder(20), DefaultValue(null), EditorDisplay(\"Model\")")
AssetReference<Model> Model;
/// <summary>
/// The draw passes to use for rendering this object.
/// </summary>
API_FIELD(Attributes="EditorOrder(15), DefaultValue(DrawPass.Default), EditorDisplay(\"Model\")")
DrawPass DrawModes = DrawPass::Default;
/// <summary>
/// The baked lightmap entry.
/// </summary>
@@ -74,6 +69,17 @@ public:
/// </summary>
API_PROPERTY() void SetBoundsScale(float value);
/// <summary>
/// Gets the draw passes to use for rendering this object.
/// </summary>
API_PROPERTY(Attributes="EditorOrder(15), DefaultValue(DrawPass.Default), EditorDisplay(\"Model\")")
DrawPass GetDrawModes() const;
/// <summary>
/// Sets the draw passes to use for rendering this object.
/// </summary>
API_PROPERTY() void SetDrawModes(DrawPass value);
/// <summary>
/// Gets the model Level Of Detail bias value. Allows to increase or decrease rendered model quality.
/// </summary>

View File

@@ -5,6 +5,7 @@
#include "LargeWorlds.h"
#include "SceneQuery.h"
#include "SceneObjectsFactory.h"
#include "FlaxEngine.Gen.h"
#include "Scene/Scene.h"
#include "Engine/Content/Content.h"
#include "Engine/Content/Deprecated.h"
@@ -960,9 +961,6 @@ bool LevelImpl::unloadScene(Scene* scene)
// Simple enqueue scene root object to be deleted
scene->DeleteObject();
// Force flush deleted objects so we actually delete unloaded scene objects (prevent from reloading their managed objects, etc.)
ObjectsRemovalService::Flush();
return false;
}
@@ -1125,6 +1123,32 @@ SceneResult SceneLoader::OnBegin(Args& args)
_lastSceneLoadTime = DateTime::Now();
StartFrame = Engine::UpdateCount;
// Validate arguments
if (!args.Data.IsArray())
{
LOG(Error, "Invalid Data member.");
CallSceneEvent(SceneEventType::OnSceneLoadError, nullptr, Guid::Empty);
return SceneResult::Failed;
}
// Peek scene node value (it's the first actor serialized)
SceneId = JsonTools::GetGuid(args.Data[0], "ID");
if (!SceneId.IsValid())
{
LOG(Error, "Invalid scene id.");
CallSceneEvent(SceneEventType::OnSceneLoadError, nullptr, SceneId);
return SceneResult::Failed;
}
// Peek meta
if (args.EngineBuild < 6000)
{
LOG(Error, "Invalid serialized engine build.");
CallSceneEvent(SceneEventType::OnSceneLoadError, nullptr, SceneId);
return SceneResult::Failed;
}
Modifier->EngineBuild = args.EngineBuild;
// Scripting backend should be loaded for the current project before loading scene
if (!Scripting::HasGameModulesLoaded())
{
@@ -1138,27 +1162,7 @@ SceneResult SceneLoader::OnBegin(Args& args)
MessageBox::Show(TEXT("Failed to load scripts.\n\nCannot load scene without game script modules.\n\nSee logs for more info."), TEXT("Missing game modules"), MessageBoxButtons::OK, MessageBoxIcon::Error);
}
#endif
return SceneResult::Failed;
}
// Peek meta
if (args.EngineBuild < 6000)
{
LOG(Error, "Invalid serialized engine build.");
return SceneResult::Failed;
}
if (!args.Data.IsArray())
{
LOG(Error, "Invalid Data member.");
return SceneResult::Failed;
}
Modifier->EngineBuild = args.EngineBuild;
// Peek scene node value (it's the first actor serialized)
SceneId = JsonTools::GetGuid(args.Data[0], "ID");
if (!SceneId.IsValid())
{
LOG(Error, "Invalid scene id.");
CallSceneEvent(SceneEventType::OnSceneLoadError, nullptr, SceneId);
return SceneResult::Failed;
}
@@ -1166,6 +1170,7 @@ SceneResult SceneLoader::OnBegin(Args& args)
if (Level::FindScene(SceneId) != nullptr)
{
LOG(Info, "Scene {0} is already loaded.", SceneId);
CallSceneEvent(SceneEventType::OnSceneLoadError, nullptr, SceneId);
return SceneResult::Failed;
}

View File

@@ -389,11 +389,14 @@ void Scene::BeginPlay(SceneBeginData* data)
if (model == nullptr)
CreateCsgModel();
}
Ticking.SetTicking(true);
}
void Scene::EndPlay()
{
// Improve scene cleanup performance by removing all data from scene rendering and ticking containers
Ticking.SetTicking(false);
Ticking.Clear();
Rendering.Clear();
Navigation.Clear();

View File

@@ -56,6 +56,7 @@ public:
Layer = 4,
StaticFlags = 8,
AutoDelayDuringRendering = 16, // Conditionally allow updating data during rendering when writes are locked
DrawModes = 32,
Auto = Visual | Bounds | Layer,
};

View File

@@ -44,7 +44,7 @@ void SceneTicking::TickData::Tick()
{
TickScripts(Scripts);
for (int32 i = 0; i < Ticks.Count(); i++)
for (int32 i = 0; i < Ticks.Count() && _canTick; i++)
Ticks.Get()[i].Call();
}
@@ -66,7 +66,7 @@ void SceneTicking::TickData::TickExecuteInEditor()
{
TickScripts(ScriptsExecuteInEditor);
for (int32 i = 0; i < TicksExecuteInEditor.Count(); i++)
for (int32 i = 0; i < TicksExecuteInEditor.Count() && _canTick; i++)
TicksExecuteInEditor.Get()[i].Call();
}
@@ -89,10 +89,8 @@ SceneTicking::FixedUpdateTickData::FixedUpdateTickData()
void SceneTicking::FixedUpdateTickData::TickScripts(const Array<Script*>& scripts)
{
for (auto* script : scripts)
{
script->OnFixedUpdate();
}
for (int32 i = 0; i < scripts.Count() && _canTick; i++)
scripts.Get()[i]->OnFixedUpdate();
}
SceneTicking::UpdateTickData::UpdateTickData()
@@ -102,36 +100,30 @@ SceneTicking::UpdateTickData::UpdateTickData()
void SceneTicking::UpdateTickData::TickScripts(const Array<Script*>& scripts)
{
for (auto* script : scripts)
{
script->OnUpdate();
}
for (int32 i = 0; i < scripts.Count() && _canTick; i++)
scripts.Get()[i]->OnUpdate();
}
SceneTicking::LateUpdateTickData::LateUpdateTickData()
: TickData(64)
: TickData(0)
{
}
void SceneTicking::LateUpdateTickData::TickScripts(const Array<Script*>& scripts)
{
for (auto* script : scripts)
{
script->OnLateUpdate();
}
for (int32 i = 0; i < scripts.Count() && _canTick; i++)
scripts.Get()[i]->OnLateUpdate();
}
SceneTicking::LateFixedUpdateTickData::LateFixedUpdateTickData()
: TickData(64)
: TickData(0)
{
}
void SceneTicking::LateFixedUpdateTickData::TickScripts(const Array<Script*>& scripts)
{
for (auto* script : scripts)
{
script->OnLateFixedUpdate();
}
for (int32 i = 0; i < scripts.Count() && _canTick; i++)
scripts.Get()[i]->OnLateFixedUpdate();
}
void SceneTicking::AddScript(Script* obj)
@@ -167,3 +159,11 @@ void SceneTicking::Clear()
LateUpdate.Clear();
LateFixedUpdate.Clear();
}
void SceneTicking::SetTicking(bool enable)
{
FixedUpdate._canTick = enable;
Update._canTick = enable;
LateUpdate._canTick = enable;
LateFixedUpdate._canTick = enable;
}

View File

@@ -46,6 +46,9 @@ public:
/// </summary>
class FLAXENGINE_API TickData
{
protected:
friend SceneTicking;
bool _canTick = true;
public:
Array<Script*> Scripts;
Array<Tick> Ticks;
@@ -134,6 +137,11 @@ public:
/// </summary>
void Clear();
/// <summary>
/// Changes the ability to tick. When disabled, the ticking functions won't be called. Can be called during ticking (eg. when scene is unloaded) to stp performing any more ticks.
/// </summary>
void SetTicking(bool enable);
public:
/// <summary>
/// The fixed update tick function.

View File

@@ -83,17 +83,19 @@ void AppleFileSystem::GetSpecialFolderPath(const SpecialFolder type, String& res
switch (type)
{
case SpecialFolder::Desktop:
result = home / TEXT("/Desktop");
result = home / TEXT("/Desktop"); // TODO: should be NSDesktopDirectory
break;
case SpecialFolder::Documents:
result = home / TEXT("/Documents");
result = home / TEXT("/Documents"); // TODO: should be NSDocumentDirectory
break;
case SpecialFolder::Pictures:
result = home / TEXT("/Pictures");
result = home / TEXT("/Pictures"); // TODO: should be NSPicturesDirectory
break;
case SpecialFolder::AppData:
result = home / TEXT("/Library/Application Support"); // TODO: should be NSApplicationSupportDirectory
break;
case SpecialFolder::LocalAppData:
result = home / TEXT("/Library/Caches");
result = home / TEXT("/Library/Caches"); // TODO: should be NSApplicationSupportDirectory
break;
case SpecialFolder::ProgramData:
result = home / TEXT("/Library/Application Support");

View File

@@ -338,17 +338,35 @@ void LinuxFileSystem::GetSpecialFolderPath(const SpecialFolder type, String& res
switch (type)
{
case SpecialFolder::Desktop:
result = home / TEXT("Desktop");
{
String desktopDir;
if (!Platform::GetEnvironmentVariable(TEXT("XDG_DESKTOP_DIR"), desktopDir))
result = desktopDir;
else
result = home / TEXT("Desktop");
break;
}
case SpecialFolder::Documents:
result = String::Empty;
break;
case SpecialFolder::Pictures:
result = home / TEXT("Pictures");
{
String picturesDir;
if (!Platform::GetEnvironmentVariable(TEXT("XDG_PICTURES_DIR"), picturesDir))
result = picturesDir;
else
result = home / TEXT("Pictures");
break;
}
case SpecialFolder::AppData:
result = TEXT("/usr/share");
{
String configHome;
if (!Platform::GetEnvironmentVariable(TEXT("XDG_CONFIG_HOME"), configHome))
result = configHome;
else
result = home / TEXT(".config");
break;
}
case SpecialFolder::LocalAppData:
{
String dataHome;

View File

@@ -1,5 +1,7 @@
// Copyright (c) Wojciech Figat. All rights reserved.
using System;
namespace FlaxEngine
{
partial struct FontOptions
@@ -11,7 +13,7 @@ namespace FlaxEngine
/// <returns><c>true</c> if this object has the same value as <paramref name="other" />; otherwise, <c>false</c> </returns>
public bool Equals(FontOptions other)
{
return Hinting == other.Hinting && Flags == other.Flags;
return Hinting == other.Hinting && Flags == other.Flags && RasterMode == other.RasterMode;
}
/// <inheritdoc />
@@ -23,10 +25,7 @@ namespace FlaxEngine
/// <inheritdoc />
public override int GetHashCode()
{
unchecked
{
return ((int)Hinting * 397) ^ (int)Flags;
}
return HashCode.Combine((int)Hinting, (int)Flags, (int)RasterMode);
}
/// <summary>
@@ -37,7 +36,7 @@ namespace FlaxEngine
/// <returns><c>true</c> if <paramref name="left" /> has the same value as <paramref name="right" />; otherwise, <c>false</c>.</returns>
public static bool operator ==(FontOptions left, FontOptions right)
{
return left.Hinting == right.Hinting && left.Flags == right.Flags;
return left.Hinting == right.Hinting && left.Flags == right.Flags && left.RasterMode == right.RasterMode;
}
/// <summary>
@@ -48,7 +47,7 @@ namespace FlaxEngine
/// <returns><c>true</c> if <paramref name="left" /> has a different value than <paramref name="right" />; otherwise,<c>false</c>.</returns>
public static bool operator !=(FontOptions left, FontOptions right)
{
return left.Hinting != right.Hinting || left.Flags != right.Flags;
return left.Hinting != right.Hinting || left.Flags != right.Flags || left.RasterMode != right.RasterMode;
}
}
}

View File

@@ -66,6 +66,22 @@ API_ENUM(Attributes="Flags") enum class FontFlags : byte
Italic = 4,
};
/// <summary>
/// The font rasterization mode.
/// </summary>
API_ENUM() enum class FontRasterMode : byte
{
/// <summary>
/// Use the default FreeType rasterizer to render font atlases.
/// </summary>
Bitmap,
/// <summary>
/// Use the Multi-channel Signed Distance Field (MSDF) generator to render font atlases. Need to be rendered with a compatible material.
/// </summary>
MSDF,
};
DECLARE_ENUM_OPERATORS(FontFlags);
/// <summary>
@@ -76,7 +92,7 @@ API_STRUCT() struct FontOptions
DECLARE_SCRIPTING_TYPE_MINIMAL(FontOptions);
/// <summary>
/// The hinting.
/// The font hinting used when rendering characters.
/// </summary>
API_FIELD() FontHinting Hinting;
@@ -84,6 +100,11 @@ API_STRUCT() struct FontOptions
/// The flags.
/// </summary>
API_FIELD() FontFlags Flags;
/// <summary>
/// The font rasterization mode.
/// </summary>
API_FIELD() FontRasterMode RasterMode;
};
/// <summary>
@@ -91,7 +112,7 @@ API_STRUCT() struct FontOptions
/// </summary>
API_CLASS(NoSpawn) class FLAXENGINE_API FontAsset : public BinaryAsset
{
DECLARE_BINARY_ASSET_HEADER(FontAsset, 3);
DECLARE_BINARY_ASSET_HEADER(FontAsset, 4);
friend Font;
private:

View File

@@ -9,6 +9,7 @@
#include "Engine/Content/Content.h"
#include "Engine/Engine/EngineService.h"
#include "Engine/Threading/Threading.h"
#include "Engine/Render2D/MSDFGenerator.h"
#include "IncludeFreeType.h"
#include <ThirdParty/freetype/ftsynth.h>
#include <ThirdParty/freetype/ftbitmap.h>
@@ -125,7 +126,11 @@ bool FontManager::AddNewEntry(Font* font, Char c, FontCharacterEntry& entry)
// Set load flags
uint32 glyphFlags = FT_LOAD_NO_BITMAP;
const bool useAA = EnumHasAnyFlags(options.Flags, FontFlags::AntiAliasing);
if (useAA)
if (options.RasterMode == FontRasterMode::MSDF)
{
glyphFlags |= FT_LOAD_NO_AUTOHINT | FT_LOAD_NO_HINTING;
}
else if (useAA)
{
switch (options.Hinting)
{
@@ -185,75 +190,109 @@ bool FontManager::AddNewEntry(Font* font, Char c, FontCharacterEntry& entry)
FT_GlyphSlot_Oblique(face->glyph);
}
// Render glyph to the bitmap
FT_GlyphSlot glyph = face->glyph;
FT_Render_Glyph(glyph, useAA ? FT_RENDER_MODE_NORMAL : FT_RENDER_MODE_MONO);
FT_Bitmap* bitmap = &glyph->bitmap;
FT_Bitmap tmpBitmap;
if (bitmap->pixel_mode != FT_PIXEL_MODE_GRAY)
int32 glyphWidth = 0;
int32 glyphHeight = 0;
if (options.RasterMode == FontRasterMode::Bitmap)
{
// Convert the bitmap to 8bpp grayscale
FT_Bitmap_New(&tmpBitmap);
FT_Bitmap_Convert(Library, bitmap, &tmpBitmap, 4);
bitmap = &tmpBitmap;
}
ASSERT(bitmap && bitmap->pixel_mode == FT_PIXEL_MODE_GRAY);
// Render glyph to the bitmap
FT_GlyphSlot glyph = face->glyph;
FT_Render_Glyph(glyph, useAA ? FT_RENDER_MODE_NORMAL : FT_RENDER_MODE_MONO);
// Fill the character data
entry.AdvanceX = Convert26Dot6ToRoundedPixel<int16>(glyph->advance.x);
entry.OffsetY = glyph->bitmap_top;
entry.OffsetX = glyph->bitmap_left;
entry.IsValid = true;
entry.BearingY = Convert26Dot6ToRoundedPixel<int16>(glyph->metrics.horiBearingY);
entry.Height = Convert26Dot6ToRoundedPixel<int16>(glyph->metrics.height);
FT_Bitmap* bitmap = &glyph->bitmap;
FT_Bitmap tmpBitmap;
if (bitmap->pixel_mode != FT_PIXEL_MODE_GRAY)
{
// Convert the bitmap to 8bpp grayscale
FT_Bitmap_New(&tmpBitmap);
FT_Bitmap_Convert(Library, bitmap, &tmpBitmap, 4);
bitmap = &tmpBitmap;
}
ASSERT(bitmap && bitmap->pixel_mode == FT_PIXEL_MODE_GRAY);
// Allocate memory
const int32 glyphWidth = bitmap->width;
const int32 glyphHeight = bitmap->rows;
GlyphImageData.Clear();
GlyphImageData.Resize(glyphWidth * glyphHeight);
// Fill the character data
entry.AdvanceX = Convert26Dot6ToRoundedPixel<int16>(glyph->advance.x);
entry.OffsetY = glyph->bitmap_top;
entry.OffsetX = glyph->bitmap_left;
entry.IsValid = true;
entry.BearingY = Convert26Dot6ToRoundedPixel<int16>(glyph->metrics.horiBearingY);
entry.Height = Convert26Dot6ToRoundedPixel<int16>(glyph->metrics.height);
// End for empty glyphs
if (GlyphImageData.IsEmpty())
{
entry.TextureIndex = MAX_uint8;
// Allocate memory
glyphWidth = bitmap->width;
glyphHeight = bitmap->rows;
GlyphImageData.Clear();
GlyphImageData.Resize(glyphWidth * glyphHeight);
// End for empty glyphs
if (GlyphImageData.IsEmpty())
{
entry.TextureIndex = MAX_uint8;
if (bitmap == &tmpBitmap)
{
FT_Bitmap_Done(Library, bitmap);
bitmap = nullptr;
}
return false;
}
// Copy glyph data after rasterization (row by row)
for (int32 row = 0; row < glyphHeight; row++)
{
Platform::MemoryCopy(&GlyphImageData[row * glyphWidth], &bitmap->buffer[row * bitmap->pitch], glyphWidth);
}
// Normalize gray scale images not using 256 colors
if (bitmap->num_grays != 256)
{
const int32 scale = 255 / (bitmap->num_grays - 1);
for (byte& pixel : GlyphImageData)
pixel *= scale;
}
// Free temporary bitmap if used
if (bitmap == &tmpBitmap)
{
FT_Bitmap_Done(Library, bitmap);
bitmap = nullptr;
}
return false;
}
// Copy glyph data after rasterization (row by row)
for (int32 row = 0; row < glyphHeight; row++)
else
{
Platform::MemoryCopy(&GlyphImageData[row * glyphWidth], &bitmap->buffer[row * bitmap->pitch], glyphWidth);
}
// Generate bitmap for MSDF
FT_GlyphSlot glyph = face->glyph;
// Normalize gray scale images not using 256 colors
if (bitmap->num_grays != 256)
{
const int32 scale = 255 / (bitmap->num_grays - 1);
for (byte& pixel : GlyphImageData)
// Set advance in advance
entry.AdvanceX = Convert26Dot6ToRoundedPixel<int16>(glyph->advance.x);
int16 msdf_top = 0;
int16 msdf_left = 0;
MSDFGenerator::GenerateMSDF(glyph, GlyphImageData, glyphWidth, glyphHeight, msdf_top, msdf_left);
// End for empty glyphs
if (GlyphImageData.IsEmpty())
{
pixel *= scale;
entry.TextureIndex = MAX_uint8;
return false;
}
}
// Free temporary bitmap if used
if (bitmap == &tmpBitmap)
{
FT_Bitmap_Done(Library, bitmap);
bitmap = nullptr;
// Fill the remaining character data
entry.OffsetY = msdf_top;
entry.OffsetX = msdf_left;
entry.IsValid = true;
entry.BearingY = Convert26Dot6ToRoundedPixel<int16>(glyph->metrics.horiBearingY);
entry.Height = Convert26Dot6ToRoundedPixel<int16>(glyph->metrics.height);
}
// Find atlas for the character texture
PixelFormat atlasFormat = options.RasterMode == FontRasterMode::MSDF ? PixelFormat::R8G8B8A8_UNorm : PixelFormat::R8_UNorm;
int32 atlasIndex = 0;
const FontTextureAtlasSlot* slot = nullptr;
for (; atlasIndex < Atlases.Count() && slot == nullptr; atlasIndex++)
{
if (Atlases[atlasIndex]->GetFormat() != atlasFormat)
continue;
slot = Atlases[atlasIndex]->AddEntry(glyphWidth, glyphHeight, GlyphImageData);
}
atlasIndex--;
// Check if there is no atlas for this character
@@ -261,7 +300,7 @@ bool FontManager::AddNewEntry(Font* font, Char c, FontCharacterEntry& entry)
{
// Create new atlas
auto atlas = Content::CreateVirtualAsset<FontTextureAtlas>();
atlas->Setup(PixelFormat::R8_UNorm, FontTextureAtlas::PaddingStyle::PadWithZero);
atlas->Setup(atlasFormat, FontTextureAtlas::PaddingStyle::PadWithZero);
Atlases.Add(atlas);
atlasIndex++;

View File

@@ -105,6 +105,14 @@ public:
return _height;
}
/// <summary>
/// Gets the atlas pixel format.
/// </summary>
FORCE_INLINE PixelFormat GetFormat() const
{
return _format;
}
/// <summary>
/// Gets the atlas size.
/// </summary>
@@ -186,8 +194,8 @@ public:
/// <summary>
/// Returns glyph's bitmap data of the slot.
/// </summary>
/// <param name="slot">The slot in atlas.</param>
/// <param name="width">The width of the slot.</param>
/// <param name="slot">The slot in atlas.</param>
/// <param name="width">The width of the slot.</param>
/// <param name="height">The height of the slot.</param>
/// <param name="stride">The stride of the slot.</param>
/// <returns>The pointer to the bitmap data of the given slot.</returns>

View File

@@ -0,0 +1,142 @@
// Copyright (c) Wojciech Figat. All rights reserved.
#pragma once
#include "Engine/Core/Collections/Array.h"
#include "Engine/Core/Math/Math.h"
#include <ThirdParty/freetype/ftoutln.h>
#include <ThirdParty/msdfgen/msdfgen.h>
#include <ThirdParty/msdfgen/msdfgen/core/ShapeDistanceFinder.h>
class MSDFGenerator
{
using Point2 = msdfgen::Point2;
using Shape = msdfgen::Shape;
using Contour = msdfgen::Contour;
using EdgeHolder = msdfgen::EdgeHolder;
static Point2 ftPoint2(const FT_Vector& vector, double scale)
{
return Point2(scale * vector.x, scale * vector.y);
}
struct FtContext
{
double scale;
Point2 position;
Shape* shape;
Contour* contour;
};
static int ftMoveTo(const FT_Vector* to, void* user)
{
FtContext* context = reinterpret_cast<FtContext*>(user);
if (!(context->contour && context->contour->edges.empty()))
context->contour = &context->shape->addContour();
context->position = ftPoint2(*to, context->scale);
return 0;
}
static int ftLineTo(const FT_Vector* to, void* user)
{
FtContext* context = reinterpret_cast<FtContext*>(user);
Point2 endpoint = ftPoint2(*to, context->scale);
if (endpoint != context->position)
{
context->contour->addEdge(EdgeHolder(context->position, endpoint));
context->position = endpoint;
}
return 0;
}
static int ftConicTo(const FT_Vector* control, const FT_Vector* to, void* user)
{
FtContext* context = reinterpret_cast<FtContext*>(user);
Point2 endpoint = ftPoint2(*to, context->scale);
if (endpoint != context->position)
{
context->contour->addEdge(EdgeHolder(context->position, ftPoint2(*control, context->scale), endpoint));
context->position = endpoint;
}
return 0;
}
static int ftCubicTo(const FT_Vector* control1, const FT_Vector* control2, const FT_Vector* to, void* user)
{
FtContext* context = reinterpret_cast<FtContext*>(user);
Point2 endpoint = ftPoint2(*to, context->scale);
if (endpoint != context->position || msdfgen::crossProduct(ftPoint2(*control1, context->scale) - endpoint, ftPoint2(*control2, context->scale) - endpoint))
{
context->contour->addEdge(EdgeHolder(context->position, ftPoint2(*control1, context->scale), ftPoint2(*control2, context->scale), endpoint));
context->position = endpoint;
}
return 0;
}
static void correctWinding(Shape& shape, Shape::Bounds& bounds)
{
Point2 p(bounds.l - (bounds.r - bounds.l) - 1, bounds.b - (bounds.t - bounds.b) - 1);
double distance = msdfgen::SimpleTrueShapeDistanceFinder::oneShotDistance(shape, p);
if (distance > 0)
{
for (auto& contour : shape.contours)
contour.reverse();
}
}
public:
static void GenerateMSDF(FT_GlyphSlot glyph, Array<byte>& output, int32& outputWidth, int32& outputHeight, int16& top, int16& left)
{
Shape shape;
shape.contours.clear();
shape.inverseYAxis = true;
FtContext context = { };
context.scale = 1.0 / 64.0;
context.shape = &shape;
FT_Outline_Funcs ftFunctions;
ftFunctions.move_to = &ftMoveTo;
ftFunctions.line_to = &ftLineTo;
ftFunctions.conic_to = &ftConicTo;
ftFunctions.cubic_to = &ftCubicTo;
ftFunctions.shift = 0;
ftFunctions.delta = 0;
FT_Outline_Decompose(&glyph->outline, &ftFunctions, &context);
shape.normalize();
edgeColoringSimple(shape, 3.0);
// Todo: make this configurable
// Also hard-coded in material: MSDFFontMaterial
const double pxRange = 4.0;
Shape::Bounds bounds = shape.getBounds();
int32 width = static_cast<int32>(Math::CeilToInt(bounds.r - bounds.l + pxRange));
int32 height = static_cast<int32>(Math::CeilToInt(bounds.t - bounds.b + pxRange));
msdfgen::Bitmap<float, 4> msdf(width, height);
auto transform = msdfgen::Vector2(Math::Ceil(-bounds.l + pxRange / 2.0), Math::Ceil(-bounds.b + pxRange / 2.0));
msdfgen::SDFTransformation t(msdfgen::Projection(1.0, transform), msdfgen::Range(pxRange));
correctWinding(shape, bounds);
generateMTSDF(msdf, shape, t);
output.Resize(width * height * 4);
const msdfgen::BitmapConstRef<float, 4>& bitmap = msdf;
for (int y = 0; y < height; ++y)
{
for (int x = 0; x < width; ++x)
{
output[(y * width + x) * 4] = msdfgen::pixelFloatToByte(bitmap(x, y)[0]);
output[(y * width + x) * 4 + 1] = msdfgen::pixelFloatToByte(bitmap(x, y)[1]);
output[(y * width + x) * 4 + 2] = msdfgen::pixelFloatToByte(bitmap(x, y)[2]);
output[(y * width + x) * 4 + 3] = msdfgen::pixelFloatToByte(bitmap(x, y)[3]);
}
}
outputWidth = width;
outputHeight = height;
top = height - static_cast<int16>(transform.y);
left = -static_cast<int16>(transform.x);
}
};

View File

@@ -14,6 +14,7 @@ public class Render2D : EngineModule
base.Setup(options);
options.PrivateDependencies.Add("freetype");
options.PrivateDependencies.Add("msdfgen");
options.PrivateDefinitions.Add("RENDER2D_USE_LINE_AA");
}

View File

@@ -2,6 +2,7 @@
#include "Render2D.h"
#include "Font.h"
#include "FontAsset.h"
#include "FontManager.h"
#include "FontTextureAtlas.h"
#include "RotatedRectangle.h"
@@ -79,6 +80,7 @@ enum class DrawCallType : byte
FillTexture,
FillTexturePoint,
DrawChar,
DrawCharMSDF,
DrawCharMaterial,
Custom,
Material,
@@ -169,6 +171,7 @@ struct CachedPSO
GPUPipelineState* PS_Color_NoAlpha;
GPUPipelineState* PS_Font;
GPUPipelineState* PS_FontMSDF;
GPUPipelineState* PS_BlurH;
GPUPipelineState* PS_BlurV;
@@ -461,6 +464,7 @@ CanDrawCallCallback CanDrawCallBatch[] =
CanDrawCallCallbackTexture, // FillTexture,
CanDrawCallCallbackTexture, // FillTexturePoint,
CanDrawCallCallbackChar, // DrawChar,
CanDrawCallCallbackChar, // DrawCharMSDF,
CanDrawCallCallbackCharMaterial, // DrawCharMaterial,
CanDrawCallCallbackFalse, // Custom,
CanDrawCallCallbackMaterial, // Material,
@@ -523,6 +527,12 @@ bool CachedPSO::Init(GPUShader* shader, bool useDepth)
if (PS_Font->Init(desc))
return true;
//
desc.BlendMode = BlendingMode::AlphaBlend;
desc.PS = shader->GetPS("PS_FontMSDF");
PS_FontMSDF = GPUDevice::Instance->CreatePipelineState();
if (PS_FontMSDF->Init(desc))
return true;
//
desc.PS = shader->GetPS("PS_LineAA");
PS_LineAA = GPUDevice::Instance->CreatePipelineState();
if (PS_LineAA->Init(desc))
@@ -1001,6 +1011,10 @@ void DrawBatch(int32 startIndex, int32 count)
Context->BindSR(0, d.AsChar.Tex);
Context->SetState(CurrentPso->PS_Font);
break;
case DrawCallType::DrawCharMSDF:
Context->BindSR(0, d.AsChar.Tex);
Context->SetState(CurrentPso->PS_FontMSDF);
break;
case DrawCallType::DrawCharMaterial:
{
// Apply and bind material
@@ -1193,7 +1207,7 @@ void Render2D::DrawText(Font* font, const StringView& text, const Color& color,
}
else
{
drawCall.Type = DrawCallType::DrawChar;
drawCall.Type = font->GetAsset()->GetOptions().RasterMode == FontRasterMode::MSDF ? DrawCallType::DrawCharMSDF : DrawCallType::DrawChar;
drawCall.AsChar.Mat = nullptr;
}
Float2 pointer = location;
@@ -1312,7 +1326,7 @@ void Render2D::DrawText(Font* font, const StringView& text, const Color& color,
}
else
{
drawCall.Type = DrawCallType::DrawChar;
drawCall.Type = font->GetAsset()->GetOptions().RasterMode == FontRasterMode::MSDF ? DrawCallType::DrawCharMSDF : DrawCallType::DrawChar;
drawCall.AsChar.Mat = nullptr;
}
for (int32 lineIndex = 0; lineIndex < Lines.Count(); lineIndex++)
@@ -1447,8 +1461,28 @@ void Render2D::DrawRectangle(const Rectangle& rect, const Color& color1, const C
RENDER2D_CHECK_RENDERING_STATE;
const auto& mask = ClipLayersStack.Peek().Mask;
float thick = thickness;
thickness *= (TransformCached.M11 + TransformCached.M22 + TransformCached.M33) * 0.3333333f;
// When lines thickness is very large, don't use corner caps and place line ends to not overlap
if (thickness > 4.0f)
{
thick *= Math::Lerp(0.6f, 1.0f, Math::Saturate(thick - 4.0f)); // Smooth transition between soft LineAA and harsh FillRect
Float2 totalMin = rect.GetUpperLeft() - Float2(thick * 0.5f);
Float2 totalMax = rect.GetBottomRight() + Float2(thick * 0.5f);
Float2 size = totalMax - totalMin;
Render2DDrawCall& drawCall = DrawCalls.AddOne();
drawCall.Type = NeedAlphaWithTint(color1, color2, color3, color4) ? DrawCallType::FillRect : DrawCallType::FillRectNoAlpha;
drawCall.StartIB = IBIndex;
drawCall.CountIB = 6 * 4;
// TODO: interpolate colors from corners to extended rectangle edges properly
WriteRect(Rectangle(totalMin.X, totalMin.Y, size.X, thick), color1, color2, color2, color1);
WriteRect(Rectangle(totalMin.X, totalMin.Y + rect.Size.Y, size.X, thick), color4, color3, color3, color4);
WriteRect(Rectangle(totalMin.X, totalMin.Y + thick, thick, rect.Size.Y - thick), color1, color1, color4, color4);
WriteRect(Rectangle(totalMax.X - thick, totalMin.Y + thick, thick, rect.Size.Y - thick), color2, color2, color3, color3);
return;
}
Float2 points[5];
ApplyTransform(rect.GetUpperLeft(), points[0]);
ApplyTransform(rect.GetUpperRight(), points[1]);

View File

@@ -2,8 +2,6 @@
#pragma once
#include "FlaxEngine.Gen.h"
class GPUTexture;
/// <summary>

View File

@@ -158,6 +158,7 @@ void GBufferPass::Fill(RenderContext& renderContext, GPUTexture* lightBuffer)
renderContext.Buffers->GBuffer3->View(),
};
renderContext.View.Pass = DrawPass::GBuffer;
context->SetViewportAndScissors(renderContext.Buffers->GetViewport());
// Clear GBuffer
{

View File

@@ -90,6 +90,7 @@ public:
Float3 ProbesOrigin;
float ProbesSpacing = 0.0f;
Int3 ProbeScrollOffsets;
bool PendingUpdate = true;
Int3 ProbeScrollClears;
void Clear()
@@ -97,6 +98,7 @@ public:
ProbesOrigin = Float3::Zero;
ProbeScrollOffsets = Int3::Zero;
ProbeScrollClears = Int3::Zero;
PendingUpdate = true;
}
} Cascades[4];
@@ -457,9 +459,12 @@ bool DynamicDiffuseGlobalIlluminationPass::RenderInner(RenderContext& renderCont
//const uint64 cascadeFrequencies[] = { 1, 1, 1, 1 };
//const uint64 cascadeFrequencies[] = { 10, 10, 10, 10 };
bool cascadeSkipUpdate[4];
int32 maxCascadesPerFrame = renderContext.View.IsSingleFrame ? cascadesCount : 2;
for (int32 cascadeIndex = 0; cascadeIndex < cascadesCount; cascadeIndex++)
{
cascadeSkipUpdate[cascadeIndex] = !clear && (ddgiData.LastFrameUsed % cascadeFrequencies[cascadeIndex]) != 0 && GPU_SPREAD_WORKLOAD;
auto& cascade = ddgiData.Cascades[cascadeIndex];
cascade.PendingUpdate |= !clear && (ddgiData.LastFrameUsed % cascadeFrequencies[cascadeIndex]) != 0 && GPU_SPREAD_WORKLOAD;
cascadeSkipUpdate[cascadeIndex] = !cascade.PendingUpdate || maxCascadesPerFrame-- <= 0;
}
// Compute scrolling (probes are placed around camera but are scrolling to increase stability during movement)
@@ -468,6 +473,7 @@ bool DynamicDiffuseGlobalIlluminationPass::RenderInner(RenderContext& renderCont
if (cascadeSkipUpdate[cascadeIndex])
continue;
auto& cascade = ddgiData.Cascades[cascadeIndex];
cascade.PendingUpdate = false;
// Calculate the count of grid cells between the view origin and the scroll anchor
const Float3 volumeOrigin = cascade.ProbesOrigin + Float3(cascade.ProbeScrollOffsets) * cascade.ProbesSpacing;

View File

@@ -408,7 +408,8 @@ public:
if (GLOBAL_SDF_ACTOR_IS_STATIC(a) && ObjectTypes.Contains(a->GetTypeHandle()))
{
ScopeWriteLock lock(Locker);
OnSceneRenderingDirty(BoundingBox::FromSphere(prevBounds));
if (flags != DrawModes && flags != Layer && flags != StaticFlags)
OnSceneRenderingDirty(BoundingBox::FromSphere(prevBounds));
OnSceneRenderingDirty(a->GetBox());
}
}

View File

@@ -13,13 +13,20 @@ public class RequireActorAttribute : Attribute
/// The required type.
/// </summary>
public Type RequiredType;
/// <summary>
/// Whether to include inherited types.
/// </summary>
public bool IncludeInheritedTypes;
/// <summary>
/// Initializes a new instance of the <see cref="RequireActorAttribute"/> class.
/// </summary>
/// <param name="type">The required type.</param>
public RequireActorAttribute(Type type)
/// <param name="includeInheritedTypes">Whether to include inherited types.</param>
public RequireActorAttribute(Type type, bool includeInheritedTypes = false)
{
RequiredType = type;
IncludeInheritedTypes = includeInheritedTypes;
}
}

View File

@@ -4,7 +4,6 @@
#include "Engine/Core/Collections/Dictionary.h"
#include "Engine/Core/Types/Guid.h"
#include "FlaxEngine.Gen.h"
/// <summary>
/// Object serialization modification base class. Allows to extend the serialization process by custom effects like object ids mapping.
@@ -12,17 +11,18 @@
class FLAXENGINE_API ISerializeModifier
{
public:
/// <summary>
/// Number of engine build when data was serialized. Useful to upgrade data from the older storage format.
/// </summary>
uint32 EngineBuild = FLAXENGINE_VERSION_BUILD;
uint32 EngineBuild;
// Utility for scene deserialization to track currently mapped in Prefab Instance object IDs into IdsMapping.
int32 CurrentInstance = -1;
int32 CurrentInstance;
/// <summary>
/// The object IDs mapping. Key is a serialized object id, value is mapped value to use.
/// </summary>
Dictionary<Guid, Guid> IdsMapping;
ISerializeModifier();
};

View File

@@ -25,6 +25,13 @@
#include "Engine/Content/Asset.h"
#include "Engine/Level/SceneObject.h"
#include "Engine/Utilities/Encryption.h"
#include "FlaxEngine.Gen.h"
ISerializeModifier::ISerializeModifier()
{
EngineBuild = FLAXENGINE_VERSION_BUILD;
CurrentInstance = -1;
}
void ISerializable::DeserializeIfExists(DeserializeStream& stream, const char* memberName, ISerializeModifier* modifier)
{

View File

@@ -5,6 +5,7 @@
#include "JsonWriters.h"
#include "JsonSerializer.h"
#include "MemoryReadStream.h"
#include "FlaxEngine.Gen.h"
#include "Engine/Core/Types/Variant.h"
#include "Engine/Core/Collections/Dictionary.h"
#include "Engine/Core/Math/Vector2.h"

View File

@@ -148,7 +148,6 @@ void ShadowsOfMordor::Builder::Dispose()
#include "Engine/Serialization/FileWriteStream.h"
#include "Engine/Engine/CommandLine.h"
#include "Engine/Scripting/Scripting.h"
#include "FlaxEngine.Gen.h"
namespace ShadowsOfMordor
{

View File

@@ -549,19 +549,22 @@ bool Terrain::DrawSetup(RenderContext& renderContext)
const DrawPass drawModes = DrawModes & renderContext.View.Pass;
if (drawModes == DrawPass::GlobalSDF)
{
const float chunkSize = TERRAIN_UNITS_PER_VERTEX * (float)_chunkSize;
const float posToUV = 0.25f / chunkSize;
Float4 localToUV(posToUV, posToUV, 0.0f, 0.0f);
const float chunkScale = 0.25f / (TERRAIN_UNITS_PER_VERTEX * (float)_chunkSize); // Patch heightfield is divided into 4x4 chunks
for (const TerrainPatch* patch : _patches)
{
if (!patch->Heightmap)
continue;
GPUTexture* heightfield = patch->Heightmap->GetTexture();
float size = (float)heightfield->Width();
Float4 localToUV;
localToUV.X = localToUV.Y = chunkScale * (size - 1) / size; // Skip the last edge texel
localToUV.Z = localToUV.W = 0.5f / size; // Include half-texel offset
Transform patchTransform;
patchTransform.Translation = patch->_offset + Vector3(0, patch->_yOffset, 0);
patchTransform.Orientation = Quaternion::Identity;
patchTransform.Scale = Float3(1.0f, patch->_yHeight, 1.0f);
patchTransform = _transform.LocalToWorld(patchTransform);
GlobalSignDistanceFieldPass::Instance()->RasterizeHeightfield(this, patch->Heightmap->GetTexture(), patchTransform, patch->_bounds, localToUV);
GlobalSignDistanceFieldPass::Instance()->RasterizeHeightfield(this, heightfield, patchTransform, patch->_bounds, localToUV);
}
return true;
}

View File

@@ -9,6 +9,7 @@
#include "Engine/Content/Assets/Model.h"
#include "Engine/Graphics/GPUBuffer.h"
#include "Engine/Graphics/Models/ModelData.h"
#include "Engine/Graphics/Models/MeshAccessor.h"
#include "Engine/Profiler/ProfilerCPU.h"
PACK_STRUCT(struct GPUBVH {
@@ -321,7 +322,6 @@ void MeshAccelerationStructure::Add(Model* model, int32 lodIndex)
lodIndex = Math::Clamp(lodIndex, model->HighestResidentLODIndex(), model->LODs.Count() - 1);
ModelLOD& lod = model->LODs[lodIndex];
_meshes.EnsureCapacity(_meshes.Count() + lod.Meshes.Count());
bool failed = false;
for (int32 i = 0; i < lod.Meshes.Count(); i++)
{
auto& mesh = lod.Meshes[i];
@@ -336,25 +336,19 @@ void MeshAccelerationStructure::Add(Model* model, int32 lodIndex)
auto& meshData = _meshes.AddOne();
meshData.Asset = model;
model->AddReference();
if (model->IsVirtual())
{
meshData.Indices = mesh.GetTriangleCount() * 3;
meshData.Vertices = mesh.GetVertexCount();
failed |= mesh.DownloadDataGPU(MeshBufferType::Index, meshData.IndexBuffer);
failed |= mesh.DownloadDataGPU(MeshBufferType::Vertex0, meshData.VertexBuffer);
}
else
{
failed |= mesh.DownloadDataCPU(MeshBufferType::Index, meshData.IndexBuffer, meshData.Indices);
failed |= mesh.DownloadDataCPU(MeshBufferType::Vertex0, meshData.VertexBuffer, meshData.Vertices);
}
if (failed)
MeshAccessor accessor;
MeshBufferType bufferTypes[2] = { MeshBufferType::Index, MeshBufferType::Vertex0 };
if (accessor.LoadMesh(&mesh, false, ToSpan(bufferTypes, 2)))
return;
if (!meshData.IndexBuffer.IsAllocated() && meshData.IndexBuffer.Length() != 0)
{
// BVH nodes modifies index buffer (sorts data in-place) so clone it
meshData.IndexBuffer.Copy(meshData.IndexBuffer.Get(), meshData.IndexBuffer.Length());
}
auto indexStream = accessor.Index();
auto positionStream = accessor.Position();
if (!indexStream.IsValid() || !positionStream.IsValid())
return;
meshData.Indices = indexStream.GetCount();
meshData.Vertices = positionStream.GetCount();
meshData.IndexBuffer.Copy(indexStream.GetData());
meshData.VertexBuffer.Allocate(meshData.Vertices * sizeof(Float3));
positionStream.CopyTo(ToSpan(meshData.VertexBuffer.Get<Float3>(), meshData.Vertices));
meshData.Use16BitIndexBuffer = mesh.Use16BitIndexBuffer();
meshData.Bounds = mesh.GetBox();
}

View File

@@ -722,6 +722,10 @@ bool TextureTool::ImportTextureDirectXTex(ImageType type, const StringView& path
errorMsg = String::Format(TEXT("Imported texture has not full mip chain, loaded mips count: {0}, expected: {1}"), sourceMipLevels, mipLevels);
return true;
}
if (options.GenerateMipMaps && !isPowerOfTwo)
{
LOG(Warning, "Cannot generate mip maps for texture '{}' that size is not power of two. Use Resize or Max Size to change dimensions.", StringUtils::GetFileName(path), width, height);
}
// Allocate memory for texture data
auto& data = textureData.Items;

View File

@@ -627,6 +627,10 @@ bool TextureTool::ImportTextureStb(ImageType type, const StringView& path, Textu
errorMsg = String::Format(TEXT("Imported texture has not full mip chain, loaded mips count: {0}, expected: {1}"), sourceMipLevels, mipLevels);
return true;
}
if (options.GenerateMipMaps && !isPowerOfTwo)
{
LOG(Warning, "Cannot generate mip maps for texture '{}' that size is not power of two. Use Resize or Max Size to change dimensions.", StringUtils::GetFileName(path), width, height);
}
// Decompress if texture is compressed (next steps need decompressed input data, for eg. mip maps generation or format changing)
if (PixelFormatExtensions::IsCompressed(textureDataSrc->Format))

View File

@@ -25,12 +25,12 @@ namespace FlaxEngine.GUI
/// <summary>
/// Gets a value indicating whether canvas is 2D (screen-space).
/// </summary>
public bool Is2D => _canvas.RenderMode == CanvasRenderMode.ScreenSpace;
public bool Is2D => _canvas.Is2D;
/// <summary>
/// Gets a value indicating whether canvas is 3D (world-space or camera-space).
/// </summary>
public bool Is3D => _canvas.RenderMode != CanvasRenderMode.ScreenSpace;
public bool Is3D => _canvas.Is3D;
/// <summary>
/// Initializes a new instance of the <see cref="CanvasRootControl"/> class.

View File

@@ -298,6 +298,7 @@ namespace FlaxEngine.GUI
{
case CanvasRenderMode.WorldSpace:
case CanvasRenderMode.WorldSpaceFaceCamera:
case CanvasRenderMode.GPUTexture:
scale = 1.0f;
break;
default:

View File

@@ -595,7 +595,7 @@ namespace FlaxEngine.GUI
Size = new Float2(size.X - margin, size.Y),
Font = Font,
TextColor = TextColor * 0.9f,
TextColorHighlighted = TextColorHighlighted,
TextColorHighlighted = TextColorHighlighted.Brightness < 0.05f ? Color.Lerp(TextColorHighlighted, Color.White, 0.3f) : TextColorHighlighted,
HorizontalAlignment = HorizontalAlignment,
VerticalAlignment = VerticalAlignment,
Text = _items[i],

View File

@@ -1,6 +1,8 @@
// Copyright (c) Wojciech Figat. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
#if FLAX_EDITOR
using FlaxEditor.Options;
#endif
@@ -42,6 +44,38 @@ namespace FlaxEngine.GUI
'<',
};
/// <summary>
/// The allowable characters to use for the text.
/// </summary>
[Flags]
public enum AllowableCharacters
{
/// <summary>
/// Wether to not allow any character in the text.
/// </summary>
None = 0,
/// <summary>
/// Whether to use letters in the text.
/// </summary>
Letters = 1 << 0,
/// <summary>
/// Whether to use numbers in the text.
/// </summary>
Numbers = 1 << 1,
/// <summary>
/// Whether to use symbols in the text.
/// </summary>
Symbols = 1 << 2,
/// <summary>
/// Whether to use all characters in the text.
/// </summary>
All = Letters | Numbers | Symbols,
}
/// <summary>
/// Default height of the text box
/// </summary>
@@ -86,6 +120,11 @@ namespace FlaxEngine.GUI
/// Flag used to indicate whenever text can contain multiple lines.
/// </summary>
protected bool _isMultiline;
/// <summary>
/// The characters to allow in the text.
/// </summary>
protected AllowableCharacters _charactersToAllow = AllowableCharacters.All;
/// <summary>
/// Flag used to indicate whenever text is read-only and cannot be modified by the user.
@@ -188,6 +227,16 @@ namespace FlaxEngine.GUI
}
}
/// <summary>
/// The character to allow in the text.
/// </summary>
[EditorOrder(41), Tooltip("The character to allow in the text.")]
public AllowableCharacters CharactersToAllow
{
get => _charactersToAllow;
set => _charactersToAllow = value;
}
/// <summary>
/// Gets or sets the maximum number of characters the user can type into the text box control.
/// </summary>
@@ -395,15 +444,42 @@ namespace FlaxEngine.GUI
value = value.GetLines()[0];
}
if (_text != value)
if (_text.Equals(value, StringComparison.Ordinal))
return;
if (CharactersToAllow != AllowableCharacters.All)
{
Deselect();
ResetViewOffset();
_text = value;
OnTextChanged();
if (CharactersToAllow == AllowableCharacters.None)
{
value = string.Empty;
}
else
{
if (!CharactersToAllow.HasFlag(AllowableCharacters.Letters))
{
if (value != null)
value = new string(value.Where(c => !char.IsLetter(c)).ToArray());
}
if (!CharactersToAllow.HasFlag(AllowableCharacters.Numbers))
{
if (value != null)
value = new string(value.Where(c => !char.IsNumber(c)).ToArray());
}
if (!CharactersToAllow.HasFlag(AllowableCharacters.Symbols))
{
if (value != null)
value = new string(value.Where(c => !char.IsSymbol(c)).ToArray());
}
value ??= string.Empty;
}
}
Deselect();
ResetViewOffset();
_text = value;
OnTextChanged();
}
/// <summary>

View File

@@ -166,8 +166,26 @@ namespace FlaxEngine.GUI
[NoSerialize, HideInEditor]
public Float2 LocalLocation
{
get => _bounds.Location - (_parent != null ? _parent._bounds.Size * (_anchorMax + _anchorMin) * 0.5f : Float2.Zero) + _bounds.Size * _pivot;
set => Bounds = new Rectangle(value + (_parent != null ? _parent.Bounds.Size * (_anchorMax + _anchorMin) * 0.5f : Float2.Zero) - _bounds.Size * _pivot, _bounds.Size);
get
{
var anchor = Float2.Zero;
if (_parent != null)
{
_parent.GetDesireClientArea(out var parentBounds);
anchor = parentBounds.Location + parentBounds.Size * (_anchorMin + _anchorMax) * 0.5f;
}
return _bounds.Location - anchor + _bounds.Size * _pivot;
}
set
{
var anchor = Float2.Zero;
if (_parent != null)
{
_parent.GetDesireClientArea(out var parentBounds);
anchor = parentBounds.Location + parentBounds.Size * (_anchorMin + _anchorMax) * 0.5f;
}
Bounds = new Rectangle(value + anchor - _bounds.Size * _pivot, _bounds.Size);
}
}
/// <summary>

View File

@@ -6,7 +6,6 @@ namespace FlaxEngine.GUI
/// Base class for container controls that can offset controls in a view (eg. scroll panels).
/// </summary>
/// <seealso cref="FlaxEngine.GUI.ContainerControl" />
[HideInEditor]
public class ScrollableControl : ContainerControl
{
/// <summary>

View File

@@ -33,6 +33,11 @@ namespace FlaxEngine
/// The world space rendering mode that places Canvas as any other object in the scene and orients it to face the camera. The size of the Canvas can be set manually using its Transform, and UI elements will render in front of or behind other objects in the scene based on 3D placement. This is useful for UIs that are meant to be a part of the world. This is also known as a 'diegetic interface'.
/// </summary>
WorldSpaceFaceCamera = 3,
/// <summary>
/// The off-screen rendering mode that draws the contents of the canvas into a GPU texture that can be used in the scene or by other systems. The size of the canvas is automatically set to the size of the texture.
/// </summary>
GPUTexture = 4,
}
/// <summary>
@@ -105,7 +110,7 @@ namespace FlaxEngine
private CanvasRenderMode _renderMode;
private readonly CanvasRootControl _guiRoot;
private CanvasRenderer _renderer;
private bool _isLoading, _isRegisteredForTick;
private bool _isLoading, _isRegisteredForTick, _isRegisteredForOnDraw;
/// <summary>
/// Gets or sets the canvas rendering mode.
@@ -169,6 +174,8 @@ namespace FlaxEngine
private bool Editor_IsCameraSpace => _renderMode == CanvasRenderMode.CameraSpace;
private bool Editor_IsGPUTexture => _renderMode == CanvasRenderMode.GPUTexture;
private bool Editor_UseRenderCamera => _renderMode == CanvasRenderMode.CameraSpace || _renderMode == CanvasRenderMode.WorldSpaceFaceCamera;
#endif
@@ -206,6 +213,12 @@ namespace FlaxEngine
[EditorOrder(60), Limit(0.01f), EditorDisplay("Canvas"), VisibleIf("Editor_IsCameraSpace"), Tooltip("Distance from the RenderCamera to place the plane with GUI. If the screen is resized, changes resolution, or the camera frustum changes, the Canvas will automatically change size to match as well.")]
public float Distance { get; set; } = 500;
/// <summary>
/// Gets or sets the output texture for the canvas when render mode is set to <see cref="CanvasRenderMode.GPUTexture"/>. The size of the canvas will be automatically set to the size of the texture. The canvas will render its content into this texture.
/// </summary>
[EditorOrder(70), NoSerialize, EditorDisplay("Canvas"), VisibleIf("Editor_IsGPUTexture")]
public GPUTexture OutputTexture { get; set; }
/// <summary>
/// Gets the canvas GUI root control.
/// </summary>
@@ -329,6 +342,11 @@ namespace FlaxEngine
_isRegisteredForTick = false;
Scripting.Update -= OnUpdate;
}
if (_isRegisteredForOnDraw)
{
_isRegisteredForOnDraw = false;
Scripting.Draw -= OnDraw;
}
}
/// <summary>
@@ -358,7 +376,7 @@ namespace FlaxEngine
/// <summary>
/// Gets a value indicating whether canvas is 3D (world-space or camera-space).
/// </summary>
public bool Is3D => _renderMode != CanvasRenderMode.ScreenSpace;
public bool Is3D => _renderMode != CanvasRenderMode.ScreenSpace && _renderMode != CanvasRenderMode.GPUTexture;
/// <summary>
/// Gets the world matrix used to transform the GUI from the local space to the world space. Handles canvas rendering mode
@@ -491,6 +509,11 @@ namespace FlaxEngine
{
if (_isLoading)
return;
if (_isRegisteredForOnDraw)
{
_isRegisteredForOnDraw = false;
Scripting.Draw -= OnDraw;
}
switch (_renderMode)
{
@@ -563,7 +586,32 @@ namespace FlaxEngine
}
break;
}
case CanvasRenderMode.GPUTexture:
{
if (!_isRegisteredForOnDraw)
{
_isRegisteredForOnDraw = true;
Scripting.Draw += OnDraw;
}
break;
}
}
}
private void OnDraw()
{
var outputTexture = OutputTexture;
if (!outputTexture || !outputTexture.IsAllocated)
return;
var context = GPUDevice.Instance.MainContext;
_guiRoot.Size = outputTexture.Size;
Profiler.BeginEvent("UI Canvas");
Profiler.BeginEventGPU("UI Canvas");
context.Clear(outputTexture.View(), Color.Transparent);
Render2D.CallDrawing(GUI, context, outputTexture);
Profiler.EndEvent();
Profiler.EndEventGPU();
}
private void OnUpdate()