Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Expose and cleanup editor gizmos #2127

Merged
merged 4 commits into from
Jul 26, 2024
Merged

Conversation

Eideren
Copy link
Collaborator

@Eideren Eideren commented Jan 26, 2024

PR Details

This PR provides support for user-defined gizmos, most of the logic was already there, I just had to clean out the editor only stuff from the api.

Here's a simple example:

[GizmoComponent(typeof(TestClass), isMainGizmo:false/*When true, only the first gizmo on an entity with true is visible, false means that it is always visible*/)]
public class Gizmo : IEntityGizmo
{
    private bool _selected, _enabled;
    private TestClass _component;
    private ModelComponent _model;
    private Material _material, _materialOnSelect;

    public bool IsEnabled
    {
        get
        {
            return _enabled;
        }
        set
        {
            _enabled = value;
            _model.Enabled = _enabled;
        }
    }

    public float SizeFactor { get; set; }

    public bool IsSelected
    {
        get
        {
            return _selected;
        }
        set
        {
            _selected = value;
            _model.Materials[0] = _selected ? _materialOnSelect : _material;
            // The logic below shows gizmos for all components when they are on in the gizmo settings, and when off, only shows the one from the selected entity
            // Removing the line hides gizmos even when selected when the gizmo settings is off
            _model.Enabled = _selected || _enabled;
        }
    }

    public Gizmo(TestClass component) // This constructor is called by the editor
    {
        _component = component;
    }

    public bool HandlesComponentId(OpaqueComponentId pickedComponentId, out Entity? selection)
    {
        // This function is called when scene picking/mouse clicking in the scene on a gizmo
        // The engine calls this function on each gizmos, gizmos in term notify the engine
        // when the given component comes from them, and provide the editor with the corresponding entity this gizmo represents
        if (pickedComponentId.Match(_model))
        {
            selection = _component.Entity;
            return true;
        }
        selection = null;
        return false;
    }

    public void Initialize(IServiceRegistry services, Scene editorScene)
    {
        var graphicsDevice = services.GetSafeServiceAs<IGraphicsDeviceService>().GraphicsDevice;

        var sphere = GeometricPrimitive.Sphere.New(graphicsDevice);

        var vertexBuffer = sphere.VertexBuffer;
        var indexBuffer = sphere.IndexBuffer;
        var vertexBufferBinding = new VertexBufferBinding(vertexBuffer, new VertexPositionNormalTexture().GetLayout(), vertexBuffer.ElementCount);
        var indexBufferBinding = new IndexBufferBinding(indexBuffer, sphere.IsIndex32Bits, indexBuffer.ElementCount);

        _material = Material.New(graphicsDevice, new MaterialDescriptor
        {
            Attributes =
            {
                Emissive = new MaterialEmissiveMapFeature(new ComputeColor(new Color4(0.25f,0.75f,0.25f,0.05f).ToColorSpace(graphicsDevice.ColorSpace))) { UseAlpha = true },
                Transparency = new MaterialTransparencyBlendFeature()
            },
        });
        _materialOnSelect = Material.New(graphicsDevice, new MaterialDescriptor
        {
            Attributes =
            {
                Emissive = new MaterialEmissiveMapFeature(new ComputeColor(new Color4(0.25f,0.75f,0.25f,0.5f).ToColorSpace(graphicsDevice.ColorSpace))) { UseAlpha = true },
                Transparency = new MaterialTransparencyBlendFeature()
            },
        });

        _model = new ModelComponent
        {
            Model = new Model
            {
                (_selected ? _materialOnSelect : _material),
                new Mesh
                {
                    Draw = new MeshDraw
                    {
                        StartLocation = 0,
                        // You can swap to LineList or LineStrip to show the model in wireframe mode, you'll have to adapt your index buffer to that new type though
                        PrimitiveType = PrimitiveType.TriangleList,
                        VertexBuffers = new[] { vertexBufferBinding },
                        IndexBuffer = indexBufferBinding,
                        DrawCount = indexBuffer.ElementCount,
                    }
                }
            },
            RenderGroup = IEntityGizmo.PickingRenderGroup, // This RenderGroup allows scene picking/selection, use a different one if you don't want selection
            Enabled = _selected || _enabled
        };

        var entity = new Entity($"{nameof(Gizmo)} for {_component.Entity.Name}"){ _model };
        entity.Transform.UseTRS = false; // We're controlling the matrix directly in this case
        entity.Scene = editorScene;

        vertexBuffer.DisposeBy(entity);
        indexBuffer.DisposeBy(entity); // Attach buffers to the entity for manual disposal later
    }

    public void Dispose()
    {
        _model.Entity.Scene = null;
        _model.Entity.Dispose(); // Clear the two buffers we attached above
    }

    public void Update()
    {
        // Ensures the gizmo follows the entity it is representing, note that UseTRS is disabled above to improve performance and ensure that there are no world space issues
        _model.Entity.Transform.LocalMatrix = _component.Entity.Transform.WorldMatrix;
    }
}

image

Types of changes

  • Docs change / refactoring / dependency upgrade
  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist

  • My change requires a change to the documentation. - I'll take care of that once this is in
  • I have added tests to cover my changes.
  • All new and existing tests passed.
  • I have built and run the editor to try this change out.

@VaclavElias
Copy link
Contributor

Ohh, does it mean, I can access it through code only and add to any entity as it is just a regular component?

@Eideren
Copy link
Collaborator Author

Eideren commented Jan 26, 2024

@VaclavElias It's not really a component, it only inherits from the IEntityGizmo interface.
The editor has facilities to automate the spawning and management of those gizmos when creating and destroying components with gizmos. The player as no such thing built in, but it's not that hard to implement on your side: create an instance alongside the component you want to have a gizmo on, call Initialize on the instance after creation, then Update on every update, and dispose when the component is removed from the scene. Once I'm looking at creating documentation for this I'll see if I can setup a processor or similar to automatically toggle gizmos on and off in a running game.

@VaclavElias
Copy link
Contributor

Thanks for the hints as usually. I will try it.

Yes, it could be useful to enable/disable it through running game as I found it still very useful even now, to orient in the 3D, when debugging/testing running game.

Also, if I may suggest. Adding optionally text X,Y,Z would be also helpful.

@Kryptos-FR Kryptos-FR self-requested a review January 26, 2024 17:07
@Eideren Eideren mentioned this pull request Jan 28, 2024
28 tasks
# Conflicts:
#	sources/editor/Stride.Assets.Presentation/StrideDefaultAssetsPlugin.cs
Copy link
Member

@Kryptos-FR Kryptos-FR left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A couple of remarks reading the code. I'll try it locally next before approving.

sources/engine/Stride.Engine/Engine/Gizmos/IEntityGizmo.cs Outdated Show resolved Hide resolved
sources/engine/Stride.Engine/Engine/OpaqueComponentId.cs Outdated Show resolved Hide resolved
/// <summary>
/// Contains the id of a component
/// </summary>
public readonly ref struct OpaqueComponentId
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this struct?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll quickly cover the way picking works just to make sure we're on the same page;
Scene picking renders models and reads the component id of the one under the mouse cursor, so, those ids come from the rendering components, not the component the gizmo is observing.
The gizmo can freely create any kind of component or mesh, so we don't know in advance which model component map to which gizmos. We have to ask the gizmo whether that id comes from them.
This logic lives in user-land, and we can't just give the user a random int and expect him to figure it out, so this thin api over the int should be more intuitive as to how users should work with that particular part of the gizmo api.

This is one of the weaker area of the api I feel, so if you can think of something that is more intuitive than this, feel free to let me know.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for clarifying. As long as the documentation mentions that, it's fine for me. It is a simple solution and probably good enough for now. We can refine later if more usage scenarios appear.

}
catch (MissingMethodException e)
{
throw new MissingMethodException($"{nameof(IEntityGizmo)} '{gizmoType}' must have a constructor with exactly one parameter of type '{component.GetType()}'", e);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was the default exception message not satisfying?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would output System.MissingMethodException: Constructor on type 'Gizmo' not found., it is a bit misleading since you may very well have a constructor on that type, but we require a constructor with exactly one argument of a specific type.
Just making things clearer for our users.

@Kryptos-FR
Copy link
Member

Kryptos-FR commented Jul 24, 2024

Looks like it can only work for a user gizmo if isMainGizmo is true. In my testing, it doesn't show when false. @Eideren Is that expected?

@Eideren
Copy link
Collaborator Author

Eideren commented Jul 24, 2024

@Kryptos-FR it is not, I have two entities here, one with the component as first, and the other one as second, both show up with isMainGizmo:false as expected
image
Can you send a repro ?

@Kryptos-FR
Copy link
Member

@Eideren looks like it is working now. Maybe I needed to restart the editor. LGTM.

@Eideren Eideren changed the title [Editor][Plugin] Expose and cleanup gizmos feat: Expose and cleanup editor gizmos Jul 26, 2024
@Eideren Eideren merged commit 8dd9a99 into stride3d:master Jul 26, 2024
2 checks passed
@Eideren
Copy link
Collaborator Author

Eideren commented Jul 26, 2024

Thanks for reviewing @Kryptos-FR

@Eideren Eideren deleted the gizmo branch July 26, 2024 10:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants