├── Scripts ├── fps.gd.uid ├── camera.gd.uid ├── cb_v_sync.gd.uid ├── renderer_label.gd.uid ├── spin_lights.gd.uid ├── faux_gi_status_label.gd.uid ├── renderer_label.gd ├── cb_v_sync.gd ├── faux_gi_status_label.gd ├── spin_lights.gd ├── fps.gd └── camera.gd ├── O1S_FauxGI ├── faux_gi.gd.uid ├── faux_gi.tscn └── faux_gi.gd ├── .editorconfig ├── .gitignore ├── .gitattributes ├── O1S_Logo.svg.import ├── project.godot ├── LICENSE ├── README.md ├── export_presets.cfg ├── O1S_Logo.svg └── Scenes └── scene_3d.tscn /Scripts/fps.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dpjhlb182qd2e 2 | -------------------------------------------------------------------------------- /O1S_FauxGI/faux_gi.gd.uid: -------------------------------------------------------------------------------- 1 | uid://oosew8x85qnq 2 | -------------------------------------------------------------------------------- /Scripts/camera.gd.uid: -------------------------------------------------------------------------------- 1 | uid://d06yfe050b2oh 2 | -------------------------------------------------------------------------------- /Scripts/cb_v_sync.gd.uid: -------------------------------------------------------------------------------- 1 | uid://b74n28fy47qj1 2 | -------------------------------------------------------------------------------- /Scripts/renderer_label.gd.uid: -------------------------------------------------------------------------------- 1 | uid://h3bi55a3ev55 2 | -------------------------------------------------------------------------------- /Scripts/spin_lights.gd.uid: -------------------------------------------------------------------------------- 1 | uid://b4hkm2djkjy16 2 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | -------------------------------------------------------------------------------- /Scripts/faux_gi_status_label.gd.uid: -------------------------------------------------------------------------------- 1 | uid://bu0i1okglx0ic 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Godot 4+ specific ignores 2 | .godot/ 3 | /android/ 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Normalize EOL for all files that Git considers text files. 2 | * text=auto eol=lf 3 | -------------------------------------------------------------------------------- /Scripts/renderer_label.gd: -------------------------------------------------------------------------------- 1 | extends Label 2 | 3 | func _ready(): 4 | text = RenderingServer.get_current_rendering_method() 5 | -------------------------------------------------------------------------------- /Scripts/cb_v_sync.gd: -------------------------------------------------------------------------------- 1 | extends CheckButton 2 | 3 | func _on_toggled( toggled_on ): 4 | DisplayServer.window_set_vsync_mode( DisplayServer.VSYNC_ENABLED 5 | if toggled_on else DisplayServer.VSYNC_DISABLED ) 6 | -------------------------------------------------------------------------------- /Scripts/faux_gi_status_label.gd: -------------------------------------------------------------------------------- 1 | extends Label 2 | @onready var faux_gi = $"../../FauxGI" 3 | 4 | func _process( _delta ): 5 | if faux_gi.bounce_gain >= 0.01: 6 | text = str( faux_gi.active_VPLs ) + " VPLs, " + str( faux_gi.active_VDLs ) + " VDLs, gain = " + str( faux_gi.bounce_gain ) 7 | else: 8 | text = "FauxGI Disabled" 9 | -------------------------------------------------------------------------------- /Scripts/spin_lights.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends Node3D 3 | 4 | #var rd : RenderingDevice 5 | #func _init(): 6 | #rd = RenderingServer.get_rendering_device() 7 | #rd.buffer_get_data() 8 | 9 | @export var speed := 1.0 10 | var rot := -2.35 11 | func _process( delta : float ): 12 | if true: 13 | rot += delta * speed 14 | if rot >= 2.0*PI: 15 | rot -= 2 * PI 16 | elif rot < 0.0: 17 | rot += 2 * PI 18 | else: 19 | rot = -2.35 20 | transform.basis = Basis.from_euler( Vector3( 0, rot, 0 ) ) 21 | 22 | #var tex = get_viewport().get_viewport_rid() 23 | -------------------------------------------------------------------------------- /O1S_FauxGI/faux_gi.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=4 format=3 uid="uid://b3vmtf81ju271"] 2 | 3 | [ext_resource type="Script" uid="uid://oosew8x85qnq" path="res://O1S_FauxGI/faux_gi.gd" id="1_kxh3g"] 4 | 5 | [sub_resource type="StandardMaterial3D" id="StandardMaterial3D_gpxcq"] 6 | shading_mode = 0 7 | vertex_color_use_as_albedo = true 8 | 9 | [sub_resource type="ImmediateMesh" id="ImmediateMesh_gpxcq"] 10 | 11 | [node name="FauxGI" type="Node3D"] 12 | script = ExtResource("1_kxh3g") 13 | 14 | [node name="RaycastDebug" type="MeshInstance3D" parent="."] 15 | material_override = SubResource("StandardMaterial3D_gpxcq") 16 | cast_shadow = 0 17 | ignore_occlusion_culling = true 18 | mesh = SubResource("ImmediateMesh_gpxcq") 19 | -------------------------------------------------------------------------------- /O1S_Logo.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://b14pus72atr7j" 6 | path="res://.godot/imported/O1S_Logo.svg-27a3260e1bf8a1a1e1537a554be9956e.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://O1S_Logo.svg" 14 | dest_files=["res://.godot/imported/O1S_Logo.svg-27a3260e1bf8a1a1e1537a554be9956e.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=1.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /project.godot: -------------------------------------------------------------------------------- 1 | ; Engine configuration file. 2 | ; It's best edited using the editor UI and not directly, 3 | ; since the parameters that go here are not all obvious. 4 | ; 5 | ; Format: 6 | ; [section] ; section goes between [] 7 | ; param=value ; assign values to parameters 8 | 9 | config_version=5 10 | 11 | [application] 12 | 13 | config/name="FauxGI" 14 | run/main_scene="res://Scenes/scene_3d.tscn" 15 | config/features=PackedStringArray("4.4", "GL Compatibility") 16 | boot_splash/show_image=false 17 | config/icon="uid://b14pus72atr7j" 18 | 19 | [editor] 20 | 21 | movie_writer/movie_file="D:/O1S_Gaming/O1S_FauxGI.avi" 22 | 23 | [file_customization] 24 | 25 | folder_colors={ 26 | "res://O1S_FauxGI/": "orange", 27 | "res://Scripts/": "teal" 28 | } 29 | 30 | [physics] 31 | 32 | 3d/physics_engine="Jolt Physics" 33 | 34 | [rendering] 35 | 36 | renderer/rendering_method="gl_compatibility" 37 | renderer/rendering_method.mobile="gl_compatibility" 38 | reflections/reflection_atlas/reflection_size=128 39 | anti_aliasing/quality/msaa_3d=1 40 | anti_aliasing/quality/screen_space_aa=1 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 greg1solution 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 | -------------------------------------------------------------------------------- /Scripts/fps.gd: -------------------------------------------------------------------------------- 1 | #@tool 2 | extends Label 3 | 4 | @export var update_s := 0.25 5 | 6 | # cascaded exponential filter 7 | var gain : float = 1 8 | var filt_delta_1 : float = 1 9 | var filt_delta_2 : float = 1 10 | #var filt_delta_3 : float = 1 11 | #var filt_delta_4 : float = 1 12 | 13 | var accum_s : float = 0.0 14 | var accum_frames : int = 0 15 | func _process( delta : float ): 16 | # do the math every time 17 | filt_delta_1 = lerpf( filt_delta_1, delta, gain ) 18 | filt_delta_2 = lerpf( filt_delta_2, filt_delta_1, gain ) 19 | #filt_delta_3 = lerpf( filt_delta_3, filt_delta_2, gain ) 20 | #filt_delta_4 = lerpf( filt_delta_4, filt_delta_3, gain ) 21 | 22 | # update the text periodically 23 | accum_s += delta 24 | accum_frames += 1 25 | if accum_s >= update_s: 26 | accum_s -= update_s 27 | var fps : float = 1.0 / max( filt_delta_2, 1e-5 ) 28 | if fps >= 1000.0: 29 | text = "%1.0f fps" % fps 30 | elif fps >= 100.0: 31 | text = "%1.1f fps" % fps 32 | elif fps >= 10.0: 33 | text = "%1.2f fps" % fps 34 | else: 35 | text = "%1.3f fps" % fps 36 | # adjust the gain so we filter over ~ 1/4 s 37 | gain = clampf( 4.0 * update_s / accum_frames, 1e-5, 1.0 ) 38 | accum_frames = 0 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # O1S Faux Global Illumination for Godot 2 | 3 | ## Summary 4 | O1S_FauxGI is a lightweight approximation to global illumination for the Godot game engine. 5 | It supports all three renderers (Compatibility / Mobile / Forward+), and is tested on the 6 | latest stable version of Godot (4.4.1). 7 | 8 | Any light source with shadows enabled in the scene is considered a primary source. The first bounce is simulated 9 | by adding Virtual Point Lights. All subsequent bounces are simulated by controlling the ambient term in a 10 | WorldEnvironment node (if linked). 11 | 12 | ## Getting Started 13 | Using FauxGI is simple: 14 | 1. Copy the O1S_FauxGI folder into your own project 15 | 2. "Instantiate Child Scene.." under the top level Node, and select "faux_gi.tscn" 16 | 3. Make sure the scene's geometry has collision shapes and the lights cast shadows 17 | 4. Link a WorldEnvironment node (if present) to the `Environment Node` 18 | 19 | You can optionally play with the FauxGI settings while watching the results in the 3D preview window. 20 | 21 | ## Principles 22 | The FauxGI node takes the principles of Godot's [Faking global illumination](https://docs.godotengine.org/en/stable/tutorials/3d/global_illumination/faking_global_illumination.html) page and automates them. 23 | All light sources in the scene are scanned on _ready(). Then on every _physics_process(), each active light source in the scene can add a Virtual Point Light (VPL) to approximate the first bounce indirect light. All VPLs are then processed, weighted by energy and distance to the camera, to simulate secondary bounces using the ambient light term in the `Environment Node`. 24 | 25 | Raycasts are used to determine where to place the VPLs, hence the need for collision shapes. The number of raycasts per VPL is controlled with the `Oversample` setting (with 0 disabling the raycasts entirely, simply placing the VPL at a specific fraction of the way from the light's `Range`). The more raycasts per VPL, the better they can adapt to the (possibly changing) geometry around them. 26 | 27 | Spotlights can control more than one VPL, controlled with the `VPLs per Spot` parameter. The VPLs will all work the same way, all generated within the spotlight's cone. 28 | 29 | Omnilights look best when using at least 4 VPLs, as the the VPLs can be near multiple surfaces. If each Omnilight only has a single VPL it is much harder to get bounced light on the back of other objects in the space. Omnilights are implemented by casting their VPLs as if there were `VPLs per Omni` spotlights radiating outward. 30 | 31 | Directional lights are handled as if the camera itself were an omnilight, finding where to place the `Directional VPLs`. Those VPLs are then all checked to see if the directional lights can reach them, and if so the directional lights contribute the color and energy information to the VPLs. Currently all directional lights will share the same VPLs. 32 | 33 | FauxGI keeps a pool of VPLs, added via the RenderingServer to minimize overhead (so they do not need to interact with the scene tree). 34 | 35 | To get around the Compatibility light rendering limitations (each mesh can only support "8 Spot + 8 Omni"), FauxGI instances the Virtual Point Lights as a mixture of Omni and Spot lights with a 180 degree angle. As the VPLs do not cast shadows, this essentially doubles the number of VPLs that can be rendered per mesh. 36 | 37 | ![FGI_Demo_Image](https://github.com/user-attachments/assets/6cf08f9f-e85c-4453-b8b5-10bd06872867) 38 | -------------------------------------------------------------------------------- /export_presets.cfg: -------------------------------------------------------------------------------- 1 | [preset.0] 2 | 3 | name="Windows Desktop" 4 | platform="Windows Desktop" 5 | runnable=true 6 | advanced_options=true 7 | dedicated_server=false 8 | custom_features="" 9 | export_filter="all_resources" 10 | include_filter="" 11 | exclude_filter="" 12 | export_path=".builds/FauxGI.exe" 13 | patches=PackedStringArray() 14 | encryption_include_filters="" 15 | encryption_exclude_filters="" 16 | seed=0 17 | encrypt_pck=false 18 | encrypt_directory=false 19 | script_export_mode=2 20 | 21 | [preset.0.options] 22 | 23 | custom_template/debug="" 24 | custom_template/release="" 25 | debug/export_console_wrapper=0 26 | binary_format/embed_pck=true 27 | texture_format/s3tc_bptc=true 28 | texture_format/etc2_astc=false 29 | binary_format/architecture="x86_64" 30 | codesign/enable=false 31 | codesign/timestamp=true 32 | codesign/timestamp_server_url="" 33 | codesign/digest_algorithm=1 34 | codesign/description="" 35 | codesign/custom_options=PackedStringArray() 36 | application/modify_resources=false 37 | application/icon="" 38 | application/console_wrapper_icon="" 39 | application/icon_interpolation=4 40 | application/file_version="" 41 | application/product_version="" 42 | application/company_name="" 43 | application/product_name="" 44 | application/file_description="" 45 | application/copyright="" 46 | application/trademarks="" 47 | application/export_angle=0 48 | application/export_d3d12=0 49 | application/d3d12_agility_sdk_multiarch=true 50 | ssh_remote_deploy/enabled=false 51 | ssh_remote_deploy/host="user@host_ip" 52 | ssh_remote_deploy/port="22" 53 | ssh_remote_deploy/extra_args_ssh="" 54 | ssh_remote_deploy/extra_args_scp="" 55 | ssh_remote_deploy/run_script="Expand-Archive -LiteralPath '{temp_dir}\\{archive_name}' -DestinationPath '{temp_dir}' 56 | $action = New-ScheduledTaskAction -Execute '{temp_dir}\\{exe_name}' -Argument '{cmd_args}' 57 | $trigger = New-ScheduledTaskTrigger -Once -At 00:00 58 | $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries 59 | $task = New-ScheduledTask -Action $action -Trigger $trigger -Settings $settings 60 | Register-ScheduledTask godot_remote_debug -InputObject $task -Force:$true 61 | Start-ScheduledTask -TaskName godot_remote_debug 62 | while (Get-ScheduledTask -TaskName godot_remote_debug | ? State -eq running) { Start-Sleep -Milliseconds 100 } 63 | Unregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorAction:SilentlyContinue" 64 | ssh_remote_deploy/cleanup_script="Stop-ScheduledTask -TaskName godot_remote_debug -ErrorAction:SilentlyContinue 65 | Unregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorAction:SilentlyContinue 66 | Remove-Item -Recurse -Force '{temp_dir}'" 67 | 68 | [preset.1] 69 | 70 | name="Web" 71 | platform="Web" 72 | runnable=true 73 | advanced_options=false 74 | dedicated_server=false 75 | custom_features="" 76 | export_filter="all_resources" 77 | include_filter="" 78 | exclude_filter="" 79 | export_path=".builds/web/FauxGI.html" 80 | patches=PackedStringArray() 81 | encryption_include_filters="" 82 | encryption_exclude_filters="" 83 | seed=0 84 | encrypt_pck=false 85 | encrypt_directory=false 86 | script_export_mode=2 87 | 88 | [preset.1.options] 89 | 90 | custom_template/debug="" 91 | custom_template/release="" 92 | variant/extensions_support=false 93 | variant/thread_support=false 94 | vram_texture_compression/for_desktop=true 95 | vram_texture_compression/for_mobile=false 96 | html/export_icon=true 97 | html/custom_html_shell="" 98 | html/head_include="" 99 | html/canvas_resize_policy=2 100 | html/focus_canvas_on_start=true 101 | html/experimental_virtual_keyboard=false 102 | progressive_web_app/enabled=false 103 | progressive_web_app/ensure_cross_origin_isolation_headers=true 104 | progressive_web_app/offline_page="" 105 | progressive_web_app/display=1 106 | progressive_web_app/orientation=0 107 | progressive_web_app/icon_144x144="" 108 | progressive_web_app/icon_180x180="" 109 | progressive_web_app/icon_512x512="" 110 | progressive_web_app/background_color=Color(0, 0, 0, 1) 111 | -------------------------------------------------------------------------------- /Scripts/camera.gd: -------------------------------------------------------------------------------- 1 | #@tool 2 | class_name FreeLookCamera extends Camera3D 3 | 4 | # Modifier keys' speed multiplier 5 | const SHIFT_MULTIPLIER = 2.5 6 | const ALT_MULTIPLIER = 1.0 / SHIFT_MULTIPLIER 7 | const MAX_VEL := 10.0 # 343.0 * 2.0 # mach 2 8 | 9 | @export_range(0.0, 1.0) var sensitivity = 0.25 10 | 11 | # Mouse state 12 | var _mouse_position = Vector2(0.0, 0.0) 13 | var _total_pitch = 0.0 14 | 15 | # Movement state 16 | var _direction = Vector3.ZERO 17 | var _velocity = Vector3.ZERO 18 | var _acceleration = 30 19 | var _deceleration = -10 20 | var _vel_multiplier = MAX_VEL * 0.5 21 | 22 | # Keyboard state 23 | var _w = false 24 | var _s = false 25 | var _a = false 26 | var _d = false 27 | var _q = false 28 | var _e = false 29 | var _shift = false 30 | var _alt = false 31 | 32 | func _input(event): 33 | # Receives mouse motion 34 | if event is InputEventMouseMotion: 35 | _mouse_position = event.relative 36 | 37 | # Receives mouse button input 38 | if event is InputEventMouseButton: 39 | match event.button_index: 40 | MOUSE_BUTTON_RIGHT: # Only allows rotation if right click down 41 | Input.set_mouse_mode( 42 | Input.MOUSE_MODE_CAPTURED if event.pressed 43 | else Input.MOUSE_MODE_VISIBLE) 44 | MOUSE_BUTTON_WHEEL_UP: # Increases max velocity 45 | _vel_multiplier = clamp(_vel_multiplier * 1.1, 0.2, MAX_VEL) 46 | MOUSE_BUTTON_WHEEL_DOWN: # Decereases max velocity 47 | _vel_multiplier = clamp(_vel_multiplier / 1.1, 0.2, MAX_VEL) 48 | 49 | # Receives key input 50 | if event is InputEventKey: 51 | match event.keycode: 52 | KEY_W: 53 | _w = event.pressed 54 | KEY_S: 55 | _s = event.pressed 56 | KEY_A: 57 | _a = event.pressed 58 | KEY_D: 59 | _d = event.pressed 60 | KEY_Q: 61 | _q = event.pressed 62 | KEY_E: 63 | _e = event.pressed 64 | 65 | # Updates mouselook and movement every frame 66 | #var accum : float = 1e20 67 | #var last_basis_z : Vector3 68 | func _physics_process( _delta ): 69 | #go_above() 70 | pass 71 | 72 | func _process( delta ): 73 | _update_mouselook() 74 | _update_movement( delta ) 75 | 76 | # unpdate the terrain patch instances 77 | #if not Engine.is_editor_hint(): 78 | #var num_instances : int = \ 79 | #$"../../Geometry/O1S_GroundTruth".place_dynamic_instances( 80 | #position, -basis.z ) 81 | #$"../UI/CountLabel".text = "%d instances" % num_instances 82 | 83 | # Updates camera movement 84 | func _update_movement(delta): 85 | # Computes desired direction from key states 86 | _direction = Vector3((_d as float) - (_a as float), 87 | (_e as float) - (_q as float), 88 | (_s as float) - (_w as float)) 89 | 90 | # Computes the change in velocity due to desired direction and "drag" 91 | # The "drag" is a constant acceleration on the camera to bring it's velocity to 0 92 | var offset = _direction.normalized() * _acceleration * _vel_multiplier * delta \ 93 | + _velocity.normalized() * _deceleration * _vel_multiplier * delta 94 | 95 | # Compute modifiers' speed multiplier 96 | var speed_multi = 1 97 | if _shift: speed_multi *= SHIFT_MULTIPLIER 98 | if _alt: speed_multi *= ALT_MULTIPLIER 99 | 100 | # Checks if we should bother translating the camera 101 | if _direction == Vector3.ZERO and offset.length_squared() > _velocity.length_squared(): 102 | # Sets the velocity to 0 to prevent jittering due to imperfect deceleration 103 | _velocity = Vector3.ZERO 104 | else: 105 | # Clamps speed to stay within maximum value (_vel_multiplier) 106 | _velocity.x = clamp( _velocity.x + offset.x, -_vel_multiplier, _vel_multiplier ) 107 | _velocity.y = clamp( _velocity.y + offset.y, -_vel_multiplier, _vel_multiplier ) 108 | _velocity.z = clamp( _velocity.z + offset.z, -_vel_multiplier, _vel_multiplier ) 109 | 110 | translate( _velocity * delta * speed_multi ) 111 | 112 | var dist_above_ground = ground_plane.distance_to( position ) 113 | if dist_above_ground < 1.0: 114 | position += ground_plane.normal * (1.0 - dist_above_ground ) 115 | 116 | # Updates mouse look 117 | func _update_mouselook(): 118 | # Only rotates mouse if the mouse is captured 119 | if Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED: 120 | _mouse_position *= sensitivity 121 | var yaw = _mouse_position.x 122 | var pitch = _mouse_position.y 123 | _mouse_position = Vector2( 0, 0 ) 124 | 125 | # Prevents looking up/down too far 126 | pitch = clamp( pitch, -90 - _total_pitch, 90 - _total_pitch ) 127 | _total_pitch += pitch 128 | 129 | rotate_y( deg_to_rad( -yaw ) ) 130 | rotate_object_local( Vector3( 1,0,0 ), deg_to_rad( -pitch ) ) 131 | 132 | #var heading := wrapf( rad_to_deg( atan2( basis.z.z, basis.z.x ) ), 0.0, 360.0 ) 133 | #$"../UI/HeadingLabel".text = "Compass: %1.2f" % heading 134 | 135 | var ground_plane := Plane( Vector3( 0,1,0 ), -1e5 ) 136 | func go_above(): 137 | var space_state = get_world_3d().direct_space_state 138 | var query = PhysicsRayQueryParameters3D.create( 139 | position + Vector3( 0, 100, 0 ), position + Vector3( 0, -100, 0 ) ) 140 | #query.collide_with_areas = true 141 | var result = space_state.intersect_ray( query ) 142 | if result: 143 | ground_plane = Plane( result.normal, result.position ) 144 | else: 145 | ground_plane = Plane( Vector3( 0,1,0 ), -1e5 ) 146 | -------------------------------------------------------------------------------- /O1S_Logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 15 | 22 | 25 | 26 | 27 | 29 | 30 | 32 | image/svg+xml 33 | 35 | 36 | 37 | 38 | 39 | 42 | 51 | 55 | 59 | 63 | 67 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /Scenes/scene_3d.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=10 format=3 uid="uid://btirfsc7rf58b"] 2 | 3 | [ext_resource type="PackedScene" uid="uid://b3vmtf81ju271" path="res://O1S_FauxGI/faux_gi.tscn" id="1_q5cge"] 4 | [ext_resource type="Script" uid="uid://d06yfe050b2oh" path="res://Scripts/camera.gd" id="2_pjwey"] 5 | [ext_resource type="Script" uid="uid://b4hkm2djkjy16" path="res://Scripts/spin_lights.gd" id="3_ok1gl"] 6 | [ext_resource type="Script" uid="uid://dpjhlb182qd2e" path="res://Scripts/fps.gd" id="4_cqamk"] 7 | [ext_resource type="Script" uid="uid://b74n28fy47qj1" path="res://Scripts/cb_v_sync.gd" id="5_c5wjy"] 8 | [ext_resource type="Script" uid="uid://h3bi55a3ev55" path="res://Scripts/renderer_label.gd" id="7_pjwey"] 9 | 10 | [sub_resource type="Environment" id="Environment_q5cge"] 11 | ambient_light_source = 2 12 | ambient_light_color = Color(0.666238, 0.945608, 0.728612, 1) 13 | ambient_light_energy = 0.149056 14 | tonemap_mode = 3 15 | volumetric_fog_density = 0.271 16 | 17 | [sub_resource type="StandardMaterial3D" id="StandardMaterial3D_bshdy"] 18 | albedo_color = Color(0.428036, 0.395602, 0.919279, 1) 19 | 20 | [sub_resource type="StandardMaterial3D" id="StandardMaterial3D_6ffuc"] 21 | albedo_color = Color(1, 0.635294, 0.105882, 1) 22 | 23 | [node name="OneRoomScene3D" type="Node3D"] 24 | 25 | [node name="WorldEnvironment" type="WorldEnvironment" parent="."] 26 | environment = SubResource("Environment_q5cge") 27 | 28 | [node name="FauxGI" parent="." node_paths=PackedStringArray("top_node", "label_node", "environment_node") instance=ExtResource("1_q5cge")] 29 | top_node = NodePath("..") 30 | label_node = NodePath("../UICanvas/FauxGIStatusLabel") 31 | bounce_gain = 1.0 32 | environment_node = NodePath("../WorldEnvironment") 33 | 34 | [node name="SpinSpot" type="Node3D" parent="."] 35 | transform = Transform3D(-0.784784, 0, -0.619769, 0, 1, 0, 0.619769, 0, -0.784784, 0, 0, 0) 36 | script = ExtResource("3_ok1gl") 37 | 38 | [node name="SpotLight3D" type="SpotLight3D" parent="SpinSpot"] 39 | transform = Transform3D(0.952537, 0.131593, -0.274512, 0.304381, -0.396676, 0.866026, 0.00507104, -0.908477, -0.417903, 0.79985, 3.19763, -0.713908) 40 | light_color = Color(0.192157, 1, 0.32549, 1) 41 | light_energy = 2.0 42 | shadow_enabled = true 43 | distance_fade_enabled = true 44 | spot_range = 10.0 45 | spot_angle = 30.0 46 | 47 | [node name="OmniLight3D" type="OmniLight3D" parent="SpinSpot"] 48 | transform = Transform3D(-0.128348, 0, -0.991729, 0, 1, 0, 0.991729, 0, -0.128348, -0.711475, 1.74288, -0.629222) 49 | light_energy = 1.5 50 | shadow_enabled = true 51 | 52 | [node name="SpinDir" type="Node3D" parent="."] 53 | transform = Transform3D(-0.712174, 0, -0.702003, 0, 1, 0, 0.702003, 0, -0.712174, 0, 0, 0) 54 | script = ExtResource("3_ok1gl") 55 | speed = 0.345 56 | 57 | [node name="DirectionalLight3D" type="DirectionalLight3D" parent="SpinDir"] 58 | transform = Transform3D(0.982008, -0.188398, 0.0128751, 0.0752052, 0.452719, 0.888476, -0.173216, -0.871523, 0.458742, 0, 2, 0) 59 | light_color = Color(0.804364, 0.600976, 0.890119, 1) 60 | shadow_enabled = true 61 | directional_shadow_mode = 0 62 | directional_shadow_max_distance = 15.0 63 | directional_shadow_pancake_size = 0.0 64 | sky_mode = 1 65 | 66 | [node name="OmniLight3D" type="OmniLight3D" parent="SpinDir"] 67 | transform = Transform3D(-0.859002, 0, 0.511972, 0, 1, 0, -0.511972, 0, -0.859002, 2.056, -0.19, -0.521) 68 | light_color = Color(1, 0, 0, 1) 69 | shadow_enabled = true 70 | omni_range = 3.0 71 | 72 | [node name="Geometry" type="CSGCombiner3D" parent="."] 73 | use_collision = true 74 | 75 | [node name="CSGBox3D" type="CSGBox3D" parent="Geometry"] 76 | transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.637817, 1.77835, 0.395508) 77 | size = Vector3(6.75, 5.25, 5.75) 78 | 79 | [node name="CSGBox3D3" type="CSGBox3D" parent="Geometry/CSGBox3D"] 80 | operation = 2 81 | size = Vector3(6.5, 5, 5.5) 82 | 83 | [node name="CSGCylinder3D" type="CSGCylinder3D" parent="Geometry/CSGBox3D"] 84 | transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.59072, 0) 85 | operation = 2 86 | radius = 2.00195 87 | height = 0.474365 88 | sides = 18 89 | 90 | [node name="CSGTorus3D" type="CSGTorus3D" parent="Geometry"] 91 | transform = Transform3D(0.629602, -0.550534, 0.548191, 0.658256, 0.752794, 0, -0.412675, 0.36085, 0.836353, 0.501693, 0.64787, -0.303848) 92 | sides = 36 93 | ring_sides = 12 94 | material = SubResource("StandardMaterial3D_bshdy") 95 | 96 | [node name="CSGCylinder3D" type="CSGCylinder3D" parent="Geometry"] 97 | transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.57043, 0.998739, -0.908187) 98 | sides = 24 99 | 100 | [node name="CSGSphere3D" type="CSGSphere3D" parent="Geometry"] 101 | transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.66665, 0.410131, -1.90844) 102 | radial_segments = 24 103 | rings = 12 104 | 105 | [node name="CSGBox3D2" type="CSGBox3D" parent="Geometry"] 106 | transform = Transform3D(0.707107, 0, -0.707107, 0, 1, 0, 0.707107, 0, 0.707107, -1.53112, 0.463222, 1.48286) 107 | material = SubResource("StandardMaterial3D_6ffuc") 108 | metadata/_edit_group_ = true 109 | 110 | [node name="CSGBox3D3" type="CSGBox3D" parent="Geometry"] 111 | transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.80649, -0.955527, -2.43044) 112 | size = Vector3(15.4212, 0.1, 36.4354) 113 | 114 | [node name="UICanvas" type="CanvasLayer" parent="."] 115 | 116 | [node name="FPS" type="Label" parent="UICanvas"] 117 | offset_right = 76.0 118 | offset_bottom = 23.0 119 | text = "FPS" 120 | script = ExtResource("4_cqamk") 121 | 122 | [node name="cbVSync" type="CheckButton" parent="UICanvas"] 123 | offset_left = 1.0 124 | offset_top = 29.0 125 | offset_right = 45.0 126 | offset_bottom = 53.0 127 | button_pressed = true 128 | script = ExtResource("5_c5wjy") 129 | 130 | [node name="VSyncLabel" type="Label" parent="UICanvas"] 131 | offset_left = 52.0 132 | offset_top = 30.0 133 | offset_right = 92.0 134 | offset_bottom = 53.0 135 | text = "V-Sync" 136 | 137 | [node name="FauxGIStatusLabel" type="Label" parent="UICanvas"] 138 | anchors_preset = 1 139 | anchor_left = 1.0 140 | anchor_right = 1.0 141 | offset_left = -261.0 142 | offset_bottom = 23.0 143 | grow_horizontal = 0 144 | text = "11 VPL, 0 VDL, 0.1491 amb " 145 | horizontal_alignment = 1 146 | 147 | [node name="RendererLabel" type="Label" parent="UICanvas"] 148 | anchors_preset = 1 149 | anchor_left = 1.0 150 | anchor_right = 1.0 151 | offset_left = -262.0 152 | offset_top = 33.0 153 | offset_right = -1.0 154 | offset_bottom = 56.0 155 | grow_horizontal = 0 156 | text = "Renderer" 157 | horizontal_alignment = 1 158 | script = ExtResource("7_pjwey") 159 | 160 | [node name="Camera3D" type="Camera3D" parent="."] 161 | transform = Transform3D(0.538808, -0.388396, 0.747553, 0, 0.887378, 0.461043, -0.842429, -0.248414, 0.478126, 3.0704, 3.96238, 2.2976) 162 | script = ExtResource("2_pjwey") 163 | 164 | [connection signal="toggled" from="UICanvas/cbVSync" to="UICanvas/cbVSync" method="_on_toggled"] 165 | -------------------------------------------------------------------------------- /O1S_FauxGI/faux_gi.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends Node3D 3 | ## Faux Global Illumination 4 | ##[br][br] 5 | ## O1S Gaming 6 | ## Jonathan Dummer 7 | ## (MIT License) 8 | ## 9 | ## Using the existing lights and collision geometry, approximate GI by placing 10 | ## Virtual Point Lights (VPL) as appropriate. A primary goal is running on even the 11 | ## Compatibilty renderer, so design around those limits. (Each mesh can light 12 | ## up to 8 Omnis + 8 Spots, with a max of 32 lights active in view.) Don't use 13 | ## shadows on the VPLs. 14 | ## 15 | ## Minimal implementation, for each original... 16 | ## SpotLight3D: itself, 1+ VPL 17 | ## OmniLight3D: itself, 1+ VPL 18 | ## DirectionalLight3D: itself, 1+ VPL 19 | ## 20 | ## While the number of VPLs is limited, we can place and power them using an 21 | ## average of multiple raycast samples, so the final results are representative 22 | ## of more than a single sample. 23 | ## 24 | ## Notes: 25 | ## * we can approx an omni with a spot @ 180 deg 26 | ## - this wouldn't work with shadows (~70 deg looks OK), but... 27 | ## - so a mesh can have 16 VPLs 28 | ## - still a dark spot, normal matters 29 | ## 30 | ## for evenly distributing points in a 2D field see: 31 | ## https://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences/ 32 | ## and for going from 2D to 3D normal vectors see: 33 | ## Vector3.octahedron_decode( uv: Vector2 ) 34 | 35 | ## Set the VPLs' attenuation (1.0 is Godot standard, 2.0 is physically correct, 0.5 widens & evens out the VPL contributions) 36 | const VPL_attenuation : float = 0.75 37 | ## VPLs use omni *AND* spot (180 deg), Compatibility's per-mesh limit is "8 spot + 8 omni" 38 | const VPLs_use_spots : bool = true 39 | ## VPLs cast shadows; if you enable, slower and VPLs_use_spots should be false 40 | const VPLS_cast_shadows : bool = false 41 | ## Do we want to spawn VPLs for source lights which don't cast shadows? 42 | const include_shadowless : bool = false 43 | ## Do we want to hotkey GTRL+G to toggle FauxGI? 44 | const enable_ctrl_g : bool = true 45 | ## scales all light power 46 | const scale_all_light_energy : float = 0.25 47 | ## ignore any VPLs with negligible energy 48 | const vpl_energy_floor : float = 1e-5 49 | ## the colors for visualizing raycasts 50 | const ray_hit_color := Color.LIGHT_GREEN 51 | const ray_miss_color := Color.LIGHT_CORAL 52 | const vpl_vis_color := Color.RED 53 | 54 | @export_group( "Scene Integration" ) 55 | ## The node containing all the lights we wish to GI-ify 56 | @export var top_node : Node3D = null 57 | ## A label if we want some status text 58 | @export var label_node : Label = null 59 | ## Maximum number of Virtual Point Lights we can add to the scene to simulate GI 60 | @export_range( 1, 1024 ) var max_vpls : int = 32 61 | ## Maximum number of directional sources we can add to the scene to simulate GI 62 | @export_range( 1, 32 ) var max_directionals : int = 8 63 | ## What fraction of energy is preserved each bounce 64 | @export_range( 0.0, 1.0, 0.01 ) var bounce_gain : float = 0.25 65 | ## Visualize the raycasts? 66 | @export var show_raycasts : bool = false 67 | ## Visualize the VPLs? 68 | @export var show_vpls : bool = false 69 | 70 | @export_group( "Ambient" ) 71 | ## Should this update the "Ambient Light" in a WorldEnvironment node? 72 | @export var environment_node : WorldEnvironment = null 73 | ## How strong should this effect be 74 | @export_range( 0.0, 1.0, 0.01 ) var ambient_gain : float = 0.25 75 | ## Do we want a always-on ambient? 76 | @export_range( 0.0, 0.25, 0.001, "or_greater" ) var base_ambient_energy : float = 0.05 77 | @export_color_no_alpha var base_ambient_color : Color = Color(1,1,1) 78 | 79 | @export_group( "Filtering" ) 80 | ## What percent of oversamples use repeatable instead of changing pseudo-random 81 | ## vectors (low values require heavier temporal filtering) 82 | @export_range( 0.0, 100.0, 0.1 ) var percent_stable : float = 100.0 83 | ## Median-of-3 filtering 84 | @export var median_of_3 : bool = false 85 | ## Filter the VPLs over time (0=never update/infinite filtering, 1=instant update/no filtering) 86 | @export_range( 0.01, 1.0, 0.005, "exp") var temporal_filter : float = 0.25 87 | 88 | @export_group( "Optimization", "opt" ) 89 | ## Don't create VPLs if the source light is farther than this 90 | @export var opt_max_dist : float = 20.0 91 | ## Don't create VPLs if the source light is farther than this from the view volume 92 | @export var opt_expand_view_volume : float = 2.0 93 | ## Fade in lights' contributions as it approaches the view volume 94 | @export var opt_fade_in_dist : float = 2.0 95 | ## Reduce the number of VPLs per light as they get farther away? Can cause flickering! 96 | @export var opt_lod_vpl_count : bool = false 97 | ## Reduce the number of oversamples per VPLs as they get farther away? 98 | @export var opt_lod_oversample : bool = true 99 | 100 | @export_group( "Spot & Omni Lights" ) 101 | ## How many VPLs to generate per source SpotLight3D, 1+ 102 | @export_range( 1, 8 ) var vpls_per_spot : int = 1: 103 | set( value ): 104 | vpls_per_spot = value 105 | light_data_stale = true 106 | ## How many VPLs to generate per source OmniLight3D, 1+ (>= 4 looks good) 107 | @export_range( 1, 32 ) var vpls_per_omni : int = 4: 108 | set( value ): 109 | vpls_per_omni = value 110 | light_data_stale = true 111 | ## How many raycasts per VPL per _physics_process for spot and omni lights, 0+ 112 | @export_range( 0, 100 ) var oversample : int = 8 113 | ## place the VPL (0=at light origin, 1=at intersection) 114 | @export_range( 0.0, 1.1, 0.01 ) var placement_fraction : float = 0.5 115 | ## Offset the intersection point from the surface 116 | @export var surface_offset : float = 0.1 117 | 118 | @export_group( "Directional Lights" ) 119 | ## How many shared VPLs to approximate all DirectionalLight3D's. A value 120 | ## of 0 will use a Virtual Directional Light per source instead (which is 121 | ## a cheap but horrible approximation for indoors) 122 | @export_range( 0, 16 ) var directional_vpls : int = 1: 123 | set( value ): 124 | directional_vpls = value 125 | light_data_stale = true 126 | ## How many raycasts per VPL per _physics_process for directional lights, 1+ 127 | @export_range( 1, 100 ) var oversample_dir : int = 16 128 | ## Do we want one additional VPL in the camera's looking vector? 0=no 129 | @export var add_looking_VPL : bool = true: 130 | set( value ): 131 | add_looking_VPL = value 132 | light_data_stale = true 133 | ## Max distance for the placement of directional light bounces 134 | @export var directional_proximity : float = 20.0 135 | ## Max distance to check for directional light being intercepted 136 | @export var dir_scan_length : float = 100.0 137 | 138 | 139 | # original light sources in the scene 140 | var light_sources : Array[ Light3D ] = [] 141 | # track all (possibly noisy) VPL target data, indexed by the casting light and a sub-index 142 | var VPL_targets : Dictionary[ Light3D, Dictionary ] = {} 143 | # do we need to start fresh with the temporal data? 144 | var light_data_stale : bool = true 145 | # all directional lights share a set of VPLs, so we need a universal Key for our dictionaries 146 | var token_directional_light := DirectionalLight3D.new() # don't attach 147 | 148 | # keep a pool of our Virtual Points Lights 149 | var VPL_inst : Array[ RID ] = [] 150 | var VPL_light : Array[ RID ] = [] 151 | var last_active_VPLs : int = 0 152 | var active_VPLs : int = 0 153 | 154 | # keep a pool of our Virtual Directional Lights 155 | var VDL_inst : Array[ RID ] = [] 156 | var VDL_light : Array[ RID ] = [] 157 | var last_active_VDLs : int = 0 158 | var active_VDLs : int = 0 159 | 160 | # know where the camera is 161 | var _camera : Camera3D = null 162 | 163 | var fauxgi_time_s : float = 0.0 164 | 165 | # physics stuff 166 | var query := PhysicsRayQueryParameters3D.new() 167 | var space_state : PhysicsDirectSpaceState3D = null 168 | enum ray_storage { energy, pos, norm, rad, color, dist_frac, sort_score } 169 | enum renderer_type { forward_plus, mobile, gl_compatibility } 170 | 171 | # visualize raycasts and VPL positions for debug 172 | var raycast_hits : PackedVector3Array = [] 173 | var raycast_misses : PackedVector3Array = [] 174 | var vpl_markers : PackedVector3Array = [] 175 | @onready var draw_rays : ImmediateMesh = $RaycastDebug.mesh 176 | 177 | # info on how this is being used, these won't change during runtime 178 | var in_editor : bool = Engine.is_editor_hint() 179 | var render : renderer_type = renderer_type.get( RenderingServer.get_current_rendering_method() ) 180 | 181 | func _unhandled_key_input( event ): 182 | # add the hotkey CTRL+G to toggle global illumination 183 | if enable_ctrl_g and (event is InputEventKey): 184 | if event.pressed and (event.keycode == KEY_G) and event.is_command_or_control_pressed(): 185 | bounce_gain = 1.0 if (bounce_gain < 0.5) else 0.0 186 | get_viewport().set_input_as_handled() 187 | 188 | #func test_early_exit() -> bool: 189 | #print( "eval" ) 190 | #return true 191 | 192 | ## I need to raycast, which happens here in the physics process 193 | var rescan_in_n : int = 60 # scan for light changes every second or so 194 | var amb_dirty : bool = true 195 | func _physics_process( _delta ): 196 | var ts_in_us : int = Time.get_ticks_usec() 197 | var ts_physics : int = ts_in_us 198 | rescan_in_n -= 1 199 | if in_editor or (rescan_in_n <= 0): 200 | rescan_in_n = randi_range( 30, 90 ) 201 | allocate_VPLs( max_vpls ) 202 | allocate_VDLs( max_directionals ) 203 | scan_light_sources() 204 | #if true or test_early_exit(): 205 | #print( "scan" ) 206 | #if fauxgi_time_s > 0.02: 207 | #print( "%1.3f [s]" % fauxgi_time_s ) 208 | active_VPLs = 0 209 | active_VDLs = 0 210 | raycast_hits.clear() 211 | raycast_misses.clear() 212 | vpl_markers.clear() 213 | var vis : bool = is_visible_in_tree() 214 | if (bounce_gain >= 0.01) and vis: 215 | # do I need to refresh all light data? 216 | if light_data_stale: 217 | VPL_targets = {} 218 | # info from the camera 219 | var cam_planes := _camera.get_frustum() 220 | var cam_pos : Vector3 = _camera.global_position 221 | # now run through all active light sources in the scene 222 | var active_dir_lights : Array[ DirectionalLight3D ] 223 | for light in light_sources: 224 | var use_light : bool = (light.light_energy > 0.0) and light.is_visible_in_tree() 225 | var fade_factor : float = 1.0 226 | var vpls_for_this_light : int = 1 227 | var oversample_for_this_light : int = oversample 228 | if use_light and (light.get_class() != "DirectionalLight3D"): 229 | # see if this source is within the view volume 230 | var light_pos : Vector3 = light.global_position 231 | var dist_to_light : float = cam_pos.distance_to( light_pos ) 232 | var dist_outside_view : float = dist_to_light - opt_max_dist 233 | for plane_idx in range( 2, 6 ): 234 | dist_outside_view = max( dist_outside_view, 235 | cam_planes[ plane_idx ].distance_to( light_pos ) - opt_expand_view_volume ) 236 | if dist_outside_view > opt_fade_in_dist: 237 | use_light = false 238 | else: 239 | # fade? 240 | if dist_outside_view > 0.0: 241 | fade_factor = 1.0 - dist_outside_view / opt_fade_in_dist 242 | # Apply LOD reductions of the VPL count and/or the oversample...only reduce to 1/2 243 | vpls_for_this_light = vpls_per_omni if (light.get_class() == "OmniLight3D") else vpls_per_spot 244 | if opt_lod_vpl_count: 245 | vpls_for_this_light = roundi( lerpf( vpls_for_this_light, 1, 0.5 * dist_to_light / opt_max_dist ) ) 246 | if opt_lod_oversample and (oversample > 0): 247 | oversample_for_this_light = roundi( lerpf( oversample_for_this_light, 1, 0.5 * dist_to_light / opt_max_dist ) ) 248 | if use_light: 249 | VPL_targets.get_or_add( light, {} ) 250 | match light.get_class(): 251 | "DirectionalLight3D": active_dir_lights.push_back( light ) 252 | "OmniLight3D": process_omni( light, vpls_for_this_light, oversample_for_this_light, fade_factor ) 253 | "SpotLight3D": process_spot( light, vpls_for_this_light, oversample_for_this_light, fade_factor ) 254 | else: 255 | erase_light_data( light ) 256 | # Directional lights 257 | handle_all_directional_lights( active_dir_lights ) 258 | amb_dirty = true 259 | # done with physics raycasts 260 | ts_physics = Time.get_ticks_usec() 261 | # do something with that info 262 | filter_and_emit_VPLs() 263 | else: 264 | VPL_targets.clear() 265 | 266 | if (bounce_gain < 0.01) or (ambient_gain < 0.01) or not vis: 267 | if amb_dirty: 268 | disable_ambient_secondaries() 269 | amb_dirty = false 270 | 271 | # the user may wish to display raycasts 272 | draw_rays.clear_surfaces() 273 | if not raycast_hits.is_empty(): 274 | draw_rays.surface_begin( Mesh.PRIMITIVE_LINES ) 275 | for rce in raycast_hits: 276 | draw_rays.surface_add_vertex( rce ) 277 | draw_rays.surface_set_color( ray_hit_color ) 278 | draw_rays.surface_end() 279 | if not raycast_misses.is_empty(): 280 | draw_rays.surface_begin( Mesh.PRIMITIVE_LINES ) 281 | for rce in raycast_misses: 282 | draw_rays.surface_add_vertex( rce ) 283 | draw_rays.surface_set_color( ray_miss_color ) 284 | draw_rays.surface_end() 285 | if not vpl_markers.is_empty(): 286 | draw_rays.surface_begin( Mesh.PRIMITIVE_LINES ) 287 | for vplm in vpl_markers: 288 | draw_rays.surface_add_vertex( vplm ) 289 | draw_rays.surface_set_color( vpl_vis_color ) 290 | draw_rays.surface_end() 291 | # deactivate any VPLs that need it 292 | if active_VPLs < last_active_VPLs: 293 | for idx in range( active_VPLs, last_active_VPLs ): 294 | disable_VPL( idx ) 295 | if last_active_VPLs != active_VPLs: 296 | last_active_VPLs = active_VPLs 297 | #print( last_active_VPLs, " active VPLs" ) 298 | # same for VDLs 299 | if active_VDLs < last_active_VDLs: 300 | for idx in range( active_VDLs, last_active_VDLs ): 301 | disable_VDL( idx ) 302 | if last_active_VDLs != active_VDLs: 303 | last_active_VDLs = active_VDLs 304 | #print( last_active_VDLs, " active VDLs" ) 305 | # status? 306 | if label_node: 307 | if bounce_gain < 0.01: 308 | label_node.text = "FauxGI DISABLED" 309 | else: 310 | label_node.text = ("%d VPL, %d VDL, %1.4f amb " % 311 | [ active_VPLs, active_VDLs, ambient_energy ] ) 312 | 313 | # how long did that take me? 314 | var time_physics_s : float = (ts_physics - ts_in_us) * 1e-6 315 | var time_vpls_s : float = (Time.get_ticks_usec() - ts_physics) * 1e-6 316 | fauxgi_time_s = lerpf( fauxgi_time_s, time_physics_s + time_vpls_s, 0.1 ) 317 | 318 | var ambient_energy : float = 0.0 319 | func disable_ambient_secondaries(): 320 | if environment_node and environment_node.environment: 321 | ambient_energy = base_ambient_energy 322 | #environment_node.environment.ambient_light_source = Environment.AMBIENT_SOURCE_DISABLED 323 | environment_node.environment.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR 324 | environment_node.environment.ambient_light_color = base_ambient_color 325 | environment_node.environment.ambient_light_energy = ambient_energy 326 | 327 | # median-of-3 filtering 328 | var VPL_med_1 : Dictionary[ Light3D, Dictionary ] = {} 329 | var VPL_med_2 : Dictionary[ Light3D, Dictionary ] = {} 330 | # cascaded exponential filtering 331 | var VPL_filt_1 : Dictionary[ Light3D, Dictionary ] = {} 332 | var VPL_filt_2 : Dictionary[ Light3D, Dictionary ] = {} 333 | var VPL_filt_3 : Dictionary[ Light3D, Dictionary ] = {} 334 | 335 | func erase_light_data( light : Light3D ): 336 | VPL_targets.erase( light ) 337 | VPL_med_1.erase( light ) 338 | VPL_med_2.erase( light ) 339 | VPL_filt_1.erase( light ) 340 | VPL_filt_2.erase( light ) 341 | VPL_filt_3.erase( light ) 342 | 343 | func filter_and_emit_VPLs(): 344 | if light_data_stale: 345 | VPL_med_1.clear() 346 | VPL_med_2.clear() 347 | VPL_filt_1.clear() 348 | VPL_filt_2.clear() 349 | VPL_filt_3.clear() 350 | light_data_stale = false 351 | # cascaded exponential filtering 352 | for light in VPL_targets: 353 | if not VPL_filt_1.has( light ): 354 | VPL_med_1[ light ] = VPL_targets[ light ].duplicate( true ) 355 | VPL_med_2[ light ] = VPL_targets[ light ].duplicate( true ) 356 | VPL_filt_1[ light ] = VPL_targets[ light ].duplicate( true ) 357 | VPL_filt_2[ light ] = VPL_targets[ light ].duplicate( true ) 358 | VPL_filt_3[ light ] = VPL_targets[ light ].duplicate( true ) 359 | for idx in VPL_targets[ light ]: 360 | if not VPL_filt_1[ light ].has( idx ): 361 | VPL_med_1[ light ][ idx ] = VPL_targets[ light ][ idx ].duplicate( true ) 362 | VPL_med_2[ light ][ idx ] = VPL_targets[ light ][ idx ].duplicate( true ) 363 | VPL_filt_1[ light ][ idx ] = VPL_targets[ light ][ idx ].duplicate( true ) 364 | VPL_filt_2[ light ][ idx ] = VPL_targets[ light ][ idx ].duplicate( true ) 365 | VPL_filt_3[ light ][ idx ] = VPL_targets[ light ][ idx ].duplicate( true ) 366 | for key in VPL_targets[ light ][ idx ]: 367 | # median of 3 368 | var newval = VPL_targets[ light ][ idx ][ key ] 369 | if median_of_3: 370 | # local copies 371 | var m1 = VPL_med_1[ light ][ idx ][ key ] 372 | var m2 = VPL_med_2[ light ][ idx ][ key ] 373 | # update history 374 | VPL_med_2[ light ][ idx ][ key ] = m1 375 | VPL_med_1[ light ][ idx ][ key ] = newval 376 | # filter 377 | match typeof( newval ): 378 | TYPE_FLOAT: 379 | newval = (newval + m1 + m2 - 380 | max( max( newval, m1 ), m2 ) - 381 | min( min( newval, m1 ), m2 ) ) 382 | TYPE_VECTOR3: 383 | newval = (newval + m1 + m2 - 384 | newval.max( m1 ).max( m2 ) - 385 | newval.min( m1 ).min( m2 ) ) 386 | # cascade in reverse order 387 | VPL_filt_3[ light ][ idx ][ key ] = lerp( 388 | VPL_filt_3[ light ][ idx ][ key ], 389 | newval, #VPL_targets[ light ][ idx ][ key ], 390 | temporal_filter ) 391 | VPL_filt_2[ light ][ idx ][ key ] = lerp( 392 | VPL_filt_2[ light ][ idx ][ key ], 393 | VPL_filt_3[ light ][ idx ][ key ], 394 | temporal_filter ) 395 | VPL_filt_1[ light ][ idx ][ key ] = lerp( 396 | VPL_filt_1[ light ][ idx ][ key ], 397 | VPL_filt_2[ light ][ idx ][ key ], 398 | temporal_filter ) 399 | # Gather all active VPLs into a convenient array 400 | var preVPLs : Array[ Dictionary ] = [] 401 | for light in VPL_targets: 402 | for idx in VPL_targets[ light ]: 403 | if VPL_filt_1[ light ][ idx ][ ray_storage.energy ] > vpl_energy_floor: 404 | # get an actual copy, then modify that copy 405 | var preVPL : Dictionary = VPL_filt_1[ light ][ idx ].duplicate( true ) 406 | # directional VPLs already have a color, but the token directional light does not 407 | preVPL.get_or_add( ray_storage.color, light.light_color ) 408 | # how do we want to sort? 409 | #preVPL[ ray_storage.sort_score ] = (preVPL[ ray_storage.energy ] 410 | #/( 1.0 + preVPL[ ray_storage.pos ].distance_squared_to( _camera.global_position ) ) ) 411 | preVPL[ ray_storage.sort_score ] = -preVPL[ ray_storage.pos ].distance_squared_to( _camera.global_position ) 412 | # keep it 413 | preVPLs.push_back( preVPL ) 414 | # and do we want to modify ambient to simulate secondary+ bounces? 415 | if environment_node and environment_node.environment and (ambient_gain > 0.0): 416 | var global_color := base_ambient_color * base_ambient_energy 417 | var global_energy : float = base_ambient_energy 418 | for preVPL in preVPLs: 419 | var r : float = 1.0 * preVPL[ ray_storage.rad ] 420 | var d : float = _camera.global_position.distance_to( preVPL[ ray_storage.pos ] ) 421 | if d < r: 422 | var e : float = preVPL[ ray_storage.energy ] * sqrt(1.0 - d / r) * ambient_gain 423 | global_color += preVPL[ ray_storage.color ] * e 424 | global_energy += e 425 | # and in case we are updating the environmental ambient... 426 | environment_node.environment.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR 427 | environment_node.environment.ambient_light_color = global_color if (global_energy == 0.0) else (global_color / global_energy) 428 | ambient_energy = global_energy 429 | environment_node.environment.ambient_light_energy = ambient_energy 430 | #print( global_energy ) 431 | 432 | # Do we need a second pass to filter out the top N VPLs? 433 | if preVPLs.size() > max_vpls: 434 | # sort most to least energetic 435 | preVPLs.sort_custom( func(a, b): return a[ ray_storage.sort_score ] > b[ ray_storage.sort_score ] ) 436 | # do I want to average all the lights that will get cut? 437 | if false: 438 | var avg_VPL : Dictionary = preVPLs[ 0 ] 439 | for key in avg_VPL: 440 | avg_VPL[ key ] *= 0.0 441 | var sum_energy : float = 0.0 442 | for idx in range( max_vpls - 1, preVPLs.size() ): 443 | var e : float = preVPLs[ idx ][ ray_storage.energy ] 444 | sum_energy += e; 445 | for key in preVPLs[ idx ]: 446 | avg_VPL[ key ] += preVPLs[ idx ][ key ] * e 447 | var gain : float = 1.0 / sum_energy 448 | for key in avg_VPL: 449 | avg_VPL[ key ] *= gain 450 | avg_VPL[ ray_storage.energy ] = sum_energy / (preVPLs.size() + 1 - max_vpls) 451 | # just throw away the rest 452 | preVPLs.resize( max_vpls ) 453 | # debug? 454 | if show_vpls: 455 | for preVPL in preVPLs: 456 | var mid = to_local( preVPL[ ray_storage.pos ] ) 457 | var r = preVPL[ ray_storage.rad ] * 0.1 458 | vpl_markers.push_back( mid + Vector3( -r,0,0 ) ) 459 | vpl_markers.push_back( mid + Vector3( +r,0,0 ) ) 460 | vpl_markers.push_back( mid + Vector3( 0,-r,0 ) ) 461 | vpl_markers.push_back( mid + Vector3( 0,+r,0 ) ) 462 | vpl_markers.push_back( mid + Vector3( 0,0,-r ) ) 463 | vpl_markers.push_back( mid + Vector3( 0,0,+r ) ) 464 | # finally add the VPLs 465 | active_VPLs = 0 466 | for preVPL in preVPLs: 467 | config_VPL( active_VPLs, 468 | preVPL[ ray_storage.pos ], 469 | preVPL[ ray_storage.color ], 470 | preVPL[ ray_storage.energy ], 471 | preVPL[ ray_storage.rad ] ) 472 | active_VPLs += 1 473 | 474 | func process_ray_dummy( from : Vector3, to : Vector3, done_frac : float = 0.5 ) -> Dictionary: 475 | # This function is only called if Oversample is 0, i.e. no raycasts 476 | var res_light : Dictionary 477 | var total_d : float = from.distance_to( to ) 478 | var done_d : float = total_d * done_frac 479 | res_light[ ray_storage.pos ] = lerp( from, to, done_frac * placement_fraction ) 480 | res_light[ ray_storage.norm ] = to.direction_to( from ) # just say the normal is back along this ray 481 | res_light[ ray_storage.rad ] = (total_d - 0.5 * done_d) * 1.25 482 | res_light[ ray_storage.energy ] = sqrt( 1.0 - done_frac ) 483 | return res_light 484 | 485 | var collisions : Dictionary = {} 486 | func raycast( from : Vector3, to : Vector3 ) -> Dictionary: 487 | # do the ray cast 488 | query.from = from 489 | query.to = to 490 | var res_ray := space_state.intersect_ray( query ) 491 | # draw it? 492 | if show_raycasts: 493 | if res_ray: 494 | raycast_hits.push_back( to_local( from ) ) 495 | raycast_hits.push_back( to_local( res_ray.position ) ) 496 | else: 497 | raycast_misses.push_back( to_local( from ) ) 498 | raycast_misses.push_back( to_local( to ) ) 499 | return res_ray 500 | 501 | func process_single_ray( from : Vector3, to : Vector3 ) -> Dictionary: 502 | # 503 | var res_light : Dictionary 504 | var res_ray := raycast( from, to ) 505 | if res_ray: 506 | var total_d : float = from.distance_to( to ) 507 | var done_d : float = from.distance_to( res_ray.position ) 508 | var done_frac : float = done_d / maxf( total_d, 1e-6 ) 509 | res_light[ ray_storage.pos ] = lerp( from, to, done_frac * placement_fraction ) 510 | res_light[ ray_storage.norm ] = res_ray.normal 511 | res_light[ ray_storage.rad ] = (total_d - 0.5 * done_d) * 1.25 512 | res_light[ ray_storage.energy ] = sqrt( 1.0 - done_frac ) \ 513 | * absf( res_ray.normal.dot( from.direction_to( to ) ) ) 514 | return res_light 515 | 516 | func process_rays_average( from : Vector3, rays : PackedVector3Array ) -> Dictionary: 517 | var avg_ray_res : Dictionary 518 | var sum_energy : float = 0.0 519 | for ray in rays: 520 | var ray_res := process_single_ray( from, from + ray) 521 | if ray_res: 522 | # weight by energy 523 | var e = ray_res[ ray_storage.energy ] 524 | if e > 0.0: 525 | sum_energy += e 526 | ray_res[ ray_storage.pos ] *= e 527 | ray_res[ ray_storage.norm ] *= e 528 | ray_res[ ray_storage.rad ] *= e 529 | if not avg_ray_res: 530 | avg_ray_res = ray_res 531 | else: 532 | for key in ray_res: 533 | avg_ray_res[ key ] += ray_res[ key ] 534 | if avg_ray_res: 535 | # divide by sum_energy, which had to be > 0 to create avg_ray_res 536 | var gain = 1.0 / sum_energy 537 | avg_ray_res[ ray_storage.norm ] *= gain 538 | avg_ray_res[ ray_storage.pos ] *= gain 539 | avg_ray_res[ ray_storage.pos ] += avg_ray_res[ ray_storage.norm ] * (surface_offset * placement_fraction) 540 | avg_ray_res[ ray_storage.rad ] *= gain 541 | # and the energy itself is scaled by the number of samples 542 | avg_ray_res[ ray_storage.energy ] = sum_energy / rays.size() 543 | return avg_ray_res 544 | 545 | #func raycast_average( rays_from_to : PackedVector3Array, 546 | #target : Vector3, look : Vector3, 547 | #dist : float ) -> Dictionary: 548 | #var avg_ray_res : Dictionary 549 | #var sum_score : float = 0.0 550 | #for ray_idx in range( 0, rays_from_to.size(), 2 ): 551 | #var res_ray := raycast( rays_from_to[ ray_idx ], rays_from_to[ ray_idx + 1 ] ) 552 | #if res_ray: 553 | #var score : float = lerpf( 1.0, 0.1, clamp( 554 | #target.distance_to( res_ray.position ) / dist, 0.0, 1.0 ) ) 555 | #score *= max( 0.0, look.dot( res_ray.position - target ) ) 556 | #if (score > 0.0) and (look.dot( res_ray.normal ) < 0.0): 557 | #sum_score += score 558 | #if avg_ray_res: 559 | #avg_ray_res[ ray_storage.pos ] += res_ray.position * score 560 | #avg_ray_res[ ray_storage.norm ] += res_ray.normal * score 561 | #else: 562 | #avg_ray_res[ ray_storage.pos ] = res_ray.position * score 563 | #avg_ray_res[ ray_storage.norm ] = res_ray.normal * score 564 | #if avg_ray_res: 565 | #avg_ray_res[ ray_storage.energy ] = 2.0 * sum_score / rays_from_to.size() 566 | #avg_ray_res[ ray_storage.pos ] /= sum_score 567 | #avg_ray_res[ ray_storage.norm ] /= sum_score 568 | ## debug 569 | #$DirCastDebug.global_position = avg_ray_res[ ray_storage.pos ] 570 | #return avg_ray_res 571 | 572 | func jitter_ray_angle( ray : Vector3, N : int, deg : float, 573 | percent_quasirandom : float = percent_stable ) -> PackedVector3Array: 574 | var rays : PackedVector3Array = [] 575 | var length : float = -ray.length() 576 | var xform := Quaternion( Vector3(0,0,-1), ray ) 577 | var rand_thresh : float = N * percent_quasirandom * 0.01 578 | var samp_2d : Vector2 579 | for samp in range( 0, N ): 580 | if (samp >= rand_thresh): 581 | samp_2d = ( Vector2( randf_range(-0.5, 0.5), randf_range(-0.5, 0.5) ) * 582 | (deg / 120.0) + Vector2(0.5,0.5)) 583 | else: 584 | samp_2d = qrnd_distrib( samp, deg / 120.0 ) 585 | rays.push_back( (xform * Vector3.octahedron_decode( samp_2d )) * length ) 586 | return rays 587 | 588 | var cached_omni_rays : PackedVector3Array = [] 589 | func distribute_omni_rays( N : int ) -> PackedVector3Array: 590 | if N != cached_omni_rays.size(): 591 | cached_omni_rays.clear() 592 | var remaining : int = 0 593 | match abs( N ): 594 | 0: # do nothing 595 | pass 596 | 1: # this is a single point 597 | cached_omni_rays.append( Vector3.DOWN ) 598 | 2: # up and down 599 | const _two_dirs : Array[ Vector3 ] = [ 600 | Vector3(0,+1,0), Vector3(0,-1,0) ] 601 | cached_omni_rays.append_array( _two_dirs ) 602 | 3: # 3 points in a plane (no up/down) 603 | const _three_dirs : Array[ Vector3 ] = [ 604 | Vector3(1,0,0), Vector3(-0.6,0,0.8), Vector3(-0.6,0,-0.8) ] 605 | cached_omni_rays.append_array( _three_dirs ) 606 | 4, 5: # 4 faces of a tetrahedron, decent 607 | const _tds : float = 1.0 / sqrt( 3.0 ) 608 | const _tet_dirs : Array[ Vector3 ] = [ 609 | Vector3(1,1,-1) * _tds, Vector3(1,-1,1) * _tds, 610 | Vector3(-1,1,1) * _tds, Vector3(-1,-1,-1) * _tds ] 611 | cached_omni_rays.append_array( _tet_dirs ) 612 | if N > 4: 613 | cached_omni_rays.append( Vector3.ONE ) 614 | 6, 7: # 6 faces of a cube (better), plus more if needed 615 | const _cube_dirs : Array[ Vector3 ] = [ 616 | Vector3(0,0,+1), Vector3(0,0,-1), 617 | Vector3(0,+1,0), Vector3(0,-1,0), 618 | Vector3(+1,0,0), Vector3(-1,0,0) ] 619 | cached_omni_rays.append_array( _cube_dirs ) 620 | # add extras randomly 621 | remaining = N - 6 622 | _: # 8 points of a cube, plus extra 623 | for i in range(-1,2,2): # -1,1 624 | for j in range(-1,2,2): # -1,1 625 | for k in range(-1,2,2): # -1,1 626 | cached_omni_rays.append( Vector3(i,j,k).normalized() ) 627 | # add extras randomly 628 | remaining = N - 8 629 | # we we need any extra? 630 | for i in remaining: 631 | cached_omni_rays.push_back( Vector3.octahedron_decode( 632 | qrnd_distrib( i * 17 + 33 ) ) ) 633 | return cached_omni_rays.duplicate() 634 | 635 | func process_rays_angle( from : Vector3, to : Vector3, N : int, deg : float ) -> Dictionary: 636 | if N > 0: 637 | var rays := jitter_ray_angle( to - from, N, deg ) 638 | return process_rays_average( from, rays ) 639 | else: 640 | return process_ray_dummy( from, to, 0.5 ) 641 | 642 | func update_light_target( light : Light3D, idx : int, data : Dictionary, modulate : float ): 643 | if light and data: 644 | # scale in the actual light energy here 645 | data[ ray_storage.energy ] *= modulate * light.light_indirect_energy 646 | VPL_targets[ light ][ idx ] = data 647 | 648 | func zero_light_target( light : Light3D, idx : int ): 649 | if light and VPL_targets.has( light ) and VPL_targets[ light ].has( idx ): 650 | VPL_targets[ light ][ idx ][ ray_storage.energy ] = 0.0 651 | 652 | func process_light_rays( light : Light3D, rays : PackedVector3Array, local_oversample : int, angle_deg : float, modulate : float ): 653 | for ray_idx in rays.size(): 654 | var ray_res := process_rays_angle( 655 | light.global_position, 656 | light.global_position + rays[ ray_idx ], 657 | local_oversample, angle_deg ) 658 | if ray_res: 659 | update_light_target( light, ray_idx, ray_res, modulate ) 660 | active_VPLs += 1 661 | else: 662 | zero_light_target( light, ray_idx ) 663 | 664 | func process_directional_light_rays( lights : Array[ DirectionalLight3D ], 665 | base_rays : PackedVector3Array, 666 | angle_deg : float, modulate : float ): 667 | # the first ray may be special 668 | var jitter_angle : float = 45.0 if add_looking_VPL else angle_deg 669 | for ray_idx in base_rays.size(): 670 | # oversample each base ray 671 | var N : int = max( 1, oversample_dir ) 672 | var sum_color := Color(0,0,0,0) 673 | var sum_position := Vector3.ZERO 674 | var sum_normal := Vector3.ZERO 675 | var sum_energy : float = 0.0 676 | var rays := jitter_ray_angle( base_rays[ ray_idx ], N, jitter_angle ) 677 | jitter_angle = angle_deg # for next time 678 | for ray in rays: 679 | # I don't want these raycasts draw, even in debug 680 | query.from = _camera.global_position 681 | query.to = _camera.global_position + ray 682 | var res := space_state.intersect_ray( query ) 683 | if res: 684 | var norm : Vector3 = res.normal 685 | # we hit something, now see if the directional lights can hit it too 686 | for light in lights: 687 | var e : float = norm.dot( light.global_basis.z ) * light.light_energy 688 | if e > 0.0: 689 | # do a raycast to make sure the directional light doesn't hit anything 690 | var pos : Vector3 = lerp( _camera.global_position, res.position, 0.875 ) 691 | if not raycast( pos + light.global_basis.z * 0.001, 692 | pos + light.global_basis.z * dir_scan_length ): 693 | sum_energy += e 694 | sum_color += light.light_color * e 695 | sum_position += pos * e 696 | sum_normal += norm * e 697 | if sum_energy > 0.0: 698 | var light_entry : Dictionary = {} 699 | light_entry[ ray_storage.energy ] = sum_energy / N 700 | light_entry[ ray_storage.pos ] = lerp( _camera.global_position, 701 | sum_position / sum_energy, placement_fraction ) 702 | light_entry[ ray_storage.norm ] = sum_normal / sum_energy 703 | light_entry[ ray_storage.color ] = sum_color / sum_energy 704 | #print( light_entry[ ray_storage.color ] ) 705 | light_entry[ ray_storage.rad ] = directional_proximity * 2.0 706 | update_light_target( token_directional_light, ray_idx, light_entry, modulate ) 707 | active_VPLs += 1 708 | else: 709 | zero_light_target( token_directional_light, ray_idx ) 710 | 711 | func handle_all_directional_lights( lights : Array[ DirectionalLight3D ] ): 712 | # like an omnilight, cast rays from the camera 713 | if lights: 714 | if (directional_vpls > 0) or add_looking_VPL: 715 | VPL_targets.get_or_add( token_directional_light, {} ) 716 | 717 | var rays := distribute_omni_rays( directional_vpls ) 718 | if add_looking_VPL: # add it to the front 719 | rays.insert( 0, -_camera.global_basis.z ) 720 | for i in rays.size(): 721 | rays[i] *= directional_proximity 722 | # do the ray casts 723 | var compensate_N_pts : float = 1.0 / rays.size() 724 | var angle_deg : float = sqrt( 14400.0 / max( 1, directional_vpls ) ) 725 | process_directional_light_rays( lights, rays, angle_deg, 726 | bounce_gain * compensate_N_pts )# * scale_all_light_energy ) 727 | else: 728 | for light in lights: 729 | trivial_directional( light ) 730 | else: 731 | erase_light_data( token_directional_light ) 732 | 733 | func trivial_directional( light : Light3D ): 734 | var e : float = (light.light_energy * light.light_indirect_energy * 735 | scale_all_light_energy * bounce_gain) 736 | # directly place a Virtual Directional Light, instead of VPLs 737 | config_VDL( active_VDLs, light.global_basis.z, light.light_color, e ) 738 | active_VDLs += 1 739 | 740 | func process_spot( light : Light3D, num_vpls : int, local_oversample : int, fade_factor : float ): 741 | var compensate_N_pts : float = 1.0 / num_vpls 742 | # these rays never use real random jitter 743 | var rays := jitter_ray_angle( -light.spot_range * light.global_basis.z, 744 | num_vpls, light.spot_angle, 100.0 ) 745 | # remove any extraneous VPLs associated with this light 746 | for i in range( num_vpls, VPL_targets[ light ].size() ): 747 | VPL_targets[ light ].erase( i ) 748 | # do the work 749 | process_light_rays( light, rays, local_oversample, light.spot_angle * sqrt( compensate_N_pts ), 750 | light.light_energy * bounce_gain * scale_all_light_energy * compensate_N_pts * fade_factor ) 751 | 752 | func process_omni( light : Light3D, num_vpls : int, local_oversample : int, fade_factor : float ): 753 | # where do I want to cast rays, and size them correctly 754 | var rays := distribute_omni_rays( num_vpls ) 755 | for i in rays.size(): 756 | rays[i] *= light.omni_range 757 | # remove any extraneous VPLs associated with this light 758 | for i in range( num_vpls, VPL_targets[ light ].size() ): 759 | VPL_targets[ light ].erase( i ) 760 | # do the ray casts 761 | var compensate_N_pts : float = 1.0 / rays.size() 762 | var angle_deg : float = sqrt( 14400.0 * compensate_N_pts ) 763 | process_light_rays( light, rays, local_oversample, angle_deg, 764 | light.light_energy * bounce_gain * scale_all_light_energy * compensate_N_pts * fade_factor ) 765 | 766 | func _ready(): 767 | print( "Renderer: ", render ) 768 | # grab the camera 769 | if in_editor: 770 | # Get EditorInterface this way because it does not exist in a build 771 | var editor_interface := Engine.get_singleton( "EditorInterface" ) 772 | _camera = editor_interface.get_editor_viewport_3d().get_camera_3d() 773 | else: 774 | _camera = get_viewport().get_camera_3d() 775 | # set up for raycasts (which are all in global space) 776 | space_state = get_world_3d().direct_space_state 777 | # and get our initial set of light sources 778 | scan_light_sources() 779 | 780 | func scan_light_sources(): 781 | # find all possible source lights (they don't need to be owned, so I can get procedural ones) 782 | const sources_must_be_owned := false 783 | const recursive_search := true 784 | var source_nodes : Array[ Node ] 785 | if top_node: 786 | # the user told us where to look 787 | source_nodes = top_node.find_children("", "Light3D", recursive_search, sources_must_be_owned ) 788 | else: 789 | # no user direction, try the parent of the FauxGI node 790 | var parent = get_parent() 791 | if parent: 792 | source_nodes = parent.find_children("", "Light3D", recursive_search, sources_must_be_owned ) 793 | var new_light_sources : Array[ Light3D ] = [] 794 | if source_nodes: 795 | # store all vetted light sources 796 | for sn_light : Light3D in source_nodes: 797 | # maybe only look at lights which cast shadows 798 | if sn_light.shadow_enabled or include_shadowless: 799 | new_light_sources.push_back( sn_light ) 800 | if new_light_sources != light_sources: 801 | # something is different! 802 | light_sources = new_light_sources 803 | light_data_stale = true 804 | # report 805 | #print( "Source count is ", new_light_sources.size() ) 806 | #print( "VPL max count is ", VPL_light.size() ) 807 | 808 | func qrnd_distrib( index : int, scale_01 : float = 1.0 ) -> Vector2: 809 | const mid2d := Vector2.ONE * 0.5 810 | const golden_2d := 1.32471795724474602596 811 | const g2 := Vector2( 1.0 / golden_2d, 1.0 / golden_2d / golden_2d ) 812 | return ( ( mid2d + g2 * index ).posmod( 1.0 ) - mid2d) * scale_01 + mid2d; 813 | 814 | func _enter_tree(): 815 | allocate_VPLs( max_vpls ) 816 | allocate_VDLs( max_directionals ) 817 | 818 | func _exit_tree(): 819 | allocate_VPLs( 0 ) 820 | allocate_VDLs( 0 ) 821 | 822 | func allocate_VPLs( N : int ): 823 | if N != VPL_inst.size(): 824 | # safety first 825 | N = max( 0, min( max_vpls, N ) ) 826 | print( "VPL count ", VPL_inst.size(), " -> ", N ) 827 | # do we need to remove some RIDs? 828 | while VPL_inst.size() > N: 829 | RenderingServer.free_rid( VPL_inst.pop_back() ) 830 | while VPL_light.size() > N: 831 | RenderingServer.free_rid( VPL_light.pop_back() ) 832 | # do we need to add some RIDs? 833 | var scenario = get_world_3d().scenario 834 | while VPL_inst.size() < N: 835 | # spot is slightly slower, but alternating lets the engine render 8 omni + 8 spot per mesh 836 | var spot_instead : bool = ((VPL_inst.size() & 1) == 1) and VPLs_use_spots 837 | # each light needs an instance, attached to the scenario 838 | var instance : RID = RenderingServer.instance_create() 839 | RenderingServer.instance_set_scenario( instance, scenario ) 840 | # create and attach the light (each light needs different parameters, 841 | # so we can't just share a single light across multiple instances) 842 | var light : RID 843 | if spot_instead: 844 | light = RenderingServer.spot_light_create() 845 | else: 846 | light = RenderingServer.omni_light_create() 847 | RenderingServer.instance_set_base( instance, light ) 848 | # configure any constant parameters here 849 | RenderingServer.instance_set_visible( instance , false ) 850 | RenderingServer.light_set_param( light, RenderingServer.LIGHT_PARAM_SPECULAR, 0.0 ) 851 | if spot_instead: 852 | RenderingServer.light_set_param( light, RenderingServer.LIGHT_PARAM_SPOT_ANGLE, 180.0 ) 853 | RenderingServer.light_set_param( light, RenderingServer.LIGHT_PARAM_SPOT_ATTENUATION, VPL_attenuation ) 854 | else: 855 | RenderingServer.light_set_param( light, RenderingServer.LIGHT_PARAM_ATTENUATION, VPL_attenuation ) 856 | if VPLS_cast_shadows: 857 | if not spot_instead: 858 | if renderer_type.gl_compatibility == render: 859 | # the compatibility renderer can't do DUAL_PARABOLOID 860 | RenderingServer.light_omni_set_shadow_mode( light, 861 | RenderingServer.LIGHT_OMNI_SHADOW_CUBE ) 862 | else: 863 | # DUAL_PARABOLOID is faster, if supported 864 | RenderingServer.light_omni_set_shadow_mode( light, 865 | RenderingServer.LIGHT_OMNI_SHADOW_DUAL_PARABOLOID ) 866 | RenderingServer.light_set_shadow( light, true ) 867 | # keep a copy around 868 | VPL_inst.append( instance ) 869 | VPL_light.append( light ) 870 | 871 | func disable_VPL( index : int ): 872 | if index < VPL_inst.size(): 873 | RenderingServer.instance_set_visible( VPL_inst[ index ] , false ) 874 | 875 | func config_VPL( index : int, 876 | pos : Vector3, 877 | color : Color, 878 | energy : float, 879 | dist : float ): 880 | if index < min( VPL_inst.size(), VPL_light.size() ): 881 | # instance parameters 882 | var inst : RID = VPL_inst[ index ] 883 | RenderingServer.instance_set_visible( inst , true ) 884 | RenderingServer.instance_set_transform( inst, Transform3D( Basis.IDENTITY, pos ) ) 885 | # light parameters 886 | var light : RID = VPL_light[ index ] 887 | RenderingServer.light_set_color( light, color ) 888 | RenderingServer.light_set_param( light, RenderingServer.LIGHT_PARAM_ENERGY, energy ) 889 | RenderingServer.light_set_param( light, RenderingServer.LIGHT_PARAM_RANGE, dist ) 890 | 891 | func allocate_VDLs( N : int ): 892 | if N != VDL_inst.size(): 893 | # safety first 894 | N = max( 0, min( max_directionals, N ) ) 895 | print( "VDL count ", VDL_inst.size(), " -> ", N ) 896 | # do we need to remove some RIDs? 897 | while VDL_inst.size() > N: 898 | RenderingServer.free_rid( VDL_inst.pop_back() ) 899 | while VDL_light.size() > N: 900 | RenderingServer.free_rid( VDL_light.pop_back() ) 901 | # do we need to add some RIDs? 902 | var scenario = get_world_3d().scenario 903 | while VDL_inst.size() < N: 904 | # each light needs an instance, attached to the scenario 905 | var instance : RID = RenderingServer.instance_create() 906 | RenderingServer.instance_set_scenario( instance, scenario ) 907 | # create and attach the light (each light needs different parameters, 908 | # so we can't just share a single light across multiple instances) 909 | var light : RID = RenderingServer.directional_light_create() 910 | RenderingServer.instance_set_base( instance, light ) 911 | # configure any constant parameters here 912 | RenderingServer.instance_set_visible( instance , false ) 913 | RenderingServer.light_set_param( light, RenderingServer.LIGHT_PARAM_SPECULAR, 0.0 ) 914 | # keep a copy around 915 | VDL_inst.append( instance ) 916 | VDL_light.append( light ) 917 | 918 | func disable_VDL( index : int ): 919 | if index < VDL_inst.size(): 920 | RenderingServer.instance_set_visible( VDL_inst[ index ] , false ) 921 | 922 | func config_VDL( index : int, 923 | direction : Vector3, 924 | color : Color, 925 | energy : float ): 926 | if index < min( VDL_inst.size(), VDL_light.size() ): 927 | # instance parameters 928 | var inst : RID = VDL_inst[ index ] 929 | RenderingServer.instance_set_visible( inst , true ) 930 | RenderingServer.instance_set_transform( inst, 931 | Transform3D( Quaternion( Vector3(0,0,-1), direction ), Vector3.ZERO ) ) 932 | # light parameters 933 | var light : RID = VDL_light[ index ] 934 | RenderingServer.light_set_color( light, color ) 935 | RenderingServer.light_set_param( light, RenderingServer.LIGHT_PARAM_ENERGY, energy ) 936 | --------------------------------------------------------------------------------