Scene builder

The scene builder is a Node that lives in the root of the scene. Its job is to create viewports and hold data about materials the proxies can use. It’s a central node that lets us identify whether a scene uses the toon shader plugin.

Building the builder

Add a new Node named “ToonSceneBuilder” at the root of your toon shaded scene and assign it a new script. Save it in /addons/gdquest.toon-controller/Tools/ToonSceneBuilder.gd

Add the tool keyword so it can run in the editor. We give it a class so Godot adds it to the Add Node dialog (and since it’s inside the addons folder, Godot will list it when the plugin is active).

tool
class_name ToonSceneBuilder
extends Node

Data

As the central access point to the scene, it holds data we use everywhere else:

enum DataType { LIGHT, SPECULAR }

const VIEW_NAMES := ["ToonLightDataView", "ToonSpecularDataView"]

export var shadow_resolution: int = 2048 setget _set_shadow_resolution
export var specular_material: SpatialMaterial
export var white_diffuse_material: SpatialMaterial
export var specular_ignores_shadows := false setget _set_specular_ignores_shadows

var light_data: Viewport
var specular_data: Viewport


func _set_shadow_resolution(value: int) -> void:
    pass


func _set_specular_ignores_shadows(value: bool) -> void:
    pass

Initialize

In _ready, we need to create defaults if they don’t exist.

func _ready() -> void:
    if not specular_material:
        specular_material = SpatialMaterial.new()
        specular_material.albedo_color = Color.black
        specular_material.roughness = 0.4
        specular_material.flags_disable_ambient_light = true

    if not white_diffuse_material:
        white_diffuse_material = SpatialMaterial.new()
        white_diffuse_material.flags_disable_ambient_light = true

We use the root of the scene several times, so we cache it in an onready variable.

We create _find_viewport to take the type of data (light or specular). We use find_node on the root of the scene to look for the viewports.

In the editor, get_tree().root refers to the root of the editor, but in-game, it’s the root of the scene, so a ternary operator differentiates it.

onready var scene_root := get_tree().edited_scene_root if Engine.editor_hint else get_tree().root


func _find_viewport(type: int) -> Viewport:
    var viewport_name: String = VIEW_NAMES[type]

    var container: ViewportContainer = scene_root.find_node(viewport_name)

    if container:
        return container.get_child(0) as Viewport
    else:
        return null

Back in _ready, we find the two viewports.

    light_data = _find_viewport(DataType.LIGHT)
    specular_data = _find_viewport(DataType.SPECULAR)

If we’re in-game, that’s it. If we’re in the editor and if they’re missing, we need to create them with _build_data.

if Engine.editor_hint:
    if not light_data:
        light_data = _build_data(DataType.LIGHT)
    if not specular_data:
        specular_data = _build_data(DataType.SPECULAR)

The function _build_data creates the viewport, sets its data, and returns it.

func _build_data(type: int) -> Viewport:
    var view := ViewportContainer.new()
    view.name = VIEW_NAMES[type]
    view.stretch = true
    view.anchor_right = 1
    view.anchor_bottom = 1
    view.self_modulate.a = 0

    var viewport := Viewport.new()
    viewport.transparent_bg = true
    viewport.world = World.new()
    viewport.usage = Viewport.USAGE_3D_NO_EFFECTS
    viewport.render_target_update_mode = Viewport.UPDATE_ALWAYS
    viewport.msaa = ProjectSettings.get_settings("rendering/quality/filters/msaa")
    viewport.shadow_atlas_size = shadow_resolution

The view belongs to the container, and the container belongs to the scene root. add_child sets that hierarchy. It’s important to set their owner properties, so Godot saves them to the TSCN file.

view.add_child(viewport)

scene_root.add_child(view)
view.owner = scene_root
viewport.owner = scene_root

return viewport

Shadow control

Let’s populate the setters for the shadow control. The first step for the shadow resolution is to set the variable.

Godot calls setters when it creates nodes from the TSCN file, and the light and specular data are not in the scene tree yet. We wait until the class is ready with yield, then set the shadow resolution on the viewports.

Note that the specular data has a ternary if statement for the shadow ignoring boolean.

func _set_shadow_resolution(value: int) -> void:
    shadow_resolution = value

    if not is_inside_tree():
        yield(self, "ready")

    if light_data:
        light_data.shadow_atlas_size = shadow_resolution
    if specular_data:
        specular_data.shadow_atlas_size = (
            0
            if specular_ignores_shadows
            else shadow_resolution
        )

The setter for the boolean is even simpler: we set the value, wait if we need to, then set the shadow_resolution variable. We need to use self because setters of the current class aren’t called without it.

func _set_specular_ignores_shadows(value: bool) -> void:
    specular_ignores_shadows = value

    if not is_inside_tree():
        yield(self, "ready")

    self.shadow_resolution = shadow_resolution

Node order

Render targets must be fully rendered before the objects that use them, and nodes are coarsely rendered from the top of the scene tree to the bottom. We can change the order of nodes with move_child. Godot won’t move nodes when it’s busy creating them, so we have to use call_deferred to delay the moving until the time between frames.

We queue the ToonSceneBuilder’s move before building the viewports in _ready.

if Engine.editor_hint:
    if not get_index() == 0:
        scene_root.call_deferred("move_child", self, 0)

    if not light_data:
        light_data = _build_data(DataType.LIGHT)

    if not specular_data:
        specular_data = _build_data(DataType.SPECULAR)

We queue moving the viewports after adding them as children in _build_data.

scene_root.add_child(view)
scene_root.call_deferred("move_child", view, type + 1)

In conclusion

We have a tool script that, as soon as it’s added to a scene, sets itself to the top of the tree and creates two fully configured viewports. It also holds a reference to those viewports. This will be important for building remote proxies and the plugin.

Reloading the scene creates the viewports.

The ToonSceneBuilder code

tool
class_name ToonSceneBuilder
extends Node

enum DataType { LIGHT, SPECULAR }

const VIEW_NAMES := ["ToonLightDataView", "ToonSpecularDataView"]

export var shadow_resolution: int = 2048 setget _set_shadow_resolution
export var specular_material: SpatialMaterial
export var white_diffuse_material: SpatialMaterial
export var specular_ignores_shadows := false setget _set_specular_ignores_shadows

var light_data: Viewport
var specular_data: Viewport

onready var scene_root := (
    get_tree().edited_scene_root
    if Engine.editor_hint
    else get_tree().root
)


func _ready() -> void:
    if not specular_material:
        specular_material = SpatialMaterial.new()
        specular_material.albedo_color = Color.black
        specular_material.roughness = 0.4
        specular_material.flags_disable_ambient_light = true

    if not white_diffuse_material:
        white_diffuse_material = SpatialMaterial.new()
        white_diffuse_material.flags_disable_ambient_light = true

    light_data = _find_viewport(DataType.LIGHT)
    specular_data = _find_viewport(DataType.SPECULAR)

    if Engine.editor_hint:
        if not get_index() == 0:
            scene_root.call_deferred("move_child", self, 0)

        if not light_data:
            light_data = _build_data(DataType.LIGHT)

        if not specular_data:
            specular_data = _build_data(DataType.SPECULAR)


func _find_viewport(type: int) -> Viewport:
    var viewport_name: String = VIEW_NAMES[type]

    var container: ToonViewportContainer = scene_root.find_node(
        viewport_name, true, false
    )

    if container:
        return container.get_child(0) as Viewport
    else:
        return null


func _build_data(type: int) -> Viewport:
    var view := ToonViewportContainer.new()
    view.name = VIEW_NAMES[type]
    view.stretch = true
    view.anchor_right = 1
    view.anchor_bottom = 1
    view.self_modulate.a = 0

    var viewport := Viewport.new()
    viewport.transparent_bg = true
    viewport.world = World.new()
    viewport.usage = Viewport.USAGE_3D_NO_EFFECTS
    viewport.render_target_update_mode = Viewport.UPDATE_ALWAYS
    viewport.msaa = ProjectSettings.get_setting("rendering/quality/filters/msaa")
    viewport.shadow_atlas_size = shadow_resolution
    view.add_child(viewport)

    scene_root.add_child(view)
    scene_root.call_deferred("move_child", view, type + 1)
    view.owner = scene_root
    viewport.owner = scene_root

    return viewport


func _set_shadow_resolution(value: int) -> void:
    shadow_resolution = value

    if not is_inside_tree():
        yield(self, "ready")

    if light_data:
        light_data.shadow_atlas_size = shadow_resolution
    if specular_data:
        specular_data.shadow_atlas_size = (
            0
            if specular_ignores_shadows
            else shadow_resolution
        )


func _set_specular_ignores_shadows(value: bool) -> void:
    specular_ignores_shadows = value

    if not is_inside_tree():
        yield(self, "ready")

    self.shadow_resolution = shadow_resolution