├── addons ├── MacroAccess │ ├── macroaccess.gdshaderinc │ ├── plugin.cfg │ ├── macroaccess.tscn │ ├── plugin.gd │ └── macroaccess.gd └── VertexHandles │ ├── plugin.cfg │ ├── plugin.gd │ ├── VertexHandles.gd │ └── VertexHandlesGizmo.gd ├── .gitattributes ├── scripts ├── commands │ └── RestartEditor.gd ├── ShaderTextEdit.gd ├── xr │ ├── world_grab.gd │ └── XREye3D.gd ├── web │ └── WebInput.gd ├── audio │ ├── FFTTexture.gd │ └── RecordWAV.gd ├── Feedback.gd ├── ShaderTexture.gd └── hello_triangle.gd ├── .gitignore ├── .github └── FUNDING.yml ├── LICENSE └── README.md /addons/MacroAccess/macroaccess.gdshaderinc: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /addons/VertexHandles/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="VertexHandles" 4 | description="Allows mesh vertices to be moved via handles" 5 | author="celyk" 6 | version="0.0" 7 | script="plugin.gd" 8 | -------------------------------------------------------------------------------- /addons/MacroAccess/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="MacroAccess" 4 | description="This addons implements a workaround for the lack of preprocessor access from in script" 5 | author="celyk" 6 | version="0.0" 7 | script="plugin.gd" 8 | -------------------------------------------------------------------------------- /scripts/commands/RestartEditor.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | class_name RestartEditor extends EditorScript 3 | 4 | ## An EditorScript that restarts the editor 5 | 6 | func _run() -> void: 7 | EditorInterface.save_all_scenes() 8 | EditorInterface.restart_editor.call_deferred(true) 9 | -------------------------------------------------------------------------------- /addons/MacroAccess/macroaccess.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=3 uid="uid://gpcvj3eu10o7"] 2 | 3 | [ext_resource type="Script" uid="uid://ch66toxiry17c" path="res://godot-useful-stuff/addons/MacroAccess/macroaccess.gd" id="1_t5rj2"] 4 | 5 | [node name="macroaccess" type="Node"] 6 | script = ExtResource("1_t5rj2") 7 | -------------------------------------------------------------------------------- /addons/MacroAccess/plugin.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorPlugin 3 | 4 | 5 | func _enter_tree(): 6 | var plugin_dir : String = get_script().resource_path.path_join("..").simplify_path() 7 | add_autoload_singleton("MacroAccess", plugin_dir.path_join("macroaccess.tscn")) 8 | 9 | func _exit_tree(): 10 | remove_autoload_singleton("MacroAccess") 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Godot 4+ specific ignores 2 | .godot/ 3 | 4 | # Godot-specific ignores 5 | .import/ 6 | export.cfg 7 | export_credentials.cfg 8 | *.uid 9 | 10 | # Imported translations (automatically generated from CSV files) 11 | *.translation 12 | 13 | # Mono-specific ignores 14 | .mono/ 15 | data_*/ 16 | mono_crash.*.json 17 | 18 | *.DS_Store 19 | -------------------------------------------------------------------------------- /addons/VertexHandles/plugin.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorPlugin 3 | 4 | const MyCustomGizmoPlugin = preload("VertexHandlesGizmo.gd") 5 | 6 | var gizmo_plugin = MyCustomGizmoPlugin.new(self) 7 | 8 | func _forward_canvas_gui_input(event: InputEvent) -> bool: 9 | if event is InputEventMouseMotion: 10 | update_overlays() 11 | return true 12 | 13 | return false 14 | 15 | func _enter_tree() -> void: 16 | # Initialization of the plugin goes here. 17 | add_node_3d_gizmo_plugin(gizmo_plugin) 18 | 19 | 20 | func _exit_tree() -> void: 21 | # Clean-up of the plugin goes here. 22 | remove_node_3d_gizmo_plugin(gizmo_plugin) 23 | -------------------------------------------------------------------------------- /scripts/ShaderTextEdit.gd: -------------------------------------------------------------------------------- 1 | class_name ShaderTextEdit extends TextEdit 2 | 3 | ## A text box for editing a shader at runtime. 4 | ## [br][color=purple]Made by celyk[/color] 5 | ## @tutorial(celyk's repo): https://github.com/celyk/godot-useful-stuff 6 | 7 | ## The shader which you want to edit. 8 | @export var target_shader : Shader 9 | 10 | ## Sets the code of the shader to the current text. 11 | func recompile(): 12 | if target_shader: 13 | target_shader.code = text 14 | 15 | func _input(event): 16 | if event is InputEventKey: 17 | if event.alt_pressed and event.keycode == KEY_ENTER: 18 | if get_viewport().gui_get_focus_owner() == self: 19 | recompile() 20 | 21 | func _ready(): 22 | if target_shader: 23 | # set text to initial shader file 24 | text = target_shader.code 25 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: celyk 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 celyk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /addons/MacroAccess/macroaccess.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends Node 3 | 4 | var _macros := {} 5 | 6 | # Adds a macro to the shader include. Overrides any macros of the same name that come before the #include directive, but not after. 7 | func set_shader_macro(name : StringName, code : String = "") -> void: 8 | _macros[name] = code 9 | 10 | _update() 11 | 12 | ## Gets a previously set macro by set_shader_macro() 13 | func get_shader_macro(name : StringName) -> String: 14 | return _macros[name] 15 | 16 | func clear_shader_macros() -> void: 17 | _macros.clear() 18 | 19 | _update() 20 | 21 | ## Updates the ShaderInclude resource, which triggers all shaders that depend on it to recompile! 22 | func _update(): 23 | var plugin_dir : String = get_script().resource_path.path_join("..").simplify_path() 24 | var include_file : ShaderInclude = load(plugin_dir.path_join("macroaccess.gdshaderinc")) 25 | 26 | var new_code : String = "" 27 | for name in _macros.keys(): 28 | new_code += "#ifdef " + name + "\n" 29 | new_code += "#undef " + name + "\n" 30 | new_code += "#endif\n" 31 | 32 | new_code += "#define " + name + " " + _macros[name] + "\n\n" 33 | 34 | include_file.code = new_code 35 | print(include_file.code) 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # godot-useful-stuff 2 | 3 | Here I will accumulate my most helpful scripts, addons, shaders, and examples for Godot 4. I try to maintain a degree of quality. 4 | 5 | Rendering is my primary interest, so many things here will revolve around that. 6 | 7 | To download a folder you can use https://download-directory.github.io/ 8 | 9 | # Hello, Triangle! 10 | An example for doing basic graphics using the low level RenderingDevice 11 | 12 | ![HelloTriangle](https://github.com/user-attachments/assets/04e94828-aea1-4a9d-bc41-d949aea83fcb) 13 | 14 | # Feedback 15 | A custom node for creating texture feedback effects with ease 16 | 17 | https://github.com/user-attachments/assets/afd33554-0e91-4315-8c8c-71518e276eee 18 | 19 | # XREye3D 20 | A node I developed to make it possible to detach each view from the XRCamera3D. It works by rendering each view to a seperate SubViewport. I took great care to ensure the projection is correct for Quest headsets 21 | 22 | https://github.com/user-attachments/assets/2dafe7dc-febb-4110-b0e7-7827c6011b62 23 | 24 | # VertexHandles 25 | An experiment to allow vertices of a mesh to be grabbed inside the editor 26 | 27 | https://github.com/user-attachments/assets/c509b3be-b005-4c79-ba3b-94d36a6e3033 28 | 29 | # MacroAccess 30 | This addon is a workaround for the lack of preprocessor access from in script. You have to use the addon's shader include to access the macros 31 | 32 | https://github.com/user-attachments/assets/cedca5ab-d958-4f09-9a4c-dd549c0e494f 33 | 34 | -------------------------------------------------------------------------------- /addons/VertexHandles/VertexHandles.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | class_name VertexHandles extends Node3D 3 | 4 | ## Add a new [VertexHandles] as a child of [MeshInstance3D] to modify it's mesh 5 | ## [br][color=purple]Made by celyk[/color] 6 | 7 | 8 | # PUBLIC 9 | 10 | @export var wireframe := true : 11 | set(value): 12 | wireframe = value 13 | _request_redraw.emit() 14 | @export var wireframe_color := Color.CORNFLOWER_BLUE : 15 | set(value): 16 | wireframe_color = value 17 | _request_redraw.emit() 18 | 19 | # Array of point arrays 20 | @export var point_arrays := [] : set = _set_points 21 | 22 | # The target mesh whose vertices we want to modify 23 | @onready var mesh = get_parent().mesh 24 | 25 | 26 | # PRIVATE 27 | 28 | signal _request_redraw 29 | 30 | # TODO 31 | # - Add options 32 | # - Support double vertices 33 | # - Support multiple surfaces 34 | # - Fix handle rendering bug 35 | # - Sort handles from back to front 36 | 37 | func _ready() -> void: 38 | assert(get_parent() is MeshInstance3D) 39 | 40 | _refresh_point_arrays() 41 | 42 | # Refresh the points just in case we are dealing with a new mesh 43 | EditorInterface.get_selection().selection_changed.connect(_refresh_point_arrays) 44 | 45 | func _refresh_point_arrays(): 46 | if not self in EditorInterface.get_selection().get_selected_nodes(): 47 | return 48 | 49 | get_parent().mesh = _to_array_mesh(get_parent().mesh) 50 | mesh = get_parent().mesh 51 | 52 | point_arrays = [] 53 | 54 | for i in range(0,mesh.get_surface_count()): 55 | var arrays = mesh.surface_get_arrays(i) 56 | 57 | point_arrays.push_back(arrays[Mesh.ARRAY_VERTEX]) 58 | 59 | func _update_mesh(): 60 | if is_node_ready() and not point_arrays.is_empty(): 61 | mesh = get_parent().mesh 62 | 63 | var surface_arrays := [] 64 | for i in range(0,mesh.get_surface_count()): 65 | var arrays = mesh.surface_get_arrays(i) 66 | surface_arrays.push_back(arrays) 67 | 68 | # Cache the primitive type 69 | var type : Mesh.PrimitiveType = _get_primitive_type(mesh) 70 | 71 | mesh.clear_surfaces() 72 | 73 | for i in range(0,surface_arrays.size()): 74 | surface_arrays[i][Mesh.ARRAY_VERTEX] = PackedVector3Array( point_arrays[i] ) 75 | 76 | mesh.add_surface_from_arrays(type, surface_arrays[i]) 77 | 78 | _request_redraw.emit() 79 | 80 | func _set_points(value): 81 | point_arrays = value 82 | _update_mesh() 83 | 84 | func set_point(i:int, point_idx:int, p:Vector3): 85 | point_arrays[i][point_idx] = p 86 | _update_mesh() 87 | 88 | func _to_array_mesh(_mesh:Mesh) -> ArrayMesh: 89 | var surface_arrays := [] 90 | for i in range(0,_mesh.get_surface_count()): 91 | var arrays = _mesh.surface_get_arrays(i) 92 | surface_arrays.push_back(arrays) 93 | 94 | # Cache the primitive type 95 | var type : Mesh.PrimitiveType = _get_primitive_type(_mesh) 96 | 97 | _mesh = ArrayMesh.new() 98 | 99 | for i in range(0,surface_arrays.size()): 100 | _mesh.add_surface_from_arrays(type, surface_arrays[i]) 101 | 102 | return _mesh 103 | 104 | func _get_primitive_type(_mesh:Mesh, id:=0) -> Mesh.PrimitiveType: 105 | return RenderingServer.mesh_get_surface(_mesh.get_rid(), 0)["primitive"] 106 | -------------------------------------------------------------------------------- /scripts/xr/world_grab.gd: -------------------------------------------------------------------------------- 1 | class_name WorldGrab extends RefCounted 2 | 3 | ## The WorldGrab utility makes it easy to add world-grab navigation to your XR project! 4 | ## [br][color=purple]Made by celyk[/color] 5 | ## 6 | ## Right now, WorldGrab is designed for a single use case: art viewing. 7 | ## [br]It allows one to grab the world with both hands and move it around, viewing it from all angles; It is not restricted by any up direction. 8 | ## [br] 9 | ## [br]Example usage: 10 | ## [codeblock] 11 | ## wg = WorldGrab.new() 12 | ## soon() 13 | ## [/codeblock] 14 | ## @tutorial(celyk's repo): https://github.com/celyk/godot-useful-stuff 15 | ## @tutorial(xr-grid): https://github.com/V-Sekai/V-Sekai.xr-grid 16 | 17 | ## The transform that takes one to the other. Intended for a one handed grab. 18 | func get_grab_transform(from : Transform3D, to : Transform3D) -> Transform3D: 19 | return to * from.affine_inverse() 20 | 21 | ## For orbitting around a central point, without scale, like spinning a globe. 22 | func get_orbit_transform(from_pivot : Vector3, from_b : Vector3, to_pivot : Vector3, to_b : Vector3) -> Transform3D: 23 | # Center the pivot 24 | from_b -= from_pivot 25 | to_b -= to_pivot 26 | 27 | # Gather information on the shortest rotation 28 | var axis : Vector3 = from_b.cross(to_b) 29 | if axis == Vector3(): axis = Vector3.RIGHT 30 | var angle : float = from_b.angle_to(to_b) 31 | 32 | # Construct the transformation that orbits about the pivot, with no scale! 33 | return Transform3D().translated(-from_pivot).rotated(axis.normalized(), angle).translated(to_pivot) 34 | 35 | ## This is a transformation which takes line (from_a,from_b) to line (to_a,to_b). It is analagous to pinch gesture on a touch screen. 36 | func get_pinch_transform(from_a : Vector3, from_b : Vector3, to_a : Vector3, to_b : Vector3) -> Transform3D: 37 | var delta_scale : float = sqrt((to_b-to_a).length_squared() / (from_b-from_a).length_squared()) 38 | 39 | # Orbit around pivot point a, and scale so that b is fixed in place. 40 | # According to symmetry, it is the same as if a and b are swapped. 41 | return get_orbit_transform(from_a, from_b, to_a, to_b).translated(-to_a).scaled(Vector3.ONE * delta_scale).translated(to_a) 42 | 43 | ## Separable blending of position, rotation and scale. Fine tune smoothing for maximum comfort. 44 | func split_blend( 45 | from : Transform3D, 46 | to : Transform3D, 47 | pos_weight : float = 0.0, 48 | rot_weight : float = 0.0, 49 | scale_weight : float = 0.0, 50 | from_pivot : Vector3 = Vector3(), 51 | to_pivot : Vector3 = Vector3()) -> Transform3D: 52 | 53 | var src_scale : Vector3 = from.basis.get_scale() 54 | var src_rot : Quaternion = from.basis.get_rotation_quaternion() 55 | 56 | var dst_scale : Vector3 = to.basis.get_scale() 57 | var dst_rot : Quaternion = to.basis.get_rotation_quaternion() 58 | 59 | var basis_inv : Basis = from.basis.inverse() 60 | from.basis = Basis(src_rot.slerp(dst_rot, rot_weight).normalized()) * Basis.from_scale(src_scale.lerp(dst_scale, scale_weight)) 61 | 62 | #from.origin -= from_pivot 63 | #to.origin -= to_pivot 64 | from.origin = from.origin.lerp(to.origin, pos_weight) 65 | #from.origin = from_pivot.lerp(to_pivot, pos_weight) + from.origin.slerp(to.origin, pos_weight) 66 | 67 | #from.origin -= from_pivot 68 | #from.origin = from.basis * (basis_inv * from.origin) 69 | #from.origin += from_pivot.lerp(to_pivot, pos_weight) 70 | 71 | return from 72 | -------------------------------------------------------------------------------- /addons/VertexHandles/VertexHandlesGizmo.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorNode3DGizmoPlugin 3 | 4 | var editor_plugin : EditorPlugin 5 | 6 | func _init(_editor_plugin:EditorPlugin): 7 | editor_plugin = _editor_plugin 8 | 9 | create_material("main", Color(1,1,1), false, true, true) 10 | create_handle_material("handles",false) 11 | 12 | const MyCustomNode3D = preload("VertexHandles.gd") 13 | func _has_gizmo(node): 14 | return node is MyCustomNode3D 15 | 16 | func _create_gizmo(for_node_3d: Node3D) -> EditorNode3DGizmo: 17 | if not _has_gizmo(for_node_3d): 18 | return null 19 | 20 | var gizmo = EditorNode3DGizmo.new() 21 | 22 | # Allows the node3d associated with this gizmo to request redraw 23 | for_node_3d._request_redraw.connect(_redraw.bind(gizmo)) 24 | 25 | return gizmo 26 | 27 | # show gizmo name in visibility list 28 | func _get_gizmo_name(): 29 | return "VertexHandlesGizmo" 30 | 31 | func _get_handle_name(gizmo,id,secondary): 32 | return str(id) 33 | 34 | func _get_handle_value(gizmo,id,secondary): 35 | var node3d : Node3D = gizmo.get_node_3d() 36 | return node3d.point_arrays[0][id] 37 | 38 | func _set_handle(gizmo,id,secondary,camera,point): 39 | var node3d : Node3D = gizmo.get_node_3d() 40 | 41 | # Construct the view ray in world space 42 | var ray_from : Vector3 = camera.project_ray_origin(point) 43 | var ray_dir : Vector3 = camera.project_ray_normal(point) 44 | 45 | # Intersect the ray with a camera facing plane 46 | var plane = Plane(camera.get_camera_transform().basis[2], node3d.global_transform * node3d.point_arrays[0][id]) 47 | var p = Geometry3D.segment_intersects_convex(ray_from,ray_from+ray_dir*16384,[plane]) 48 | 49 | if p.is_empty(): 50 | return 51 | 52 | p = p[0] 53 | 54 | # Transform the intersection point from world space to local node space 55 | p = node3d.global_transform.affine_inverse() * p 56 | 57 | node3d.set_point(0, id, p) 58 | 59 | _redraw(gizmo) 60 | 61 | func _commit_handle(gizmo,id,secondary,restore,cancel): 62 | var node3d : Node3D = gizmo.get_node_3d() 63 | 64 | var undo : EditorUndoRedoManager = editor_plugin.get_undo_redo() 65 | 66 | # Allows user to undo 67 | undo.create_action("Move handle " + str(id)) 68 | undo.add_do_method(node3d, "set_point", 0, id, node3d.point_arrays[0][id]) 69 | undo.add_undo_method(node3d, "set_point", 0, id, restore) 70 | undo.commit_action(false) 71 | 72 | func _redraw(gizmo): 73 | gizmo.clear() 74 | 75 | var node3d : Node3D = gizmo.get_node_3d() 76 | 77 | if node3d.wireframe: 78 | var lines = PackedVector3Array() 79 | 80 | var mdt := MeshDataTool.new() 81 | 82 | for surface_id in range(0,(node3d.mesh as Mesh).get_surface_count()): 83 | mdt.create_from_surface(node3d.mesh, surface_id) 84 | for face_id in range(0,mdt.get_face_count()): 85 | for j in range(0,3): 86 | lines.push_back( mdt.get_vertex(mdt.get_face_vertex(face_id,j)) ) 87 | lines.push_back( mdt.get_vertex(mdt.get_face_vertex(face_id,(j+1)%3 )) ) 88 | 89 | gizmo.add_lines(lines, get_material("main", gizmo), false, node3d.wireframe_color) 90 | 91 | 92 | var handles := PackedVector3Array() 93 | 94 | for i in range(0,node3d.point_arrays.size()): 95 | var point_array = node3d.point_arrays[i] 96 | 97 | for j in range(0,point_array.size()): 98 | handles.push_back( point_array[j] ) 99 | #print(arrays[Mesh.ARRAY_VERTEX][j]) 100 | 101 | gizmo.add_handles(handles, get_material("handles", gizmo), [], false) 102 | 103 | 104 | #gizmo.set_hidden(not gizmo.is_subgizmo_selected(0)) 105 | -------------------------------------------------------------------------------- /scripts/web/WebInput.gd: -------------------------------------------------------------------------------- 1 | class_name WebInput 2 | extends Node 3 | 4 | ## A singleton for accessing device sensor APIs from the web 5 | ## [br][color=purple]Made by celyk[/color] 6 | ## @tutorial(celyk's repo): https://github.com/celyk/godot-useful-stuff 7 | 8 | 9 | # TODO 10 | # - Test different browsers 11 | 12 | ## Requests and initializes the sensors. Required by iOS to prompt user for permission to access sensors 13 | static func request_sensors() -> void: 14 | if !OS.has_feature("web"): return 15 | 16 | if _init_sensors() != OK: 17 | return 18 | 19 | _is_initialized = true 20 | 21 | static func get_rotation() -> Vector3: 22 | if not _is_initialized: return Vector3() 23 | 24 | var v := _get_js_vector("rotation") 25 | 26 | # deg_to_rad() 27 | v *= TAU / 360.0 28 | 29 | return v 30 | 31 | static func get_accelerometer() -> Vector3: 32 | if not _is_initialized: return Input.get_accelerometer() 33 | return -_browser_to_godot_coordinates(_get_js_vector("acceleration")) 34 | 35 | static func get_gravity() -> Vector3: 36 | if not _is_initialized: return Input.get_gravity() 37 | return -_browser_to_godot_coordinates(_get_js_vector("gravity")) 38 | 39 | static func get_gyroscope() -> Vector3: 40 | if not _is_initialized: return Input.get_gyroscope() 41 | 42 | var v := _get_js_vector("gyroscope") 43 | 44 | # deg_to_rad() 45 | v *= TAU / 360.0 46 | 47 | # Reorient the vector to support all the browsers... 48 | v = _browser_to_godot_coordinates(v) 49 | 50 | return v 51 | 52 | static func _browser_to_godot_coordinates(v : Vector3) -> Vector3: 53 | #if OS.has_feature("web_ios") || true: 54 | # v = Vector3(-v.x, v.z, v.y) 55 | 56 | var orientation := _screen_get_orientation() 57 | v = _reorient_sensor_vector(v, orientation) 58 | 59 | return v 60 | 61 | static func _reorient_sensor_vector(v : Vector3, i : DisplayServer.ScreenOrientation = 0) -> Vector3: 62 | match i: 63 | DisplayServer.SCREEN_LANDSCAPE: 64 | v = Vector3(-v.y, v.x, v.z) 65 | DisplayServer.SCREEN_PORTRAIT: 66 | v = Vector3(v.x, v.y, v.z) # Portrait is the default orientation, even on iPad 67 | DisplayServer.SCREEN_REVERSE_LANDSCAPE: 68 | v = Vector3(v.y, -v.x, v.z) 69 | DisplayServer.SCREEN_REVERSE_PORTRAIT: 70 | v = Vector3(-v.x, -v.y, v.z) 71 | 72 | return v 73 | 74 | static var _cached_orientation := "" 75 | static func _screen_get_orientation() -> DisplayServer.ScreenOrientation: 76 | if not _is_initialized: return DisplayServer.screen_get_orientation() 77 | 78 | match _cached_orientation: 79 | "portrait-primary": 80 | return DisplayServer.SCREEN_PORTRAIT 81 | "portrait-secondary": 82 | return DisplayServer.SCREEN_REVERSE_PORTRAIT 83 | "landscape-primary": 84 | return DisplayServer.SCREEN_LANDSCAPE 85 | "landscape-secondary": 86 | return DisplayServer.SCREEN_REVERSE_LANDSCAPE 87 | 88 | return DisplayServer.SCREEN_PORTRAIT 89 | 90 | static var _cached_js_objects := {} 91 | static func _get_js_vector(name:String) -> Vector3: 92 | if _cached_js_objects.get(name) == null: 93 | _cached_js_objects[name] = JavaScriptBridge.get_interface(name) 94 | 95 | var js_object : JavaScriptObject = _cached_js_objects[name] 96 | return Vector3(js_object.x, js_object.y, js_object.z) 97 | 98 | static var _is_initialized := false 99 | static var _js_callback : JavaScriptObject 100 | static func _init_sensors() -> Error: 101 | if !OS.has_feature("web"): return ERR_UNAVAILABLE 102 | 103 | print("Initializing sensors") 104 | JavaScriptBridge.eval(_js_code, true) 105 | 106 | _cached_orientation = JavaScriptBridge.eval("screen_orientation", true) 107 | 108 | var js_screen : JavaScriptObject = JavaScriptBridge.get_interface("screen") 109 | 110 | _js_callback = JavaScriptBridge.create_callback(_on_orientation_changed) 111 | js_screen.orientation.onchange = _js_callback 112 | 113 | return OK 114 | 115 | static func _on_orientation_changed(args:Array): 116 | _cached_orientation = args[0].target.type 117 | 118 | const _js_code := ''' 119 | var rotation = { x: 0, y: 0, z: 0 }; 120 | var acceleration = { x: 0, y: 0, z: 0 }; 121 | var gravity = { x: 0, y: 0, z: 0 }; 122 | var gyroscope = { x: 0, y: 0, z: 0 }; 123 | var screen_orientation = "" 124 | 125 | // Not supported by the web 126 | //var magnetometer = { x: 0, y: 0, z: 0 }; 127 | 128 | // https://stackoverflow.com/a/9039885 129 | function is_platform_iOS() { 130 | return [ 131 | 'iPad Simulator', 132 | 'iPhone Simulator', 133 | 'iPod Simulator', 134 | 'iPad', 135 | 'iPhone', 136 | 'iPod' 137 | ].includes(navigator.platform) 138 | // iPad on iOS 13 detection 139 | || (navigator.userAgent.includes("Mac") && "ontouchend" in document) 140 | } 141 | 142 | function registerMotionListener() { 143 | window.ondevicemotion = function(event) { 144 | if (event.acceleration.x === null) return; 145 | 146 | acceleration.x = event.accelerationIncludingGravity.x; 147 | acceleration.y = event.accelerationIncludingGravity.y; 148 | acceleration.z = event.accelerationIncludingGravity.z; 149 | 150 | // Have to ammend iOS because it doesn't conform to the specification... 151 | if (is_platform_iOS()) { 152 | acceleration.x = -acceleration.x; 153 | acceleration.y = -acceleration.y; 154 | acceleration.z = -acceleration.z; 155 | } 156 | 157 | gravity.x = acceleration.x - event.acceleration.x; 158 | gravity.y = acceleration.y - event.acceleration.y; 159 | gravity.z = acceleration.z - event.acceleration.z; 160 | 161 | gyroscope.x = event.rotationRate.alpha; 162 | gyroscope.y = event.rotationRate.beta; 163 | gyroscope.z = event.rotationRate.gamma; 164 | } 165 | 166 | window.ondeviceorientation = function(event) { 167 | rotation.x = event.beta; 168 | rotation.y = event.gamma; 169 | rotation.z = event.alpha; 170 | } 171 | } 172 | 173 | // Request permission for iOS 13+ devices 174 | console.log("Requesting sensors"); 175 | function onClick() { 176 | screen_orientation = screen.orientation.type; 177 | 178 | // feature detect 179 | if (typeof DeviceMotionEvent.requestPermission === 'function') { 180 | DeviceMotionEvent.requestPermission() 181 | .then(permissionState => { 182 | if (permissionState === 'granted') { 183 | //window.addEventListener('devicemotion', () => {}); 184 | registerMotionListener(); 185 | } 186 | }) 187 | .catch(console.error); 188 | } else { 189 | // handle regular non iOS 13+ devices 190 | registerMotionListener(); 191 | } 192 | } 193 | 194 | onClick(); 195 | //window.addEventListener("click", onClick); 196 | ''' 197 | -------------------------------------------------------------------------------- /scripts/audio/FFTTexture.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | class_name FFTTexture extends ImageTexture 3 | 4 | @export var fft_size := 2048 5 | @export var sample_rate := 44100 6 | @export var mix_rate := 44100 7 | 8 | func _init() -> void: 9 | AudioServer.bus_layout_changed.connect(_validate_audio_state) 10 | start_record.call_deferred() 11 | 12 | ## Start the recording 13 | func start_record(): 14 | if _initialize_bus() != OK: 15 | push_error("RecordWAV: ", "Failed to initialize bus") 16 | return 17 | 18 | if _initialize_microphone() != OK: 19 | push_error("RecordWAV: ", "Failed to initialize microphone") 20 | _cleanup_microphone() 21 | return 22 | 23 | if _initialize_meter() != OK: 24 | push_error("RecordWAV: ", "Failed to initialize volume meter") 25 | _cleanup_microphone() 26 | return 27 | 28 | ## Stop the recording 29 | func stop_record(): 30 | # Prevent the microphone from hurting our ears 31 | _cleanup_microphone() 32 | 33 | emit_changed() 34 | 35 | # PRIVATE 36 | 37 | func _initialize_bus() -> Error: 38 | var bus_idx := AudioServer.get_bus_index("Record") 39 | if bus_idx != -1: 40 | return OK 41 | 42 | AudioServer.add_bus() 43 | 44 | bus_idx = AudioServer.bus_count - 1 45 | AudioServer.set_bus_name(bus_idx, "Record") 46 | AudioServer.set_bus_mute(bus_idx, true) 47 | 48 | #AudioServer.add_bus_effect(bus_idx, AudioEffectCapture.new()) 49 | #AudioServer.add_bus_effect(bus_idx, _effect_record) 50 | 51 | # Refresh the bus layout 52 | AudioServer.bus_layout_changed.emit() 53 | AudioServer.bus_renamed.emit(bus_idx, "New Bus", "Record") 54 | 55 | push_warning("RecordWAV: ", "A bus for recording has been added to the audio bus layout") 56 | 57 | return OK 58 | 59 | static var _microphone_stream_player : AudioStreamPlayer 60 | const _microphone_node_name := "AudioStreamPlayerMicrophone" 61 | func _initialize_microphone() -> Error: 62 | var tree := Engine.get_main_loop() as SceneTree 63 | 64 | # Check to see if the AudioStreamPlayer already exists somewhere 65 | if _microphone_stream_player == null: 66 | _microphone_stream_player = tree.root.find_child(_microphone_node_name, false) 67 | 68 | # If it exists, we're good to go 69 | if _microphone_stream_player and _microphone_stream_player.is_inside_tree(): 70 | return OK 71 | 72 | _microphone_stream_player = AudioStreamPlayer.new() 73 | _microphone_stream_player.stream = AudioStreamMicrophone.new() 74 | _microphone_stream_player.bus = "Record" 75 | _microphone_stream_player.name = _microphone_node_name 76 | 77 | tree.root.add_child(_microphone_stream_player) 78 | 79 | # Playback must start inside tree 80 | _microphone_stream_player.playing = true 81 | 82 | return OK 83 | 84 | func _cleanup_microphone(): 85 | # Another chance to find the node somewhere 86 | if _microphone_stream_player == null: 87 | var tree := Engine.get_main_loop() as SceneTree 88 | _microphone_stream_player = tree.root.find_child(_microphone_node_name, false) 89 | 90 | # Free the AudioStreamPlayer 91 | if _microphone_stream_player and !_microphone_stream_player.is_queued_for_deletion(): 92 | _microphone_stream_player.queue_free() 93 | 94 | # FFT for tracking peaks in volume 95 | var _spectrum_analyzer_instance : AudioEffectSpectrumAnalyzerInstance 96 | var _start_t_msec : int = 0 97 | func _initialize_meter() -> Error: 98 | var bus_idx := AudioServer.get_bus_index("Record") 99 | if bus_idx == -1: 100 | return ERR_UNCONFIGURED 101 | 102 | # Reset these before recording 103 | _start_t_msec = Time.get_ticks_msec() 104 | _max_volume_t_msec = Time.get_ticks_msec() 105 | _max_volume = Vector2(0,0) 106 | 107 | # Start processing the FFT 108 | var tree := Engine.get_main_loop() as SceneTree 109 | tree.process_frame.connect(_record_process) 110 | 111 | # Check to see if the effect already exists 112 | var effects := _get_audio_effects(bus_idx, ["AudioEffectSpectrumAnalyzer"]) 113 | 114 | if not effects.is_empty(): 115 | var effect : AudioEffectSpectrumAnalyzer = effects.back() 116 | effect.fft_size = AudioEffectSpectrumAnalyzer.FFT_SIZE_2048 117 | 118 | # Search for the effect to get it's index 119 | var effect_idx : int = _get_audio_effects(bus_idx).rfind(effect) 120 | 121 | _spectrum_analyzer_instance = AudioServer.get_bus_effect_instance(bus_idx, effect_idx) 122 | else: 123 | # No effect found. Initialize it 124 | var _spectrum_analyzer = AudioEffectSpectrumAnalyzer.new() 125 | _spectrum_analyzer.fft_size = AudioEffectSpectrumAnalyzer.FFT_SIZE_2048 126 | 127 | AudioServer.add_bus_effect(bus_idx, _spectrum_analyzer) 128 | 129 | _spectrum_analyzer_instance = AudioServer.get_bus_effect_instance(bus_idx, AudioServer.get_bus_effect_count(bus_idx)-1) 130 | 131 | # Refresh the bus layout 132 | AudioServer.bus_layout_changed.emit() 133 | 134 | return OK 135 | 136 | var _max_volume : Vector2 137 | var _max_volume_t_msec := 0 138 | var _fft : Array[Vector2] 139 | func _record_process(): 140 | var volume := _spectrum_analyzer_instance.get_magnitude_for_frequency_range(0.0, 20000, AudioEffectSpectrumAnalyzerInstance.MAGNITUDE_AVERAGE) 141 | #print(volume, _max_volume) 142 | 143 | #print(volume > 0.001) 144 | 145 | _fft.resize(fft_size / 2) 146 | 147 | var bin_size := sample_rate / _fft.size() 148 | 149 | for i in range(0, _fft.size()-1): 150 | var new_fft_value := 50 * _spectrum_analyzer_instance.get_magnitude_for_frequency_range(bin_size * i, bin_size * (i+1), AudioEffectSpectrumAnalyzerInstance.MAGNITUDE_MAX) 151 | 152 | if new_fft_value.x > _fft[i].x: 153 | _fft[i] = new_fft_value 154 | else: 155 | _fft[i] = lerp(_fft[i], new_fft_value, 0.2) # / float(_fft.size()) 156 | 157 | #_fft[i] = new_fft_value 158 | 159 | var average := (volume.x + volume.y) / 2.0 160 | if average > _max_volume.x: 161 | _max_volume.x = average 162 | _max_volume_t_msec = Time.get_ticks_msec() 163 | 164 | var img := Image.create_empty(fft_size, 2, false, Image.FORMAT_RGBAF) 165 | for i in range(0, _fft.size()): 166 | var magnitude := max(_fft[i].x, _fft[i].y) 167 | 168 | var v := Vector4(magnitude, 0, 0, 1) 169 | var col := Color(v.x, v.y, v.z, v.w) 170 | #col = Color.AQUA 171 | img.set_pixel(i, 0, col) 172 | img.set_pixel(i, 1, col) 173 | 174 | #print(_fft) 175 | #print(_spectrum_analyzer_instance) 176 | 177 | set_image(img) 178 | 179 | func _validate_audio_state(): 180 | var bus_idx := AudioServer.get_bus_index("Record") 181 | 182 | # Get's audio bus effect objects with an optional type filter 183 | func _get_audio_effects(bus_idx:int, filter:=[]) -> Array[AudioEffect]: 184 | var effects : Array[AudioEffect] 185 | 186 | for i in range(0, AudioServer.get_bus_effect_count(bus_idx)): 187 | var effect : AudioEffect = AudioServer.get_bus_effect(bus_idx, i) 188 | effects.append(effect) 189 | 190 | if filter.is_empty(): 191 | return effects 192 | 193 | var filtered_effects : Array[AudioEffect] 194 | for effect in effects: 195 | if effect.get_class() in filter: 196 | filtered_effects.append(effect) 197 | 198 | return filtered_effects 199 | -------------------------------------------------------------------------------- /scripts/xr/XREye3D.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | class_name XREye3D extends Camera3D 3 | 4 | ## Splits off individual views from an XRCamera. Like for pulling out your eyeball in VR 5 | ## Unsure if this will work yet without a custom pojection matrix 6 | ## Despite that, this Node may still be useful for rendering something custom to each eye 7 | 8 | ## The view index associated with this eye 9 | @export var index := 0 : 10 | set(value): 11 | index = value 12 | _setup_blit() 13 | 14 | ## Set an external Viewport in case you want access to it 15 | @export var external_viewport : Viewport : 16 | set(value): 17 | # Reinitialize internal viewport if this value is being nulled 18 | if value == null: #and internal_viewport == external_viewport: 19 | external_viewport = null 20 | _setup_internal_viewport() 21 | _setup_blit() 22 | return 23 | 24 | # Set the value 25 | external_viewport = value 26 | 27 | # The internal viewport is no longer needed 28 | if internal_viewport: 29 | remove_child(internal_viewport) 30 | 31 | # Now it is a proxy for the external viewport 32 | internal_viewport = external_viewport 33 | 34 | # Update the viewport with the latest info 35 | _setup_blit() 36 | 37 | ## The viewport that is used internally to render the view. Equals to external viewport when specified 38 | var internal_viewport : Viewport 39 | 40 | ## A quad for copying the rendered view into the main XR viewport 41 | var blit_quad : MeshInstance3D 42 | 43 | 44 | # TODO 45 | # - Support an external viewport 46 | # - Support an external XRCamera3D (one that is not parent) 47 | # - Render everything to left eye for recording videos 48 | # - Add a warning if we get a bad projection matrix 49 | 50 | 51 | # PRIVATE 52 | 53 | # Warns user if the node is setup incorrectly 54 | func _get_configuration_warnings() -> PackedStringArray: 55 | var warnings : PackedStringArray 56 | 57 | if not (get_parent() is XRCamera3D): 58 | warnings.append("XREye3D must be child of XRCamera3D") 59 | 60 | return warnings 61 | 62 | # Akin to set_perspective(), but the space is sheared around the camera position. 63 | func _set_perspective_and_shear(camera:Camera3D, fov:float, z_near:float, z_far:float, shear:Vector2): 64 | camera.set_frustum(2.0 * tan(deg_to_rad(fov/2.0)) * z_near, shear * z_near, z_near, z_far) 65 | 66 | func _ready() -> void: 67 | if Engine.is_editor_hint(): return 68 | if not (get_parent() is XRCamera3D): return 69 | 70 | if external_viewport: 71 | internal_viewport = external_viewport 72 | else: 73 | _setup_internal_viewport() 74 | 75 | _setup_blit() 76 | 77 | func _process(delta: float) -> void: 78 | RenderingServer.call_on_render_thread(_setup_projection) 79 | 80 | func _setup_projection() -> void: 81 | if Engine.is_editor_hint(): return 82 | if not (get_parent() is XRCamera3D): return 83 | 84 | var screen_resolution = XRServer.primary_interface.get_render_target_size() 85 | var screen_aspect : float = float(screen_resolution.x) / screen_resolution.y 86 | 87 | # Set the eye transform relative to the head camera 88 | global_transform = XRServer.primary_interface.get_transform_for_view(index, XRServer.world_origin) 89 | 90 | var flip_y : bool = ProjectSettings.get("rendering/renderer/rendering_method") != "gl_compatibility" 91 | var projection_matrix : Projection = XRServer.primary_interface.get_projection_for_view(index, screen_aspect, get_parent().near, get_parent().far) 92 | 93 | fov = projection_matrix.get_fov() 94 | 95 | var actual_aspect = projection_matrix[1][1] / projection_matrix[0][0] 96 | fov = projection_matrix.get_fovy(fov, 1.0/actual_aspect) 97 | 98 | projection_matrix = Projection.create_depth_correction(flip_y) * projection_matrix 99 | 100 | #projection_matrix = XRServer.primary_interface.get_projection_for_view(index, screen_aspect, get_parent().near, get_parent().far) 101 | 102 | 103 | #fov = projection_matrix.get_far_plane_half_extents().y / projection_matrix.get_z_far() 104 | #fov = rad_to_deg(2.0 * atan(fov)) 105 | #fov = 100 106 | 107 | # Take out the non uniform aspect 108 | projection_matrix = Projection(Transform3D().scaled( 109 | Vector3(1.0/(actual_aspect/screen_aspect),1,1)) 110 | ) * projection_matrix 111 | 112 | internal_viewport.size.x = internal_viewport.size.y * actual_aspect 113 | 114 | #projection_matrix = Projection 115 | #print(XRServer.primary_interface.get_render_target_size()) 116 | 117 | # Match the projection the best we can. May not work for all XR devices without custom projection 118 | near = get_parent().near 119 | far = get_parent().far 120 | #fov = get_parent().fov 121 | 122 | #fov = rad_to_deg(2.0 * atan(fov)) 123 | #fov += Input.get_axis("ui_down","ui_up")*.1 124 | 125 | #fov = 98 126 | print("FOV: ", fov) 127 | 128 | #fov = 100 # This number works on Quest 2 129 | 130 | #fov = projection_matrix.get_fovy(projection_matrix.get_fov(), aspect) #get_parent().fov 131 | #fov = lerp(50.0, 130.0, 0.5+0.5*sin(Time.get_ticks_msec() / 1000.0)) 132 | 133 | var shear := Transform3D(projection_matrix).basis[2] 134 | shear.x /= projection_matrix[0][0] 135 | shear.y /= projection_matrix[1][1] 136 | shear.z = 1.0 137 | 138 | _set_perspective_and_shear(self, fov, near, far, Vector2(shear.x, shear.y)) 139 | blit_quad.material_override.set_shader_parameter("scale", Vector2(actual_aspect,1)) 140 | 141 | func _setup_internal_viewport() -> void: 142 | if internal_viewport: 143 | remove_child(internal_viewport) 144 | 145 | internal_viewport = SubViewport.new() 146 | internal_viewport.transparent_bg = get_viewport().transparent_bg 147 | 148 | add_child(internal_viewport) 149 | 150 | func _setup_blit() -> void: 151 | if internal_viewport == null: 152 | return 153 | 154 | RenderingServer.viewport_attach_camera(internal_viewport.get_viewport_rid(), get_camera_rid()) 155 | 156 | # Set resolution 157 | internal_viewport.size = XRServer.primary_interface.get_render_target_size() 158 | 159 | if blit_quad == null: 160 | blit_quad = MeshInstance3D.new() 161 | blit_quad.mesh = QuadMesh.new() 162 | blit_quad.mesh.size = Vector2(2,2) 163 | 164 | # No idea why this doesn't work 165 | #blit_quad.material_override = ShaderMaterial.new() 166 | #blit_quad.material_override.resource_local_to_scene = true 167 | #blit_quad.material_override.shader = Shader.new() 168 | #blit_quad.material_override.shader.code = _blit_shader_code 169 | 170 | var _material : Material = ShaderMaterial.new() 171 | _material.resource_local_to_scene = true 172 | _material.shader = Shader.new() 173 | _material.shader.code = _blit_shader_code 174 | blit_quad.material_override = _material 175 | 176 | add_child(blit_quad) 177 | 178 | # Update the uniforms 179 | blit_quad.material_override.set_shader_parameter("view", index) 180 | blit_quad.material_override.set_shader_parameter("view_tex", internal_viewport.get_texture()) 181 | 182 | 183 | const _blit_shader_code : String = " 184 | shader_type spatial; 185 | 186 | render_mode unshaded, cull_disabled; 187 | 188 | uniform int view = 0; 189 | uniform sampler2D view_tex : source_color; 190 | uniform vec2 scale = vec2(1.0); 191 | 192 | void vertex(){ 193 | // Cull everything 194 | POSITION = vec4(0,0,2,1); 195 | 196 | // Find a way to detect XRCamera so we can render only to that 197 | // Otherwise, undefined behavior could ensue 198 | bool xr_camera = EYE_OFFSET != vec3(0); 199 | 200 | // Workaround because VIEW_INDEX doesn't work on the OpenGL renderer 201 | int my_view_index = (EYE_OFFSET.x > 0.0) ? 1 : 0; 202 | 203 | // Render it if this is the desired view 204 | if (xr_camera && my_view_index == view){ 205 | POSITION = vec4(VERTEX.xy, 0.999999, 1.0); 206 | POSITION.xy *= scale; 207 | } 208 | } 209 | 210 | void fragment(){ 211 | vec2 uv = SCREEN_UV; 212 | 213 | // Unsure why this is needed for it to work on the OpenGL renderer 214 | if(OUTPUT_IS_SRGB){ 215 | uv.y = 1.0 - uv.y; 216 | } 217 | 218 | if (uv.x > 0.5+0.5*sin(TIME*3.0)) discard; 219 | 220 | ALBEDO = texture(view_tex, uv).rgb; 221 | //ALPHA = 0.5; 222 | } 223 | " 224 | -------------------------------------------------------------------------------- /scripts/Feedback.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | class_name Feedback extends SubViewport 3 | 4 | ## [br]A node for creating texture feedback loops. 5 | ## [br][color=purple]Made by celyk[/color] 6 | ## @tutorial(celyk's repo): https://github.com/celyk/godot-useful-stuff 7 | ## 8 | ## Feedback is a [Viewport] that simply copies it's parent viewport, enabling safe access to the parent viewports previous frame. 9 | ## [br] 10 | ## [br]It automatically takes the size of the parent viewport. 11 | 12 | ## Make Feedback use 2D rendering. Can make 8-bit colors more consistent 13 | @export var only_2d := false 14 | 15 | # PRIVATE 16 | 17 | # TODO: 18 | # simplify rendering 19 | # cache shader 20 | # xr support 21 | 22 | var _parent_viewport : Viewport 23 | 24 | func _init(): 25 | size = size 26 | render_target_update_mode = SubViewport.UPDATE_ALWAYS 27 | 28 | func _notification(what): 29 | match what: 30 | NOTIFICATION_POST_ENTER_TREE: # the node entered the tree and is ready 31 | _init_blit() 32 | 33 | _safe_disconnect(_parent_viewport, "size_changed", _handle_resize) 34 | 35 | _parent_viewport = _find_parent_viewport() 36 | _handle_resize() 37 | 38 | 39 | # editor shenanigans 40 | if Engine.is_editor_hint() && has_method("_do_handle_editor") && call("_do_handle_editor"): 41 | return 42 | 43 | 44 | _safe_connect(_parent_viewport, "size_changed", _handle_resize) 45 | 46 | NOTIFICATION_PREDELETE: 47 | _cleanup_blit() 48 | _safe_disconnect(_parent_viewport, "size_changed", _handle_resize) 49 | 50 | func _find_parent_viewport(): 51 | return get_parent().get_viewport() # get_viewport() on a viewport returns itself 52 | 53 | func _handle_resize(): 54 | size = _parent_viewport.size 55 | _material.set_shader_parameter("tex", _parent_viewport.get_texture()) 56 | 57 | func _safe_connect(obj : Object, sig: StringName, callable : Callable, flags : int = 0) -> void: 58 | if obj && !obj.is_connected(sig, callable): obj.connect(sig, callable, flags) 59 | func _safe_disconnect(obj : Object, sig: StringName, callable : Callable) -> void: 60 | if obj && obj.is_connected(sig, callable): obj.disconnect(sig, callable) 61 | 62 | 63 | # RENDERING 64 | 65 | var _p_viewport : RID 66 | var _p_scenario : RID 67 | var _p_camera : RID 68 | var _p_base : RID 69 | var _p_instance : RID 70 | var _material : Material 71 | var _p_light_base : RID 72 | var _p_light_instance : RID 73 | 74 | func _init_blit() -> void: 75 | if _p_viewport.is_valid(): 76 | # fixes a bug when switching scene 77 | RenderingServer.viewport_set_scenario(_p_viewport, _p_scenario) 78 | RenderingServer.viewport_attach_camera(_p_viewport, _p_camera) 79 | return 80 | 81 | _p_scenario = RenderingServer.scenario_create() 82 | _p_viewport = get_viewport_rid() 83 | 84 | RenderingServer.viewport_set_scenario(_p_viewport, _p_scenario) 85 | 86 | # camera setup 87 | _p_camera = RenderingServer.camera_create(); 88 | RenderingServer.viewport_attach_camera(_p_viewport, _p_camera) 89 | var p_env = RenderingServer.environment_create() 90 | RenderingServer.camera_set_environment(_p_camera, p_env) 91 | RenderingServer.camera_set_transform(_p_camera, Transform3D(Basis(), Vector3(0, 0, 1))) 92 | RenderingServer.camera_set_orthogonal(_p_camera, 2.1, 0.1, 10) 93 | 94 | # quad setup 95 | _p_base = RenderingServer.mesh_create() 96 | _p_instance = RenderingServer.instance_create2(_p_base, _p_scenario) 97 | 98 | var quad_mesh = QuadMesh.new() 99 | quad_mesh.size = Vector2(2,2) 100 | var arr = quad_mesh.get_mesh_arrays() 101 | RenderingServer.mesh_add_surface_from_arrays(_p_base, RenderingServer.PRIMITIVE_TRIANGLES, arr) 102 | 103 | _material = ShaderMaterial.new() 104 | _material.resource_local_to_scene = true 105 | _material.shader = Shader.new() 106 | _material.shader.code = _blit_shader_code_spatial 107 | 108 | RenderingServer.mesh_surface_set_material(_p_base, 0, _material.get_rid()) 109 | 110 | # light setup 111 | _p_light_base = RenderingServer.directional_light_create() 112 | _p_light_instance = RenderingServer.instance_create2(_p_light_base, _p_scenario) 113 | 114 | func _cleanup_blit() -> void: 115 | RenderingServer.free_rid(_p_instance) 116 | RenderingServer.free_rid(_p_base) 117 | RenderingServer.free_rid(_p_camera) 118 | RenderingServer.free_rid(_p_scenario) 119 | RenderingServer.free_rid(_p_light_instance) 120 | RenderingServer.free_rid(_p_light_base) 121 | 122 | 123 | # DATA 124 | 125 | const _blit_shader_code_spatial = ''' 126 | shader_type spatial; 127 | 128 | render_mode cull_disabled, ambient_light_disabled, depth_draw_never; 129 | 130 | uniform sampler2D tex : source_color, filter_nearest; 131 | 132 | void vertex(){ 133 | POSITION = MODEL_MATRIX * vec4(VERTEX,1); 134 | UV.y = 1.0 - UV.y; 135 | } 136 | 137 | void fragment(){ 138 | FOG = vec4(0); 139 | } 140 | 141 | // This expects 0-1 range input, outside that range it behaves poorly. 142 | vec3 srgb_to_linear(vec3 color) { 143 | // Approximation from http://chilliant.blogspot.com/2012/08/srgb-approximations-for-hlsl.html 144 | return mix(pow((color.rgb + vec3(0.055)) * (1.0 / (1.0 + 0.055)), vec3(2.4)), color.rgb * (1.0 / 12.92), lessThan(color.rgb, vec3(0.04045))); 145 | } 146 | 147 | // Workaround for ALBEDO color error on the compatibility renderer 148 | void light(){ 149 | vec4 samp = textureLod(tex, UV, 0.0); 150 | 151 | if(OUTPUT_IS_SRGB){ 152 | SPECULAR_LIGHT = srgb_to_linear(samp.rgb); 153 | } 154 | else{ 155 | SPECULAR_LIGHT = samp.rgb; 156 | } 157 | 158 | ALPHA = samp.a; 159 | } 160 | ''' 161 | 162 | 163 | const _blit_shader_code_canvas_item = ''' 164 | shader_type canvas_item; 165 | 166 | render_mode blend_disabled; 167 | 168 | uniform sampler2D tex : source_color, filter_nearest; 169 | 170 | void vertex(){ 171 | POSITION = MODEL_MATRIX * vec4(VERTEX,1); 172 | UV.y = 1.0 - UV.y; 173 | } 174 | 175 | void fragment(){ 176 | vec4 samp = textureLod(tex, UV, 0.0); 177 | COLOR = samp; 178 | } 179 | ''' 180 | 181 | 182 | # JANK 183 | # you can safely delete this section 184 | 185 | var _editor_viewport : Control 186 | func _find_editor_viewport(node : Node) -> void: 187 | if node.get_class() == "CanvasItemEditorViewport": 188 | _safe_disconnect(_editor_viewport, "resized", _handle_2d_editor_resize) 189 | 190 | _parent_viewport = get_tree().get_root() 191 | _editor_viewport = node 192 | 193 | _handle_2d_editor_resize() 194 | 195 | _safe_connect( _editor_viewport, "resized", _handle_2d_editor_resize) 196 | 197 | var parent = node.get_parent() 198 | if parent && parent.get_class() == "Node3DEditorViewport": 199 | _safe_disconnect(_editor_viewport, "resized", _handle_2d_editor_resize) 200 | 201 | _parent_viewport = get_tree().get_root() 202 | _editor_viewport = parent 203 | 204 | _handle_2d_editor_resize() 205 | 206 | _safe_connect( _editor_viewport, "resized", _handle_2d_editor_resize) 207 | 208 | 209 | # texture space to world space transform 210 | func _rect_to_transform(rect : Rect2) -> Transform2D: 211 | return Transform2D(Vector2(rect.size.x,0), Vector2(0,rect.size.y), rect.position) 212 | 213 | func _handle_2d_editor_resize(): 214 | size = _editor_viewport.size 215 | _material.set_shader_parameter("tex", _parent_viewport.get_texture()) 216 | 217 | var transform := Transform2D() 218 | transform = transform.translated(Vector2(1,1)).scaled(Vector2(0.5,0.5)) 219 | transform = _rect_to_transform( _editor_viewport.get_global_rect() ) * transform 220 | transform = _editor_viewport.get_viewport_transform().scaled(Vector2(1,1)/Vector2(_parent_viewport.size)) * transform 221 | transform = transform.translated(-Vector2(0.5,0.5)).scaled(Vector2(2,2)) 222 | transform = transform.affine_inverse() 223 | 224 | RenderingServer.instance_set_transform(_p_instance, Transform3D(transform)) 225 | 226 | func _do_handle_editor() -> bool: 227 | # The issue is that our scene root is not used for rendering when inside the editor 228 | # We must find the actual viewport used 229 | _safe_disconnect( get_tree().get_root(), "gui_focus_changed", _find_editor_viewport) 230 | 231 | if _parent_viewport != get_tree().get_edited_scene_root().get_parent(): 232 | return false 233 | 234 | _safe_connect( get_tree().get_root(), "gui_focus_changed", _find_editor_viewport) 235 | 236 | return true 237 | 238 | func _exit_tree(): 239 | _safe_disconnect( get_tree().get_root(), "gui_focus_changed", _find_editor_viewport) 240 | _safe_disconnect(_editor_viewport, "resized", _handle_2d_editor_resize) 241 | -------------------------------------------------------------------------------- /scripts/ShaderTexture.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | class_name ShaderTexture extends ImageTexture 3 | 4 | ## A texture that takes a canvas_item shader to generate the output. Useful for caching procedural textures 5 | ## [br][color=purple]Made by celyk[/color] 6 | ## @tutorial(celyk's repo): https://github.com/celyk/godot-useful-stuff 7 | 8 | 9 | # UX design flaws: 10 | # - Expensive shaders can still run in the inspector preview and kill FPS 11 | # - Nested Texture2D is incredibly hard to navigate. Needs a node graph? 12 | 13 | 14 | #region Interface 15 | 16 | ## Click me to render the texture based on the current shader code 17 | @export_tool_button("Generate", "Callable") var generate_pulse = generate 18 | 19 | ## The size of the Texture2D to be generated 20 | @export var size := Vector2i(128,128) : 21 | set(value): 22 | size = value 23 | _update_size() 24 | 25 | ## Enable HDR to give more precision to each channel 26 | @export var hdr := false : 27 | set(value): 28 | hdr = value 29 | _update_hdr() 30 | 31 | ## Enable writing of the alpha channel 32 | @export var transparency := true : 33 | set(value): 34 | transparency = value 35 | _update_transparency() 36 | 37 | ## Enable mipmap generation 38 | @export var mipmaps := true : 39 | set(value): 40 | mipmaps = value 41 | _update_mipmaps() 42 | 43 | ## Enable mipmap generation [WIP]. For custom mipmap generation, use floor(UV.x) to check mip level in the fragment shader 44 | @export var custom_mipmaps := false : 45 | set(value): 46 | custom_mipmaps = value 47 | _update_custom_mipmaps() 48 | 49 | ## Set [size] to match the input texture. Helpful for processing textures 50 | @export var use_texture_size := false : 51 | set(value): 52 | use_texture_size = value 53 | _update_size() 54 | 55 | ## Rerender the shader once every frame [WIP] 56 | @export var live := false : 57 | set(value): 58 | live = value 59 | 60 | if not _initialized: return 61 | 62 | if live: 63 | RenderingServer.viewport_set_update_mode(_p_viewport, RenderingServer.VIEWPORT_UPDATE_ALWAYS) 64 | else: 65 | RenderingServer.viewport_set_update_mode(_p_viewport, RenderingServer.VIEWPORT_UPDATE_DISABLED) 66 | 67 | ## Automatically regenerate the Texture2D when the shader has changed. Otherwise it must be manually generated 68 | @export var regenerate_on_change := true : 69 | set(value): 70 | regenerate_on_change = value 71 | 72 | ## Save the current image to a file [WIP] 73 | @export_tool_button("Save", "Callable") var save_pulse = save_output 74 | ## Load the previously saved image into the input texture slot [WIP] 75 | @export_tool_button("Load last", "Callable") var load_last_pulse = load_last 76 | 77 | ## The primary texture slot. Maps to TEXTURE in canvas_item shaders 78 | @export var input_texture : Texture2D : 79 | set(value): 80 | input_texture = value 81 | _update_input() 82 | 83 | ## The ShaderMaterial with your custom shader that you want to be rendered 84 | @export var material : ShaderMaterial : 85 | set(value): 86 | material = value 87 | 88 | if material == null: return 89 | 90 | _update_material() 91 | 92 | #endregion 93 | 94 | #region Methods 95 | 96 | ## Render the texture based on the current shader code 97 | func generate() -> void: 98 | if not _initialized: return 99 | if live: return 100 | 101 | RenderingServer.viewport_set_update_mode(_p_viewport, RenderingServer.VIEWPORT_UPDATE_ONCE) 102 | 103 | # Wait for the frame to render 104 | await RenderingServer.frame_post_draw 105 | 106 | _blit_viewport() 107 | 108 | ## Call to prompt the user to save the texture as a png [WIP] 109 | func save_output() -> void: 110 | if input_texture == null: return 111 | 112 | var path := input_texture.resource_path.get_basename() 113 | 114 | if not input_texture.is_built_in(): 115 | var new_path = _find_unique_filename(path) 116 | get_image().save_png(new_path + ".png") 117 | EditorInterface.get_resource_filesystem().scan() 118 | 119 | # If the resource isn't associated with a specific file, open a file save dialog 120 | elif Engine.is_editor_hint(): 121 | var dialog := EditorFileDialog.new() 122 | dialog.file_mode = EditorFileDialog.FILE_MODE_SAVE_FILE 123 | dialog.filters = PackedStringArray(["*.png, *.jpg, *.jpeg ; Supported Images"]) 124 | 125 | dialog.file_selected.connect( 126 | func(_path: String): 127 | get_image().save_png(_path) 128 | EditorInterface.get_resource_filesystem().scan() 129 | ) 130 | 131 | var tree := Engine.get_main_loop() as SceneTree 132 | tree.root.add_child(dialog) 133 | 134 | dialog.popup_file_dialog() 135 | 136 | ## Call to load the previously saved texture into the input texture slot [WIP] 137 | func load_last() -> void: 138 | pass 139 | 140 | #endregion 141 | 142 | #region Initialization 143 | 144 | var _initialized := false 145 | func _init() -> void: 146 | _setup_viewport() 147 | _update_size() 148 | 149 | # Prevent generation on load 150 | (func(): _initialized = true).call_deferred() 151 | 152 | #endregion 153 | 154 | #region Update state 155 | 156 | func _update_material(): 157 | RenderingServer.canvas_item_set_material(_p_canvas_item, material.get_rid()) 158 | 159 | _safe_connect(material, "changed", _on_shader_changed) 160 | _safe_connect(material.shader, "changed", _on_shader_changed) 161 | 162 | generate() 163 | 164 | func _update_size(): 165 | var internal_size := _get_internal_size() 166 | RenderingServer.viewport_set_size(_p_viewport, internal_size.x, internal_size.y) 167 | 168 | _update_rect() 169 | 170 | generate() 171 | 172 | func _update_hdr(): 173 | RenderingServer.viewport_set_use_hdr_2d(_p_viewport, hdr) 174 | 175 | generate() 176 | 177 | func _update_transparency(): 178 | RenderingServer.viewport_set_transparent_background(_p_viewport, transparency) 179 | 180 | generate() 181 | 182 | func _update_mipmaps(): 183 | generate() 184 | 185 | func _update_custom_mipmaps(): 186 | generate() 187 | 188 | func _update_input(): 189 | _update_rect() 190 | 191 | func _on_shader_changed(): 192 | if regenerate_on_change: 193 | print("regenerate_on_change Time to change") 194 | generate() 195 | 196 | #endregion 197 | 198 | #region Rect setup 199 | 200 | func _update_rect(): 201 | RenderingServer.canvas_item_clear(_p_canvas_item) 202 | 203 | var internal_size := _get_internal_size() 204 | 205 | if input_texture != null: 206 | RenderingServer.canvas_item_add_texture_rect(_p_canvas_item, Rect2(Vector2(), internal_size), input_texture.get_rid()) 207 | else: 208 | RenderingServer.canvas_item_add_rect(_p_canvas_item, Rect2(Vector2(), internal_size), Color(1,1,1,1)) 209 | 210 | func _get_internal_size() -> Vector2i: 211 | if use_texture_size and input_texture != null: 212 | return input_texture.get_size() 213 | 214 | return size 215 | 216 | var _p_viewport : RID 217 | var _p_canvas : RID 218 | var _p_canvas_item : RID 219 | func _setup_viewport(): 220 | _p_viewport = RenderingServer.viewport_create() 221 | _p_canvas = RenderingServer.canvas_create() 222 | 223 | RenderingServer.viewport_attach_canvas(_p_viewport, _p_canvas) 224 | RenderingServer.viewport_set_update_mode(_p_viewport, RenderingServer.VIEWPORT_UPDATE_DISABLED) 225 | RenderingServer.viewport_set_clear_mode(_p_viewport, RenderingServer.VIEWPORT_CLEAR_ALWAYS) 226 | RenderingServer.viewport_set_active(_p_viewport, true) 227 | RenderingServer.viewport_set_use_hdr_2d(_p_viewport, hdr) 228 | RenderingServer.viewport_set_transparent_background(_p_viewport, transparency) 229 | 230 | _p_canvas_item = RenderingServer.canvas_item_create() 231 | RenderingServer.canvas_item_set_parent(_p_canvas_item, _p_canvas) 232 | 233 | func _blit_viewport(): 234 | var p_tex : RID = RenderingServer.viewport_get_texture(_p_viewport) 235 | 236 | if not p_tex.is_valid(): return 237 | 238 | var img := RenderingServer.texture_2d_get(p_tex) 239 | #if not img.is_valid(): return 240 | 241 | if mipmaps: 242 | img.generate_mipmaps() 243 | 244 | set_image(img) 245 | 246 | func _notification(what: int) -> void: 247 | if what == NOTIFICATION_PREDELETE: # Cleanup 248 | RenderingServer.free_rid(_p_viewport) 249 | RenderingServer.free_rid(_p_canvas_item) 250 | RenderingServer.free_rid(_p_canvas) 251 | 252 | #endregion 253 | 254 | 255 | func _find_unique_filename(path : String): 256 | path = path.get_basename() 257 | var base_dir := path.get_base_dir() 258 | 259 | var file_name := path.get_file() 260 | var underscore_idx := file_name.rfind("_") 261 | 262 | # Strip the file name of the _number 263 | if underscore_idx != -1: 264 | file_name = file_name.substr(0, underscore_idx) 265 | 266 | var dir := DirAccess.open(base_dir) 267 | 268 | var files := dir.get_files() 269 | for i in range(0, files.size()+1): 270 | # Put the _number back 271 | var new_file_name := file_name + "_" + str(i) 272 | var new_path := base_dir.path_join(new_file_name) 273 | 274 | # Be sure that no other file in the directory has this file name 275 | if dir.file_exists(new_file_name + ".png"): 276 | continue 277 | 278 | return new_path 279 | 280 | return ERR_FILE_NOT_FOUND 281 | 282 | func _safe_connect(obj : Object, sig: StringName, callable : Callable, flags : int = 0) -> void: 283 | if obj && !obj.is_connected(sig, callable): obj.connect(sig, callable, flags) 284 | func _safe_disconnect(obj : Object, sig: StringName, callable : Callable) -> void: 285 | if obj && obj.is_connected(sig, callable): obj.disconnect(sig, callable) 286 | -------------------------------------------------------------------------------- /scripts/audio/RecordWAV.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | class_name RecordWAV extends AudioStreamWAV 3 | 4 | ## A Resource that allows recording from the primary microphone. 5 | ## [br][color=purple]Made by celyk[/color] 6 | ## @tutorial(celyk's repo): https://github.com/celyk/godot-useful-stuff 7 | ## 8 | ## RecordWAV adds a bus named "Record" to the audio bus layout. You are free to use the Audio tab to add additional effects to the bus! 9 | ## [br] 10 | ## [br]The following audio settings must be set: 11 | ## [br]`audio/driver/enable_input = true` 12 | ## [br]`audio/general/ios/session_category = Play and Record` 13 | ## [br] 14 | ## [br]Code example WIP 15 | ## [codeblock] 16 | ## var record_wav = RecordWAV.new() 17 | ## record_wav.start_record() 18 | ## ... 19 | ## record_wav.stop_record() 20 | ## [/codeblock] 21 | 22 | # TODO 23 | # - Add record/stop buttons to mimick tape recorder interface 24 | # - Is there any way to detect the mic in real time? 25 | # - Is there any way to bypass the SceneTree? 26 | # - Is there any way to bypass the audio bus? 27 | # - Microphone should never ever go to Master 28 | # - Make the system more resilient 29 | # - Normalize audio clip? Could delegate to another AudioStream 30 | 31 | # Some buttons for the inspector 32 | @export_tool_button("Record", "DebugSkipBreakpointsOff") var record_button := func(): recording = true 33 | @export_tool_button("Stop", "EditorPathSmoothHandle") var stop_button := func(): recording = false 34 | 35 | ## Just a variable that controls the recording. Still working on it 36 | @export var recording := false : 37 | set(value): 38 | if value == recording: return 39 | 40 | recording = value 41 | 42 | if recording: 43 | start_record() 44 | else: 45 | stop_record() 46 | 47 | ## Automatically crops the recording around the peak 48 | @export var crop_to_peak := true 49 | 50 | ## Time in seconds subtracted from the start of the recording. Use negative values for padding around the volume peak 51 | @export var crop_begin := -0.1 52 | ## Time in seconds subtracted from the end of the recording. Useful for avoiding unwanted sound 53 | @export var crop_end := 0.1 54 | 55 | func _validate_property(property: Dictionary): 56 | # Prevent var recording from being saved true by PROPERTY_USAGE_STORAGE 57 | if property.name == "recording": 58 | property.usage = PROPERTY_USAGE_EDITOR 59 | 60 | # Hide some inhertited properties that clutter the UI 61 | var hide_me := false 62 | var disable_me := false 63 | match property.name: 64 | #"loop_mode": hide_me = true 65 | #"loop_begin": hide_me = true 66 | #"loop_end": hide_me = true 67 | "format": disable_me = true 68 | "mix_rate": disable_me = true 69 | "stereo": disable_me = true 70 | 71 | if hide_me: 72 | # Block the PROPERTY_USAGE_EDITOR bitflag 73 | property.usage &= ~PROPERTY_USAGE_EDITOR 74 | 75 | if disable_me: 76 | property.usage |= PROPERTY_USAGE_READ_ONLY 77 | 78 | 79 | func _init() -> void: 80 | AudioServer.bus_layout_changed.connect(_validate_audio_state) 81 | 82 | ## Start the recording 83 | func start_record(): 84 | format = AudioStreamWAV.FORMAT_16_BITS 85 | #mix_rate = 48000 86 | stereo = false 87 | 88 | if _initialize_bus() != OK: 89 | push_error("RecordWAV: ", "Failed to initialize bus") 90 | return 91 | 92 | if _initialize_microphone() != OK: 93 | push_error("RecordWAV: ", "Failed to initialize microphone") 94 | _cleanup_microphone() 95 | return 96 | 97 | if _initialize_meter() != OK: 98 | push_error("RecordWAV: ", "Failed to initialize volume meter") 99 | _cleanup_microphone() 100 | return 101 | 102 | 103 | _effect_record.set_recording_active(true) 104 | 105 | ## Stop the recording 106 | func stop_record(): 107 | _effect_record.set_recording_active(false) 108 | 109 | var wav_recording := _effect_record.get_recording() 110 | 111 | # Avoid cropping if there's too much data 112 | if crop_begin != 0.0 or crop_end != 0.0: 113 | var start_t : float = 0.0 + crop_begin 114 | var end_t : float = wav_recording.get_length() - crop_end 115 | 116 | if crop_to_peak: 117 | start_t += get_peak_t() 118 | 119 | # Prevent negative crop start time. Unsure why it's needed 120 | start_t = max(start_t, 0.0) 121 | 122 | _crop_wav(wav_recording, start_t, end_t) 123 | 124 | data = wav_recording.data 125 | format = wav_recording.format 126 | mix_rate = wav_recording.mix_rate 127 | stereo = wav_recording.stereo 128 | 129 | # Prevent the microphone from hurting our ears 130 | _cleanup_microphone() 131 | 132 | emit_changed() 133 | 134 | ## Get the time when the volume peaked 135 | func get_peak_t() -> float: 136 | return (_max_volume_t_msec - _start_t_msec) / 1000.0 137 | 138 | 139 | # PRIVATE 140 | 141 | var _effect_record := AudioEffectRecord.new() 142 | func _initialize_bus() -> Error: 143 | var bus_idx := AudioServer.get_bus_index("Record") 144 | if bus_idx != -1: 145 | _effect_record = AudioServer.get_bus_effect(bus_idx, 0) 146 | return OK 147 | 148 | AudioServer.add_bus() 149 | 150 | bus_idx = AudioServer.bus_count - 1 151 | AudioServer.set_bus_name(bus_idx, "Record") 152 | AudioServer.set_bus_mute(bus_idx, true) 153 | 154 | #AudioServer.add_bus_effect(bus_idx, AudioEffectCapture.new()) 155 | AudioServer.add_bus_effect(bus_idx, _effect_record) 156 | 157 | # Refresh the bus layout 158 | AudioServer.bus_layout_changed.emit() 159 | AudioServer.bus_renamed.emit(bus_idx, "New Bus", "Record") 160 | 161 | push_warning("RecordWAV: ", "A bus for recording has been added to the audio bus layout") 162 | 163 | return OK 164 | 165 | static var _microphone_stream_player : AudioStreamPlayer 166 | const _microphone_node_name := "AudioStreamPlayerMicrophone" 167 | func _initialize_microphone() -> Error: 168 | var tree := Engine.get_main_loop() as SceneTree 169 | 170 | # Check to see if the AudioStreamPlayer already exists somewhere 171 | if _microphone_stream_player == null: 172 | _microphone_stream_player = tree.root.find_child(_microphone_node_name, false) 173 | 174 | # If it exists, we're good to go 175 | if _microphone_stream_player and _microphone_stream_player.is_inside_tree(): 176 | return OK 177 | 178 | _microphone_stream_player = AudioStreamPlayer.new() 179 | _microphone_stream_player.stream = AudioStreamMicrophone.new() 180 | _microphone_stream_player.bus = "Record" 181 | _microphone_stream_player.name = _microphone_node_name 182 | 183 | tree.root.add_child(_microphone_stream_player) 184 | 185 | # Playback must start inside tree 186 | _microphone_stream_player.playing = true 187 | 188 | return OK 189 | 190 | func _cleanup_microphone(): 191 | # Another chance to find the node somewhere 192 | if _microphone_stream_player == null: 193 | var tree := Engine.get_main_loop() as SceneTree 194 | _microphone_stream_player = tree.root.find_child(_microphone_node_name, false) 195 | 196 | # Free the AudioStreamPlayer 197 | if _microphone_stream_player and !_microphone_stream_player.is_queued_for_deletion(): 198 | _microphone_stream_player.queue_free() 199 | 200 | # FFT for tracking peaks in volume 201 | var _spectrum_analyzer_instance : AudioEffectSpectrumAnalyzerInstance 202 | var _start_t_msec : int = 0 203 | func _initialize_meter() -> Error: 204 | var bus_idx := AudioServer.get_bus_index("Record") 205 | if bus_idx == -1: 206 | return ERR_UNCONFIGURED 207 | 208 | # Reset these before recording 209 | _start_t_msec = Time.get_ticks_msec() 210 | _max_volume_t_msec = Time.get_ticks_msec() 211 | _max_volume = Vector2(0,0) 212 | 213 | # Start processing the FFT 214 | var tree := Engine.get_main_loop() as SceneTree 215 | tree.process_frame.connect(_record_process) 216 | 217 | # Check to see if the effect already exists 218 | var effects := _get_audio_effects(bus_idx, ["AudioEffectSpectrumAnalyzer"]) 219 | 220 | if not effects.is_empty(): 221 | var effect : AudioEffectSpectrumAnalyzer = effects.back() 222 | 223 | # Search for the effect to get it's index 224 | var effect_idx : int = _get_audio_effects(bus_idx).rfind(effect) 225 | 226 | _spectrum_analyzer_instance = AudioServer.get_bus_effect_instance(bus_idx, effect_idx) 227 | else: 228 | # No effect found. Initialize it 229 | var _spectrum_analyzer = AudioEffectSpectrumAnalyzer.new() 230 | AudioServer.add_bus_effect(bus_idx, _spectrum_analyzer) 231 | 232 | _spectrum_analyzer_instance = AudioServer.get_bus_effect_instance(bus_idx, AudioServer.get_bus_effect_count(bus_idx)-1) 233 | 234 | # Refresh the bus layout 235 | AudioServer.bus_layout_changed.emit() 236 | 237 | return OK 238 | 239 | var _max_volume : Vector2 240 | var _max_volume_t_msec := 0 241 | func _record_process(): 242 | # Disconnect this function if recording is over 243 | if _effect_record and (not _effect_record.is_recording_active()): 244 | var tree := Engine.get_main_loop() as SceneTree 245 | tree.process_frame.disconnect(_record_process) 246 | return 247 | 248 | var volume := _spectrum_analyzer_instance.get_magnitude_for_frequency_range(0.0, 20000, AudioEffectSpectrumAnalyzerInstance.MAGNITUDE_AVERAGE) 249 | #print(volume, _max_volume) 250 | 251 | var average := (volume.x + volume.y) / 2.0 252 | if average > _max_volume.x: 253 | _max_volume.x = average 254 | _max_volume_t_msec = Time.get_ticks_msec() 255 | 256 | func _validate_audio_state(): 257 | var bus_idx := AudioServer.get_bus_index("Record") 258 | if bus_idx == -1: 259 | if _effect_record and _effect_record.is_recording_active(): 260 | push_error("RecordWAV: ", "Bus was removed during recording. Removing microphone now") 261 | _cleanup_microphone() 262 | 263 | func _crop_wav(wav:AudioStreamWAV, start_t:float, end_t:=-1.0): 264 | var start_sample_pos : int = start_t * wav.mix_rate 265 | 266 | var byte_per_sample := _get_bytes_per_sample(wav) 267 | 268 | if end_t < 0.0: 269 | wav.data = wav.data.slice(start_sample_pos * byte_per_sample, -1) 270 | else: 271 | var end_sample_pos : int = end_t * wav.mix_rate 272 | wav.data = wav.data.slice(start_sample_pos * byte_per_sample, end_sample_pos * byte_per_sample) 273 | 274 | func _get_bytes_per_sample(wav:AudioStreamWAV) -> int: 275 | var bytes_per_sample : int 276 | match format: 277 | FORMAT_8_BITS: 278 | bytes_per_sample = 1 279 | FORMAT_16_BITS: 280 | bytes_per_sample = 2 281 | _: 282 | bytes_per_sample = 1 283 | 284 | if wav.stereo: 285 | bytes_per_sample *= 2 286 | 287 | return bytes_per_sample 288 | 289 | # Get's audio bus effect objects with an optional type filter 290 | func _get_audio_effects(bus_idx:int, filter:=[]) -> Array[AudioEffect]: 291 | var effects : Array[AudioEffect] 292 | 293 | for i in range(0, AudioServer.get_bus_effect_count(bus_idx)): 294 | var effect : AudioEffect = AudioServer.get_bus_effect(bus_idx, i) 295 | effects.append(effect) 296 | 297 | if filter.is_empty(): 298 | return effects 299 | 300 | var filtered_effects : Array[AudioEffect] 301 | for effect in effects: 302 | if effect.get_class() in filter: 303 | filtered_effects.append(effect) 304 | 305 | return filtered_effects 306 | -------------------------------------------------------------------------------- /scripts/hello_triangle.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | class_name HelloTriangleEffect extends CompositorEffect 3 | 4 | ## This script serves as an example for rendering directly into the scene via the CompositorEffect API. It even shows how to support VR rendering 5 | ## [br][color=purple]Made by celyk[/color] 6 | ## @tutorial(celyk's repo): https://github.com/celyk/godot-useful-stuff 7 | 8 | # TODO 9 | # Review cleanup step 10 | # Fix hardcoded _framebuffer_format 11 | # Switch to UniformSetCacheRD and implement uniform set 12 | 13 | 14 | # PUBLIC 15 | 16 | ## Set this to push the transform of a Node3D for testing 17 | @export var target_node_unique_name : String 18 | var transform : Transform3D 19 | 20 | ## Set to true to support XR 21 | const vr_rendering_support := false 22 | 23 | # PRIVATE 24 | 25 | var _RD : RenderingDevice 26 | var _p_framebuffer : RID 27 | var _framebuffer_format 28 | 29 | var _p_render_pipeline : RID 30 | var _p_render_pipeline_uniform_set : RID 31 | var _p_vertex_buffer : RID 32 | var _p_vertex_array : RID 33 | var _p_shader : RID 34 | var _clear_colors := PackedColorArray([Color.DARK_BLUE]) 35 | 36 | func _init(): 37 | effect_callback_type = CompositorEffect.EFFECT_CALLBACK_TYPE_POST_TRANSPARENT 38 | 39 | _RD = RenderingServer.get_rendering_device() 40 | RenderingServer.call_on_render_thread(_initialize_render) 41 | 42 | ## Inject preprocessor flags in shader code to help compile variants. In this case to conditionally support VR rendering 43 | func _insert_shader_macros(source : String) -> String: 44 | var macros : String 45 | 46 | if vr_rendering_support: 47 | # Allows the shader to work in VR multiview rendering by enabling the GL_EXT_multiview extension 48 | macros += "#define USE_MULTIVIEW\n" 49 | 50 | source = source.replace("//INSERT_MACROS", macros) 51 | 52 | return source 53 | 54 | func _compile_shader(source_fragment : String = _default_source_fragment, source_vertex : String = _default_source_vertex) -> RID: 55 | source_fragment = _insert_shader_macros(source_fragment) 56 | source_vertex = _insert_shader_macros(source_vertex) 57 | 58 | var src := RDShaderSource.new() 59 | src.source_fragment = source_fragment 60 | src.source_vertex = source_vertex 61 | 62 | var shader_spirv : RDShaderSPIRV = _RD.shader_compile_spirv_from_source(src) 63 | 64 | var err = shader_spirv.get_stage_compile_error(RenderingDevice.SHADER_STAGE_VERTEX) 65 | if err: push_error( err ) 66 | err = shader_spirv.get_stage_compile_error(RenderingDevice.SHADER_STAGE_FRAGMENT) 67 | if err: push_error( err ) 68 | 69 | var p_shader : RID = _RD.shader_create_from_spirv(shader_spirv) 70 | 71 | return p_shader 72 | 73 | func _initialize_render(view_count := 1): 74 | # My guess at the internal framebuffer format, based on source code. It will be verified in _render_callback before actual usage 75 | var attachment_formats = [RDAttachmentFormat.new(),RDAttachmentFormat.new()] 76 | attachment_formats[0].usage_flags = _RD.TEXTURE_USAGE_COLOR_ATTACHMENT_BIT 77 | attachment_formats[0].format = RenderingDevice.DATA_FORMAT_R16G16B16A16_SFLOAT 78 | attachment_formats[1].usage_flags = _RD.TEXTURE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT 79 | attachment_formats[1].format = _RD.DATA_FORMAT_D24_UNORM_S8_UINT if _RD.texture_is_format_supported_for_usage(_RD.DATA_FORMAT_D24_UNORM_S8_UINT, attachment_formats[1].usage_flags) else _RD.DATA_FORMAT_D32_SFLOAT_S8_UINT 80 | 81 | _framebuffer_format = _RD.framebuffer_format_create( attachment_formats ) 82 | 83 | # If we got a framebuffer already, just get that format 84 | if _p_framebuffer.is_valid(): 85 | _framebuffer_format = _RD.framebuffer_get_format(_p_framebuffer) 86 | 87 | # Compile using the default shader source defined at the end of this file 88 | _p_shader = _compile_shader() 89 | 90 | # Create vertex buffer 91 | var vertex_buffer_bytes : PackedByteArray = _vertex_buffer.to_byte_array() 92 | _p_vertex_buffer = _RD.vertex_buffer_create(vertex_buffer_bytes.size(), vertex_buffer_bytes) 93 | 94 | # A little trick to reuse the same buffer for multiple attributes 95 | var vertex_buffers := [_p_vertex_buffer, _p_vertex_buffer] 96 | 97 | var sizeof_float := 4 # Needed to compute byte offset and stride 98 | var stride := 7 # How far until the next element 99 | 100 | var vertex_attrs = [RDVertexAttribute.new(), RDVertexAttribute.new()] 101 | vertex_attrs[0].format = _RD.DATA_FORMAT_R32G32B32_SFLOAT # vec3 equivalent 102 | vertex_attrs[0].location = 0 # layout binding 103 | vertex_attrs[0].offset = 0 * sizeof_float 104 | vertex_attrs[0].stride = stride * sizeof_float # How far until the next element, in bytes 105 | vertex_attrs[1].format = _RD.DATA_FORMAT_R32G32B32A32_SFLOAT # vec4 equivalent 106 | vertex_attrs[1].location = 1 # layout binding 107 | vertex_attrs[1].offset = 3 * sizeof_float 108 | vertex_attrs[1].stride = stride * sizeof_float # How far until the next element, in bytes 109 | var vertex_format = _RD.vertex_format_create(vertex_attrs) 110 | 111 | # Create a VAO, which keeps all of our vertex state handy for later 112 | _p_vertex_array = _RD.vertex_array_create(_vertex_buffer.size()/stride, vertex_format, vertex_buffers) 113 | 114 | # Inform the rasterizer what we need to do 115 | var raster_state = RDPipelineRasterizationState.new() 116 | raster_state.cull_mode = RenderingDevice.POLYGON_CULL_DISABLED 117 | var depth_state = RDPipelineDepthStencilState.new() 118 | depth_state.enable_depth_write = true 119 | depth_state.enable_depth_test = true 120 | depth_state.depth_compare_operator = RenderingDevice.COMPARE_OP_GREATER 121 | 122 | var blend = RDPipelineColorBlendState.new() 123 | blend.attachments.push_back( RDPipelineColorBlendStateAttachment.new() ) 124 | 125 | # Finally, create the render pipeline 126 | _p_render_pipeline = _RD.render_pipeline_create( 127 | _p_shader, 128 | _framebuffer_format, 129 | vertex_format, 130 | _RD.RENDER_PRIMITIVE_TRIANGLES, 131 | raster_state, 132 | RDPipelineMultisampleState.new(), 133 | depth_state, 134 | blend) 135 | 136 | func _render_callback(_effect_callback_type : int, render_data : RenderData): 137 | # Exit if we are not at the correct stage of rendering 138 | if _effect_callback_type != effect_callback_type: return 139 | 140 | var render_scene_buffers : RenderSceneBuffersRD = render_data.get_render_scene_buffers() 141 | var render_scene_data : RenderSceneData = render_data.get_render_scene_data() 142 | 143 | # Exit if, for whatever reason, we cannot aquire buffers 144 | if not render_scene_buffers: return 145 | 146 | # Ask for a framebuffer with multiview for VR rendering 147 | var view_count : int = render_scene_buffers.get_view_count() 148 | _p_framebuffer = FramebufferCacheRD.get_cache_multipass([render_scene_buffers.get_color_texture(), render_scene_buffers.get_depth_texture() ], [], view_count) 149 | 150 | # Verify that the framebuffer format is correct. If not, we need to reinitialize the render pipeline with the correct format 151 | if _framebuffer_format != _RD.framebuffer_get_format(_p_framebuffer): 152 | #_cleanup() 153 | 154 | if _p_render_pipeline.is_valid(): 155 | _RD.free_rid(_p_render_pipeline) 156 | if _p_shader.is_valid(): 157 | _RD.free_rid(_p_shader) 158 | if _p_vertex_array.is_valid(): 159 | _RD.free_rid(_p_vertex_array) 160 | if _p_vertex_buffer.is_valid(): 161 | _RD.free_rid(_p_vertex_buffer) 162 | 163 | _initialize_render(view_count) 164 | _p_framebuffer = FramebufferCacheRD.get_cache_multipass([render_scene_buffers.get_color_texture(), render_scene_buffers.get_depth_texture() ], [], view_count) 165 | 166 | 167 | _RD.draw_command_begin_label("Hello, Triangle!", Color(1.0, 1.0, 1.0, 1.0)) 168 | 169 | # Queue draw commands, without clearing whats already in the frame 170 | var draw_list : int = -1 171 | if Engine.get_version_info().major == 4 and Engine.get_version_info().minor < 4: 172 | draw_list = _RD.call("draw_list_begin", 173 | _p_framebuffer, 174 | _RD.INITIAL_ACTION_CONTINUE, 175 | _RD.FINAL_ACTION_CONTINUE, 176 | _RD.INITIAL_ACTION_CONTINUE, 177 | _RD.FINAL_ACTION_CONTINUE, 178 | _clear_colors, 179 | 1.0, 180 | 0, 181 | Rect2()) 182 | else: 183 | draw_list = _RD.call("draw_list_begin", 184 | _p_framebuffer, 185 | _RD.DRAW_DEFAULT_ALL, 186 | _clear_colors, 187 | 1.0, 188 | 0, 189 | Rect2(), 190 | 0) 191 | 192 | _RD.draw_list_bind_render_pipeline(draw_list, _p_render_pipeline) 193 | _RD.draw_list_bind_vertex_array(draw_list, _p_vertex_array) 194 | 195 | # Hacky stuff to get the target node 196 | if target_node_unique_name: 197 | var tree := Engine.get_main_loop() as SceneTree 198 | var root : Node = tree.edited_scene_root if Engine.is_editor_hint() else tree.current_scene 199 | var node_3d : Node3D = root.get_node("%"+target_node_unique_name) 200 | transform = node_3d.global_transform 201 | 202 | # Setup model view projection, accounting for VR rendering with multiview 203 | var MVPs : Array[Projection] 204 | var buffer := PackedFloat32Array() 205 | var sizeof_float := 4 206 | buffer.resize(view_count * 16 * sizeof_float) 207 | for view in range(0, view_count): 208 | var MVP : Projection = render_scene_data.get_view_projection(view) 209 | 210 | # A little something to allow Godot 4.3 beta to work. 4.3 beta 3 fixed this 211 | if "4.3-beta" in Engine.get_version_info().string: 212 | MVP = Projection.create_depth_correction(true) * MVP 213 | 214 | MVP *= Projection(render_scene_data.get_cam_transform().inverse() * transform) 215 | MVPs.append(MVP) 216 | 217 | for i in range(0,16): 218 | buffer[i + view * 16] = MVPs[view][i/4][i%4] 219 | 220 | # Send data to our shader 221 | var buffer_bytes : PackedByteArray = buffer.to_byte_array() 222 | var p_uniform_buffer : RID = _RD.uniform_buffer_create(buffer_bytes.size(), buffer_bytes) 223 | 224 | var uniforms = [] 225 | var uniform := RDUniform.new() 226 | uniform.binding = 0 227 | uniform.uniform_type = _RD.UNIFORM_TYPE_UNIFORM_BUFFER 228 | uniform.add_id(p_uniform_buffer) 229 | uniforms.push_back( uniform ) 230 | 231 | # Uniform set from last frame needs to be freed 232 | if _p_render_pipeline_uniform_set.is_valid(): 233 | _RD.free_rid(_p_render_pipeline_uniform_set) 234 | 235 | # Bind the new uniform set 236 | _p_render_pipeline_uniform_set = _RD.uniform_set_create(uniforms, _p_shader, 0) 237 | _RD.draw_list_bind_uniform_set(draw_list, _p_render_pipeline_uniform_set, 0) 238 | 239 | # Draw it! 240 | _RD.draw_list_draw(draw_list, false, 1) 241 | 242 | _RD.draw_list_end() 243 | 244 | _RD.draw_command_end_label() 245 | 246 | func _notification(what): 247 | if what == NOTIFICATION_PREDELETE: # Cleanup 248 | if _p_render_pipeline.is_valid(): 249 | _RD.free_rid(_p_render_pipeline) 250 | if _p_shader.is_valid(): 251 | _RD.free_rid(_p_shader) 252 | if _p_vertex_array.is_valid(): 253 | _RD.free_rid(_p_vertex_array) 254 | if _p_vertex_buffer.is_valid(): 255 | _RD.free_rid(_p_vertex_buffer) 256 | if _p_render_pipeline_uniform_set.is_valid(): 257 | _RD.free_rid(_p_render_pipeline_uniform_set) 258 | if _p_framebuffer.is_valid(): 259 | _RD.free_rid(_p_framebuffer) 260 | 261 | var _vertex_buffer := PackedFloat32Array([ 262 | -0.5,-0.288675,0, 1,0,0,1, 263 | 0.5,-0.288675,0, 0,1,0,1, 264 | 0,0.57735,0, 0,0,1,1, 265 | ]) 266 | 267 | const _default_source_vertex = " 268 | #version 450 269 | 270 | 271 | //INSERT_MACROS 272 | 273 | 274 | #ifdef USE_MULTIVIEW 275 | #extension GL_EXT_multiview : enable // Support stereoscopic VR rendering 276 | #else //USE_MULTIVIEW 277 | #define gl_ViewIndex 0 // Use mono rendering only 278 | #endif //USE_MULTIVIEW 279 | 280 | layout(location = 0) in vec3 a_Position; 281 | layout(location = 1) in vec4 a_Color; 282 | 283 | layout(set = 0, binding = 0) uniform UniformBufferObject { 284 | mat4 MVP[2]; 285 | }; 286 | 287 | layout(location = 2) out vec4 v_Color; 288 | 289 | void main(){ 290 | v_Color = a_Color; 291 | 292 | gl_Position = MVP[gl_ViewIndex] * vec4(a_Position, 1); 293 | } 294 | " 295 | 296 | const _default_source_fragment = " 297 | #version 450 298 | 299 | 300 | //INSERT_MACROS 301 | 302 | 303 | layout(location = 2) in vec4 a_Color; 304 | 305 | layout(location = 0) out vec4 frag_color; // Bound to buffer index 0 306 | 307 | void main(){ 308 | frag_color = a_Color; 309 | } 310 | " 311 | --------------------------------------------------------------------------------