mirror of
https://github.com/godotengine/godot-demo-projects.git
synced 2026-01-04 15:00:09 +01:00
Refactor Material Creator Plugin Demo to use custom resource-based data (#1219)
This commit is contained in:
@@ -1,20 +1,25 @@
|
|||||||
# Material Creator Plugin Demo
|
# Material Creator Plugin Demo
|
||||||
|
|
||||||
This plugin demo contains a custom material creator
|
This plugin demo contains a custom material creation dock
|
||||||
interface using a custom dock in the editor.
|
inside the Godot editor.
|
||||||
|
|
||||||
Custom docks are made of Control nodes, they run in the
|
Custom docks are made of Control nodes, they run in the
|
||||||
editor, and any behavior must be done through `tool` scripts.
|
editor, and any behavior must be done through `tool` scripts.
|
||||||
For more information, see this documentation article:
|
For more information, see this documentation article:
|
||||||
https://docs.godotengine.org/en/latest/tutorials/plugins/editor/making_plugins.html#a-custom-dock
|
https://docs.godotengine.org/en/latest/tutorials/plugins/editor/making_plugins.html#a-custom-dock
|
||||||
|
|
||||||
This plugin allows you to specify color, metallic, and
|
## Features
|
||||||
roughness values, and then use it as a material.
|
- Adjust albedo color, metallic and rouphness values interactively.
|
||||||
|
- Apply the generated material to selected 3D nodes in the editor.
|
||||||
|
- Save and load materials in two ways:
|
||||||
|
- `.silly_mat`: Custom Godot Resource type, handled by custom saver/loader
|
||||||
|
included in the plygin.
|
||||||
|
- `.mtxt`: Plain-text format. Useful for external editing or as an
|
||||||
|
interchange format.
|
||||||
|
- `.tres`: Standard Godot resource format (works without the custom
|
||||||
|
loader).
|
||||||
|
|
||||||
You can apply this material directly to Spatial
|
## Implementation notes
|
||||||
nodes by selecting them and then clicking "Apply".
|
- `.silly_mat` format is registered through `SillyMatFormatSaver` and
|
||||||
This shows how a plugin can interact closely with the
|
`SillyMatFormatLoader` in the plugin.
|
||||||
editor, manipulating nodes the user selects.
|
- Custm docks are built from `Control` nodes and run as `@tool` scripts.
|
||||||
|
|
||||||
Alternatively, you can also save the material to
|
|
||||||
a file, and then load it back into the plugin later.
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
extends Panel
|
extends Panel
|
||||||
# In this file, the word "silly" is used to make it obvious that the name is arbitrary.
|
# In this file, the word "silly" is used to make it obvious that the name is arbitrary.
|
||||||
|
|
||||||
var silly_material_resource = preload("res://addons/material_creator/material_resource.gd")
|
var silly_material_resource := preload("res://addons/material_creator/material_resource.gd")
|
||||||
var editor_interface: EditorInterface
|
var editor_interface: EditorInterface
|
||||||
|
|
||||||
|
|
||||||
@@ -11,8 +11,26 @@ func _ready() -> void:
|
|||||||
$VBoxContainer/ApplyButton.pressed.connect(apply_pressed)
|
$VBoxContainer/ApplyButton.pressed.connect(apply_pressed)
|
||||||
$VBoxContainer/SaveButton.pressed.connect(save_pressed)
|
$VBoxContainer/SaveButton.pressed.connect(save_pressed)
|
||||||
$VBoxContainer/LoadButton.pressed.connect(load_pressed)
|
$VBoxContainer/LoadButton.pressed.connect(load_pressed)
|
||||||
$SaveMaterialDialog.file_selected.connect(save_file_selected)
|
|
||||||
|
$SaveMaterialDialog.file_mode = FileDialog.FILE_MODE_SAVE_FILE
|
||||||
|
$SaveMaterialDialog.access = FileDialog.ACCESS_RESOURCES
|
||||||
|
$SaveMaterialDialog.current_dir = "res://materials"
|
||||||
|
$SaveMaterialDialog.current_file = "new_material.silly_mat"
|
||||||
|
$SaveMaterialDialog.filters = PackedStringArray([
|
||||||
|
"*.silly_mat ; Silly Material (resource)",
|
||||||
|
"*.tres ; Godot Resource (resource)",
|
||||||
|
"*.mtxt ; Silly Material (source)"
|
||||||
|
])
|
||||||
|
$SaveMaterialDialog.confirmed.connect(_on_save_confirmed)
|
||||||
|
|
||||||
|
$LoadMaterialDialog.access = FileDialog.ACCESS_RESOURCES
|
||||||
|
$LoadMaterialDialog.filters = PackedStringArray([
|
||||||
|
"*.silly_mat ; Silly Material (resource)",
|
||||||
|
"*.tres ; Godot Resource (resource)",
|
||||||
|
"*.mtxt ; Silly Material (source)"
|
||||||
|
])
|
||||||
$LoadMaterialDialog.file_selected.connect(load_file_selected)
|
$LoadMaterialDialog.file_selected.connect(load_file_selected)
|
||||||
|
|
||||||
RenderingServer.canvas_item_set_clip(get_canvas_item(), true)
|
RenderingServer.canvas_item_set_clip(get_canvas_item(), true)
|
||||||
|
|
||||||
|
|
||||||
@@ -24,12 +42,55 @@ func load_pressed() -> void:
|
|||||||
$LoadMaterialDialog.popup_centered_ratio()
|
$LoadMaterialDialog.popup_centered_ratio()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_save_confirmed() -> void:
|
||||||
|
var path = $SaveMaterialDialog.get_current_path()
|
||||||
|
if path.is_empty():
|
||||||
|
push_error("Material Creator: No path chosen for saving.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# If user typed no extension, default to .silly_mat (resource path).
|
||||||
|
if not path.get_file().contains("."):
|
||||||
|
path += ".silly_mat"
|
||||||
|
|
||||||
|
var ext = path.get_extension().to_lower()
|
||||||
|
|
||||||
|
# Ensure directory exists under res:// when saving inside project.
|
||||||
|
var dir = path.get_base_dir()
|
||||||
|
if path.begins_with("res://") and not DirAccess.dir_exists_absolute(dir):
|
||||||
|
var mk := DirAccess.make_dir_recursive_absolute(dir)
|
||||||
|
if mk != OK:
|
||||||
|
push_error("Material Creator: Can't create folder: %s (%s)" % [dir, error_string(mk)])
|
||||||
|
return
|
||||||
|
|
||||||
|
var res: Resource = _silly_resource_from_values()
|
||||||
|
|
||||||
|
match ext:
|
||||||
|
"mtxt":
|
||||||
|
# Write SOURCE file (no ResourceSaver, works anywhere).
|
||||||
|
var ok := _write_source_silly(path, res)
|
||||||
|
if not ok:
|
||||||
|
push_error("Material Creator: Failed to write source .mtxt at %s" % path)
|
||||||
|
else:
|
||||||
|
print("Material Creator: Wrote source to ", path)
|
||||||
|
"silly_mat", "tres":
|
||||||
|
# Save RESOURCE (requires your custom saver for .silly_mat).
|
||||||
|
res.resource_path = path
|
||||||
|
var err := ResourceSaver.save(res, path)
|
||||||
|
if err != OK:
|
||||||
|
push_error("Material Creator: Failed to save resource: %s (%s)" % [path, error_string(err)])
|
||||||
|
else:
|
||||||
|
print("Material Creator: Saved resource to ", path)
|
||||||
|
_:
|
||||||
|
push_error("Material Creator: Unsupported extension: ." + ext)
|
||||||
|
|
||||||
|
|
||||||
func apply_pressed() -> void:
|
func apply_pressed() -> void:
|
||||||
# Using the passed in editor interface, get the selected nodes in the editor.
|
# Using the passed in editor interface, get the selected nodes in the editor.
|
||||||
var editor_selection: EditorSelection = editor_interface.get_selection()
|
var editor_selection: EditorSelection = editor_interface.get_selection()
|
||||||
var selected_nodes := editor_selection.get_selected_nodes()
|
var selected_nodes := editor_selection.get_selected_nodes()
|
||||||
if selected_nodes.is_empty():
|
if selected_nodes.is_empty():
|
||||||
push_error("Material Creator: Can't apply the material, because there are no nodes selected!")
|
push_error("Material Creator: Can't apply the material, because there are no nodes selected!")
|
||||||
|
return
|
||||||
|
|
||||||
var new_material: StandardMaterial3D = _silly_resource_from_values().make_material()
|
var new_material: StandardMaterial3D = _silly_resource_from_values().make_material()
|
||||||
# Go through the selected nodes and see if they have the "set_surface_override_material"
|
# Go through the selected nodes and see if they have the "set_surface_override_material"
|
||||||
@@ -40,57 +101,65 @@ func apply_pressed() -> void:
|
|||||||
node.set_surface_override_material(0, new_material)
|
node.set_surface_override_material(0, new_material)
|
||||||
|
|
||||||
|
|
||||||
func save_file_selected(path: String) -> bool:
|
|
||||||
var silly_resource: Variant = _silly_resource_from_values()
|
|
||||||
# Make a file, store the silly material as a JSON string.
|
|
||||||
var file := FileAccess.open(path, FileAccess.WRITE)
|
|
||||||
file.store_string(silly_resource.make_json())
|
|
||||||
|
|
||||||
return true
|
|
||||||
|
|
||||||
|
|
||||||
func load_file_selected(path: String) -> bool:
|
func load_file_selected(path: String) -> bool:
|
||||||
var SpatialMaterial_Silly: StandardMaterial3D = null
|
var ext := path.get_extension().to_lower()
|
||||||
|
|
||||||
# Make a new silly resource (which in this case actually is a node)
|
if ext == "mtxt":
|
||||||
# and initialize it.
|
# Load SOURCE by manual parse (works inside/outside res://)
|
||||||
var silly_resource: Variant = silly_material_resource.new()
|
var loaded := _read_source_silly(path)
|
||||||
#silly_resource.init()
|
if loaded == null:
|
||||||
|
push_error("Material Creator: Failed to parse source at %s" % path)
|
||||||
# If the file exists, then open it.
|
return false
|
||||||
if FileAccess.file_exists(path):
|
$VBoxContainer/AlbedoColorPicker.color = loaded.albedo_color
|
||||||
var file := FileAccess.open(path, FileAccess.READ)
|
$VBoxContainer/MetallicSlider.value = loaded.metallic_strength
|
||||||
|
$VBoxContainer/RoughnessSlider.value = loaded.roughness_strength
|
||||||
# Get the JSON string and convert it into a silly material.
|
return true
|
||||||
var json_dict_as_string := file.get_line()
|
else:
|
||||||
if json_dict_as_string != null:
|
# Load RESOURCE via ResourceLoader (silly_mat via your loader, tres via built-in)
|
||||||
silly_resource.from_json(json_dict_as_string)
|
var silly_resource: Resource = ResourceLoader.load(path)
|
||||||
else:
|
if silly_resource == null:
|
||||||
|
push_error("Material Creator: Failed to load resource at %s" % path)
|
||||||
return false
|
return false
|
||||||
|
|
||||||
$VBoxContainer/AlbedoColorPicker.color = silly_resource.albedo_color
|
$VBoxContainer/AlbedoColorPicker.color = silly_resource.albedo_color
|
||||||
$VBoxContainer/MetallicSlider.value = silly_resource.metallic_strength
|
$VBoxContainer/MetallicSlider.value = silly_resource.metallic_strength
|
||||||
$VBoxContainer/RoughnessSlider.value = silly_resource.roughness_strength
|
$VBoxContainer/RoughnessSlider.value = silly_resource.roughness_strength
|
||||||
|
|
||||||
# Return `true` to indicate success.
|
|
||||||
return true
|
return true
|
||||||
|
|
||||||
# If the file does not exist, then return `false` to indicate failure.
|
|
||||||
return false
|
|
||||||
|
|
||||||
|
func _silly_resource_from_values() -> Resource:
|
||||||
func _silly_resource_from_values() -> Variant:
|
|
||||||
# Get the values from the sliders and color picker.
|
|
||||||
var color: Color = $VBoxContainer/AlbedoColorPicker.color
|
var color: Color = $VBoxContainer/AlbedoColorPicker.color
|
||||||
var metallic: float = $VBoxContainer/MetallicSlider.value
|
var metallic: float = $VBoxContainer/MetallicSlider.value
|
||||||
var roughness: float = $VBoxContainer/RoughnessSlider.value
|
var roughness: float = $VBoxContainer/RoughnessSlider.value
|
||||||
# Make a new silly resource (which in this case actually is a node) and initialize it.
|
var silly_res: Resource = silly_material_resource.new()
|
||||||
var silly_resource: Variant = silly_material_resource.new()
|
silly_res.albedo_color = color
|
||||||
#silly_resource.init()
|
silly_res.metallic_strength = metallic
|
||||||
|
silly_res.roughness_strength = roughness
|
||||||
|
return silly_res
|
||||||
|
|
||||||
# Assign the values.
|
# ---------------------------------------------------------------
|
||||||
silly_resource.albedo_color = color
|
# Source (.mtxt) helpers.
|
||||||
silly_resource.metallic_strength = metallic
|
# ---------------------------------------------------------------
|
||||||
silly_resource.roughness_strength = roughness
|
|
||||||
|
|
||||||
return silly_resource
|
func _write_source_silly(path: String, res: Resource) -> bool:
|
||||||
|
var mat_file := FileAccess.open(path, FileAccess.WRITE)
|
||||||
|
if mat_file == null:
|
||||||
|
return false
|
||||||
|
mat_file.store_line("SILLY_MAT v1")
|
||||||
|
mat_file.store_line(res.albedo_color.to_html(true)) # RGBA hex
|
||||||
|
mat_file.store_line(str(res.metallic_strength))
|
||||||
|
mat_file.store_line(str(res.roughness_strength))
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func _read_source_silly(path: String) -> Resource:
|
||||||
|
var mat_file := FileAccess.open(path, FileAccess.READ)
|
||||||
|
if mat_file == null:
|
||||||
|
return null
|
||||||
|
var header := mat_file.get_line()
|
||||||
|
if not header.begins_with("SILLY_MAT"):
|
||||||
|
return null
|
||||||
|
var mat_res := silly_material_resource.new()
|
||||||
|
mat_res.albedo_color = Color(mat_file.get_line()) # from hex string
|
||||||
|
mat_res.metallic_strength = float(mat_file.get_line())
|
||||||
|
mat_res.roughness_strength = float(mat_file.get_line())
|
||||||
|
return mat_res
|
||||||
|
|||||||
40
plugins/addons/material_creator/material_format_loader.gd
Normal file
40
plugins/addons/material_creator/material_format_loader.gd
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
@tool
|
||||||
|
extends ResourceFormatLoader
|
||||||
|
class_name SillyMatFormatLoader
|
||||||
|
## Custom loader for the .silly_mat file format.
|
||||||
|
## Allows Godot to recognize and load SillyMaterialResource files.
|
||||||
|
## Register this loader in the EditorPlugin to enable saving/loading resources.
|
||||||
|
|
||||||
|
|
||||||
|
## Returns the list of file extensions this loader supports.
|
||||||
|
func _get_recognized_extensions() -> PackedStringArray:
|
||||||
|
# Returns only ".silly_mat"
|
||||||
|
return PackedStringArray(["silly_mat"])
|
||||||
|
|
||||||
|
|
||||||
|
## Returns what resource type this loader handles.
|
||||||
|
func _handles_type(typename: StringName) -> bool:
|
||||||
|
return typename == "SillyMaterialResource"
|
||||||
|
|
||||||
|
|
||||||
|
## Returns the resource type name based on file extension.
|
||||||
|
func _get_resource_type(path: String) -> String:
|
||||||
|
return "SillyMaterialResource" if path.get_extension() == "silly_mat" else ""
|
||||||
|
|
||||||
|
|
||||||
|
## Main load function. Reads .silly_mat and constructs a SillyMaterialResource.
|
||||||
|
func _load(path: String, original_path: String, use_sub_threads, cache_mode):
|
||||||
|
var mat_file = FileAccess.open(path, FileAccess.READ)
|
||||||
|
if mat_file == null:
|
||||||
|
return ERR_CANT_OPEN
|
||||||
|
|
||||||
|
# Check header line to validate file format version.
|
||||||
|
if mat_file.get_line() != "SILLY_MAT v1":
|
||||||
|
return ERR_PARSE_ERROR
|
||||||
|
|
||||||
|
# Create and Fill SillyMaterialResource
|
||||||
|
var mat_res: SillyMaterialResource = SillyMaterialResource.new()
|
||||||
|
mat_res.albedo_color = Color(mat_file.get_line())
|
||||||
|
mat_res.metallic_strength = float(mat_file.get_line())
|
||||||
|
mat_res.roughness_strength = float(mat_file.get_line())
|
||||||
|
return mat_res
|
||||||
35
plugins/addons/material_creator/material_format_saver.gd
Normal file
35
plugins/addons/material_creator/material_format_saver.gd
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
@tool
|
||||||
|
extends ResourceFormatSaver
|
||||||
|
class_name SillyMatFormatSaver
|
||||||
|
## Custom saver for the .silly_mat file format.
|
||||||
|
## Works together with SillyMatFormatLoader to make SillyMaterialResource.
|
||||||
|
|
||||||
|
|
||||||
|
## This saver only supports SilluMaterialResource.
|
||||||
|
func _recognize(resource: Resource) -> bool:
|
||||||
|
return resource is SillyMaterialResource
|
||||||
|
|
||||||
|
|
||||||
|
## Return list of file extensions this saver will write.
|
||||||
|
func _get_recognized_extensions(resource: Resource) -> PackedStringArray:
|
||||||
|
return PackedStringArray(["silly_mat"])
|
||||||
|
|
||||||
|
|
||||||
|
## Main save function.
|
||||||
|
## Serializes a SillyMaterialResource into .silly_mat format.
|
||||||
|
##
|
||||||
|
## It will write simple text-based format, one property per line.
|
||||||
|
func _save(resource: Resource, path: String, flags: int) -> int:
|
||||||
|
var mat_res: SillyMaterialResource = resource as SillyMaterialResource
|
||||||
|
if mat_res == null:
|
||||||
|
return ERR_INVALID_DATA
|
||||||
|
|
||||||
|
var mat_file := FileAccess.open(path, FileAccess.WRITE)
|
||||||
|
if mat_file == null:
|
||||||
|
return ERR_CANT_OPEN
|
||||||
|
|
||||||
|
mat_file.store_line("SILLY_MAT v1")
|
||||||
|
mat_file.store_line(mat_res.albedo_color.to_html(true)) # Stored in HTML hex.
|
||||||
|
mat_file.store_line(str(mat_res.metallic_strength))
|
||||||
|
mat_file.store_line(str(mat_res.roughness_strength))
|
||||||
|
return OK
|
||||||
@@ -12,9 +12,16 @@
|
|||||||
extends EditorPlugin
|
extends EditorPlugin
|
||||||
|
|
||||||
var io_material_dialog: Panel
|
var io_material_dialog: Panel
|
||||||
|
var _loader: SillyMatFormatLoader
|
||||||
|
var _saver: SillyMatFormatSaver
|
||||||
|
|
||||||
|
|
||||||
func _enter_tree() -> void:
|
func _enter_tree() -> void:
|
||||||
|
_loader = SillyMatFormatLoader.new()
|
||||||
|
_saver = SillyMatFormatSaver.new()
|
||||||
|
ResourceLoader.add_resource_format_loader(_loader)
|
||||||
|
ResourceSaver.add_resource_format_saver(_saver)
|
||||||
|
|
||||||
io_material_dialog = preload("res://addons/material_creator/material_dock.tscn").instantiate()
|
io_material_dialog = preload("res://addons/material_creator/material_dock.tscn").instantiate()
|
||||||
io_material_dialog.editor_interface = get_editor_interface()
|
io_material_dialog.editor_interface = get_editor_interface()
|
||||||
add_control_to_dock(DOCK_SLOT_LEFT_UL, io_material_dialog)
|
add_control_to_dock(DOCK_SLOT_LEFT_UL, io_material_dialog)
|
||||||
@@ -22,3 +29,10 @@ func _enter_tree() -> void:
|
|||||||
|
|
||||||
func _exit_tree() -> void:
|
func _exit_tree() -> void:
|
||||||
remove_control_from_docks(io_material_dialog)
|
remove_control_from_docks(io_material_dialog)
|
||||||
|
|
||||||
|
if _loader:
|
||||||
|
ResourceLoader.remove_resource_format_loader(_loader)
|
||||||
|
_loader = null
|
||||||
|
if _saver:
|
||||||
|
ResourceSaver.remove_resource_format_saver(_saver)
|
||||||
|
_saver = null
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
@tool
|
@tool
|
||||||
extends Node
|
extends Resource
|
||||||
# NOTE: In theory, this would extend from Resource, but until saving and loading resources
|
class_name SillyMaterialResource
|
||||||
# works in Godot, we'll stick with extending from Node and using JSON files to save/load data.
|
|
||||||
#
|
|
||||||
# See `material_import.gd` for more information.
|
|
||||||
|
|
||||||
var albedo_color := Color.BLACK
|
# Use export to make properties visible and serializable in the inspector and for resource saving/loading.
|
||||||
var metallic_strength := 0.0
|
@export var albedo_color: Color = Color.BLACK
|
||||||
var roughness_strength := 0.0
|
@export var metallic_strength: float = 0.0
|
||||||
|
@export var roughness_strength: float = 0.0
|
||||||
|
|
||||||
|
|
||||||
|
# Create a StandardMaterial3D from the resource's properties.
|
||||||
# Convert our data into an dictionary so we can convert it
|
# Convert our data into an dictionary so we can convert it
|
||||||
# into the JSON format.
|
# into the JSON format.
|
||||||
func make_json() -> String:
|
func make_json() -> String:
|
||||||
@@ -41,10 +40,8 @@ func from_json(json_dict_as_string: String) -> void:
|
|||||||
|
|
||||||
# Make a StandardMaterial3D using our variables.
|
# Make a StandardMaterial3D using our variables.
|
||||||
func make_material() -> StandardMaterial3D:
|
func make_material() -> StandardMaterial3D:
|
||||||
var material := StandardMaterial3D.new()
|
var mat = StandardMaterial3D.new()
|
||||||
|
mat.albedo_color = albedo_color
|
||||||
material.albedo_color = albedo_color
|
mat.metallic = metallic_strength
|
||||||
material.metallic = metallic_strength
|
mat.roughness = roughness_strength
|
||||||
material.roughness = roughness_strength
|
return mat
|
||||||
|
|
||||||
return material
|
|
||||||
|
|||||||
Reference in New Issue
Block a user