Explorar o código

Improve Run-time File Saving and Loading demo (#1196)

- Add runtime MP3 and WAV loading with example files.
- Add runtime FBX loading (but no example files, due to known issues with
  the current FBX runtime loading implementation).
- Use supersampling for the 3D preview to further improve quality.
- Shorten file path to example glTF scene.
- Mention audio duration in the UI when loading an audio file.
Hugo Locurcio hai 3 meses
pai
achega
c97a648356

+ 6 - 2
loading/runtime_save_load/README.md

@@ -17,7 +17,8 @@ Can be loaded and saved at run-time:
 Can be loaded at run-time:
 Can be loaded at run-time:
 
 
 - Images (TGA, BMP, SVG[^2])
 - Images (TGA, BMP, SVG[^2])
-- Audio (Ogg Vorbis)
+- 3D scenes (FBX[^3])
+- Audio (Ogg Vorbis, MP3, WAV)
 - Fonts (TTF, OTF, WOFF, WOFF2, PFB, PFM, BMFont)
 - Fonts (TTF, OTF, WOFF, WOFF2, PFB, PFM, BMFont)
 
 
 [^1]: Manipulating custom binary formats is possible using the FileAccess and
 [^1]: Manipulating custom binary formats is possible using the FileAccess and
@@ -27,6 +28,9 @@ PackedByteArray classes, but this is not shown in this demo.
 with `.svg` extension using the FileAccess class, but this is not shown in
 with `.svg` extension using the FileAccess class, but this is not shown in
 this demo.
 this demo.
 
 
+[^3]: There are known issues with runtime FBX loading, as mentioned in issue
+[#96043](https://github.com/godotengine/godot/issues/96043).
+
 See the [Saving and Loading (Serialization)](/loading/serialization/) demo for
 See the [Saving and Loading (Serialization)](/loading/serialization/) demo for
 an example of saving/loading game progress.
 an example of saving/loading game progress.
 
 
@@ -42,7 +46,7 @@ Check out this demo on the asset library: https://godotengine.org/asset-library/
 
 
 ## Licenses
 ## Licenses
 
 
-- Files in `examples/3d_scenes/plastic_monobloc_chair_01_1k/` are copyright
+- Files in `examples/3d_scenes/gltf/` are copyright
   [Poly Haven](https://polyhaven.com/a/plastic_monobloc_chair_01)
   [Poly Haven](https://polyhaven.com/a/plastic_monobloc_chair_01)
   and are licensed under [CC0 1.0 Universal](https://creativecommons.org/publicdomain/zero/1.0/).
   and are licensed under [CC0 1.0 Universal](https://creativecommons.org/publicdomain/zero/1.0/).
 - Files in `examples/audio/` are copyright [Red Eclipse](https://redeclipse.net)
 - Files in `examples/audio/` are copyright [Red Eclipse](https://redeclipse.net)

+ 0 - 0
loading/runtime_save_load/examples/3d_scenes/plastic_monobloc_chair_01_1k/plastic_monobloc_chair_01.bin → loading/runtime_save_load/examples/3d_scenes/gltf/plastic_monobloc_chair_01.bin


+ 0 - 0
loading/runtime_save_load/examples/3d_scenes/plastic_monobloc_chair_01_1k/plastic_monobloc_chair_01_1k.gltf → loading/runtime_save_load/examples/3d_scenes/gltf/plastic_monobloc_chair_01_1k.gltf


+ 0 - 0
loading/runtime_save_load/examples/3d_scenes/plastic_monobloc_chair_01_1k/textures/plastic_monobloc_chair_01_arm_1k.jpg → loading/runtime_save_load/examples/3d_scenes/gltf/textures/plastic_monobloc_chair_01_arm_1k.jpg


+ 0 - 0
loading/runtime_save_load/examples/3d_scenes/plastic_monobloc_chair_01_1k/textures/plastic_monobloc_chair_01_diff_1k.jpg → loading/runtime_save_load/examples/3d_scenes/gltf/textures/plastic_monobloc_chair_01_diff_1k.jpg


+ 0 - 0
loading/runtime_save_load/examples/3d_scenes/plastic_monobloc_chair_01_1k/textures/plastic_monobloc_chair_01_nor_gl_1k.jpg → loading/runtime_save_load/examples/3d_scenes/gltf/textures/plastic_monobloc_chair_01_nor_gl_1k.jpg


BIN=BIN
loading/runtime_save_load/examples/audio/item_spawn.mp3


BIN=BIN
loading/runtime_save_load/examples/audio/item_spawn.wav


+ 54 - 30
loading/runtime_save_load/runtime_save_load.gd

@@ -6,6 +6,7 @@ extends Control
 @onready var plain_text_viewer_label := $MarginContainer/VBoxContainer/Result/PlainTextViewer/Label as Label
 @onready var plain_text_viewer_label := $MarginContainer/VBoxContainer/Result/PlainTextViewer/Label as Label
 @onready var texture_viewer := $MarginContainer/VBoxContainer/Result/TextureViewer as TextureRect
 @onready var texture_viewer := $MarginContainer/VBoxContainer/Result/TextureViewer as TextureRect
 @onready var audio_player := $MarginContainer/VBoxContainer/Result/AudioPlayer as Button
 @onready var audio_player := $MarginContainer/VBoxContainer/Result/AudioPlayer as Button
+@onready var audio_player_information := $MarginContainer/VBoxContainer/Result/AudioPlayer/Information as Label
 @onready var audio_stream_player := $MarginContainer/VBoxContainer/Result/AudioPlayer/AudioStreamPlayer as AudioStreamPlayer
 @onready var audio_stream_player := $MarginContainer/VBoxContainer/Result/AudioPlayer/AudioStreamPlayer as AudioStreamPlayer
 @onready var scene_viewer := $MarginContainer/VBoxContainer/Result/SceneViewer as SubViewportContainer
 @onready var scene_viewer := $MarginContainer/VBoxContainer/Result/SceneViewer as SubViewportContainer
 @onready var scene_viewer_camera := $MarginContainer/VBoxContainer/Result/SceneViewer/SubViewport/Camera3D as Camera3D
 @onready var scene_viewer_camera := $MarginContainer/VBoxContainer/Result/SceneViewer/SubViewport/Camera3D as Camera3D
@@ -24,19 +25,6 @@ var zip_reader := ZIPReader.new()
 # so that it can be exported later.
 # so that it can be exported later.
 var scene_viewer_root_node: Node
 var scene_viewer_root_node: Node
 
 
-func _on_browse_pressed() -> void:
-	file_dialog.popup_centered_ratio()
-
-
-func _on_file_path_text_submitted(new_text: String) -> void:
-	open_file(new_text)
-	# Put the caret at the end of the submitted text.
-	file_path_edit.caret_column = file_path_edit.text.length()
-
-
-func _on_file_dialog_file_selected(path: String) -> void:
-	open_file(path)
-
 
 
 func reset_visibility() -> void:
 func reset_visibility() -> void:
 	plain_text_viewer.visible = false
 	plain_text_viewer.visible = false
@@ -58,6 +46,20 @@ func reset_visibility() -> void:
 	export_button.disabled = false
 	export_button.disabled = false
 
 
 
 
+func _on_browse_pressed() -> void:
+	file_dialog.popup_centered_ratio()
+
+
+func _on_file_path_text_submitted(new_text: String) -> void:
+	open_file(new_text)
+	# Put the caret at the end of the submitted text.
+	file_path_edit.caret_column = file_path_edit.text.length()
+
+
+func _on_file_dialog_file_selected(path: String) -> void:
+	open_file(path)
+
+
 func _on_audio_player_pressed() -> void:
 func _on_audio_player_pressed() -> void:
 	audio_stream_player.play()
 	audio_stream_player.play()
 
 
@@ -98,8 +100,8 @@ func _on_export_file_dialog_file_selected(path: String) -> void:
 			image.save_webp(path)
 			image.save_webp(path)
 
 
 	elif audio_player.visible:
 	elif audio_player.visible:
-		# Ogg Vorbis audio can't be exported at runtime to a standard format
-		# (only WAV files can be using `AudioStreamWAV.save_to_wav()`).
+		# Ogg Vorbis and MP3 audio can't be exported at runtime to a standard format
+		# (only WAV files can be saved from WAV sources using `AudioStreamWAV.save_to_wav()`).
 		pass
 		pass
 
 
 	elif scene_viewer.visible:
 	elif scene_viewer.visible:
@@ -166,31 +168,53 @@ func open_file(path: String) -> void:
 		texture_viewer.texture = ImageTexture.create_from_image(image)
 		texture_viewer.texture = ImageTexture.create_from_image(image)
 
 
 	# Audio.
 	# Audio.
-	# Run-time MP3 and WAV loading aren't supported by the engine yet.
-	elif path_lower.ends_with(".ogg"):
+	elif path_lower.ends_with(".ogg") or path_lower.ends_with(".mp3") or path_lower.ends_with(".wav"):
 		# `AudioStreamOggVorbis.load_from_buffer()` can alternatively be used
 		# `AudioStreamOggVorbis.load_from_buffer()` can alternatively be used
 		# if you have Ogg Vorbis data in a PackedByteArray instead of a file.
 		# if you have Ogg Vorbis data in a PackedByteArray instead of a file.
-		audio_stream_player.stream = AudioStreamOggVorbis.load_from_file(path)
+		if path_lower.ends_with(".ogg"):
+			audio_stream_player.stream = AudioStreamOggVorbis.load_from_file(path)
+		elif path_lower.ends_with(".mp3"):
+			audio_stream_player.stream = AudioStreamMP3.load_from_file(path)
+		elif path_lower.ends_with(".wav"):
+			audio_stream_player.stream = AudioStreamWAV.load_from_file(path)
 		reset_visibility()
 		reset_visibility()
 		export_button.disabled = true
 		export_button.disabled = true
 		audio_player.visible = true
 		audio_player.visible = true
+		var duration := roundi(audio_stream_player.stream.get_length())
+		@warning_ignore("integer_division")
+		audio_player_information.text = "Duration: %02d:%02d" % [duration / 60, duration % 60]
 
 
 	# 3D scenes.
 	# 3D scenes.
-	elif path_lower.ends_with(".gltf") or path_lower.ends_with(".glb"):
+	elif path_lower.ends_with(".gltf") or path_lower.ends_with(".glb") or path_lower.ends_with(".fbx"):
 		# GLTFState is used by GLTFDocument to store the loaded scene's state.
 		# GLTFState is used by GLTFDocument to store the loaded scene's state.
 		# GLTFDocument is the class that handles actually loading glTF data into a Godot node tree,
 		# GLTFDocument is the class that handles actually loading glTF data into a Godot node tree,
 		# which means it supports glTF features such as lights and cameras.
 		# which means it supports glTF features such as lights and cameras.
-		var gltf_document := GLTFDocument.new()
-		var gltf_state := GLTFState.new()
-		var error := gltf_document.append_from_file(path, gltf_state)
-		if error == OK:
-			scene_viewer_root_node = gltf_document.generate_scene(gltf_state)
-			reset_visibility()
-			scene_viewer.add_child(scene_viewer_root_node)
-			export_file_dialog.filters = ["*.gltf ; glTF Text Scene", "*.glb ; glTF Binary Scene"]
-			scene_viewer.visible = true
-		else:
-			show_error('Couldn\'t load "%s" as a glTF scene (error code: %s).' % [path.get_file(), error_string(error)])
+		#
+		# The same applies to FBX, except FBXState and FBXDocument are used instead.
+		if path_lower.ends_with(".gltf") or path_lower.ends_with(".glb"):
+			var gltf_document := GLTFDocument.new()
+			var gltf_state := GLTFState.new()
+			var error := gltf_document.append_from_file(path, gltf_state)
+			if error == OK:
+				scene_viewer_root_node = gltf_document.generate_scene(gltf_state)
+				reset_visibility()
+				scene_viewer.add_child(scene_viewer_root_node)
+				export_file_dialog.filters = ["*.gltf ; glTF Text Scene", "*.glb ; glTF Binary Scene"]
+				scene_viewer.visible = true
+			else:
+				show_error('Couldn\'t load "%s" as a glTF scene (error code: %s).' % [path.get_file(), error_string(error)])
+		elif path_lower.ends_with(".fbx"):
+			var fbx_document := FBXDocument.new()
+			var fbx_state := FBXState.new()
+			var error := fbx_document.append_from_file(path, fbx_state)
+			if error == OK:
+				scene_viewer_root_node = fbx_document.generate_scene(fbx_state)
+				reset_visibility()
+				scene_viewer.add_child(scene_viewer_root_node)
+				export_file_dialog.filters = ["*.fbx ; FBX Scene"]
+				scene_viewer.visible = true
+			else:
+				show_error('Couldn\'t load "%s" as a FBX scene (error code: %s).' % [path.get_file(), error_string(error)])
 
 
 	# Fonts.
 	# Fonts.
 	elif (
 	elif (

+ 8 - 2
loading/runtime_save_load/runtime_save_load.tscn

@@ -55,7 +55,7 @@ text = "Browse"
 
 
 [node name="FileDialog" type="FileDialog" parent="MarginContainer/VBoxContainer/HBoxContainer"]
 [node name="FileDialog" type="FileDialog" parent="MarginContainer/VBoxContainer/HBoxContainer"]
 title = "Open a File"
 title = "Open a File"
-size = Vector2i(392, 159)
+size = Vector2i(392, 175)
 ok_button_text = "Open"
 ok_button_text = "Open"
 file_mode = 0
 file_mode = 0
 access = 2
 access = 2
@@ -83,7 +83,6 @@ layout_mode = 2
 expand_mode = 3
 expand_mode = 3
 
 
 [node name="AudioPlayer" type="Button" parent="MarginContainer/VBoxContainer/Result"]
 [node name="AudioPlayer" type="Button" parent="MarginContainer/VBoxContainer/Result"]
-visible = false
 layout_mode = 2
 layout_mode = 2
 theme_override_font_sizes/font_size = 24
 theme_override_font_sizes/font_size = 24
 text = "Play Audio"
 text = "Play Audio"
@@ -91,6 +90,12 @@ text = "Play Audio"
 [node name="AudioStreamPlayer" type="AudioStreamPlayer" parent="MarginContainer/VBoxContainer/Result/AudioPlayer"]
 [node name="AudioStreamPlayer" type="AudioStreamPlayer" parent="MarginContainer/VBoxContainer/Result/AudioPlayer"]
 volume_db = -10.0
 volume_db = -10.0
 
 
+[node name="Information" type="Label" parent="MarginContainer/VBoxContainer/Result/AudioPlayer"]
+layout_mode = 0
+offset_top = 48.0
+offset_right = 40.0
+offset_bottom = 71.0
+
 [node name="SceneViewer" type="SubViewportContainer" parent="MarginContainer/VBoxContainer/Result"]
 [node name="SceneViewer" type="SubViewportContainer" parent="MarginContainer/VBoxContainer/Result"]
 visible = false
 visible = false
 custom_minimum_size = Vector2(1050, 400)
 custom_minimum_size = Vector2(1050, 400)
@@ -100,6 +105,7 @@ stretch = true
 [node name="SubViewport" type="SubViewport" parent="MarginContainer/VBoxContainer/Result/SceneViewer"]
 [node name="SubViewport" type="SubViewport" parent="MarginContainer/VBoxContainer/Result/SceneViewer"]
 handle_input_locally = false
 handle_input_locally = false
 msaa_3d = 2
 msaa_3d = 2
+scaling_3d_scale = 2.0
 size = Vector2i(1050, 400)
 size = Vector2i(1050, 400)
 render_target_update_mode = 0
 render_target_update_mode = 0