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.
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
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
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
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
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)
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.
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