Browse Source

Overhaul silly material creator plugin demo (#1261)

Aaron Franke 2 months ago
parent
commit
0ae09b7e5a
56 changed files with 899 additions and 495 deletions
  1. 7 2
      plugins/.gitignore
  2. 11 11
      plugins/README.md
  3. BIN
      plugins/addons/custom_node/heart_icon.png
  4. 0 40
      plugins/addons/custom_node/heart_icon.png.import
  5. 2 1
      plugins/addons/custom_node/heart_plugin.gd
  6. 140 23
      plugins/addons/material_creator/README.md
  7. 72 0
      plugins/addons/material_creator/editor/importers/import_silly_material.gd
  8. 1 0
      plugins/addons/material_creator/editor/importers/import_silly_material.gd.uid
  9. 71 0
      plugins/addons/material_creator/editor/importers/import_standard_material_3d.gd
  10. 1 0
      plugins/addons/material_creator/editor/importers/import_standard_material_3d.gd.uid
  11. 38 0
      plugins/addons/material_creator/editor/load_and_save/material_format_loader.gd
  12. 1 0
      plugins/addons/material_creator/editor/load_and_save/material_format_loader.gd.uid
  13. 34 0
      plugins/addons/material_creator/editor/load_and_save/material_format_saver.gd
  14. 1 0
      plugins/addons/material_creator/editor/load_and_save/material_format_saver.gd.uid
  15. 145 0
      plugins/addons/material_creator/editor/material_creator.gd
  16. 0 0
      plugins/addons/material_creator/editor/material_creator.gd.uid
  17. 151 0
      plugins/addons/material_creator/editor/material_dock.tscn
  18. 9 0
      plugins/addons/material_creator/examples/blue.tres
  19. 1 0
      plugins/addons/material_creator/examples/cyan.silly_mat_loadable
  20. 1 0
      plugins/addons/material_creator/examples/cyan.silly_mat_loadable.uid
  21. 1 0
      plugins/addons/material_creator/examples/green_as_standard_mat.silly_mat_importable
  22. 15 0
      plugins/addons/material_creator/examples/green_as_standard_mat.silly_mat_importable.import
  23. 1 0
      plugins/addons/material_creator/examples/yellow.silly_mat_importable
  24. 15 0
      plugins/addons/material_creator/examples/yellow.silly_mat_importable.import
  25. 1 0
      plugins/addons/material_creator/examples/yellow_tinted_red.silly_mat_importable
  26. 15 0
      plugins/addons/material_creator/examples/yellow_tinted_red.silly_mat_importable.import
  27. 0 165
      plugins/addons/material_creator/material_creator.gd
  28. 0 83
      plugins/addons/material_creator/material_dock.tscn
  29. 0 40
      plugins/addons/material_creator/material_format_loader.gd
  30. 0 35
      plugins/addons/material_creator/material_format_saver.gd
  31. 37 29
      plugins/addons/material_creator/material_plugin.gd
  32. 0 47
      plugins/addons/material_creator/material_resource.gd
  33. 1 1
      plugins/addons/material_creator/plugin.cfg
  34. 109 0
      plugins/addons/material_creator/silly_material_resource.gd
  35. 0 0
      plugins/addons/material_creator/silly_material_resource.gd.uid
  36. 0 15
      plugins/addons/material_import_plugin/test.mtxt.import
  37. 0 0
      plugins/addons/simple_import_plugin/README.md
  38. 1 1
      plugins/addons/simple_import_plugin/import.gd
  39. 0 0
      plugins/addons/simple_import_plugin/import.gd.uid
  40. 1 1
      plugins/addons/simple_import_plugin/plugin.cfg
  41. 0 0
      plugins/addons/simple_import_plugin/plugin.gd
  42. 0 0
      plugins/addons/simple_import_plugin/plugin.gd.uid
  43. 0 0
      plugins/addons/simple_import_plugin/test.mtxt
  44. 15 0
      plugins/addons/simple_import_plugin/test.mtxt.import
  45. 1 1
      plugins/project.godot
  46. BIN
      plugins/screenshots/heart_custom_node.webp
  47. BIN
      plugins/screenshots/heart_plugin.png
  48. BIN
      plugins/screenshots/main_screen_plugin.png
  49. BIN
      plugins/screenshots/main_screen_plugin.webp
  50. BIN
      plugins/screenshots/material_creator_plugin_1.png
  51. BIN
      plugins/screenshots/material_creator_plugin_2.png
  52. BIN
      plugins/screenshots/material_creator_plugin_applied.webp
  53. BIN
      plugins/screenshots/material_creator_plugin_dock.webp
  54. BIN
      plugins/screenshots/material_creator_plugin_imported_file_is_read_only.webp
  55. BIN
      plugins/screenshots/material_import_plugin.png
  56. BIN
      plugins/screenshots/simple_import_plugin.webp

+ 7 - 2
plugins/.gitignore

@@ -1,2 +1,7 @@
-# "Silly Material" files written by the editor plugin
-*.silly_mat
+# "Silly Material" files written by the editor plugin.
+# The example files already tracked by Git are not affected.
+*.silly_mat_importable
+*.silly_mat_importable.import
+*.silly_mat_loadable
+*.silly_mat_loadable.uid
+*.tres

+ 11 - 11
plugins/README.md

@@ -21,14 +21,16 @@ This project contains 4 plugins:
 * The custom node plugin shows how to create a custom node type
   using `add_custom_type`. [More info](addons/custom_node).
 
-* The material import plugin shows how to make a plugin handle importing
-  a custom file type (mtxt). [More info](addons/material_import_plugin).
+* The main screen plugin is a minimal example of how to create a plugin
+  with a main screen. [More info](addons/main_screen).
 
 * The material creator plugin shows how to add a custom dock with some
-  simple functionality. [More info](addons/material_creator).
+  simple functionality, and shows how to create a custom Resource type
+  with custom loading, saving, importing, and exporting logic,
+  including editor integrations. [More info](addons/material_creator).
 
-* The main screen plugin is a minimal example of how to create a plugin
-  with a main screen. [More info](addons/main_screen).
+* The simple import plugin shows how to make a simple plugin handle importing
+  a custom file type (mtxt). [More info](addons/simple_import_plugin).
 
 To use these plugins in another project, copy any of these
 folders to the `addons/` folder in a Godot project, and then
@@ -44,12 +46,10 @@ This can be done via the terminal: `zip -r custom_node.zip custom_node/*`
 
 ## Screenshots
 
-![Heart Plugin](screenshots/heart_plugin.png)
-
-![Main Screen Plugin](screenshots/main_screen_plugin.png)
+![Heart Custom Node](screenshots/heart_custom_node.webp)
 
-![Material Import Plugin](screenshots/material_import_plugin.png)
+![Main Screen Plugin](screenshots/main_screen_plugin.webp)
 
-![Material Creator Plugin 1](screenshots/material_creator_plugin_1.png)
+![Material Creator Plugin](screenshots/material_creator_plugin_applied.webp)
 
-![Material Creator Plugin 2](screenshots/material_creator_plugin_2.png)
+![Simple Import Plugin](screenshots/simple_import_plugin.webp)

BIN
plugins/addons/custom_node/heart_icon.png


+ 0 - 40
plugins/addons/custom_node/heart_icon.png.import

@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://c3c8tfihdvaw7"
-path="res://.godot/imported/heart_icon.png-8f04adf78b3bd1a5c39f790588a1fa78.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://addons/custom_node/heart_icon.png"
-dest_files=["res://.godot/imported/heart_icon.png-8f04adf78b3bd1a5c39f790588a1fa78.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1

+ 2 - 1
plugins/addons/custom_node/heart_plugin.gd

@@ -4,7 +4,8 @@ extends EditorPlugin
 
 func _enter_tree() -> void:
 	# When this plugin node enters tree, add the custom type.
-	add_custom_type("Heart", "Node2D", preload("res://addons/custom_node/heart.gd"), preload("res://addons/custom_node/heart_icon.png"))
+	var icon: Texture2D = preload("res://addons/custom_node/heart.png")
+	add_custom_type("Heart", "Node2D", preload("res://addons/custom_node/heart.gd"), icon)
 
 
 func _exit_tree() -> void:

+ 140 - 23
plugins/addons/material_creator/README.md

@@ -1,25 +1,142 @@
 # Material Creator Plugin Demo
 
-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
-
-## 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).
-
-## 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.
+This plugin demo demonstrates these things:
+
+- How to create a custom dock in the Godot editor with basic functionality.
+  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
+
+- How to create a custom Resource type, and provide logic for
+  serializing, deserializing, and converting this Resource type.
+
+- Editor integration with classes for loading, saving, and importing
+  this Resource type, including optional import customization.
+
+For a more comprehensive example, see the GLTF module in Godot's source code.
+
+For a less comprehensive example, see the "simple_import_plugin" folder.
+
+## Importing vs Loading
+
+The custom Resource type in this demo is supplemented by two different
+sets of editor classes:
+
+- [EditorImportPlugin](https://docs.godotengine.org/en/stable/classes/class_editorimportplugin.html)
+  in the `importers/` folder, which allow customizing how
+  files are imported into Godot as Resources of different
+  types and optionally with import settings.
+  Imported files have `.import` files generated next to them.
+
+- [ResourceFormatLoader](https://docs.godotengine.org/en/stable/classes/class_resourceformatloader.html)
+  and [ResourceFormatSaver](https://docs.godotengine.org/en/stable/classes/class_resourceformatsaver.html)
+  in the `load_and_save/` folder, which allow easily
+  editing files in the inspector and saving them back.
+  Resource files have `.uid` files generated next to them.
+
+These two approaches are mutually exclusive.
+You may only use one approach at a time for a given file extension.
+This demo showcases both by using 2 different file extensions.
+
+In actual projects, you should either choose [EditorImportPlugin](https://docs.godotengine.org/en/stable/classes/class_editorimportplugin.html)(s)
+for a configurable import, OR [ResourceFormatLoader](https://docs.godotengine.org/en/stable/classes/class_resourceformatloader.html)
+for a writeable resource load.
+The choice depends on if you treat the file as an external source asset
+which should be imported and may be customized at import
+([EditorImportPlugin](https://docs.godotengine.org/en/stable/classes/class_editorimportplugin.html)),
+or if you treat the file as an internal Godot resource meant to be natively
+and directly edited within Godot
+([ResourceFormatLoader](https://docs.godotengine.org/en/stable/classes/class_resourceformatloader.html)).
+
+For example, a glTF file may be generated by Blender, and therefore
+is not intended to be edited directly in Godot, so it should use
+[EditorImportPlugin](https://docs.godotengine.org/en/stable/classes/class_editorimportplugin.html).
+Similarly, a PNG file is typically generated by an image editor, and Godot
+needs to convert it to a different internal format, like a `.ctex` file
+for a VRAM-compressed texture, so it should use
+[EditorImportPlugin](https://docs.godotengine.org/en/stable/classes/class_editorimportplugin.html).
+However, files like `.tres` and `.tscn` are Godot-native formats meant to be
+edited directly in Godot, so they should use
+[ResourceFormatLoader](https://docs.godotengine.org/en/stable/classes/class_resourceformatloader.html).
+
+Once you choose one approach, create scripts deriving the appropriate classes,
+override their callback functions, and register them in your plugin's
+[`*_plugin.gd`](material_plugin.gd) script.
+
+## Example Files
+
+The [`examples/`](examples/) folder contains several example files:
+
+- `blue.tres`: Directly saving a SillyMaterialResource using Godot's built-in `.tres` format,
+  without any custom loader/saver logic or import/export logic, available for all Resource types.
+  This can be edited in Godot's inspector and saved back.
+
+- `cyan.silly_mat_loadable`: Storing a SillyMaterialResource as a custom format,
+  such as via [ResourceFormatSaver](https://docs.godotengine.org/en/stable/classes/class_resourceformatsaver.html), which is loaded back using a custom [ResourceFormatLoader](https://docs.godotengine.org/en/stable/classes/class_resourceformatloader.html).
+  This can be edited in Godot's inspector and saved back.
+
+- `green_as_standard_mat.silly_mat_importable`: Storing a SillyMaterialResource as a custom format,
+  which is imported as a StandardMaterial3D using custom import/export logic. This shows how
+  importers can import files as any Resource type, converting custom files to data Godot can use.
+  Imported files are read-only and cannot be edited in the inspector.
+
+- `yellow.silly_mat_importable`: Storing a SillyMaterialResource as a custom format,
+  which is imported as a SillyMaterialResource using custom import/export logic.
+  Imported files are read-only and cannot be edited in the inspector.
+
+- `yellow_tinted_red.silly_mat_importable`: Storing a SillyMaterialResource as a custom format,
+  which is imported as a SillyMaterialResource using custom import/export logic.
+
+  - This file has the same exact contents as `yellow.silly_mat_importable`, but the
+    corresponding `.import` file has a flag set to tint the albedo color towards red,
+	which makes the material appear orange instead of yellow.
+    This demonstrates how importers can use import settings to modify data during import.
+    After import, imported files are read-only and cannot be edited in the inspector.
+
+  - If you try to load this file using "Load Imported Material (EditorImportPlugin)" in the editor,
+    or call `ResourceLoader.load()` in GDScript, it will load the imported version, which
+	includes the red tint, so the albedo color will be orange. If you try to import this
+	file using "Import Material (directly at runtime)" in the editor, or call
+	`SillyMaterialResource.read_from_file()` in GDScript, it will directly read the original
+	file's contents, ignoring the import process, so the albedo color will be yellow.
+	This demonstrates how files can be loaded within Godot's import process (editor only),
+	or bypass the import process entirely (works in editor and at runtime).
+
+![Material Creator Plugin Imported File is Read-Only](../../screenshots/material_creator_plugin_imported_file_is_read_only.webp)
+
+## Editor Buttons
+
+The material creator dock has these 6 buttons:
+
+- "Apply Material": Applies the current material to all selected MeshInstance3D nodes in the editor.
+
+- "Save Material (ResourceFormatSaver)": Saves the current
+  material to a `.silly_mat_loadable` file using the custom
+  [ResourceFormatSaver](https://docs.godotengine.org/en/stable/classes/class_resourceformatsaver.html),
+  or to a `.tres` file using Godot's built-in ResourceFormatSaverText.
+  This can be edited in Godot's inspector and saved back.
+
+- "Export Material (directly at runtime)": Exports the current material
+  to a `.silly_mat_*` file using the functions on SillyMaterialResource.
+  This works for files outside of the `res://` folder, and can be done at runtime.
+
+- "Load Material (ResourceFormatLoader)": Loads a
+  `.silly_mat_loadable` file using the custom
+  [ResourceFormatLoader](https://docs.godotengine.org/en/stable/classes/class_resourceformatloader.html),
+  or loads a `.tres` file using Godot's built-in ResourceFormatLoaderText.
+  This can be edited in Godot's inspector and saved back.
+
+- "Load Imported Material (EditorImportPlugin)": Loads a
+  `.silly_mat_importable` that was imported by an
+  [EditorImportPlugin](https://docs.godotengine.org/en/stable/classes/class_editorimportplugin.html).
+  The loaded data actually comes from the corresponding
+  imported file saved as `res://.godot/imported/something.silly_mat_importable-hash.res`.
+  Imported files are read-only and cannot be edited in the inspector.
+
+- "Import Material (directly at runtime)": Imports a `.silly_mat_*` directly from the
+  source file, performing an import on request instead of loading data the editor imported earlier.
+  This ignores any editor import settings, works for files outside of the `res://` folder,
+  and can be done at runtime.
+
+![Material Creator Plugin Dock](../../screenshots/material_creator_plugin_dock.webp)

+ 72 - 0
plugins/addons/material_creator/editor/importers/import_silly_material.gd

@@ -0,0 +1,72 @@
+## Imports a `.silly_mat_importable` as a SillyMaterialResource.
+## This class needs to be registered in the EditorPlugin to be used.
+##
+## Unlike loaders, importers can be configured, and multiple can exist in
+## the same project. Selecting and configuring is done in the "Import" dock.
+## However, imported files cannot be modified as easily as loaded files.
+## See the "load_and_save" folder for an example of how to use loaders and savers.
+##
+## In actual projects, you should either choose EditorImportPlugin(s) for a
+## configurable import, OR ResourceFormatLoader for a writeable resource load.
+## Only one handling can exist at a given time for a given file extension.
+## This demo exposes both by using 2 different file extensions.
+@tool
+class_name ImportSillyMatAsSillyMaterialResource
+extends EditorImportPlugin
+
+
+func _get_importer_name() -> String:
+	return "demos.silly_material_importable.silly_material_resource"
+
+
+func _get_visible_name() -> String:
+	return "Silly Material Resource"
+
+
+func _get_recognized_extensions() -> PackedStringArray:
+	return ["silly_mat_importable"]
+
+
+func _get_save_extension() -> String:
+	return "res"
+
+
+func _get_resource_type() -> String:
+	# Note: This MUST be a native Godot type, it can't be a GDScript type.
+	# Therefore it has to be "Resource" instead of "SillyMaterialResource".
+	return "Resource"
+
+
+func _get_preset_count() -> int:
+	return 0
+
+
+func _get_preset_name(preset: int) -> String:
+	return "Default"
+
+
+func _get_import_options(_path: String, preset: int) -> Array[Dictionary]:
+	var ret: Array[Dictionary] = [
+		{
+			"name": "make_more_red",
+			"default_value": false,
+		}
+	]
+	return ret
+
+
+func _get_import_order() -> int:
+	return ResourceImporter.IMPORT_ORDER_DEFAULT
+
+
+func _get_option_visibility(path: String, option: StringName, options: Dictionary) -> bool:
+	return true
+
+
+func _import(source_file: String, save_path: String, options: Dictionary, r_platform_variants: Array[String], r_gen_files: Array[String]) -> Error:
+	var silly_mat_res := SillyMaterialResource.read_from_file(source_file)
+	if options.has("make_more_red") and options["make_more_red"]:
+		silly_mat_res.albedo_color = silly_mat_res.albedo_color.lerp(Color.RED, 0.5)
+	# This will save to a file path like `res://.godot/imported/something.res`.
+	var imported_path: String = "%s.%s" % [save_path, _get_save_extension()]
+	return ResourceSaver.save(silly_mat_res, imported_path)

+ 1 - 0
plugins/addons/material_creator/editor/importers/import_silly_material.gd.uid

@@ -0,0 +1 @@
+uid://u2esoq3eygve

+ 71 - 0
plugins/addons/material_creator/editor/importers/import_standard_material_3d.gd

@@ -0,0 +1,71 @@
+## Imports a `.silly_mat_importable` as a StandardMaterial3D.
+## This class needs to be registered in the EditorPlugin to be used.
+##
+## Unlike loaders, importers can be configured, and multiple can exist in
+## the same project. Selecting and configuring is done in the "Import" dock.
+## However, imported files cannot be modified as easily as loaded files.
+## See the "load_and_save" folder for an example of how to use loaders and savers.
+##
+## In actual projects, you should either choose EditorImportPlugin(s) for a
+## configurable import, OR ResourceFormatLoader for a writeable resource load.
+## Only one handling can exist at a given time for a given file extension.
+## This demo exposes both by using 2 different file extensions.
+@tool
+class_name ImportSillyMatAsStandardMaterial3D
+extends EditorImportPlugin
+
+
+func _get_importer_name() -> String:
+	return "demos.silly_material_importable.standard_material_3d"
+
+
+func _get_visible_name() -> String:
+	return "Standard Material 3D"
+
+
+func _get_recognized_extensions() -> PackedStringArray:
+	return ["silly_mat_importable"]
+
+
+func _get_save_extension() -> String:
+	return "res"
+
+
+func _get_resource_type() -> String:
+	return "StandardMaterial3D"
+
+
+func _get_preset_count() -> int:
+	return 0
+
+
+func _get_preset_name(preset: int) -> String:
+	return "Default"
+
+
+func _get_import_options(_path: String, preset: int) -> Array[Dictionary]:
+	var ret: Array[Dictionary] = [
+		{
+			"name": "make_more_red",
+			"default_value": false,
+		}
+	]
+	return ret
+
+
+func _get_import_order() -> int:
+	return ResourceImporter.IMPORT_ORDER_DEFAULT
+
+
+func _get_option_visibility(path: String, option: StringName, options: Dictionary) -> bool:
+	return true
+
+
+func _import(source_file: String, save_path: String, options: Dictionary, r_platform_variants: Array[String], r_gen_files: Array[String]) -> Error:
+	var silly_mat_res := SillyMaterialResource.read_from_file(source_file)
+	if options.has("make_more_red") and options["make_more_red"]:
+		silly_mat_res.albedo_color = silly_mat_res.albedo_color.lerp(Color.RED, 0.5)
+	var standard_mat: StandardMaterial3D = silly_mat_res.to_material()
+	# This will save to a file path like `res://.godot/imported/something.res`.
+	var imported_path: String = "%s.%s" % [save_path, _get_save_extension()]
+	return ResourceSaver.save(standard_mat, imported_path)

+ 1 - 0
plugins/addons/material_creator/editor/importers/import_standard_material_3d.gd.uid

@@ -0,0 +1 @@
+uid://cmgoxx63wybil

+ 38 - 0
plugins/addons/material_creator/editor/load_and_save/material_format_loader.gd

@@ -0,0 +1,38 @@
+## Custom loader for the `.silly_mat_loadable` file format.
+## Works together with `SillyMatFormatSaver` to support saving and loading.
+## This class needs to be registered in the EditorPlugin to be used.
+##
+## Loaders can easily have the loaded data be modified and saved back into
+## the file. However, only one loader can exist, and the loading cannot be
+## configured, unlike importers which are configurable in the "Import" dock.
+## See the "importers" folder for two examples of how to use importers.
+##
+## In actual projects, you should either choose ResourceFormatLoader for a
+## writeable resource load, OR EditorImportPlugin(s) for a configurable import.
+## Only one handling can exist at a given time for a given file extension.
+## This demo exposes both by using 2 different file extensions.
+@tool
+class_name SillyMatFormatLoader
+extends ResourceFormatLoader
+
+
+## Callback to return an array of the file extensions this loader can load.
+func _get_recognized_extensions() -> PackedStringArray:
+	return PackedStringArray(["silly_mat_loadable"])
+
+
+## Callback to return the resource type name based on file extension.
+func _get_resource_type(path: String) -> String:
+	if path.get_extension() == "silly_mat_loadable":
+		return "SillyMaterialResource"
+	return ""
+
+
+## Callback to return what resource type this loader handles.
+func _handles_type(type_name: StringName) -> bool:
+	return type_name == &"SillyMaterialResource"
+
+
+## Main callback to actually perform the loading.
+func _load(path: String, original_path: String, use_sub_threads: bool, cache_mode: int) -> Variant:
+	return SillyMaterialResource.read_from_file(original_path)

+ 1 - 0
plugins/addons/material_creator/editor/load_and_save/material_format_loader.gd.uid

@@ -0,0 +1 @@
+uid://b004gkmug1qlt

+ 34 - 0
plugins/addons/material_creator/editor/load_and_save/material_format_saver.gd

@@ -0,0 +1,34 @@
+## Custom saver for the `.silly_mat_loadable` file format.
+## Works together with `SillyMatFormatLoader` to support loading and saving.
+## This class needs to be registered in the EditorPlugin to be used.
+##
+## Loaders can easily have the loaded data be modified and saved back into
+## the file. However, only one loader can exist, and the loading cannot be
+## configured, unlike importers which are configurable in the "Import" dock.
+## See the "importers" folder for two examples of how to use importers.
+##
+## In actual projects, you should either choose ResourceFormatLoader for a
+## writeable resource load, OR EditorImportPlugin(s) for a configurable import.
+## Only one handling can exist at a given time for a given file extension.
+## This demo exposes both by using 2 different file extensions.
+@tool
+class_name SillyMatFormatSaver
+extends ResourceFormatSaver
+
+
+## Callback to return an array of the file extensions this saver can write.
+func _get_recognized_extensions(resource: Resource) -> PackedStringArray:
+	return PackedStringArray(["silly_mat_loadable"])
+
+
+## Callback to determine if a given Resource is supported by this saver.
+func _recognize(resource: Resource) -> bool:
+	return resource is SillyMaterialResource
+
+
+## Main callback to actually perform the saving.
+func _save(resource: Resource, path: String, flags: int) -> Error:
+	var mat_res: SillyMaterialResource = resource as SillyMaterialResource
+	if mat_res == null:
+		return ERR_INVALID_DATA
+	return mat_res.write_to_file(path)

+ 1 - 0
plugins/addons/material_creator/editor/load_and_save/material_format_saver.gd.uid

@@ -0,0 +1 @@
+uid://cvaleef5ekitp

+ 145 - 0
plugins/addons/material_creator/editor/material_creator.gd

@@ -0,0 +1,145 @@
+# The word "silly" is used to make it obvious that the name is arbitrary.
+@tool
+extends Panel
+
+
+var editor_interface: EditorInterface
+
+@onready var albedo_color_picker: ColorPickerButton = $VBoxContainer/AlbedoColorPicker
+@onready var metallic_slider: HSlider = $VBoxContainer/MetallicSlider
+@onready var roughness_slider: HSlider = $VBoxContainer/RoughnessSlider
+
+@onready var save_material_dialog: FileDialog = $SaveMaterialDialog
+@onready var export_material_dialog: FileDialog = $ExportMaterialDialog
+@onready var load_material_importer_dialog: FileDialog = $LoadMaterialImporterDialog
+@onready var load_material_loader_dialog: FileDialog = $LoadMaterialLoaderDialog
+@onready var import_material_directly_dialog: FileDialog = $ImportMaterialDirectlyDialog
+
+
+func _ready() -> void:
+	if not name.contains(" "):
+		printerr("Warning: Material Creator dock doesn't have a space in its node name, so it will be displayed without any spacing.")
+	save_material_dialog.current_path = "res://addons/material_creator/example/"
+	save_material_dialog.current_file = "new_material.silly_mat_loadable"
+	export_material_dialog.current_path = "res://addons/material_creator/example/"
+	export_material_dialog.current_file = "new_material.silly_mat_importable"
+	load_material_importer_dialog.current_path = "res://addons/material_creator/example/"
+	load_material_loader_dialog.current_path = "res://addons/material_creator/example/"
+	import_material_directly_dialog.current_path = ProjectSettings.globalize_path("res://addons/material_creator/example/")
+	RenderingServer.canvas_item_set_clip(get_canvas_item(), true)
+
+
+func _save_or_export_file(path: String) -> void:
+	if path.is_empty():
+		printerr("Material Creator: No path chosen for saving.")
+		return
+	# Ensure directory exists before trying to save to it.
+	var dir: String = path.get_base_dir()
+	if not DirAccess.dir_exists_absolute(dir):
+		var err: Error = DirAccess.make_dir_recursive_absolute(dir)
+		if err != OK:
+			printerr("Material Creator: Can't create folder: %s (%s)" % [dir, error_string(err)])
+			return
+	var silly_mat: SillyMaterialResource = _create_silly_material_from_editor_values()
+	var ext: String = path.get_extension().to_lower()
+	var err: Error
+	var is_in_project: bool = path.begins_with("res://") or path.begins_with("user://")
+	if ext == "tres":
+		err = ResourceSaver.save(silly_mat, path)
+		if not is_in_project:
+			printerr("Material Creator: Warning: When saving outside of the Godot project, "
+					+ "prefer exporting instead. A Godot resource may not be functional "
+					+ "without the context of its original project (ex: script paths).")
+	elif ext == "silly_mat_loadable" and is_in_project:
+		err = ResourceSaver.save(silly_mat, path)
+	else:
+		err = silly_mat.write_to_file(path)
+	if err != OK:
+		printerr("Material Creator: Failed to save to %s, reason: %s" % [path, error_string(err)])
+	else:
+		print("Material Creator: Successfully saved to ", path)
+	# Inform the editor that files have changed on disk.
+	var efs: EditorFileSystem = editor_interface.get_resource_filesystem()
+	efs.scan()
+
+
+func load_file_resource_loader(path: String) -> void:
+	var loaded_file: Resource = ResourceLoader.load(path)
+	if loaded_file == null:
+		printerr("Material Creator: Failed to load file at %s" % path)
+		return
+	if loaded_file is SillyMaterialResource:
+		edit_silly_material(loaded_file)
+		return
+	if loaded_file is StandardMaterial3D:
+		edit_silly_material(SillyMaterialResource.from_material(loaded_file))
+		return
+
+
+func load_file_directly(path: String) -> void:
+	var silly_mat := SillyMaterialResource.read_from_file(path)
+	if silly_mat == null:
+		printerr("Material Creator: Failed to directly load file at %s" % path)
+	edit_silly_material(silly_mat)
+
+
+func edit_silly_material(silly_mat: SillyMaterialResource) -> void:
+	albedo_color_picker.color = silly_mat.albedo_color
+	metallic_slider.value = silly_mat.metallic_strength
+	roughness_slider.value = silly_mat.roughness_strength
+
+
+func _create_silly_material_from_editor_values() -> SillyMaterialResource:
+	var color: Color = albedo_color_picker.color
+	var metallic: float = metallic_slider.value
+	var roughness: float = roughness_slider.value
+	var silly_res := SillyMaterialResource.new()
+	silly_res.albedo_color = color
+	silly_res.metallic_strength = metallic
+	silly_res.roughness_strength = roughness
+	return silly_res
+
+
+func _apply_material_to_nodes(selected_nodes: Array[Node]) -> void:
+	if selected_nodes.is_empty():
+		printerr("Material Creator: Can't apply the material because there are no nodes selected!")
+		return
+	var new_material: StandardMaterial3D = _create_silly_material_from_editor_values().to_material()
+	# Go through the selected nodes and see if they are MeshInstance3D nodes.
+	# If they do, then call it to set the material to the silly material.
+	var applied: bool = false
+	for node in selected_nodes:
+		if node is MeshInstance3D:
+			node.set_surface_override_material(0, new_material)
+			applied = true
+	if applied:
+		print("Material Creator: Applied material to selected MeshInstance3D nodes!")
+	else:
+		printerr("Material Creator: Can't apply the material because there are no MeshInstance3D nodes selected!")
+
+
+func _on_apply_button_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: Array[Node] = editor_selection.get_selected_nodes()
+	_apply_material_to_nodes(selected_nodes)
+
+
+func _on_save_button_pressed() -> void:
+	save_material_dialog.popup_centered(save_material_dialog.min_size * EditorInterface.get_editor_scale())
+
+
+func _on_export_button_pressed() -> void:
+	export_material_dialog.popup_centered(export_material_dialog.min_size * EditorInterface.get_editor_scale())
+
+
+func _on_load_button_importer_pressed() -> void:
+	load_material_importer_dialog.popup_centered(load_material_importer_dialog.min_size * EditorInterface.get_editor_scale())
+
+
+func _on_load_button_loader_pressed() -> void:
+	load_material_loader_dialog.popup_centered(load_material_loader_dialog.min_size * EditorInterface.get_editor_scale())
+
+
+func _on_import_button_directly_pressed() -> void:
+	import_material_directly_dialog.popup_centered(import_material_directly_dialog.min_size * EditorInterface.get_editor_scale())

+ 0 - 0
plugins/addons/material_creator/material_creator.gd.uid → plugins/addons/material_creator/editor/material_creator.gd.uid


+ 151 - 0
plugins/addons/material_creator/editor/material_dock.tscn

@@ -0,0 +1,151 @@
+[gd_scene load_steps=2 format=3 uid="uid://bo31028pgti5e"]
+
+[ext_resource type="Script" uid="uid://dy86u5ti4fb3m" path="res://addons/material_creator/editor/material_creator.gd" id="1"]
+
+[node name="Material Creator" type="Panel"]
+custom_minimum_size = Vector2(220, 530)
+offset_right = 220.0
+offset_bottom = 491.0
+script = ExtResource("1")
+
+[node name="VBoxContainer" type="VBoxContainer" parent="."]
+custom_minimum_size = Vector2(200, 510)
+layout_mode = 1
+anchors_preset = -1
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_left = 10.0
+offset_top = 10.0
+offset_right = -10.0
+offset_bottom = -10.0
+grow_horizontal = 2
+grow_vertical = 2
+
+[node name="AlbedoLabel" type="Label" parent="VBoxContainer"]
+custom_minimum_size = Vector2(16, 16)
+layout_mode = 2
+text = "Albedo Color:"
+
+[node name="AlbedoColorPicker" type="ColorPickerButton" parent="VBoxContainer"]
+custom_minimum_size = Vector2(0, 32)
+layout_mode = 2
+color = Color(1, 1, 1, 1)
+
+[node name="MetallicLabel" type="Label" parent="VBoxContainer"]
+custom_minimum_size = Vector2(16, 16)
+layout_mode = 2
+text = "Metallic Strength:"
+
+[node name="MetallicSlider" type="HSlider" parent="VBoxContainer"]
+custom_minimum_size = Vector2(16, 16)
+layout_mode = 2
+max_value = 1.0
+step = 0.05
+
+[node name="RoughnessLabel" type="Label" parent="VBoxContainer"]
+custom_minimum_size = Vector2(16, 16)
+layout_mode = 2
+text = "Roughness Strength:"
+
+[node name="RoughnessSlider" type="HSlider" parent="VBoxContainer"]
+custom_minimum_size = Vector2(16, 16)
+layout_mode = 2
+max_value = 1.0
+step = 0.05
+ticks_on_borders = true
+
+[node name="HSeparator" type="HSeparator" parent="VBoxContainer"]
+custom_minimum_size = Vector2(8, 8)
+layout_mode = 2
+
+[node name="ApplyButton" type="Button" parent="VBoxContainer"]
+custom_minimum_size = Vector2(0, 35)
+layout_mode = 2
+tooltip_text = "Applies this material to all selected MeshInstance3D nodes."
+text = "Apply Material"
+
+[node name="SaveButton" type="Button" parent="VBoxContainer"]
+custom_minimum_size = Vector2(0, 35)
+layout_mode = 2
+tooltip_text = "Save a `.silly_mat_loadable` file using a custom ResourceFormatSaver or save a `.tres` using Godot's built-in ResourceFormatSaverText."
+text = "Save Material
+(ResourceFormatSaver)"
+
+[node name="ExportButton" type="Button" parent="VBoxContainer"]
+custom_minimum_size = Vector2(0, 35)
+layout_mode = 2
+tooltip_text = "Export a `.silly_mat_*` file using the functions on SillyMaterialResource. This works for files outside of the `res://` folder, and can be done at runtime."
+text = "Export Material
+(directly at runtime)"
+
+[node name="LoadButtonLoader" type="Button" parent="VBoxContainer"]
+custom_minimum_size = Vector2(0, 60)
+layout_mode = 2
+tooltip_text = "Load a `.silly_mat_loadable` using a custom ResourceFormatLoader or load a `.tres` using Godot's built-in ResourceFormatLoaderText."
+text = "Load Material
+(ResourceFormatLoader)"
+clip_text = true
+
+[node name="LoadButtonImporter" type="Button" parent="VBoxContainer"]
+custom_minimum_size = Vector2(0, 60)
+layout_mode = 2
+tooltip_text = "Load a `.silly_mat_importable` that was imported by an EditorImportPlugin. The loaded data actually comes from the corresponding imported file saved as `res://.godot/imported/something.silly_mat_importable-hash.res`."
+text = "Load Imported Material
+(EditorImportPlugin)"
+clip_text = true
+
+[node name="ImportButtonDirectly" type="Button" parent="VBoxContainer"]
+custom_minimum_size = Vector2(0, 60)
+layout_mode = 2
+tooltip_text = "Import a `.silly_mat_*` directly from the source file, performing an import on request instead of loading data the editor imported earlier. This ignores any editor import settings, works for files outside of the `res://` folder, and can be done at runtime."
+text = "Import Material
+(directly at runtime)"
+clip_text = true
+
+[node name="SaveMaterialDialog" type="FileDialog" parent="."]
+size = Vector2i(1000, 500)
+min_size = Vector2i(1000, 500)
+filters = PackedStringArray("*.silly_mat_loadable ; Loadable Silly Material (editable)", "*.tres ; Godot Resource (resource)")
+
+[node name="ExportMaterialDialog" type="FileDialog" parent="."]
+size = Vector2i(1000, 500)
+min_size = Vector2i(1000, 500)
+access = 2
+filters = PackedStringArray("*.silly_mat_importable ; Importable Silly Material (customizable)", "*.silly_mat_loadable ; Loadable Silly Material (editable)")
+
+[node name="LoadMaterialLoaderDialog" type="FileDialog" parent="."]
+title = "Open a File"
+size = Vector2i(1000, 500)
+min_size = Vector2i(1000, 500)
+ok_button_text = "Open"
+file_mode = 0
+filters = PackedStringArray("*.silly_mat_loadable ; Loadable Silly Material (editable)", "*.tres ; Godot Resource (resource)")
+
+[node name="LoadMaterialImporterDialog" type="FileDialog" parent="."]
+title = "Open a File"
+size = Vector2i(1000, 500)
+min_size = Vector2i(1000, 500)
+ok_button_text = "Open"
+file_mode = 0
+filters = PackedStringArray("*.silly_mat_importable ; Importable Silly Material (customizable)")
+
+[node name="ImportMaterialDirectlyDialog" type="FileDialog" parent="."]
+title = "Open a File"
+size = Vector2i(1000, 500)
+min_size = Vector2i(1000, 500)
+ok_button_text = "Open"
+file_mode = 0
+access = 2
+filters = PackedStringArray("*.silly_mat_importable ; Importable Silly Material (customizable)", "*.silly_mat_loadable ; Loadable Silly Material (editable)")
+
+[connection signal="pressed" from="VBoxContainer/ApplyButton" to="." method="_on_apply_button_pressed"]
+[connection signal="pressed" from="VBoxContainer/SaveButton" to="." method="_on_save_button_pressed"]
+[connection signal="pressed" from="VBoxContainer/ExportButton" to="." method="_on_export_button_pressed"]
+[connection signal="pressed" from="VBoxContainer/LoadButtonLoader" to="." method="_on_load_button_loader_pressed"]
+[connection signal="pressed" from="VBoxContainer/LoadButtonImporter" to="." method="_on_load_button_importer_pressed"]
+[connection signal="pressed" from="VBoxContainer/ImportButtonDirectly" to="." method="_on_import_button_directly_pressed"]
+[connection signal="file_selected" from="SaveMaterialDialog" to="." method="_save_or_export_file"]
+[connection signal="file_selected" from="ExportMaterialDialog" to="." method="_save_or_export_file"]
+[connection signal="file_selected" from="LoadMaterialLoaderDialog" to="." method="load_file_resource_loader"]
+[connection signal="file_selected" from="LoadMaterialImporterDialog" to="." method="load_file_resource_loader"]
+[connection signal="file_selected" from="ImportMaterialDirectlyDialog" to="." method="load_file_directly"]

+ 9 - 0
plugins/addons/material_creator/examples/blue.tres

@@ -0,0 +1,9 @@
+[gd_resource type="Resource" script_class="SillyMaterialResource" load_steps=2 format=3 uid="uid://d1o64hhg6sxuk"]
+
+[ext_resource type="Script" uid="uid://bjnq25fa3ptjc" path="res://addons/material_creator/silly_material_resource.gd" id="1_l7fpc"]
+
+[resource]
+script = ExtResource("1_l7fpc")
+albedo_color = Color(0.06666667, 0.06666667, 0.93333334, 1)
+metallic_strength = 0.9
+roughness_strength = 0.1

+ 1 - 0
plugins/addons/material_creator/examples/cyan.silly_mat_loadable

@@ -0,0 +1 @@
+{"albedo_color":[0.0666666701436043,0.933333337306976,0.933333337306976],"metallic_strength":0.8,"roughness_strength":0.2}

+ 1 - 0
plugins/addons/material_creator/examples/cyan.silly_mat_loadable.uid

@@ -0,0 +1 @@
+uid://fhh5emd0x6wb

+ 1 - 0
plugins/addons/material_creator/examples/green_as_standard_mat.silly_mat_importable

@@ -0,0 +1 @@
+{"albedo_color":[0.0666666701436043,0.933333337306976,0.0666666701436043],"metallic_strength":0.35,"roughness_strength":0.65}

+ 15 - 0
plugins/addons/material_creator/examples/green_as_standard_mat.silly_mat_importable.import

@@ -0,0 +1,15 @@
+[remap]
+
+importer="demos.silly_material_importable.standard_material_3d"
+type="StandardMaterial3D"
+uid="uid://sy27jfukvqij"
+path="res://.godot/imported/green_as_standard_mat.silly_mat_importable-8bc4ac1c11ef82b73959db611c6e0025.res"
+
+[deps]
+
+source_file="res://addons/material_creator/examples/green_as_standard_mat.silly_mat_importable"
+dest_files=["res://.godot/imported/green_as_standard_mat.silly_mat_importable-8bc4ac1c11ef82b73959db611c6e0025.res"]
+
+[params]
+
+make_more_red=false

+ 1 - 0
plugins/addons/material_creator/examples/yellow.silly_mat_importable

@@ -0,0 +1 @@
+{"albedo_color":[0.933333337306976,0.933333337306976,0.0666666701436043],"metallic_strength":0.65,"roughness_strength":0.35}

+ 15 - 0
plugins/addons/material_creator/examples/yellow.silly_mat_importable.import

@@ -0,0 +1,15 @@
+[remap]
+
+importer="demos.silly_material_importable.silly_material_resource"
+type="Resource"
+uid="uid://b38mu7kfwyrt0"
+path="res://.godot/imported/yellow.silly_mat_importable-3de5551b2840cbea5353335a74390454.res"
+
+[deps]
+
+source_file="res://addons/material_creator/examples/yellow.silly_mat_importable"
+dest_files=["res://.godot/imported/yellow.silly_mat_importable-3de5551b2840cbea5353335a74390454.res"]
+
+[params]
+
+make_more_red=false

+ 1 - 0
plugins/addons/material_creator/examples/yellow_tinted_red.silly_mat_importable

@@ -0,0 +1 @@
+{"albedo_color":[0.933333337306976,0.933333337306976,0.0666666701436043],"metallic_strength":0.65,"roughness_strength":0.35}

+ 15 - 0
plugins/addons/material_creator/examples/yellow_tinted_red.silly_mat_importable.import

@@ -0,0 +1,15 @@
+[remap]
+
+importer="demos.silly_material_importable.silly_material_resource"
+type="Resource"
+uid="uid://dhhdox2m0kn0l"
+path="res://.godot/imported/yellow_tinted_red.silly_mat_importable-d6610364b07d235546934af33df4be98.res"
+
+[deps]
+
+source_file="res://addons/material_creator/examples/yellow_tinted_red.silly_mat_importable"
+dest_files=["res://.godot/imported/yellow_tinted_red.silly_mat_importable-d6610364b07d235546934af33df4be98.res"]
+
+[params]
+
+make_more_red=true

+ 0 - 165
plugins/addons/material_creator/material_creator.gd

@@ -1,165 +0,0 @@
-@tool
-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 editor_interface: EditorInterface
-
-
-func _ready() -> void:
-	# Connect all of the signals we'll need to save and load silly materials.
-	$VBoxContainer/ApplyButton.pressed.connect(apply_pressed)
-	$VBoxContainer/SaveButton.pressed.connect(save_pressed)
-	$VBoxContainer/LoadButton.pressed.connect(load_pressed)
-
-	$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)
-
-
-func save_pressed() -> void:
-	$SaveMaterialDialog.popup_centered_ratio()
-
-
-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"
-	# function (which only MeshInstance3D has by default). If they do, then set the material
-	# to the silly material.
-	for node in selected_nodes:
-		if node.has_method(&"set_surface_override_material"):
-			node.set_surface_override_material(0, new_material)
-
-
-func load_file_selected(path: String) -> bool:
-	var ext := path.get_extension().to_lower()
-
-	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
-
-
-func _silly_resource_from_values() -> Resource:
-	var color: Color = $VBoxContainer/AlbedoColorPicker.color
-	var metallic: float = $VBoxContainer/MetallicSlider.value
-	var roughness: float = $VBoxContainer/RoughnessSlider.value
-	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
-
-# ---------------------------------------------------------------
-# Source (.mtxt) helpers.
-# ---------------------------------------------------------------
-
-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

+ 0 - 83
plugins/addons/material_creator/material_dock.tscn

@@ -1,83 +0,0 @@
-[gd_scene load_steps=2 format=3 uid="uid://bo31028pgti5e"]
-
-[ext_resource type="Script" uid="uid://dy86u5ti4fb3m" path="res://addons/material_creator/material_creator.gd" id="1"]
-
-[node name="Material Creator" type="Panel"]
-custom_minimum_size = Vector2(208, 0)
-offset_right = 220.0
-offset_bottom = 340.0
-script = ExtResource("1")
-
-[node name="VBoxContainer" type="VBoxContainer" parent="."]
-layout_mode = 0
-anchor_left = 0.5
-anchor_right = 0.5
-anchor_bottom = 1.0
-offset_left = -100.0
-offset_right = 100.0
-grow_horizontal = 2
-grow_vertical = 2
-
-[node name="AlbedoLabel" type="Label" parent="VBoxContainer"]
-layout_mode = 2
-text = "Albedo Color:"
-
-[node name="AlbedoColorPicker" type="ColorPickerButton" parent="VBoxContainer"]
-custom_minimum_size = Vector2(0, 32)
-layout_mode = 2
-color = Color(1, 1, 1, 1)
-
-[node name="MetallicLabel" type="Label" parent="VBoxContainer"]
-layout_mode = 2
-text = "Metallic Strength:"
-
-[node name="MetallicSlider" type="HSlider" parent="VBoxContainer"]
-layout_mode = 2
-max_value = 1.0
-step = 0.05
-
-[node name="RoughnessLabel" type="Label" parent="VBoxContainer"]
-layout_mode = 2
-text = "Roughness Strength:"
-
-[node name="RoughnessSlider" type="HSlider" parent="VBoxContainer"]
-layout_mode = 2
-max_value = 1.0
-step = 0.05
-ticks_on_borders = true
-
-[node name="HSeparator" type="HSeparator" parent="VBoxContainer"]
-layout_mode = 2
-
-[node name="ApplyButton" type="Button" parent="VBoxContainer"]
-layout_mode = 2
-text = "Apply Material"
-
-[node name="SaveButton" type="Button" parent="VBoxContainer"]
-layout_mode = 2
-text = "Save Material"
-
-[node name="LoadButton" type="Button" parent="VBoxContainer"]
-layout_mode = 2
-text = "Load Material"
-clip_text = true
-
-[node name="Label" type="Label" parent="VBoxContainer/LoadButton"]
-visible = false
-layout_mode = 0
-anchor_right = 1.0
-anchor_bottom = 1.0
-offset_bottom = -10.0
-grow_horizontal = 2
-grow_vertical = 2
-text = "Load silly material and
-apply to selected node(s)"
-
-[node name="SaveMaterialDialog" type="FileDialog" parent="."]
-filters = PackedStringArray("*.silly_mat")
-
-[node name="LoadMaterialDialog" type="FileDialog" parent="."]
-title = "Open a File"
-ok_button_text = "Open"
-file_mode = 0
-filters = PackedStringArray("*.silly_mat")

+ 0 - 40
plugins/addons/material_creator/material_format_loader.gd

@@ -1,40 +0,0 @@
-@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

+ 0 - 35
plugins/addons/material_creator/material_format_saver.gd

@@ -1,35 +0,0 @@
-@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

+ 37 - 29
plugins/addons/material_creator/material_plugin.gd

@@ -1,38 +1,46 @@
-# A simple (and silly) material resource plugin. Allows you to make a really simple material
-# from a custom dock, that you can save and load, and apply to selected MeshInstances.
-#
-# SPECIAL NOTE: This technically should be using EditorImportPlugin and EditorExportPlugin
-# to handle the input and output of the silly material. However, currently you cannot export
-# custom resources in Godot, so instead we're using JSON files instead.
-#
-# This example should be replaced when EditorImportPlugin and EditorExportPlugin are both
-# fully working and you can save custom resources.
-
+## A simple (and silly) material resource plugin. Allows you to make a really
+## simple material from a custom dock, which can be applied to meshes,
+## saved to files, loaded from files, imported from files, and more.
+##
+## See the documentation in the `README.md` file for more information,
+## and also the documentation in each class (Ctrl+Click on these in Godot):
+## - SillyMaterialResource
+## - ImportSillyMatAsSillyMaterialResource
+## - ImportSillyMatAsStandardMaterial3D
+## - SillyMatFormatLoader
+## - SillyMatFormatSaver
 @tool
 extends EditorPlugin
 
-var io_material_dialog: Panel
-var _loader: SillyMatFormatLoader
-var _saver: SillyMatFormatSaver
 
+var _material_creator_dock: Panel
+var _silly_mat_loader := SillyMatFormatLoader.new()
+var _silly_mat_saver := SillyMatFormatSaver.new()
+var _import_as_silly_mat_res := ImportSillyMatAsSillyMaterialResource.new()
+var _import_as_standard_mat := ImportSillyMatAsStandardMaterial3D.new()
 
-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)
+func _enter_tree() -> void:
+	# Set up the loader and saver.
+	ResourceLoader.add_resource_format_loader(_silly_mat_loader)
+	ResourceSaver.add_resource_format_saver(_silly_mat_saver)
+	# Set up the importers.
+	add_import_plugin(_import_as_silly_mat_res)
+	add_import_plugin(_import_as_standard_mat)
+	# Set up the silly material creator dock.
+	const dock_scene: PackedScene = preload("res://addons/material_creator/editor/material_dock.tscn")
+	_material_creator_dock = dock_scene.instantiate()
+	_material_creator_dock.editor_interface = get_editor_interface()
+	var dock_scale: float = EditorInterface.get_editor_scale() * 0.85
+	_material_creator_dock.custom_minimum_size *= dock_scale
+	for child in _material_creator_dock.find_children("*", "Control"):
+		child.custom_minimum_size *= dock_scale
+	add_control_to_dock(DOCK_SLOT_LEFT_UL, _material_creator_dock)
 
 
 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
+	remove_control_from_docks(_material_creator_dock)
+	ResourceLoader.remove_resource_format_loader(_silly_mat_loader)
+	ResourceSaver.remove_resource_format_saver(_silly_mat_saver)
+	remove_import_plugin(_import_as_silly_mat_res)
+	remove_import_plugin(_import_as_standard_mat)

+ 0 - 47
plugins/addons/material_creator/material_resource.gd

@@ -1,47 +0,0 @@
-@tool
-extends Resource
-class_name SillyMaterialResource
-
-# 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 a dictionary so we can convert it
-# into the JSON format.
-func make_json() -> String:
-	var json_dict := {}
-
-	json_dict["albedo_color"] = {}
-	json_dict["albedo_color"]["r"] = albedo_color.r
-	json_dict["albedo_color"]["g"] = albedo_color.g
-	json_dict["albedo_color"]["b"] = albedo_color.b
-
-	json_dict["metallic_strength"] = metallic_strength
-	json_dict["roughness_strength"] = roughness_strength
-
-	return JSON.stringify(json_dict)
-
-
-# Convert the passed in string to a JSON dictionary, and then
-# fill in our data.
-func from_json(json_dict_as_string: String) -> void:
-	var json_dict: Dictionary = JSON.parse_string(json_dict_as_string)
-
-	albedo_color.r = json_dict["albedo_color"]["r"]
-	albedo_color.g = json_dict["albedo_color"]["g"]
-	albedo_color.b = json_dict["albedo_color"]["b"]
-
-	metallic_strength = json_dict["metallic_strength"]
-	roughness_strength = json_dict["roughness_strength"]
-
-
-# Make a StandardMaterial3D using our variables.
-func make_material() -> StandardMaterial3D:
-	var mat = StandardMaterial3D.new()
-	mat.albedo_color = albedo_color
-	mat.metallic = metallic_strength
-	mat.roughness = roughness_strength
-	return mat

+ 1 - 1
plugins/addons/material_creator/plugin.cfg

@@ -2,6 +2,6 @@
 
 name="Material Creator Plugin Demo"
 description="Loads and saves a 3D Material from an external text file"
-author="TwistedTwigleg"
+author="Aaron Franke, TwistedTwigleg, Šarūnas Ramonas"
 version="1.0"
 script="material_plugin.gd"

+ 109 - 0
plugins/addons/material_creator/silly_material_resource.gd

@@ -0,0 +1,109 @@
+## Example class that can be imported, exported, loaded, saved, etc, in various ways.
+##
+## - To perform an editor import as a `SillyMaterialResource`, the class
+##   `ImportSillyMatAsSillyMaterialResource` will handle files in the `res://`
+##   folder ending in `.silly_mat_importable` and import them.
+##   as long as "Silly Material Resource" is selected in the Import dock.
+##   Then `ResourceLoader.load()` will return a read-only `SillyMaterialResource`.
+##
+## - To perform an editor import as a `StandardMaterial3D`, the class
+##   `ImportSillyMatAsStandardMaterial3D` will handle files in the `res://`
+##   folder ending in `.silly_mat_importable` and import them,
+##   as long as "Standard Material 3D" is selected in the Import dock.
+##   Then `ResourceLoader.load()` will return a read-only `StandardMaterial3D`.
+##
+## - To perform an editor load as a SillyMaterialResource, the class
+##   `SillyMatFormatLoader` will handle files in the `res://`
+##   folder ending in `.silly_mat_loadable` and load them.
+##   Then `ResourceLoader.load()` will return a writeable `SillyMaterialResource`.
+##   This can then be saved back to a file with `SillyMatFormatSaver`.
+##
+## - To perform a runtime (or editor) import into a StandardMaterial3D, run the
+##   `read_from_file` function, which reads the data from a file and runs
+##   `from_json_dictionary`, then run `to_material` to generate a material.
+##
+## - To perform a runtime (or editor) export of a StandardMaterial3D, run
+##   `from_material` to convert a material, then run the `write_to_file`
+##   function, which runs `to_json_dictionary` and saves this to a file.
+##
+## These functions should be placed in this class to support runtime imports
+## and exports, but the editor classes can also make use of these functions,
+## allowing the editor-only classes to be lightweight wrappers.
+##
+## For a more comprehensive example, see the GLTF module in Godot's source code.
+## For a less comprehensive example, see the "simple_import_plugin" folder.
+@tool
+class_name SillyMaterialResource
+extends Resource
+
+
+# Use export to make properties visible in the inspector
+# and serializable 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
+
+
+## Given a Dictionary parsed from JSON data, read in the data as a new SillyMaterialResource.
+static func from_json_dictionary(json_dictionary: Dictionary) -> SillyMaterialResource:
+	var ret := SillyMaterialResource.new()
+	# Note: In an actual importer where you need to handle arbitrary user data,
+	# you may wish to do things like checking if the key exists, checking if
+	# the value is an array, checking if the array has a length of 3, checking
+	# if each value in the array is a number, and so on.
+	# For simplicity, these things are omitted from this demo's example code.
+	var albedo_array: Array = json_dictionary["albedo_color"]
+	ret.albedo_color.r = albedo_array[0]
+	ret.albedo_color.g = albedo_array[1]
+	ret.albedo_color.b = albedo_array[2]
+	ret.metallic_strength = json_dictionary["metallic_strength"]
+	ret.roughness_strength = json_dictionary["roughness_strength"]
+	return ret
+
+
+## Convert SillyMaterialResource data into a Dictionary for saving as JSON.
+## To perform a runtime export of a StandardMaterial3D, run this function after `from_material`.
+func to_json_dictionary() -> Dictionary:
+	return {
+		"albedo_color": [albedo_color.r, albedo_color.g, albedo_color.b],
+		"metallic_strength": metallic_strength,
+		"roughness_strength": roughness_strength,
+	}
+
+
+## Given a StandardMaterial3D, copy its data to a new SillyMaterialResource.
+static func from_material(mat: StandardMaterial3D) -> SillyMaterialResource:
+	var ret := SillyMaterialResource.new()
+	ret.albedo_color = mat.albedo_color
+	ret.metallic_strength = mat.metallic
+	ret.roughness_strength = mat.roughness
+	return ret
+
+
+## Create a new StandardMaterial3D using the data in this SillyMaterialResource.
+func to_material() -> StandardMaterial3D:
+	var mat = StandardMaterial3D.new()
+	mat.albedo_color = albedo_color
+	mat.metallic = metallic_strength
+	mat.roughness = roughness_strength
+	return mat
+
+
+## Wrapper around `from_json_dictionary` that reads from a file at the given path.
+static func read_from_file(path: String) -> SillyMaterialResource:
+	var mat_file := FileAccess.open(path, FileAccess.READ)
+	if mat_file == null:
+		return null
+	var json_dict: Dictionary = JSON.parse_string(mat_file.get_as_text())
+	return from_json_dictionary(json_dict)
+
+
+## Wrapper around `to_json_dictionary` that writes to a file at the given path.
+func write_to_file(path: String) -> Error:
+	var mat_file := FileAccess.open(path, FileAccess.WRITE)
+	if mat_file == null:
+		return ERR_CANT_OPEN
+	var json_dict: Dictionary = to_json_dictionary()
+	mat_file.store_string(JSON.stringify(json_dict))
+	mat_file.store_string("\n")
+	return OK

+ 0 - 0
plugins/addons/material_creator/material_resource.gd.uid → plugins/addons/material_creator/silly_material_resource.gd.uid


+ 0 - 15
plugins/addons/material_import_plugin/test.mtxt.import

@@ -1,15 +0,0 @@
-[remap]
-
-importer="demos.sillymaterial"
-type="Material"
-uid="uid://bqja7mgxfmfqa"
-path="res://.godot/imported/test.mtxt-32ce4469df24b9f725d1e3476ff3b332.res"
-
-[deps]
-
-source_file="res://addons/material_import_plugin/test.mtxt"
-dest_files=["res://.godot/imported/test.mtxt-32ce4469df24b9f725d1e3476ff3b332.res"]
-
-[params]
-
-use_red_anyway=false

+ 0 - 0
plugins/addons/material_import_plugin/README.md → plugins/addons/simple_import_plugin/README.md


+ 1 - 1
plugins/addons/material_import_plugin/import.gd → plugins/addons/simple_import_plugin/import.gd

@@ -7,7 +7,7 @@ enum Preset {
 
 
 func _get_importer_name() -> String:
-	return "demos.sillymaterial"
+	return "demos.mtxt"
 
 
 func _get_visible_name() -> String:

+ 0 - 0
plugins/addons/material_import_plugin/import.gd.uid → plugins/addons/simple_import_plugin/import.gd.uid


+ 1 - 1
plugins/addons/material_import_plugin/plugin.cfg → plugins/addons/simple_import_plugin/plugin.cfg

@@ -1,6 +1,6 @@
 [plugin]
 
-name="Material Importer Plugin Demo"
+name="Simple Importer Plugin Demo"
 description="Imports a 3D Material from an external text file"
 author="George Marques"
 version="1.0"

+ 0 - 0
plugins/addons/material_import_plugin/plugin.gd → plugins/addons/simple_import_plugin/plugin.gd


+ 0 - 0
plugins/addons/material_import_plugin/plugin.gd.uid → plugins/addons/simple_import_plugin/plugin.gd.uid


+ 0 - 0
plugins/addons/material_import_plugin/test.mtxt → plugins/addons/simple_import_plugin/test.mtxt


+ 15 - 0
plugins/addons/simple_import_plugin/test.mtxt.import

@@ -0,0 +1,15 @@
+[remap]
+
+importer="demos.mtxt"
+type="Material"
+uid="uid://bqja7mgxfmfqa"
+path="res://.godot/imported/test.mtxt-cc369242bc971647fccdadd6e971f1d0.res"
+
+[deps]
+
+source_file="res://addons/simple_import_plugin/test.mtxt"
+dest_files=["res://.godot/imported/test.mtxt-cc369242bc971647fccdadd6e971f1d0.res"]
+
+[params]
+
+use_red_anyway=false

+ 1 - 1
plugins/project.godot

@@ -31,7 +31,7 @@ gdscript/warnings/untyped_declaration=1
 
 [editor_plugins]
 
-enabled=PackedStringArray("res://addons/custom_node/plugin.cfg", "res://addons/main_screen/plugin.cfg", "res://addons/material_creator/plugin.cfg", "res://addons/material_import_plugin/plugin.cfg")
+enabled=PackedStringArray("res://addons/custom_node/plugin.cfg", "res://addons/main_screen/plugin.cfg", "res://addons/material_creator/plugin.cfg", "res://addons/simple_import_plugin/plugin.cfg")
 
 [rendering]
 

BIN
plugins/screenshots/heart_custom_node.webp


BIN
plugins/screenshots/heart_plugin.png


BIN
plugins/screenshots/main_screen_plugin.png


BIN
plugins/screenshots/main_screen_plugin.webp


BIN
plugins/screenshots/material_creator_plugin_1.png


BIN
plugins/screenshots/material_creator_plugin_2.png


BIN
plugins/screenshots/material_creator_plugin_applied.webp


BIN
plugins/screenshots/material_creator_plugin_dock.webp


BIN
plugins/screenshots/material_creator_plugin_imported_file_is_read_only.webp


BIN
plugins/screenshots/material_import_plugin.png


BIN
plugins/screenshots/simple_import_plugin.webp