Skip to content

Commit

Permalink
Text are not being affected when an object is outside bounds (#1904) (#…
Browse files Browse the repository at this point in the history
…2369)

Now TextMeshPro labels are hidden when they leave their scenes.
Added Assembly definitions to let systems access some dependencies.
Calculations are performed only when the Transform of the TMP changes or when its properties change (we are only interested in text though).
It enables and disables the TMP in the VisibilityTextShapeSystem, according to what was calculated in the UpdateTextShapeSystem (added IsContainedInScene flag to TexhShapeComponent to transmit the result).
It properly calculates the bounds of the TMP when it is disabled by caching the latest valid bounds size (added vector to TextShapeComponent) and using it when the bounds are zero.
It takes into account the changes in the text of the TMP of the scene, and it calculates the visibility accordingly.
It forces the TMP to rebuild when the text changes, in order to be able to use the proper bounds.
  • Loading branch information
QThund authored Oct 14, 2024
1 parent 63ea02d commit acf956e
Show file tree
Hide file tree
Showing 13 changed files with 135 additions and 15 deletions.
3 changes: 2 additions & 1 deletion Explorer/Assets/DCL/PluginSystem/World/TextShapePlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using DCL.SDKComponents.TextShape.System;
using ECS.Abstract;
using ECS.LifeCycle;
using SceneRunner.Scene;
using System;
using System.Collections.Generic;
using System.Threading;
Expand Down Expand Up @@ -59,7 +60,7 @@ public void InjectToWorld(ref ArchSystemsWorldBuilder<Arch.Core.World> builder,
var buffer = sharedDependencies.EntityEventsBuilder.Rent<TextShapeComponent>();

InstantiateTextShapeSystem.InjectToWorld(ref builder, textMeshProPool, fontsStorage, materialPropertyBlock, instantiationFrameTimeBudgetProvider, buffer);
UpdateTextShapeSystem.InjectToWorld(ref builder, fontsStorage, materialPropertyBlock, buffer);
UpdateTextShapeSystem.InjectToWorld(ref builder, fontsStorage, materialPropertyBlock, buffer, sharedDependencies.SceneData);
VisibilityTextShapeSystem.InjectToWorld(ref builder, buffer);

finalizeWorldSystems.RegisterReleasePoolableComponentSystem<TextMeshPro, TextShapeComponent>(ref builder, componentPoolsRegistry);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,32 @@
using DCL.Optimization.Pools;
using System;
using TMPro;
using UnityEngine;

namespace DCL.SDKComponents.TextShape.Component
{
public readonly struct TextShapeComponent : IPoolableComponentProvider<TextMeshPro>
public struct TextShapeComponent : IPoolableComponentProvider<TextMeshPro>
{
public readonly TextMeshPro TextMeshPro;

public TextMeshPro PoolableComponent => TextMeshPro;
public Type PoolableComponentType => typeof(TextMeshPro);

/// <summary>
/// Whether the bounding box of the text shape is fully contained in the boundaries of the scene it belongs to.
/// </summary>
public bool IsContainedInScene;

/// <summary>
/// The size of the bounding box of the text the last time it was enabled (when disable, it equals zero).
/// </summary>
public Vector3 LastValidBoundingBoxSize;

public TextShapeComponent(TextMeshPro textShape)
{
TextMeshPro = textShape;
LastValidBoundingBoxSize = textShape.renderer.bounds.size; // Note: Using Renderer because the bounds of the TMP does not return what we need
IsContainedInScene = false;
}

public void Dispose() { }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"reference": "GUID:fc4fd35fb877e904d8cedee73b2256f6"
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using DCL.SDKComponents.TextShape.System;
using ECS.Abstract;
using ECS.Unity.Transforms.Components;
using SceneRunner.Scene;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
Expand All @@ -19,9 +20,9 @@ public class TextShapeDemoWorld : IDemoWorld
{
private readonly IDemoWorld origin;

public TextShapeDemoWorld(World world, IFontsStorage fontsStorage, params (PBTextShape textShape, PBVisibilityComponent visibility, PBBillboard billboard)[] list) : this(world, fontsStorage, list.AsReadOnly()) { }
public TextShapeDemoWorld(World world, IFontsStorage fontsStorage, ISceneData sceneData, params (PBTextShape textShape, PBVisibilityComponent visibility, PBBillboard billboard)[] list) : this(world, fontsStorage, sceneData, list.AsReadOnly()) { }

public TextShapeDemoWorld(World world, IFontsStorage fontsStorage, IReadOnlyList<(PBTextShape textShape, PBVisibilityComponent visibility, PBBillboard billboard)> list)
public TextShapeDemoWorld(World world, IFontsStorage fontsStorage, ISceneData sceneData, IReadOnlyList<(PBTextShape textShape, PBVisibilityComponent visibility, PBBillboard billboard)> list)
{
var pool = new GameObjectPool<TextMeshPro>(null, () => new GameObject().AddComponent<TextMeshPro>());

Expand All @@ -35,7 +36,7 @@ public TextShapeDemoWorld(World world, IFontsStorage fontsStorage, IReadOnlyList
w.Create(textShape, visibility, billboard, NewTransform());
},
w => new InstantiateTextShapeSystem(w, pool, fontsStorage, new MaterialPropertyBlock(), new NullPerformanceBudget(), buffer),
w => new UpdateTextShapeSystem(w, fontsStorage, new MaterialPropertyBlock(), buffer),
w => new UpdateTextShapeSystem(w, fontsStorage, new MaterialPropertyBlock(), buffer, sceneData),
w => new VisibilityTextShapeSystem(w, buffer));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
using DCL.ECSComponents;
using DCL.SDKComponents.TextShape.Component;
using DCL.SDKComponents.TextShape.Fonts;
using SceneRunner.EmptyScene;
using SceneRunner.Scene;
using System;
using UnityEngine;

Expand All @@ -21,7 +23,7 @@ public class WarmUpSettingsTextShapeDemoWorld : IDemoWorld
private readonly PBVisibilityComponent visibility;
private readonly PBBillboard billboard;

public WarmUpSettingsTextShapeDemoWorld(TextShapeProperties textShapeProperties, BillboardProperties billboardProperties, Func<bool> visible, IFontsStorage fontsStorage) : this(new PBTextShape(), new PBVisibilityComponent(), new PBBillboard(), textShapeProperties, billboardProperties, visible, fontsStorage) { }
public WarmUpSettingsTextShapeDemoWorld(TextShapeProperties textShapeProperties, BillboardProperties billboardProperties, Func<bool> visible, IFontsStorage fontsStorage) : this(new PBTextShape(), new PBVisibilityComponent(), new PBBillboard(), textShapeProperties, billboardProperties, visible, fontsStorage, new SceneStateProvider(), new EmptySceneData(Array.Empty<Vector2Int>())) { }

public WarmUpSettingsTextShapeDemoWorld(
PBTextShape textShape,
Expand All @@ -30,7 +32,9 @@ public WarmUpSettingsTextShapeDemoWorld(
TextShapeProperties textShapeProperties,
BillboardProperties billboardProperties,
Func<bool> visible,
IFontsStorage fontsStorage
IFontsStorage fontsStorage,
ISceneStateProvider sceneStateProvider,
ISceneData sceneData
)
{
this.textShape = textShape;
Expand All @@ -52,6 +56,7 @@ IFontsStorage fontsStorage
new TextShapeDemoWorld(
world,
fontsStorage,
sceneData,
(textShape, visibility, billboard)
)
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"reference": "GUID:fc4fd35fb877e904d8cedee73b2256f6"
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ private void InstantiateRemaining(Entity entity, in TransformComponent transform
// IF there is a visibility component, it will set it invisible in the visibility system
textMeshPro.enabled = true;
changedTextMeshes.Add(entity, component);

// It is necessary to store the first valid bounding box of the TMP in order to calculate its visibility later
component.LastValidBoundingBoxSize = textMeshPro.renderer.bounds.size; // Note: Using Renderer because the bounds of the TMP does not return what we need
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
using DCL.SDKComponents.TextShape.Fonts;
using ECS.Abstract;
using ECS.Unity.Groups;
using TMPro;
using SceneRunner.Scene;
using UnityEngine;
using Utility;

namespace DCL.SDKComponents.TextShape.System
{
Expand All @@ -19,31 +20,83 @@ public partial class UpdateTextShapeSystem : BaseUnityLoopSystem
{
private readonly IFontsStorage fontsStorage;
private readonly MaterialPropertyBlock materialPropertyBlock;
private readonly ParcelMathHelper.SceneGeometry sceneGeometry;

private readonly EntityEventBuffer<TextShapeComponent> changedTextMeshes;

public UpdateTextShapeSystem(World world, IFontsStorage fontsStorage, MaterialPropertyBlock materialPropertyBlock,
EntityEventBuffer<TextShapeComponent> changedTextMeshes) : base(world)
EntityEventBuffer<TextShapeComponent> changedTextMeshes, ISceneData sceneData) : base(world)
{
this.fontsStorage = fontsStorage;
this.materialPropertyBlock = materialPropertyBlock;
this.changedTextMeshes = changedTextMeshes;
this.sceneGeometry = sceneData.Geometry;
}

protected override void Update(float t)
{
UpdateTextsQuery(World!);
// Note: It must occur after UpdateTextsQuery in order to properly calculate the bounds of the text with the latest state,
// and the incoming value of IsDirty flag of the PBTextShape must be available, that's why it is reset in a separate
// query as a final step
CalculateIfTextShapesAreInsideSceneBoundariesQuery(World);
ResetDirtyFlagQuery(World);
}

[Query]
private void UpdateTexts(Entity entity, in TextShapeComponent textShapeComponent, in PBTextShape textShape)
[All(typeof(TextShapeComponent), typeof(PBTextShape))]
private void UpdateTexts(Entity entity, ref TextShapeComponent textShapeComponent, in PBTextShape textShape)
{
if (textShape.IsDirty)
{
textShapeComponent.TextMeshPro.Apply(textShape, fontsStorage, materialPropertyBlock);
changedTextMeshes.Add(entity, textShapeComponent);
textShape.IsDirty = false;
}
}

[Query]
[All(typeof(PBTextShape))]
private void ResetDirtyFlag(PBTextShape textShape)
{
textShape.IsDirty = false;
}

/// <summary>
/// Calculates whether the TextMeshPro labels are inside their scenes or not, according to the bounding box of the
/// label and the boundaries of the scene. It stores the result in the TextShapeComponent.
/// This is checked when the transformations of the label change and when the text of the label changes.
/// </summary>
/// <param name="textShapeComponent">The text shape that contains the TextMeshPro to check.</param>
/// <param name="pbTextShape">The latest state of the text shape in the scene.</param>
[Query]
[All(typeof(TextShapeComponent), typeof(PBTextShape))]
private void CalculateIfTextShapesAreInsideSceneBoundaries(ref TextShapeComponent textShapeComponent, PBTextShape pbTextShape)
{
if (!textShapeComponent.TextMeshPro.transform.hasChanged && !pbTextShape.IsDirty)
return;

// Resets the transform changed flag
textShapeComponent.TextMeshPro.transform.hasChanged = false;

// It has to be immediately rebuilt after its text changes, otherwise it will be updated after this frame and the bounding box will be obsolete
if (pbTextShape.IsDirty)
{
textShapeComponent.TextMeshPro.enabled = true; // It must be enabled, otherwise the bounds will be invalid
textShapeComponent.TextMeshPro.ForceMeshUpdate(true, true);
}

Bounds textWorldBounds = textShapeComponent.TextMeshPro.renderer.bounds; // Note: Using Renderer because the bounds of the TMP does not return what we need

// When the TMP is disabled, the size of its bounds equals zero, so we need to use the latest valid size it had instead
if (!textShapeComponent.TextMeshPro.enabled)
textWorldBounds.size = textShapeComponent.LastValidBoundingBoxSize;

textShapeComponent.IsContainedInScene = textShapeComponent.TextMeshPro.transform.position.y <= sceneGeometry.Height &&
sceneGeometry.CircumscribedPlanes.Contains(textWorldBounds);

// Stores the size of the current bounding box of the TMP while it is enabled
if (textShapeComponent.TextMeshPro.enabled)
textShapeComponent.LastValidBoundingBoxSize = textWorldBounds.size;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Arch.Core;
using Arch.System;
using Arch.SystemGroups;
using DCL.Diagnostics;
using DCL.SDKComponents.TextShape.Component;
Expand All @@ -16,9 +17,26 @@ public partial class VisibilityTextShapeSystem : VisibilitySystemBase<TextShapeC
{
public VisibilityTextShapeSystem(World world, EntityEventBuffer<TextShapeComponent> changedTextMeshes) : base(world, changedTextMeshes) { }

protected override void Update(float t)
{
base.Update(t);
UpdateVisibilityDependingOnSceneBoundariesQuery(World);
}

protected override void UpdateVisibilityInternal(in TextShapeComponent component, bool visible)
{
component.TextMeshPro.enabled = visible;
}

/// <summary>
/// Enables or disables all TextMeshPro labels depending on whether they are fully inside their scenes or not.
/// </summary>
/// <param name="textShape">The text shape whose TextMeshPro will be modified.</param>
[Query]
[All(typeof(TextShapeComponent))]
private void UpdateVisibilityDependingOnSceneBoundaries(ref TextShapeComponent textShape)
{
textShape.TextMeshPro.enabled = textShape.IsContainedInScene;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ private void CheckPrimitive(ref PrimitiveColliderComponent primitiveCollider)
auxiliaryBounds.center = collider.transform.position;
auxiliaryBounds.extents = collider.bounds.extents;
primitiveCollider.SDKCollider.ForceActiveBySceneBounds(auxiliaryBounds.max.y <= sceneGeometry.Height
&& sceneGeometry.CircumscribedPlanes.Intersects(auxiliaryBounds));
&& sceneGeometry.CircumscribedPlanes.Contains(auxiliaryBounds));
}

[Query]
Expand Down Expand Up @@ -123,7 +123,7 @@ void ProcessColliders(List<SDKCollider> colliders)
// While the collider remains inactive, the bounds will continue to be zero, causing incorrect calculations.
// Therefore, it is necessary to force the collider to be activated at least once
sdkCollider.ForceActiveBySceneBounds(auxiliaryBounds.extents == Vector3.zero
|| (auxiliaryBounds.max.y <= sceneGeometry.Height && sceneGeometry.CircumscribedPlanes.Intersects(auxiliaryBounds)));
|| (auxiliaryBounds.max.y <= sceneGeometry.Height && sceneGeometry.CircumscribedPlanes.Contains(auxiliaryBounds)));

// write the structure back
colliders[i] = sdkCollider;
Expand Down
10 changes: 8 additions & 2 deletions Explorer/Assets/Scripts/Utility/ParcelMathHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,14 @@ public static Vector2Int ParcelPosition(this Transform transform) =>
public static Vector2Int ToParcel(this CanBeDirty<Vector3> position) =>
position.Value.ToParcel();

/// <summary>
/// Checks whether a bounding box is fully contained into the XZ boundaries of the scene.
/// </summary>
/// <param name="boundingPlanes">The planes that define the boundaries of the scene.</param>
/// <param name="bounds">The bounding box to check.</param>
/// <returns>True if the box is contained; False otherwise.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool Intersects(this in SceneCircumscribedPlanes boundingPlanes, Bounds bounds)
public static bool Contains(this in SceneCircumscribedPlanes boundingPlanes, Bounds bounds)
{
Vector3 min = bounds.min;
Vector3 max = bounds.max;
Expand All @@ -154,7 +160,7 @@ public static bool Intersects(this in SceneCircumscribedPlanes boundingPlanes, B
/// <param name="point">The point to check.</param>
/// <returns>True if the point intersects the rectangle; False otherwise.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool Intersects(this in SceneCircumscribedPlanes boundingPlanes, Vector3 point) =>
public static bool Contains(this in SceneCircumscribedPlanes boundingPlanes, Vector3 point) =>
boundingPlanes.MinX < point.x && boundingPlanes.MaxX > point.x
&& boundingPlanes.MinZ < point.z && boundingPlanes.MaxZ > point.z;

Expand Down

0 comments on commit acf956e

Please sign in to comment.