Adding a custom button to the ToonSceneBuilder

With plugins, we can do more than react. We can change the inspector to inject new functionality. In this tutorial, we look at how to add a button whose job is to find mesh instances and connect the light and specular viewports to their shaders.

Register a custom inspector plugin

Create a new script next to plugin.gd called ToonInspector.gd. This is a tool script that extends EditorInspectorPlugin. Its only variable is a single button.

tool
class_name ToonInspector
extends EditorInspectorPlugin

var connect_button: Button

Check if current object is the right one

The button only appears when the ToonSceneBuilder is selected, so we need a can_handle function to return true when that is the selected object’s inspector. Otherwise, we return false.

func can_handle(object: Object) -> bool:
    return object is ToonSceneBuilder

Adding the button happens in parse_category. Godot calls this when populating the inspector. We don’t care about the category, but we do care about the object type.

func parse_category(object: Object, category: String) -> void:
    if object is ToonSceneBuilder:

In that if block, we create the button if it’s missing and connect to its signal. Then we call add_custom_control to add the button to the list. Note how we pass object along into the signal connection. It’s the ToonSceneBuilder and we need it to access the viewports.

        if not connect_button:
            connect_button = Button.new()
            connect_button.text = "Connect ViewportTextures"
            connect_button.connect(
                "button_down", self, "_on_Connect_Button_down", [object]
            )
            add_custom_control(connect_button)

React on signal

In _on_Connect_Button_down, we grab the light and specular data, create ViewportTextures with them, then find every mesh instance that has a ShaderMaterial that has a light_data and specular_data parameter and apply that texture to it.

func _on_Connect_Button_down(builder: ToonSceneBuilder) -> void:
    var light_data: Viewport = builder.light_data
    var specular_data: Viewport = builder.specular_data

    var light_texture := light_data.get_texture()
    var specular_texture := specular_data.get_texture()

Much like nodes created in the editor but not having the owner property set to save properly, viewport textures need a viewport_path to save. This path is relative to the root of the scene, so we grab it from the editor.

var top_parent: Node = Engine.get_main_loop().edited_scene_root

light_texture.viewport_path = top_parent.get_path_to(light_data)
specular_texture.viewport_path = top_parent.get_path_to(specular_data)

Finally, we call a function _set_materials that recursively sets all MeshInstance materials appropriately.

    _set_materials(top_parent, light_texture, specular_texture)


func _set_materials(
    parent: Node, light_data: ViewportTexture, specular_data: ViewportTexture
) -> void:
    if parent is MeshInstance:

We loop over each mesh instance’s materials count. If it’s a ShaderMaterial, we call set_shader_param to set it to the new viewport textures. If the shader doesn’t have that parameter, then no harm. Otherwise, it’s set accordingly.

    for mat in parent.get_surface_material_count():
        var material: Material = parent.get_surface_material(mat)
        if material and material is ShaderMaterial:
            material.set_shader_param("light_data", light_data)
            material.set_shader_param(
                "specular_data", specular_data
            )

Regardless of the outcome, we recursively step through each of the node’s children to find any further mesh instance.

for child in parent.get_children():
    _set_materials(child, light_data, specular_data)

In conclusion

This, above all, highlights that the Godot editor really is a video game cleverly disguised as an editor running on its own game engine. You can apply textures, buttons, and other GUI controls to the inspector, if you know how to look for it.

tool
class_name ToonInspector
extends EditorInspectorPlugin

var connect_button: Button


func can_handle(object: Object) -> bool:
    return object is ToonSceneBuilder


func parse_category(object: Object, category: String) -> void:
    if object is ToonSceneBuilder:
        if not connect_button:
            connect_button = Button.new()
            connect_button.text = "Connect ViewportTextures"
            connect_button.connect(
                "button_down", self, "_on_Connect_Button_down", [object]
            )
            add_custom_control(connect_button)


func _on_Connect_Button_down(builder: ToonSceneBuilder) -> void:
    var light_data: Viewport = builder.light_data
    var specular_data: Viewport = builder.specular_data

    var light_texture := light_data.get_texture()
    var specular_texture := specular_data.get_texture()
    
    var top_parent: Node = Engine.get_main_loop().edited_scene_root

    light_texture.viewport_path = top_parent.get_path_to(light_data)
    specular_texture.viewport_path = top_parent.get_path_to(specular_data)
    _set_materials(top_parent, light_texture, specular_texture)


func _set_materials(
    parent: Node, light_data: ViewportTexture, specular_data: ViewportTexture
) -> void:
    if parent is MeshInstance:
        for mat in parent.get_surface_material_count():
            var material: Material = parent.get_surface_material(mat)
            if material and material is ShaderMaterial:
                material.set_shader_param("light_data", light_data)
                material.set_shader_param(
                    "specular_data", specular_data
                )
    for child in parent.get_children():
        _set_materials(child, light_data, specular_data)