In this chapter, we’ll make the proxy builder apply materials and react to originals being renamed or deleted.
The proxy has two more jobs: making sure the proxies’ settings match the export variables and applying materials on the proxies.
We start with the _set_materials
function that takes a parent and a data type. This will be a recursive function that calls itself, replacing the parent by each child (which in turn is parent to another) until we can’t find any more MeshInstance
to apply materials to. This is because it could be a Spatial
that holds several meshes instead of a single MeshInstance
. The user shouldn’t have to worry about that.
For safety, we stop the function if the parent is null
.
func _set_materials(parent: Node, type: int) -> void: if not parent: return
If the parent is a MeshInstance
, we apply materials. We get the number of surfaces and iterate over them in a for
loop.
if parent is MeshInstance: var mat_count: int = parent.get_surface_material_count() for mat in range(mat_count):
We check the datatype and apply the correct material. Note the ternary operator in the specular material; if we have an override, we apply it, otherwise we use the default one in ToonSceneBuilder
.
match type: ToonSceneBuilder.DataType.LIGHT: parent.set_surface_material(mat, builder.white_diffuse_material) ToonSceneBuilder.DataType.SPECULAR: parent.set_surface_material(mat, specular_material if specular_material else builder.specular_material)
Then, outside of the if
block, we apply the recursion. We call the function again for each children.
for child in parent.get_children(): _set_materials(child, type)
We can call _set_materials
in the _ready
function at the end to make sure any proxies have the right materials. This should happen in both the editor and in game, so it should be outside of the Engine.editor_hint
if
block.
func _ready() -> void: #... if Engine.editor_hint: #... _set_materials(light_proxy, ToonSceneBuilder.DataType.LIGHT) _set_materials(specular_proxy, ToonSceneBuilder.DataType.SPECULAR)
We have some setter functions to populate. We start with _set_light_role
. It sets the light color to red, green or blue in the light data and disables all but the key light in the specular data.
Like previous setters, we apply the value, check if we’re in the editor and yield until ready.
func _set_light_role(value: int) -> void: light_role = value if not Engine.editor_hint: return if not is_inside_tree(): yield(self, "ready")
If the proxy is a light, we apply the appropriate settings: color for light data, energy and visibility in the specular data.
if light_proxy is Light: match light_role: LightRole.KEY: light_proxy.light_color = Color.red LightRole.FILL: light_proxy.light_color = Color.green LightRole.KICK: light_proxy.light_color = Color.blue if specular_proxy is Light: match light_role: LightRole.KEY: specular_proxy.light_energy = 1 specular_proxy.show() LightRole.FILL: specular_proxy.light_energy = 0 specular_proxy.hide() LightRole.KICK: specular_proxy.light_energy = 0 specular_proxy.hide()
Why not delete the fill and kick light from the specular? It’d make for a cleaner scene tree, but it’s far easier to call show()
or hide()
than to detect the need to add a brand new light or delete an existing one.
For _sets_emits_shadows
, we toggle the shadow_enabled
property on any lights we find.
func _set_emits_shadows(value: bool) -> void: emits_shadows = value if not Engine.editor_hint: return if not is_inside_tree(): yield(self, "ready") if light_proxy is Light: light_proxy.shadow_enabled = emits_shadows specular_proxy.shadow_enabled = emits_shadows
We use the _set_materials
functions we created to apply the specular material when it changes. If the user clears it, it becomes null
and the ToonSceneBuilder
’s will be used. Otherwise, it applies the new one.
func _set_specular_material(value: ShaderMaterial) -> void: specular_material = value if not is_inside_tree(): yield(self, "ready") _set_materials(specular_proxy, ToonSceneBuilder.DataType.SPECULAR)
When the designer renames a node, the proxy should rename the original too. Names are how we find them at the start of the game or in the editor and if they don’t match, the proxy will create a brand new node instead.
In the Engine.editor_hint
if block inside of _ready
, we connect to the parent node’s renamed
signal. We also connect to the tree_exiting
signal to keep track of the user deleting an original.
But when the scene exits or changes, tree_exiting
also happens, so we need a mechanism to detect that case. We connect to the scene root’s tree_exiting
, too. If tree_exiting
happens on both, then the scene is transitioning and we don’t delete the proxy.
if Engine.editor_hint: _build_missing_proxies(parent) parent.connect("renamed", self, "_on_parent_renamed") parent.connect("tree_exiting", self, "_on_parent_tree_exiting") scene_root.connect("tree_exiting", self, "_on_root_tree_exiting")
When the user renames the node, we have to apply the new name to the proxy and update the RemoteTransform
’s path to the new name.
func _on_parent_renamed() -> void: if light_proxy: light_proxy.name = get_parent().name light_remote.remote_path = ( "../%s" % get_parent().get_path_to(light_proxy) ) if specular_proxy: specular_proxy.name = get_parent().name specular_remote.remote_path = ( "../%s" % get_parent().get_path_to(specular_proxy) )
When the user deletes the parent, we set an abort_deletion
class variable to false, and connect to the scene tree’s idle_frame
signal with a function. Waiting for a frame lets us see if the scene root also exits. If it does, then we abort. Otherwise, then we can clean up the proxies from the scene tree.
Why not use a yield? A yield state of a deleted object cannot resume. Godot throws an error and fails to delete the proxies. The signal, meanwhile, lives in the scene tree object and persists.
Note the use of the CONNECT_ONESHOT
flag. If the scene root does not get deleted, we don’t want it to keep triggering every frame on an invalid object.
func _on_parent_tree_exiting() -> void: abort_deletion = false Engine.get_main_loop().connect("idle_frame", self, "_on_SceneTree_idle_frame", [], CONNECT_ONESHOT) func _on_SceneTree_idle_frame() -> void: if abort_deletion: return if light_proxy: light_proxy.queue_free() if specular_proxy: specular_proxy.queue_free() func _on_root_tree_exiting() -> void: abort_deletion = true
Save your work and reload the scene. The proxy builder automatically populates the viewports with a proxy and ensures they have the correct materials. It keeps their name in sync and cleans up when the parent cleans up.
tool class_name ToonProxyBuilder extends Node enum LightRole { KEY, FILL, KICK } export (LightRole) var light_role := 0 setget _set_light_role export var emits_shadows := false setget _set_emits_shadows export var specular_material: ShaderMaterial setget _set_specular_material var light_proxy: Node var specular_proxy: Node var light_remote: RemoteTransform var specular_remote: RemoteTransform var abort_deletion := false onready var scene_root: Node = ( get_tree().edited_scene_root if Engine.editor_hint else get_tree().root ) onready var builder: ToonSceneBuilder = scene_root.find_node( "ToonSceneBuilder", true, false ) func _ready(): if not builder: return var parent := get_parent() light_proxy = builder.light_data.find_node(parent.name, true, false) specular_proxy = builder.specular_data.find_node(parent.name, true, false) if Engine.editor_hint: _build_missing_proxies(parent) parent.connect("renamed", self, "_on_parent_renamed") parent.connect("tree_exiting", self, "_on_parent_tree_exiting") scene_root.connect("tree_exiting", self, "_on_root_tree_exiting") _set_materials(light_proxy, ToonSceneBuilder.DataType.LIGHT) _set_materials(specular_proxy, ToonSceneBuilder.DataType.SPECULAR) func _build_missing_proxies(parent: Node) -> void: var light_missing: bool = light_proxy == null var specular_missing: bool = specular_proxy == null yield(get_tree(), "idle_frame") if light_missing or specular_missing: parent.remove_child(self) if light_missing: var result := _build_remote_duplicates( parent, ToonSceneBuilder.DataType.LIGHT ) light_proxy = result.proxy light_remote = result.proxy_remote else: light_remote = parent.find_node("LightRemote", true, false) if specular_missing: var result := _build_remote_duplicates( parent, ToonSceneBuilder.DataType.SPECULAR ) specular_proxy = result.proxy specular_remote = result.proxy_remote else: specular_remote = parent.find_node("SpecularRemote", true, false) self.light_role = light_role self.emits_shadows = emits_shadows if light_missing or specular_missing: parent.add_child(self) owner = scene_root func _build_remote_duplicates(parent: Node, type: int) -> Dictionary: var proxy: Node = parent.duplicate() var proxy_remote := RemoteTransform.new() parent.add_child(proxy_remote) proxy_remote.owner = scene_root match type: ToonSceneBuilder.DataType.LIGHT: builder.light_data.add_child(proxy) if proxy is Light: proxy.light_color = Color.red proxy_remote.name = "LightRemote" ToonSceneBuilder.DataType.SPECULAR: builder.specular_data.add_child(proxy) proxy_remote.name = "SpecularRemote" proxy.owner = scene_root proxy_remote.remote_path = "../%s" % parent.get_path_to(proxy) return {"proxy": proxy, "proxy_remote": proxy_remote} func _set_materials(parent: Node, type: int) -> void: if not parent: return if parent is MeshInstance: var mat_count: int = parent.get_surface_material_count() for mat in range(mat_count): match type: ToonSceneBuilder.DataType.LIGHT: parent.set_surface_material(mat, builder.white_diffuse_material) ToonSceneBuilder.DataType.SPECULAR: parent.set_surface_material(mat, specular_material if specular_material else builder.specular_material) for child in parent.get_children(): _set_materials(child, type) func _set_light_role(value: int) -> void: light_role = value if not Engine.editor_hint: return if not is_inside_tree(): yield(self, "ready") if light_proxy is Light: match light_role: LightRole.KEY: light_proxy.light_color = Color.red LightRole.FILL: light_proxy.light_color = Color.green LightRole.KICK: light_proxy.light_color = Color.blue if specular_proxy is Light: match light_role: LightRole.KEY: specular_proxy.light_energy = 1 specular_proxy.show() LightRole.FILL: specular_proxy.light_energy = 0 specular_proxy.hide() LightRole.KICK: specular_proxy.light_energy = 0 specular_proxy.hide() func _set_emits_shadows(value: bool) -> void: emits_shadows = value if not Engine.editor_hint: return if not is_inside_tree(): yield(self, "ready") if light_proxy is Light: light_proxy.shadow_enabled = emits_shadows specular_proxy.shadow_enabled = emits_shadows func _set_specular_material(value: ShaderMaterial) -> void: specular_material = value if not is_inside_tree(): yield(self, "ready") _set_materials(specular_proxy, ToonSceneBuilder.DataType.SPECULAR) func _on_parent_renamed() -> void: if light_proxy: light_proxy.name = get_parent().name light_remote.remote_path = ( "../%s" % get_parent().get_path_to(light_proxy) ) if specular_proxy: specular_proxy.name = get_parent().name specular_remote.remote_path = ( "../%s" % get_parent().get_path_to(specular_proxy) ) func _on_parent_tree_exiting() -> void: abort_deletion = false Engine.get_main_loop().connect("idle_frame", self, "_on_SceneTree_idle_frame", [], CONNECT_ONESHOT) func _on_SceneTree_idle_frame() -> void: if abort_deletion: return if light_proxy: light_proxy.queue_free() if specular_proxy: specular_proxy.queue_free() func _on_root_tree_exiting() -> void: abort_deletion = true