├── .gitattributes ├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── README.md ├── addons ├── MacroAccess │ ├── macroaccess.gd │ ├── macroaccess.gdshaderinc │ ├── macroaccess.tscn │ ├── plugin.cfg │ └── plugin.gd └── VertexHandles │ ├── VertexHandles.gd │ ├── VertexHandlesGizmo.gd │ ├── plugin.cfg │ └── plugin.gd └── scripts ├── Feedback.gd ├── ShaderRenderTool.gd ├── ShaderTextEdit.gd ├── ShaderTexture.gd ├── audio └── RecordWAV.gd ├── hello_triangle.gd └── xr ├── XREye3D.gd └── world_grab.gd /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 include_file : ShaderInclude = preload("res://addons/MacroAccess/macroaccess.gdshaderinc") 24 | 25 | var new_code : String = "" 26 | for name in _macros.keys(): 27 | new_code += "#ifdef " + name + "\n" 28 | new_code += "#undef " + name + "\n" 29 | new_code += "#endif\n" 30 | 31 | new_code += "#define " + name + " " + _macros[name] + "\n\n" 32 | 33 | include_file.code = new_code 34 | print(include_file.code) 35 | -------------------------------------------------------------------------------- /addons/MacroAccess/macroaccess.gdshaderinc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/celyk/godot-useful-stuff/f46034cd6ef71dc3d0bbfd026053b6722c5e0f74/addons/MacroAccess/macroaccess.gdshaderinc -------------------------------------------------------------------------------- /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.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 | -------------------------------------------------------------------------------- /addons/MacroAccess/plugin.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorPlugin 3 | 4 | 5 | func _enter_tree(): 6 | add_autoload_singleton("MacroAccess", "res://addons/macroaccess/macroaccess.tscn") 7 | 8 | func _exit_tree(): 9 | remove_autoload_singleton("MacroAccess") 10 | -------------------------------------------------------------------------------- /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 | 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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 | ## 7 | ## Feedback is a [Viewport] that simply copies it's parent viewport, enabling safe access to the parent viewports previous frame. 8 | ## [br] 9 | ## [br]It automatically takes the size of the parent viewport. 10 | ## 11 | ## @tutorial(celyk's repo): https://github.com/celyk/godot-useful-stuff 12 | 13 | 14 | # PRIVATE 15 | 16 | # TODO: 17 | # simplify rendering 18 | # cache shader 19 | # xr support 20 | 21 | var _parent_viewport : Viewport 22 | 23 | func _init(): 24 | size = size 25 | render_target_update_mode = SubViewport.UPDATE_ALWAYS 26 | 27 | func _notification(what): 28 | match what: 29 | NOTIFICATION_POST_ENTER_TREE: # the node entered the tree and is ready 30 | _init_blit() 31 | 32 | _safe_disconnect(_parent_viewport, "size_changed", _handle_resize) 33 | 34 | _parent_viewport = _find_parent_viewport() 35 | _handle_resize() 36 | 37 | 38 | # editor shenanigans 39 | if Engine.is_editor_hint() && has_method("_do_handle_editor") && call("_do_handle_editor"): 40 | return 41 | 42 | 43 | _safe_connect(_parent_viewport, "size_changed", _handle_resize) 44 | 45 | NOTIFICATION_PREDELETE: 46 | _cleanup_blit() 47 | _safe_disconnect(_parent_viewport, "size_changed", _handle_resize) 48 | 49 | func _find_parent_viewport(): 50 | return get_parent().get_viewport() # get_viewport() on a viewport returns itself 51 | 52 | func _handle_resize(): 53 | size = _parent_viewport.size 54 | _material.set_shader_parameter("tex", _parent_viewport.get_texture()) 55 | 56 | func _safe_connect(obj : Object, sig: StringName, callable : Callable, flags : int = 0) -> void: 57 | if obj && !obj.is_connected(sig, callable): obj.connect(sig, callable, flags) 58 | func _safe_disconnect(obj : Object, sig: StringName, callable : Callable) -> void: 59 | if obj && obj.is_connected(sig, callable): obj.disconnect(sig, callable) 60 | 61 | 62 | # RENDERING 63 | 64 | var _p_viewport : RID 65 | var _p_scenario : RID 66 | var _p_camera : RID 67 | var _p_base : RID 68 | var _p_instance : RID 69 | var _material : Material 70 | var _p_light_base : RID 71 | var _p_light_instance : RID 72 | 73 | func _init_blit() -> void: 74 | if _p_viewport.is_valid(): 75 | # fixes a bug when switching scene 76 | RenderingServer.viewport_set_scenario(_p_viewport, _p_scenario) 77 | RenderingServer.viewport_attach_camera(_p_viewport, _p_camera) 78 | return 79 | 80 | _p_scenario = RenderingServer.scenario_create() 81 | _p_viewport = get_viewport_rid() 82 | 83 | RenderingServer.viewport_set_scenario(_p_viewport, _p_scenario) 84 | 85 | # camera setup 86 | _p_camera = RenderingServer.camera_create(); 87 | RenderingServer.viewport_attach_camera(_p_viewport, _p_camera) 88 | var p_env = RenderingServer.environment_create() 89 | RenderingServer.camera_set_environment(_p_camera, p_env) 90 | RenderingServer.camera_set_transform(_p_camera, Transform3D(Basis(), Vector3(0, 0, 1))) 91 | RenderingServer.camera_set_orthogonal(_p_camera, 2.1, 0.1, 10) 92 | 93 | # quad setup 94 | _p_base = RenderingServer.mesh_create() 95 | _p_instance = RenderingServer.instance_create2(_p_base, _p_scenario) 96 | 97 | var quad_mesh = QuadMesh.new() 98 | quad_mesh.size = Vector2(2,2) 99 | var arr = quad_mesh.get_mesh_arrays() 100 | RenderingServer.mesh_add_surface_from_arrays(_p_base, RenderingServer.PRIMITIVE_TRIANGLES, arr) 101 | 102 | _material = ShaderMaterial.new() 103 | _material.resource_local_to_scene = true 104 | _material.shader = Shader.new() 105 | _material.shader.code = _blit_shader_code 106 | 107 | RenderingServer.mesh_surface_set_material(_p_base, 0, _material.get_rid()) 108 | 109 | # light setup 110 | _p_light_base = RenderingServer.directional_light_create() 111 | _p_light_instance = RenderingServer.instance_create2(_p_light_base, _p_scenario) 112 | 113 | func _cleanup_blit() -> void: 114 | RenderingServer.free_rid(_p_instance) 115 | RenderingServer.free_rid(_p_base) 116 | RenderingServer.free_rid(_p_camera) 117 | RenderingServer.free_rid(_p_scenario) 118 | RenderingServer.free_rid(_p_light_instance) 119 | RenderingServer.free_rid(_p_light_base) 120 | 121 | 122 | # DATA 123 | 124 | const _blit_shader_code = " 125 | shader_type spatial; 126 | 127 | render_mode cull_disabled, ambient_light_disabled, depth_draw_never; 128 | 129 | uniform sampler2D tex : source_color, filter_nearest; 130 | 131 | void vertex(){ 132 | POSITION = MODEL_MATRIX * vec4(VERTEX,1); 133 | UV.y = 1.0 - UV.y; 134 | } 135 | 136 | void fragment(){ 137 | FOG = vec4(0); 138 | } 139 | 140 | // This expects 0-1 range input, outside that range it behaves poorly. 141 | vec3 srgb_to_linear(vec3 color) { 142 | // Approximation from http://chilliant.blogspot.com/2012/08/srgb-approximations-for-hlsl.html 143 | 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))); 144 | } 145 | 146 | // Workaround for ALBEDO color error on the compatibility renderer 147 | void light(){ 148 | vec4 samp = textureLod(tex, UV, 0.0); 149 | 150 | if(OUTPUT_IS_SRGB){ 151 | SPECULAR_LIGHT = srgb_to_linear(samp.rgb); 152 | } 153 | else{ 154 | SPECULAR_LIGHT = samp.rgb; 155 | } 156 | 157 | ALPHA = samp.a; 158 | } 159 | " 160 | 161 | 162 | # JANK 163 | # you can safely delete this section 164 | 165 | var _editor_viewport : Control 166 | func _find_editor_viewport(node : Node) -> void: 167 | if node.get_class() == "CanvasItemEditorViewport": 168 | _safe_disconnect(_editor_viewport, "resized", _handle_2d_editor_resize) 169 | 170 | _parent_viewport = get_tree().get_root() 171 | _editor_viewport = node 172 | 173 | _handle_2d_editor_resize() 174 | 175 | _safe_connect( _editor_viewport, "resized", _handle_2d_editor_resize) 176 | 177 | var parent = node.get_parent() 178 | if parent && parent.get_class() == "Node3DEditorViewport": 179 | _safe_disconnect(_editor_viewport, "resized", _handle_2d_editor_resize) 180 | 181 | _parent_viewport = get_tree().get_root() 182 | _editor_viewport = parent 183 | 184 | _handle_2d_editor_resize() 185 | 186 | _safe_connect( _editor_viewport, "resized", _handle_2d_editor_resize) 187 | 188 | 189 | # texture space to world space transform 190 | func _rect_to_transform(rect : Rect2) -> Transform2D: 191 | return Transform2D(Vector2(rect.size.x,0), Vector2(0,rect.size.y), rect.position) 192 | 193 | func _handle_2d_editor_resize(): 194 | size = _editor_viewport.size 195 | _material.set_shader_parameter("tex", _parent_viewport.get_texture()) 196 | 197 | var transform := Transform2D() 198 | transform = transform.translated(Vector2(1,1)).scaled(Vector2(0.5,0.5)) 199 | transform = _rect_to_transform( _editor_viewport.get_global_rect() ) * transform 200 | transform = _editor_viewport.get_viewport_transform().scaled(Vector2(1,1)/Vector2(_parent_viewport.size)) * transform 201 | transform = transform.translated(-Vector2(0.5,0.5)).scaled(Vector2(2,2)) 202 | transform = transform.affine_inverse() 203 | 204 | RenderingServer.instance_set_transform(_p_instance, Transform3D(transform)) 205 | 206 | func _do_handle_editor() -> bool: 207 | # The issue is that our scene root is not used for rendering when inside the editor 208 | # We must find the actual viewport used 209 | _safe_disconnect( get_tree().get_root(), "gui_focus_changed", _find_editor_viewport) 210 | 211 | if _parent_viewport != get_tree().get_edited_scene_root().get_parent(): 212 | return false 213 | 214 | _safe_connect( get_tree().get_root(), "gui_focus_changed", _find_editor_viewport) 215 | 216 | return true 217 | 218 | func _exit_tree(): 219 | _safe_disconnect( get_tree().get_root(), "gui_focus_changed", _find_editor_viewport) 220 | _safe_disconnect(_editor_viewport, "resized", _handle_2d_editor_resize) 221 | -------------------------------------------------------------------------------- /scripts/ShaderRenderTool.gd: -------------------------------------------------------------------------------- 1 | class_name ShaderRenderTool extends RefCounted 2 | 3 | ## Helper tool for rendering plain GLSL shaders to a texture via [RenderingDevice]. 4 | ## 5 | ## Blah blah blah blah. 6 | ## [br] 7 | ## [br]Below is an example of how ShaderRenderTool may be used. 8 | ## [codeblock] 9 | ## var grt = ShaderRenderTool.new() 10 | ## 11 | ## grt.set_shader(grt.compile_shader()) 12 | ## 13 | ## var img = grt.render() 14 | ## [/codeblock] 15 | 16 | 17 | # PUBLIC 18 | 19 | func compile_shader(source_fragment : String = _default_source_fragment, source_vertex : String = _default_source_vertex) -> RID: 20 | var src := RDShaderSource.new() 21 | src.source_fragment = source_fragment 22 | src.source_vertex = source_vertex 23 | 24 | var shader_spirv : RDShaderSPIRV = _RD.shader_compile_spirv_from_source(src) 25 | 26 | var err = shader_spirv.get_stage_compile_error(RenderingDevice.SHADER_STAGE_VERTEX) 27 | if err: push_error( err ) 28 | err = shader_spirv.get_stage_compile_error(RenderingDevice.SHADER_STAGE_FRAGMENT) 29 | if err: push_error( err ) 30 | 31 | var p_shader : RID = _RD.shader_create_from_spirv(shader_spirv) 32 | 33 | return p_shader 34 | 35 | func set_shader(value : RID): 36 | _p_shader = value 37 | 38 | _init_render_pipeline() 39 | 40 | func render() -> Image: 41 | # Queue draw commands, without clearing whats already in the frame 42 | var draw_list : int = -1 43 | if Engine.get_version_info().major == 4 and Engine.get_version_info().minor < 4: 44 | draw_list = _RD.call("draw_list_begin", 45 | _p_framebuffer, 46 | _RD.INITIAL_ACTION_CLEAR, 47 | _RD.FINAL_ACTION_READ, 48 | _RD.INITIAL_ACTION_CLEAR, 49 | _RD.FINAL_ACTION_READ, 50 | _clear_colors, 51 | 1.0, 52 | 0, 53 | Rect2()) 54 | else: 55 | draw_list = _RD.call("draw_list_begin", 56 | _p_framebuffer, 57 | _RD.DRAW_CLEAR_COLOR_ALL, 58 | _clear_colors, 59 | 1.0, 60 | 0, 61 | Rect2(), 62 | 0) 63 | 64 | _RD.draw_list_bind_render_pipeline(draw_list, _p_render_pipeline) 65 | _RD.draw_list_bind_vertex_array(draw_list, _p_vertex_array) 66 | _RD.draw_list_bind_index_array(draw_list, _p_index_array) 67 | #_RD.draw_list_bind_uniform_set(draw_list, _p_render_pipeline_uniform_set, 0) 68 | _RD.draw_list_draw(draw_list, true, 1) 69 | _RD.draw_list_end() 70 | 71 | # Actually render 72 | _RD.submit() 73 | 74 | # Wait for threads 75 | _RD.sync() 76 | 77 | var output_bytes : PackedByteArray = _RD.texture_get_data(_attachments[0], 0) 78 | var img := Image.create_from_data(_size.x, _size.y, false, Image.FORMAT_RGBAF, output_bytes) 79 | 80 | return img 81 | 82 | 83 | # PRIVATE 84 | 85 | var _size = Vector2i(256,256) 86 | var _color_format := RenderingDevice.DATA_FORMAT_R32G32B32A32_SFLOAT 87 | #var _desired_framebuffer_format : RenderingDevice.DataFormat = RenderingDevice.DataFormat.DATA_FORMAT_R32G32B32A32_SFLOAT 88 | 89 | var _RD : RenderingDevice 90 | var _attachments = [] 91 | var _framebuffer_format 92 | var _p_framebuffer: RID 93 | 94 | var _p_render_pipeline : RID 95 | var _p_render_pipeline_uniform_set : RID 96 | var _p_vertex_array : RID 97 | var _p_index_array : RID 98 | var _p_shader : RID 99 | var _clear_colors := PackedColorArray([Color.DARK_RED]) 100 | 101 | func _init(): 102 | # Create rendering device on a seperate thread 103 | _RD = RenderingServer.create_local_rendering_device() 104 | 105 | _init_framebuffer() 106 | 107 | func _init_framebuffer(): 108 | var attachment_formats = [RDAttachmentFormat.new(),RDAttachmentFormat.new()] 109 | attachment_formats[0].format = _color_format 110 | attachment_formats[0].usage_flags = _RD.TEXTURE_USAGE_SAMPLING_BIT | _RD.TEXTURE_USAGE_CAN_COPY_FROM_BIT | _RD.TEXTURE_USAGE_COLOR_ATTACHMENT_BIT 111 | attachment_formats[1].format = _RD.DATA_FORMAT_D32_SFLOAT 112 | attachment_formats[1].usage_flags = _RD.TEXTURE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT 113 | 114 | var color_texture_format = RDTextureFormat.new() 115 | color_texture_format.texture_type = _RD.TEXTURE_TYPE_2D 116 | color_texture_format.width = _size.x 117 | color_texture_format.height = _size.y 118 | color_texture_format.format = attachment_formats[0].format 119 | color_texture_format.usage_bits = attachment_formats[0].usage_flags 120 | 121 | _attachments.push_back( _RD.texture_create(color_texture_format,RDTextureView.new()) ) 122 | 123 | var depth_texture_format = RDTextureFormat.new() 124 | depth_texture_format.texture_type = _RD.TEXTURE_TYPE_2D 125 | depth_texture_format.width = _size.x 126 | depth_texture_format.height = _size.y 127 | depth_texture_format.format = attachment_formats[1].format 128 | depth_texture_format.usage_bits = attachment_formats[1].usage_flags 129 | 130 | _attachments.push_back( _RD.texture_create(depth_texture_format,RDTextureView.new()) ) 131 | 132 | if _RD.texture_is_valid(_attachments[0]) and _RD.texture_is_format_supported_for_usage(attachment_formats[0].format,attachment_formats[0].usage_flags): 133 | print("format0 supported") 134 | else: print("format0 not supported") 135 | if _RD.texture_is_valid(_attachments[1]) and _RD.texture_is_format_supported_for_usage(attachment_formats[1].format,attachment_formats[1].usage_flags): 136 | print("format1 supported") 137 | else: print("format1 not supported") 138 | 139 | _framebuffer_format = _RD.framebuffer_format_create( attachment_formats ) 140 | _p_framebuffer = _RD.framebuffer_create( _attachments ) 141 | #_p_framebuffer = _RD.framebuffer_create_empty(_size) 142 | 143 | func _init_render_pipeline(): 144 | var vertex_buffer_bytes : PackedByteArray = _vertex_buffer.to_byte_array() 145 | var index_buffer_bytes : PackedByteArray = _index_buffer.to_byte_array() 146 | 147 | var p_vertex_buffer : RID = _RD.vertex_buffer_create(vertex_buffer_bytes.size(), vertex_buffer_bytes) 148 | var p_index_buffer : RID = _RD.index_buffer_create(6, _RD.INDEX_BUFFER_FORMAT_UINT32, index_buffer_bytes) 149 | 150 | var vertex_buffers := [p_vertex_buffer, p_vertex_buffer] 151 | 152 | var sizeof_float = 4 153 | 154 | var vertex_attrs = [RDVertexAttribute.new(), RDVertexAttribute.new()] 155 | vertex_attrs[0].format = _RD.DATA_FORMAT_R32G32B32_SFLOAT 156 | vertex_attrs[0].location = 0 157 | vertex_attrs[0].offset = 0*sizeof_float 158 | vertex_attrs[0].stride = 5*sizeof_float 159 | vertex_attrs[1].format = _RD.DATA_FORMAT_R32G32_SFLOAT 160 | vertex_attrs[1].location = 1 161 | vertex_attrs[1].offset = 3*sizeof_float 162 | vertex_attrs[1].stride = 5*sizeof_float 163 | var vertex_format = _RD.vertex_format_create(vertex_attrs) 164 | 165 | #var vertex_count = _index_buffer.size() 166 | #vertex_count = 3 167 | _p_vertex_array = _RD.vertex_array_create(_vertex_buffer.size()/5, vertex_format, vertex_buffers)#, PackedInt64Array([0,3])) 168 | _p_index_array = _RD.index_array_create(p_index_buffer, 0, _index_buffer.size()) 169 | 170 | var uniforms = [] 171 | var uniform := RDUniform.new() 172 | #uniform.binding = 0 173 | #uniform.uniform_type = _RD. 174 | #uniforms.push_back( uniform ) 175 | 176 | #_p_render_pipeline_uniform_set = _RD.uniform_set_create(uniforms,_p_shader,0) 177 | 178 | var raster_state = RDPipelineRasterizationState.new() 179 | var depth_state = RDPipelineDepthStencilState.new() 180 | depth_state.enable_depth_write = true 181 | depth_state.enable_depth_test = true 182 | depth_state.depth_compare_operator = RenderingDevice.COMPARE_OP_LESS 183 | #depth_state.depth_compare_operator = RenderingDevice.COMPARE_OP_ALWAYS 184 | 185 | var blend = RDPipelineColorBlendState.new() 186 | blend.attachments.push_back( RDPipelineColorBlendStateAttachment.new() ) 187 | 188 | _p_render_pipeline = _RD.render_pipeline_create( 189 | _p_shader, 190 | _framebuffer_format, 191 | vertex_format, 192 | _RD.RENDER_PRIMITIVE_TRIANGLES, 193 | raster_state, 194 | RDPipelineMultisampleState.new(), 195 | depth_state, 196 | blend) 197 | 198 | # Destructor... RefCounted may have issues 199 | func _notification(what): 200 | if what == NOTIFICATION_PREDELETE: 201 | _RD.free_rid(_p_framebuffer) 202 | 203 | # Free all framebuffer attachments 204 | for p_texture in _attachments: 205 | _RD.free_rid(p_texture) 206 | 207 | #_RD.free_rid(_framebuffer_format) ?? 208 | #_RD.free_rid(_p_render_pipeline_uniform_set) 209 | _RD.free_rid(_p_vertex_array) 210 | _RD.free_rid(_p_index_array) 211 | _RD.free_rid(_p_shader) 212 | 213 | _RD.free() 214 | 215 | # DATA 216 | #var _vertex_buffer := PackedFloat32Array([-1,-1,0, 0,0, 1,-1,0, 1,0, 1,1,0, 1,1, -1,1,0, 0,1]) 217 | #var _index_buffer := PackedInt32Array([0,1,2, 2,3,0]) 218 | var _vertex_buffer := PackedFloat32Array([ 219 | -.5,-.8,.5, 0,0, 220 | 1,-1,.5, 1,0, 221 | 0,1,.5, 1,1, 222 | -1,1,1, .5,.5, 223 | 1,1,1, .5,.5, 224 | 0,0,0, 0,0 225 | ]) 226 | var _index_buffer := PackedInt32Array([0,1,2, 3,4,5]) 227 | 228 | const _default_source_vertex = " 229 | #version 450 230 | 231 | layout(location = 0) in vec3 a_Position; 232 | layout(location = 1) in vec2 a_Uv; 233 | 234 | layout(location = 2) out vec2 v_Uv; 235 | 236 | void main(){ 237 | v_Uv = a_Uv; 238 | gl_Position = vec4(a_Position, 1); 239 | } 240 | " 241 | 242 | const _default_source_fragment = " 243 | #version 450 244 | 245 | layout(location = 2) in vec2 v_Uv; 246 | 247 | layout(location = 0) out vec4 COLOR; 248 | 249 | //layout(location = 0) uniform float u_test; 250 | 251 | void main(){ 252 | COLOR = vec4(v_Uv, 1, 1); 253 | } 254 | " 255 | -------------------------------------------------------------------------------- /scripts/ShaderTextEdit.gd: -------------------------------------------------------------------------------- 1 | class_name ShaderTextEdit extends TextEdit 2 | 3 | ## A text box for editing a shader at runtime. 4 | 5 | ## The shader which you want to edit. 6 | @export var target_shader : Shader 7 | 8 | ## Sets the code of the shader to the current text. 9 | func recompile(): 10 | if target_shader: 11 | target_shader.code = text 12 | 13 | func _input(event): 14 | if event is InputEventKey: 15 | if event.alt_pressed and event.keycode == KEY_ENTER: 16 | if get_viewport().gui_get_focus_owner() == self: 17 | recompile() 18 | 19 | func _ready(): 20 | if target_shader: 21 | # set text to initial shader file 22 | text = target_shader.code 23 | -------------------------------------------------------------------------------- /scripts/ShaderTexture.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | class_name ShaderTexture extends ImageTexture 3 | 4 | ## A texture that takes a shader to generate the output. Useful for caching procedural textures 5 | 6 | 7 | #region Interface 8 | 9 | @export_tool_button("Generate", "Callable") var generate_pulse = generate 10 | 11 | @export var size := Vector2i(128,128) : 12 | set(value): 13 | size = value 14 | _update_size() 15 | 16 | @export var hdr := false : 17 | set(value): 18 | hdr = value 19 | _update_hdr() 20 | 21 | @export var transparency := true : 22 | set(value): 23 | transparency = value 24 | _update_transparency() 25 | 26 | @export var use_texture_size := false : 27 | set(value): 28 | use_texture_size = value 29 | _update_size() 30 | 31 | @export_tool_button("Save", "Callable") var save_pulse = save_output 32 | @export_tool_button("Load last", "Callable") var load_last_pulse = load_last 33 | 34 | @export var input_texture : Texture2D : 35 | set(value): 36 | input_texture = value 37 | _update_input() 38 | 39 | @export var material : ShaderMaterial : 40 | set(value): 41 | material = value 42 | 43 | if material == null: return 44 | 45 | _update_material() 46 | 47 | #endregion 48 | 49 | #region Methods 50 | 51 | func generate() -> void: 52 | if not _initialized: return 53 | 54 | RenderingServer.viewport_set_update_mode(_p_viewport, RenderingServer.VIEWPORT_UPDATE_ONCE) 55 | 56 | # Wait for the frame to render 57 | await RenderingServer.frame_post_draw 58 | 59 | _blit_viewport() 60 | 61 | func save_output() -> void: 62 | if input_texture == null: return 63 | 64 | var path := input_texture.resource_path.get_basename() 65 | 66 | if not input_texture.is_built_in(): 67 | var new_path = _find_unique_filename(path) 68 | get_image().save_png(new_path + ".png") 69 | EditorInterface.get_resource_filesystem().scan() 70 | 71 | # If the resource isn't associated with a specific file, open a file save dialog 72 | elif Engine.is_editor_hint(): 73 | var dialog := EditorFileDialog.new() 74 | dialog.file_mode = EditorFileDialog.FILE_MODE_SAVE_FILE 75 | dialog.filters = PackedStringArray(["*.png, *.jpg, *.jpeg ; Supported Images"]) 76 | 77 | dialog.file_selected.connect( 78 | func(_path: String): 79 | get_image().save_png(_path) 80 | EditorInterface.get_resource_filesystem().scan() 81 | ) 82 | 83 | var tree := Engine.get_main_loop() as SceneTree 84 | tree.root.add_child(dialog) 85 | 86 | dialog.popup_file_dialog() 87 | 88 | func load_last() -> void: 89 | pass 90 | 91 | #endregion 92 | 93 | #region Initialization 94 | 95 | var _initialized := false 96 | func _init() -> void: 97 | _setup_viewport() 98 | _update_size() 99 | 100 | # Prevent generation on load 101 | (func(): _initialized = true).call_deferred() 102 | 103 | #endregion 104 | 105 | #region Update state 106 | 107 | func _update_material(): 108 | RenderingServer.canvas_item_set_material(_p_canvas_item, material.get_rid()) 109 | 110 | generate() 111 | 112 | func _update_size(): 113 | var internal_size := _get_internal_size() 114 | RenderingServer.viewport_set_size(_p_viewport, internal_size.x, internal_size.y) 115 | 116 | _update_rect() 117 | 118 | generate() 119 | 120 | func _update_hdr(): 121 | RenderingServer.viewport_set_use_hdr_2d(_p_viewport, hdr) 122 | 123 | generate() 124 | 125 | func _update_transparency(): 126 | RenderingServer.viewport_set_transparent_background(_p_viewport, transparency) 127 | 128 | generate() 129 | 130 | func _update_input(): 131 | _update_rect() 132 | 133 | #endregion 134 | 135 | #region Rect setup 136 | 137 | func _update_rect(): 138 | RenderingServer.canvas_item_clear(_p_canvas_item) 139 | 140 | var internal_size := _get_internal_size() 141 | 142 | if input_texture != null: 143 | RenderingServer.canvas_item_add_texture_rect(_p_canvas_item, Rect2(Vector2(), internal_size), input_texture.get_rid()) 144 | else: 145 | RenderingServer.canvas_item_add_rect(_p_canvas_item, Rect2(Vector2(), internal_size), Color(1,1,1,1)) 146 | 147 | func _get_internal_size() -> Vector2i: 148 | if use_texture_size and input_texture != null: 149 | return input_texture.get_size() 150 | 151 | return size 152 | 153 | var _p_viewport : RID 154 | var _p_canvas : RID 155 | var _p_canvas_item : RID 156 | func _setup_viewport(): 157 | _p_viewport = RenderingServer.viewport_create() 158 | _p_canvas = RenderingServer.canvas_create() 159 | 160 | RenderingServer.viewport_attach_canvas(_p_viewport, _p_canvas) 161 | RenderingServer.viewport_set_update_mode(_p_viewport, RenderingServer.VIEWPORT_UPDATE_DISABLED) 162 | RenderingServer.viewport_set_clear_mode(_p_viewport, RenderingServer.VIEWPORT_CLEAR_ALWAYS) 163 | RenderingServer.viewport_set_active(_p_viewport, true) 164 | RenderingServer.viewport_set_use_hdr_2d(_p_viewport, hdr) 165 | RenderingServer.viewport_set_transparent_background(_p_viewport, transparency) 166 | 167 | _p_canvas_item = RenderingServer.canvas_item_create() 168 | RenderingServer.canvas_item_set_parent(_p_canvas_item, _p_canvas) 169 | 170 | func _blit_viewport(): 171 | var p_tex : RID = RenderingServer.viewport_get_texture(_p_viewport) 172 | 173 | if not p_tex.is_valid(): return 174 | 175 | var img := RenderingServer.texture_2d_get(p_tex) 176 | if not p_tex.is_valid(): return 177 | set_image(img) 178 | 179 | func _notification(what: int) -> void: 180 | if what == NOTIFICATION_PREDELETE: # Cleanup 181 | RenderingServer.free_rid(_p_viewport) 182 | RenderingServer.free_rid(_p_canvas_item) 183 | RenderingServer.free_rid(_p_canvas) 184 | 185 | #endregion 186 | 187 | 188 | func _find_unique_filename(path : String): 189 | path = path.get_basename() 190 | var base_dir := path.get_base_dir() 191 | 192 | var file_name := path.get_file() 193 | var underscore_idx := file_name.rfind("_") 194 | 195 | # Strip the file name of the _number 196 | if underscore_idx != -1: 197 | file_name = file_name.substr(0, underscore_idx) 198 | 199 | var dir := DirAccess.open(base_dir) 200 | 201 | var files := dir.get_files() 202 | for i in range(0, files.size()+1): 203 | # Put the _number back 204 | var new_file_name := file_name + "_" + str(i) 205 | var new_path := base_dir.path_join(new_file_name) 206 | 207 | # Be sure that no other file in the directory has this file name 208 | if dir.file_exists(new_file_name + ".png"): 209 | continue 210 | 211 | return new_path 212 | 213 | return ERR_FILE_NOT_FOUND 214 | -------------------------------------------------------------------------------- /scripts/audio/RecordWAV.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | class_name RecordWAV extends AudioStreamWAV 3 | 4 | ## A Resource that allows recording from the primary microphone. 5 | ## 6 | ## 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! 7 | ## [br] 8 | ## [br]The following audio settings must be set: 9 | ## [br]`audio/driver/enable_input = true` 10 | ## [br]`audio/general/ios/session_category = Play and Record` 11 | ## [br] 12 | ## [br]Code example WIP 13 | ## [codeblock] 14 | ## var record_wav = RecordWAV.new() 15 | ## record_wav.start_record() 16 | ## ... 17 | ## record_wav.stop_record() 18 | ## [/codeblock] 19 | 20 | # TODO 21 | # - Is there any way to detect the mic in real time? 22 | # - Is there any way to bypass the SceneTree? 23 | # - Is there any way to bypass the audio bus? 24 | # - Microphone should never ever go to Master 25 | # - Make the system more resilient 26 | # - Normalize audio clip? Could delegate to another AudioStream 27 | 28 | #@export_tool_button("Pulse") var pulse : _ 29 | ## Just a variable that controls the recording. Still working on it 30 | @export var recording := false : 31 | set(value): 32 | if value == recording: return 33 | 34 | recording = value 35 | 36 | if recording: 37 | start_record() 38 | else: 39 | stop_record() 40 | 41 | ## Automatically crops the recording around the peak 42 | @export var crop_to_peak := true 43 | 44 | ## Controls how much padding is added before the peak of the recording 45 | @export var crop_padding := 0.1 46 | 47 | func _init() -> void: 48 | AudioServer.bus_layout_changed.connect(_validate_audio_state) 49 | 50 | ## Start the recording 51 | func start_record(): 52 | format = AudioStreamWAV.FORMAT_16_BITS 53 | #mix_rate = 48000 54 | stereo = false 55 | 56 | if _initialize_bus() != OK: 57 | printerr("Failed to initialize bus") 58 | return 59 | 60 | if _initialize_microphone() != OK: 61 | printerr("Failed to initialize microphone") 62 | _cleanup_microphone() 63 | return 64 | 65 | _effect_record.set_recording_active(true) 66 | 67 | ## Stop the recording 68 | func stop_record(): 69 | _effect_record.set_recording_active(false) 70 | 71 | var wav_recording := _effect_record.get_recording() 72 | 73 | # Avoid cropping if there's too much data 74 | if crop_to_peak and wav_recording.get_length() < 3.0: 75 | var start_t := _find_peak(wav_recording) - crop_padding 76 | _crop_wav(wav_recording, start_t) 77 | else: 78 | print_debug("Too much data, cropping avoided") 79 | 80 | data = wav_recording.data 81 | format = wav_recording.format 82 | mix_rate = wav_recording.mix_rate 83 | stereo = wav_recording.stereo 84 | 85 | # Prevent the microphone from hurting our ears 86 | _cleanup_microphone() 87 | 88 | emit_changed() 89 | 90 | 91 | # PRIVATE 92 | 93 | var _effect_record := AudioEffectRecord.new() 94 | func _initialize_bus() -> Error: 95 | var bus_idx := AudioServer.get_bus_index("Record") 96 | if bus_idx != -1: 97 | _effect_record = AudioServer.get_bus_effect(bus_idx, 0) 98 | return OK 99 | 100 | AudioServer.add_bus() 101 | 102 | bus_idx = AudioServer.bus_count - 1 103 | AudioServer.set_bus_name(bus_idx, "Record") 104 | AudioServer.set_bus_mute(bus_idx, true) 105 | 106 | #AudioServer.add_bus_effect(bus_idx, AudioEffectCapture.new()) 107 | AudioServer.add_bus_effect(bus_idx, _effect_record) 108 | 109 | # Refresh the bus layout 110 | AudioServer.bus_layout_changed.emit() 111 | AudioServer.bus_renamed.emit(bus_idx, "New Bus", "Record") 112 | 113 | return OK 114 | 115 | static var _microphone_stream_player : AudioStreamPlayer 116 | const _microphone_node_name := "AudioStreamPlayerMicrophone" 117 | func _initialize_microphone() -> Error: 118 | var tree := Engine.get_main_loop() as SceneTree 119 | 120 | # Check to see if the AudioStreamPlayer already exists somewhere 121 | if _microphone_stream_player == null: 122 | _microphone_stream_player = tree.root.find_child(_microphone_node_name, false) 123 | 124 | # If it exists, we're good to go 125 | if _microphone_stream_player and _microphone_stream_player.is_inside_tree(): 126 | return OK 127 | 128 | _microphone_stream_player = AudioStreamPlayer.new() 129 | _microphone_stream_player.stream = AudioStreamMicrophone.new() 130 | _microphone_stream_player.bus = "Record" 131 | _microphone_stream_player.name = _microphone_node_name 132 | 133 | tree.root.add_child(_microphone_stream_player) 134 | 135 | # Playback must start inside tree 136 | _microphone_stream_player.playing = true 137 | 138 | return OK 139 | 140 | func _cleanup_microphone(): 141 | # Another chance to find the node somewhere 142 | if _microphone_stream_player == null: 143 | var tree := Engine.get_main_loop() as SceneTree 144 | _microphone_stream_player = tree.root.find_child(_microphone_node_name, false) 145 | 146 | # Free the AudioStreamPlayer 147 | if _microphone_stream_player and !_microphone_stream_player.is_queued_for_deletion(): 148 | _microphone_stream_player.queue_free() 149 | 150 | func _validate_audio_state(): 151 | var bus_idx := AudioServer.get_bus_index("Record") 152 | if bus_idx == -1: 153 | if _effect_record and _effect_record.is_recording_active(): 154 | push_error("Bus was removed during recording. Removing microphone now") 155 | _cleanup_microphone() 156 | 157 | func _crop_wav(wav:AudioStreamWAV, start_t:float, end_t:=-1.0): 158 | var start_sample_pos : int = start_t * wav.mix_rate 159 | 160 | var byte_per_sample := _get_bytes_per_sample(wav) 161 | 162 | if end_t < 0.0: 163 | wav.data = wav.data.slice(start_sample_pos * byte_per_sample, -1) 164 | else: 165 | var end_sample_pos : int = end_t * wav.mix_rate 166 | wav.data = wav.data.slice(start_sample_pos * byte_per_sample, end_sample_pos * byte_per_sample) 167 | 168 | ''' 169 | func _instantiate_playback() -> AudioStreamPlayback: 170 | var playback := self.instantiate_playback() 171 | 172 | var t := _find_peak(self) 173 | print(t) 174 | playback.start(t) 175 | 176 | return playback 177 | ''' 178 | 179 | func _get_bytes_per_sample(wav:AudioStreamWAV) -> int: 180 | var bytes_per_sample : int 181 | match format: 182 | FORMAT_8_BITS: 183 | bytes_per_sample = 1 184 | FORMAT_16_BITS: 185 | bytes_per_sample = 2 186 | _: 187 | bytes_per_sample = 1 188 | 189 | if wav.stereo: 190 | bytes_per_sample *= 2 191 | 192 | return bytes_per_sample 193 | 194 | 195 | # JANK 196 | 197 | # Finds the time in seconds of the loudest point in the sound 198 | func _find_peak(wav:AudioStreamWAV) -> float: 199 | if wav.format > 1: return 0.0 200 | 201 | var min_value := 2**16 202 | var max_value := -2**16 203 | 204 | #var difference := 2**31 205 | 206 | var sample_pos := 0 207 | 208 | var bytes_per_sample : int = _get_bytes_per_sample(wav) 209 | 210 | var num_samples := wav.data.size() / bytes_per_sample 211 | 212 | 213 | 214 | for i in range(0, num_samples): 215 | var pos := i * bytes_per_sample 216 | 217 | var value : int 218 | 219 | match format: 220 | FORMAT_8_BITS: 221 | value = wav.data.decode_s8(pos) 222 | FORMAT_16_BITS: 223 | value = wav.data.decode_s16(pos) 224 | 225 | if stereo: 226 | match format: 227 | FORMAT_8_BITS: 228 | value += wav.data.decode_s8(pos+1) 229 | FORMAT_16_BITS: 230 | value += wav.data.decode_s16(pos+2) 231 | 232 | value /= 2 233 | 234 | if value < min_value: 235 | min_value = value 236 | sample_pos = i 237 | 238 | if value > max_value: 239 | max_value = value 240 | sample_pos = i 241 | 242 | var max_t := sample_pos * 1.0 / wav.mix_rate 243 | 244 | #print("minmax: ", min_value, " ", max_value) 245 | 246 | return max_t 247 | -------------------------------------------------------------------------------- /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 5 | 6 | 7 | # TODO 8 | # Review cleanup step 9 | # Fix hardcoded _framebuffer_format 10 | # Support legacy get_view_projection() behavior 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 | 21 | # PRIVATE 22 | 23 | var _RD : RenderingDevice 24 | var _p_framebuffer : RID 25 | var _framebuffer_format 26 | 27 | var _p_render_pipeline : RID 28 | var _p_render_pipeline_uniform_set : RID 29 | var _p_vertex_buffer : RID 30 | var _p_vertex_array : RID 31 | var _p_shader : RID 32 | var _clear_colors := PackedColorArray([Color.DARK_BLUE]) 33 | 34 | func _init(): 35 | effect_callback_type = CompositorEffect.EFFECT_CALLBACK_TYPE_POST_TRANSPARENT 36 | 37 | _RD = RenderingServer.get_rendering_device() 38 | RenderingServer.call_on_render_thread(_initialize_render) 39 | 40 | func _compile_shader(source_fragment : String = _default_source_fragment, source_vertex : String = _default_source_vertex) -> RID: 41 | var src := RDShaderSource.new() 42 | src.source_fragment = source_fragment 43 | src.source_vertex = source_vertex 44 | 45 | var shader_spirv : RDShaderSPIRV = _RD.shader_compile_spirv_from_source(src) 46 | 47 | var err = shader_spirv.get_stage_compile_error(RenderingDevice.SHADER_STAGE_VERTEX) 48 | if err: push_error( err ) 49 | err = shader_spirv.get_stage_compile_error(RenderingDevice.SHADER_STAGE_FRAGMENT) 50 | if err: push_error( err ) 51 | 52 | var p_shader : RID = _RD.shader_create_from_spirv(shader_spirv) 53 | 54 | return p_shader 55 | 56 | func _initialize_render(view_count := 1): 57 | # My guess at the internal framebuffer format, based on source code. It will be verified in _render_callback before actual usage 58 | var attachment_formats = [RDAttachmentFormat.new(),RDAttachmentFormat.new()] 59 | attachment_formats[0].usage_flags = _RD.TEXTURE_USAGE_COLOR_ATTACHMENT_BIT 60 | attachment_formats[0].format = RenderingDevice.DATA_FORMAT_R16G16B16A16_SFLOAT 61 | attachment_formats[1].usage_flags = _RD.TEXTURE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT 62 | 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 63 | 64 | _framebuffer_format = _RD.framebuffer_format_create( attachment_formats ) 65 | 66 | # If we got a framebuffer already, just get that format 67 | if _p_framebuffer.is_valid(): 68 | _framebuffer_format = _RD.framebuffer_get_format(_p_framebuffer) 69 | 70 | 71 | # Compile using the default shader source defined at the end of this file 72 | _p_shader = _compile_shader() 73 | 74 | # Create vertex buffer 75 | var vertex_buffer_bytes : PackedByteArray = _vertex_buffer.to_byte_array() 76 | _p_vertex_buffer = _RD.vertex_buffer_create(vertex_buffer_bytes.size(), vertex_buffer_bytes) 77 | 78 | # A little trick to reuse the same buffer for multiple attributes 79 | var vertex_buffers := [_p_vertex_buffer, _p_vertex_buffer] 80 | 81 | var sizeof_float := 4 # Needed to compute byte offset and stride 82 | var stride := 7 # How far until the next element 83 | 84 | var vertex_attrs = [RDVertexAttribute.new(), RDVertexAttribute.new()] 85 | vertex_attrs[0].format = _RD.DATA_FORMAT_R32G32B32_SFLOAT # vec3 equivalent 86 | vertex_attrs[0].location = 0 # layout binding 87 | vertex_attrs[0].offset = 0 * sizeof_float 88 | vertex_attrs[0].stride = stride * sizeof_float # How far until the next element, in bytes 89 | vertex_attrs[1].format = _RD.DATA_FORMAT_R32G32B32A32_SFLOAT # vec4 equivalent 90 | vertex_attrs[1].location = 1 # layout binding 91 | vertex_attrs[1].offset = 3 * sizeof_float 92 | vertex_attrs[1].stride = stride * sizeof_float # How far until the next element, in bytes 93 | var vertex_format = _RD.vertex_format_create(vertex_attrs) 94 | 95 | # Create a VAO, which keeps all of our vertex state handy for later 96 | _p_vertex_array = _RD.vertex_array_create(_vertex_buffer.size()/stride, vertex_format, vertex_buffers) 97 | 98 | # Inform the rasterizer what we need to do 99 | var raster_state = RDPipelineRasterizationState.new() 100 | raster_state.cull_mode = RenderingDevice.POLYGON_CULL_DISABLED 101 | var depth_state = RDPipelineDepthStencilState.new() 102 | depth_state.enable_depth_write = true 103 | depth_state.enable_depth_test = true 104 | depth_state.depth_compare_operator = RenderingDevice.COMPARE_OP_GREATER 105 | 106 | var blend = RDPipelineColorBlendState.new() 107 | blend.attachments.push_back( RDPipelineColorBlendStateAttachment.new() ) 108 | 109 | # Finally, create the render pipeline 110 | _p_render_pipeline = _RD.render_pipeline_create( 111 | _p_shader, 112 | _framebuffer_format, 113 | vertex_format, 114 | _RD.RENDER_PRIMITIVE_TRIANGLES, 115 | raster_state, 116 | RDPipelineMultisampleState.new(), 117 | depth_state, 118 | blend) 119 | 120 | func _render_callback(_effect_callback_type : int, render_data : RenderData): 121 | # Exit if we are not at the correct stage of rendering 122 | if _effect_callback_type != effect_callback_type: return 123 | 124 | var render_scene_buffers : RenderSceneBuffersRD = render_data.get_render_scene_buffers() 125 | var render_scene_data : RenderSceneData = render_data.get_render_scene_data() 126 | 127 | # Exit if, for whatever reason, we cannot aquire buffers 128 | if not render_scene_buffers: return 129 | 130 | # Ask for a framebuffer with multiview for VR rendering 131 | var view_count : int = render_scene_buffers.get_view_count() 132 | _p_framebuffer = FramebufferCacheRD.get_cache_multipass([render_scene_buffers.get_color_texture(), render_scene_buffers.get_depth_texture() ], [], view_count) 133 | 134 | # Verify that the framebuffer format is correct. If not, we need to reinitialize the render pipeline with the correct format 135 | if _framebuffer_format != _RD.framebuffer_get_format(_p_framebuffer): 136 | #_cleanup() 137 | 138 | if _p_render_pipeline.is_valid(): 139 | _RD.free_rid(_p_render_pipeline) 140 | if _p_shader.is_valid(): 141 | _RD.free_rid(_p_shader) 142 | if _p_vertex_array.is_valid(): 143 | _RD.free_rid(_p_vertex_array) 144 | if _p_vertex_buffer.is_valid(): 145 | _RD.free_rid(_p_vertex_buffer) 146 | 147 | _initialize_render(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 | 151 | _RD.draw_command_begin_label("Hello, Triangle!", Color(1.0, 1.0, 1.0, 1.0)) 152 | 153 | # Queue draw commands, without clearing whats already in the frame 154 | var draw_list : int = -1 155 | if Engine.get_version_info().major == 4 and Engine.get_version_info().minor < 4: 156 | draw_list = _RD.call("draw_list_begin", 157 | _p_framebuffer, 158 | _RD.INITIAL_ACTION_CONTINUE, 159 | _RD.FINAL_ACTION_CONTINUE, 160 | _RD.INITIAL_ACTION_CONTINUE, 161 | _RD.FINAL_ACTION_CONTINUE, 162 | _clear_colors, 163 | 1.0, 164 | 0, 165 | Rect2()) 166 | else: 167 | draw_list = _RD.call("draw_list_begin", 168 | _p_framebuffer, 169 | _RD.DRAW_IGNORE_ALL, 170 | _clear_colors, 171 | 1.0, 172 | 0, 173 | Rect2(), 174 | 0) 175 | 176 | _RD.draw_list_bind_render_pipeline(draw_list, _p_render_pipeline) 177 | _RD.draw_list_bind_vertex_array(draw_list, _p_vertex_array) 178 | 179 | # Hacky stuff to get the target node 180 | if target_node_unique_name: 181 | var tree := Engine.get_main_loop() as SceneTree 182 | var root : Node = tree.edited_scene_root if Engine.is_editor_hint() else tree.current_scene 183 | var node_3d : Node3D = root.get_node("%"+target_node_unique_name) 184 | transform = node_3d.global_transform 185 | 186 | # Setup model view projection, accounting for VR rendering with multiview 187 | var MVPs : Array[Projection] 188 | var buffer := PackedFloat32Array() 189 | var sizeof_float := 4 190 | buffer.resize(view_count * 16 * sizeof_float) 191 | for view in range(0, view_count): 192 | var MVP : Projection = render_scene_data.get_view_projection(view) 193 | 194 | # A little something to allow Godot 4.3 beta to work. 4.3 beta 3 fixed this 195 | if "4.3-beta" in Engine.get_version_info().string: 196 | MVP = Projection.create_depth_correction(true) * MVP 197 | 198 | MVP *= Projection(render_scene_data.get_cam_transform().inverse() * transform) 199 | MVPs.append(MVP) 200 | 201 | for i in range(0,16): 202 | buffer[i + view * 16] = MVPs[view][i/4][i%4] 203 | 204 | # Send data to our shader 205 | var buffer_bytes : PackedByteArray = buffer.to_byte_array() 206 | var p_uniform_buffer : RID = _RD.uniform_buffer_create(buffer_bytes.size(), buffer_bytes) 207 | 208 | var uniforms = [] 209 | var uniform := RDUniform.new() 210 | uniform.binding = 0 211 | uniform.uniform_type = _RD.UNIFORM_TYPE_UNIFORM_BUFFER 212 | uniform.add_id(p_uniform_buffer) 213 | uniforms.push_back( uniform ) 214 | 215 | # Uniform set from last frame needs to be freed 216 | if _p_render_pipeline_uniform_set.is_valid(): 217 | _RD.free_rid(_p_render_pipeline_uniform_set) 218 | 219 | # Bind the new uniform set 220 | _p_render_pipeline_uniform_set = _RD.uniform_set_create(uniforms, _p_shader, 0) 221 | _RD.draw_list_bind_uniform_set(draw_list, _p_render_pipeline_uniform_set, 0) 222 | 223 | # Draw it! 224 | _RD.draw_list_draw(draw_list, false, 1) 225 | 226 | _RD.draw_list_end() 227 | 228 | _RD.draw_command_end_label() 229 | 230 | func _notification(what): 231 | if what == NOTIFICATION_PREDELETE: # Cleanup 232 | if _p_render_pipeline.is_valid(): 233 | _RD.free_rid(_p_render_pipeline) 234 | if _p_shader.is_valid(): 235 | _RD.free_rid(_p_shader) 236 | if _p_vertex_array.is_valid(): 237 | _RD.free_rid(_p_vertex_array) 238 | if _p_vertex_buffer.is_valid(): 239 | _RD.free_rid(_p_vertex_buffer) 240 | if _p_render_pipeline_uniform_set.is_valid(): 241 | _RD.free_rid(_p_render_pipeline_uniform_set) 242 | if _p_framebuffer.is_valid(): 243 | _RD.free_rid(_p_framebuffer) 244 | 245 | var _vertex_buffer := PackedFloat32Array([ 246 | -0.5,-0.288675,0, 1,0,0,1, 247 | 0.5,-0.288675,0, 0,1,0,1, 248 | 0,0.57735,0, 0,0,1,1, 249 | ]) 250 | 251 | const _default_source_vertex = " 252 | #version 450 253 | 254 | #extension GL_EXT_multiview : enable 255 | 256 | layout(location = 0) in vec3 a_Position; 257 | layout(location = 1) in vec4 a_Color; 258 | 259 | layout(set = 0, binding = 0) uniform UniformBufferObject { 260 | mat4 MVP[2]; 261 | }; 262 | 263 | layout(location = 2) out vec4 v_Color; 264 | 265 | void main(){ 266 | v_Color = a_Color; 267 | 268 | gl_Position = MVP[gl_ViewIndex] * vec4(a_Position, 1); 269 | } 270 | " 271 | 272 | const _default_source_fragment = " 273 | #version 450 274 | 275 | layout(location = 2) in vec4 a_Color; 276 | 277 | layout(location = 0) out vec4 frag_color; // Bound to buffer index 0 278 | 279 | void main(){ 280 | frag_color = a_Color; 281 | } 282 | " 283 | -------------------------------------------------------------------------------- /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/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 | --------------------------------------------------------------------------------