Table of Contents

Gizmo

中级 程序员

Gizmo 是一种可以在组件上实现的工具,用于在设计师调整组件值时提供可视化的辅助。

下面是一个可以实现的完整示例:

using Stride.Core;
using Stride.Core.Mathematics;
using Stride.Engine;
using Stride.Engine.Gizmos;
using Stride.Graphics;
using Stride.Graphics.GeometricPrimitives;
using Stride.Rendering;
using Stride.Rendering.Materials;
using Stride.Rendering.Materials.ComputeColors;

// 我们将为下面这个类实现一个 Gizmo
public class MyScript : StartupScript
{
    
}

// 这个特性向引擎说明下面的 gizmo 类绑定到 'MyScript',
// Game Studio 会识别它,并为场景中的每个 'MyScript' 生成该类的实例
[GizmoComponent(typeof(MyScript), isMainGizmo:false/*为 true 时,实体上只有第一个为 true 的 gizmo 可见,为 false 表示始终可见*/)]
public class Gizmo : IEntityGizmo
{
    private bool _selected, _enabled;
    private MyScript _component;
    private ModelComponent _model;
    private Material _material, _materialOnSelect;

    // 当编辑器视图设置中全局开启或关闭 gizmo 时,会设置这个属性
    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;
            // 下面的逻辑让 gizmo 在设置开启时对所有组件显示,
            // 关闭时只显示所选实体的 gizmo
            // 删除这一行会让 gizmo 在设置关闭时即使被选中也不显示
            _model.Enabled = _selected || _enabled;
        }
    }

    // 此构造函数由编辑器调用,
    // Gizmo 类必须包含一个带单个组件类型参数的构造函数
    public Gizmo(MyScript component)
    {
        _component = component;
    }

    public bool HandlesComponentId(OpaqueComponentId pickedComponentId, out Entity? selection)
    {
        // 当在场景中对 gizmo 进行场景拾取/鼠标点击时会调用此函数,
        // 引擎会逐个调用每个 gizmo,gizmo 又通过返回 true 来通知引擎
        // 该组件来自它们,并向编辑器提供此 gizmo 所代表的对应实体
        if (pickedComponentId.Match(_model))
        {
            selection = _component.Entity;
            return true;
        }
        selection = null;
        return false;
    }

    public void Initialize(IServiceRegistry services, Scene editorScene)
    {
        // 作为初始化的一部分,我们在编辑器场景中创建一个模型来可视化 gizmo
        var graphicsDevice = services.GetSafeServiceAs<IGraphicsDeviceService>().GraphicsDevice;

        // 在我们的例子中,我们直接依赖 GeometricPrimitive API 为我们创建一个球体
        // 你不必立即创建它,可以延迟到组件处于合适的状态时再创建
        // 你也可以在下面的 Update() 函数中动态地创建和更新它
        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,
                        // 你可以换成 LineList 或 LineStrip,以线框模式显示模型,
                        // 不过需要相应地调整你的索引缓冲区类型
                        PrimitiveType = PrimitiveType.TriangleList,
                        VertexBuffers = new[] { vertexBufferBinding },
                        IndexBuffer = indexBufferBinding,
                        DrawCount = indexBuffer.ElementCount,
                    }
                }
            },
            RenderGroup = IEntityGizmo.PickingRenderGroup, // 这个 RenderGroup 允许场景拾取/选择,如果不想要选择功能请换用另一个
            Enabled = _selected || _enabled
        };

        var entity = new Entity($"{nameof(Gizmo)} for {_component.Entity.Name}"){ _model };
        entity.Transform.UseTRS = false; // 在本例中我们直接控制矩阵
        entity.Scene = editorScene;

        vertexBuffer.DisposeBy(entity);
        indexBuffer.DisposeBy(entity); // 将缓冲区附加到实体上,以便稍后手动释放
    }

    public void Dispose()
    {
        _model.Entity.Scene = null;
        _model.Entity.Dispose(); // 清除我们上面附加的两个缓冲区
    }

    public void Update()
    {
        // 在这里根据 MyScript 的状态更新 gizmo 的外观
        // 这里我们只是确保 gizmo 跟随它所代表的实体移动,
        // 注意上面禁用了 UseTRS 以提升性能并确保不会出现世界空间的问题
        _model.Entity.Transform.LocalMatrix = _component.Entity.Transform.WorldMatrix;
    }
}

效果如下:

绿色球体 gizmo

请注意,如果你在编辑器处于打开状态时添加了这个新的 gizmo,可能需要重启编辑器。