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

This commit is contained in:
Wojtek Figat
2026-03-27 11:22:32 +01:00
129 changed files with 1493 additions and 402 deletions

View File

@@ -8,6 +8,8 @@
#include "Loading/Tasks/LoadAssetTask.h"
#include "Engine/Core/Log.h"
#include "Engine/Core/LogContext.h"
#include "Engine/Graphics/GPUDevice.h"
#include "Engine/Physics/Physics.h"
#include "Engine/Profiler/ProfilerCPU.h"
#include "Engine/Profiler/ProfilerMemory.h"
#include "Engine/Scripting/ManagedCLR/MCore.h"
@@ -703,6 +705,38 @@ void Asset::onUnload_MainThread()
OnUnloaded(this);
}
bool Asset::WaitForInitGraphics()
{
#define IS_GPU_NOT_READY() (GPUDevice::Instance == nullptr || GPUDevice::Instance->GetState() != GPUDevice::DeviceState::Ready)
if (!IsInMainThread() && IS_GPU_NOT_READY())
{
PROFILE_CPU();
ZoneColor(TracyWaitZoneColor);
int32 timeout = 1000;
while (IS_GPU_NOT_READY() && timeout-- > 0)
Platform::Sleep(1);
if (IS_GPU_NOT_READY())
return true;
}
#undef IS_GPU_NOT_READY
return false;
}
bool Asset::WaitForInitPhysics()
{
if (!IsInMainThread() && !Physics::DefaultScene)
{
PROFILE_CPU();
ZoneColor(TracyWaitZoneColor);
int32 timeout = 1000;
while (!Physics::DefaultScene && timeout-- > 0)
Platform::Sleep(1);
if (!Physics::DefaultScene)
return true;
}
return false;
}
#if USE_EDITOR
bool Asset::OnCheckSave(const StringView& path) const

View File

@@ -285,6 +285,10 @@ protected:
virtual void onRename(const StringView& newPath) = 0;
#endif
// Utilities to ensure specific engine systems are initialized before loading asset (eg. assets can be loaded during engine startup).
static bool WaitForInitGraphics();
static bool WaitForInitPhysics();
public:
// [ManagedScriptingObject]
String ToString() const override;

View File

@@ -6,7 +6,6 @@
#include "Engine/Content/Deprecated.h"
#include "Engine/Content/Upgraders/ShaderAssetUpgrader.h"
#include "Engine/Content/Factories/BinaryAssetFactory.h"
#include "Engine/Graphics/GPUDevice.h"
#include "Engine/Graphics/RenderTools.h"
#include "Engine/Graphics/Materials/MaterialShader.h"
#include "Engine/Graphics/Shaders/Cache/ShaderCacheManager.h"
@@ -157,16 +156,8 @@ Asset::LoadResult Material::load()
FlaxChunk* materialParamsChunk;
// Wait for the GPU Device to be ready (eg. case when loading material before GPU init)
#define IS_GPU_NOT_READY() (GPUDevice::Instance == nullptr || GPUDevice::Instance->GetState() != GPUDevice::DeviceState::Ready)
if (!IsInMainThread() && IS_GPU_NOT_READY())
{
int32 timeout = 1000;
while (IS_GPU_NOT_READY() && timeout-- > 0)
Platform::Sleep(1);
if (IS_GPU_NOT_READY())
return LoadResult::InvalidData;
}
#undef IS_GPU_NOT_READY
if (WaitForInitGraphics())
return LoadResult::CannotLoadData;
// If engine was compiled with shaders compiling service:
// - Material should be changed in need to convert it to the newer version (via Visject Surface)

View File

@@ -19,10 +19,10 @@ Variant MaterialBase::GetParameterValue(const StringView& name)
if (!IsLoaded() && WaitForLoaded())
return Variant::Null;
const auto param = Params.Get(name);
if (IsMaterialInstance() && param && !param->IsOverride() && ((MaterialInstance*)this)->GetBaseMaterial())
return ((MaterialInstance*)this)->GetBaseMaterial()->GetParameterValue(name);
if (param)
{
return param->GetValue();
}
LOG(Warning, "Missing material parameter '{0}' in material {1}", String(name), ToString());
return Variant::Null;
}

View File

@@ -57,6 +57,8 @@ public:
/// <summary>
/// Gets the material parameter value.
/// </summary>
/// <remarks>For material instances that inherit a base material, returned value might come from base material if the current one doesn't override it.</remarks>
/// <param name="name">The parameter name.</param>
/// <returns>The parameter value.</returns>
API_FUNCTION() Variant GetParameterValue(const StringView& name);

View File

@@ -46,6 +46,7 @@ namespace
ContentStorageService ContentStorageServiceInstance;
TimeSpan ContentStorageManager::UnusedStorageLifetime = TimeSpan::FromSeconds(0.5f);
TimeSpan ContentStorageManager::UnusedDataChunksLifetime = TimeSpan::FromSeconds(10);
FlaxStorageReference ContentStorageManager::GetStorage(const StringView& path, bool loadIt)

View File

@@ -15,7 +15,12 @@ class FLAXENGINE_API ContentStorageManager
{
public:
/// <summary>
/// Auto-release timeout for unused asset chunks.
/// Auto-release timeout for unused asset files.
/// </summary>
static TimeSpan UnusedStorageLifetime;
/// <summary>
/// Auto-release timeout for unused asset data chunks.
/// </summary>
static TimeSpan UnusedDataChunksLifetime;

View File

@@ -286,14 +286,14 @@ FlaxStorage::LockData FlaxStorage::LockSafe()
uint32 FlaxStorage::GetRefCount() const
{
return (uint32)Platform::AtomicRead((intptr*)&_refCount);
return (uint32)Platform::AtomicRead(&_refCount);
}
bool FlaxStorage::ShouldDispose() const
{
return Platform::AtomicRead((intptr*)&_refCount) == 0 &&
Platform::AtomicRead((intptr*)&_chunksLock) == 0 &&
Platform::GetTimeSeconds() - _lastRefLostTime >= 0.5; // TTL in seconds
return Platform::AtomicRead(&_refCount) == 0 &&
Platform::AtomicRead(&_chunksLock) == 0 &&
Platform::GetTimeSeconds() - _lastRefLostTime >= ContentStorageManager::UnusedStorageLifetime.GetTotalSeconds();
}
uint32 FlaxStorage::GetMemoryUsage() const

View File

@@ -19,6 +19,7 @@
#include "Engine/Animations/AnimEvent.h"
#include "Engine/Level/Actors/EmptyActor.h"
#include "Engine/Level/Actors/StaticModel.h"
#include "Engine/Level/Actors/AnimatedModel.h"
#include "Engine/Level/Prefabs/Prefab.h"
#include "Engine/Level/Prefabs/PrefabManager.h"
#include "Engine/Level/Scripts/ModelPrefab.h"
@@ -82,6 +83,11 @@ bool ImportModel::TryGetImportOptions(const StringView& path, Options& options)
struct PrefabObject
{
enum
{
Model,
SkinnedModel,
} Type;
int32 NodeIndex;
String Name;
String AssetPath;
@@ -280,7 +286,7 @@ CreateAssetResult ImportModel::Import(CreateAssetContext& context)
options.SplitObjects = false;
options.ObjectIndex = -1;
// Import all of the objects recursive but use current model data to skip loading file again
// Import all the objects recursive but use current model data to skip loading file again
options.Cached = &cached;
HashSet<String> objectNames;
Function<bool(Options& splitOptions, const StringView& objectName, String& outputPath, MeshData* meshData)> splitImport = [&context, &autoImportOutput, &objectNames](Options& splitOptions, const StringView& objectName, String& outputPath, MeshData* meshData)
@@ -335,12 +341,24 @@ CreateAssetResult ImportModel::Import(CreateAssetContext& context)
auto& group = meshesByName[groupIndex];
// Cache object options (nested sub-object import removes the meshes)
prefabObject.NodeIndex = group.First()->NodeIndex;
prefabObject.Name = group.First()->Name;
MeshData* firstMesh = group.First();
prefabObject.NodeIndex = firstMesh->NodeIndex;
prefabObject.Name = firstMesh->Name;
splitOptions.Type = ModelTool::ModelType::Model;
// Detect model type
if ((firstMesh->BlendIndices.HasItems() && firstMesh->BlendWeights.HasItems()) || firstMesh->BlendShapes.HasItems())
{
splitOptions.Type = ModelTool::ModelType::SkinnedModel;
prefabObject.Type = PrefabObject::SkinnedModel;
}
else
{
splitOptions.Type = ModelTool::ModelType::Model;
prefabObject.Type = PrefabObject::Model;
}
splitOptions.ObjectIndex = groupIndex;
if (!splitImport(splitOptions, group.GetKey(), prefabObject.AssetPath, group.First()))
if (!splitImport(splitOptions, group.GetKey(), prefabObject.AssetPath, firstMesh))
{
prefabObjects.Add(prefabObject);
}
@@ -734,24 +752,38 @@ CreateAssetResult ImportModel::CreatePrefab(CreateAssetContext& context, const M
nodeActors.Clear();
for (const PrefabObject& e : prefabObjects)
{
if (e.NodeIndex == nodeIndex)
if (e.NodeIndex != nodeIndex)
continue;
Actor* a = nullptr;
switch (e.Type)
{
case PrefabObject::Model:
{
auto* actor = New<StaticModel>();
actor->SetName(e.Name);
if (auto* model = Content::LoadAsync<Model>(e.AssetPath))
{
actor->Model = model;
}
nodeActors.Add(actor);
a = actor;
break;
}
case PrefabObject::SkinnedModel:
{
auto* actor = New<AnimatedModel>();
if (auto* skinnedModel = Content::LoadAsync<SkinnedModel>(e.AssetPath))
actor->SkinnedModel = skinnedModel;
a = actor;
break;
}
default:
continue;
}
a->SetName(e.Name);
nodeActors.Add(a);
}
Actor* nodeActor = nodeActors.Count() == 1 ? nodeActors[0] : New<EmptyActor>();
if (nodeActors.Count() > 1)
{
for (Actor* e : nodeActors)
{
e->SetParent(nodeActor);
}
}
if (nodeActors.Count() != 1)
{

View File

@@ -155,6 +155,7 @@ void Screen::SetCursorLock(CursorLockMode mode)
bool inRelativeMode = Input::Mouse->IsRelative();
if (mode == CursorLockMode::Clipped)
win->StartClippingCursor(bounds);
#if PLATFORM_SDL
else if (mode == CursorLockMode::Locked)
{
// Use mouse clip region to restrict the cursor in one spot
@@ -162,6 +163,10 @@ void Screen::SetCursorLock(CursorLockMode mode)
}
else if (CursorLock == CursorLockMode::Locked || CursorLock == CursorLockMode::Clipped)
win->EndClippingCursor();
#else
else if (CursorLock == CursorLockMode::Clipped)
win->EndClippingCursor();
#endif
// Enable relative mode when cursor is restricted
if (mode != CursorLockMode::None)

View File

@@ -88,9 +88,8 @@ void TerrainMaterialShader::Bind(BindParameters& params)
}
// Bind terrain textures
const auto heightmap = drawCall.Terrain.Patch->Heightmap->GetTexture();
const auto splatmap0 = drawCall.Terrain.Patch->Splatmap[0] ? drawCall.Terrain.Patch->Splatmap[0]->GetTexture() : nullptr;
const auto splatmap1 = drawCall.Terrain.Patch->Splatmap[1] ? drawCall.Terrain.Patch->Splatmap[1]->GetTexture() : nullptr;
GPUTexture* heightmap, *splatmap0, *splatmap1;
drawCall.Terrain.Patch->GetTextures(heightmap, splatmap0, splatmap1);
context->BindSR(0, heightmap);
context->BindSR(1, splatmap0);
context->BindSR(2, splatmap1);

View File

@@ -936,7 +936,9 @@ void InputService::Update()
break;
}
}
#if PLATFORM_SDL
WindowsManager::WindowsLocker.Unlock();
#endif
// Send input events for the focused window
for (const auto& e : InputEvents)
@@ -990,6 +992,9 @@ void InputService::Update()
break;
}
}
#if !PLATFORM_SDL
WindowsManager::WindowsLocker.Unlock();
#endif
// Skip if game has no focus to handle the input
if (!Engine::HasGameViewportFocus())

View File

@@ -448,8 +448,7 @@ void SceneObjectsFactory::PrefabSyncData::InitNewObjects()
void SceneObjectsFactory::SetupPrefabInstances(Context& context, const PrefabSyncData& data)
{
PROFILE_CPU_NAMED("SetupPrefabInstances");
const int32 count = data.Data.Size();
ASSERT(count <= data.SceneObjects.Count());
const int32 count = Math::Min<int32>(data.Data.Size(), data.SceneObjects.Count());
Dictionary<Guid, Guid> parentIdsLookup;
for (int32 i = 0; i < count; i++)
{

View File

@@ -209,6 +209,13 @@ void Collider::CreateShape()
// Create shape
const bool isTrigger = _isTrigger && CanBeTrigger();
_shape = PhysicsBackend::CreateShape(this, shape, Material, IsActiveInHierarchy(), isTrigger);
if (!_shape)
{
LOG(Error, "Failed to create physics shape for actor '{}'", GetNamePath());
if (shape.Type == CollisionShape::Types::ConvexMesh && Float3(shape.ConvexMesh.Scale).MinValue() <= 0)
LOG(Warning, "Convex Mesh colliders cannot have negative scale");
return;
}
PhysicsBackend::SetShapeContactOffset(_shape, _contactOffset);
UpdateLayerBits();
}
@@ -293,18 +300,20 @@ void Collider::BeginPlay(SceneBeginData* data)
if (_shape == nullptr)
{
CreateShape();
// Check if parent is a rigidbody
const auto rigidBody = dynamic_cast<RigidBody*>(GetParent());
if (rigidBody && CanAttach(rigidBody))
if (_shape)
{
// Attach to the rigidbody
Attach(rigidBody);
}
else
{
// Be a static collider
CreateStaticActor();
// Check if parent is a rigidbody
const auto rigidBody = dynamic_cast<RigidBody*>(GetParent());
if (rigidBody && CanAttach(rigidBody))
{
// Attach to the rigidbody
Attach(rigidBody);
}
else
{
// Be a static collider
CreateStaticActor();
}
}
}

View File

@@ -257,6 +257,8 @@ Asset::LoadResult CollisionData::load()
CollisionData::LoadResult CollisionData::load(const SerializedOptions* options, byte* dataPtr, int32 dataSize)
{
if (WaitForInitPhysics())
return LoadResult::CannotLoadData;
PROFILE_MEM(Physics);
// Load options

View File

@@ -1204,6 +1204,8 @@ void ScenePhysX::PreSimulateCloth(int32 i)
PROFILE_MEM(PhysicsCloth);
auto clothPhysX = ClothsList[i];
auto& clothSettings = Cloths[clothPhysX];
if (!clothSettings.Actor)
return;
if (clothSettings.Actor->OnPreUpdate())
{
@@ -2686,10 +2688,13 @@ void* PhysicsBackend::CreateShape(PhysicsColliderActor* collider, const Collisio
PxGeometryHolder geometryPhysX;
GetShapeGeometry(geometry, geometryPhysX);
PxShape* shapePhysX = PhysX->createShape(geometryPhysX.any(), materialsPhysX.Get(), materialsPhysX.Count(), true, shapeFlags);
shapePhysX->userData = collider;
if (shapePhysX)
{
shapePhysX->userData = collider;
#if PHYSX_DEBUG_NAMING
shapePhysX->setName("Shape");
shapePhysX->setName("Shape");
#endif
}
return shapePhysX;
}

View File

@@ -178,6 +178,27 @@ void RenderAntiAliasingPass(RenderContext& renderContext, GPUTexture* input, GPU
}
}
void RenderLightBuffer(const SceneRenderTask* task, GPUContext* context, RenderContext& renderContext, GPUTexture* lightBuffer, const GPUTextureDescription& tempDesc)
{
context->ResetRenderTarget();
auto colorGradingLUT = ColorGradingPass::Instance()->RenderLUT(renderContext);
auto tempBuffer = RenderTargetPool::Get(tempDesc);
RENDER_TARGET_POOL_SET_NAME(tempBuffer, "TempBuffer");
EyeAdaptationPass::Instance()->Render(renderContext, lightBuffer);
PostProcessingPass::Instance()->Render(renderContext, lightBuffer, tempBuffer, colorGradingLUT);
context->ResetRenderTarget();
if (renderContext.List->Settings.AntiAliasing.Mode == AntialiasingMode::TemporalAntialiasing)
{
TAA::Instance()->Render(renderContext, tempBuffer, lightBuffer->View());
Swap(lightBuffer, tempBuffer);
}
RenderTargetPool::Release(lightBuffer);
context->SetRenderTarget(task->GetOutputView());
context->SetViewportAndScissors(task->GetOutputViewport());
context->Draw(tempBuffer);
RenderTargetPool::Release(tempBuffer);
}
bool Renderer::IsReady()
{
// Warm up first (state getters initialize content loading so do it for all first)
@@ -350,10 +371,12 @@ void RenderInner(SceneRenderTask* task, RenderContext& renderContext, RenderCont
// Perform postFx volumes blending and query before rendering
task->CollectPostFxVolumes(renderContext);
renderContext.List->BlendSettings();
auto aaMode = EnumHasAnyFlags(renderContext.View.Flags, ViewFlags::AntiAliasing) ? renderContext.List->Settings.AntiAliasing.Mode : AntialiasingMode::None;
if (aaMode == AntialiasingMode::TemporalAntialiasing && view.IsOrthographicProjection())
aaMode = AntialiasingMode::None; // TODO: support TAA in ortho projection (see RenderView::Prepare to jitter projection matrix better)
renderContext.List->Settings.AntiAliasing.Mode = aaMode;
{
auto aaMode = EnumHasAnyFlags(renderContext.View.Flags, ViewFlags::AntiAliasing) ? renderContext.List->Settings.AntiAliasing.Mode : AntialiasingMode::None;
if (aaMode == AntialiasingMode::TemporalAntialiasing && view.IsOrthographicProjection())
aaMode = AntialiasingMode::None; // TODO: support TAA in ortho projection (see RenderView::Prepare to jitter projection matrix better)
renderContext.List->Settings.AntiAliasing.Mode = aaMode;
}
// Initialize setup
RenderSetup& setup = renderContext.List->Setup;
@@ -375,7 +398,7 @@ void RenderInner(SceneRenderTask* task, RenderContext& renderContext, RenderCont
(ssrSettings.Intensity > ZeroTolerance && ssrSettings.TemporalEffect && EnumHasAnyFlags(renderContext.View.Flags, ViewFlags::SSR)) ||
renderContext.List->Settings.AntiAliasing.Mode == AntialiasingMode::TemporalAntialiasing;
}
setup.UseTemporalAAJitter = aaMode == AntialiasingMode::TemporalAntialiasing;
setup.UseTemporalAAJitter = renderContext.List->Settings.AntiAliasing.Mode == AntialiasingMode::TemporalAntialiasing;
setup.UseGlobalSurfaceAtlas = renderContext.View.Mode == ViewMode::GlobalSurfaceAtlas ||
(EnumHasAnyFlags(renderContext.View.Flags, ViewFlags::GI) && renderContext.List->Settings.GlobalIllumination.Mode == GlobalIlluminationMode::DDGI);
setup.UseGlobalSDF = (graphicsSettings->EnableGlobalSDF && EnumHasAnyFlags(view.Flags, ViewFlags::GlobalSDF)) ||
@@ -630,22 +653,7 @@ void RenderInner(SceneRenderTask* task, RenderContext& renderContext, RenderCont
}
if (renderContext.View.Mode == ViewMode::LightBuffer)
{
auto colorGradingLUT = ColorGradingPass::Instance()->RenderLUT(renderContext);
auto tempBuffer = RenderTargetPool::Get(tempDesc);
RENDER_TARGET_POOL_SET_NAME(tempBuffer, "TempBuffer");
EyeAdaptationPass::Instance()->Render(renderContext, lightBuffer);
PostProcessingPass::Instance()->Render(renderContext, lightBuffer, tempBuffer, colorGradingLUT);
context->ResetRenderTarget();
if (aaMode == AntialiasingMode::TemporalAntialiasing)
{
TAA::Instance()->Render(renderContext, tempBuffer, lightBuffer->View());
Swap(lightBuffer, tempBuffer);
}
RenderTargetPool::Release(lightBuffer);
context->SetRenderTarget(task->GetOutputView());
context->SetViewportAndScissors(task->GetOutputViewport());
context->Draw(tempBuffer);
RenderTargetPool::Release(tempBuffer);
RenderLightBuffer(task, context, renderContext, lightBuffer, tempDesc);
return;
}
@@ -656,11 +664,13 @@ void RenderInner(SceneRenderTask* task, RenderContext& renderContext, RenderCont
ReflectionsPass::Instance()->Render(renderContext, *lightBuffer);
if (renderContext.View.Mode == ViewMode::Reflections)
{
context->ResetRenderTarget();
context->SetRenderTarget(task->GetOutputView());
context->SetViewportAndScissors(task->GetOutputViewport());
context->Draw(lightBuffer);
RenderTargetPool::Release(lightBuffer);
renderContext.List->Settings.ToneMapping.Mode = ToneMappingMode::Neutral;
renderContext.List->Settings.Bloom.Enabled = false;
renderContext.List->Settings.LensFlares.Intensity = 0.0f;
renderContext.List->Settings.CameraArtifacts.GrainAmount = 0.0f;
renderContext.List->Settings.CameraArtifacts.ChromaticDistortion = 0.0f;
renderContext.List->Settings.CameraArtifacts.VignetteIntensity = 0.0f;
RenderLightBuffer(task, context, renderContext, lightBuffer, tempDesc);
return;
}
@@ -716,7 +726,7 @@ void RenderInner(SceneRenderTask* task, RenderContext& renderContext, RenderCont
renderContext.List->RunCustomPostFxPass(context, renderContext, PostProcessEffectLocation::BeforePostProcessingPass, frameBuffer, tempBuffer);
// Temporal Anti-Aliasing (goes before post processing)
if (aaMode == AntialiasingMode::TemporalAntialiasing)
if (renderContext.List->Settings.AntiAliasing.Mode == AntialiasingMode::TemporalAntialiasing)
{
TAA::Instance()->Render(renderContext, frameBuffer, tempBuffer->View());
Swap(frameBuffer, tempBuffer);

View File

@@ -20,6 +20,7 @@
#define RAPIDJSON_NEW(x) New<x>
#define RAPIDJSON_DELETE(x) Delete(x)
#define RAPIDJSON_NOMEMBERITERATORCLASS
#define RAPIDJSON_PARSE_DEFAULT_FLAGS kParseTrailingCommasFlag
//#define RAPIDJSON_MALLOC(size) ::malloc(size)
//#define RAPIDJSON_REALLOC(ptr, new_size) ::realloc(ptr, new_size)
//#define RAPIDJSON_FREE(ptr) ::free(ptr)

View File

@@ -8,13 +8,22 @@ using Flax.Build.NativeCpp;
/// </summary>
public class Terrain : EngineModule
{
/// <summary>
/// Enables terrain editing and changing at runtime. If your game doesn't use procedural terrain in game then disable this option to reduce build size.
/// </summary>
public static bool WithEditing = true;
/// <inheritdoc />
public override void Setup(BuildOptions options)
{
base.Setup(options);
options.PrivateDependencies.Add("Physics");
if (!WithEditing)
{
options.PublicDefinitions.Add("TERRAIN_EDITING=0");
}
options.PrivateDependencies.Add("Physics");
if (options.Target.IsEditor)
{
options.PrivateDependencies.Add("ContentImporters");

View File

@@ -306,6 +306,16 @@ void Terrain::SetPhysicalMaterials(const Array<JsonAssetReference<PhysicalMateri
}
}
int32 Terrain::GetHeightmapSize() const
{
return GetChunkSize() * ChunksCountEdge + 1;
}
float Terrain::GetPatchSize() const
{
return TERRAIN_UNITS_PER_VERTEX * ChunksCountEdge * GetChunkSize();
}
TerrainPatch* Terrain::GetPatch(const Int2& patchCoord) const
{
return GetPatch(patchCoord.X, patchCoord.Y);

View File

@@ -21,11 +21,14 @@ struct RenderView;
// Amount of units per terrain geometry vertex (can be adjusted per terrain instance using non-uniform scale factor)
#define TERRAIN_UNITS_PER_VERTEX 100.0f
// Enable/disable terrain editing and changing at runtime. If your game doesn't use procedural terrain in game then disable this option to reduce build size.
#ifndef TERRAIN_EDITING
// Enables terrain editing and changing at runtime. If your game doesn't use procedural terrain in game then disable this option to reduce build size.
#define TERRAIN_EDITING 1
#endif
// Enable/disable terrain heightmap samples modification and gather. Used by the editor to modify the terrain with the brushes.
#define TERRAIN_UPDATING 1
// [Deprecated in 1.12, use TERRAIN_EDITING instead]
#define TERRAIN_UPDATING (TERRAIN_EDITING)
// Enable/disable terrain physics collision drawing
#define TERRAIN_USE_PHYSICS_DEBUG (USE_EDITOR && 1)
@@ -240,6 +243,18 @@ public:
return static_cast<int32>(_chunkSize);
}
/// <summary>
/// Gets the heightmap texture size (square) used by a single patch (shared by all chunks within that patch).
/// </summary>
/// <remarks>ChunkSize * ChunksCountEdge + 1</remarks>
API_PROPERTY() int32 GetHeightmapSize() const;
/// <summary>
/// Gets the size of the patch in world-units (square) without actor scale.
/// </summary>
/// <remarks>UnitsPerVertex * ChunksCountEdge * ChunkSize</remarks>
API_PROPERTY() float GetPatchSize() const;
/// <summary>
/// Gets the terrain patches count. Each patch contains 16 chunks arranged into a 4x4 square.
/// </summary>
@@ -329,7 +344,6 @@ public:
API_FUNCTION() void SetChunkOverrideMaterial(API_PARAM(Ref) const Int2& patchCoord, API_PARAM(Ref) const Int2& chunkCoord, MaterialBase* value);
#if TERRAIN_EDITING
/// <summary>
/// Setups the terrain patch using the specified heightmap data.
/// </summary>
@@ -352,10 +366,6 @@ public:
/// <returns>True if failed, otherwise false.</returns>
API_FUNCTION() bool SetupPatchSplatMap(API_PARAM(Ref) const Int2& patchCoord, int32 index, int32 splatMapLength, const Color32* splatMap, bool forceUseVirtualStorage = false);
#endif
public:
#if TERRAIN_EDITING
/// <summary>
/// Setups the terrain. Clears the existing data.
/// </summary>

View File

@@ -17,6 +17,7 @@
#include "Engine/Threading/Threading.h"
#if TERRAIN_EDITING
#include "Engine/Core/Math/Packed.h"
#include "Engine/Core/Collections/ArrayExtensions.h"
#include "Engine/Graphics/PixelFormatExtensions.h"
#include "Engine/Graphics/RenderTools.h"
#include "Engine/Graphics/RenderView.h"
@@ -27,11 +28,6 @@
#include "Editor/Editor.h"
#include "Engine/ContentImporters/AssetsImportingManager.h"
#endif
#endif
#if TERRAIN_EDITING || TERRAIN_UPDATING
#include "Engine/Core/Collections/ArrayExtensions.h"
#endif
#if USE_EDITOR
#include "Engine/Debug/DebugDraw.h"
#endif
#if TERRAIN_USE_PHYSICS_DEBUG
@@ -90,7 +86,7 @@ void TerrainPatch::Init(Terrain* terrain, int16 x, int16 z)
Splatmap[i] = nullptr;
}
_heightfield = nullptr;
#if TERRAIN_UPDATING
#if TERRAIN_EDITING
_cachedHeightMap.Resize(0);
_cachedHolesMask.Resize(0);
_wasHeightModified = false;
@@ -114,7 +110,7 @@ void TerrainPatch::Init(Terrain* terrain, int16 x, int16 z)
TerrainPatch::~TerrainPatch()
{
#if TERRAIN_UPDATING
#if TERRAIN_EDITING
SAFE_DELETE(_dataHeightmap);
for (int32 i = 0; i < TERRAIN_MAX_SPLATMAPS_COUNT; i++)
{
@@ -134,6 +130,13 @@ RawDataAsset* TerrainPatch::GetHeightfield() const
return _heightfield.Get();
}
void TerrainPatch::GetTextures(GPUTexture*& heightmap, GPUTexture*& splatmap0, GPUTexture*& splatmap1) const
{
heightmap = Heightmap->GetTexture();
splatmap0 = Splatmap[0] ? Splatmap[0]->GetTexture() : nullptr;
splatmap1 = Splatmap[1] ? Splatmap[1]->GetTexture() : nullptr;
}
void TerrainPatch::RemoveLightmap()
{
for (auto& chunk : Chunks)
@@ -178,7 +181,7 @@ void TerrainPatch::UpdateTransform()
_collisionVertices.Resize(0);
}
#if TERRAIN_EDITING || TERRAIN_UPDATING
#if TERRAIN_EDITING
bool IsValidMaterial(const JsonAssetReference<PhysicalMaterial>& e)
{
@@ -217,7 +220,7 @@ struct TerrainDataUpdateInfo
// When using physical materials, then get splatmaps data required for per-triangle material indices
void GetSplatMaps()
{
#if TERRAIN_UPDATING
#if TERRAIN_EDITING
if (SplatMaps[0])
return;
if (UsePhysicalMaterials())
@@ -1021,7 +1024,7 @@ bool TerrainPatch::SetupHeightMap(int32 heightMapLength, const float* heightMap,
_terrain->UpdateBounds();
_terrain->UpdateLayerBits();
#if TERRAIN_UPDATING
#if TERRAIN_EDITING
// Invalidate cache
_cachedHeightMap.Resize(0);
_cachedHolesMask.Resize(0);
@@ -1169,7 +1172,7 @@ bool TerrainPatch::SetupSplatMap(int32 index, int32 splatMapLength, const Color3
}
#endif
#if TERRAIN_UPDATING
#if TERRAIN_EDITING
// Invalidate cache
_cachedSplatMap[index].Resize(0);
_wasSplatmapModified[index] = false;
@@ -1191,7 +1194,7 @@ bool TerrainPatch::InitializeHeightMap()
return SetupHeightMap(heightmap.Count(), heightmap.Get());
}
#if TERRAIN_UPDATING
#if TERRAIN_EDITING
float* TerrainPatch::GetHeightmapData()
{
@@ -2631,7 +2634,7 @@ void TerrainPatch::Serialize(SerializeStream& stream, const void* otherObj)
}
stream.EndArray();
#if TERRAIN_UPDATING
#if TERRAIN_EDITING
SaveHeightData();
SaveSplatData();
#endif

View File

@@ -12,6 +12,10 @@
struct RayCastHit;
class TerrainMaterialShader;
#ifndef TERRAIN_EDITING
#define TERRAIN_EDITING 1
#endif
/// <summary>
/// Represents single terrain patch made of 16 terrain chunks.
/// </summary>
@@ -34,7 +38,7 @@ private:
void* _physicsHeightField;
CriticalSection _collisionLocker;
float _collisionScaleXZ;
#if TERRAIN_UPDATING
#if TERRAIN_EDITING
Array<float> _cachedHeightMap;
Array<byte> _cachedHolesMask;
Array<Color32> _cachedSplatMap[TERRAIN_MAX_SPLATMAPS_COUNT];
@@ -189,6 +193,8 @@ public:
return _bounds;
}
void GetTextures(GPUTexture*& heightmap, GPUTexture*& splatmap0, GPUTexture*& splatmap1) const;
public:
/// <summary>
/// Removes the lightmap data from the terrain patch.
@@ -220,7 +226,7 @@ public:
/// <param name="holesMask">The holes mask (optional). Normalized to 0-1 range values with holes mask per-vertex. Must match the heightmap dimensions.</param>
/// <param name="forceUseVirtualStorage">If set to <c>true</c> patch will use virtual storage by force. Otherwise it can use normal texture asset storage on drive (valid only during Editor). Runtime-created terrain can only use virtual storage (in RAM).</param>
/// <returns>True if failed, otherwise false.</returns>
API_FUNCTION() bool SetupHeightMap(int32 heightMapLength, API_PARAM(Ref) const float* heightMap, API_PARAM(Ref) const byte* holesMask = nullptr, bool forceUseVirtualStorage = false);
API_FUNCTION() bool SetupHeightMap(int32 heightMapLength, const float* heightMap, const byte* holesMask = nullptr, bool forceUseVirtualStorage = false);
/// <summary>
/// Setups the terrain patch layer weights using the specified splatmaps data.
@@ -230,14 +236,12 @@ public:
/// <param name="splatMap">The splat map. Each array item contains 4 layer weights.</param>
/// <param name="forceUseVirtualStorage">If set to <c>true</c> patch will use virtual storage by force. Otherwise it can use normal texture asset storage on drive (valid only during Editor). Runtime-created terrain can only use virtual storage (in RAM).</param>
/// <returns>True if failed, otherwise false.</returns>
API_FUNCTION() bool SetupSplatMap(int32 index, int32 splatMapLength, API_PARAM(Ref) const Color32* splatMap, bool forceUseVirtualStorage = false);
#endif
API_FUNCTION() bool SetupSplatMap(int32 index, int32 splatMapLength, const Color32* splatMap, bool forceUseVirtualStorage = false);
#if TERRAIN_UPDATING
/// <summary>
/// Gets the raw pointer to the heightmap data.
/// Gets the raw pointer to the heightmap data. Array size is square of Terrain.HeightmapSize.
/// </summary>
/// <returns>The heightmap data.</returns>
/// <returns>The heightmap data. Null if empty or failed to access it.</returns>
API_FUNCTION() float* GetHeightmapData();
/// <summary>
@@ -246,9 +250,9 @@ public:
API_FUNCTION() void ClearHeightmapCache();
/// <summary>
/// Gets the raw pointer to the holes mask data.
/// Gets the raw pointer to the holes mask data. Array size is square of Terrain.HeightmapSize.
/// </summary>
/// <returns>The holes mask data.</returns>
/// <returns>The holes mask data. Null if empty/unused or failed to access it.</returns>
API_FUNCTION() byte* GetHolesMaskData();
/// <summary>
@@ -257,10 +261,10 @@ public:
API_FUNCTION() void ClearHolesMaskCache();
/// <summary>
/// Gets the raw pointer to the splat map data.
/// Gets the raw pointer to the splat map data. Array size is square of Terrain.HeightmapSize.
/// </summary>
/// <param name="index">The zero-based index of the splatmap texture.</param>
/// <returns>The splat map data.</returns>
/// <returns>The splat map data. Null if empty/unused or failed to access it.</returns>
API_FUNCTION() Color32* GetSplatMapData(int32 index);
/// <summary>
@@ -280,7 +284,7 @@ public:
/// <param name="modifiedOffset">The offset from the first row and column of the heightmap data (offset destination x and z start position).</param>
/// <param name="modifiedSize">The size of the heightmap to modify (x and z). Amount of samples in each direction.</param>
/// <returns>True if failed, otherwise false.</returns>
API_FUNCTION() bool ModifyHeightMap(API_PARAM(Ref) const float* samples, API_PARAM(Ref) const Int2& modifiedOffset, API_PARAM(Ref) const Int2& modifiedSize);
API_FUNCTION() bool ModifyHeightMap(const float* samples, const Int2& modifiedOffset, const Int2& modifiedSize);
/// <summary>
/// Modifies the terrain patch holes mask with the given samples.
@@ -289,7 +293,7 @@ public:
/// <param name="modifiedOffset">The offset from the first row and column of the holes map data (offset destination x and z start position).</param>
/// <param name="modifiedSize">The size of the holes map to modify (x and z). Amount of samples in each direction.</param>
/// <returns>True if failed, otherwise false.</returns>
API_FUNCTION() bool ModifyHolesMask(API_PARAM(Ref) const byte* samples, API_PARAM(Ref) const Int2& modifiedOffset, API_PARAM(Ref) const Int2& modifiedSize);
API_FUNCTION() bool ModifyHolesMask(const byte* samples, const Int2& modifiedOffset, const Int2& modifiedSize);
/// <summary>
/// Modifies the terrain patch splat map (layers mask) with the given samples.
@@ -299,7 +303,7 @@ public:
/// <param name="modifiedOffset">The offset from the first row and column of the splat map data (offset destination x and z start position).</param>
/// <param name="modifiedSize">The size of the splat map to modify (x and z). Amount of samples in each direction.</param>
/// <returns>True if failed, otherwise false.</returns>
API_FUNCTION() bool ModifySplatMap(int32 index, API_PARAM(Ref) const Color32* samples, API_PARAM(Ref) const Int2& modifiedOffset, API_PARAM(Ref) const Int2& modifiedSize);
API_FUNCTION() bool ModifySplatMap(int32 index, const Color32* samples, const Int2& modifiedOffset, const Int2& modifiedSize);
private:
bool UpdateHeightData(struct TerrainDataUpdateInfo& info, const Int2& modifiedOffset, const Int2& modifiedSize, bool wasHeightRangeChanged, bool wasHeightChanged);

View File

@@ -588,6 +588,7 @@ void ModelTool::Options::Serialize(SerializeStream& stream, const void* otherObj
SERIALIZE(SloppyOptimization);
SERIALIZE(LODTargetError);
SERIALIZE(ImportMaterials);
SERIALIZE(CreateEmptyMaterialSlots);
SERIALIZE(ImportMaterialsAsInstances);
SERIALIZE(InstanceToImportAs);
SERIALIZE(ImportTextures);
@@ -643,6 +644,7 @@ void ModelTool::Options::Deserialize(DeserializeStream& stream, ISerializeModifi
DESERIALIZE(SloppyOptimization);
DESERIALIZE(LODTargetError);
DESERIALIZE(ImportMaterials);
DESERIALIZE(CreateEmptyMaterialSlots);
DESERIALIZE(ImportMaterialsAsInstances);
DESERIALIZE(InstanceToImportAs);
DESERIALIZE(ImportTextures);
@@ -1019,7 +1021,7 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option
options.ImportTypes |= ImportDataTypes::Skeleton;
break;
case ModelType::Prefab:
options.ImportTypes = ImportDataTypes::Geometry | ImportDataTypes::Nodes | ImportDataTypes::Animations;
options.ImportTypes = ImportDataTypes::Geometry | ImportDataTypes::Nodes | ImportDataTypes::Skeleton | ImportDataTypes::Animations;
if (options.ImportMaterials)
options.ImportTypes |= ImportDataTypes::Materials;
if (options.ImportTextures)
@@ -1045,6 +1047,8 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option
{
for (auto& mesh : lod.Meshes)
{
if (mesh->BlendShapes.IsEmpty())
continue;
for (int32 blendShapeIndex = mesh->BlendShapes.Count() - 1; blendShapeIndex >= 0; blendShapeIndex--)
{
auto& blendShape = mesh->BlendShapes[blendShapeIndex];
@@ -1209,7 +1213,9 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option
for (int32 i = 0; i < meshesCount; i++)
{
const auto mesh = data.LODs[0].Meshes[i];
if (mesh->BlendIndices.IsEmpty() || mesh->BlendWeights.IsEmpty())
// If imported mesh has skeleton but no indices or weights then need to setup those (except in Prefab mode when we conditionally import meshes based on type)
if ((mesh->BlendIndices.IsEmpty() || mesh->BlendWeights.IsEmpty()) && data.Skeleton.Bones.HasItems() && (options.Type != ModelType::Prefab))
{
auto indices = Int4::Zero;
auto weights = Float4::UnitX;
@@ -1326,7 +1332,7 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option
auto& texture = data.Textures[i];
// Auto-import textures
if (autoImportOutput.IsEmpty() || EnumHasNoneFlags(options.ImportTypes, ImportDataTypes::Textures) || texture.FilePath.IsEmpty())
if (autoImportOutput.IsEmpty() || EnumHasNoneFlags(options.ImportTypes, ImportDataTypes::Textures) || texture.FilePath.IsEmpty() || options.CreateEmptyMaterialSlots)
continue;
String assetPath = GetAdditionalImportPath(autoImportOutput, importedFileNames, StringUtils::GetFileNameWithoutExtension(texture.FilePath));
#if COMPILE_WITH_ASSETS_IMPORTER
@@ -1384,6 +1390,10 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option
}
}
// The rest of the steps this function performs become irrelevant when we're only creating slots.
if (options.CreateEmptyMaterialSlots)
continue;
if (options.ImportMaterialsAsInstances)
{
// Create material instance
@@ -2021,12 +2031,11 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option
#undef REMAP_VERTEX_BUFFER
// Remap blend shapes
dstMesh->BlendShapes.Resize(srcMesh->BlendShapes.Count());
dstMesh->BlendShapes.EnsureCapacity(srcMesh->BlendShapes.Count(), false);
for (int32 blendShapeIndex = 0; blendShapeIndex < srcMesh->BlendShapes.Count(); blendShapeIndex++)
{
const auto& srcBlendShape = srcMesh->BlendShapes[blendShapeIndex];
auto& dstBlendShape = dstMesh->BlendShapes[blendShapeIndex];
BlendShape dstBlendShape;
dstBlendShape.Name = srcBlendShape.Name;
dstBlendShape.Weight = srcBlendShape.Weight;
dstBlendShape.Vertices.EnsureCapacity(srcBlendShape.Vertices.Count());
@@ -2035,17 +2044,12 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option
auto v = srcBlendShape.Vertices[i];
v.VertexIndex = remap[v.VertexIndex];
if (v.VertexIndex != ~0u)
{
dstBlendShape.Vertices.Add(v);
}
}
}
// Remove empty blend shapes
for (int32 blendShapeIndex = dstMesh->BlendShapes.Count() - 1; blendShapeIndex >= 0; blendShapeIndex--)
{
if (dstMesh->BlendShapes[blendShapeIndex].Vertices.IsEmpty())
dstMesh->BlendShapes.RemoveAt(blendShapeIndex);
// Add only valid blend shapes
if (dstBlendShape.Vertices.HasItems())
dstMesh->BlendShapes.Add(dstBlendShape);
}
// Optimize generated LOD
@@ -2092,6 +2096,8 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option
{
for (auto& mesh : lod.Meshes)
{
if (mesh->BlendShapes.IsEmpty())
continue;
for (auto& blendShape : mesh->BlendShapes)
{
// Compute min/max for used vertex indices

View File

@@ -311,16 +311,19 @@ public:
public: // Materials
// If checked, the importer will create materials for model meshes as specified in the file.
API_FIELD(Attributes="EditorOrder(400), EditorDisplay(\"Materials\"), VisibleIf(nameof(ShowGeometry))")
API_FIELD(Attributes="EditorOrder(399), EditorDisplay(\"Materials\"), VisibleIf(nameof(ShowGeometry))")
bool ImportMaterials = true;
// If checked, the importer will create empty material slots for every material without importing materials nor textures.
API_FIELD(Attributes="EditorOrder(400), EditorDisplay(\"Materials\"), VisibleIf(nameof(ShowGeometry))")
bool CreateEmptyMaterialSlots = false;
// If checked, the importer will create the model's materials as instances of a base material.
API_FIELD(Attributes="EditorOrder(401), EditorDisplay(\"Materials\"), VisibleIf(nameof(ImportMaterials)), VisibleIf(nameof(ShowGeometry))")
API_FIELD(Attributes="EditorOrder(401), EditorDisplay(\"Materials\"), VisibleIf(nameof(ImportMaterials)), VisibleIf(nameof(ShowGeometry)), VisibleIf(nameof(CreateEmptyMaterialSlots), true)")
bool ImportMaterialsAsInstances = false;
// The material used as the base material that will be instanced as the imported model's material.
API_FIELD(Attributes="EditorOrder(402), EditorDisplay(\"Materials\"), VisibleIf(nameof(ImportMaterialsAsInstances)), VisibleIf(nameof(ShowGeometry))")
API_FIELD(Attributes="EditorOrder(402), EditorDisplay(\"Materials\"), VisibleIf(nameof(ImportMaterialsAsInstances)), VisibleIf(nameof(ShowGeometry)), VisibleIf(nameof(CreateEmptyMaterialSlots), true)")
AssetReference<MaterialBase> InstanceToImportAs;
// If checked, the importer will import texture files used by the model and any embedded texture resources.
API_FIELD(Attributes="EditorOrder(410), EditorDisplay(\"Materials\"), VisibleIf(nameof(ShowGeometry))")
API_FIELD(Attributes="EditorOrder(410), EditorDisplay(\"Materials\"), VisibleIf(nameof(ShowGeometry)), VisibleIf(nameof(CreateEmptyMaterialSlots), true)")
bool ImportTextures = true;
// If checked, the importer will try to keep the model's current overridden material slots, instead of importing materials from the source file.
API_FIELD(Attributes="EditorOrder(420), EditorDisplay(\"Materials\", \"Keep Overridden Materials\"), VisibleIf(nameof(ShowGeometry))")

View File

@@ -414,6 +414,7 @@ bool TextureTool::UpdateTexture(GPUContext* context, GPUTexture* texture, int32
Array<byte> tempData;
if (textureFormat != dataFormat)
{
PROFILE_CPU_NAMED("ConvertTexture");
auto dataSampler = PixelFormatSampler::Get(dataFormat);
auto textureSampler = PixelFormatSampler::Get(textureFormat);
if (!dataSampler || !textureSampler)

View File

@@ -5,14 +5,14 @@ using System;
namespace FlaxEngine.GUI
{
/// <summary>
/// Radial menu control that arranges child controls (of type Image) in a circle.
/// Radial menu control that arranges child controls (of type <see cref="FlaxEngine.GUI.Image"/>) in a circle.
/// </summary>
/// <seealso cref="FlaxEngine.GUI.ContainerControl" />
public class RadialMenu : ContainerControl
{
private bool _materialIsDirty = true;
private float _angle;
private float _selectedSegment;
private int _selectedSegment;
private int _highlightSegment = -1;
private MaterialBase _material;
private MaterialInstance _materialInstance;
@@ -27,7 +27,7 @@ namespace FlaxEngine.GUI
private bool ShowMatProp => _material != null;
/// <summary>
/// The material to use for menu background drawing.
/// The material used for menu background drawing.
/// </summary>
[EditorOrder(1)]
public MaterialBase Material
@@ -44,7 +44,7 @@ namespace FlaxEngine.GUI
}
/// <summary>
/// Gets or sets the edge offset.
/// Gets or sets the offset of the outer edge from the bounds of the Control.
/// </summary>
[EditorOrder(2), Range(0, 1)]
public float EdgeOffset
@@ -59,7 +59,7 @@ namespace FlaxEngine.GUI
}
/// <summary>
/// Gets or sets the thickness.
/// Gets or sets the thickness of the menu.
/// </summary>
[EditorOrder(3), Range(0, 1), VisibleIf(nameof(ShowMatProp))]
public float Thickness
@@ -74,7 +74,7 @@ namespace FlaxEngine.GUI
}
/// <summary>
/// Gets or sets control background color (transparent color (alpha=0) means no background rendering).
/// Gets or sets control background color (transparent color means no background rendering).
/// </summary>
[VisibleIf(nameof(ShowMatProp))]
public new Color BackgroundColor
@@ -88,7 +88,7 @@ namespace FlaxEngine.GUI
}
/// <summary>
/// Gets or sets the color of the highlight.
/// Gets or sets the color of the outer edge highlight.
/// </summary>
[VisibleIf(nameof(ShowMatProp))]
public Color HighlightColor
@@ -130,19 +130,43 @@ namespace FlaxEngine.GUI
}
/// <summary>
/// The selected callback
/// The material instance of <see cref="Material"/> used to draw the menu.
/// </summary>
[HideInEditor]
public MaterialInstance MaterialInstance => _materialInstance;
/// <summary>
/// The selected callback.
/// </summary>
[HideInEditor]
public Action<int> Selected;
/// <summary>
/// The allow change selection when inside
/// Invoked when the hovered segment is changed.
/// </summary>
[HideInEditor]
public Action<int> HoveredSelectionChanged;
/// <summary>
/// The selected segment.
/// </summary>
[HideInEditor]
public int SelectedSegment => _selectedSegment;
/// <summary>
/// Allows the selected to change when the mouse is moved in the empty center of the menu.
/// </summary>
[VisibleIf(nameof(ShowMatProp))]
public bool AllowChangeSelectionWhenInside;
/// <summary>
/// The center as button
/// Allows the selected to change when the mouse is moved outside of the menu.
/// </summary>
[VisibleIf(nameof(ShowMatProp))]
public bool AllowChangeSelectionWhenOutside;
/// <summary>
/// Wether the center is a button.
/// </summary>
[VisibleIf(nameof(ShowMatProp))]
public bool CenterAsButton;
@@ -225,7 +249,7 @@ namespace FlaxEngine.GUI
var min = ((1 - _edgeOffset) - _thickness) * USize * 0.5f;
var max = (1 - _edgeOffset) * USize * 0.5f;
var val = ((USize * 0.5f) - location).Length;
if (Mathf.IsInRange(val, min, max) || val < min && AllowChangeSelectionWhenInside)
if (Mathf.IsInRange(val, min, max) || val < min && AllowChangeSelectionWhenInside || val > max && AllowChangeSelectionWhenOutside)
{
UpdateAngle(ref location);
}
@@ -276,7 +300,7 @@ namespace FlaxEngine.GUI
var min = ((1 - _edgeOffset) - _thickness) * USize * 0.5f;
var max = (1 - _edgeOffset) * USize * 0.5f;
var val = ((USize * 0.5f) - location).Length;
if (Mathf.IsInRange(val, min, max) || val < min && AllowChangeSelectionWhenInside)
if (Mathf.IsInRange(val, min, max) || val < min && AllowChangeSelectionWhenInside || val > max && AllowChangeSelectionWhenOutside)
{
UpdateAngle(ref location);
}
@@ -347,6 +371,28 @@ namespace FlaxEngine.GUI
base.PerformLayout(force);
}
/// <summary>
/// Updates the current angle and selected segment of the radial menu based on the specified location inside of the control.
/// </summary>
/// <param name="location">The position used to determine the angle and segment selection within the radial menu.</param>
public void UpdateAngle(ref Float2 location)
{
float previousSelectedSegment = _selectedSegment;
var size = new Float2(USize);
var p = (size * 0.5f) - location;
var sa = (1.0f / _segmentCount) * Mathf.TwoPi;
_angle = Mathf.Atan2(p.X, p.Y);
_angle = Mathf.Ceil((_angle - (sa * 0.5f)) / sa) * sa;
_selectedSegment = Mathf.RoundToInt((_angle < 0 ? Mathf.TwoPi + _angle : _angle) / sa);
if (float.IsNaN(_angle) || float.IsInfinity(_angle))
_angle = 0;
_materialInstance.SetParameterValue("RadialMenu_Rotation", -_angle + Mathf.Pi);
if (previousSelectedSegment != _selectedSegment)
HoveredSelectionChanged?.Invoke((int)_selectedSegment);
}
private void UpdateSelectionColor()
{
Color color;
@@ -368,20 +414,6 @@ namespace FlaxEngine.GUI
_materialInstance.SetParameterValue("RadialMenu_SelectionColor", color);
}
private void UpdateAngle(ref Float2 location)
{
var size = new Float2(USize);
var p = (size * 0.5f) - location;
var sa = (1.0f / _segmentCount) * Mathf.TwoPi;
_angle = Mathf.Atan2(p.X, p.Y);
_angle = Mathf.Ceil((_angle - (sa * 0.5f)) / sa) * sa;
_selectedSegment = _angle;
_selectedSegment = Mathf.RoundToInt((_selectedSegment < 0 ? Mathf.TwoPi + _selectedSegment : _selectedSegment) / sa);
if (float.IsNaN(_angle) || float.IsInfinity(_angle))
_angle = 0;
_materialInstance.SetParameterValue("RadialMenu_Rotation", -_angle + Mathf.Pi);
}
private static Float2 Rotate2D(Float2 point, float angle)
{
return new Float2(Mathf.Cos(angle) * point.X + Mathf.Sin(angle) * point.Y,