mirror of
https://github.com/godotengine/godot-demo-projects.git
synced 2025-12-16 05:20:06 +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
|
||||
|
||||
This plugin demo contains a custom material creator
|
||||
interface using a custom dock in the editor.
|
||||
This plugin demo contains a custom material creation dock
|
||||
inside the Godot editor.
|
||||
|
||||
Custom docks are made of Control nodes, they run in the
|
||||
editor, and any behavior must be done through `tool` scripts.
|
||||
For more information, see this documentation article:
|
||||
https://docs.godotengine.org/en/latest/tutorials/plugins/editor/making_plugins.html#a-custom-dock
|
||||
|
||||
This plugin allows you to specify color, metallic, and
|
||||
roughness values, and then use it as a material.
|
||||
## Features
|
||||
- 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
|
||||
nodes by selecting them and then clicking "Apply".
|
||||
This shows how a plugin can interact closely with the
|
||||
editor, manipulating nodes the user selects.
|
||||
|
||||
Alternatively, you can also save the material to
|
||||
a file, and then load it back into the plugin later.
|
||||
## Implementation notes
|
||||
- `.silly_mat` format is registered through `SillyMatFormatSaver` and
|
||||
`SillyMatFormatLoader` in the plugin.
|
||||
- Custm docks are built from `Control` nodes and run as `@tool` scripts.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
extends Panel
|
||||
# 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
|
||||
|
||||
|
||||
@@ -11,8 +11,26 @@ func _ready() -> void:
|
||||
$VBoxContainer/ApplyButton.pressed.connect(apply_pressed)
|
||||
$VBoxContainer/SaveButton.pressed.connect(save_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)
|
||||
|
||||
RenderingServer.canvas_item_set_clip(get_canvas_item(), true)
|
||||
|
||||
|
||||
@@ -24,12 +42,55 @@ func load_pressed() -> void:
|
||||
$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:
|
||||
# Using the passed in editor interface, get the selected nodes in the editor.
|
||||
var editor_selection: EditorSelection = editor_interface.get_selection()
|
||||
var selected_nodes := editor_selection.get_selected_nodes()
|
||||
if selected_nodes.is_empty():
|
||||
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()
|
||||
# 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)
|
||||
|
||||
|
||||
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:
|
||||
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)
|
||||
# and initialize it.
|
||||
var silly_resource: Variant = silly_material_resource.new()
|
||||
#silly_resource.init()
|
||||
|
||||
# If the file exists, then open it.
|
||||
if FileAccess.file_exists(path):
|
||||
var file := FileAccess.open(path, FileAccess.READ)
|
||||
|
||||
# Get the JSON string and convert it into a silly material.
|
||||
var json_dict_as_string := file.get_line()
|
||||
if json_dict_as_string != null:
|
||||
silly_resource.from_json(json_dict_as_string)
|
||||
else:
|
||||
if ext == "mtxt":
|
||||
# Load SOURCE by manual parse (works inside/outside res://)
|
||||
var loaded := _read_source_silly(path)
|
||||
if loaded == null:
|
||||
push_error("Material Creator: Failed to parse source at %s" % path)
|
||||
return false
|
||||
$VBoxContainer/AlbedoColorPicker.color = loaded.albedo_color
|
||||
$VBoxContainer/MetallicSlider.value = loaded.metallic_strength
|
||||
$VBoxContainer/RoughnessSlider.value = loaded.roughness_strength
|
||||
return true
|
||||
else:
|
||||
# Load RESOURCE via ResourceLoader (silly_mat via your loader, tres via built-in)
|
||||
var silly_resource: Resource = ResourceLoader.load(path)
|
||||
if silly_resource == null:
|
||||
push_error("Material Creator: Failed to load resource at %s" % path)
|
||||
return false
|
||||
|
||||
$VBoxContainer/AlbedoColorPicker.color = silly_resource.albedo_color
|
||||
$VBoxContainer/MetallicSlider.value = silly_resource.metallic_strength
|
||||
$VBoxContainer/RoughnessSlider.value = silly_resource.roughness_strength
|
||||
|
||||
# Return `true` to indicate success.
|
||||
return true
|
||||
|
||||
# If the file does not exist, then return `false` to indicate failure.
|
||||
return false
|
||||
|
||||
|
||||
func _silly_resource_from_values() -> Variant:
|
||||
# Get the values from the sliders and color picker.
|
||||
func _silly_resource_from_values() -> Resource:
|
||||
var color: Color = $VBoxContainer/AlbedoColorPicker.color
|
||||
var metallic: float = $VBoxContainer/MetallicSlider.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_resource: Variant = silly_material_resource.new()
|
||||
#silly_resource.init()
|
||||
var silly_res: Resource = silly_material_resource.new()
|
||||
silly_res.albedo_color = color
|
||||
silly_res.metallic_strength = metallic
|
||||
silly_res.roughness_strength = roughness
|
||||
return silly_res
|
||||
|
||||
# Assign the values.
|
||||
silly_resource.albedo_color = color
|
||||
silly_resource.metallic_strength = metallic
|
||||
silly_resource.roughness_strength = roughness
|
||||
# ---------------------------------------------------------------
|
||||
# Source (.mtxt) helpers.
|
||||
# ---------------------------------------------------------------
|
||||
|
||||
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
|
||||
|
||||
var io_material_dialog: Panel
|
||||
var _loader: SillyMatFormatLoader
|
||||
var _saver: SillyMatFormatSaver
|
||||
|
||||
|
||||
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.editor_interface = get_editor_interface()
|
||||
add_control_to_dock(DOCK_SLOT_LEFT_UL, io_material_dialog)
|
||||
@@ -22,3 +29,10 @@ func _enter_tree() -> void:
|
||||
|
||||
func _exit_tree() -> void:
|
||||
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
|
||||
extends Node
|
||||
# NOTE: In theory, this would extend from Resource, but until saving and loading resources
|
||||
# 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.
|
||||
extends Resource
|
||||
class_name SillyMaterialResource
|
||||
|
||||
var albedo_color := Color.BLACK
|
||||
var metallic_strength := 0.0
|
||||
var roughness_strength := 0.0
|
||||
# Use export to make properties visible and serializable in the inspector and for resource saving/loading.
|
||||
@export var albedo_color: Color = Color.BLACK
|
||||
@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
|
||||
# into the JSON format.
|
||||
func make_json() -> String:
|
||||
@@ -41,10 +40,8 @@ func from_json(json_dict_as_string: String) -> void:
|
||||
|
||||
# Make a StandardMaterial3D using our variables.
|
||||
func make_material() -> StandardMaterial3D:
|
||||
var material := StandardMaterial3D.new()
|
||||
|
||||
material.albedo_color = albedo_color
|
||||
material.metallic = metallic_strength
|
||||
material.roughness = roughness_strength
|
||||
|
||||
return material
|
||||
var mat = StandardMaterial3D.new()
|
||||
mat.albedo_color = albedo_color
|
||||
mat.metallic = metallic_strength
|
||||
mat.roughness = roughness_strength
|
||||
return mat
|
||||
|
||||
Reference in New Issue
Block a user