diff --git a/addons/sky_3d/LICENSE.txt b/addons/sky_3d/LICENSE.txt new file mode 100644 index 0000000..8351979 --- /dev/null +++ b/addons/sky_3d/LICENSE.txt @@ -0,0 +1,26 @@ +MIT License + +Copyright (c) 2023-2025 Cory Petkovsek and Contributors +Copyright (c) 2021 J. Cuéllar + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +---- + +Third party assets have their own licenses found under assets/thirdparty. diff --git a/addons/sky_3d/README.md b/addons/sky_3d/README.md new file mode 100644 index 0000000..ff35c53 --- /dev/null +++ b/addons/sky_3d/README.md @@ -0,0 +1,3 @@ +# Sky3D + +See the README in the code repository at https://github.com/TokisanGames/Sky3D. diff --git a/addons/sky_3d/ThirdParty.md b/addons/sky_3d/ThirdParty.md new file mode 100644 index 0000000..82894de --- /dev/null +++ b/addons/sky_3d/ThirdParty.md @@ -0,0 +1,8 @@ +## Attribution for Third Party Assets + +`Milkyway.jpg` "[The Milky Way panorama](https://www.eso.org/public/images/eso0932a/)" by ESO/S. Brunier, licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/ +). +[Read more](/addons/sky_3d/assets/thirdparty/textures/milkyway/LICENSE.md) + +`MoonMap.png` Copyright (c) 2019 GPoSM, MIT License +[Read more](/addons/sky_3d/assets/thirdparty/textures/moon/LICENSE.md) diff --git a/addons/sky_3d/assets/resources/MoonRender.tscn b/addons/sky_3d/assets/resources/MoonRender.tscn new file mode 100644 index 0000000..162fb37 --- /dev/null +++ b/addons/sky_3d/assets/resources/MoonRender.tscn @@ -0,0 +1,33 @@ +[gd_scene load_steps=5 format=3 uid="uid://hyy7u72h77"] + +[ext_resource type="Shader" path="res://addons/sky_3d/shaders/SimpleMoon.gdshader" id="1_h4nhh"] +[ext_resource type="Texture2D" uid="uid://2r8ylu6rg5dp" path="res://addons/sky_3d/assets/thirdparty/textures/moon/MoonMap.png" id="2_fnh72"] + + + +[sub_resource type="ShaderMaterial" id="ShaderMaterial_5qbl8"] +render_priority = 0 +shader = ExtResource("1_h4nhh") +shader_parameter/_sun_direction = Vector3(-0.976421, -0.0982674, -0.192214) +shader_parameter/_texture = ExtResource("2_fnh72") + +[sub_resource type="SphereMesh" id="2"] + +[node name="MoonRender" type="SubViewport"] +own_world_3d = true +transparent_bg = true +msaa_3d = 3 +render_target_update_mode = 4 + +[node name="MoonTransform" type="Node3D" parent="."] + +[node name="Camera3D" type="Camera3D" parent="MoonTransform"] +transform = Transform3D(-1, 0, 3.25841e-07, 0, 1, 0, -3.25841e-07, 0, -1, 0, 0, 0) +projection = 1 +size = 2.59 + +[node name="Mesh" type="MeshInstance3D" parent="MoonTransform/Camera3D"] +transform = Transform3D(8.74228e-08, -2, -7.78829e-07, -2, -8.74228e-08, 6.77626e-20, -3.40438e-14, 7.78829e-07, -2, -4.00785e-07, 0, -1.23) +material_override = SubResource("ShaderMaterial_5qbl8") +cast_shadow = 0 +mesh = SubResource("2") diff --git a/addons/sky_3d/assets/resources/SNoise.tres b/addons/sky_3d/assets/resources/SNoise.tres new file mode 100644 index 0000000..403196e --- /dev/null +++ b/addons/sky_3d/assets/resources/SNoise.tres @@ -0,0 +1,7 @@ +[gd_resource type="NoiseTexture2D" load_steps=2 format=3 uid="uid://cfqk60lpl5ljv"] + +[sub_resource type="FastNoiseLite" id="1"] + +[resource] +seamless = true +noise = SubResource("1") diff --git a/addons/sky_3d/assets/resources/SunMoonLightFade.tres b/addons/sky_3d/assets/resources/SunMoonLightFade.tres new file mode 100644 index 0000000..1f9f2db --- /dev/null +++ b/addons/sky_3d/assets/resources/SunMoonLightFade.tres @@ -0,0 +1,5 @@ +[gd_resource type="Curve" format=3 uid="uid://70482fhm3qg7"] + +[resource] +_data = [Vector2(0, 0), 0.0, 0.0, 0, 0, Vector2(0.617886, 0), 0.0, 0.0467088, 1, 0, Vector2(0.699187, 1), 0.0, 0.0, 0, 0, Vector2(1, 1), 0.0, 0.0, 0, 0] +point_count = 4 diff --git a/addons/sky_3d/assets/textures/SkyIcon.png b/addons/sky_3d/assets/textures/SkyIcon.png new file mode 100644 index 0000000..21cf426 --- /dev/null +++ b/addons/sky_3d/assets/textures/SkyIcon.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:223f2de7d03b46ac3bfad4e6df489153a148cef6eecd94f8404eb9a8775a2367 +size 860 diff --git a/addons/sky_3d/assets/textures/SkyIcon.png.import b/addons/sky_3d/assets/textures/SkyIcon.png.import new file mode 100644 index 0000000..5e9c96c --- /dev/null +++ b/addons/sky_3d/assets/textures/SkyIcon.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bwooawylkf7f2" +path="res://.godot/imported/SkyIcon.png-7b3cbfa68354965cdbfdba8b95dacf67.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/sky_3d/assets/textures/SkyIcon.png" +dest_files=["res://.godot/imported/SkyIcon.png-7b3cbfa68354965cdbfdba8b95dacf67.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/addons/sky_3d/assets/textures/noise.jpg b/addons/sky_3d/assets/textures/noise.jpg new file mode 100644 index 0000000..2a715f5 Binary files /dev/null and b/addons/sky_3d/assets/textures/noise.jpg differ diff --git a/addons/sky_3d/assets/textures/noise.jpg.import b/addons/sky_3d/assets/textures/noise.jpg.import new file mode 100644 index 0000000..0b2a782 --- /dev/null +++ b/addons/sky_3d/assets/textures/noise.jpg.import @@ -0,0 +1,35 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://djpfuyxkryegn" +path.s3tc="res://.godot/imported/noise.jpg-b47d5d36d9abfdaec2a09c140a78ba24.s3tc.ctex" +metadata={ +"imported_formats": ["s3tc_bptc"], +"vram_texture": true +} + +[deps] + +source_file="res://addons/sky_3d/assets/textures/noise.jpg" +dest_files=["res://.godot/imported/noise.jpg-b47d5d36d9abfdaec2a09c140a78ba24.s3tc.ctex"] + +[params] + +compress/mode=2 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=true +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/addons/sky_3d/assets/textures/noise2.png b/addons/sky_3d/assets/textures/noise2.png new file mode 100644 index 0000000..d655f31 --- /dev/null +++ b/addons/sky_3d/assets/textures/noise2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6a2479230d29ea9e7e7b447eaedb12175143fae01e2ced63751b2828f9665193 +size 145149 diff --git a/addons/sky_3d/assets/textures/noise2.png.import b/addons/sky_3d/assets/textures/noise2.png.import new file mode 100644 index 0000000..cfdee0a --- /dev/null +++ b/addons/sky_3d/assets/textures/noise2.png.import @@ -0,0 +1,35 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bd7u40ws8s57f" +path.s3tc="res://.godot/imported/noise2.png-37a5cbd5451b4b9c2c18ab32099e6965.s3tc.ctex" +metadata={ +"imported_formats": ["s3tc_bptc"], +"vram_texture": true +} + +[deps] + +source_file="res://addons/sky_3d/assets/textures/noise2.png" +dest_files=["res://.godot/imported/noise2.png-37a5cbd5451b4b9c2c18ab32099e6965.s3tc.ctex"] + +[params] + +compress/mode=2 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/addons/sky_3d/assets/textures/noiseClouds.png b/addons/sky_3d/assets/textures/noiseClouds.png new file mode 100644 index 0000000..06bc4b7 --- /dev/null +++ b/addons/sky_3d/assets/textures/noiseClouds.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c7e202bec424f46e3d6183886e621c4121e0a2d120370602016b8cf9fdb2136f +size 125654 diff --git a/addons/sky_3d/assets/textures/noiseClouds.png.import b/addons/sky_3d/assets/textures/noiseClouds.png.import new file mode 100644 index 0000000..75c7159 --- /dev/null +++ b/addons/sky_3d/assets/textures/noiseClouds.png.import @@ -0,0 +1,35 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cecwdqjol0ckd" +path.s3tc="res://.godot/imported/noiseClouds.png-07511094ef8b72a49cc860b35c60d0fd.s3tc.ctex" +metadata={ +"imported_formats": ["s3tc_bptc"], +"vram_texture": true +} + +[deps] + +source_file="res://addons/sky_3d/assets/textures/noiseClouds.png" +dest_files=["res://.godot/imported/noiseClouds.png-07511094ef8b72a49cc860b35c60d0fd.s3tc.ctex"] + +[params] + +compress/mode=2 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=true +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/addons/sky_3d/assets/thirdparty/textures/milkyway/LICENSE.md b/addons/sky_3d/assets/thirdparty/textures/milkyway/LICENSE.md new file mode 100644 index 0000000..740539e --- /dev/null +++ b/addons/sky_3d/assets/thirdparty/textures/milkyway/LICENSE.md @@ -0,0 +1,40 @@ +# Attribution +`Milkyway.jpg` "[The Milky Way panorama](https://www.eso.org/public/images/eso0932a/)" by ESO/S. Brunier, licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/ +). + +`StarField.jpg` is the same image modified to hide all but the brightest stars. + +Your game credits should include attribution similar to the above with a link to the license where reasonable. + +e.g. `The Milky Way panorama by ESO/S. Brunier, licensed under CC BY 4.0 and used in original and modified forms.` + +Higher resolutions and more information are available at: https://www.eso.org/public/images/eso0932a/ + +Licensing questions answered here: https://www.eso.org/public/copyright/ + +## License +[Creative Commons Attribution 4.0 International License](https://creativecommons.org/licenses/by/4.0/) + + +## Relevant FAQs + +[From ESO](https://www.eso.org/public/copyright/) + +``` +Q: I am making an electronic game. Where should I place the credit? +A: As long as the credit is clearly visible to all users and reproduced in full, for instance in a splashscreen, all is fine. +``` + +[From CC](https://wiki.creativecommons.org/wiki/Recommended_practices_for_attribution#This_is_a_great_attribution) + +``` +This is a great attribution +"Creative Commons 10th Birthday Celebration San Francisco"(source link) by Timothy Vollmer is licensed under CC BY 4.0 (license link) + +Let’s go through TASL: +Title? "Creative Commons 10th Birthday Celebration San Francisco" +Author? "Timothy Vollmer" - linked to his profile page +Source? "Creative Commons 10th Birthday Celebration San Francisco" - linked to original Flickr page +License? "CC BY 4.0" - linked to license deed +Most importantly, this attribution reasonably includes all the relevant information provided by the author. +``` diff --git a/addons/sky_3d/assets/thirdparty/textures/milkyway/Milkyway.jpg b/addons/sky_3d/assets/thirdparty/textures/milkyway/Milkyway.jpg new file mode 100644 index 0000000..7b8e7a9 Binary files /dev/null and b/addons/sky_3d/assets/thirdparty/textures/milkyway/Milkyway.jpg differ diff --git a/addons/sky_3d/assets/thirdparty/textures/milkyway/Milkyway.jpg.import b/addons/sky_3d/assets/thirdparty/textures/milkyway/Milkyway.jpg.import new file mode 100644 index 0000000..45c6d1d --- /dev/null +++ b/addons/sky_3d/assets/thirdparty/textures/milkyway/Milkyway.jpg.import @@ -0,0 +1,35 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://c1vwcdcdvb74a" +path.s3tc="res://.godot/imported/Milkyway.jpg-a06044e407cf6953ab6e46a2f0f17f3a.s3tc.ctex" +metadata={ +"imported_formats": ["s3tc_bptc"], +"vram_texture": true +} + +[deps] + +source_file="res://addons/sky_3d/assets/thirdparty/textures/milkyway/Milkyway.jpg" +dest_files=["res://.godot/imported/Milkyway.jpg-a06044e407cf6953ab6e46a2f0f17f3a.s3tc.ctex"] + +[params] + +compress/mode=2 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=true +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/addons/sky_3d/assets/thirdparty/textures/milkyway/StarField.jpg b/addons/sky_3d/assets/thirdparty/textures/milkyway/StarField.jpg new file mode 100644 index 0000000..a865944 Binary files /dev/null and b/addons/sky_3d/assets/thirdparty/textures/milkyway/StarField.jpg differ diff --git a/addons/sky_3d/assets/thirdparty/textures/milkyway/StarField.jpg.import b/addons/sky_3d/assets/thirdparty/textures/milkyway/StarField.jpg.import new file mode 100644 index 0000000..4053f5c --- /dev/null +++ b/addons/sky_3d/assets/thirdparty/textures/milkyway/StarField.jpg.import @@ -0,0 +1,35 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bm7dot7t7u1q4" +path.s3tc="res://.godot/imported/StarField.jpg-9d224e20fcbe8fdc7ff5028dae269839.s3tc.ctex" +metadata={ +"imported_formats": ["s3tc_bptc"], +"vram_texture": true +} + +[deps] + +source_file="res://addons/sky_3d/assets/thirdparty/textures/milkyway/StarField.jpg" +dest_files=["res://.godot/imported/StarField.jpg-9d224e20fcbe8fdc7ff5028dae269839.s3tc.ctex"] + +[params] + +compress/mode=2 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=true +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/addons/sky_3d/assets/thirdparty/textures/moon/LICENSE.md b/addons/sky_3d/assets/thirdparty/textures/moon/LICENSE.md new file mode 100644 index 0000000..daf0439 --- /dev/null +++ b/addons/sky_3d/assets/thirdparty/textures/moon/LICENSE.md @@ -0,0 +1,23 @@ +MIT License + +Copyright (c) 2019 GPoSM + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +Source: https://www.highend3d.com/downloads/3d-textures/c/16k-earth-w-4k-moon-free#dialog_license diff --git a/addons/sky_3d/assets/thirdparty/textures/moon/MoonMap.png b/addons/sky_3d/assets/thirdparty/textures/moon/MoonMap.png new file mode 100644 index 0000000..8bd2fc4 --- /dev/null +++ b/addons/sky_3d/assets/thirdparty/textures/moon/MoonMap.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5eec6a4b631e9948f5c10be8b1d417bb8fca294b6321d2a993fc7198b62c384a +size 6120738 diff --git a/addons/sky_3d/assets/thirdparty/textures/moon/MoonMap.png.import b/addons/sky_3d/assets/thirdparty/textures/moon/MoonMap.png.import new file mode 100644 index 0000000..cff29f1 --- /dev/null +++ b/addons/sky_3d/assets/thirdparty/textures/moon/MoonMap.png.import @@ -0,0 +1,35 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://2r8ylu6rg5dp" +path.s3tc="res://.godot/imported/MoonMap.png-81d542c3fafff4e5b235a177af6f3221.s3tc.ctex" +metadata={ +"imported_formats": ["s3tc_bptc"], +"vram_texture": true +} + +[deps] + +source_file="res://addons/sky_3d/assets/thirdparty/textures/moon/MoonMap.png" +dest_files=["res://.godot/imported/MoonMap.png-81d542c3fafff4e5b235a177af6f3221.s3tc.ctex"] + +[params] + +compress/mode=2 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=true +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/addons/sky_3d/plugin.cfg b/addons/sky_3d/plugin.cfg new file mode 100644 index 0000000..307189f --- /dev/null +++ b/addons/sky_3d/plugin.cfg @@ -0,0 +1,7 @@ +[plugin] + +name="Sky3D" +description="Atmospheric Day/Night Cycle" +author="J. Cuéllar, Cory Petkovsek, Contributors" +version="2.0" +script="src/Plugin.gd" diff --git a/addons/sky_3d/shaders/AtmFog.gdshader b/addons/sky_3d/shaders/AtmFog.gdshader new file mode 100644 index 0000000..0f82693 --- /dev/null +++ b/addons/sky_3d/shaders/AtmFog.gdshader @@ -0,0 +1,212 @@ +// Copyright (c) 2023-2025 Cory Petkovsek and Contributors +// Copyright (c) 2021 J. Cuellar + +shader_type spatial; +render_mode blend_mix, cull_disabled, unshaded; + +uniform vec2 _color_correction_params; + +uniform float _fog_density = .0001; +uniform float _fog_rayleigh_depth = .115; +uniform float _fog_mie_depth = 0.; +uniform float _fog_falloff = 3.; +uniform float _fog_start = 0.; +uniform float _fog_end = 1000.; +uniform vec3 _sun_direction = vec3(.25, -.25, .25); +uniform vec3 _moon_direction = vec3(.45, -.25, .45); + +uniform float _atm_darkness = .5; +uniform float _atm_sun_intensity = 30.; +uniform vec4 _atm_day_tint: source_color = vec4(.78, .85, .98, 1.); +uniform vec4 _atm_horizon_light_tint: source_color = vec4(.98, .73, .49, 1.); +uniform vec4 _atm_night_tint: source_color = vec4(.16, .2, .25, 1.); +uniform vec3 _atm_level_params = vec3(1., 0., 0.); +uniform float _atm_thickness = .7; + +uniform vec3 _atm_beta_ray; +uniform vec3 _atm_beta_mie; + +uniform vec3 _atm_sun_partial_mie_phase; +uniform vec4 _atm_sun_mie_tint: source_color = vec4(1.); +uniform float _atm_sun_mie_intensity = 1.; + +uniform vec3 _atm_moon_partial_mie_phase; +uniform vec4 _atm_moon_mie_tint: source_color = vec4(.13, .18, .29, 1.); +uniform float _atm_moon_mie_intensity = .7; + +uniform sampler2D DEPTH_TEXTURE : hint_depth_texture, filter_linear_mipmap; +uniform sampler2D SCREEN_TEXTURE : hint_screen_texture, filter_linear_mipmap; + +// Inc +//------------------------------------------------------------------------------ +const float kPI = 3.1415927f; +const float kINV_PI = 0.3183098f; +const float kHALF_PI = 1.5707963f; +const float kINV_HALF_PI = 0.6366198f; +const float kQRT_PI = 0.7853982f; +const float kINV_QRT_PI = 1.2732395f; +const float kPI4 = 12.5663706f; +const float kINV_PI4 = 0.0795775f; +const float k3PI16 = 0.1193662f; +const float kTAU = 6.2831853f; +const float kINV_TAU = 0.1591549f; +const float kE = 2.7182818f; + +float saturate(float value){ + return clamp(value, 0.0, 1.0); +} + +vec3 saturateRGB(vec3 value){ + return clamp(value.rgb, 0.0, 1.0); +} + +// pow3 +vec3 contrastLevel(vec3 vec, float level){ + return mix(vec, vec * vec * vec, level); +} + +vec3 tonemapPhoto(vec3 color, float exposure, float level){ + color.rgb *= exposure; + return mix(color.rgb, 1.0 - exp(-color.rgb), level); +} + +vec2 equirectUV(vec3 norm){ + vec2 ret; + ret.x = (atan(norm.x, norm.z) + kPI) * kINV_TAU; + ret.y = acos(norm.y) * kINV_PI; + return ret; +} + +// Atmosphere Inc +//------------------------------------------------------------------------------ +const float RAYLEIGH_ZENITH_LENGTH = 8.4e3; +const float MIE_ZENITH_LENGTH = 1.25e3; + +float rayleighPhase(float mu){ + return k3PI16 * (1.0 + mu * mu); +} + +float miePhase(float mu, vec3 partial){ + return kPI4 * (partial.x) * (pow(partial.y - partial.z * mu, -1.5)); +} + +// Simplifield for more performance +void simpleOpticalDepth(float y, out float sr, out float sm){ + y = max(0.03, y + 0.03) + _atm_level_params.y; + y = 1.0 / (y * _atm_level_params.x); + sr = y * RAYLEIGH_ZENITH_LENGTH; + sm = y * MIE_ZENITH_LENGTH; +} + +// Paper based +void opticalDepth(float y, out float sr, out float sm){ + y = max(0.0, y); + y = saturate(y * _atm_level_params.x); + + float zenith = acos(y); + zenith = cos(zenith) + 0.15 * pow(93.885 - ((zenith * 180.0) / kPI), -1.253); + zenith = 1.0 / (zenith + _atm_level_params.y); + + sr = zenith * RAYLEIGH_ZENITH_LENGTH; + sm = zenith * MIE_ZENITH_LENGTH; +} + +vec3 atmosphericScattering(float sr, float sm, vec2 mu, vec3 mult, float depth){ + vec3 betaMie = _atm_beta_mie; + vec3 betaRay = _atm_beta_ray * _atm_thickness; + + vec3 extcFactor = saturateRGB(exp(-(betaRay * sr + betaMie * sm))); + + float extcFF = mix(saturate(_atm_thickness * 0.5), 1.0, mult.x); + vec3 finalExtcFactor = mix(1.0 - extcFactor, (1.0 - extcFactor) * extcFactor, extcFF); + + float rayleighPhase = rayleighPhase(mu.x); + vec3 BRT = betaRay * rayleighPhase * saturate(depth * _fog_rayleigh_depth); + vec3 BMT = betaMie * miePhase(mu.x, _atm_sun_partial_mie_phase); + BMT *= _atm_sun_mie_intensity * _atm_sun_mie_tint.rgb * saturate(depth * _fog_mie_depth); + + vec3 BRMT = (BRT + BMT) / (betaRay + betaMie); + vec3 scatter = _atm_sun_intensity * (BRMT * finalExtcFactor) * _atm_day_tint.rgb * mult.y; + scatter = mix(scatter, scatter * (1.0 - extcFactor), _atm_darkness); + + vec3 lcol = mix(_atm_day_tint.rgb, _atm_horizon_light_tint.rgb, mult.x); + vec3 nscatter = (1.0 - extcFactor) * _atm_night_tint.rgb * saturate(depth * _fog_rayleigh_depth); + nscatter += miePhase(mu.y, _atm_moon_partial_mie_phase) * + _atm_moon_mie_tint.rgb * _atm_moon_mie_intensity * 0.005 * saturate(depth * _fog_mie_depth); + + return (scatter * lcol) + nscatter; +} + +// Fog +//------------------------------------------------------------------------------ +float fogExp(float depth, float density){ + return 1.0 - saturate(exp2(-depth * density)); +} + +float fogFalloff(float y, float zeroLevel, float falloff){ + return saturate(exp(-(y + zeroLevel) * falloff)); +} + +float fogDistance(float depth){ + float d = depth; + d = (_fog_end - d) / (_fog_end - _fog_start); + return saturate(1.0 - d); +} + +void computeCoords(vec2 uv, float depth, mat4 camMat, mat4 invProjMat, + out vec3 viewDir, out vec3 worldPos){ + + vec3 ndc = vec3(uv * 2.0 - 1.0, depth); + + // ViewDir + vec4 view = invProjMat * vec4(ndc, 1.0); + viewDir = view.xyz / view.w; + + // worldPos + view = camMat * view; + view.xyz /= view.w; + view.xyz -= (camMat * vec4(0.0, 0.0, 0.0, 1.0)).xyz; + worldPos = view.xyz; +} + +// Varyings +//------------------------------------------------------------------------------ +varying mat4 camera_matrix; +varying vec4 angle_mult; +varying flat int angle; + +void vertex(){ + POSITION = vec4(VERTEX.xy, 1.0, 1.0); + angle = 0; //ignore; without this the line below errors in 4.3-stable + angle_mult.x = saturate(1.0 - _sun_direction.y); + angle_mult.y = saturate(_sun_direction.y + 0.45); + angle_mult.z = saturate(-_sun_direction.y + 0.30); + angle_mult.w = saturate(-_sun_direction.y + 0.60); + camera_matrix = INV_VIEW_MATRIX; +} + +void fragment(){ + float depthRaw = texture(DEPTH_TEXTURE, SCREEN_UV).r; + + vec3 view; vec3 worldPos; + computeCoords(SCREEN_UV, depthRaw, camera_matrix, INV_PROJECTION_MATRIX, view, worldPos); + worldPos = normalize(worldPos); + + float linearDepth = -view.z; + float fogFactor = fogExp(linearDepth, _fog_density); + fogFactor *= fogFalloff(worldPos.y, 0.0, _fog_falloff); + fogFactor *= fogDistance(linearDepth); + + vec2 mu = vec2(dot(_sun_direction, worldPos), dot(_moon_direction, worldPos)); + float sr; float sm; simpleOpticalDepth(worldPos.y + _atm_level_params.z, sr, sm); + vec3 scatter = atmosphericScattering(sr, sm, mu.xy, angle_mult.xyz, linearDepth); + + vec3 tint = scatter; + vec4 fogColor = vec4(tint.rgb, fogFactor); + fogColor = vec4((fogColor.rgb), saturate(fogColor.a)); + fogColor.rgb = tonemapPhoto(fogColor.rgb, _color_correction_params.y, _color_correction_params.x); + + ALBEDO = fogColor.rgb; + ALPHA = fogColor.a; + //ALPHA = (depthRaw) < 0.999999999999 ? fogColor.a: 0.0; // Exclude sky. +} \ No newline at end of file diff --git a/addons/sky_3d/shaders/AtmFog.gdshader.uid b/addons/sky_3d/shaders/AtmFog.gdshader.uid new file mode 100644 index 0000000..9266df4 --- /dev/null +++ b/addons/sky_3d/shaders/AtmFog.gdshader.uid @@ -0,0 +1 @@ +uid://bn8sv775a1kvs diff --git a/addons/sky_3d/shaders/CloudsCumulus.gdshader b/addons/sky_3d/shaders/CloudsCumulus.gdshader new file mode 100644 index 0000000..34d511a --- /dev/null +++ b/addons/sky_3d/shaders/CloudsCumulus.gdshader @@ -0,0 +1,198 @@ +// Copyright (c) 2023-2025 Cory Petkovsek and Contributors +// Copyright (c) 2021 J. Cuellar +// This shader is based on DanilS clouds shader with MIT License +// See https://github.com/danilw/godot-utils-and-other/tree/master/Dynamic%20sky%20and%20reflection + +shader_type spatial; +render_mode unshaded, blend_mix, depth_draw_never, cull_front, skip_vertex_transform; + +uniform vec3 _sun_direction; +uniform vec3 _moon_direction; +uniform float _clouds_coverage; +uniform float _clouds_thickness; +uniform float _clouds_absorption; +uniform float _clouds_noise_freq; +uniform float _clouds_sky_tint_fade; +uniform float _clouds_intensity; +uniform float _clouds_size; +uniform float _clouds_speed; +uniform vec3 _clouds_direction; +uniform sampler2D _clouds_texture; + +uniform vec4 _clouds_day_color: source_color; +uniform vec4 _clouds_horizon_light_color: source_color; +uniform vec4 _clouds_night_color: source_color; + +const int kCLOUDS_STEP = 10; + +uniform vec3 _clouds_partial_mie_phase; +uniform float _clouds_mie_intensity; +uniform vec4 _atm_sun_mie_tint; +uniform vec4 _atm_moon_mie_tint: source_color; + +const float kPI = 3.1415927f; +const float kINV_PI = 0.3183098f; +const float kHALF_PI = 1.5707963f; +const float kINV_HALF_PI = 0.6366198f; +const float kQRT_PI = 0.7853982f; +const float kINV_QRT_PI = 1.2732395f; +const float kPI4 = 12.5663706f; +const float kINV_PI4 = 0.0795775f; +const float k3PI16 = 0.1193662f; +const float kTAU = 6.2831853f; +const float kINV_TAU = 0.1591549f; +const float kE = 2.7182818f; + +float saturate(float value){ + return clamp(value, 0.0, 1.0); +} + +vec3 saturateRGB(vec3 value){ + return clamp(value.rgb, 0.0, 1.0); +} + +float pow3(float real){ + return real * real * real; +} + +float noiseClouds(vec3 p){ + vec3 pos = vec3(p * 0.01); + pos.z *= 256.0; + vec2 offset = vec2(0.317, 0.123); + vec4 uv= vec4(0.0); + uv.xy = pos.xy + offset * floor(pos.z); + uv.zw = uv.xy + offset; + float x1 = textureLod(_clouds_texture, uv.xy, 0.0).r; + float x2 = textureLod(_clouds_texture, uv.zw, 0.0).r; + return mix(x1, x2, fract(pos.z)); +} + +float cloudsFBM(vec3 p, float l){ + float ret; + ret = 0.51749673 * noiseClouds(p); + p *= l; + ret += 0.25584929 * noiseClouds(p); + p *= l; + ret += 0.12527603 * noiseClouds(p); + p *= l; + ret += 0.06255931 * noiseClouds(p); + return ret; +} + +float noiseCloudsFBM(vec3 p, float freq){ + return cloudsFBM(p, freq); +} + +float remap(float value, float fromMin, float fromMax, float toMin, float toMax){ + return toMin + (value - fromMin) * (toMax - toMin) / (fromMax - fromMin); +} + +float cloudsDensity(vec3 p, vec3 offset, float t){ + vec3 pos = p * 0.0212242 - offset; + float dens = noiseCloudsFBM(pos, _clouds_noise_freq); + dens += dens; + + float cov = 1.0-_clouds_coverage; + cov = smoothstep(0.00, (cov * 3.5) + t, dens); + dens *= cov; + dens = remap(dens, 1.0-cov, 1.0, 0.0, 1.0); + + return saturate(dens); +} + +bool IntersectSphere(float r, vec3 origin, vec3 dir, out float t, out vec3 nrm) +{ + origin += vec3(0.0, 450.0, 0.0); + float a = dot(dir, dir); + float b = 2.0 * dot(origin, dir); + float c = dot(origin, origin) - r * r; + float d = b * b - 4.0 * a * c; + if(d < 0.0) return false; + + d = sqrt(d); + a *= 2.0; + float t1 = 0.5 * (-b + d); + float t2 = 0.5 * (-b - d); + + if(t1<0.0) t1 = t2; + if(t2 < 0.0) t2 = t1; + t1 = min(t1, t2); + + if(t1 < 0.0) return false; + nrm = origin + t1 * dir; + t = t1; + + return true; +} + +float miePhase(float mu, vec3 partial){ + return kPI4 * (partial.x) * (pow(partial.y - partial.z * mu, -1.5)); +} + +vec4 renderClouds2(vec3 ro, vec3 rd, float tm, float am){ + vec4 ret = vec4(0, 0, 0, 0); + vec3 wind = _clouds_direction * (tm * _clouds_speed); + float a = 0.0; + + // n and tt doesnt need to be initialized since it would be set by IntersectSphere + vec3 n; float tt; + if(IntersectSphere(500, ro, rd, tt, n)) + { + float marchStep = float(kCLOUDS_STEP) * _clouds_thickness; + vec3 dirStep = rd / rd.y * marchStep; + vec3 pos = n * _clouds_size; + + vec2 mu = vec2(dot(_sun_direction, rd), dot(_moon_direction, rd)); + vec3 mph = ((miePhase(mu.x, _clouds_partial_mie_phase) * _atm_sun_mie_tint.rgb) + + miePhase(mu.y, _clouds_partial_mie_phase) * am); + + vec4 t = vec4(1.0); + t.rgb += (mph.rgb * _clouds_mie_intensity); + + for(int i = 0; i < kCLOUDS_STEP; i++) + { + float h = float(i) * 0.1; // / float(kCLOUDS_STEP); + + float density = cloudsDensity(pos, wind, h); + float sh = saturate(exp(-_clouds_absorption * density * marchStep)); + t *= sh; + ret += (t * (exp(h) * 0.571428571) * density * marchStep); + a += (1.0 - sh) * (1.0 - a); + pos += dirStep; + } + return vec4(ret.rgb * _clouds_intensity, a); + } + return vec4(ret.rgb * _clouds_intensity, a); +} + + +varying vec4 world_pos; +varying vec4 moon_coords; +varying vec3 deep_space_coords; +varying vec4 angle_mult; + +void vertex(){ + vec4 vert = vec4(VERTEX, 0.0); + POSITION = PROJECTION_MATRIX * MODELVIEW_MATRIX * vert; + POSITION.z = 0.0000001; + + world_pos = (MODEL_MATRIX * vert); + angle_mult.x = saturate(1.0 - _sun_direction.y); + angle_mult.y = saturate(_sun_direction.y + 0.45); + angle_mult.z = saturate(-_sun_direction.y + 0.30); + angle_mult.w = saturate(-_sun_direction.y + 0.60); +} + +void fragment(){ + vec3 ray = normalize(world_pos).xyz; + float horizonBlend = saturate((ray.y+0.01) * 50.0); + + vec4 clouds = renderClouds2(vec3(0.0, 0.0, 0.0), ray, TIME, angle_mult.z); + clouds.a = saturate(clouds.a); + clouds.rgb *= mix(mix(_clouds_day_color.rgb, _clouds_horizon_light_color.rgb, angle_mult.x), + _clouds_night_color.rgb, angle_mult.w); + clouds.a = mix(0.0, clouds.a, horizonBlend); + + ALBEDO = clouds.rgb; + ALPHA = pow3(clouds.a); +} diff --git a/addons/sky_3d/shaders/CloudsCumulus.gdshader.uid b/addons/sky_3d/shaders/CloudsCumulus.gdshader.uid new file mode 100644 index 0000000..90b883c --- /dev/null +++ b/addons/sky_3d/shaders/CloudsCumulus.gdshader.uid @@ -0,0 +1 @@ +uid://brbic4veeaixo diff --git a/addons/sky_3d/shaders/LICENSE b/addons/sky_3d/shaders/LICENSE new file mode 100644 index 0000000..9405a8e --- /dev/null +++ b/addons/sky_3d/shaders/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 J. Cuéllar + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/addons/sky_3d/shaders/PerVertexSky.gdshader b/addons/sky_3d/shaders/PerVertexSky.gdshader new file mode 100644 index 0000000..91e0d3a --- /dev/null +++ b/addons/sky_3d/shaders/PerVertexSky.gdshader @@ -0,0 +1,299 @@ +// Copyright (c) 2023-2025 Cory Petkovsek and Contributors +// Copyright (c) 2021 J. Cuellar + +shader_type spatial; +render_mode unshaded, depth_draw_never, cull_front, skip_vertex_transform; + +// Color correction +uniform vec2 _color_correction_params; +uniform vec4 _ground_color: source_color; +uniform float _horizon_level; + +// uniforms +uniform vec3 _sun_direction; +uniform vec3 _moon_direction; + +uniform float _moon_size; +uniform mat3 _moon_matrix; +uniform mat3 _deep_space_matrix; + +// Atmospheric Scattering +uniform float _atm_darkness; +uniform float _atm_sun_intensity; +uniform vec4 _atm_day_tint: source_color; +uniform vec4 _atm_horizon_light_tint: source_color; +uniform vec4 _atm_night_tint: source_color; +uniform vec3 _atm_level_params; +uniform float _atm_thickness; + +// Sun mie phase +uniform vec4 _atm_sun_mie_tint: source_color; +uniform float _atm_sun_mie_intensity; +uniform vec4 _atm_moon_mie_tint: source_color; +uniform float _atm_moon_mie_intensity; +uniform vec3 _atm_beta_ray; +uniform vec3 _atm_beta_mie; +uniform vec3 _atm_sun_partial_mie_phase; +uniform vec3 _atm_moon_partial_mie_phase; + +// Sun disk +uniform vec4 _sun_disk_color: source_color; +uniform float _sun_disk_intensity; +uniform float _sun_disk_size; + +uniform vec4 _moon_color: source_color; +uniform sampler2D _moon_texture: source_color, repeat_disable; + +// Background +uniform sampler2D _background_texture: source_color; +uniform vec4 _background_color: source_color; + +// Stars Field +uniform vec4 _stars_field_color: source_color; +uniform sampler2D _stars_field_texture: source_color; +uniform float _stars_scintillation; +uniform float _stars_scintillation_speed; + +uniform sampler2D _noise_tex: source_color; + +// Clouds +uniform float _clouds_coverage; +uniform float _clouds_thickness; +uniform float _clouds_absorption; +uniform float _clouds_sky_tint_fade; +uniform float _clouds_intensity; +uniform float _clouds_size; +uniform vec2 _clouds_uv; +uniform float _clouds_speed; +uniform vec2 _clouds_direction; +uniform vec4 _clouds_day_color: source_color; +uniform vec4 _clouds_horizon_light_color: source_color; +uniform vec4 _clouds_night_color: source_color; +uniform sampler2D _clouds_texture; + +// Inc +//------------------------------------------------------------------------------ +const float kPI = 3.1415927f; +const float kINV_PI = 0.3183098f; +const float kHALF_PI = 1.5707963f; +const float kINV_HALF_PI = 0.6366198f; +const float kQRT_PI = 0.7853982f; +const float kINV_QRT_PI = 1.2732395f; +const float kPI4 = 12.5663706f; +const float kINV_PI4 = 0.0795775f; +const float k3PI16 = 0.1193662f; +const float kTAU = 6.2831853f; +const float kINV_TAU = 0.1591549f; +const float kE = 2.7182818f; + +float saturate(float value){ + return clamp(value, 0.0, 1.0); +} + +vec3 saturateRGB(vec3 value){ + return clamp(value.rgb, 0.0, 1.0); +} + +// pow3 +vec3 contrastLevel(vec3 vec, float level){ + return mix(vec, vec * vec * vec, level); +} + +vec3 tonemapPhoto(vec3 color, float exposure, float level){ + color.rgb *= exposure; + return mix(color.rgb, 1.0 - exp(-color.rgb), level); +} + +vec2 equirectUV(vec3 norm){ + vec2 ret; + ret.x = (atan(norm.x, norm.z) + kPI) * kINV_TAU; + ret.y = acos(norm.y) * kINV_PI; + return ret; +} + +/* +float random(vec2 uv){ + float ret = dot(uv, vec2(12.9898, 78.233)); + return fract(43758.5453 * sin(ret)); +} +*/ + +float disk(vec3 norm, vec3 coords, lowp float size){ + float dist = length(norm - coords); + return 1.0 - step(size, dist); +} + +// Atmosphere Inc +//------------------------------------------------------------------------------ +const float RAYLEIGH_ZENITH_LENGTH = 8.4e3; +const float MIE_ZENITH_LENGTH = 1.25e3; + +float rayleighPhase(float mu){ + return k3PI16 * (1.0 + mu * mu); +} + +float miePhase(float mu, vec3 partial){ + return kPI4 * (partial.x) * (pow(partial.y - partial.z * mu, -1.5)); +} + +// Simplifield for more performance +void simpleOpticalDepth(float y, out float sr, out float sm){ + y = max(0.03, y + 0.03) + _atm_level_params.y; + y = 1.0 / (y * _atm_level_params.x); + sr = y * RAYLEIGH_ZENITH_LENGTH; + sm = y * MIE_ZENITH_LENGTH; +} + +// Paper based +void opticalDepth(float y, out float sr, out float sm){ + y = max(0.0, y); + y = saturate(y * _atm_level_params.x); + + float zenith = acos(y); + zenith = cos(zenith) + 0.15 * pow(93.885 - ((zenith * 180.0) / kPI), -1.253); + zenith = 1.0 / (zenith + _atm_level_params.y); + + sr = zenith * RAYLEIGH_ZENITH_LENGTH; + sm = zenith * MIE_ZENITH_LENGTH; +} + +vec3 atmosphericScattering(float sr, float sm, vec2 mu, vec3 mult){ + vec3 betaMie = _atm_beta_mie; + vec3 betaRay = _atm_beta_ray * _atm_thickness; + + vec3 extcFactor = saturateRGB(exp(-(betaRay * sr + betaMie * sm))); + + float extcFF = mix(saturate(_atm_thickness * 0.5), 1.0, mult.x); + vec3 finalExtcFactor = mix(1.0 - extcFactor, (1.0 - extcFactor) * extcFactor, extcFF); + + float rayleighPhase = rayleighPhase(mu.x); + vec3 BRT = betaRay * rayleighPhase; + vec3 BMT = betaMie * miePhase(mu.x, _atm_sun_partial_mie_phase); + BMT *= _atm_sun_mie_intensity * _atm_sun_mie_tint.rgb; + + vec3 BRMT = (BRT + BMT) / (betaRay + betaMie); + vec3 scatter = _atm_sun_intensity * (BRMT * finalExtcFactor) * _atm_day_tint.rgb * mult.y; + scatter = mix(scatter, scatter * (1.0 - extcFactor), _atm_darkness); + + vec3 lcol = mix(_atm_day_tint.rgb, _atm_horizon_light_tint.rgb, mult.x); + vec3 nscatter = (1.0 - extcFactor) * _atm_night_tint.rgb; + nscatter += miePhase(mu.y, _atm_moon_partial_mie_phase) * + _atm_moon_mie_tint.rgb * _atm_moon_mie_intensity * 0.005; + + return (scatter * lcol) + nscatter; +} + +// Clouds +//------------------------------------------------------------------------------ +float noiseClouds(vec2 coords, vec2 offset){ + float speed = TIME * _clouds_speed * .5; // .5 matches cumulus clouds + vec2 wind = offset * speed; + vec2 wind2 = (offset + offset) * speed; + float a = textureLod(_clouds_texture, coords.xy * _clouds_uv - wind, 0.0).r; + float b = textureLod(_clouds_texture, coords.xy * _clouds_uv - wind2, 0.0).r; + return ((a + b) * 0.5); +} + +float cloudsDensity(vec2 p, vec2 offset){ + float d = noiseClouds(p, offset); + float c = 1.0 - _clouds_coverage; + d = d - c; + return saturate(d); +} + +vec4 renderClouds(vec3 pos){ + pos.xy = pos.xz / pos.y; + pos *= _clouds_size; + float density = cloudsDensity(pos.xy, _clouds_direction); + float sh = saturate(exp(-_clouds_absorption * density)); + float a = saturate(density * _clouds_thickness); + return vec4(vec3(density*sh) * _clouds_intensity, a); +} + +// Varyings +//------------------------------------------------------------------------------ +varying vec4 world_pos; +varying vec4 moon_coords; +varying vec3 deep_space_coords; +varying vec4 angle_mult; +varying vec3 scatter; + +void vertex(){ + vec4 vert = vec4(VERTEX, 0.0); + POSITION = PROJECTION_MATRIX * MODELVIEW_MATRIX * vert; + POSITION.z = 0.00000001; + + world_pos = MODEL_MATRIX * vert; + moon_coords.xyz = (_moon_matrix * VERTEX) / _moon_size + 0.5; + moon_coords.w = dot(world_pos.xyz, _moon_direction.xyz); + deep_space_coords.xyz = (_deep_space_matrix * VERTEX).xyz; + + angle_mult.x = saturate(1.0 - _sun_direction.y); + angle_mult.y = saturate(_sun_direction.y + 0.45); + angle_mult.z = saturate(-_sun_direction.y + 0.30); + angle_mult.w = saturate(-_sun_direction.y + 0.60); + + // Atmosphere + vec3 worldPos = normalize(world_pos).xyz; + vec2 mu = vec2(dot(_sun_direction, worldPos), dot(_moon_direction, worldPos)); + float sr, sm; + simpleOpticalDepth(worldPos.y + _atm_level_params.z + _horizon_level, sr, sm); + scatter = atmosphericScattering(sr, sm, mu.xy, angle_mult.xyz); +} + +void fragment(){ + vec3 col = vec3(0.0); + vec3 worldPos = normalize(world_pos).xyz; + vec3 cloudsPos = worldPos; + worldPos.y += _horizon_level; + float horizonBlend = saturate((worldPos.y - 0.03) * 3.0); + col.rgb += scatter.rgb; + + // Near Space + vec3 nearSpace = vec3(0.0); + vec3 sunDisk = disk(worldPos, _sun_direction, _sun_disk_size) * + _sun_disk_color.rgb * scatter.rgb; + sunDisk *= _sun_disk_intensity; + + // Moon + vec4 moon = texture(_moon_texture, vec2(-moon_coords.x+1.0, moon_coords.y)); + moon.rgb = contrastLevel(moon.rgb * _moon_color.rgb, _moon_color.a); + moon *= saturate(moon_coords.w); + float moonMask = saturate(1.0 - moon.a); + nearSpace = moon.rgb + (sunDisk.rgb * moonMask); + col.rgb += nearSpace; + + vec3 deepSpace = vec3(0.0); + vec2 deepSpaceUV = equirectUV(normalize(deep_space_coords)); + + // Background + vec3 deepSpaceBackground = textureLod(_background_texture, deepSpaceUV, 0.0).rgb; + deepSpaceBackground *= _background_color.rgb; + deepSpaceBackground = contrastLevel(deepSpaceBackground, _background_color.a); + deepSpace.rgb += deepSpaceBackground.rgb * moonMask; + + // Stars Field + float starsScintillation = textureLod(_noise_tex, UV + (TIME * _stars_scintillation_speed), 0.0).r; + starsScintillation = mix(1.0, starsScintillation * 1.5, _stars_scintillation); + + vec3 starsField = textureLod(_stars_field_texture, deepSpaceUV, 0.0).rgb * _stars_field_color.rgb; + starsField = saturateRGB(mix(starsField.rgb, starsField.rgb * starsScintillation, _stars_scintillation)); + //deepSpace.rgb -= saturate(starsField.r*10.0); + deepSpace.rgb += starsField.rgb * moonMask; + deepSpace.rgb *= angle_mult.z; + col.rgb += deepSpace.rgb * horizonBlend; + + // Clouds + vec4 clouds = renderClouds(cloudsPos); + clouds.a = saturate(clouds.a); + clouds.rgb *= mix(mix(_clouds_day_color.rgb, _clouds_horizon_light_color.rgb, angle_mult.x), + _clouds_night_color.rgb, angle_mult.w); + + clouds.a = mix(0.0, clouds.a, horizonBlend); + col.rgb = mix(col.rgb, clouds.rgb + mix(vec3(0.0), scatter, _clouds_sky_tint_fade), clouds.a); + col.rgb = mix(col.rgb, _ground_color.rgb * scatter, saturate((-worldPos.y - _atm_level_params.z)*100.0)); + + col.rgb = tonemapPhoto(col.rgb, _color_correction_params.y, _color_correction_params.x); + + ALBEDO = col.rgb; +} diff --git a/addons/sky_3d/shaders/PerVertexSky.gdshader.uid b/addons/sky_3d/shaders/PerVertexSky.gdshader.uid new file mode 100644 index 0000000..5998699 --- /dev/null +++ b/addons/sky_3d/shaders/PerVertexSky.gdshader.uid @@ -0,0 +1 @@ +uid://esppe8gjxn2o diff --git a/addons/sky_3d/shaders/SimpleMoon.gdshader b/addons/sky_3d/shaders/SimpleMoon.gdshader new file mode 100644 index 0000000..5ea7986 --- /dev/null +++ b/addons/sky_3d/shaders/SimpleMoon.gdshader @@ -0,0 +1,22 @@ +// Copyright (c) 2023-2025 Cory Petkovsek and Contributors +// Copyright (c) 2021 J. Cuellar + +shader_type spatial; +render_mode unshaded; + +uniform sampler2D _texture; +uniform vec3 _sun_direction; + +float saturate(float v){ + return clamp(v, 0.0, 1.0); +} + +varying vec3 normal; +void vertex(){ + normal = (MODEL_MATRIX * vec4(VERTEX, 0.0)).xyz; +} + +void fragment(){ + float l = saturate(max(0.0, dot(_sun_direction, normal)) * 2.0); + ALBEDO = texture(_texture, UV).rgb * l; +} diff --git a/addons/sky_3d/shaders/SimpleMoon.gdshader.uid b/addons/sky_3d/shaders/SimpleMoon.gdshader.uid new file mode 100644 index 0000000..cfbfa09 --- /dev/null +++ b/addons/sky_3d/shaders/SimpleMoon.gdshader.uid @@ -0,0 +1 @@ +uid://b32kr6wc23uit diff --git a/addons/sky_3d/shaders/Sky.gdshader b/addons/sky_3d/shaders/Sky.gdshader new file mode 100644 index 0000000..6c04706 --- /dev/null +++ b/addons/sky_3d/shaders/Sky.gdshader @@ -0,0 +1,299 @@ +// Copyright (c) 2023-2025 Cory Petkovsek and Contributors +// Copyright (c) 2021 J. Cuellar + +shader_type spatial; +render_mode unshaded, depth_draw_never, cull_front, skip_vertex_transform; + +// Color correction +uniform vec2 _color_correction_params; +uniform vec4 _ground_color: source_color; +uniform float _horizon_level; + +// uniforms +uniform vec3 _sun_direction; +uniform vec3 _moon_direction; + +uniform float _moon_size; +uniform mat3 _moon_matrix; +uniform mat3 _deep_space_matrix; + +// Atmospheric Scattering +uniform float _atm_darkness; +uniform float _atm_sun_intensity; +uniform vec4 _atm_day_tint: source_color; +uniform vec4 _atm_horizon_light_tint: source_color; +uniform vec4 _atm_night_tint: source_color; +uniform vec3 _atm_level_params; +uniform float _atm_thickness; + +// Sun mie phase +uniform vec4 _atm_sun_mie_tint: source_color; +uniform float _atm_sun_mie_intensity; +uniform vec4 _atm_moon_mie_tint: source_color; +uniform float _atm_moon_mie_intensity; +uniform vec3 _atm_beta_ray; +uniform vec3 _atm_beta_mie; +uniform vec3 _atm_sun_partial_mie_phase; +uniform vec3 _atm_moon_partial_mie_phase; + +// Sun disk +uniform vec4 _sun_disk_color: source_color; +uniform float _sun_disk_intensity; +uniform float _sun_disk_size; + +uniform vec4 _moon_color: source_color; +uniform sampler2D _moon_texture: source_color, repeat_disable; + +// Background +uniform sampler2D _background_texture: source_color; +uniform vec4 _background_color: source_color; + +// Stars Field +uniform vec4 _stars_field_color: source_color; +uniform sampler2D _stars_field_texture: source_color; +uniform float _stars_scintillation; +uniform float _stars_scintillation_speed; + +uniform sampler2D _noise_tex: source_color; + +// Clouds +uniform float _clouds_coverage; +uniform float _clouds_thickness; +uniform float _clouds_absorption; +uniform float _clouds_sky_tint_fade; +uniform float _clouds_intensity; +uniform float _clouds_size; +uniform vec2 _clouds_uv; +uniform float _clouds_speed; +uniform vec2 _clouds_direction; +uniform vec4 _clouds_day_color: source_color; +uniform vec4 _clouds_horizon_light_color: source_color; +uniform vec4 _clouds_night_color: source_color; +uniform sampler2D _clouds_texture; + + +// Inc +//------------------------------------------------------------------------------ +const float kPI = 3.1415927f; +const float kINV_PI = 0.3183098f; +const float kHALF_PI = 1.5707963f; +const float kINV_HALF_PI = 0.6366198f; +const float kQRT_PI = 0.7853982f; +const float kINV_QRT_PI = 1.2732395f; +const float kPI4 = 12.5663706f; +const float kINV_PI4 = 0.0795775f; +const float k3PI16 = 0.1193662f; +const float kTAU = 6.2831853f; +const float kINV_TAU = 0.1591549f; +const float kE = 2.7182818f; + +float saturate(float value){ + return clamp(value, 0.0, 1.0); +} + +vec3 saturateRGB(vec3 value){ + return clamp(value.rgb, 0.0, 1.0); +} + +// pow3 +vec3 contrastLevel(vec3 vec, float level){ + return mix(vec, vec * vec * vec, level); +} + +vec3 tonemapPhoto(vec3 color, float exposure, float level){ + color.rgb *= exposure; + return mix(color.rgb, 1.0 - exp(-color.rgb), level); +} + +vec2 equirectUV(vec3 norm){ + vec2 ret; + ret.x = (atan(norm.x, norm.z) + kPI) * kINV_TAU; + ret.y = acos(norm.y) * kINV_PI; + return ret; +} + +/* +float random(vec2 uv){ + float ret = dot(uv, vec2(12.9898, 78.233)); + return fract(43758.5453 * sin(ret)); +} +*/ + +float disk(vec3 norm, vec3 coords, lowp float size){ + float dist = length(norm - coords); + return 1.0 - step(size, dist); +} + +// Atmosphere Inc +//------------------------------------------------------------------------------ +const float RAYLEIGH_ZENITH_LENGTH = 8.4e3; +const float MIE_ZENITH_LENGTH = 1.25e3; + +float rayleighPhase(float mu){ + return k3PI16 * (1.0 + mu * mu); +} + +float miePhase(float mu, vec3 partial){ + return kPI4 * (partial.x) * (pow(partial.y - partial.z * mu, -1.5)); +} + +// Simplifield for more performance +void simpleOpticalDepth(float y, out float sr, out float sm){ + y = max(0.03, y + 0.03) + _atm_level_params.y; + y = 1.0 / (y * _atm_level_params.x); + sr = y * RAYLEIGH_ZENITH_LENGTH; + sm = y * MIE_ZENITH_LENGTH; +} + +// Paper based +void opticalDepth(float y, out float sr, out float sm){ + y = max(0.0, y); + y = saturate(y * _atm_level_params.x); + + float zenith = acos(y); + zenith = cos(zenith) + 0.15 * pow(93.885 - ((zenith * 180.0) / kPI), -1.253); + zenith = 1.0 / (zenith + _atm_level_params.y); + + sr = zenith * RAYLEIGH_ZENITH_LENGTH; + sm = zenith * MIE_ZENITH_LENGTH; +} + +vec3 atmosphericScattering(float sr, float sm, vec2 mu, vec3 mult){ + vec3 betaMie = _atm_beta_mie; + vec3 betaRay = _atm_beta_ray * _atm_thickness; + + vec3 extcFactor = saturateRGB(exp(-(betaRay * sr + betaMie * sm))); + + float extcFF = mix(saturate(_atm_thickness * 0.5), 1.0, mult.x); + vec3 finalExtcFactor = mix(1.0 - extcFactor, (1.0 - extcFactor) * extcFactor, extcFF); + float rayleighPhase = rayleighPhase(mu.x); + vec3 BRT = betaRay * rayleighPhase; + vec3 BMT = betaMie * miePhase(mu.x, _atm_sun_partial_mie_phase); + BMT *= _atm_sun_mie_intensity * _atm_sun_mie_tint.rgb; + + vec3 BRMT = (BRT + BMT) / (betaRay + betaMie); + vec3 scatter = _atm_sun_intensity * (BRMT * finalExtcFactor) * _atm_day_tint.rgb * mult.y; + scatter = mix(scatter, scatter * (1.0 - extcFactor), _atm_darkness); + + vec3 lcol = mix(_atm_day_tint.rgb, _atm_horizon_light_tint.rgb, mult.x); + vec3 nscatter = (1.0 - extcFactor) * _atm_night_tint.rgb; + nscatter += miePhase(mu.y, _atm_moon_partial_mie_phase) * + _atm_moon_mie_tint.rgb * _atm_moon_mie_intensity * 0.005; + + return (scatter * lcol) + nscatter; +} + +// Clouds +//------------------------------------------------------------------------------ +float noiseClouds(vec2 coords, vec2 offset){ + float speed = TIME * _clouds_speed * .5; // .5 matches cumulus clouds + vec2 wind = offset * speed; + vec2 wind2 = (offset + offset) * speed; + float a = textureLod(_clouds_texture, coords.xy * _clouds_uv - wind, 0.0).r; + float b = textureLod(_clouds_texture, coords.xy * _clouds_uv - wind2, 0.0).r; + return ((a + b) * 0.5); +} + +float cloudsDensity(vec2 p, vec2 offset){ + float d = noiseClouds(p, offset); + float c = 1.0 - _clouds_coverage; + d = d - c; + //d += d; + return saturate(d); +} + +vec4 renderClouds(vec3 pos){ + pos.xy = pos.xz / pos.y; + pos *= _clouds_size; + float density = cloudsDensity(pos.xy, _clouds_direction); + float sh = saturate(exp(-_clouds_absorption * density)); + float a = saturate(density * _clouds_thickness); + return vec4(vec3(density*sh) * _clouds_intensity, a); +} + +// Varyings +//------------------------------------------------------------------------------ +varying vec4 world_pos; +varying vec4 moon_coords; +varying vec3 deep_space_coords; +varying vec4 angle_mult; + +void vertex(){ + vec4 vert = vec4(VERTEX, 0.0); + POSITION = PROJECTION_MATRIX * MODELVIEW_MATRIX * vert; + POSITION.z = 0.00000001; + + world_pos = MODEL_MATRIX * vert; + moon_coords.xyz = (_moon_matrix * VERTEX) / _moon_size + 0.5; + moon_coords.w = dot(world_pos.xyz, _moon_direction.xyz); + deep_space_coords.xyz = (_deep_space_matrix * VERTEX).xyz; + + angle_mult.x = saturate(1.0 - _sun_direction.y); + angle_mult.y = saturate(_sun_direction.y + 0.45); + angle_mult.z = saturate(-_sun_direction.y + 0.30); + angle_mult.w = saturate(-_sun_direction.y + 0.60); +} + +void fragment(){ + vec3 col = vec3(0.0); + vec3 worldPos = normalize(world_pos).xyz; + vec3 cloudsPos = worldPos; + + // Atmosphere + vec2 mu = vec2(dot(_sun_direction, worldPos), dot(_moon_direction, worldPos)); + float sr, sm; + simpleOpticalDepth(worldPos.y + _atm_level_params.z + _horizon_level, sr, sm); + + worldPos.y += _horizon_level; + float horizonBlend = saturate((worldPos.y - 0.03) * 3.0); + + vec3 scatter = atmosphericScattering(sr, sm, mu.xy, angle_mult.xyz); + col.rgb += scatter.rgb; + + // Near Space + vec3 nearSpace = vec3(0.0); + vec3 sunDisk = disk(worldPos, _sun_direction, _sun_disk_size) * + _sun_disk_color.rgb * scatter.rgb; + sunDisk *= _sun_disk_intensity; + + // Moon + vec4 moon = texture(_moon_texture, vec2(-moon_coords.x+1.0, moon_coords.y)); + moon.rgb = contrastLevel(moon.rgb * _moon_color.rgb, _moon_color.a); + moon *= saturate(moon_coords.w); + float moonMask = saturate(1.0 - moon.a); + nearSpace = moon.rgb + (sunDisk.rgb * moonMask); + col.rgb += nearSpace; + + vec3 deepSpace = vec3(0.0); + vec2 deepSpaceUV = equirectUV(normalize(deep_space_coords)); + + // Background + vec3 deepSpaceBackground = textureLod(_background_texture, deepSpaceUV, 0.0).rgb; + deepSpaceBackground *= _background_color.rgb; + deepSpaceBackground = contrastLevel(deepSpaceBackground, _background_color.a); + deepSpace.rgb += deepSpaceBackground.rgb * moonMask; + + // Stars Field + float starsScintillation = textureLod(_noise_tex, UV + (TIME * _stars_scintillation_speed), 0.0).r; + starsScintillation = mix(1.0, starsScintillation * 1.5, _stars_scintillation); + + vec3 starsField = textureLod(_stars_field_texture, deepSpaceUV, 0.0).rgb * _stars_field_color.rgb; + starsField = saturateRGB(mix(starsField.rgb, starsField.rgb * starsScintillation, _stars_scintillation)); + //deepSpace.rgb -= saturate(starsField.r*10.0); + deepSpace.rgb += starsField.rgb * moonMask; + deepSpace.rgb *= angle_mult.z; + col.rgb += deepSpace.rgb * horizonBlend; + + // Clouds + vec4 clouds = renderClouds(cloudsPos); + clouds.a = saturate(clouds.a); + clouds.rgb *= mix(mix(_clouds_day_color.rgb, _clouds_horizon_light_color.rgb, angle_mult.x), + _clouds_night_color.rgb, angle_mult.w); + + clouds.a = mix(0.0, clouds.a, horizonBlend); + col.rgb = mix(col.rgb, clouds.rgb + mix(vec3(0.0), scatter, _clouds_sky_tint_fade), clouds.a); + col.rgb = mix(col.rgb, _ground_color.rgb * scatter, saturate((-worldPos.y - _atm_level_params.z)*100.0)); + + col.rgb = tonemapPhoto(col.rgb, _color_correction_params.y, _color_correction_params.x); + ALBEDO = col.rgb; +} diff --git a/addons/sky_3d/shaders/Sky.gdshader.uid b/addons/sky_3d/shaders/Sky.gdshader.uid new file mode 100644 index 0000000..8e53eca --- /dev/null +++ b/addons/sky_3d/shaders/Sky.gdshader.uid @@ -0,0 +1 @@ +uid://cgjdgr8w1b5y diff --git a/addons/sky_3d/src/DateTimeUtil.gd b/addons/sky_3d/src/DateTimeUtil.gd new file mode 100644 index 0000000..aa39397 --- /dev/null +++ b/addons/sky_3d/src/DateTimeUtil.gd @@ -0,0 +1,28 @@ +# Copyright (c) 2023-2025 Cory Petkovsek and Contributors +# Copyright (c) 2021 J. Cuellar + +class_name DateTimeUtil + + +const TOTAL_HOURS: int = 24 + + +static func compute_leap_year(year: int) -> bool: + return (year % 4 == 0) && (year % 100 != 0) || (year % 400 == 0); + + +static func hour_to_total_hours(hour: int) -> float: + return float(hour) + + +static func hour_minutes_to_total_hours(hour: int, minutes: int) -> float: + return float(hour) + float(minutes) / 60.0 + + +static func hours_to_total_hours(hour:int, minutes: int, seconds: int) -> float: + return float(hour) + float(minutes) / 60.0 + float(seconds) / 3600.0 + + +static func full_time_to_total_hours(hour: int, minutes: int, seconds: int, milliseconds: int) -> float: + return float(hour) + float(minutes) / 60.0 + float(seconds) / 3600.0 + \ + float(milliseconds) / 3600000.0 diff --git a/addons/sky_3d/src/DateTimeUtil.gd.uid b/addons/sky_3d/src/DateTimeUtil.gd.uid new file mode 100644 index 0000000..25fa09a --- /dev/null +++ b/addons/sky_3d/src/DateTimeUtil.gd.uid @@ -0,0 +1 @@ +uid://rb48skheqwhk diff --git a/addons/sky_3d/src/OrbitalElements.gd b/addons/sky_3d/src/OrbitalElements.gd new file mode 100644 index 0000000..f87e41a --- /dev/null +++ b/addons/sky_3d/src/OrbitalElements.gd @@ -0,0 +1,31 @@ +# Copyright (c) 2023-2025 Cory Petkovsek and Contributors +# Copyright (c) 2021 J. Cuellar + +class_name OrbitalElements + +# Handles Orbital elements for planetary system + +var N: float # Longitude of the ascending node. +var i: float # The Inclination to the ecliptic. +var w: float # Argument of perihelion. +var a: float # Semi-major axis, or mean distance from sun. +var e: float # Eccentricity. +var M: float # Mean anomaly. + + +func get_orbital_elements(index: int, timeScale: float) -> void: + if index == 0: # Sun + N = 0.0 + i = 0.0 + w = 282.9404 + 4.70935e-5 * timeScale + a = 0.0 + e = 0.016709 - 1.151e-9 * timeScale + M = 356.0470 + 0.9856002585 * timeScale + else: # Moon + N = 125.1228 - 0.0529538083 * timeScale + i = 5.1454 + w = 318.0634 + 0.1643573223 * timeScale + a = 60.2666 + e = 0.054900 + M = 115.3654 + 13.0649929509 * timeScale + diff --git a/addons/sky_3d/src/OrbitalElements.gd.uid b/addons/sky_3d/src/OrbitalElements.gd.uid new file mode 100644 index 0000000..51083f8 --- /dev/null +++ b/addons/sky_3d/src/OrbitalElements.gd.uid @@ -0,0 +1 @@ +uid://d0fv2uybi1xxl diff --git a/addons/sky_3d/src/Plugin.gd b/addons/sky_3d/src/Plugin.gd new file mode 100644 index 0000000..069899b --- /dev/null +++ b/addons/sky_3d/src/Plugin.gd @@ -0,0 +1,24 @@ +# Copyright (c) 2023-2025 Cory Petkovsek and Contributors +# Copyright (c) 2021 J. Cuellar + +@tool +extends EditorPlugin + +const __sky3d_script: String = "res://addons/sky_3d/src/Sky3D.gd" +const __sky3d_icon: String = "res://addons/sky_3d/assets/textures/SkyIcon.png" +const __skydome_script: String = "res://addons/sky_3d/src/Skydome.gd" +const __skydome_icon: String = "res://addons/sky_3d/assets/textures/SkyIcon.png" +const __time_of_day_script: String = "res://addons/sky_3d/src/TimeOfDay.gd" +const __time_of_day_icon: String = "res://addons/sky_3d/assets/textures/SkyIcon.png" + + +func _enter_tree() -> void: + add_custom_type("Sky3D", "Node", load(__sky3d_script), load(__sky3d_icon)) + add_custom_type("Skydome", "Node", load(__skydome_script), load(__skydome_icon)) + add_custom_type("TimeOfDay", "Node", load(__time_of_day_script), load(__time_of_day_icon)) + + +func _exit_tree() -> void: + remove_custom_type("Sky3D") + remove_custom_type("Skydome") + remove_custom_type("TimeOfDay") diff --git a/addons/sky_3d/src/Plugin.gd.uid b/addons/sky_3d/src/Plugin.gd.uid new file mode 100644 index 0000000..b0cd308 --- /dev/null +++ b/addons/sky_3d/src/Plugin.gd.uid @@ -0,0 +1 @@ +uid://bag65s8e7l2tq diff --git a/addons/sky_3d/src/ScatterLib.gd b/addons/sky_3d/src/ScatterLib.gd new file mode 100644 index 0000000..adfc108 --- /dev/null +++ b/addons/sky_3d/src/ScatterLib.gd @@ -0,0 +1,51 @@ +# Copyright (c) 2023-2025 Cory Petkovsek and Contributors +# Copyright (c) 2021 J. Cuellar + +class_name ScatterLib + +# Atmospheric Scattering Lib. +# References: +# - Preetham and Hoffman Paper: +# See: https://developer.amd.com/wordpress/media/2012/10/ATI-LightScattering.pdf + +const n: float = 1.0003 # Index of the air refraction +const n2: float = 1.00060009 # Index of the air refraction ˆ 2 +const N: float = 2.545e25 # Molecular Density +const pn: float = 0.035 # Depolatization factor for standard air. + + +static func compute_wavelenghts_lambda(value: Vector3) -> Vector3: + return value * 1e-9 + + +static func compute_wavlenghts(lambda: Vector3) -> Vector3: + var k = 4.0 + var ret: Vector3 = lambda + ret.x = pow(lambda.x, k) + ret.y = pow(lambda.y, k) + ret.z = pow(lambda.z, k) + return ret + + +static func compute_beta_ray(wavelenghts: Vector3) -> Vector3: + var kr: float = (8.0 * pow(PI, 3.0) * pow(n2 - 1.0, 2.0) * (6.0 + 3.0 * pn)) + var ret: Vector3 = 3.0 * N * wavelenghts * (6.0 - 7.0 * pn) + ret.x = kr / ret.x + ret.y = kr / ret.y + ret.z = kr / ret.z + return ret + + +static func compute_beta_mie(mie: float, turbidity: float) -> Vector3: + var k: float = 434e-6 + return Vector3.ONE * mie * turbidity * k + + +static func get_partial_mie_phase(g: float) -> Vector3: + var g2: float = g * g + var ret: Vector3 + # ret.x = ((1.0 - g2) / (2.0 + g2)) + ret.x = 1.0 - g2 + ret.y = 1.0 + g2 + ret.z = 2.0 * g + return ret diff --git a/addons/sky_3d/src/ScatterLib.gd.uid b/addons/sky_3d/src/ScatterLib.gd.uid new file mode 100644 index 0000000..f75c14c --- /dev/null +++ b/addons/sky_3d/src/ScatterLib.gd.uid @@ -0,0 +1 @@ +uid://dus2oqhioyqwy diff --git a/addons/sky_3d/src/Sky3D.gd b/addons/sky_3d/src/Sky3D.gd new file mode 100644 index 0000000..eacecf3 --- /dev/null +++ b/addons/sky_3d/src/Sky3D.gd @@ -0,0 +1,587 @@ +# Copyright (c) 2023-2025 Cory Petkovsek and Contributors +# Copyright (c) 2021 J. Cuellar + +## Sky3D is an Atmosphereic Day/Night Cycle for Godot 4. +## +## It manages time, moving the sun, moon, and stars, and consolidates environmental lighting controls. +## To use it, remove any WorldEnvironment node from you scene, then add a new Sky3D node. +## Explore and configure the settings in the Sky3D, SunLight, MoonLight, TimeOfDay, and Skydome nodes. + +@tool +class_name Sky3D +extends WorldEnvironment + +## Emitted when the environment variable has changed. +signal environment_changed + +## The Sun DirectionalLight. +var sun: DirectionalLight3D +## The Moon DirectionalLight. +var moon: DirectionalLight3D +## The TimeOfDay node. +var tod: TimeOfDay +## The Skydome node. +var sky: Skydome + + +## Enables all rendering and time tracking. +@export var sky3d_enabled: bool = true : set = set_sky3d_enabled + +func set_sky3d_enabled(value: bool) -> void: + sky3d_enabled = value + if value: + show_sky() + resume() + else: + hide_sky() + pause() + + +##################### +## Visibility +##################### + +@export_group("Visibility") + + +## Enables the sky shader. Disable sky, lights, fog for a black sky or call hide_sky(). +@export var sky_enabled: bool = true : set = set_sky_enabled + +func set_sky_enabled(value: bool) -> void: + sky_enabled = value + if not sky: + return + sky.sky_visible = value + sky.clouds_cumulus_visible = clouds_enabled and value + + +## Enables the Sun and Moon DirectionalLights. +@export var lights_enabled: bool = true : set = set_lights_enabled + +func set_lights_enabled(value: bool) -> void: + lights_enabled = value + if not sky: + return + sky.sun_light_enable = value + sky.moon_light_enable = value + sky.__sun_light_node.visible = value && sky.__sun_light_node.light_energy > 0 + sky.__moon_light_node.visible = value && sky.__moon_light_node.light_energy > 0 + + +## Enables the screen space fog shader. +@export var fog_enabled: bool = true : set = set_fog_enabled + +func set_fog_enabled(value: bool) -> void: + fog_enabled = value + if sky: + sky.fog_visible = value + + +## Enables the 2D and cumulus cloud layers. +@export var clouds_enabled: bool = true : set = set_clouds_enabled + +func set_clouds_enabled(value: bool) -> void: + clouds_enabled = value + if not sky: + return + sky.clouds_cumulus_visible = value + sky.clouds_thickness = float(value) * 1.7 + # TODO should create an on/off in skydome so disabling this doesn't change the enabled value + + +## Disables rendering of sky, fog, and lights +func hide_sky() -> void: + sky_enabled = false + lights_enabled = false + fog_enabled = false + + +## Enables rendering of sky, fog, and lights +func show_sky() -> void: + sky_enabled = true + lights_enabled = true + fog_enabled = true + + +##################### +## Time +##################### + +@export_group("Time") + + +## Move time forward in the editor. +@export var enable_editor_time: bool = true : set = set_editor_time_enabled + +func set_editor_time_enabled(value: bool) -> void: + enable_editor_time = value + if tod: + tod.update_in_editor = value + + +## Move time forward in game. +@export var enable_game_time: bool = true : set = set_game_time_enabled + +func set_game_time_enabled(value: bool) -> void: + enable_game_time = value + if tod: + tod.update_in_game = value + + +## The time right now in hours, 0-23.99. Larger and smaller values will wrap +@export_range(0.0, 23.99, .01, "or_greater", "or_less") var current_time: float = 8.0 : set = set_current_time + +func set_current_time(value: float) -> void: + current_time = value + if tod and tod.total_hours != current_time: + tod.total_hours = value + + +## The length of a full day in real minutes. +/-1440 (24 hours), forward or backwards. +@export_range(-1440,1440,.1) var minutes_per_day: float = 15.0 : set = set_minutes_per_day + +func set_minutes_per_day(value): + minutes_per_day = value + if tod: + tod.total_cycle_in_minutes = value + + +## Frequency of updates. Set to 0.016 for 60fps. +@export_range(0.016, 10.0) var update_interval: float = 0.1 : set = set_update_interval + +func set_update_interval(value: float) -> void: + update_interval = value + if tod: + tod.update_interval = value + + +## Tracks if the sun is above the horizon. +var _is_day: bool = true + + +## Returns true if the sun is above the horizon. +func is_day() -> bool: + return _is_day + + +## Returns true if the sun is below the horizon. +func is_night() -> bool: + return not _is_day + + +## Pauses time calculation. +func pause() -> void: + if tod: + tod.pause() + + +## Resumes time calculation. +func resume() -> void: + if tod: + tod.resume() + + +func _on_timeofday_updated(time: float) -> void: + if tod: + minutes_per_day = tod.total_cycle_in_minutes + current_time = tod.total_hours + update_interval = tod.update_interval + update_day_night() + + +## Recalculates if it's currently day or night. Adjusts night ambient light if changing state or forced. +func update_day_night(force: bool = false) -> void: + if not (sky and environment): + return + + # If day transitioning to night + if abs(sky.sun_altitude) > 87 and (_is_day or force): + _is_day = false + var tween: Tween = get_tree().create_tween() + tween.set_parallel(true) + var contrib: float = minf(night_ambient_min, sky_contribution) if night_ambient else sky_contribution + tween.tween_property(environment, "ambient_light_sky_contribution", contrib, ambient_tween_time) + tween.tween_property(environment.sky.sky_material, "energy_multiplier", 1., ambient_tween_time) + + # Else if night transitioning to day + elif abs(sky.sun_altitude) <= 87 and (not _is_day or force): + _is_day = true + var tween: Tween = get_tree().create_tween() + tween.set_parallel(true) + tween.tween_property(environment, "ambient_light_sky_contribution", sky_contribution, ambient_tween_time) + tween.tween_property(environment.sky.sky_material, "energy_multiplier", reflected_energy, ambient_tween_time) + + +##################### +## Lighting +##################### + +@export_group("Lighting") + + +## Exposure used for the tonemapper. See Evironment.tonemap_exposure +@export_range(0,16,.005) var tonemap_exposure: float = 1.0: set = set_tonemap_exposure + +func set_tonemap_exposure(value: float) -> void: + if environment: + tonemap_exposure = value + environment.tonemap_exposure = value + + +## Strength of skydome and fog. +@export_range(0,16,.005) var skydome_energy: float = 1.3: set = set_skydome_energy + +func set_skydome_energy(value: float) -> void: + if sky: + skydome_energy = value + sky.exposure = value + sky.clouds_cumulus_intensity = value * .769 # (1/1.3 default sun energy) + + +## Exposure of camera connected to Environment.camera_attributes. +@export_range(0,16,.005) var camera_exposure: float = 1.0: set = set_camera_exposure + +func set_camera_exposure(value: float) -> void: + if camera_attributes: + camera_exposure = value + camera_attributes.exposure_multiplier = value + + +## Maximum strength of Sun DirectionalLight, visible during the day. +@export_range(0,16,.005) var sun_energy: float = 1.0: set = set_sun_energy + +func set_sun_energy(value: float) -> void: + sun_energy = value + if sky: + sky.sun_light_energy = value + + +## Opacity of Sun DirectionalLight shadow. +@export_range(0,1,.005) var sun_shadow_opacity: float = 1.0: set = set_sun_shadow_opacity + +func set_sun_shadow_opacity(value: float) -> void: + sun_shadow_opacity = value + if sun: + sun.shadow_opacity = value + + +## Strength of refelcted light from the PhysicalSky. See PhysicalSkyMaterial.energy_multiplier +@export_range(0,128,.005) var reflected_energy: float = 1.0: set = set_reflected_energy + +func set_reflected_energy(value: float) -> void: + if environment: + reflected_energy = value + if environment.sky: + environment.sky.sky_material.energy_multiplier = value + + +## Ratio of ambient light to sky light. See Environment.ambient_light_sky_contribution. +@export_range(0,1,.005) var sky_contribution: float = 1.0: set = set_sky_contribution + +func set_sky_contribution(value: float) -> void: + if environment: + sky_contribution = value + environment.ambient_light_sky_contribution = value + update_day_night(true) + + +## Strength of ambient light. Works outside of Reflection Probe / GI volumes and sky_contribution < 1. +## See Environment.ambient_light_energy. +@export_range(0,16,.005) var ambient_energy: float = 1.0: set = set_ambient_energy + +func set_ambient_energy(value: float) -> void: + if environment: + ambient_energy = value + environment.ambient_light_energy = value + update_day_night(true) + + +@export_subgroup("Auto Exposure") + + +## Enables CameraAttributes.auto_exposure_enabled. +@export var auto_exposure: bool = false: set = set_auto_exposure_enabled + +func set_auto_exposure_enabled(value: bool) -> void: + if camera_attributes: + auto_exposure = value + camera_attributes.auto_exposure_enabled = value + + +## Sets CameraAttributes.auto_exposure_scale. +@export_range(0.01,16,.005) var auto_exposure_scale: float = 0.2: set = set_auto_exposure_scale + +func set_auto_exposure_scale(value: float) -> void: + if camera_attributes: + auto_exposure_scale = value + camera_attributes.auto_exposure_scale = value + + +## Sets CameraAttributesPractical.auto_exposure_min_sensitivity. +@export_range(0,1600,.5) var auto_exposure_min: float = 0.0: set = set_auto_exposure_min + +func set_auto_exposure_min(value: float) -> void: + if camera_attributes: + auto_exposure_min = value + camera_attributes.auto_exposure_min_sensitivity = value + + +## Sets CameraAttributesPractical.auto_exposure_max_sensitivity. +@export_range(30,64000,.5) var auto_exposure_max: float = 800.0: set = set_auto_exposure_max + +func set_auto_exposure_max(value: float) -> void: + if camera_attributes: + auto_exposure_max = value + camera_attributes.auto_exposure_max_sensitivity = value + + +## Sets CameraAttributes.auto_exposure_speed. +@export_range(0.1,64,.1) var auto_exposure_speed: float = 0.5: set = set_auto_exposure_speed + +func set_auto_exposure_speed(value: float) -> void: + if camera_attributes: + auto_exposure_speed = value + camera_attributes.auto_exposure_speed = value + + +@export_subgroup("Night") + + +## Maximum strength of Moon DirectionalLight, visible at night. +@export_range(0,16,.005) var moon_energy: float = .3: set = set_moon_energy + +func set_moon_energy(value: float) -> void: + moon_energy = value + if moon: + sky.moon_light_energy = value + + +## Opacity of Moon DirectionalLight shadow. +@export_range(0,1,.005) var moon_shadow_opacity: float = 1.0: set = set_moon_shadow_opacity + +func set_moon_shadow_opacity(value: float) -> void: + moon_shadow_opacity = value + if moon: + moon.shadow_opacity = value + + +## Enables a different ambient light setting at night. +@export var night_ambient: bool = true: set = set_night_ambient + +func set_night_ambient(value: bool) -> void: + night_ambient = value + update_day_night(true) + + +## Strength of ambient light at night. Sky_contribution must be < 1. See Environment.ambient_light_energy. +@export_range(0,1,.005) var night_ambient_min: float = .7: set = set_night_ambient_min + +func set_night_ambient_min(value: float) -> void: + night_ambient_min = value + if night_ambient: + update_day_night(true) + + +## Transition time for ambient light change, typically transitioning between day and night. +@export_range(0,30,.05) var ambient_tween_time: float = 3.: set = set_ambient_tween_time + +func set_ambient_tween_time(value: float) -> void: + ambient_tween_time = value + + +##################### +## Setup +##################### + + +func _notification(what: int) -> void: + # Must be after _init and before _enter_tree to properly set vars like 'sky' for setters + if what in [ NOTIFICATION_SCENE_INSTANTIATED, NOTIFICATION_ENTER_TREE ]: + _initialize() + + +func _initialize() -> void: + # Create default environment + if environment == null: + environment = Environment.new() + environment.background_mode = Environment.BG_SKY + environment.sky = Sky.new() + environment.sky.sky_material = PhysicalSkyMaterial.new() + environment.sky.sky_material.use_debanding = false + environment.ambient_light_source = Environment.AMBIENT_SOURCE_SKY + environment.ambient_light_sky_contribution = 0.7 + environment.ambient_light_energy = 1.0 + environment.reflected_light_source = Environment.REFLECTION_SOURCE_SKY + environment.tonemap_mode = Environment.TONE_MAPPER_ACES + environment.tonemap_white = 6 + emit_signal("environment_changed", environment) + + # Create default camera attributes + if camera_attributes == null: + camera_attributes = CameraAttributesPractical.new() + + # Assign children nodes + + if has_node("SunLight"): + sun = $SunLight + elif is_inside_tree(): + sun = DirectionalLight3D.new() + sun.name = "SunLight" + add_child(sun, true) + sun.owner = get_tree().edited_scene_root + sun.shadow_enabled = true + + if has_node("MoonLight"): + moon = $MoonLight + elif is_inside_tree(): + moon = DirectionalLight3D.new() + moon.name = "MoonLight" + add_child(moon, true) + moon.owner = get_tree().edited_scene_root + moon.shadow_enabled = true + + if has_node("Skydome"): + sky = $Skydome + sky.environment = environment + elif is_inside_tree(): + sky = Skydome.new() + sky.name = "Skydome" + add_child(sky, true) + sky.owner = get_tree().edited_scene_root + sky.sun_light_path = "../SunLight" + sky.moon_light_path = "../MoonLight" + sky.environment = environment + + if has_node("TimeOfDay"): + tod = $TimeOfDay + elif is_inside_tree(): + tod = TimeOfDay.new() + tod.name = "TimeOfDay" + add_child(tod, true) + tod.owner = get_tree().edited_scene_root + tod.dome_path = "../Skydome" + if tod and not tod.time_changed.is_connected(_on_timeofday_updated): + tod.time_changed.connect(_on_timeofday_updated) + + +func _enter_tree() -> void: + update_day_night(true) + + +func _set(property: StringName, value: Variant) -> bool: + match property: + "environment": + sky.environment = value + environment = value + emit_signal("environment_changed", environment) + return true + return false + + +##################### +## Constants +##################### + +# Node names +const SKY_INSTANCE:= "_SkyMeshI" +const FOG_INSTANCE:= "_FogMeshI" +const MOON_INSTANCE:= "MoonRender" +const CLOUDS_C_INSTANCE:= "_CloudsCumulusI" + +# Shaders +const _sky_shader: Shader = preload("res://addons/sky_3d/shaders/Sky.gdshader") +const _pv_sky_shader: Shader = preload("res://addons/sky_3d/shaders/PerVertexSky.gdshader") +const _clouds_cumulus_shader: Shader = preload("res://addons/sky_3d/shaders/CloudsCumulus.gdshader") +const _fog_shader: Shader = preload("res://addons/sky_3d/shaders/AtmFog.gdshader") + +# Scenes +const _moon_render: PackedScene = preload("res://addons/sky_3d/assets/resources/MoonRender.tscn") + +# Textures +const _moon_texture: Texture2D = preload("res://addons/sky_3d/assets/thirdparty/textures/moon/MoonMap.png") +const _background_texture: Texture2D = preload("res://addons/sky_3d/assets/thirdparty/textures/milkyway/Milkyway.jpg") +const _stars_field_texture: Texture2D = preload("res://addons/sky_3d/assets/thirdparty/textures/milkyway/StarField.jpg") +const _sun_moon_curve_fade: Curve = preload("res://addons/sky_3d/assets/resources/SunMoonLightFade.tres") +const _stars_field_noise: Texture2D = preload("res://addons/sky_3d/assets/textures/noise.jpg") +const _clouds_texture: Texture2D = preload("res://addons/sky_3d/assets/resources/SNoise.tres") +const _clouds_cumulus_texture: Texture2D = preload("res://addons/sky_3d/assets/textures/noiseClouds.png") + +# Skydome +const DEFAULT_POSITION:= Vector3(0.0000001, 0.0000001, 0.0000001) + +# Coords +const SUN_DIR_P:= "_sun_direction" +const MOON_DIR_P:= "_moon_direction" +const MOON_MATRIX:= "_moon_matrix" + +# General +const TEXTURE_P:= "_texture" +const COLOR_CORRECTION_P:= "_color_correction_params" +const GROUND_COLOR_P:= "_ground_color" +const NOISE_TEX:= "_noise_tex" +const HORIZON_LEVEL = "_horizon_level" + +# Atmosphere +const ATM_DARKNESS_P:= "_atm_darkness" +const ATM_BETA_RAY_P:= "_atm_beta_ray" +const ATM_SUN_INTENSITY_P:= "_atm_sun_intensity" +const ATM_DAY_TINT_P:= "_atm_day_tint" +const ATM_HORIZON_LIGHT_TINT_P:= "_atm_horizon_light_tint" + +const ATM_NIGHT_TINT_P:= "_atm_night_tint" +const ATM_LEVEL_PARAMS_P:= "_atm_level_params" +const ATM_THICKNESS_P:= "_atm_thickness" +const ATM_BETA_MIE_P:= "_atm_beta_mie" + +const ATM_SUN_MIE_TINT_P:= "_atm_sun_mie_tint" +const ATM_SUN_MIE_INTENSITY_P:= "_atm_sun_mie_intensity" +const ATM_SUN_PARTIAL_MIE_PHASE_P:= "_atm_sun_partial_mie_phase" + +const ATM_MOON_MIE_TINT_P:= "_atm_moon_mie_tint" +const ATM_MOON_MIE_INTENSITY_P:= "_atm_moon_mie_intensity" +const ATM_MOON_PARTIAL_MIE_PHASE_P:= "_atm_moon_partial_mie_phase" + +# Fog +const ATM_FOG_DENSITY_P:= "_fog_density" +const ATM_FOG_RAYLEIGH_DEPTH_P:= "_fog_rayleigh_depth" +const ATM_FOG_MIE_DEPTH_P:= "_fog_mie_depth" +const ATM_FOG_FALLOFF:= "_fog_falloff" +const ATM_FOG_START:= "_fog_start" +const ATM_FOG_END:= "_fog_end" + +# Near Space +const SUN_DISK_COLOR_P:= "_sun_disk_color" +const SUN_DISK_INTENSITY_P:= "_sun_disk_intensity" +const SUN_DISK_SIZE_P:= "_sun_disk_size" +const MOON_COLOR_P:= "_moon_color" +const MOON_SIZE_P:= "_moon_size" +const MOON_TEXTURE_P:= "_moon_texture" + +# Deep Space +const DEEP_SPACE_MATRIX_P:= "_deep_space_matrix" +const BG_COL_P:= "_background_color" +const BG_TEXTURE_P:= "_background_texture" +const STARS_COLOR_P:= "_stars_field_color" +const STARS_TEXTURE_P:= "_stars_field_texture" +const STARS_SC_P:= "_stars_scintillation" +const STARS_SC_SPEED_P:= "_stars_scintillation_speed" + +# Clouds +const CLOUDS_THICKNESS:= "_clouds_thickness" +const CLOUDS_COVERAGE:= "_clouds_coverage" +const CLOUDS_ABSORPTION:= "_clouds_absorption" +const CLOUDS_SKY_TINT_FADE:= "_clouds_sky_tint_fade" +const CLOUDS_INTENSITY:= "_clouds_intensity" +const CLOUDS_SIZE:= "_clouds_size" +const CLOUDS_NOISE_FREQ:= "_clouds_noise_freq" + +const CLOUDS_UV:= "_clouds_uv" +const CLOUDS_DIRECTION:= "_clouds_direction" +const CLOUDS_SPEED:= "_clouds_speed" +const CLOUDS_TEXTURE:= "_clouds_texture" + +const CLOUDS_DAY_COLOR:= "_clouds_day_color" +const CLOUDS_HORIZON_LIGHT_COLOR:= "_clouds_horizon_light_color" +const CLOUDS_NIGHT_COLOR:= "_clouds_night_color" +const CLOUDS_MIE_INTENSITY:= "_clouds_mie_intensity" +const CLOUDS_PARTIAL_MIE_PHASE:= "_clouds_partial_mie_phase" diff --git a/addons/sky_3d/src/Sky3D.gd.uid b/addons/sky_3d/src/Sky3D.gd.uid new file mode 100644 index 0000000..5187df1 --- /dev/null +++ b/addons/sky_3d/src/Sky3D.gd.uid @@ -0,0 +1 @@ +uid://bmywk4wvcp0lr diff --git a/addons/sky_3d/src/Skydome.gd b/addons/sky_3d/src/Skydome.gd new file mode 100644 index 0000000..d71632b --- /dev/null +++ b/addons/sky_3d/src/Skydome.gd @@ -0,0 +1,1865 @@ +# Copyright (c) 2023-2025 Cory Petkovsek and Contributors +# Copyright (c) 2021 J. Cuellar + +@tool +class_name Skydome +extends Node + + +signal sun_direction_changed(value) +signal sun_transform_changed(value) +signal moon_direction_changed(value) +signal moon_transform_changed(value) +signal day_night_changed(value) +signal lights_changed + + +enum SkyQuality { + Low, High +} + +enum MoonResolution { + R64, R128, R256, R512, R1024, +} + + +var is_scene_built: bool +var sky_mesh: MeshInstance3D +var sky_sphere: SphereMesh +var moon_render: Node +var clouds_cumulus_mesh: MeshInstance3D +var fog_mesh: MeshInstance3D + +var sky_material: Material +var moon_material: Material +var clouds_cumulus_material: Material +var fog_material: Material + + +func _ready() -> void: + build_scene() + + # Update properties + # General + update_sky_visible() + update_dome_radius() + update_color_correction_params() + update_ground_color() + update_sky_layers() + update_sky_render_priority() + update_horizon_level() + + # Coords + update_sun_coords() + update_moon_coords() + + # Atmosphere + update_atm_quality() + update_beta_ray() + update_atm_darkness() + update_atm_sun_intensity() + update_atm_day_tint() + update_atm_horizon_light_tint() + update_night_intensity() + update_atm_level_params() + update_atm_thickness() + update_beta_mie() + update_atm_sun_mie_tint() + update_atm_sun_mie_intensity() + update_atm_sun_mie_anisotropy() + update_atm_moon_mie_tint() + update_atm_moon_mie_intensity() + update_atm_moon_mie_anisotropy() + + # Fog + update_fog_visible() + update_fog_atm_level_params_offset() + update_fog_density() + update_fog_start() + update_fog_end() + update_fog_rayleigh_depth() + update_fog_mie_depth() + update_fog_falloff() + update_fog_layers() + update_fog_render_priority() + + # Near space + update_sun_light_path() + update_sun_disk_color() + update_sun_disk_intensity() + update_sun_disk_size() + update_moon_color() + update_moon_light_path() + update_moon_size() + set_enable_set_moon_texture(enable_set_moon_texture) + update_moon_texture() + update_moon_resolution() + + # Near space lighting + update_sun_light_color() + update_sun_light_energy() + update_moon_light_color() + update_moon_light_energy() + + # Deep space + update_deep_space_basis() + set_set_background_texture(set_background_texture) + update_background_color() + update_background_texture() + update_stars_field_color() + set_set_stars_field_texture(set_stars_field_texture) + update_stars_field_texture() + update_stars_scintillation() + update_stars_scintillation_speed() + + # Clouds + update_clouds_thickness() + update_clouds_coverage() + update_clouds_absorption() + update_clouds_sky_tint_fade() + update_clouds_intensity() + update_clouds_size() + update_clouds_uv() + update_clouds_direction() + update_clouds_speed() + set_set_clouds_texture(set_clouds_texture) + update_clouds_texture() + + # Clouds cumulus + update_clouds_cumulus_visible() + update_clouds_cumulus_day_color() + update_clouds_cumulus_horizon_light_color() + update_clouds_cumulus_night_color() + update_clouds_cumulus_thickness() + update_clouds_cumulus_coverage() + update_clouds_cumulus_absorption() + update_clouds_cumulus_noise_freq() + update_clouds_cumulus_intensity() + update_clouds_cumulus_mie_intensity() + update_clouds_cumulus_mie_anisotropy() + update_clouds_cumulus_size() + update_clouds_cumulus_direction() + update_clouds_cumulus_speed() + set_set_clouds_cumulus_texture(set_clouds_cumulus_texture) + update_clouds_cumulus_texture() + + # Environment + __update_environment() + + +func build_scene() -> void: + if is_scene_built: + return + + # Sky Mesh + sky_mesh = MeshInstance3D.new() + sky_mesh.name = Sky3D.SKY_INSTANCE + sky_sphere = SphereMesh.new() + sky_mesh.mesh = sky_sphere + sky_material = ShaderMaterial.new() + sky_material.shader = Sky3D._pv_sky_shader + sky_material.set_shader_parameter(Sky3D.NOISE_TEX, Sky3D._stars_field_noise) + sky_mesh.material_override = sky_material + __setup_mesh_instance(sky_mesh, Sky3D.DEFAULT_POSITION) + add_child(sky_mesh) + + # Moon Render + moon_render = Sky3D._moon_render.instantiate() + moon_render.name = Sky3D.MOON_INSTANCE + var moon_mesh = moon_render.get_node("MoonTransform/Camera3D/Mesh") as MeshInstance3D + moon_material = moon_mesh.material_override + add_child(moon_render) + + # Clouds Cumulus Mesh + clouds_cumulus_mesh = MeshInstance3D.new() + clouds_cumulus_mesh.name = Sky3D.CLOUDS_C_INSTANCE + var clouds_cumulus_sphere = SphereMesh.new() + clouds_cumulus_sphere.radial_segments = 8 + clouds_cumulus_sphere.rings = 8 + clouds_cumulus_mesh.mesh = clouds_cumulus_sphere + clouds_cumulus_material = ShaderMaterial.new() + clouds_cumulus_material.shader = Sky3D._clouds_cumulus_shader + clouds_cumulus_mesh.material_override = clouds_cumulus_material + __setup_mesh_instance(clouds_cumulus_mesh, Sky3D.DEFAULT_POSITION) + add_child(clouds_cumulus_mesh) + + fog_mesh = MeshInstance3D.new() + fog_mesh.name = Sky3D.FOG_INSTANCE + var fog_screen_quad = QuadMesh.new() + var size: Vector2 + size.x = 2.0 + size.y = 2.0 + fog_screen_quad.size = size + fog_mesh.mesh = fog_screen_quad + fog_material = ShaderMaterial.new() + fog_material.shader = Sky3D._fog_shader + fog_material.render_priority = 127 + fog_mesh.material_override = fog_material + __setup_mesh_instance(fog_mesh, Vector3.ZERO) + add_child(fog_mesh) + + is_scene_built = true + + +func __setup_mesh_instance(target: MeshInstance3D, origin: Vector3) -> void: + target.transform.origin = origin + target.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF + target.custom_aabb = AABB(Vector3(-1e31, -1e31, -1e31), Vector3(2e31, 2e31, 2e31)) + + +##################### +## Global +##################### + +var sky_visible: bool = true: set = set_sky_visible +var dome_radius: float = 10.0: set = set_dome_radius +var tonemap_level: float = 0.0: set = set_tonemap_level +var exposure: float = 1.3: set = set_exposure +var ground_color:= Color(0.3, 0.3, 0.3, 1.0): set = set_ground_color +var sky_layers: int = 4: set = set_sky_layers +var sky_render_priority: int = -128: set = set_sky_render_priority +var horizon_level: float = 0.0: set = set_horizon_level + + +func set_sky_visible(value: bool) -> void: + if value == sky_visible: + return + sky_visible = value + update_sky_visible() + + +func update_sky_visible() -> void: + if !is_scene_built: + return + sky_mesh.visible = sky_visible + + +func set_dome_radius(value: float) -> void: + if value == dome_radius: + return + dome_radius = value + update_dome_radius() + + +func update_dome_radius() -> void: + if !is_scene_built: + return + sky_mesh.scale = dome_radius * Vector3.ONE + clouds_cumulus_mesh.scale = dome_radius * Vector3.ONE + + +func set_tonemap_level(value: float) -> void: + if value == tonemap_level: + return + tonemap_level = value + update_color_correction_params() + + +func set_exposure(value: float) -> void: + if value == exposure: + return + exposure = value + update_color_correction_params() + + +func update_color_correction_params() -> void: + if !is_scene_built: + return + var p: Vector2 + p.x = tonemap_level + p.y = exposure + sky_material.set_shader_parameter(Sky3D.COLOR_CORRECTION_P, p) + fog_material.set_shader_parameter(Sky3D.COLOR_CORRECTION_P, p) + + + +func set_ground_color(value: Color) -> void: + if value == ground_color: + return + ground_color = value + update_ground_color() + + +func update_ground_color() -> void: + if !is_scene_built: + return + sky_material.set_shader_parameter(Sky3D.GROUND_COLOR_P, ground_color) + + +func set_sky_layers(value: int) -> void: + if sky_layers == value: + return + sky_layers = value + update_sky_layers() + + +func update_sky_layers() -> void: + if !is_scene_built: + return + sky_mesh.layers = sky_layers + clouds_cumulus_mesh.layers = sky_layers + + +func set_sky_render_priority(value: int) -> void: + if value == sky_render_priority: + return + sky_render_priority = value + update_sky_render_priority() + + +func update_sky_render_priority() -> void: + if !is_scene_built: + return + sky_material.render_priority = sky_render_priority + clouds_cumulus_material.render_priority = sky_render_priority + 1 + + +func set_horizon_level(value: float) -> void: + if value == horizon_level: + return + horizon_level = value + update_horizon_level() + + +func update_horizon_level() -> void: + if !is_scene_built: + return + sky_material.set_shader_parameter(Sky3D.HORIZON_LEVEL, horizon_level) + + +##################### +## Sun Coords +##################### + +var sun_azimuth: float = 0.0: set = set_sun_azimuth +var sun_altitude: float = -27.387: set = set_sun_altitude +var __finish_set_sun_pos: bool = false +var __sun_transform:= Transform3D() + + +func set_sun_azimuth(value: float) -> void: + if value == sun_azimuth: + return + sun_azimuth = value + update_sun_coords() + + +func set_sun_altitude(value: float) -> void: + if value == sun_altitude: + return + sun_altitude = value + update_sun_coords() + + +func get_sun_transform() -> Transform3D: + return __sun_transform + + +func sun_direction() -> Vector3: + return __sun_transform.origin - Sky3D.DEFAULT_POSITION + + +func update_sun_coords() -> void: + if !is_scene_built: + return + + var azimuth: float = sun_azimuth * TOD_Math.DEG_TO_RAD + var altitude: float = sun_altitude * TOD_Math.DEG_TO_RAD + + __finish_set_sun_pos = false + if not __finish_set_sun_pos: + __sun_transform.origin = TOD_Math.to_orbit(altitude, azimuth) + __finish_set_sun_pos = true + + if __finish_set_sun_pos: + __sun_transform = __sun_transform.looking_at(Sky3D.DEFAULT_POSITION, Vector3.LEFT) + + __set_day_state(altitude) + emit_signal("sun_transform_changed", __sun_transform) + emit_signal("sun_transform_changed", sun_direction()) + + sky_material.set_shader_parameter(Sky3D.SUN_DIR_P, sun_direction()) + fog_material.set_shader_parameter(Sky3D.SUN_DIR_P, sun_direction()) + moon_material.set_shader_parameter(Sky3D.SUN_DIR_P, sun_direction()) + clouds_cumulus_material.set_shader_parameter(Sky3D.SUN_DIR_P, sun_direction()) + + if __sun_light_node != null: + #if __sun_light_node.light_energy > 0.0 && (abs(sun_altitude) < 90.0 + if __sun_light_node.light_energy > 0.0: + __sun_light_node.transform = __sun_transform + + update_night_intensity() + update_sun_light_color() + update_sun_light_energy() + update_moon_light_energy() + __update_environment() + + +##################### +## Moon Coords +##################### + +var moon_azimuth: float = 5.0: set = set_moon_azimuth +var moon_altitude: float = -80.0: set = set_moon_altitude +var __finish_set_moon_pos = false +var __moon_transform:= Transform3D() + + +func set_moon_azimuth(value: float) -> void: + if value == moon_azimuth: + return + moon_azimuth = value + update_moon_coords() + + +func set_moon_altitude(value: float) -> void: + if value == moon_altitude: + return + moon_altitude = value + update_moon_coords() + + +func get_moon_transform() -> Transform3D: + return __moon_transform + + +func moon_direction() -> Vector3: + return __moon_transform.origin - Sky3D.DEFAULT_POSITION + + +func update_moon_coords() -> void: + if !is_scene_built: + return + + var azimuth: float = moon_azimuth * TOD_Math.DEG_TO_RAD + var altitude: float = moon_altitude * TOD_Math.DEG_TO_RAD + + __finish_set_moon_pos = false + if not __finish_set_moon_pos: + __moon_transform.origin = TOD_Math.to_orbit(altitude, azimuth) + __finish_set_moon_pos = true + + if __finish_set_moon_pos: + __moon_transform = __moon_transform.looking_at(Sky3D.DEFAULT_POSITION, Vector3.LEFT) + + emit_signal("moon_transform_changed", __moon_transform) + emit_signal("moon_direction_changed", moon_direction()) + + sky_material.set_shader_parameter(Sky3D.MOON_DIR_P, moon_direction()) + fog_material.set_shader_parameter(Sky3D.MOON_DIR_P, moon_direction()) + moon_material.set_shader_parameter(Sky3D.MOON_DIR_P, moon_direction()) + clouds_cumulus_material.set_shader_parameter(Sky3D.MOON_DIR_P, moon_direction()) + sky_material.set_shader_parameter(Sky3D.MOON_MATRIX, __moon_transform.basis.inverse()) + + var moon_instance_transform = moon_render.get_node("MoonTransform") as Node3D + moon_instance_transform.transform = __moon_transform + + if __moon_light_node != null: + #if __moon_light_node.light_energy > 0.0 && (abs(moon_altitude) < 90.0): + if __moon_light_node.light_energy > 0.0: + __moon_light_node.transform = __moon_transform + + __moon_light_altitude_mult = TOD_Math.saturate(moon_direction().y) + + update_night_intensity() + set_moon_light_color(moon_light_color) + update_moon_light_energy() + __update_environment() + + +##################### +## Atmosphere +##################### + +var atm_quality: int = 1: set = set_atm_quality +var atm_wavelenghts:= Vector3(680.0, 550.0, 440.0): set = set_atm_wavelenghts +var atm_darkness: float = 0.5: set = set_atm_darkness +var atm_sun_intensity: float = 18.0: set = set_atm_sun_intensity +var atm_day_tint:= Color(0.807843, 0.909804, 1.0): set = set_atm_day_tint +var atm_horizon_light_tint:= Color(0.980392, 0.635294, 0.462745, 1.0): set = set_atm_horizon_light_tint +var atm_enable_moon_scatter_mode: bool = false: set = set_atm_enable_moon_scatter_mode +var atm_night_tint:= Color(0.168627, 0.2, 0.25098, 1.0): set = set_atm_night_tint +var atm_level_params:= Vector3(1.0, 0.0, 0.0): set = set_atm_level_params +var atm_thickness: float = 0.7: set = set_atm_thickness +var atm_mie: float = 0.07: set = set_atm_mie +var atm_turbidity: float = 0.001: set = set_atm_turbidity +var atm_sun_mie_tint:= Color(1.0, 1.0, 1.0, 1.0): set = set_atm_sun_mie_tint +var atm_sun_mie_intensity: float = 1.0: set = set_atm_sun_mie_intensity +var atm_sun_mie_anisotropy: float = 0.8: set = set_atm_sun_mie_anisotropy +var atm_moon_mie_tint:= Color(0.137255, 0.184314, 0.292196): set = set_atm_moon_mie_tint +var atm_moon_mie_intensity: float = 0.7: set = set_atm_moon_mie_intensity +var atm_moon_mie_anisotropy: float = 0.8: set = set_atm_moon_mie_anisotropy + + +func set_atm_quality(value: int) -> void: + if value == atm_quality: + return + atm_quality = value + update_atm_quality() + + +func update_atm_quality() -> void: + if !is_scene_built: + return + if atm_quality == SkyQuality.Low: + sky_material.shader = Sky3D._sky_shader + sky_sphere.radial_segments = 16 + sky_sphere.rings = 8 + else: + sky_material.shader = Sky3D._pv_sky_shader + sky_sphere.radial_segments = 64 + sky_sphere.rings = 64 + + +func set_atm_wavelenghts(value : Vector3) -> void: + if value == atm_wavelenghts: + return + atm_wavelenghts = value + update_beta_ray() + + +func update_beta_ray() -> void: + if !is_scene_built: + return + + var wll = ScatterLib.compute_wavelenghts_lambda(atm_wavelenghts) + var wls = ScatterLib.compute_wavlenghts(wll) + var betaRay = ScatterLib.compute_beta_ray(wls) + sky_material.set_shader_parameter(Sky3D.ATM_BETA_RAY_P, betaRay) + fog_material.set_shader_parameter(Sky3D.ATM_BETA_RAY_P, betaRay) + + +func set_atm_darkness(value: float) -> void: + if value == atm_darkness: + return + atm_darkness = value + update_atm_darkness() + + +func update_atm_darkness() -> void: + if !is_scene_built: + return + sky_material.set_shader_parameter(Sky3D.ATM_DARKNESS_P, atm_darkness) + fog_material.set_shader_parameter(Sky3D.ATM_DARKNESS_P, atm_darkness) + + +func set_atm_sun_intensity(value: float) -> void: + if value == atm_sun_intensity: + return + atm_sun_intensity = value + update_atm_sun_intensity() + + +func update_atm_sun_intensity() -> void: + if !is_scene_built: + return + sky_material.set_shader_parameter(Sky3D.ATM_SUN_INTENSITY_P, atm_sun_intensity) + fog_material.set_shader_parameter(Sky3D.ATM_SUN_INTENSITY_P, atm_sun_intensity) + + +func set_atm_day_tint(value: Color) -> void: + if value == atm_day_tint: + return + atm_day_tint = value + update_atm_day_tint() + + +func update_atm_day_tint() -> void: + if !is_scene_built: + return + sky_material.set_shader_parameter(Sky3D.ATM_DAY_TINT_P, atm_day_tint) + fog_material.set_shader_parameter(Sky3D.ATM_DAY_TINT_P, atm_day_tint) + + +func set_atm_horizon_light_tint(value: Color) -> void: + if value == atm_horizon_light_tint: + return + atm_horizon_light_tint = value + update_atm_horizon_light_tint() + + +func update_atm_horizon_light_tint() -> void: + if !is_scene_built: + return + sky_material.set_shader_parameter(Sky3D.ATM_HORIZON_LIGHT_TINT_P, atm_horizon_light_tint) + fog_material.set_shader_parameter(Sky3D.ATM_HORIZON_LIGHT_TINT_P, atm_horizon_light_tint) + + +func set_atm_enable_moon_scatter_mode(value: bool) -> void: + if value == atm_enable_moon_scatter_mode: + return + atm_enable_moon_scatter_mode = value + update_night_intensity() + + +func set_atm_night_tint(value: Color) -> void: + if value == atm_night_tint: + return + atm_night_tint = value + update_night_intensity() + + +func update_night_intensity() -> void: + if !is_scene_built: + return + + var tint: Color = atm_night_tint * atm_night_intensity() + sky_material.set_shader_parameter(Sky3D.ATM_NIGHT_TINT_P, tint) + fog_material.set_shader_parameter(Sky3D.ATM_NIGHT_TINT_P, atm_night_tint * fog_atm_night_intensity()) + + set_atm_moon_mie_intensity(atm_moon_mie_intensity) + + +func set_atm_level_params(value: Vector3) -> void: + if value == atm_level_params: + return + atm_level_params = value + update_atm_level_params() + + +func update_atm_level_params() -> void: + if !is_scene_built: + return + sky_material.set_shader_parameter(Sky3D.ATM_LEVEL_PARAMS_P, atm_level_params) + fog_material.set_shader_parameter(Sky3D.ATM_LEVEL_PARAMS_P, atm_level_params + fog_atm_level_params_offset) + + +func set_atm_thickness(value: float) -> void: + if value == atm_thickness: + return + atm_thickness = value + update_atm_thickness() + + +func update_atm_thickness() -> void: + if !is_scene_built: + return + sky_material.set_shader_parameter(Sky3D.ATM_THICKNESS_P, atm_thickness) + fog_material.set_shader_parameter(Sky3D.ATM_THICKNESS_P, atm_thickness) + + +func set_atm_mie(value: float) -> void: + if value == atm_mie: + return + atm_mie = value + update_beta_mie() + + +func set_atm_turbidity(value: float) -> void: + if value == atm_turbidity: + return + atm_turbidity = value + update_beta_mie() + + +func update_beta_mie() -> void: + if !is_scene_built: + return + + var bm = ScatterLib.compute_beta_mie(atm_mie, atm_turbidity) + sky_material.set_shader_parameter(Sky3D.ATM_BETA_MIE_P, bm) + fog_material.set_shader_parameter(Sky3D.ATM_BETA_MIE_P, bm) + + +func set_atm_sun_mie_tint(value: Color) -> void: + if value == atm_sun_mie_tint: + return + atm_sun_mie_tint = value + update_atm_sun_mie_tint() + + +func update_atm_sun_mie_tint() -> void: + if !is_scene_built: + return + sky_material.set_shader_parameter(Sky3D.ATM_SUN_MIE_TINT_P, atm_sun_mie_tint) + fog_material.set_shader_parameter(Sky3D.ATM_SUN_MIE_TINT_P, atm_sun_mie_tint) + clouds_cumulus_material.set_shader_parameter(Sky3D.ATM_SUN_MIE_TINT_P, atm_sun_mie_tint) + + +func set_atm_sun_mie_intensity(value: float) -> void: + if value == atm_sun_mie_intensity: + return + atm_sun_mie_intensity = value + update_atm_sun_mie_intensity() + + +func update_atm_sun_mie_intensity() -> void: + if !is_scene_built: + return + sky_material.set_shader_parameter(Sky3D.ATM_SUN_MIE_INTENSITY_P, atm_sun_mie_intensity) + fog_material.set_shader_parameter(Sky3D.ATM_SUN_MIE_INTENSITY_P, atm_sun_mie_intensity) + + +func set_atm_sun_mie_anisotropy(value: float) -> void: + if value == atm_sun_mie_anisotropy: + return + atm_sun_mie_anisotropy = value + update_atm_sun_mie_anisotropy() + + +func update_atm_sun_mie_anisotropy() -> void: + if !is_scene_built: + return + var partial = ScatterLib.get_partial_mie_phase(atm_sun_mie_anisotropy) + sky_material.set_shader_parameter(Sky3D.ATM_SUN_PARTIAL_MIE_PHASE_P, partial) + fog_material.set_shader_parameter(Sky3D.ATM_SUN_PARTIAL_MIE_PHASE_P, partial) + + +func set_atm_moon_mie_tint(value: Color) -> void: + if value == atm_moon_mie_tint: + return + atm_moon_mie_tint = value + update_atm_moon_mie_tint() + + +func update_atm_moon_mie_tint() -> void: + if !is_scene_built: + return + sky_material.set_shader_parameter(Sky3D.ATM_MOON_MIE_TINT_P, atm_moon_mie_tint) + fog_material.set_shader_parameter(Sky3D.ATM_MOON_MIE_TINT_P, atm_moon_mie_tint) + clouds_cumulus_material.set_shader_parameter(Sky3D.ATM_MOON_MIE_TINT_P, atm_moon_mie_tint) + + +func set_atm_moon_mie_intensity(value: float) -> void: + if value == atm_moon_mie_intensity: + return + atm_moon_mie_intensity = value + update_atm_sun_mie_intensity() + + +func update_atm_moon_mie_intensity() -> void: + if !is_scene_built: + return + sky_material.set_shader_parameter(Sky3D.ATM_MOON_MIE_INTENSITY_P, atm_moon_mie_intensity * atm_moon_phases_mult()) + fog_material.set_shader_parameter(Sky3D.ATM_MOON_MIE_INTENSITY_P, atm_moon_mie_intensity * atm_moon_phases_mult()) + + +func set_atm_moon_mie_anisotropy(value: float) -> void: + if value == atm_moon_mie_anisotropy: + return + atm_moon_mie_anisotropy = value + update_atm_moon_mie_anisotropy() + + +func update_atm_moon_mie_anisotropy() -> void: + if !is_scene_built: + return + var partial = ScatterLib.get_partial_mie_phase(atm_moon_mie_anisotropy) + sky_material.set_shader_parameter(Sky3D.ATM_MOON_PARTIAL_MIE_PHASE_P, partial) + fog_material.set_shader_parameter(Sky3D.ATM_MOON_PARTIAL_MIE_PHASE_P, partial) + + +func atm_moon_phases_mult() -> float: + if not atm_enable_moon_scatter_mode: + return atm_night_intensity() + return TOD_Math.saturate(-sun_direction().dot(moon_direction()) + 0.60) + + +func atm_night_intensity() -> float: + if not atm_enable_moon_scatter_mode: + return TOD_Math.saturate(-sun_direction().y + 0.30) + return TOD_Math.saturate(moon_direction().y) * atm_moon_phases_mult() + + +func fog_atm_night_intensity() -> float: + if not atm_enable_moon_scatter_mode: + return TOD_Math.saturate(-sun_direction().y + 0.70) + return TOD_Math.saturate(-sun_direction().y) * atm_moon_phases_mult() + + +##################### +## Fog +##################### + +var fog_visible: bool = true: set = set_fog_visible +var fog_atm_level_params_offset:= Vector3(0.0, 0.0, -1.0): set = set_fog_atm_level_params_offset +var fog_density: float = 0.00015: set = set_fog_density +var fog_start: float = 0.0: set = set_fog_start +var fog_end: float = 1000: set = set_fog_end +var fog_rayleigh_depth: float = 0.116: set = set_fog_rayleigh_depth +var fog_mie_depth: float = 0.0001: set = set_fog_mie_depth +var fog_falloff: float = 3.0: set = set_fog_falloff +var fog_layers: int = 524288: set = set_fog_layers +var fog_render_priority: int = 123: set = set_fog_render_priority + + +func set_fog_visible(value: bool) -> void: + if value == fog_visible: + return + fog_visible = value + update_fog_visible() + + +func update_fog_visible() -> void: + if !is_scene_built: + return + fog_mesh.visible = fog_visible + + +func set_fog_atm_level_params_offset(value: Vector3) -> void: + if value == fog_atm_level_params_offset: + return + fog_atm_level_params_offset = value + update_fog_atm_level_params_offset() + + +func update_fog_atm_level_params_offset() -> void: + if !is_scene_built: + return + fog_material.set_shader_parameter(Sky3D.ATM_LEVEL_PARAMS_P, atm_level_params + fog_atm_level_params_offset) + + +func set_fog_density(value: float) -> void: + if value == fog_density: + return + fog_density = value + update_fog_density() + + +func update_fog_density() -> void: + if !is_scene_built: + return + fog_material.set_shader_parameter(Sky3D.ATM_FOG_DENSITY_P, fog_density) + + +func set_fog_start(value: float) -> void: + if value == fog_start: + return + fog_start = value + update_fog_start() + + +func update_fog_start() -> void: + if !is_scene_built: + return + fog_material.set_shader_parameter(Sky3D.ATM_FOG_START, fog_start) + + +func set_fog_end(value: float) -> void: + if value == fog_end: + return + fog_end = value + update_fog_end() + + +func update_fog_end() -> void: + if !is_scene_built: + return + fog_material.set_shader_parameter(Sky3D.ATM_FOG_END, fog_end) + + +func set_fog_rayleigh_depth(value: float) -> void: + if value == fog_rayleigh_depth: + return + fog_rayleigh_depth = value + update_fog_rayleigh_depth() + + +func update_fog_rayleigh_depth() -> void: + if !is_scene_built: + return + fog_material.set_shader_parameter(Sky3D.ATM_FOG_RAYLEIGH_DEPTH_P, fog_rayleigh_depth) + + +func set_fog_mie_depth(value: float) -> void: + if value == fog_mie_depth: + return + fog_mie_depth = value + update_fog_mie_depth() + + +func update_fog_mie_depth() -> void: + if !is_scene_built: + return + fog_material.set_shader_parameter(Sky3D.ATM_FOG_MIE_DEPTH_P, fog_mie_depth) + + +func set_fog_falloff(value: float) -> void: + if value == fog_falloff: + return + fog_falloff = value + update_fog_falloff() + + +func update_fog_falloff() -> void: + if !is_scene_built: + return + fog_material.set_shader_parameter(Sky3D.ATM_FOG_FALLOFF, fog_falloff) + + +func set_fog_layers(value: int) -> void: + if value == fog_layers: + return + fog_layers = value + update_fog_layers() + + +func update_fog_layers() -> void: + if !is_scene_built: + return + fog_mesh.layers = fog_layers + + +func set_fog_render_priority(value: int) -> void: + if value == fog_render_priority: + return + fog_render_priority = value + update_fog_render_priority() + + +func update_fog_render_priority() -> void: + if !is_scene_built: + return + fog_material.render_priority = fog_render_priority + + +##################### +## Near space +##################### + +var sun_disk_color:= Color(0.996094, 0.541334, 0.140076): set = set_sun_disk_color +var sun_disk_intensity: float = 2.0: set = set_sun_disk_intensity +var sun_disk_size: float = 0.015: set = set_sun_disk_size +var moon_color:= Color.WHITE: set = set_moon_color +var moon_size: float = 0.07: set = set_moon_size +var enable_set_moon_texture = false: set = set_enable_set_moon_texture +var moon_texture: Texture2D = null: set = set_moon_texture +var moon_resolution: int = MoonResolution.R256: set = set_moon_resolution + + +func set_sun_disk_color(value: Color) -> void: + if value == sun_disk_color: + return + sun_disk_color = value + update_sun_disk_color() + + +func update_sun_disk_color() -> void: + if !is_scene_built: + return + sky_material.set_shader_parameter(Sky3D.SUN_DISK_COLOR_P, sun_disk_color) + + +func set_sun_disk_intensity(value: float) -> void: + if value == sun_disk_intensity: + return + sun_disk_intensity = value + update_sun_disk_intensity() + + +func update_sun_disk_intensity() -> void: + if !is_scene_built: + return + sky_material.set_shader_parameter(Sky3D.SUN_DISK_INTENSITY_P, sun_disk_intensity) + + +func set_sun_disk_size(value: float) -> void: + if value == sun_disk_size: + return + sun_disk_size = value + update_sun_disk_size() + + +func update_sun_disk_size() -> void: + if !is_scene_built: + return + sky_material.set_shader_parameter(Sky3D.SUN_DISK_SIZE_P, sun_disk_size) + + +func set_moon_color(value: Color) -> void: + if value == moon_color: + return + moon_color = value + update_moon_color() + + +func update_moon_color() -> void: + if !is_scene_built: + return + sky_material.set_shader_parameter(Sky3D.MOON_COLOR_P, moon_color) + + +func set_moon_size(value: float) -> void: + if value == moon_size: + return + moon_size = value + update_moon_size() + + +func update_moon_size() -> void: + if !is_scene_built: + return + sky_material.set_shader_parameter(Sky3D.MOON_SIZE_P, moon_size) + + +func set_enable_set_moon_texture(value: bool) -> void: + enable_set_moon_texture = value + if not value: + set_moon_texture(Sky3D._moon_texture) + notify_property_list_changed() + + +func set_moon_texture(value: Texture2D) -> void: + if value == moon_texture: + return + moon_texture = value + update_moon_texture() + + +func update_moon_texture() -> void: + if !is_scene_built: + return + moon_material.set_shader_parameter(Sky3D.TEXTURE_P, moon_texture) + + +func set_moon_resolution(value: int) -> void: + if value == moon_resolution: + return + moon_resolution = value + update_moon_resolution() + + +func update_moon_resolution() -> void: + if !is_scene_built: + return + match moon_resolution: + MoonResolution.R64: moon_render.size = Vector2.ONE * 64 + MoonResolution.R128: moon_render.size = Vector2.ONE * 128 + MoonResolution.R256: moon_render.size = Vector2.ONE * 256 + MoonResolution.R512: moon_render.size = Vector2.ONE * 512 + MoonResolution.R1024: moon_render.size = Vector2.ONE * 1024 + sky_material.set_shader_parameter(Sky3D.MOON_TEXTURE_P, moon_render.get_texture()) + + +##################### +## Sun +##################### + +# Original sun light (0.984314, 0.843137, 0.788235) +# Original sun horizon (1.0, 0.384314, 0.243137, 1.0) + +@export var sun_light_enable: bool = true +var sun_light_color:= Color.WHITE : set = set_sun_light_color +var sun_horizon_light_color:= Color(.98, 0.523, 0.294, 1.0): set = set_sun_horizon_light_color +var sun_light_energy: float = 1.0: set = set_sun_light_energy +var __sun_light_node: DirectionalLight3D = null +var sun_light_path: NodePath: set = set_sun_light_path + + +func set_sun_light_color(value: Color) -> void: + if value == sun_light_color: + return + sun_light_color = value + update_sun_light_color() + + +func update_sun_light_color() -> void: + if __sun_light_node == null: + return + var sun_light_altitude_mult: float = TOD_Math.saturate(sun_direction().y * 2) + __sun_light_node.light_color = TOD_Math.plerp_color(sun_horizon_light_color, sun_light_color, sun_light_altitude_mult) + + +func set_sun_horizon_light_color(value: Color) -> void: + if value == sun_horizon_light_color: + return + sun_horizon_light_color = value + update_sun_light_color() + + +func set_sun_light_energy(value: float) -> void: + if value == sun_light_energy: + return + sun_light_energy = value + update_sun_light_energy() + + +func update_sun_light_energy() -> void: + if __sun_light_node != null: + # Light energy should depend on how much of the sun disk is visible + var y = sun_direction().y + var sun_light_factor: float = TOD_Math.saturate((y + sun_disk_size) / (2 * sun_disk_size)); + __sun_light_node.light_energy = TOD_Math.lerp_f(0.0, sun_light_energy, sun_light_factor) + + +func set_sun_light_path(value: NodePath) -> void: + sun_light_path = value + update_sun_light_path() + update_sun_coords() + + +func update_sun_light_path() -> void: + if sun_light_path != null: + __sun_light_node = get_node_or_null(sun_light_path) as DirectionalLight3D + else: + __sun_light_node = null + + +##################### +## Moon +##################### + +@export var moon_light_enable: bool = true +var moon_light_color:= Color(0.572549, 0.776471, 0.956863, 1.0): set = set_moon_light_color +var moon_light_energy: float = 0.3: set = set_moon_light_energy +var moon_light_path: NodePath: set = set_moon_light_path +var __moon_light_node: DirectionalLight3D +var __moon_light_altitude_mult: float = 0.0 + + +func set_moon_light_color(value: Color) -> void: + if value == moon_light_color: + return + moon_light_color = value + update_moon_light_color() + + +func update_moon_light_color() -> void: + if __moon_light_node == null: + return + __moon_light_node.light_color = moon_light_color + + +func set_moon_light_energy(value: float) -> void: + moon_light_energy = value + update_moon_light_energy() + + +func update_moon_light_energy() -> void: + if __moon_light_node == null: + return + + var l: float = TOD_Math.lerp_f(0.0, moon_light_energy, __moon_light_altitude_mult) + l*= atm_moon_phases_mult() + + var fade = (1.0 - sun_direction().y) * 0.5 + __moon_light_node.light_energy = l * Sky3D._sun_moon_curve_fade.sample_baked(fade) + + +func set_moon_light_path(value: NodePath) -> void: + moon_light_path = value + update_moon_light_path() + update_moon_coords() + + +func update_moon_light_path() -> void: + if moon_light_path != null: + __moon_light_node = get_node_or_null(moon_light_path) as DirectionalLight3D + else: + __moon_light_node = null + + +##################### +## Deep space +##################### + +var deep_space_euler:= Vector3(-0.752, 2.56, 0.0): set = set_deep_space_euler +var deep_space_quat:= Quaternion.IDENTITY: set = set_deep_space_quat +var __deep_space_basis: Basis +var background_color:= Color(0.709804, 0.709804, 0.709804, 0.854902): set = set_background_color + + +func set_deep_space_euler(value: Vector3) -> void: + deep_space_euler = value + __deep_space_basis = Basis.from_euler(value) + update_deep_space_basis() + var quat: Quaternion = __deep_space_basis.get_rotation_quaternion() + if deep_space_quat.angle_to(quat) < 0.01: + return + deep_space_quat = quat + + +func set_deep_space_quat(value: Quaternion) -> void: + deep_space_quat = value + __deep_space_basis = Basis(value) + update_deep_space_basis() + var euler: Vector3 = __deep_space_basis.get_euler() + if deep_space_euler.angle_to(euler) < 0.01: + return + deep_space_euler = euler + + +func update_deep_space_basis() -> void: + if !is_scene_built: + return + sky_material.set_shader_parameter(Sky3D.DEEP_SPACE_MATRIX_P, __deep_space_basis) + + +func set_background_color(value: Color) -> void: + if value == background_color: + return + background_color = value + update_background_color() + + +func update_background_color() -> void: + if !is_scene_built: + return + sky_material.set_shader_parameter(Sky3D.BG_COL_P, background_color) + + +var set_background_texture: bool = false: set = set_set_background_texture +var background_texture: Texture2D = null: set = _set_background_texture +var stars_field_color:= Color.WHITE: set = set_stars_field_color +var set_stars_field_texture: bool = false: set = set_set_stars_field_texture +func set_set_background_texture(value: bool) -> void: + set_background_texture = value + if not value: + _set_background_texture(Sky3D._background_texture) + notify_property_list_changed() + + +func update_background_texture() -> void: + if !is_scene_built: + return + sky_material.set_shader_parameter(Sky3D.BG_TEXTURE_P, background_texture) + + +func _set_background_texture(value: Texture2D) -> void: + if value == background_texture: + return + background_texture = value + update_background_texture() + + +func update_stars_field_color() -> void: + if !is_scene_built: + return + sky_material.set_shader_parameter(Sky3D.STARS_COLOR_P, stars_field_color) + + +func set_stars_field_color(value: Color) -> void: + if value == stars_field_color: + return + stars_field_color = value + update_stars_field_color() + + +func set_set_stars_field_texture(value: bool) -> void: + set_stars_field_texture = value + if not value: + _set_stars_field_texture(Sky3D._stars_field_texture) + notify_property_list_changed() + + +func update_stars_field_texture() -> void: + if !is_scene_built: + return + sky_material.set_shader_parameter(Sky3D.STARS_TEXTURE_P, stars_field_texture) + + +var stars_field_texture: Texture2D = null: set = _set_stars_field_texture +var stars_scintillation: float = 0.75: set = set_stars_scintillation +var stars_scintillation_speed: float = 0.01: set = set_stars_scintillation_speed +func _set_stars_field_texture(value: Texture2D) -> void: + if value == stars_field_texture: + return + stars_field_texture = value + update_stars_field_texture() + + +func update_stars_scintillation() -> void: + if !is_scene_built: + return + sky_material.set_shader_parameter(Sky3D.STARS_SC_P, stars_scintillation) + + +func set_stars_scintillation(value: float) -> void: + if value == stars_scintillation: + return + stars_scintillation = value + update_stars_scintillation() + + +func update_stars_scintillation_speed() -> void: + if !is_scene_built: + return + sky_material.set_shader_parameter(Sky3D.STARS_SC_SPEED_P, stars_scintillation_speed) + + +func set_stars_scintillation_speed(value: float) -> void: + if value == stars_scintillation_speed: + return + stars_scintillation_speed = value + update_stars_scintillation_speed() + + +##################### +## 2D Clouds +##################### + +var clouds_thickness: float = 1.7: set = set_clouds_thickness +var clouds_coverage: float = 0.5: set = set_clouds_coverage +var clouds_absorption: float = 2.0: set = set_clouds_absorption +var clouds_sky_tint_fade: float = 0.5: set = set_clouds_sky_tint_fade +var clouds_intensity: float = 10.0: set = set_clouds_intensity +var clouds_size: float = 2.0: set = set_clouds_size +var clouds_uv:= Vector2(0.16, 0.11): set = set_clouds_uv +var clouds_direction:= Vector2(0.25, 0.25): set = set_clouds_direction +var clouds_speed: float = 0.07: set = set_clouds_speed +var set_clouds_texture: bool = false: set = set_set_clouds_texture +var clouds_texture: Texture2D = null: set = _set_clouds_texture + + +func set_clouds_thickness(value: float) -> void: + if value == clouds_thickness: + return + clouds_thickness = value + update_clouds_thickness() + + +func update_clouds_thickness() -> void: + if !is_scene_built: + return + sky_material.set_shader_parameter(Sky3D.CLOUDS_THICKNESS, clouds_thickness) + + +func set_clouds_coverage(value: float) -> void: + if value == clouds_coverage: + return + clouds_coverage = value + update_clouds_coverage() + + +func update_clouds_coverage() -> void: + if !is_scene_built: + return + sky_material.set_shader_parameter(Sky3D.CLOUDS_COVERAGE, clouds_coverage) + + +func set_clouds_absorption(value: float) -> void: + if value == clouds_absorption: + return + clouds_absorption = value + update_clouds_absorption() + + +func update_clouds_absorption() -> void: + if !is_scene_built: + return + sky_material.set_shader_parameter(Sky3D.CLOUDS_ABSORPTION, clouds_absorption) + + +func set_clouds_sky_tint_fade(value: float) -> void: + if value == clouds_sky_tint_fade: + return + clouds_sky_tint_fade = value + update_clouds_sky_tint_fade() + + +func update_clouds_sky_tint_fade() -> void: + if !is_scene_built: + return + sky_material.set_shader_parameter(Sky3D.CLOUDS_SKY_TINT_FADE, clouds_sky_tint_fade) + + +func set_clouds_intensity(value: float) -> void: + if value == clouds_intensity: + return + clouds_intensity = value + update_clouds_intensity() + + +func update_clouds_intensity() -> void: + if !is_scene_built: + return + sky_material.set_shader_parameter(Sky3D.CLOUDS_INTENSITY, clouds_intensity) + + +func set_clouds_size(value: float) -> void: + if value == clouds_size: + return + clouds_size = value + update_clouds_size() + + +func update_clouds_size() -> void: + if !is_scene_built: + return + sky_material.set_shader_parameter(Sky3D.CLOUDS_SIZE, clouds_size) + + +func set_clouds_uv(value: Vector2) -> void: + if value == clouds_uv: + return + clouds_uv = value + update_clouds_uv() + + +func update_clouds_uv() -> void: + if !is_scene_built: + return + sky_material.set_shader_parameter(Sky3D.CLOUDS_UV, clouds_uv) + + +func set_clouds_direction(value: Vector2) -> void: + if value == clouds_direction: + return + clouds_direction = value + update_clouds_direction() + + +func update_clouds_direction() -> void: + if !is_scene_built: + return + sky_material.set_shader_parameter(Sky3D.CLOUDS_DIRECTION, clouds_direction) + + +func set_clouds_speed(value: float) -> void: + if value == clouds_speed: + return + clouds_speed = value + update_clouds_speed() + + +func update_clouds_speed() -> void: + if !is_scene_built: + return + sky_material.set_shader_parameter(Sky3D.CLOUDS_SPEED, clouds_speed) + + +func set_set_clouds_texture(value: bool) -> void: + set_clouds_texture = value + if not value: + _set_clouds_texture(Sky3D._clouds_texture) + notify_property_list_changed() + + +func _set_clouds_texture(value: Texture2D) -> void: + if value == clouds_texture: + return + clouds_texture = value + update_clouds_texture() + + +func update_clouds_texture() -> void: + if !is_scene_built: + return + sky_material.set_shader_parameter(Sky3D.CLOUDS_TEXTURE, clouds_texture) + + +##################### +## Cumulus Clouds +##################### + +var clouds_cumulus_visible: bool = true: set = set_clouds_cumulus_visible +var clouds_cumulus_day_color:= Color(0.823529, 0.87451, 1.0, 1.0): set = set_clouds_cumulus_day_color +var clouds_cumulus_horizon_light_color:= Color(.98, 0.43, 0.15, 1.0): set = set_clouds_cumulus_horizon_light_color +var clouds_cumulus_night_color:= Color(0.090196, 0.094118, 0.129412, 1.0): set = set_clouds_cumulus_night_color +var clouds_cumulus_thickness: float = 0.0243: set = set_clouds_cumulus_thickness +var clouds_cumulus_coverage: float = 0.55: set = set_clouds_cumulus_coverage +var clouds_cumulus_absorption: float = 2.0: set = set_clouds_cumulus_absorption +var clouds_cumulus_noise_freq: float = 2.7: set = set_clouds_cumulus_noise_freq +var clouds_cumulus_intensity: float = 1.0: set = set_clouds_cumulus_intensity +var clouds_cumulus_mie_intensity: float = 1.0: set = set_clouds_cumulus_mie_intensity +var clouds_cumulus_mie_anisotropy: float = 0.206: set = set_clouds_cumulus_mie_anisotropy +var clouds_cumulus_size: float = 0.5: set = set_clouds_cumulus_size +var clouds_cumulus_direction:= Vector3(0.25, 0.1, 0.25): set = set_clouds_cumulus_direction +var clouds_cumulus_speed: float = 0.05: set = set_clouds_cumulus_speed +var set_clouds_cumulus_texture: bool = false: set = set_set_clouds_cumulus_texture +var clouds_cumulus_texture: Texture2D = null: set = _set_clouds_cumulus_texture + + +func set_clouds_cumulus_visible(value: bool) -> void: + if value == clouds_cumulus_visible: + return + clouds_cumulus_visible = value + update_clouds_cumulus_visible() + + +func update_clouds_cumulus_visible() -> void: + if !is_scene_built: + return + clouds_cumulus_mesh.visible = clouds_cumulus_visible + + +func set_clouds_cumulus_day_color(value: Color) -> void: + if value == clouds_cumulus_day_color: + return + clouds_cumulus_day_color = value + update_clouds_cumulus_day_color() + + +func update_clouds_cumulus_day_color() -> void: + if !is_scene_built: + return + clouds_cumulus_material.set_shader_parameter(Sky3D.CLOUDS_DAY_COLOR, clouds_cumulus_day_color) + sky_material.set_shader_parameter(Sky3D.CLOUDS_DAY_COLOR, clouds_cumulus_day_color) + + +func set_clouds_cumulus_horizon_light_color(value: Color) -> void: + if value == clouds_cumulus_horizon_light_color: + return + clouds_cumulus_horizon_light_color = value + update_clouds_cumulus_horizon_light_color() + + +func update_clouds_cumulus_horizon_light_color() -> void: + if !is_scene_built: + return + clouds_cumulus_material.set_shader_parameter(Sky3D.CLOUDS_HORIZON_LIGHT_COLOR, clouds_cumulus_horizon_light_color) + sky_material.set_shader_parameter(Sky3D.CLOUDS_HORIZON_LIGHT_COLOR, clouds_cumulus_horizon_light_color) + + +func set_clouds_cumulus_night_color(value: Color) -> void: + if value == clouds_cumulus_night_color: + return + clouds_cumulus_night_color = value + update_clouds_cumulus_night_color() + + +func update_clouds_cumulus_night_color() -> void: + if !is_scene_built: + return + clouds_cumulus_material.set_shader_parameter(Sky3D.CLOUDS_NIGHT_COLOR, clouds_cumulus_night_color) + sky_material.set_shader_parameter(Sky3D.CLOUDS_NIGHT_COLOR, clouds_cumulus_night_color) + + +func set_clouds_cumulus_thickness(value: float) -> void: + if value == clouds_cumulus_thickness: + return + clouds_cumulus_thickness = value + update_clouds_cumulus_thickness() + + +func update_clouds_cumulus_thickness() -> void: + if !is_scene_built: + return + clouds_cumulus_material.set_shader_parameter(Sky3D.CLOUDS_THICKNESS, clouds_cumulus_thickness) + + +func set_clouds_cumulus_coverage(value: float) -> void: + if value == clouds_cumulus_coverage: + return + clouds_cumulus_coverage = value + update_clouds_cumulus_coverage() + + +func update_clouds_cumulus_coverage() -> void: + if !is_scene_built: + return + clouds_cumulus_material.set_shader_parameter(Sky3D.CLOUDS_COVERAGE, clouds_cumulus_coverage) + + +func set_clouds_cumulus_absorption(value: float) -> void: + if value == clouds_cumulus_absorption: + return + clouds_cumulus_absorption = value + update_clouds_cumulus_absorption() + + +func update_clouds_cumulus_absorption() -> void: + if !is_scene_built: + return + clouds_cumulus_material.set_shader_parameter(Sky3D.CLOUDS_ABSORPTION, clouds_cumulus_absorption) + + +func set_clouds_cumulus_noise_freq(value: float) -> void: + if value == clouds_cumulus_noise_freq: + return + clouds_cumulus_noise_freq = value + update_clouds_cumulus_noise_freq() + + +func update_clouds_cumulus_noise_freq() -> void: + if !is_scene_built: + return + clouds_cumulus_material.set_shader_parameter(Sky3D.CLOUDS_NOISE_FREQ, clouds_cumulus_noise_freq) + + +func set_clouds_cumulus_intensity(value: float) -> void: + if value == clouds_cumulus_intensity: + return + clouds_cumulus_intensity = value + update_clouds_cumulus_intensity() + + +func update_clouds_cumulus_intensity() -> void: + if !is_scene_built: + return + clouds_cumulus_material.set_shader_parameter(Sky3D.CLOUDS_INTENSITY, clouds_cumulus_intensity) + + +func set_clouds_cumulus_mie_intensity(value: float) -> void: + if value == clouds_cumulus_mie_intensity: + return + clouds_cumulus_mie_intensity = value + update_clouds_cumulus_mie_intensity() + + +func update_clouds_cumulus_mie_intensity() -> void: + if !is_scene_built: + return + clouds_cumulus_material.set_shader_parameter(Sky3D.CLOUDS_MIE_INTENSITY, clouds_cumulus_mie_intensity) + + +func set_clouds_cumulus_mie_anisotropy(value: float) -> void: + if value == clouds_cumulus_mie_anisotropy: + return + clouds_cumulus_mie_anisotropy = value + update_clouds_cumulus_mie_anisotropy() + + +func update_clouds_cumulus_mie_anisotropy() -> void: + if !is_scene_built: + return + var partial = ScatterLib.get_partial_mie_phase(clouds_cumulus_mie_anisotropy) + clouds_cumulus_material.set_shader_parameter(Sky3D.CLOUDS_PARTIAL_MIE_PHASE, partial) + + +func set_clouds_cumulus_size(value: float) -> void: + if value == clouds_cumulus_size: + return + clouds_cumulus_size = value + update_clouds_cumulus_size() + + +func update_clouds_cumulus_size() -> void: + if !is_scene_built: + return + clouds_cumulus_material.set_shader_parameter(Sky3D.CLOUDS_SIZE, clouds_cumulus_size) + + +func set_clouds_cumulus_direction(value: Vector3) -> void: + if value == clouds_cumulus_direction: + return + clouds_cumulus_direction = value + update_clouds_cumulus_direction() + + +func update_clouds_cumulus_direction() -> void: + if !is_scene_built: + return + clouds_cumulus_material.set_shader_parameter(Sky3D.CLOUDS_DIRECTION, clouds_cumulus_direction) + + +func set_clouds_cumulus_speed(value: float) -> void: + if value == clouds_cumulus_speed: + return + clouds_cumulus_speed = value + update_clouds_cumulus_speed() + + +func update_clouds_cumulus_speed() -> void: + if !is_scene_built: + return + clouds_cumulus_material.set_shader_parameter(Sky3D.CLOUDS_SPEED, clouds_cumulus_speed) + + +func set_set_clouds_cumulus_texture(value: bool) -> void: + set_clouds_cumulus_texture = value + if not value: + _set_clouds_cumulus_texture(Sky3D._clouds_cumulus_texture) + notify_property_list_changed() + + +func _set_clouds_cumulus_texture(value: Texture2D) -> void: + if value == clouds_cumulus_texture: + return + clouds_cumulus_texture = value + update_clouds_cumulus_texture() + + +func update_clouds_cumulus_texture() -> void: + if !is_scene_built: + return + clouds_cumulus_material.set_shader_parameter(Sky3D.CLOUDS_TEXTURE, clouds_cumulus_texture) + + +##################### +## Environment +##################### + +var __enable_environment: bool = false +var environment: Environment = null: set = set_environment + + +func set_environment(value: Environment) -> void: + environment = value + __enable_environment = true if environment != null else false + if __enable_environment: + __update_environment() + + +func __update_environment() -> void: + if not __enable_environment or not __sun_light_node: + return + var factor = TOD_Math.saturate(-sun_direction().y + 0.60) + var col = TOD_Math.plerp_color(__sun_light_node.light_color, atm_night_tint * atm_night_intensity(), factor) + col.a = 1. + col.v = clamp(col.v, .35, 1.) + environment.ambient_light_color = col + + + +##################### +## Lighting +##################### + +var __day: bool: get = is_day + + +func is_day() -> bool: + return __day == true + + +func __set_day_state(v: float, threshold: float = 1.80) -> void: + # Signal when day has changed to night, or back + if __day == true and abs(v) > threshold: + __day = false + emit_signal("day_night_changed", __day) + elif __day == false and abs(v) <= threshold: + __day = true + emit_signal("day_night_changed", __day) + + # Adjust lights and signal. Happens at a different time from "day time" above + if __sun_light_node and __moon_light_node: + if sun_light_enable and not __sun_light_node.visible and __sun_light_node.light_energy > 0.0: + __sun_light_node.visible = true + __moon_light_node.visible = false + emit_signal("lights_changed") + elif moon_light_enable and not __moon_light_node.visible and __moon_light_node.light_energy > 0.0: + __sun_light_node.visible = false + __moon_light_node.visible = true + emit_signal("lights_changed") + + +func _get_property_list() -> Array: + var ret:= Array() + ret.push_back({name = "Skydome", type = TYPE_NIL, usage = PROPERTY_USAGE_CATEGORY}) + + # Global + ret.push_back({name = "Global", type = TYPE_NIL, usage = PROPERTY_USAGE_GROUP}) + ret.push_back({name = "sky_visible", type = TYPE_BOOL}) + ret.push_back({name = "dome_radius", type = TYPE_FLOAT}) + ret.push_back({name = "tonemap_level", type = TYPE_FLOAT, hint = PROPERTY_HINT_RANGE, hint_string = "0.0, 1.0"}) + ret.push_back({name = "exposure", type = TYPE_FLOAT}) + ret.push_back({name = "ground_color", type = TYPE_COLOR}) + ret.push_back({name = "sky_layers", type = TYPE_INT, hint = PROPERTY_HINT_LAYERS_3D_RENDER}) + ret.push_back({name = "sky_render_priority", type = TYPE_INT, hint = PROPERTY_HINT_RANGE, hint_string = "-128, 127"}) + ret.push_back({name = "horizon_level", type = TYPE_FLOAT}) + + # Sun + ret.push_back({name = "Sun", type = TYPE_NIL, usage = PROPERTY_USAGE_GROUP}) + ret.push_back({name = "sun_altitude", type = TYPE_FLOAT, hint = PROPERTY_HINT_RANGE, hint_string = "-180.0, 180.0"}) + ret.push_back({name = "sun_azimuth", type = TYPE_FLOAT, hint = PROPERTY_HINT_RANGE, hint_string = "-180.0, 180.0"}) + ret.push_back({name = "sun_disk_color", type = TYPE_COLOR}) + ret.push_back({name = "sun_disk_intensity", type = TYPE_FLOAT, hint = PROPERTY_HINT_RANGE, hint_string = "0.0, 100.0"}) + ret.push_back({name = "sun_disk_size", type = TYPE_FLOAT, hint = PROPERTY_HINT_RANGE, hint_string = "0.0, 0.5"}) + ret.push_back({name = "sun_light_path", type = TYPE_NODE_PATH}) + ret.push_back({name = "sun_light_color", type = TYPE_COLOR}) + ret.push_back({name = "sun_horizon_light_color", type = TYPE_COLOR}) + ret.push_back({name = "sun_light_energy", type = TYPE_FLOAT}) + + # Moon + ret.push_back({name = "Moon", type = TYPE_NIL, usage = PROPERTY_USAGE_GROUP}) + ret.push_back({name = "moon_altitude", type = TYPE_FLOAT, hint = PROPERTY_HINT_RANGE, hint_string = "-180.0, 180.0"}) + ret.push_back({name = "moon_azimuth", type = TYPE_FLOAT, hint = PROPERTY_HINT_RANGE, hint_string = "-180.0, 180.0"}) + ret.push_back({name = "moon_color", type = TYPE_COLOR}) + ret.push_back({name = "moon_size", type = TYPE_FLOAT}) + ret.push_back({name = "enable_set_moon_texture", type = TYPE_BOOL}) + + if enable_set_moon_texture: + ret.push_back({name = "moon_texture", type = TYPE_OBJECT, hint = PROPERTY_HINT_FILE, hint_string = "Texture2D"}) + + ret.push_back({name = "moon_resolution", type = TYPE_INT, hint = PROPERTY_HINT_ENUM, hint_string = "64, 128, 256, 512, 1024"}) + ret.push_back({name = "moon_light_path", type = TYPE_NODE_PATH}) + + ret.push_back({name = "moon_light_color", type = TYPE_COLOR}) + ret.push_back({name = "moon_light_energy", type = TYPE_FLOAT}) + + # Deep space + ret.push_back({name = "DeepSpace", type = TYPE_NIL, usage = PROPERTY_USAGE_GROUP}) + ret.push_back({name = "deep_space_euler", type = TYPE_VECTOR3}) + ret.push_back({name = "background_color", type = TYPE_COLOR}) + ret.push_back({name = "set_background_texture", type = TYPE_BOOL}) + + if set_background_texture: + ret.push_back({name = "background_texture", type = TYPE_OBJECT, hint = PROPERTY_HINT_GLOBAL_FILE, hint_string = "Texture2D"}) + + ret.push_back({name = "stars_field_color", type = TYPE_COLOR}) + ret.push_back({name = "set_stars_field_texture", type = TYPE_BOOL}) + + if set_stars_field_texture: + ret.push_back({name = "stats_field_texture", type = TYPE_OBJECT, hint = PROPERTY_HINT_FILE, hint_string = "Texture2D"}) + + ret.push_back({name = "stars_scintillation", type = TYPE_FLOAT, hint = PROPERTY_HINT_RANGE, hint_string = "0.0, 1.0"}) + ret.push_back({name = "stars_scintillation_speed", type = TYPE_FLOAT}) + + # Atmosphere + ret.push_back({name = "Atmosphere", type = TYPE_NIL, usage = PROPERTY_USAGE_GROUP, hint_string = "atm_"}) + ret.push_back({name = "atm_quality", type = TYPE_INT, hint = PROPERTY_HINT_ENUM, hint_string = "PerPixel,PerVertex"}) + ret.push_back({name = "atm_wavelenghts", type = TYPE_VECTOR3}) + ret.push_back({name = "atm_darkness", type = TYPE_FLOAT, hint = PROPERTY_HINT_RANGE, hint_string = "0.0, 1.0"}) + ret.push_back({name = "atm_sun_intensity", type = TYPE_FLOAT}) + ret.push_back({name = "atm_day_tint", type = TYPE_COLOR}) + ret.push_back({name = "atm_horizon_light_tint", type = TYPE_COLOR}) + ret.push_back({name = "atm_enable_moon_scatter_mode", type = TYPE_BOOL}) + ret.push_back({name = "atm_night_tint", type = TYPE_COLOR}) + ret.push_back({name = "atm_level_params", type = TYPE_VECTOR3}) + ret.push_back({name = "atm_thickness", type = TYPE_FLOAT, hint = PROPERTY_HINT_RANGE, hint_string = "0.0, 100.0"}) + ret.push_back({name = "atm_mie", type = TYPE_FLOAT}) + ret.push_back({name = "atm_turbidity", type = TYPE_FLOAT}) + ret.push_back({name = "atm_sun_mie_tint", type = TYPE_COLOR}) + ret.push_back({name = "atm_sun_mie_intensity", type = TYPE_FLOAT}) + ret.push_back({name = "atm_sun_mie_anisotropy", type = TYPE_FLOAT, hint = PROPERTY_HINT_RANGE, hint_string = "0.0, 0.9999999"}) + + ret.push_back({name = "atm_moon_mie_tint", type = TYPE_COLOR}) + ret.push_back({name = "atm_moon_mie_intensity", type = TYPE_FLOAT}) + ret.push_back({name = "atm_moon_mie_anisotropy", type = TYPE_FLOAT, hint = PROPERTY_HINT_RANGE, hint_string = "0.0, 0.9999999"}) + + # Fog + ret.push_back({name = "Fog", type = TYPE_NIL, usage = PROPERTY_USAGE_GROUP, hint_string = "fog_"}) + ret.push_back({name = "fog_visible", type = TYPE_BOOL}) + ret.push_back({name = "fog_atm_level_params_offset", type = TYPE_VECTOR3}) + ret.push_back({name = "fog_density", type = TYPE_FLOAT, hint = PROPERTY_HINT_EXP_EASING, hint_string = "0.0, 1.0"}) + ret.push_back({name = "fog_rayleigh_depth", type = TYPE_FLOAT, hint = PROPERTY_HINT_EXP_EASING, hint_string = "0.0, 1.0"}) + ret.push_back({name = "fog_mie_depth", type = TYPE_FLOAT, hint = PROPERTY_HINT_EXP_EASING, hint_string = "0.0, 1.0"}) + ret.push_back({name = "fog_falloff", type = TYPE_FLOAT, hint = PROPERTY_HINT_RANGE, hint_string = "0.0, 10.0"}) + ret.push_back({name = "fog_start", type = TYPE_FLOAT, hint = PROPERTY_HINT_RANGE, hint_string = "0.0, 5000.0"}) + ret.push_back({name = "fog_end", type = TYPE_FLOAT, hint = PROPERTY_HINT_RANGE, hint_string = "0.0, 5000.0"}) + ret.push_back({name = "fog_layers", type = TYPE_INT, hint = PROPERTY_HINT_LAYERS_3D_RENDER}) + ret.push_back({name = "fog_render_priority", type = TYPE_INT}) + + # 2D Clouds + ret.push_back({name = "2D Clouds", type = TYPE_NIL, usage = PROPERTY_USAGE_GROUP}) + ret.push_back({name = "clouds_thickness", type = TYPE_FLOAT}) + ret.push_back({name = "clouds_coverage", type = TYPE_FLOAT, hint = PROPERTY_HINT_RANGE, hint_string = "0.0, 1.0"}) + ret.push_back({name = "clouds_absorption", type = TYPE_FLOAT}) + ret.push_back({name = "clouds_sky_tint_fade", type = TYPE_FLOAT, hint = PROPERTY_HINT_RANGE, hint_string = "0.0, 1.0"}) + ret.push_back({name = "clouds_intensity", type = TYPE_FLOAT}) + ret.push_back({name = "clouds_size", type = TYPE_FLOAT}) + ret.push_back({name = "clouds_uv", type = TYPE_VECTOR2}) + ret.push_back({name = "clouds_direction", type = TYPE_VECTOR2}) + ret.push_back({name = "clouds_speed", type = TYPE_FLOAT}) + ret.push_back({name = "set_clouds_texture", type = TYPE_BOOL}) + + if set_clouds_texture: + ret.push_back({name = "clouds_texture", type = TYPE_OBJECT, hint = PROPERTY_HINT_FILE, hint_string = "Texture2D"}) + + # Clouds cumulus + ret.push_back({name = "Clouds Cumulus", type = TYPE_NIL, usage = PROPERTY_USAGE_GROUP}) + ret.push_back({name = "clouds_cumulus_visible", type = TYPE_BOOL}) + ret.push_back({name = "clouds_cumulus_day_color", type = TYPE_COLOR}) + ret.push_back({name = "clouds_cumulus_horizon_light_color", type = TYPE_COLOR}) + ret.push_back({name = "clouds_cumulus_night_color", type = TYPE_COLOR}) + ret.push_back({name = "clouds_cumulus_thickness", type = TYPE_FLOAT}) + ret.push_back({name = "clouds_cumulus_coverage", type = TYPE_FLOAT, hint = PROPERTY_HINT_RANGE, hint_string = "0.0, 1.0"}) + ret.push_back({name = "clouds_cumulus_absorption", type = TYPE_FLOAT}) + ret.push_back({name = "clouds_cumulus_noise_freq", type = TYPE_FLOAT, hint = PROPERTY_HINT_RANGE, hint_string = "0.0, 3.0"}) + ret.push_back({name = "clouds_cumulus_intensity", type = TYPE_FLOAT}) + ret.push_back({name = "clouds_cumulus_mie_intensity", type = TYPE_FLOAT}) + ret.push_back({name = "clouds_cumulus_mie_anisotropy", type = TYPE_FLOAT, hint = PROPERTY_HINT_RANGE, hint_string = "0.0, 0.999999"}) + ret.push_back({name = "clouds_cumulus_size", type = TYPE_FLOAT}) + ret.push_back({name = "clouds_cumulus_direction", type = TYPE_VECTOR3}) + ret.push_back({name = "clouds_cumulus_speed", type = TYPE_FLOAT}) + ret.push_back({name = "set_clouds_cumulus_texture", type = TYPE_BOOL}) + + if set_clouds_cumulus_texture: + ret.push_back({name = "clouds_cumulus_texture", type = TYPE_OBJECT, hint = PROPERTY_HINT_FILE, hint_string = "Texture2D"}) + + # Lighting + ret.push_back({name = "Lighting", type = TYPE_NIL, usage = PROPERTY_USAGE_GROUP}) + ret.push_back({name = "environment", type = TYPE_OBJECT, hint = PROPERTY_HINT_RESOURCE_TYPE, hint_string = "Resource"}) + + return ret diff --git a/addons/sky_3d/src/Skydome.gd.uid b/addons/sky_3d/src/Skydome.gd.uid new file mode 100644 index 0000000..aae0638 --- /dev/null +++ b/addons/sky_3d/src/Skydome.gd.uid @@ -0,0 +1 @@ +uid://27fj74ofndim diff --git a/addons/sky_3d/src/TOD_Math.gd b/addons/sky_3d/src/TOD_Math.gd new file mode 100644 index 0000000..29c8dc4 --- /dev/null +++ b/addons/sky_3d/src/TOD_Math.gd @@ -0,0 +1,77 @@ +# Copyright (c) 2023-2025 Cory Petkovsek and Contributors +# Copyright (c) 2021 J. Cuellar + +class_name TOD_Math + + +const RAD_TO_DEG: float = 57.2957795 +const DEG_TO_RAD: float = 0.0174533 + + +static func saturate(value: float) -> float: + return 0.0 if value < 0.0 else 1.0 if value > 1.0 else value + + +static func saturate_vec3(value: Vector3) -> Vector3: + var ret: Vector3 + ret.x = 0.0 if value.x < 0.0 else 1.0 if value.x > 1.0 else value.x + ret.y = 0.0 if value.y < 0.0 else 1.0 if value.y > 1.0 else value.y + ret.z = 0.0 if value.z < 0.0 else 1.0 if value.z > 1.0 else value.z + return ret + + +static func saturate_color(value: Color) -> Color: + var ret: Color + ret.r = 0.0 if value.r < 0.0 else 1.0 if value.r > 1.0 else value.r + ret.g = 0.0 if value.g < 0.0 else 1.0 if value.g > 1.0 else value.g + ret.b = 0.0 if value.b < 0.0 else 1.0 if value.b > 1.0 else value.b + ret.a = 0.0 if value.a < 0.0 else 1.0 if value.a > 1.0 else value.a + return ret + + +static func rev(val: float) -> float: + return val - int(floor(val / 360.0)) * 360.0 + + +static func lerp_f(from: float, to: float, t: float) -> float: + return (1 - t) * from + t * to + + +static func plerp_vec3(from: Vector3, to: Vector3, t: float) -> Vector3: + var ret: Vector3 + ret.x = (1 - t) * from.x + t * to.x + ret.y = (1 - t) * from.y + t * to.y + ret.z = (1 - t) * from.z + t * to.z + return ret + + +static func plerp_color(from: Color, to: Color, t: float) -> Color: + var ret: Color + ret.r = (1 - t) * from.r + t * to.r + ret.g = (1 - t) * from.g + t * to.g + ret.b = (1 - t) * from.b + t * to.b + ret.a = (1 - t) * from.a + t * to.a + return ret + + +static func distance(a: Vector3, b: Vector3) -> float: + var ret: float + var x: float = a.x - b.x + var y: float = a.y - b.y + var z: float = a.z - b.z + ret = x * x + y * y + z * z + return sqrt(ret) + + +static func to_orbit(theta: float, pi: float, radius: float = 1.0) -> Vector3: + var ret: Vector3 + var sinTheta: float = sin(theta) + var cosTheta: float = cos(theta) + var sinPI: float = sin(pi) + var cosPI: float = cos(pi) + + ret.x = sinTheta * sinPI + ret.y = cosTheta + ret.z = sinTheta * cosPI + return ret * radius + diff --git a/addons/sky_3d/src/TOD_Math.gd.uid b/addons/sky_3d/src/TOD_Math.gd.uid new file mode 100644 index 0000000..b1dd8ee --- /dev/null +++ b/addons/sky_3d/src/TOD_Math.gd.uid @@ -0,0 +1 @@ +uid://dtsesagkhvc07 diff --git a/addons/sky_3d/src/TimeOfDay.gd b/addons/sky_3d/src/TimeOfDay.gd new file mode 100644 index 0000000..d9bedc0 --- /dev/null +++ b/addons/sky_3d/src/TimeOfDay.gd @@ -0,0 +1,592 @@ +# Copyright (c) 2023-2025 Cory Petkovsek and Contributors +# Copyright (c) 2021 J. Cuellar + +@tool +class_name TimeOfDay +extends Node + + +signal time_changed(value) +signal day_changed(value) +signal month_changed(value) +signal year_changed(value) + +var _update_timer: Timer +var _last_update: int = 0 + + +@export var update_in_game: bool = true : + set(value): + update_in_game = value + if not Engine.is_editor_hint(): + if update_in_game: + resume() + else: + pause() + + +@export var update_in_editor: bool = true : + set(value): + update_in_editor = value + if Engine.is_editor_hint(): + if update_in_editor: + resume() + else: + pause() + + +@export_range(.016, 10) var update_interval: float = 0.1 : + set(value): + update_interval = clamp(value, .016, 10) + if is_instance_valid(_update_timer): + _update_timer.wait_time = update_interval + resume() + + +func _init() -> void: + set_total_hours(total_hours) + set_day(day) + set_month(month) + set_year(year) + set_latitude(latitude) + set_longitude(longitude) + set_utc(utc) + + +func _ready() -> void: + set_dome_path(dome_path) + + _update_timer = Timer.new() + _update_timer.name = "Timer" + add_child(_update_timer) + _update_timer.timeout.connect(_on_timeout) + _update_timer.wait_time = update_interval + resume() + + +func _on_timeout() -> void: + if system_sync: + __get_date_time_os() + else: + var delta: float = .001 * (Time.get_ticks_msec() - _last_update) + __progress_time(delta) + __update_celestial_coords() + _last_update = Time.get_ticks_msec() + + +func pause() -> void: + if is_instance_valid(_update_timer): + _update_timer.stop() + + +func resume() -> void: + if is_instance_valid(_update_timer): + # Assume resuming from a pause, so timer only gets one tick + _last_update = Time.get_ticks_msec() - update_interval + if (Engine.is_editor_hint() and update_in_editor) or \ + (not Engine.is_editor_hint() and update_in_game): + _update_timer.start() + + +##################### +## Target +##################### + +var __dome: Skydome = null +var __dome_found: bool = false +var dome_path: NodePath: set = set_dome_path + + +func set_dome_path(value: NodePath) -> void: + dome_path = value + if value != null: + __dome = get_node_or_null(value) as Skydome + + __dome_found = false if __dome == null else true + __update_celestial_coords() + + +##################### +## DateTime +##################### + +var date_time_os: Dictionary +var system_sync: bool = false +var total_cycle_in_minutes: float = 15.0 +var total_hours: float = 7.0 : set = set_total_hours +var day: int = 1: set = set_day +var month: int = 1: set = set_month +var year: int = 2025: set = set_year + + +func set_total_hours(value: float) -> void: + if total_hours != value: + total_hours = value + while total_hours > 23.9999: + total_hours -= 24 + day += 1 + while total_hours < 0.0000: + total_hours += 24 + day -= 1 + emit_signal("time_changed", total_hours) + __update_celestial_coords() + + +func set_day(value: int) -> void: + if day != value: + day = value + while day > max_days_per_month(): + day -= max_days_per_month() + month += 1 + while day < 1: + month -= 1 + day += max_days_per_month() + emit_signal("day_changed", day) + __update_celestial_coords() + + +func set_month(value: int) -> void: + if month != value: + month = value + while month > 12: + month -= 12 + year += 1 + while month < 1: + month += 12 + year -= 1 + emit_signal("month_changed", month) + __update_celestial_coords() + + +func set_year(value: int) -> void: + if year != value: + year = value + emit_signal("year_changed", year) + __update_celestial_coords() + + +func is_learp_year() -> bool: + return DateTimeUtil.compute_leap_year(year) + + +func max_days_per_month() -> int: + match month: + 1, 3, 5, 7, 8, 10, 12: + return 31 + 2: + return 29 if is_learp_year() else 28 + return 30 + + +func time_cycle_duration() -> float: + return total_cycle_in_minutes * 60.0 + + +func is_begin_of_time() -> bool: + return year == 1 && month == 1 && day == 1 + + +func is_end_of_time() -> bool: + return year == 9999 && month == 12 && day == 31 + + +##################### +## Planetary +##################### + +enum CelestialCalculationsMode{ + Simple = 0, + Realistic +} + +var celestials_calculations: int = 1: set = set_celestials_calculations +var latitude: float = 16.0: set = set_latitude +var longitude: float = 108.0: set = set_longitude +var utc: float = 7.0: set = set_utc +var compute_moon_coords: bool = true: set = set_compute_moon_coords +var compute_deep_space_coords: bool = true: set = set_compute_deep_space_coords +var moon_coords_offset := Vector2(0.0, 0.0): set = set_moon_coords_offset +var __sun_coords := Vector2.ZERO +var __moon_coords := Vector2.ZERO +var __sun_distance: float +var __true_sun_longitude: float +var __mean_sun_longitude: float +var __sideral_time: float +var __local_sideral_time: float +var __sun_orbital_elements := OrbitalElements.new() +var __moon_orbital_elements := OrbitalElements.new() + + +func set_celestials_calculations(value: int) -> void: + celestials_calculations = value + __update_celestial_coords() + notify_property_list_changed() + + +func set_latitude(value: float) -> void: + latitude = value + __update_celestial_coords() + + +func set_longitude(value: float) -> void: + longitude = value + __update_celestial_coords() + + +func set_utc(value: float) -> void: + utc = value + __update_celestial_coords() + + +func set_compute_moon_coords(value: bool) -> void: + compute_moon_coords = value + __update_celestial_coords() + notify_property_list_changed() + + +func set_compute_deep_space_coords(value: bool) -> void: + compute_deep_space_coords = value + __update_celestial_coords() + + +func set_moon_coords_offset(value: Vector2) -> void: + moon_coords_offset = value + __update_celestial_coords() + + +func __get_latitude_rad() -> float: + return latitude * TOD_Math.DEG_TO_RAD + + +func __get_total_hours_utc() -> float: + return total_hours - utc + + +func __get_time_scale() -> float: + return (367.0 * year - (7.0 * (year + ((month + 9.0) / 12.0))) / 4.0 +\ + (275.0 * month) / 9.0 + day - 730530.0) + total_hours / 24.0 + + +func __get_oblecl() -> float: + return (23.4393 - 2.563e-7 * __get_time_scale()) * TOD_Math.DEG_TO_RAD + + +##################### +## DateTime +##################### + + +func set_time(hour: int, minute: int, second: int) -> void: + set_total_hours(DateTimeUtil.hours_to_total_hours(hour, minute, second)) + + +func set_from_datetime_dict(datetime_dict: Dictionary) -> void: + set_year(datetime_dict.year) + set_month(datetime_dict.month) + set_day(datetime_dict.day) + set_time(datetime_dict.hour, datetime_dict.minute, datetime_dict.second) + + +func get_datetime_dict() -> Dictionary: + var datetime_dict := { + "year": year, + "month": month, + "day": day, + "hour": floor(total_hours), + "minute": floor(fmod(total_hours, 1.0) * 60.0), + "second": floor(fmod(total_hours * 60.0, 1.0) * 60.0) + } + return datetime_dict + + +func set_from_unix_timestamp(timestamp: int) -> void: + set_from_datetime_dict(Time.get_datetime_dict_from_unix_time(timestamp)) + + +func get_unix_timestamp() -> int: + return Time.get_unix_time_from_datetime_dict(get_datetime_dict()) + + +func __progress_time(delta: float) -> void: + if not is_zero_approx(time_cycle_duration()): + set_total_hours(total_hours + delta / time_cycle_duration() * DateTimeUtil.TOTAL_HOURS) + + +func __get_date_time_os() -> void: + date_time_os = Time.get_datetime_dict_from_system() + set_time(date_time_os.hour, date_time_os.minute, date_time_os.second) + set_day(date_time_os.day) + set_month(date_time_os.month) + set_year(date_time_os.year) + + +##################### +## Planetary +##################### + + +func __update_celestial_coords() -> void: + if not __dome_found: + return + + match celestials_calculations: + CelestialCalculationsMode.Simple: + __compute_simple_sun_coords() + __dome.sun_altitude = __sun_coords.y + __dome.sun_azimuth = __sun_coords.x + if compute_moon_coords: + __compute_simple_moon_coords() + __dome.moon_altitude = __moon_coords.y + __dome.moon_azimuth = __moon_coords.x + + if compute_deep_space_coords: + var x = Quaternion.from_euler(Vector3( (90 + latitude) * TOD_Math.DEG_TO_RAD, 0.0, 0.0)) + var y = Quaternion.from_euler(Vector3(0.0, 0.0, __sun_coords.y * TOD_Math.DEG_TO_RAD)) + __dome.deep_space_quat = x * y + + CelestialCalculationsMode.Realistic: + __compute_realistic_sun_coords() + __dome.sun_altitude = -__sun_coords.y * TOD_Math.RAD_TO_DEG + __dome.sun_azimuth = -__sun_coords.x * TOD_Math.RAD_TO_DEG + if compute_moon_coords: + __compute_realistic_moon_coords() + __dome.moon_altitude = -__moon_coords.y * TOD_Math.RAD_TO_DEG + __dome.moon_azimuth = -__moon_coords.x * TOD_Math.RAD_TO_DEG + + if compute_deep_space_coords: + var x = Quaternion.from_euler(Vector3( (90 + latitude) * TOD_Math.DEG_TO_RAD, 0.0, 0.0) ) + var y = Quaternion.from_euler(Vector3(0.0, 0.0, (180.0 - __local_sideral_time * TOD_Math.RAD_TO_DEG) * TOD_Math.DEG_TO_RAD)) + __dome.deep_space_quat = x * y + + +func __compute_simple_sun_coords() -> void: + var altitude = (__get_total_hours_utc() + (TOD_Math.DEG_TO_RAD * longitude)) * (360/24) + __sun_coords.y = (180.0 - altitude) + __sun_coords.x = latitude + + +func __compute_simple_moon_coords() -> void: + __moon_coords.y = (180.0 - __sun_coords.y) + moon_coords_offset.y + __moon_coords.x = (180.0 + __sun_coords.x) + moon_coords_offset.x + + +func __compute_realistic_sun_coords() -> void: + # Orbital Elements + __sun_orbital_elements.get_orbital_elements(0, __get_time_scale()) + __sun_orbital_elements.M = TOD_Math.rev(__sun_orbital_elements.M) + + # Mean anomaly in radians + var MRad: float = TOD_Math.DEG_TO_RAD * __sun_orbital_elements.M + + # Eccentric Anomaly + var E: float = __sun_orbital_elements.M + TOD_Math.RAD_TO_DEG * __sun_orbital_elements.e *\ + sin(MRad) * (1 + __sun_orbital_elements.e * cos(MRad)) + + var ERad: float = E * TOD_Math.DEG_TO_RAD + + # Rectangular coordinates of the sun in the plane of the ecliptic + var xv: float = cos(ERad) - __sun_orbital_elements.e + var yv: float = sin(ERad) * sqrt(1 - __sun_orbital_elements.e * __sun_orbital_elements.e) + + # Distance and true anomaly + # Convert to distance and true anomaly(r = radians, v = degrees) + var r: float = sqrt(xv * xv + yv * yv) + var v: float = TOD_Math.RAD_TO_DEG * atan2(yv, xv) + __sun_distance = r + + # True longitude + var lonSun: float = v + __sun_orbital_elements.w + lonSun = TOD_Math.rev(lonSun) + + var lonSunRad = TOD_Math.DEG_TO_RAD * lonSun + __true_sun_longitude = lonSunRad + + ## Ecliptic and ecuatorial coords + + # Ecliptic rectangular coords + var xs: float = r * cos(lonSunRad) + var ys: float = r * sin(lonSunRad) + + # Ecliptic rectangular coordinates rotate these to equatorial coordinates + var obleclCos: float = cos(__get_oblecl()) + var obleclSin: float = sin(__get_oblecl()) + + var xe: float = xs + var ye: float = ys * obleclCos - 0.0 * obleclSin + var ze: float = ys * obleclSin + 0.0 * obleclCos + + # Ascencion and declination + var RA: float = TOD_Math.RAD_TO_DEG * atan2(ye, xe) / 15 # right ascension. + var decl: float = atan2(ze, sqrt(xe * xe + ye * ye)) # declination + + # Mean longitude + var L: float = __sun_orbital_elements.w + __sun_orbital_elements.M + L = TOD_Math.rev(L) + + __mean_sun_longitude = L + + # Sideral time and hour angle + var GMST0: float = ((L/15) + 12) + __sideral_time = GMST0 + __get_total_hours_utc() + longitude / 15 # +15/15 + __local_sideral_time = TOD_Math.DEG_TO_RAD * __sideral_time * 15 + + var HA: float = (__sideral_time - RA) * 15 + var HARAD: float = TOD_Math.DEG_TO_RAD * HA + + # Hour angle and declination in rectangular coords + # HA and Decl in rectangular coords + var declCos: float = cos(decl) + var x = cos(HARAD) * declCos # X Axis points to the celestial equator in the south. + var y = sin(HARAD) * declCos # Y axis points to the horizon in the west. + var z = sin(decl) # Z axis points to the north celestial pole. + + # Rotate the rectangualar coordinates system along of the Y axis + var sinLat: float = sin(latitude * TOD_Math.DEG_TO_RAD) + var cosLat: float = cos(latitude * TOD_Math.DEG_TO_RAD) + var xhor: float = x * sinLat - z * cosLat + var yhor: float = y + var zhor: float = x * cosLat + z * sinLat + + # Azimuth and altitude + __sun_coords.x = atan2(yhor, xhor) + PI + __sun_coords.y = (PI * 0.5) - asin(zhor) # atan2(zhor, sqrt(xhor * xhor + yhor * yhor)) + + +func __compute_realistic_moon_coords() -> void: + # Orbital Elements + __moon_orbital_elements.get_orbital_elements(1, __get_time_scale()) + __moon_orbital_elements.N = TOD_Math.rev(__moon_orbital_elements.N) + __moon_orbital_elements.w = TOD_Math.rev(__moon_orbital_elements.w) + __moon_orbital_elements.M = TOD_Math.rev(__moon_orbital_elements.M) + + var NRad: float = TOD_Math.DEG_TO_RAD * __moon_orbital_elements.N + var IRad: float = TOD_Math.DEG_TO_RAD * __moon_orbital_elements.i + var MRad: float = TOD_Math.DEG_TO_RAD * __moon_orbital_elements.M + + # Eccentric anomaly + var E: float = __moon_orbital_elements.M + TOD_Math.RAD_TO_DEG * __moon_orbital_elements.e * sin(MRad) *\ + (1 + __sun_orbital_elements.e * cos(MRad)) + + var ERad = TOD_Math.DEG_TO_RAD * E + + # Rectangular coords and true anomaly + # Rectangular coordinates of the sun in the plane of the ecliptic + var xv: float = __moon_orbital_elements.a * (cos(ERad) - __moon_orbital_elements.e) + var yv: float = __moon_orbital_elements.a * (sin(ERad) * sqrt(1 - __moon_orbital_elements.e * \ + __moon_orbital_elements.e)) * sin(ERad) + + # Convert to distance and true anomaly(r = radians, v = degrees) + var r: float = sqrt(xv * xv + yv * yv) + var v: float = TOD_Math.RAD_TO_DEG * atan2(yv, xv) + v = TOD_Math.rev(v) + + var l: float = TOD_Math.DEG_TO_RAD * v + __moon_orbital_elements.w + + var cosL: float = cos(l) + var sinL: float = sin(l) + var cosNRad: float = cos(NRad) + var sinNRad: float = sin(NRad) + var cosIRad: float = cos(IRad) + + var xeclip: float = r * (cosNRad * cosL - sinNRad * sinL * cosIRad) + var yeclip: float = r * (sinNRad * cosL + cosNRad * sinL * cosIRad) + var zeclip: float = r * (sinL * sin(IRad)) + + # Geocentric coords + # Geocentric position for the moon and Heliocentric position for the planets + var lonecl: float = TOD_Math.RAD_TO_DEG * atan2(yeclip, xeclip) + lonecl = TOD_Math.rev(lonecl) + + var latecl: float = TOD_Math.RAD_TO_DEG * atan2(zeclip, sqrt(xeclip * xeclip + yeclip * yeclip)) + + # Get true sun longitude + var lonsun: float = __true_sun_longitude + + # Ecliptic longitude and latitude in radians + var loneclRad: float = TOD_Math.DEG_TO_RAD * lonecl + var lateclRad: float = TOD_Math.DEG_TO_RAD * latecl + + var nr: float = 1.0 + var xh: float = nr * cos(loneclRad) * cos(lateclRad) + var yh: float = nr * sin(loneclRad) * cos(lateclRad) + var zh: float = nr * sin(lateclRad) + + # Geocentric coords + var xs: float = 0.0 + var ys: float = 0.0 + + # Convert the geocentric position to heliocentric position + var xg: float = xh + xs + var yg: float = yh + ys + var zg: float = zh + + # Ecuatorial coords + # Cobert xg, yg un equatorial coords + var obleclCos: float = cos(__get_oblecl()) + var obleclSin: float = sin(__get_oblecl()) + + var xe: float = xg + var ye: float = yg * obleclCos - zg * obleclSin + var ze: float = yg * obleclSin + zg * obleclCos + + # Right ascention + var RA: float = TOD_Math.RAD_TO_DEG * atan2(ye, xe) + RA = TOD_Math.rev(RA) + + # Declination + var decl: float = TOD_Math.RAD_TO_DEG * atan2(ze, sqrt(xe * xe + ye * ye)) + var declRad: float = TOD_Math.DEG_TO_RAD * decl + + # Sideral time and hour angle + var HA: float = ((__sideral_time * 15) - RA) + HA = TOD_Math.rev(HA) + var HARAD: float = TOD_Math.DEG_TO_RAD * HA + + # HA y Decl in rectangular coordinates + var declCos: float = cos(declRad) + var xr: float = cos(HARAD) * declCos + var yr: float = sin(HARAD) * declCos + var zr: float = sin(declRad) + + # Rotate the rectangualar coordinates system along of the Y axis(radians) + var sinLat: float = sin(__get_latitude_rad()) + var cosLat: float = cos(__get_latitude_rad()) + + var xhor: float = xr * sinLat - zr * cosLat + var yhor: float = yr + var zhor: float = xr * cosLat + zr * sinLat + + # Azimuth and altitude + __moon_coords.x = atan2(yhor, xhor) + PI + __moon_coords.y = (PI *0.5) - atan2(zhor, sqrt(xhor * xhor + yhor * yhor)) # Mathf.Asin(zhor) + + +func _get_property_list() -> Array: + var ret: Array + ret.push_back({name = "Time Of Day", type=TYPE_NIL, usage=PROPERTY_USAGE_CATEGORY}) + + ret.push_back({name = "Target", type=TYPE_NIL, usage=PROPERTY_USAGE_GROUP}) + ret.push_back({name = "dome_path", type=TYPE_NODE_PATH}) + + ret.push_back({name = "DateTime", type=TYPE_NIL, usage=PROPERTY_USAGE_GROUP}) + ret.push_back({name = "system_sync", type=TYPE_BOOL}) + + ret.push_back({name = "total_cycle_in_minutes", type=TYPE_FLOAT}) + ret.push_back({name = "total_hours", type=TYPE_FLOAT, hint=PROPERTY_HINT_RANGE, hint_string="0.0, 24.0"}) + ret.push_back({name = "day", type=TYPE_INT, hint=PROPERTY_HINT_RANGE, hint_string="0, 31"}) + ret.push_back({name = "month", type=TYPE_INT, hint=PROPERTY_HINT_RANGE, hint_string="0, 12"}) + ret.push_back({name = "year", type=TYPE_INT, hint=PROPERTY_HINT_RANGE, hint_string="-9999, 9999"}) + + ret.push_back({name = "Planetary And Location", type=TYPE_NIL, usage=PROPERTY_USAGE_GROUP}) + ret.push_back({name = "celestials_calculations", type=TYPE_INT, hint=PROPERTY_HINT_ENUM, hint_string="Simple, Realistic"}) + ret.push_back({name = "compute_moon_coords", type=TYPE_BOOL}) + + if celestials_calculations == 0 && compute_moon_coords: + ret.push_back({name = "moon_coords_offset", type=TYPE_VECTOR2}) + + ret.push_back({name = "compute_deep_space_coords", type=TYPE_BOOL}) + ret.push_back({name = "latitude", type=TYPE_FLOAT, hint=PROPERTY_HINT_RANGE, hint_string="-90.0, 90.0"}) + ret.push_back({name = "longitude", type=TYPE_FLOAT, hint=PROPERTY_HINT_RANGE, hint_string="-180.0, 180.0"}) + ret.push_back({name = "utc", type=TYPE_FLOAT, hint=PROPERTY_HINT_RANGE, hint_string="-12.0, 12.0"}) + + return ret diff --git a/addons/sky_3d/src/TimeOfDay.gd.uid b/addons/sky_3d/src/TimeOfDay.gd.uid new file mode 100644 index 0000000..6666a5b --- /dev/null +++ b/addons/sky_3d/src/TimeOfDay.gd.uid @@ -0,0 +1 @@ +uid://bm0hx4mklpml diff --git a/addons/terrain_3d/LICENSE.txt b/addons/terrain_3d/LICENSE.txt new file mode 100644 index 0000000..368567b --- /dev/null +++ b/addons/terrain_3d/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Cory Petkovsek, Roope Palmroos, and Contributors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/addons/terrain_3d/README.md b/addons/terrain_3d/README.md new file mode 100644 index 0000000..a49fc8e --- /dev/null +++ b/addons/terrain_3d/README.md @@ -0,0 +1,47 @@ +![Terrain3D Logo](/doc/docs/images/terrain3d.jpg) + +# Terrain3D +A high performance, editable terrain system for Godot 4. + + +## Features +* Written in C++ as a GDExtension addon, which works with official builds of Godot Engine +* Can be accessed by GDScript, C#, and any language Godot supports +* Geomorphing Geometric Clipmap Mesh Terrain, as used in The Witcher 3. See [System Architecture](https://terrain3d.readthedocs.io/en/stable/docs/system_architecture.html) +* Terrains as small as 64x64m up to 65.5x65.5km (4295km^2) in variable sized regions +* Up to 32 textures +* Up to 10 levels of detail for the terrain mesh +* Foliage instancing, with up to 10 levels of detail, and a shadow impostor +* Sculpting, holes, texture painting, texture detiling, painting colors and wetness +* Imports heightmaps from [HTerrain](https://github.com/Zylann/godot_heightmap_plugin/), Gaea, World Creator, World Machine, Unity, Unreal and any tool that can export a heightmap. See [heightmaps](https://terrain3d.readthedocs.io/en/stable/docs/heightmaps.html) + + +## Getting Started + +1. Read the [Installation & Upgrades](https://terrain3d.readthedocs.io/en/stable/docs/installation.html) instructions. + +2. For support, read [Getting Help](https://terrain3d.readthedocs.io/en/stable/docs/getting_help.html) and join our [Discord server](https://tokisan.com/discord). + +3. Watch the [tutorial videos](https://terrain3d.readthedocs.io/en/stable/docs/tutorial_videos.html). + + +## Credit +Developed for the Godot community by: + +||| +|--|--| +| **Cory Petkovsek, Tokisan Games** | [](https://twitter.com/TokisanGames) [](https://github.com/TokisanGames) [](https://tokisan.com/) [](https://tokisan.com/discord) [](https://www.youtube.com/@TokisanGames)| +| **Roope Palmroos, Outobugi Games** | [](https://twitter.com/outobugi) [](https://github.com/outobugi) [](https://outobugi.com/) [](https://www.youtube.com/@outobugi)| + +And other contributors displayed on the right of the github page and in [AUTHORS.md](https://terrain3d.readthedocs.io/en/stable/docs/authors.html). + + +## Contributing + +Please see [CONTRIBUTING.md](https://github.com/TokisanGames/Terrain3D/blob/main/CONTRIBUTING.md) if you would like to help make Terrain3D the best terrain system for Godot. + + +## License + +This addon has been released under the [MIT License](https://github.com/TokisanGames/Terrain3D/blob/main/LICENSE.txt). + diff --git a/addons/terrain_3d/bin/libterrain.android.debug.arm32.so b/addons/terrain_3d/bin/libterrain.android.debug.arm32.so new file mode 100644 index 0000000..d56e1a0 Binary files /dev/null and b/addons/terrain_3d/bin/libterrain.android.debug.arm32.so differ diff --git a/addons/terrain_3d/bin/libterrain.android.debug.arm64.so b/addons/terrain_3d/bin/libterrain.android.debug.arm64.so new file mode 100644 index 0000000..7dc0b98 Binary files /dev/null and b/addons/terrain_3d/bin/libterrain.android.debug.arm64.so differ diff --git a/addons/terrain_3d/bin/libterrain.android.release.arm32.so b/addons/terrain_3d/bin/libterrain.android.release.arm32.so new file mode 100644 index 0000000..a5dd4ab Binary files /dev/null and b/addons/terrain_3d/bin/libterrain.android.release.arm32.so differ diff --git a/addons/terrain_3d/bin/libterrain.android.release.arm64.so b/addons/terrain_3d/bin/libterrain.android.release.arm64.so new file mode 100644 index 0000000..cf1c42d Binary files /dev/null and b/addons/terrain_3d/bin/libterrain.android.release.arm64.so differ diff --git a/addons/terrain_3d/bin/libterrain.ios.debug.universal.dylib b/addons/terrain_3d/bin/libterrain.ios.debug.universal.dylib new file mode 100644 index 0000000..9ef876d Binary files /dev/null and b/addons/terrain_3d/bin/libterrain.ios.debug.universal.dylib differ diff --git a/addons/terrain_3d/bin/libterrain.ios.release.universal.dylib b/addons/terrain_3d/bin/libterrain.ios.release.universal.dylib new file mode 100644 index 0000000..40423b6 Binary files /dev/null and b/addons/terrain_3d/bin/libterrain.ios.release.universal.dylib differ diff --git a/addons/terrain_3d/bin/libterrain.linux.debug.x86_64.so b/addons/terrain_3d/bin/libterrain.linux.debug.x86_64.so new file mode 100644 index 0000000..1813074 Binary files /dev/null and b/addons/terrain_3d/bin/libterrain.linux.debug.x86_64.so differ diff --git a/addons/terrain_3d/bin/libterrain.linux.release.x86_64.so b/addons/terrain_3d/bin/libterrain.linux.release.x86_64.so new file mode 100644 index 0000000..9a594ac Binary files /dev/null and b/addons/terrain_3d/bin/libterrain.linux.release.x86_64.so differ diff --git a/addons/terrain_3d/bin/libterrain.macos.debug.framework/libterrain.macos.debug b/addons/terrain_3d/bin/libterrain.macos.debug.framework/libterrain.macos.debug new file mode 100644 index 0000000..847389b Binary files /dev/null and b/addons/terrain_3d/bin/libterrain.macos.debug.framework/libterrain.macos.debug differ diff --git a/addons/terrain_3d/bin/libterrain.macos.release.framework/libterrain.macos.release b/addons/terrain_3d/bin/libterrain.macos.release.framework/libterrain.macos.release new file mode 100644 index 0000000..cf57663 Binary files /dev/null and b/addons/terrain_3d/bin/libterrain.macos.release.framework/libterrain.macos.release differ diff --git a/addons/terrain_3d/bin/libterrain.web.debug.wasm32.wasm b/addons/terrain_3d/bin/libterrain.web.debug.wasm32.wasm new file mode 100644 index 0000000..b77fbd7 Binary files /dev/null and b/addons/terrain_3d/bin/libterrain.web.debug.wasm32.wasm differ diff --git a/addons/terrain_3d/bin/libterrain.web.release.wasm32.wasm b/addons/terrain_3d/bin/libterrain.web.release.wasm32.wasm new file mode 100644 index 0000000..3fdea6f Binary files /dev/null and b/addons/terrain_3d/bin/libterrain.web.release.wasm32.wasm differ diff --git a/addons/terrain_3d/bin/libterrain.windows.debug.x86_64.dll b/addons/terrain_3d/bin/libterrain.windows.debug.x86_64.dll new file mode 100644 index 0000000..23b7f23 Binary files /dev/null and b/addons/terrain_3d/bin/libterrain.windows.debug.x86_64.dll differ diff --git a/addons/terrain_3d/bin/libterrain.windows.release.x86_64.dll b/addons/terrain_3d/bin/libterrain.windows.release.x86_64.dll new file mode 100644 index 0000000..cdff9a4 Binary files /dev/null and b/addons/terrain_3d/bin/libterrain.windows.release.x86_64.dll differ diff --git a/addons/terrain_3d/brushes/.gdignore b/addons/terrain_3d/brushes/.gdignore new file mode 100644 index 0000000..e69de29 diff --git a/addons/terrain_3d/brushes/acrylic1.exr b/addons/terrain_3d/brushes/acrylic1.exr new file mode 100644 index 0000000..14b66d6 Binary files /dev/null and b/addons/terrain_3d/brushes/acrylic1.exr differ diff --git a/addons/terrain_3d/brushes/circle0.exr b/addons/terrain_3d/brushes/circle0.exr new file mode 100644 index 0000000..78937f1 Binary files /dev/null and b/addons/terrain_3d/brushes/circle0.exr differ diff --git a/addons/terrain_3d/brushes/circle1.exr b/addons/terrain_3d/brushes/circle1.exr new file mode 100644 index 0000000..e91d97e Binary files /dev/null and b/addons/terrain_3d/brushes/circle1.exr differ diff --git a/addons/terrain_3d/brushes/circle2.exr b/addons/terrain_3d/brushes/circle2.exr new file mode 100644 index 0000000..f6931ba Binary files /dev/null and b/addons/terrain_3d/brushes/circle2.exr differ diff --git a/addons/terrain_3d/brushes/circle3.exr b/addons/terrain_3d/brushes/circle3.exr new file mode 100644 index 0000000..477ab7e Binary files /dev/null and b/addons/terrain_3d/brushes/circle3.exr differ diff --git a/addons/terrain_3d/brushes/circle4.exr b/addons/terrain_3d/brushes/circle4.exr new file mode 100644 index 0000000..b466f92 Binary files /dev/null and b/addons/terrain_3d/brushes/circle4.exr differ diff --git a/addons/terrain_3d/brushes/hill1.exr b/addons/terrain_3d/brushes/hill1.exr new file mode 100644 index 0000000..a668ab6 Binary files /dev/null and b/addons/terrain_3d/brushes/hill1.exr differ diff --git a/addons/terrain_3d/brushes/hill2.exr b/addons/terrain_3d/brushes/hill2.exr new file mode 100644 index 0000000..068e22b Binary files /dev/null and b/addons/terrain_3d/brushes/hill2.exr differ diff --git a/addons/terrain_3d/brushes/mountain1.exr b/addons/terrain_3d/brushes/mountain1.exr new file mode 100644 index 0000000..be71748 Binary files /dev/null and b/addons/terrain_3d/brushes/mountain1.exr differ diff --git a/addons/terrain_3d/brushes/mountain2.exr b/addons/terrain_3d/brushes/mountain2.exr new file mode 100644 index 0000000..ca30373 Binary files /dev/null and b/addons/terrain_3d/brushes/mountain2.exr differ diff --git a/addons/terrain_3d/brushes/mountain3.exr b/addons/terrain_3d/brushes/mountain3.exr new file mode 100644 index 0000000..ca07a1e Binary files /dev/null and b/addons/terrain_3d/brushes/mountain3.exr differ diff --git a/addons/terrain_3d/brushes/mountain4.exr b/addons/terrain_3d/brushes/mountain4.exr new file mode 100644 index 0000000..f8197fe Binary files /dev/null and b/addons/terrain_3d/brushes/mountain4.exr differ diff --git a/addons/terrain_3d/brushes/peak1.exr b/addons/terrain_3d/brushes/peak1.exr new file mode 100644 index 0000000..49d341e Binary files /dev/null and b/addons/terrain_3d/brushes/peak1.exr differ diff --git a/addons/terrain_3d/brushes/peak2.exr b/addons/terrain_3d/brushes/peak2.exr new file mode 100644 index 0000000..db74297 Binary files /dev/null and b/addons/terrain_3d/brushes/peak2.exr differ diff --git a/addons/terrain_3d/brushes/peak3.exr b/addons/terrain_3d/brushes/peak3.exr new file mode 100644 index 0000000..9383681 Binary files /dev/null and b/addons/terrain_3d/brushes/peak3.exr differ diff --git a/addons/terrain_3d/brushes/ring1.exr b/addons/terrain_3d/brushes/ring1.exr new file mode 100644 index 0000000..6396804 Binary files /dev/null and b/addons/terrain_3d/brushes/ring1.exr differ diff --git a/addons/terrain_3d/brushes/smoke.exr b/addons/terrain_3d/brushes/smoke.exr new file mode 100644 index 0000000..021947b Binary files /dev/null and b/addons/terrain_3d/brushes/smoke.exr differ diff --git a/addons/terrain_3d/brushes/square1.exr b/addons/terrain_3d/brushes/square1.exr new file mode 100644 index 0000000..3aff9cd Binary files /dev/null and b/addons/terrain_3d/brushes/square1.exr differ diff --git a/addons/terrain_3d/brushes/square2.exr b/addons/terrain_3d/brushes/square2.exr new file mode 100644 index 0000000..230113c Binary files /dev/null and b/addons/terrain_3d/brushes/square2.exr differ diff --git a/addons/terrain_3d/brushes/square3.exr b/addons/terrain_3d/brushes/square3.exr new file mode 100644 index 0000000..6da88b8 Binary files /dev/null and b/addons/terrain_3d/brushes/square3.exr differ diff --git a/addons/terrain_3d/brushes/square4.exr b/addons/terrain_3d/brushes/square4.exr new file mode 100644 index 0000000..350cd7d Binary files /dev/null and b/addons/terrain_3d/brushes/square4.exr differ diff --git a/addons/terrain_3d/brushes/square5.exr b/addons/terrain_3d/brushes/square5.exr new file mode 100644 index 0000000..f0832e0 Binary files /dev/null and b/addons/terrain_3d/brushes/square5.exr differ diff --git a/addons/terrain_3d/brushes/stones.exr b/addons/terrain_3d/brushes/stones.exr new file mode 100644 index 0000000..7ef5977 Binary files /dev/null and b/addons/terrain_3d/brushes/stones.exr differ diff --git a/addons/terrain_3d/brushes/terrain1.exr b/addons/terrain_3d/brushes/terrain1.exr new file mode 100644 index 0000000..8366b52 Binary files /dev/null and b/addons/terrain_3d/brushes/terrain1.exr differ diff --git a/addons/terrain_3d/brushes/terrain2.exr b/addons/terrain_3d/brushes/terrain2.exr new file mode 100644 index 0000000..d11a069 Binary files /dev/null and b/addons/terrain_3d/brushes/terrain2.exr differ diff --git a/addons/terrain_3d/brushes/terrain3.exr b/addons/terrain_3d/brushes/terrain3.exr new file mode 100644 index 0000000..61bfe0f Binary files /dev/null and b/addons/terrain_3d/brushes/terrain3.exr differ diff --git a/addons/terrain_3d/brushes/terrain4.exr b/addons/terrain_3d/brushes/terrain4.exr new file mode 100644 index 0000000..e8f4ce4 Binary files /dev/null and b/addons/terrain_3d/brushes/terrain4.exr differ diff --git a/addons/terrain_3d/brushes/terrain5.exr b/addons/terrain_3d/brushes/terrain5.exr new file mode 100644 index 0000000..ad3fe5b Binary files /dev/null and b/addons/terrain_3d/brushes/terrain5.exr differ diff --git a/addons/terrain_3d/brushes/terrain6.exr b/addons/terrain_3d/brushes/terrain6.exr new file mode 100644 index 0000000..0a15419 Binary files /dev/null and b/addons/terrain_3d/brushes/terrain6.exr differ diff --git a/addons/terrain_3d/brushes/texture1.exr b/addons/terrain_3d/brushes/texture1.exr new file mode 100644 index 0000000..f456b77 Binary files /dev/null and b/addons/terrain_3d/brushes/texture1.exr differ diff --git a/addons/terrain_3d/brushes/texture2.exr b/addons/terrain_3d/brushes/texture2.exr new file mode 100644 index 0000000..2624d3b Binary files /dev/null and b/addons/terrain_3d/brushes/texture2.exr differ diff --git a/addons/terrain_3d/brushes/texture3.exr b/addons/terrain_3d/brushes/texture3.exr new file mode 100644 index 0000000..690fe5e Binary files /dev/null and b/addons/terrain_3d/brushes/texture3.exr differ diff --git a/addons/terrain_3d/brushes/texture4.exr b/addons/terrain_3d/brushes/texture4.exr new file mode 100644 index 0000000..a2d96aa Binary files /dev/null and b/addons/terrain_3d/brushes/texture4.exr differ diff --git a/addons/terrain_3d/brushes/texture5.exr b/addons/terrain_3d/brushes/texture5.exr new file mode 100644 index 0000000..62aad60 Binary files /dev/null and b/addons/terrain_3d/brushes/texture5.exr differ diff --git a/addons/terrain_3d/brushes/vegetation1.exr b/addons/terrain_3d/brushes/vegetation1.exr new file mode 100644 index 0000000..d65bc6e Binary files /dev/null and b/addons/terrain_3d/brushes/vegetation1.exr differ diff --git a/addons/terrain_3d/extras/hex_grid.gdshaderinc b/addons/terrain_3d/extras/hex_grid.gdshaderinc new file mode 100644 index 0000000..0fcd552 --- /dev/null +++ b/addons/terrain_3d/extras/hex_grid.gdshaderinc @@ -0,0 +1,67 @@ +// Copyright © 2025 Cory Petkovsek, Roope Palmroos, and Contributors. +// This shader snippet draws a hex grid + +// To use it, add this line to the top of your shader: +// #include "res://addons/terrain_3d/extras/hex_grid.gdshaderinc" + +// And this line at the bottom of your shader: +// draw_hex_grid(uv2, _region_texel_size, w_normal, ALBEDO); + +mat2 rotate2d(float _angle) { + return mat2(vec2(cos(_angle),-sin(_angle)), vec2(sin(_angle), cos(_angle))); +} + +void draw_hex_grid(vec2 uv, float texel_size, vec3 normal, inout vec3 albedo) { + float hex_size = 0.02; + float line_thickness = 0.04; + + vec2 guv = (uv - vec2(0.5 * texel_size)) / hex_size; + + // Convert UV to axial hex coordinates + float q = (sqrt(3.0) / 3.0 * guv.x - 1.0 / 3.0 * guv.y); + float r = (2.0 / 3.0 * guv.y); + + // Cube coordinates for the hex (q, r, -q-r) + float x = q; + float z = r; + float y = -x - z; + + // Round to the nearest hex center + vec3 rounded = round(vec3(x, y, z)); + vec3 diff = abs(vec3(x, y, z) - rounded); + + // Fix rounding errors + if (diff.x > diff.y && diff.x > diff.z) { + rounded.x = -rounded.y - rounded.z; + } else if (diff.y > diff.z) { + rounded.y = -rounded.x - rounded.z; + } else { + rounded.z = -rounded.x - rounded.y; + } + + // Find the hex center in UV space + vec2 hex_center = vec2( + sqrt(3.0) * rounded.x + sqrt(3.0) / 2.0 * rounded.z, + 3.0 / 2.0 * rounded.z + ); + + // Relative position within the hex + vec2 local_pos = guv - hex_center; + vec2 lines_uv = local_pos; + float line = 1.0; + + for (int i = 0; i < 6; i++) { + vec2 luv = lines_uv * rotate2d(radians(60.0 * float(i) + 30.0)); + float dist = abs(dot(luv + vec2(0.90), vec2(0.0, 1.0))); + line = min(line, dist); + } + + // Filter lines by slope + float slope = 4.; // Can also assign to (auto_slope * 4.) to match grass placement + float slope_factor = clamp(dot(vec3(0., 1., 0.), slope * (normal - 1.) + 1.), 0., 1.); + + // Draw hex grid + albedo = mix(albedo, vec3(1.0), smoothstep(line_thickness + 0.02, line_thickness, line) * slope_factor); + // Draw Hex center dot + albedo = mix(albedo, vec3(0.0, 0.5, 0.5), smoothstep(0.11, 0.10, length(local_pos)) * slope_factor); +} \ No newline at end of file diff --git a/addons/terrain_3d/extras/hex_grid.gdshaderinc.uid b/addons/terrain_3d/extras/hex_grid.gdshaderinc.uid new file mode 100644 index 0000000..feee7f6 --- /dev/null +++ b/addons/terrain_3d/extras/hex_grid.gdshaderinc.uid @@ -0,0 +1 @@ +uid://mri8pfoj2mfk diff --git a/addons/terrain_3d/extras/import_sgt.gd b/addons/terrain_3d/extras/import_sgt.gd new file mode 100644 index 0000000..44bb685 --- /dev/null +++ b/addons/terrain_3d/extras/import_sgt.gd @@ -0,0 +1,42 @@ +# Copyright © 2025 Cory Petkovsek, Roope Palmroos, and Contributors. +# Import From SimpleGrassTextured +# +# This script demonstrates how to import transforms from SimpleGrassTextured. To use it: +# +# 1. Setup the mesh asset you wish to use in the asset dock. +# 1. Select your Terrain3D node. +# 1. In the inspector, click Script (very bottom) and Quick Load import_sgt.gd. +# 1. At the very top, assign your SimpleGrassTextured node. +# 1. Input the desired mesh asset ID. +# 1. Click import. The output window and console will report when finished. +# 1. Clear the script from your Terrain3D node, and save your scene. +# +# The instance transforms are now stored in your region files. +# +# Use clear_instances to erase all instances that match the assign_mesh_id. +# +# The add_transforms function (called by add_multimesh) applies the height_offset specified in the +# Terrain3DMeshAsset. +# Once the transforms are imported, you can reassign any mesh you like into this mesh slot. + +@tool +extends Terrain3D + +@export var simple_grass_textured: MultiMeshInstance3D +@export var assign_mesh_id: int +@export var import: bool = false : set = import_sgt +@export var clear_instances: bool = false : set = clear_multimeshes + + +func clear_multimeshes(value: bool) -> void: + get_instancer().clear_by_mesh(assign_mesh_id) + + +func import_sgt(value: bool) -> void: + var sgt_mm: MultiMesh = simple_grass_textured.multimesh + var global_xform: Transform3D = simple_grass_textured.global_transform + print("Starting to import %d instances from SimpleGrassTextured using mesh id %d" % [ sgt_mm.instance_count, assign_mesh_id]) + var time: int = Time.get_ticks_msec() + get_instancer().add_multimesh(assign_mesh_id, sgt_mm, simple_grass_textured.global_transform) + print("Import complete in %.2f seconds" % [ float(Time.get_ticks_msec() - time)/1000. ]) + diff --git a/addons/terrain_3d/extras/import_sgt.gd.uid b/addons/terrain_3d/extras/import_sgt.gd.uid new file mode 100644 index 0000000..00b3868 --- /dev/null +++ b/addons/terrain_3d/extras/import_sgt.gd.uid @@ -0,0 +1 @@ +uid://bllcuwetve45k diff --git a/addons/terrain_3d/extras/lightweight.gdshader b/addons/terrain_3d/extras/lightweight.gdshader new file mode 100644 index 0000000..b0716a1 --- /dev/null +++ b/addons/terrain_3d/extras/lightweight.gdshader @@ -0,0 +1,400 @@ +shader_type spatial; +render_mode blend_mix,depth_draw_opaque,cull_back,diffuse_burley,specular_schlick_ggx,skip_vertex_transform; + +/* This is an example stripped down shader with maximum performance in mind. + * Only Autoshader/Base/Over/Blend/Holes/Colormap are supported. + * All terrain normal calculations take place in vetex() as well as control map reads + * for the bilinear blend, when not skippable have moved to vertex() too. + * + * A single controlmap lookup in fragment is added at distances where the vertices spread too wide. + */ + +// Defined Constants +#define SKIP_PASS 0 +#define VERTEX_PASS 1 +#define FRAGMENT_PASS 2 + +#if CURRENT_RENDERER == RENDERER_COMPATIBILITY + #define fma(a, b, c) ((a) * (b) + (c)) + #define dFdxCoarse(a) dFdx(a) + #define dFdyCoarse(a) dFdy(a) +#endif + +// Private uniforms +uniform vec3 _camera_pos = vec3(0.f); +uniform float _mesh_size = 48.f; +uniform uint _background_mode = 1u; // NONE = 0, FLAT = 1, NOISE = 2 +uniform uint _mouse_layer = 0x80000000u; // Layer 32 +uniform float _vertex_spacing = 1.0; +uniform float _vertex_density = 1.0; // = 1/_vertex_spacing +uniform float _region_size = 1024.0; +uniform float _region_texel_size = 0.0009765625; // = 1/1024 +uniform int _region_map_size = 32; +uniform int _region_map[1024]; +uniform vec2 _region_locations[1024]; +uniform float _texture_normal_depth_array[32]; +uniform float _texture_ao_strength_array[32]; +uniform float _texture_roughness_mod_array[32]; +uniform float _texture_uv_scale_array[32]; +uniform vec4 _texture_color_array[32]; +uniform highp sampler2DArray _height_maps : repeat_disable; +uniform highp sampler2DArray _control_maps : repeat_disable; +uniform highp sampler2DArray _color_maps : source_color, filter_linear_mipmap, repeat_disable; +uniform highp sampler2DArray _texture_array_albedo : source_color, filter_linear_mipmap, repeat_enable; +uniform highp sampler2DArray _texture_array_normal : hint_normal, filter_linear_mipmap, repeat_enable; + + +// Public uniforms +uniform float auto_slope : hint_range(0, 10) = 1.0; +uniform float auto_height_reduction : hint_range(0, 1) = 0.1; +uniform int auto_base_texture : hint_range(0, 31) = 0; +uniform int auto_overlay_texture : hint_range(0, 31) = 1; + +uniform bool height_blending = true; +uniform bool world_space_normal_blend = true; +uniform float blend_sharpness : hint_range(0, 1) = 0.87; + +// Varyings & Types + +struct Material { + vec4 alb_ht; + vec4 nrm_rg; + int base; + int over; + float blend; + float nrm_depth; + float ao_str; +}; + + +varying vec3 v_vertex; +varying vec3 v_normal; +varying flat uint v_control[4]; +varying flat int v_lerp; +varying mat3 v_tbn; + +//////////////////////// +// Vertex +//////////////////////// + +// Takes in world space XZ (UV) coordinates & search depth (only applicable for background mode none) +// Returns ivec3 with: +// XY: (0 to _region_size - 1) coordinates within a region +// Z: layer index used for texturearrays, -1 if not in a region +ivec3 get_index_coord(const vec2 uv, const int search) { + vec2 r_uv = round(uv); + vec2 o_uv = mod(r_uv,_region_size); + ivec2 pos; + int bounds, layer_index = -1; + for (int i = -1; i < clamp(search, SKIP_PASS, FRAGMENT_PASS); i++) { + if ((layer_index == -1 && _background_mode == 0u ) || i < 0) { + r_uv -= i == -1 ? vec2(0.0) : vec2(float(o_uv.x <= o_uv.y), float(o_uv.y <= o_uv.x)); + pos = ivec2(floor((r_uv) * _region_texel_size)) + (_region_map_size / 2); + bounds = int(uint(pos.x | pos.y) < uint(_region_map_size)); + layer_index = (_region_map[ pos.y * _region_map_size + pos.x ] * bounds - 1); + } + } + return ivec3(ivec2(mod(r_uv,_region_size)), layer_index); +} + +// Takes in descaled (world_space / region_size) world to region space XZ (UV2) coordinates, returns vec3 with: +// XY: (0. to 1.) coordinates within a region +// Z: layer index used for texturearrays, -1 if not in a region +vec3 get_index_uv(const vec2 uv2) { + ivec2 pos = ivec2(floor(uv2)) + (_region_map_size / 2); + int bounds = int(uint(pos.x | pos.y) < uint(_region_map_size)); + int layer_index = _region_map[ pos.y * _region_map_size + pos.x ] * bounds - 1; + return vec3(uv2 - _region_locations[layer_index], float(layer_index)); +} + +void vertex() { + // Get vertex of flat plane in world coordinates and set world UV + v_vertex = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz; + + // Camera distance to vertex on flat plane + float v_vertex_xz_dist = length(v_vertex.xz - _camera_pos.xz); + + // Geomorph vertex, set end and start for linear height interpolate + float scale = MODEL_MATRIX[0][0]; + float vertex_lerp = smoothstep(0.55, 0.95, (v_vertex_xz_dist / scale - _mesh_size - 4.0) / (_mesh_size - 2.0)); + vec2 v_fract = fract(VERTEX.xz * 0.5) * 2.0; + // For LOD0 morph from a regular grid to an alternating grid to align with LOD1+ + vec2 shift = (scale < _vertex_spacing + 1e-6) ? // LOD0 or not + // Shift from regular to symetric + mix(v_fract, vec2(v_fract.x, -v_fract.y), + round(fract(round(mod(v_vertex.z * _vertex_density, 4.0)) * + round(mod(v_vertex.x * _vertex_density, 4.0)) * 0.25)) + ) : + // Symetric shift + v_fract * round((fract(v_vertex.xz * 0.25 / scale) - 0.5) * 4.0); + vec2 start_pos = v_vertex.xz * _vertex_density; + vec2 end_pos = (v_vertex.xz - shift * scale) * _vertex_density; + v_vertex.xz -= shift * scale * vertex_lerp; + + // UV coordinates in world space. Values are 0 to _region_size within regions + UV = v_vertex.xz * _vertex_density; + + // UV coordinates in region space + texel offset. Values are 0 to 1 within regions + UV2 = fma(UV, vec2(_region_texel_size), vec2(0.5 * _region_texel_size)); + + const vec3 offsets = vec3(0, 1, 2); + ivec3 indexUV[4]; + // control map lookups in vertex, used for bilinear blend in fragment. + indexUV[0] = get_index_coord(start_pos + offsets.xy, VERTEX_PASS); + indexUV[1] = get_index_coord(start_pos + offsets.yy, VERTEX_PASS); + indexUV[2] = get_index_coord(start_pos + offsets.yx, VERTEX_PASS); + indexUV[3] = get_index_coord(start_pos + offsets.xx, VERTEX_PASS); + // Mask off Scale/Rotation/Navigation bits to 0, as they are not used. + #define CONTROL_MASK 0xFFFFC07Du + v_control[0] = floatBitsToUint(texelFetch(_control_maps, indexUV[0], 0)).r & CONTROL_MASK; + v_control[1] = floatBitsToUint(texelFetch(_control_maps, indexUV[1], 0)).r & CONTROL_MASK; + v_control[2] = floatBitsToUint(texelFetch(_control_maps, indexUV[2], 0)).r & CONTROL_MASK; + v_control[3] = floatBitsToUint(texelFetch(_control_maps, indexUV[3], 0)).r & CONTROL_MASK; + bool full_auto = !bool((v_control[0] & v_control[1] & v_control[2] & v_control[3]) & 0x1u); + bool identical = !( + (v_control[0] == v_control[1]) && + (v_control[1] == v_control[2]) && + (v_control[2] == v_control[3])); + // Verticies are close enough, full auto shader, or all 4 indicies match, skip bilinear blend in fragment. + v_lerp = scale < _vertex_spacing + 1e-3 && vertex_lerp < 1e-3 && (full_auto || identical) ? 1 : 0; + + // Discard vertices for Holes. 1 lookup + bool hole = bool(v_control[3] >>2u & 0x1u); + + // Show holes to all cameras except mouse camera (on exactly 1 layer) + if ( !(CAMERA_VISIBLE_LAYERS == _mouse_layer) && + (hole || (_background_mode == 0u && indexUV[3].z == -1))) { + v_vertex.x = 0. / 0.; + } else { + // Set final vertex height & calculate vertex normals. 3 lookups + ivec3 uv_a = get_index_coord(start_pos, VERTEX_PASS); + ivec3 uv_b = get_index_coord(end_pos, VERTEX_PASS); + float h = mix(texelFetch(_height_maps, uv_a, 0).r,texelFetch(_height_maps, uv_b, 0).r,vertex_lerp); + float u = mix(texelFetch(_height_maps, get_index_coord(start_pos + vec2(1,0), VERTEX_PASS), 0).r, + texelFetch(_height_maps, get_index_coord(end_pos + vec2(1,0), VERTEX_PASS), 0).r, vertex_lerp); + float v = mix(texelFetch(_height_maps, get_index_coord(start_pos + vec2(0,1), VERTEX_PASS), 0).r, + texelFetch(_height_maps, get_index_coord(end_pos + vec2(0,1), VERTEX_PASS), 0).r, vertex_lerp); + v_vertex.y = h; + v_normal = vec3(h - u, _vertex_spacing, h - v); + } + + // Convert model space to view space w/ skip_vertex_transform render mode + VERTEX = (VIEW_MATRIX * vec4(v_vertex, 1.0)).xyz; + + // Apply terrain normals + vec3 w_normal = normalize(v_normal); + vec3 w_tangent = normalize(cross(w_normal, vec3(0.0, 0.0, 1.0))); + vec3 w_binormal = normalize(cross(w_normal, w_tangent)); + + v_tbn = mat3(w_tangent, w_normal, w_binormal); + + NORMAL = normalize((VIEW_MATRIX * vec4(w_normal, 0.0)).xyz); + BINORMAL = normalize((VIEW_MATRIX * vec4(w_binormal, 0.0)).xyz); + TANGENT = normalize((VIEW_MATRIX * vec4(w_tangent, 0.0)).xyz); +} + +//////////////////////// +// Fragment +//////////////////////// + +vec3 unpack_normal(vec4 rgba) { + return fma(rgba.xzy, vec3(2.0), vec3(-1.0)); +} + +vec3 pack_normal(vec3 n) { + return fma(normalize(n.xzy), vec3(0.5), vec3(0.5)); +} + +vec4 height_blend4(vec4 a_value, float a_height, vec4 b_value, float b_height, float blend) { + if(height_blending) { + float ma = max(a_height + (1.0 - blend), b_height + blend) - (1.001 - blend_sharpness); + float b1 = max(a_height + (1.0 - blend) - ma, 0.0); + float b2 = max(b_height + blend - ma, 0.0); + return (a_value * b1 + b_value * b2) / (b1 + b2); + } else { + float contrast = 1.0 - blend_sharpness; + float factor = (blend - contrast) / contrast; + return mix(a_value, b_value, clamp(factor, 0.0, 1.0)); + } +} + +float height_blend1(float a_value, float a_height, float b_value, float b_height, float blend) { + if(height_blending) { + float ma = max(a_height + (1.0 - blend), b_height + blend) - (1.001 - blend_sharpness); + float b1 = max(a_height + (1.0 - blend) - ma, 0.0); + float b2 = max(b_height + blend - ma, 0.0); + return (a_value * b1 + b_value * b2) / (b1 + b2); + } else { + float contrast = 1.0 - blend_sharpness; + float factor = (blend - contrast) / contrast; + return mix(a_value, b_value, clamp(factor, 0.0, 1.0)); + } +} + +// 2-4 lookups ( 2-6 with dual scaling ) +void get_material(vec4 ddxy, uint control, vec3 iuv_center, out Material out_mat) { + out_mat = Material(vec4(0.), vec4(0.), 0, 0, 0.0, 0.0, 0.0); + int region = int(iuv_center.z); + vec2 base_uv = v_vertex.xz * 0.5; + ddxy *= 0.5; + + // Enable Autoshader if outside regions or painted in regions, otherwise manual painted + bool auto_shader = region < 0 || bool(control & 0x1u); + out_mat.base = int(auto_shader) * auto_base_texture + int(!auto_shader) * int(control >>27u & 0x1Fu); + out_mat.over = int(auto_shader) * auto_overlay_texture + int(!auto_shader) * int(control >> 22u & 0x1Fu); + out_mat.blend = float(auto_shader) * clamp( + (auto_slope * 2. * ( v_tbn[1].y - 1.) + 1.) + - auto_height_reduction * .01 * v_vertex.y // Reduce as vertices get higher + , 0., 1.) + + float(!auto_shader) * float(control >>14u & 0xFFu) * 0.003921568627450; // 1./255.0 + + out_mat.nrm_depth = _texture_normal_depth_array[out_mat.base]; + out_mat.ao_str = _texture_ao_strength_array[out_mat.base]; + + vec2 matUV = base_uv; + vec4 albedo_ht = vec4(0.); + vec4 normal_rg = vec4(0.5, 0.5, 1.0, 1.0); + vec4 albedo_far = vec4(0.); + vec4 normal_far = vec4(0.5, 0.5, 1.0, 1.0); + float mat_scale = _texture_uv_scale_array[out_mat.base]; + vec4 base_dd = ddxy; + + if (out_mat.blend < 1.0) { + // 2 lookups + //each time we change scale, recalculate antitiling from baseline to maintain continuity. + matUV = base_uv * mat_scale; + base_dd *= mat_scale; + albedo_ht = textureGrad(_texture_array_albedo, vec3(matUV, float(out_mat.base)), base_dd.xy, base_dd.zw); + normal_rg = textureGrad(_texture_array_normal, vec3(matUV, float(out_mat.base)), base_dd.xy, base_dd.zw); + + // Unpack & rotate base normal for blending + normal_rg.xyz = unpack_normal(normal_rg); + } + // Apply color to base + albedo_ht.rgb *= _texture_color_array[out_mat.base].rgb; + + // Apply Roughness modifier to base + normal_rg.a = clamp(normal_rg.a + _texture_roughness_mod_array[out_mat.base], 0., 1.); + + out_mat.alb_ht = albedo_ht; + out_mat.nrm_rg = normal_rg; + + if (out_mat.blend > 0.) { + // 2 lookups + // Setup overlay texture to blend + float mat_scale2 = _texture_uv_scale_array[out_mat.over]; + vec2 matUV2 = base_uv * mat_scale2; + vec4 over_dd = ddxy * mat_scale2; + vec4 albedo_ht2 = textureGrad(_texture_array_albedo, vec3(matUV2, float(out_mat.over)), over_dd.xy, over_dd.zw); + vec4 normal_rg2 = textureGrad(_texture_array_normal, vec3(matUV2, float(out_mat.over)), over_dd.xy, over_dd.zw); + + // Unpack & rotate overlay normal for blending + normal_rg2.xyz = unpack_normal(normal_rg2); + + // Apply color to overlay + albedo_ht2.rgb *= _texture_color_array[out_mat.over].rgb; + + // Apply Roughness modifier to overlay + normal_rg2.a = clamp(normal_rg2.a + _texture_roughness_mod_array[out_mat.over], 0., 1.); + + // apply world space normal weighting from base, to overlay layer + // Its a matrix Mult, but the value is rather high, so not cutting this one. + if (world_space_normal_blend) { + albedo_ht2.a *= bool(control >>3u & 0x1u) ? 1.0 : clamp((v_tbn * normal_rg.xyz).y, 0.0, 1.0); + } + + // Blend overlay and base + out_mat.alb_ht = height_blend4(albedo_ht, albedo_ht.a, albedo_ht2, albedo_ht2.a, out_mat.blend); + out_mat.nrm_rg = height_blend4(normal_rg, albedo_ht.a, normal_rg2, albedo_ht2.a, out_mat.blend); + out_mat.nrm_depth = height_blend1(_texture_normal_depth_array[out_mat.base], albedo_ht.a, + _texture_normal_depth_array[out_mat.over], albedo_ht2.a, out_mat.blend); + out_mat.ao_str = height_blend1(_texture_ao_strength_array[out_mat.base], albedo_ht.a, + _texture_ao_strength_array[out_mat.over], albedo_ht2.a, out_mat.blend); + } + return; +} + +void fragment() { + // Recover UVs + vec2 uv = UV; + vec2 uv2 = UV2; + + vec3 base_ddx = dFdxCoarse(v_vertex); + vec3 base_ddy = dFdyCoarse(v_vertex); + vec4 base_derivatives = vec4(base_ddx.xz, base_ddy.xz); + float region_mip = log2(max(length(base_ddx.xz), length(base_ddy.xz)) * _vertex_density); + + // Colormap. 1 lookup + // For speed sake, we'll live with cross region artifacts. + #define COLOR_MAP vec4(1.0, 1.0, 1.0, 0.5) + vec3 region_uv = get_index_uv(uv2); + vec4 color_map = region_uv.z > -1.0 ? textureLod(_color_maps, region_uv, region_mip) : COLOR_MAP; + + Material mat[4]; + uint control = floatBitsToUint(texelFetch(_control_maps, get_index_coord(floor(uv), FRAGMENT_PASS), 0)).r; + get_material(base_derivatives, control, region_uv, mat[3]); + + vec4 albedo_height = mat[3].alb_ht; + vec4 normal_rough = mat[3].nrm_rg; + float normal_map_depth = mat[3].nrm_depth; + float ao_strength = mat[3].ao_str; + + // Only do blend if we really have to. + if (v_lerp == 1) { + get_material(base_derivatives, v_control[0], region_uv, mat[0]); + get_material(base_derivatives, v_control[1], region_uv, mat[1]); + get_material(base_derivatives, v_control[2], region_uv, mat[2]); + + // we dont need weights before this point when using vertex normals. + vec2 weight = fract(uv); + vec2 invert = 1.0 - weight; + vec4 weights = vec4( + invert.x * weight.y, // 0 + weight.x * weight.y, // 1 + weight.x * invert.y, // 2 + invert.x * invert.y // 3 + ); + + // Interpolate Albedo/Height/Normal/Roughness + albedo_height = + mat[0].alb_ht * weights[0] + + mat[1].alb_ht * weights[1] + + mat[2].alb_ht * weights[2] + + mat[3].alb_ht * weights[3] ; + + normal_rough = + mat[0].nrm_rg * weights[0] + + mat[1].nrm_rg * weights[1] + + mat[2].nrm_rg * weights[2] + + mat[3].nrm_rg * weights[3] ; + + normal_map_depth = + mat[0].nrm_depth * weights[0] + + mat[1].nrm_depth * weights[1] + + mat[2].nrm_depth * weights[2] + + mat[3].nrm_depth * weights[3] ; + + ao_strength = + mat[0].ao_str * weights[0] + + mat[1].ao_str * weights[1] + + mat[2].ao_str * weights[2] + + mat[3].ao_str * weights[3] ; + } + + // Wetness/roughness modifier, converting 0 - 1 range to -1 to 1 range + float roughness = fma(color_map.a - 0.5, 2.0, normal_rough.a); + + // Apply PBR + ALBEDO = albedo_height.rgb * color_map.rgb; + ROUGHNESS = roughness; + SPECULAR = 1. - normal_rough.a; + NORMAL_MAP = pack_normal(normal_rough.rgb); + NORMAL_MAP_DEPTH = normal_map_depth; + + // Higher and/or facing up, less occluded. + // This is also virtually free. + float ao = (1.0 - (albedo_height.a * log(2.1 - ao_strength))) * (1.0 - normal_rough.y); + AO = clamp(1.0 - ao * ao_strength, albedo_height.a, 1.0); + AO_LIGHT_AFFECT = albedo_height.a; + +} diff --git a/addons/terrain_3d/extras/lightweight.gdshader.uid b/addons/terrain_3d/extras/lightweight.gdshader.uid new file mode 100644 index 0000000..b4cd1db --- /dev/null +++ b/addons/terrain_3d/extras/lightweight.gdshader.uid @@ -0,0 +1 @@ +uid://bbx2xhanpq5l3 diff --git a/addons/terrain_3d/extras/lowpoly_colormap.gdshader b/addons/terrain_3d/extras/lowpoly_colormap.gdshader new file mode 100644 index 0000000..ab14af2 --- /dev/null +++ b/addons/terrain_3d/extras/lowpoly_colormap.gdshader @@ -0,0 +1,163 @@ +// Copyright © 2025 Cory Petkovsek, Roope Palmroos, and Contributors. +// This is an example of a minimal, low-poly style shader colored by the color map and wetness tools. +// No textures are needed or used in this shader. + +shader_type spatial; +render_mode blend_mix,depth_draw_opaque,cull_back,diffuse_burley,specular_schlick_ggx,skip_vertex_transform; + +// Defined Constants +#define SKIP_PASS 0 +#define VERTEX_PASS 1 +#define FRAGMENT_PASS 2 + +#if CURRENT_RENDERER == RENDERER_COMPATIBILITY + #define fma(a, b, c) ((a) * (b) + (c)) + #define dFdxCoarse(a) dFdx(a) + #define dFdyCoarse(a) dFdy(a) +#endif + +// Private uniforms +uniform vec3 _camera_pos = vec3(0.f); +uniform float _mesh_size = 48.f; +uniform uint _background_mode = 1u; // NONE = 0, FLAT = 1, NOISE = 2 +uniform uint _mouse_layer = 0x80000000u; // Layer 32 +uniform float _vertex_spacing = 1.0; +uniform float _vertex_density = 1.0; // = 1/_vertex_spacing +uniform float _region_size = 1024.0; +uniform float _region_texel_size = 0.0009765625; // = 1/1024 +uniform int _region_map_size = 32; +uniform int _region_map[1024]; +uniform vec2 _region_locations[1024]; +uniform highp sampler2DArray _height_maps : repeat_disable; +uniform highp sampler2DArray _control_maps : repeat_disable; +uniform highp sampler2DArray _color_maps : source_color, filter_nearest_mipmap, repeat_disable; + +// Public uniforms +uniform vec3 default_albedo : source_color = vec3(.38, .35, .3); +uniform float default_roughness : hint_range(0.0, 1.0, 0.01) = 0.8; + +// Varyings & Types +// Some are required for editor functions +varying float v_vertex_xz_dist; +varying vec3 v_vertex; + +//////////////////////// +// Vertex +//////////////////////// + +// Takes in world space XZ (UV) coordinates & search depth (only applicable for background mode none) +// Returns ivec3 with: +// XY: (0 to _region_size - 1) coordinates within a region +// Z: layer index used for texturearrays, -1 if not in a region +ivec3 get_index_coord(const vec2 uv, const int search) { + vec2 r_uv = round(uv); + vec2 o_uv = mod(r_uv,_region_size); + ivec2 pos; + int bounds, layer_index = -1; + for (int i = -1; i < clamp(search, SKIP_PASS, FRAGMENT_PASS); i++) { + if ((layer_index == -1 && _background_mode == 0u ) || i < 0) { + r_uv -= i == -1 ? vec2(0.0) : vec2(float(o_uv.x <= o_uv.y), float(o_uv.y <= o_uv.x)); + pos = ivec2(floor((r_uv) * _region_texel_size)) + (_region_map_size / 2); + bounds = int(uint(pos.x | pos.y) < uint(_region_map_size)); + layer_index = (_region_map[ pos.y * _region_map_size + pos.x ] * bounds - 1); + } + } + return ivec3(ivec2(mod(r_uv,_region_size)), layer_index); +} + +// Takes in descaled (world_space / region_size) world to region space XZ (UV2) coordinates, returns vec3 with: +// XY: (0. to 1.) coordinates within a region +// Z: layer index used for texturearrays, -1 if not in a region +vec3 get_index_uv(const vec2 uv2) { + ivec2 pos = ivec2(floor(uv2)) + (_region_map_size / 2); + int bounds = int(uint(pos.x | pos.y) < uint(_region_map_size)); + int layer_index = _region_map[ pos.y * _region_map_size + pos.x ] * bounds - 1; + return vec3(uv2 - _region_locations[layer_index], float(layer_index)); +} + +void vertex() { + // Get vertex of flat plane in world coordinates and set world UV + v_vertex = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz; + + // Camera distance to vertex on flat plane + v_vertex_xz_dist = length(v_vertex.xz - _camera_pos.xz); + + // Geomorph vertex, set end and start for linear height interpolate + float scale = MODEL_MATRIX[0][0]; + float vertex_lerp = smoothstep(0.55, 0.95, (v_vertex_xz_dist / scale - _mesh_size - 4.0) / (_mesh_size - 2.0)); + vec2 v_fract = fract(VERTEX.xz * 0.5) * 2.0; + // For LOD0 morph from a regular grid to an alternating grid to align with LOD1+ + vec2 shift = (scale < _vertex_spacing + 1e-6) ? // LOD0 or not + // Shift from regular to symetric + mix(v_fract, vec2(v_fract.x, -v_fract.y), + round(fract(round(mod(v_vertex.z * _vertex_density, 4.0)) * + round(mod(v_vertex.x * _vertex_density, 4.0)) * 0.25)) + ) : + // Symetric shift + v_fract * round((fract(v_vertex.xz * 0.25 / scale) - 0.5) * 4.0); + vec2 start_pos = v_vertex.xz * _vertex_density; + vec2 end_pos = (v_vertex.xz - shift * scale) * _vertex_density; + v_vertex.xz -= shift * scale * vertex_lerp; + + // UV coordinates in world space. Values are 0 to _region_size within regions + UV = v_vertex.xz * _vertex_density; + + // UV coordinates in region space + texel offset. Values are 0 to 1 within regions + UV2 = fma(UV, vec2(_region_texel_size), vec2(0.5 * _region_texel_size)); + + // Discard vertices for Holes. 1 lookup + ivec3 region = get_index_coord(start_pos, VERTEX_PASS); + uint control = floatBitsToUint(texelFetch(_control_maps, region, 0)).r; + bool hole = bool(control >>2u & 0x1u); + + // Show holes to all cameras except mouse camera (on exactly 1 layer) + if ( !(CAMERA_VISIBLE_LAYERS == _mouse_layer) && + (hole || (_background_mode == 0u && region.z < 0))) { + v_vertex.x = 0. / 0.; + } else { + // Interpolate Geomorph Start & End, set height. 2 Lookups. + ivec3 uv_a = get_index_coord(start_pos, VERTEX_PASS); + ivec3 uv_b = get_index_coord(end_pos, VERTEX_PASS); + float h = mix(texelFetch(_height_maps, uv_a, 0).r, texelFetch(_height_maps, uv_b, 0).r, vertex_lerp); + v_vertex.y = h; + } + + // Convert model space to view space w/ skip_vertex_transform render mode + VERTEX = (VIEW_MATRIX * vec4(v_vertex, 1.0)).xyz; + NORMAL = normalize((MODELVIEW_MATRIX * vec4(NORMAL, 0.0)).xyz); + BINORMAL = normalize((MODELVIEW_MATRIX * vec4(BINORMAL, 0.0)).xyz); + TANGENT = normalize((MODELVIEW_MATRIX * vec4(TANGENT, 0.0)).xyz); +} + +//////////////////////// +// Fragment +//////////////////////// + +void fragment() { + // Recover UVs + vec2 uv = UV; + vec2 uv2 = UV2; + + // Apply terrain normals + vec3 ddx = dFdxCoarse(VERTEX); + vec3 ddy = dFdyCoarse(VERTEX); + NORMAL = normalize(cross(ddy, ddx)); + TANGENT = normalize(cross(NORMAL, vec3(0.0, 0.0, 1.0))); + BINORMAL = normalize(cross(NORMAL, TANGENT)); + + // Determine if we're in a region or not (region_uv.z>0) + vec3 region_uv = get_index_uv(uv2); + + // Colormap. 1 lookup + float lod = log2(max(length(ddx.xz), length(ddy.xz)) * _vertex_density); + vec4 color_map = region_uv.z > -1.0 ? + textureLod(_color_maps, region_uv, lod) : vec4(1., 1., 1., .5); + + // Wetness/roughness modifier, converting 0 - 1 range to -1 to 1 range + float roughness = fma(color_map.a - 0.5, 2.0, default_roughness); + + // Apply PBR + ALBEDO = default_albedo * color_map.rgb; + ROUGHNESS = roughness; + SPECULAR = 1.0 - roughness; +} diff --git a/addons/terrain_3d/extras/lowpoly_colormap.gdshader.uid b/addons/terrain_3d/extras/lowpoly_colormap.gdshader.uid new file mode 100644 index 0000000..81c4d01 --- /dev/null +++ b/addons/terrain_3d/extras/lowpoly_colormap.gdshader.uid @@ -0,0 +1 @@ +uid://bda7fq1rh3nmv diff --git a/addons/terrain_3d/extras/lowpoly_minimum.gdshader b/addons/terrain_3d/extras/lowpoly_minimum.gdshader new file mode 100644 index 0000000..3c2109a --- /dev/null +++ b/addons/terrain_3d/extras/lowpoly_minimum.gdshader @@ -0,0 +1,134 @@ +// Copyright © 2025 Cory Petkovsek, Roope Palmroos, and Contributors. +// This shader is a version of minimum.gdshader with flat normals for a low poly look. +// Increase vertex_spacing for a better result. + +shader_type spatial; +render_mode blend_mix,depth_draw_opaque,cull_back,diffuse_burley,specular_schlick_ggx,skip_vertex_transform; + +// Defined Constants +#define SKIP_PASS 0 +#define VERTEX_PASS 1 +#define FRAGMENT_PASS 2 + +#if CURRENT_RENDERER == RENDERER_COMPATIBILITY + #define fma(a, b, c) ((a) * (b) + (c)) + #define dFdxCoarse(a) dFdx(a) + #define dFdyCoarse(a) dFdy(a) +#endif + +// Private uniforms +uniform vec3 _camera_pos = vec3(0.f); +uniform float _mesh_size = 48.f; +uniform uint _background_mode = 1u; // NONE = 0, FLAT = 1, NOISE = 2 +uniform uint _mouse_layer = 0x80000000u; // Layer 32 +uniform float _vertex_spacing = 1.0; +uniform float _vertex_density = 1.0; // = 1/_vertex_spacing +uniform float _region_size = 1024.0; +uniform float _region_texel_size = 0.0009765625; // = 1/1024 +uniform int _region_map_size = 32; +uniform int _region_map[1024]; +uniform vec2 _region_locations[1024]; +uniform highp sampler2DArray _height_maps : repeat_disable; +uniform highp sampler2DArray _control_maps : repeat_disable; + +// Varyings & Types +// Some are required for editor functions +varying float v_vertex_xz_dist; +varying vec3 v_vertex; + +//////////////////////// +// Vertex +//////////////////////// + +// Takes in world space XZ (UV) coordinates & search depth (only applicable for background mode none) +// Returns ivec3 with: +// XY: (0 to _region_size - 1) coordinates within a region +// Z: layer index used for texturearrays, -1 if not in a region +ivec3 get_index_coord(const vec2 uv, const int search) { + vec2 r_uv = round(uv); + vec2 o_uv = mod(r_uv,_region_size); + ivec2 pos; + int bounds, layer_index = -1; + for (int i = -1; i < clamp(search, SKIP_PASS, FRAGMENT_PASS); i++) { + if ((layer_index == -1 && _background_mode == 0u ) || i < 0) { + r_uv -= i == -1 ? vec2(0.0) : vec2(float(o_uv.x <= o_uv.y), float(o_uv.y <= o_uv.x)); + pos = ivec2(floor((r_uv) * _region_texel_size)) + (_region_map_size / 2); + bounds = int(uint(pos.x | pos.y) < uint(_region_map_size)); + layer_index = (_region_map[ pos.y * _region_map_size + pos.x ] * bounds - 1); + } + } + return ivec3(ivec2(mod(r_uv,_region_size)), layer_index); +} + +void vertex() { + // Get vertex of flat plane in world coordinates and set world UV + v_vertex = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz; + + // Camera distance to vertex on flat plane + v_vertex_xz_dist = length(v_vertex.xz - _camera_pos.xz); + + // Geomorph vertex, set end and start for linear height interpolate + float scale = MODEL_MATRIX[0][0]; + float vertex_lerp = smoothstep(0.55, 0.95, (v_vertex_xz_dist / scale - _mesh_size - 4.0) / (_mesh_size - 2.0)); + vec2 v_fract = fract(VERTEX.xz * 0.5) * 2.0; + // For LOD0 morph from a regular grid to an alternating grid to align with LOD1+ + vec2 shift = (scale < _vertex_spacing + 1e-6) ? // LOD0 or not + // Shift from regular to symetric + mix(v_fract, vec2(v_fract.x, -v_fract.y), + round(fract(round(mod(v_vertex.z * _vertex_density, 4.0)) * + round(mod(v_vertex.x * _vertex_density, 4.0)) * 0.25)) + ) : + // Symetric shift + v_fract * round((fract(v_vertex.xz * 0.25 / scale) - 0.5) * 4.0); + vec2 start_pos = v_vertex.xz * _vertex_density; + vec2 end_pos = (v_vertex.xz - shift * scale) * _vertex_density; + v_vertex.xz -= shift * scale * vertex_lerp; + + // UV coordinates in world space. Values are 0 to _region_size within regions + UV = v_vertex.xz * _vertex_density; + + // UV coordinates in region space + texel offset. Values are 0 to 1 within regions + UV2 = fma(UV, vec2(_region_texel_size), vec2(0.5 * _region_texel_size)); + + // Discard vertices for Holes. 1 lookup + ivec3 region = get_index_coord(start_pos, VERTEX_PASS); + uint control = floatBitsToUint(texelFetch(_control_maps, region, 0)).r; + bool hole = bool(control >>2u & 0x1u); + + // Show holes to all cameras except mouse camera (on exactly 1 layer) + if ( !(CAMERA_VISIBLE_LAYERS == _mouse_layer) && + (hole || (_background_mode == 0u && region.z < 0))) { + v_vertex.x = 0. / 0.; + } else { + // Interpolate Geomorph Start & End, set height. 2 Lookups. + ivec3 uv_a = get_index_coord(start_pos, VERTEX_PASS); + ivec3 uv_b = get_index_coord(end_pos, VERTEX_PASS); + float h = mix(texelFetch(_height_maps, uv_a, 0).r, texelFetch(_height_maps, uv_b, 0).r, vertex_lerp); + v_vertex.y = h; + } + + // Convert model space to view space w/ skip_vertex_transform render mode + VERTEX = (VIEW_MATRIX * vec4(v_vertex, 1.0)).xyz; + NORMAL = normalize((MODELVIEW_MATRIX * vec4(NORMAL, 0.0)).xyz); + BINORMAL = normalize((MODELVIEW_MATRIX * vec4(BINORMAL, 0.0)).xyz); + TANGENT = normalize((MODELVIEW_MATRIX * vec4(TANGENT, 0.0)).xyz); +} + +//////////////////////// +// Fragment +//////////////////////// + +void fragment() { + // Recover UVs + vec2 uv = UV; + vec2 uv2 = UV2; + + // Apply terrain normals + NORMAL = normalize(cross(dFdyCoarse(VERTEX),dFdxCoarse(VERTEX))); + TANGENT = normalize(cross(NORMAL, vec3(0.0, 0.0, 1.0))); + BINORMAL = normalize(cross(NORMAL, TANGENT)); + + // Apply PBR + ALBEDO = vec3(.2); + ROUGHNESS = .7; +} diff --git a/addons/terrain_3d/extras/lowpoly_minimum.gdshader.uid b/addons/terrain_3d/extras/lowpoly_minimum.gdshader.uid new file mode 100644 index 0000000..530e49b --- /dev/null +++ b/addons/terrain_3d/extras/lowpoly_minimum.gdshader.uid @@ -0,0 +1 @@ +uid://x11v7w7v8hqa diff --git a/addons/terrain_3d/extras/minimum.gdshader b/addons/terrain_3d/extras/minimum.gdshader new file mode 100644 index 0000000..9505182 --- /dev/null +++ b/addons/terrain_3d/extras/minimum.gdshader @@ -0,0 +1,208 @@ +// Copyright © 2025 Cory Petkovsek, Roope Palmroos, and Contributors. +// This shader is the minimum needed to allow the terrain to function, without any texturing. + +shader_type spatial; +render_mode blend_mix,depth_draw_opaque,cull_back,diffuse_burley,specular_schlick_ggx,skip_vertex_transform; + +// Defined Constants +#define SKIP_PASS 0 +#define VERTEX_PASS 1 +#define FRAGMENT_PASS 2 + +#if CURRENT_RENDERER == RENDERER_COMPATIBILITY + #define fma(a, b, c) ((a) * (b) + (c)) + #define dFdxCoarse(a) dFdx(a) + #define dFdyCoarse(a) dFdy(a) +#endif + +// Private uniforms +// Commented uniforms aren't needed for this shader, but are available for your own needs. +uniform vec3 _camera_pos = vec3(0.f); +uniform float _mesh_size = 48.f; +uniform uint _background_mode = 1u; // NONE = 0, FLAT = 1, NOISE = 2 +uniform uint _mouse_layer = 0x80000000u; // Layer 32 +uniform float _vertex_spacing = 1.0; +uniform float _vertex_density = 1.0; // = 1/_vertex_spacing +uniform float _region_size = 1024.0; +uniform float _region_texel_size = 0.0009765625; // = 1/1024 +uniform int _region_map_size = 32; +uniform int _region_map[1024]; +uniform vec2 _region_locations[1024]; +//uniform float _texture_uv_scale_array[32]; +//uniform float _texture_detile_array[32]; +//uniform vec4 _texture_color_array[32]; +uniform highp sampler2DArray _height_maps : repeat_disable; +uniform highp sampler2DArray _control_maps : repeat_disable; +//uniform highp sampler2DArray _color_maps : source_color, filter_linear_mipmap_anisotropic, repeat_disable; +//uniform highp sampler2DArray _texture_array_albedo : source_color, filter_linear_mipmap_anisotropic, repeat_enable; +//uniform highp sampler2DArray _texture_array_normal : hint_normal, filter_linear_mipmap_anisotropic, repeat_enable; + +// Varyings & Types +// Some are required for editor functions +varying float v_vertex_xz_dist; +varying vec3 v_vertex; + +//////////////////////// +// Vertex +//////////////////////// + +// Takes in world space XZ (UV) coordinates & search depth (only applicable for background mode none) +// Returns ivec3 with: +// XY: (0 to _region_size - 1) coordinates within a region +// Z: layer index used for texturearrays, -1 if not in a region +ivec3 get_index_coord(const vec2 uv, const int search) { + vec2 r_uv = round(uv); + vec2 o_uv = mod(r_uv,_region_size); + ivec2 pos; + int bounds, layer_index = -1; + for (int i = -1; i < clamp(search, SKIP_PASS, FRAGMENT_PASS); i++) { + if ((layer_index == -1 && _background_mode == 0u ) || i < 0) { + r_uv -= i == -1 ? vec2(0.0) : vec2(float(o_uv.x <= o_uv.y), float(o_uv.y <= o_uv.x)); + pos = ivec2(floor((r_uv) * _region_texel_size)) + (_region_map_size / 2); + bounds = int(uint(pos.x | pos.y) < uint(_region_map_size)); + layer_index = (_region_map[ pos.y * _region_map_size + pos.x ] * bounds - 1); + } + } + return ivec3(ivec2(mod(r_uv,_region_size)), layer_index); +} + +void vertex() { + // Get vertex of flat plane in world coordinates and set world UV + v_vertex = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz; + + // Camera distance to vertex on flat plane + v_vertex_xz_dist = length(v_vertex.xz - _camera_pos.xz); + + // Geomorph vertex, set end and start for linear height interpolate + float scale = MODEL_MATRIX[0][0]; + float vertex_lerp = smoothstep(0.55, 0.95, (v_vertex_xz_dist / scale - _mesh_size - 4.0) / (_mesh_size - 2.0)); + vec2 v_fract = fract(VERTEX.xz * 0.5) * 2.0; + // For LOD0 morph from a regular grid to an alternating grid to align with LOD1+ + vec2 shift = (scale < _vertex_spacing + 1e-6) ? // LOD0 or not + // Shift from regular to symetric + mix(v_fract, vec2(v_fract.x, -v_fract.y), + round(fract(round(mod(v_vertex.z * _vertex_density, 4.0)) * + round(mod(v_vertex.x * _vertex_density, 4.0)) * 0.25)) + ) : + // Symetric shift + v_fract * round((fract(v_vertex.xz * 0.25 / scale) - 0.5) * 4.0); + vec2 start_pos = v_vertex.xz * _vertex_density; + vec2 end_pos = (v_vertex.xz - shift * scale) * _vertex_density; + v_vertex.xz -= shift * scale * vertex_lerp; + + // UV coordinates in world space. Values are 0 to _region_size within regions + UV = v_vertex.xz * _vertex_density; + + // UV coordinates in region space + texel offset. Values are 0 to 1 within regions + UV2 = fma(UV, vec2(_region_texel_size), vec2(0.5 * _region_texel_size)); + + // Discard vertices for Holes. 1 lookup + ivec3 v_region = get_index_coord(start_pos, VERTEX_PASS); + uint control = floatBitsToUint(texelFetch(_control_maps, v_region, 0)).r; + bool hole = bool(control >>2u & 0x1u); + + // Show holes to all cameras except mouse camera (on exactly 1 layer) + if ( !(CAMERA_VISIBLE_LAYERS == _mouse_layer) && + (hole || (_background_mode == 0u && v_region.z < 0))) { + v_vertex.x = 0. / 0.; + } else { + // Interpolate Geomorph Start & End, set height. 2 Lookups. + ivec3 uv_a = get_index_coord(start_pos, VERTEX_PASS); + ivec3 uv_b = get_index_coord(end_pos, VERTEX_PASS); + float h = mix(texelFetch(_height_maps, uv_a, 0).r, texelFetch(_height_maps, uv_b, 0).r, vertex_lerp); + v_vertex.y = h; + } + + // Convert model space to view space w/ skip_vertex_transform render mode + VERTEX = (VIEW_MATRIX * vec4(v_vertex, 1.0)).xyz; + NORMAL = normalize((MODELVIEW_MATRIX * vec4(NORMAL, 0.0)).xyz); + BINORMAL = normalize((MODELVIEW_MATRIX * vec4(BINORMAL, 0.0)).xyz); + TANGENT = normalize((MODELVIEW_MATRIX * vec4(TANGENT, 0.0)).xyz); +} + +//////////////////////// +// Fragment +//////////////////////// + +void fragment() { + // Recover UVs + vec2 uv = UV; + vec2 uv2 = UV2; + + // Lookup offsets, ID and blend weight + const vec3 offsets = vec3(0, 1, 2); + vec2 index_id = floor(uv); + vec2 weight = fract(uv); + vec2 invert = 1.0 - weight; + vec4 weights = vec4( + invert.x * weight.y, // 0 + weight.x * weight.y, // 1 + weight.x * invert.y, // 2 + invert.x * invert.y // 3 + ); + + vec3 base_ddx = dFdxCoarse(v_vertex); + vec3 base_ddy = dFdyCoarse(v_vertex); + vec4 base_derivatives = vec4(base_ddx.xz, base_ddy.xz); + // Calculate the effective mipmap for regionspace, and if less than 0, + // skip all extra lookups required for bilinear blend. + float region_mip = log2(max(length(base_ddx.xz), length(base_ddy.xz)) * _vertex_density); + bool bilerp = region_mip < 0.0; + + ivec3 indexUV[4]; + // control map lookups, used for some normal lookups as well + indexUV[0] = get_index_coord(index_id + offsets.xy, FRAGMENT_PASS); + indexUV[1] = get_index_coord(index_id + offsets.yy, FRAGMENT_PASS); + indexUV[2] = get_index_coord(index_id + offsets.yx, FRAGMENT_PASS); + indexUV[3] = get_index_coord(index_id + offsets.xx, FRAGMENT_PASS); + + // Terrain normals + vec3 index_normal[4]; + float h[8]; + // allows additional derivatives, eg world noise, brush previews etc + float u = 0.0; + float v = 0.0; + + // Re-use the indexUVs for the first lookups, skipping some math. 3 lookups + h[3] = texelFetch(_height_maps, indexUV[3], 0).r; // 0 (0,0) + h[2] = texelFetch(_height_maps, indexUV[2], 0).r; // 1 (1,0) + h[0] = texelFetch(_height_maps, indexUV[0], 0).r; // 2 (0,1) + index_normal[3] = normalize(vec3(h[3] - h[2] + u, _vertex_spacing, h[3] - h[0] + v)); + + // Set flat world normal - overriden if bilerp is true + vec3 w_normal = index_normal[3]; + + // Branching smooth normals must be done seperatley for correct normals at all 4 index ids + if (bilerp) { + // 5 lookups + // Fetch the additional required height values for smooth normals + h[1] = texelFetch(_height_maps, indexUV[1], 0).r; // 3 (1,1) + h[4] = texelFetch(_height_maps, get_index_coord(index_id + offsets.yz, FRAGMENT_PASS), 0).r; // 4 (1,2) + h[5] = texelFetch(_height_maps, get_index_coord(index_id + offsets.zy, FRAGMENT_PASS), 0).r; // 5 (2,1) + h[6] = texelFetch(_height_maps, get_index_coord(index_id + offsets.zx, FRAGMENT_PASS), 0).r; // 6 (2,0) + h[7] = texelFetch(_height_maps, get_index_coord(index_id + offsets.xz, FRAGMENT_PASS), 0).r; // 7 (0,2) + + // Calculate the normal for the remaining index ids. + index_normal[0] = normalize(vec3(h[0] - h[1] + u, _vertex_spacing, h[0] - h[7] + v)); + index_normal[1] = normalize(vec3(h[1] - h[5] + u, _vertex_spacing, h[1] - h[4] + v)); + index_normal[2] = normalize(vec3(h[2] - h[6] + u, _vertex_spacing, h[2] - h[1] + v)); + + // Set interpolated world normal + w_normal = + index_normal[0] * weights[0] + + index_normal[1] * weights[1] + + index_normal[2] * weights[2] + + index_normal[3] * weights[3] ; + } + + // Apply terrain normals + vec3 w_tangent = normalize(cross(w_normal, vec3(0.0, 0.0, 1.0))); + vec3 w_binormal = normalize(cross(w_normal, w_tangent)); + NORMAL = mat3(VIEW_MATRIX) * w_normal; + TANGENT = mat3(VIEW_MATRIX) * w_tangent; + BINORMAL = mat3(VIEW_MATRIX) * w_binormal; + + // Apply PBR + ALBEDO = vec3(.2); + ROUGHNESS = .7; +} diff --git a/addons/terrain_3d/extras/minimum.gdshader.uid b/addons/terrain_3d/extras/minimum.gdshader.uid new file mode 100644 index 0000000..8d390b2 --- /dev/null +++ b/addons/terrain_3d/extras/minimum.gdshader.uid @@ -0,0 +1 @@ +uid://01qauauvd8aa diff --git a/addons/terrain_3d/extras/project_on_terrain3d.gd b/addons/terrain_3d/extras/project_on_terrain3d.gd new file mode 100644 index 0000000..d812352 --- /dev/null +++ b/addons/terrain_3d/extras/project_on_terrain3d.gd @@ -0,0 +1,96 @@ +# Copyright © 2025 Cory Petkovsek, Roope Palmroos, and Contributors. +# This script is an addon for HungryProton's Scatter https://github.com/HungryProton/scatter +# It provides a `Project on Terrain3D` modifier, which allows Scatter +# to detect the terrain height from Terrain3D without using collision. +# +# Copy this file into /addons/proton_scatter/src/modifiers +# Then uncomment everything below (select, press CTRL+K) +# In the editor, add this modifier to Scatter, then set your Terrain3D node + +#@tool +#extends "base_modifier.gd" +# +# +#signal projection_completed +# +# +#@export var terrain_node : NodePath +#@export var align_with_collision_normal := false +#@export_range(0.0, 90.0, 0.1) var max_slope = 90.0 +# +#var _terrain: Terrain3D +# +# +#func _init() -> void: + #display_name = "Project On Terrain3D" + #category = "Edit" + #can_restrict_height = false + #global_reference_frame_available = true + #local_reference_frame_available = true + #individual_instances_reference_frame_available = true + #use_global_space_by_default() +# + #documentation.add_paragraph( + #"This is a duplicate of `Project on Colliders` that queries the terrain system + #for height and sets the transform height appropriately. +# + #This modifier must have terrain_node set to a Terrain3D node.") +# + #var p := documentation.add_parameter("Terrain Node") + #p.set_type("NodePath") + #p.set_description("Set your Terrain3D node.") + # + #p = documentation.add_parameter("Align with collision normal") + #p.set_type("bool") + #p.set_description( + #"Rotate the transform to align it with the collision normal in case + #the ray cast hit a collider.") +# +# +#func _process_transforms(transforms, domain, _seed) -> void: + #if transforms.is_empty(): + #return +# + #if terrain_node: + #_terrain = domain.get_root().get_node_or_null(terrain_node) +# + #if not _terrain: + #warning += """No Terrain3D node found""" + #return +# + #if not _terrain.data: + #warning += """Terrain3DData is not initialized""" + #return +# + ## Get global transform + #var gt: Transform3D = domain.get_global_transform() + #var gt_inverse := gt.affine_inverse() + #var new_transforms_array: Array[Transform3D] = [] + #var remapped_max_slope: float = remap(max_slope, 0.0, 90.0, 0.0, 1.0) + #for i in transforms.list.size(): + #var t: Transform3D = transforms.list[i] + # + #var location: Vector3 = (gt * t).origin + #var height: float = _terrain.data.get_height(location) + #var normal: Vector3 = _terrain.data.get_normal(location) + # + #if align_with_collision_normal and not is_nan(normal.x): + #t.basis.y = normal + #t.basis.x = -t.basis.z.cross(normal) + #t.basis = t.basis.orthonormalized() +# + #if abs(Vector3.UP.dot(normal)) >= (1.0 - remapped_max_slope): + #t.origin.y = gt.origin.y if is_nan(height) else height - gt.origin.y + #new_transforms_array.push_back(t) +# + #transforms.list.clear() + #transforms.list.append_array(new_transforms_array) +# + #if transforms.is_empty(): + #warning += """All transforms have been removed. Possible reasons include: \n + #+ No collider is close enough to the shapes. + #+ Ray length is too short. + #+ Ray direction is incorrect. + #+ Collision mask is not set properly. + #+ Max slope is too low. + #""" diff --git a/addons/terrain_3d/extras/project_on_terrain3d.gd.uid b/addons/terrain_3d/extras/project_on_terrain3d.gd.uid new file mode 100644 index 0000000..6911309 --- /dev/null +++ b/addons/terrain_3d/extras/project_on_terrain3d.gd.uid @@ -0,0 +1 @@ +uid://g3opjh3m3iww diff --git a/addons/terrain_3d/extras/region_mover.gd b/addons/terrain_3d/extras/region_mover.gd new file mode 100644 index 0000000..78dcbd8 --- /dev/null +++ b/addons/terrain_3d/extras/region_mover.gd @@ -0,0 +1,52 @@ +# This script can be used to move your regions by an offset. +# Eventually this tool will find its way into a built in UI +# +# Attach it to your Terrain3D node +# Save and reload your scene +# Select your Terrain3D node +# Enter a valid `offset` where all regions will be within -16, +15 +# Run it +# It should unload the regions, rename files, and reload them +# Clear the script and resave your scene + + +@tool +extends Terrain3D + + +@export var offset: Vector2i +@export var run: bool = false : set = start_rename + + +func start_rename(val: bool = false) -> void: + if val == false or offset == Vector2i.ZERO: + return + + var dir_name: String = data_directory + data_directory = "" + var dir := DirAccess.open(dir_name) + if not dir: + print("An error occurred when trying to access the path: ", data_directory) + return + + var affected_files: PackedStringArray + var files: PackedStringArray = dir.get_files() + for file_name in files: + if file_name.match("terrain3d*.res") and not dir.current_is_dir(): + var region_loc: Vector2i = Terrain3DUtil.filename_to_location(file_name) + var new_loc: Vector2i = region_loc + offset + if new_loc.x < -16 or new_loc.x > 15 or new_loc.y < -16 or new_loc.y > 15: + push_error("New location %.0v out of bounds for region %.0v. Aborting" % [ new_loc, region_loc ]) + return + var new_name: String = "tmp_" + Terrain3DUtil.location_to_filename(new_loc) + dir.rename(file_name, new_name) + affected_files.push_back(new_name) + print("File: %s renamed to: %s" % [ file_name, new_name ]) + + for file_name in affected_files: + var new_name: String = file_name.trim_prefix("tmp_") + dir.rename(file_name, new_name) + print("File: %s renamed to: %s" % [ file_name, new_name ]) + + data_directory = dir_name + EditorInterface.get_resource_filesystem().scan() diff --git a/addons/terrain_3d/extras/region_mover.gd.uid b/addons/terrain_3d/extras/region_mover.gd.uid new file mode 100644 index 0000000..0c9b4e5 --- /dev/null +++ b/addons/terrain_3d/extras/region_mover.gd.uid @@ -0,0 +1 @@ +uid://bngnvtbm6ifkk diff --git a/addons/terrain_3d/icons/autoshader.svg b/addons/terrain_3d/icons/autoshader.svg new file mode 100644 index 0000000..5e6ee1a --- /dev/null +++ b/addons/terrain_3d/icons/autoshader.svg @@ -0,0 +1,194 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/addons/terrain_3d/icons/autoshader.svg.import b/addons/terrain_3d/icons/autoshader.svg.import new file mode 100644 index 0000000..1551553 --- /dev/null +++ b/addons/terrain_3d/icons/autoshader.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bdwolwswwy8wr" +path="res://.godot/imported/autoshader.svg-9998e61bbc6afd5b134b767acd17a425.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/terrain_3d/icons/autoshader.svg" +dest_files=["res://.godot/imported/autoshader.svg-9998e61bbc6afd5b134b767acd17a425.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=false diff --git a/addons/terrain_3d/icons/color_paint.svg b/addons/terrain_3d/icons/color_paint.svg new file mode 100644 index 0000000..317b742 --- /dev/null +++ b/addons/terrain_3d/icons/color_paint.svg @@ -0,0 +1,175 @@ + +image/svg+xml diff --git a/addons/terrain_3d/icons/color_paint.svg.import b/addons/terrain_3d/icons/color_paint.svg.import new file mode 100644 index 0000000..56d0d5d --- /dev/null +++ b/addons/terrain_3d/icons/color_paint.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://krrmpalen8xu" +path="res://.godot/imported/color_paint.svg-2a416ebf35da04135017e5c6ef53ea57.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/terrain_3d/icons/color_paint.svg" +dest_files=["res://.godot/imported/color_paint.svg-2a416ebf35da04135017e5c6ef53ea57.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=false diff --git a/addons/terrain_3d/icons/height_add.svg b/addons/terrain_3d/icons/height_add.svg new file mode 100644 index 0000000..299a1b8 --- /dev/null +++ b/addons/terrain_3d/icons/height_add.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/addons/terrain_3d/icons/height_add.svg.import b/addons/terrain_3d/icons/height_add.svg.import new file mode 100644 index 0000000..e1bbbad --- /dev/null +++ b/addons/terrain_3d/icons/height_add.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bcmbqryggekg1" +path="res://.godot/imported/height_add.svg-9e680ce71fa4c541748e081b99167369.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/terrain_3d/icons/height_add.svg" +dest_files=["res://.godot/imported/height_add.svg-9e680ce71fa4c541748e081b99167369.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=false diff --git a/addons/terrain_3d/icons/height_div.svg b/addons/terrain_3d/icons/height_div.svg new file mode 100644 index 0000000..79a6111 --- /dev/null +++ b/addons/terrain_3d/icons/height_div.svg @@ -0,0 +1,196 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/addons/terrain_3d/icons/height_div.svg.import b/addons/terrain_3d/icons/height_div.svg.import new file mode 100644 index 0000000..c894274 --- /dev/null +++ b/addons/terrain_3d/icons/height_div.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://danh7tb2v6rx7" +path="res://.godot/imported/height_div.svg-449a465f9fdd11ab59f2f1c78815408c.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/terrain_3d/icons/height_div.svg" +dest_files=["res://.godot/imported/height_div.svg-449a465f9fdd11ab59f2f1c78815408c.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=false diff --git a/addons/terrain_3d/icons/height_flat.svg b/addons/terrain_3d/icons/height_flat.svg new file mode 100644 index 0000000..98973fe --- /dev/null +++ b/addons/terrain_3d/icons/height_flat.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/addons/terrain_3d/icons/height_flat.svg.import b/addons/terrain_3d/icons/height_flat.svg.import new file mode 100644 index 0000000..fd0e685 --- /dev/null +++ b/addons/terrain_3d/icons/height_flat.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://crj0xfyiyr45u" +path="res://.godot/imported/height_flat.svg-be726a006bf06e05a7a8867510f3996e.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/terrain_3d/icons/height_flat.svg" +dest_files=["res://.godot/imported/height_flat.svg-be726a006bf06e05a7a8867510f3996e.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=false diff --git a/addons/terrain_3d/icons/height_mul.svg b/addons/terrain_3d/icons/height_mul.svg new file mode 100644 index 0000000..ef4f92e --- /dev/null +++ b/addons/terrain_3d/icons/height_mul.svg @@ -0,0 +1,201 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/addons/terrain_3d/icons/height_mul.svg.import b/addons/terrain_3d/icons/height_mul.svg.import new file mode 100644 index 0000000..b20aadc --- /dev/null +++ b/addons/terrain_3d/icons/height_mul.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bu3q0645kb3el" +path="res://.godot/imported/height_mul.svg-2dca20fa42a85408713e9bfe411f3c79.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/terrain_3d/icons/height_mul.svg" +dest_files=["res://.godot/imported/height_mul.svg-2dca20fa42a85408713e9bfe411f3c79.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=false diff --git a/addons/terrain_3d/icons/height_slope.svg b/addons/terrain_3d/icons/height_slope.svg new file mode 100644 index 0000000..fe2903d --- /dev/null +++ b/addons/terrain_3d/icons/height_slope.svg @@ -0,0 +1,105 @@ + + + + diff --git a/addons/terrain_3d/icons/height_slope.svg.import b/addons/terrain_3d/icons/height_slope.svg.import new file mode 100644 index 0000000..ed55b14 --- /dev/null +++ b/addons/terrain_3d/icons/height_slope.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://0cd7so4kw7da" +path="res://.godot/imported/height_slope.svg-e20540c5538d0c57a9d229a772b3d1b3.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/terrain_3d/icons/height_slope.svg" +dest_files=["res://.godot/imported/height_slope.svg-e20540c5538d0c57a9d229a772b3d1b3.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=false diff --git a/addons/terrain_3d/icons/height_smooth.svg b/addons/terrain_3d/icons/height_smooth.svg new file mode 100644 index 0000000..81154c3 --- /dev/null +++ b/addons/terrain_3d/icons/height_smooth.svg @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/addons/terrain_3d/icons/height_smooth.svg.import b/addons/terrain_3d/icons/height_smooth.svg.import new file mode 100644 index 0000000..eef9e53 --- /dev/null +++ b/addons/terrain_3d/icons/height_smooth.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://chrbx4xnxyiel" +path="res://.godot/imported/height_smooth.svg-d8fc43572f5984eef64c886a49988c06.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/terrain_3d/icons/height_smooth.svg" +dest_files=["res://.godot/imported/height_smooth.svg-d8fc43572f5984eef64c886a49988c06.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=false diff --git a/addons/terrain_3d/icons/height_sub.svg b/addons/terrain_3d/icons/height_sub.svg new file mode 100644 index 0000000..bb0d2a7 --- /dev/null +++ b/addons/terrain_3d/icons/height_sub.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/addons/terrain_3d/icons/height_sub.svg.import b/addons/terrain_3d/icons/height_sub.svg.import new file mode 100644 index 0000000..4dc17eb --- /dev/null +++ b/addons/terrain_3d/icons/height_sub.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://mo3hnbk3ffjs" +path="res://.godot/imported/height_sub.svg-1a14a9bb856f3db0faa02dba3c807b50.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/terrain_3d/icons/height_sub.svg" +dest_files=["res://.godot/imported/height_sub.svg-1a14a9bb856f3db0faa02dba3c807b50.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=false diff --git a/addons/terrain_3d/icons/holes.svg b/addons/terrain_3d/icons/holes.svg new file mode 100644 index 0000000..da639d9 --- /dev/null +++ b/addons/terrain_3d/icons/holes.svg @@ -0,0 +1,10 @@ + + + + + + diff --git a/addons/terrain_3d/icons/holes.svg.import b/addons/terrain_3d/icons/holes.svg.import new file mode 100644 index 0000000..8117195 --- /dev/null +++ b/addons/terrain_3d/icons/holes.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bsmaxekrmnuy2" +path="res://.godot/imported/holes.svg-a7cb97bb50d7879cd274646e207b9213.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/terrain_3d/icons/holes.svg" +dest_files=["res://.godot/imported/holes.svg-a7cb97bb50d7879cd274646e207b9213.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=false diff --git a/addons/terrain_3d/icons/layers.svg b/addons/terrain_3d/icons/layers.svg new file mode 100644 index 0000000..5c9c4c9 --- /dev/null +++ b/addons/terrain_3d/icons/layers.svg @@ -0,0 +1,102 @@ + + diff --git a/addons/terrain_3d/icons/layers.svg.import b/addons/terrain_3d/icons/layers.svg.import new file mode 100644 index 0000000..559cd21 --- /dev/null +++ b/addons/terrain_3d/icons/layers.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cs1la1mashf2e" +path="res://.godot/imported/layers.svg-4a679bb626c5179d3773f33e77e4a5e4.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/terrain_3d/icons/layers.svg" +dest_files=["res://.godot/imported/layers.svg-4a679bb626c5179d3773f33e77e4a5e4.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=false diff --git a/addons/terrain_3d/icons/multimesh.svg b/addons/terrain_3d/icons/multimesh.svg new file mode 100644 index 0000000..a62e5f9 --- /dev/null +++ b/addons/terrain_3d/icons/multimesh.svg @@ -0,0 +1,40 @@ + + + + + + diff --git a/addons/terrain_3d/icons/multimesh.svg.import b/addons/terrain_3d/icons/multimesh.svg.import new file mode 100644 index 0000000..1493feb --- /dev/null +++ b/addons/terrain_3d/icons/multimesh.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cjlcl5lf20ve0" +path="res://.godot/imported/multimesh.svg-5487b93b04ddbaae37b5d3e91f10750b.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/terrain_3d/icons/multimesh.svg" +dest_files=["res://.godot/imported/multimesh.svg-5487b93b04ddbaae37b5d3e91f10750b.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=false diff --git a/addons/terrain_3d/icons/navigation.svg b/addons/terrain_3d/icons/navigation.svg new file mode 100644 index 0000000..1056202 --- /dev/null +++ b/addons/terrain_3d/icons/navigation.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/addons/terrain_3d/icons/navigation.svg.import b/addons/terrain_3d/icons/navigation.svg.import new file mode 100644 index 0000000..d8ac263 --- /dev/null +++ b/addons/terrain_3d/icons/navigation.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://f3po5pogkv2b" +path="res://.godot/imported/navigation.svg-1e4cf210c589be8d2911c522d4a17d78.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/terrain_3d/icons/navigation.svg" +dest_files=["res://.godot/imported/navigation.svg-1e4cf210c589be8d2911c522d4a17d78.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=false diff --git a/addons/terrain_3d/icons/picker_checked.svg b/addons/terrain_3d/icons/picker_checked.svg new file mode 100644 index 0000000..653d57e --- /dev/null +++ b/addons/terrain_3d/icons/picker_checked.svg @@ -0,0 +1,43 @@ + + + + + + + diff --git a/addons/terrain_3d/icons/picker_checked.svg.import b/addons/terrain_3d/icons/picker_checked.svg.import new file mode 100644 index 0000000..ff53b15 --- /dev/null +++ b/addons/terrain_3d/icons/picker_checked.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bg8x6o32ggt88" +path="res://.godot/imported/picker_checked.svg-81f35b6ae38bccc8aa9e7ae22b530168.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/terrain_3d/icons/picker_checked.svg" +dest_files=["res://.godot/imported/picker_checked.svg-81f35b6ae38bccc8aa9e7ae22b530168.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=false diff --git a/addons/terrain_3d/icons/region_add.svg b/addons/terrain_3d/icons/region_add.svg new file mode 100644 index 0000000..56d7c92 --- /dev/null +++ b/addons/terrain_3d/icons/region_add.svg @@ -0,0 +1,95 @@ + +image/svg+xml diff --git a/addons/terrain_3d/icons/region_add.svg.import b/addons/terrain_3d/icons/region_add.svg.import new file mode 100644 index 0000000..e50c30b --- /dev/null +++ b/addons/terrain_3d/icons/region_add.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://c0tn453fsckv5" +path="res://.godot/imported/region_add.svg-a05dc161a452dd3e024f9835a737d9f0.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/terrain_3d/icons/region_add.svg" +dest_files=["res://.godot/imported/region_add.svg-a05dc161a452dd3e024f9835a737d9f0.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=false diff --git a/addons/terrain_3d/icons/region_remove.svg b/addons/terrain_3d/icons/region_remove.svg new file mode 100644 index 0000000..315657d --- /dev/null +++ b/addons/terrain_3d/icons/region_remove.svg @@ -0,0 +1,102 @@ + +image/svg+xml diff --git a/addons/terrain_3d/icons/region_remove.svg.import b/addons/terrain_3d/icons/region_remove.svg.import new file mode 100644 index 0000000..6ded856 --- /dev/null +++ b/addons/terrain_3d/icons/region_remove.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cbpo5eamf3bx2" +path="res://.godot/imported/region_remove.svg-5710e8aeb34f1eaa06e637634f4a7d16.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/terrain_3d/icons/region_remove.svg" +dest_files=["res://.godot/imported/region_remove.svg-5710e8aeb34f1eaa06e637634f4a7d16.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=false diff --git a/addons/terrain_3d/icons/terrain3d.svg b/addons/terrain_3d/icons/terrain3d.svg new file mode 100644 index 0000000..ca8c661 --- /dev/null +++ b/addons/terrain_3d/icons/terrain3d.svg @@ -0,0 +1,153 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/addons/terrain_3d/icons/terrain3d.svg.import b/addons/terrain_3d/icons/terrain3d.svg.import new file mode 100644 index 0000000..c077de1 --- /dev/null +++ b/addons/terrain_3d/icons/terrain3d.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bnsydn4jkyeyn" +path="res://.godot/imported/terrain3d.svg-eb45756f1a003759fda81eaa1db10769.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/terrain_3d/icons/terrain3d.svg" +dest_files=["res://.godot/imported/terrain3d.svg-eb45756f1a003759fda81eaa1db10769.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=false diff --git a/addons/terrain_3d/icons/texture_paint.svg b/addons/terrain_3d/icons/texture_paint.svg new file mode 100644 index 0000000..908d3f4 --- /dev/null +++ b/addons/terrain_3d/icons/texture_paint.svg @@ -0,0 +1,151 @@ + +image/svg+xml diff --git a/addons/terrain_3d/icons/texture_paint.svg.import b/addons/terrain_3d/icons/texture_paint.svg.import new file mode 100644 index 0000000..8a09d92 --- /dev/null +++ b/addons/terrain_3d/icons/texture_paint.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://duo8valena3a2" +path="res://.godot/imported/texture_paint.svg-72da4fd2096377e625a8fe09cdacb0e4.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/terrain_3d/icons/texture_paint.svg" +dest_files=["res://.godot/imported/texture_paint.svg-72da4fd2096377e625a8fe09cdacb0e4.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=false diff --git a/addons/terrain_3d/icons/texture_spray.svg b/addons/terrain_3d/icons/texture_spray.svg new file mode 100644 index 0000000..bfc3e82 --- /dev/null +++ b/addons/terrain_3d/icons/texture_spray.svg @@ -0,0 +1,165 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/addons/terrain_3d/icons/texture_spray.svg.import b/addons/terrain_3d/icons/texture_spray.svg.import new file mode 100644 index 0000000..c4acc6f --- /dev/null +++ b/addons/terrain_3d/icons/texture_spray.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://16yfxe7xe703" +path="res://.godot/imported/texture_spray.svg-326fee11cf418653e621bc222a470861.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/terrain_3d/icons/texture_spray.svg" +dest_files=["res://.godot/imported/texture_spray.svg-326fee11cf418653e621bc222a470861.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=false diff --git a/addons/terrain_3d/icons/wetness.svg b/addons/terrain_3d/icons/wetness.svg new file mode 100644 index 0000000..90c3d03 --- /dev/null +++ b/addons/terrain_3d/icons/wetness.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/addons/terrain_3d/icons/wetness.svg.import b/addons/terrain_3d/icons/wetness.svg.import new file mode 100644 index 0000000..9379330 --- /dev/null +++ b/addons/terrain_3d/icons/wetness.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bcgg0srmqsh3n" +path="res://.godot/imported/wetness.svg-9b2ddec096ab7734492b77b20c75c82b.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/terrain_3d/icons/wetness.svg" +dest_files=["res://.godot/imported/wetness.svg-9b2ddec096ab7734492b77b20c75c82b.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=false diff --git a/addons/terrain_3d/menu/bake_lod_dialog.gd b/addons/terrain_3d/menu/bake_lod_dialog.gd new file mode 100644 index 0000000..ada4fb2 --- /dev/null +++ b/addons/terrain_3d/menu/bake_lod_dialog.gd @@ -0,0 +1,30 @@ +# Copyright © 2025 Cory Petkovsek, Roope Palmroos, and Contributors. +# Bake LOD Dialog for Terrain3D +@tool +extends ConfirmationDialog + +var lod: int = 0 +var description: String = "" + + +func _ready() -> void: + set_unparent_when_invisible(true) + about_to_popup.connect(_on_about_to_popup) + visibility_changed.connect(_on_visibility_changed) + %LodBox.value_changed.connect(_on_lod_box_value_changed) + + +func _on_about_to_popup() -> void: + lod = %LodBox.value + + +func _on_visibility_changed() -> void: + # Change text on the autowrap label only when the popup is visible. + # Works around Godot issue #47005: + # https://github.com/godotengine/godot/issues/47005 + if visible: + %DescriptionLabel.text = description + + +func _on_lod_box_value_changed(p_value: float) -> void: + lod = %LodBox.value diff --git a/addons/terrain_3d/menu/bake_lod_dialog.gd.uid b/addons/terrain_3d/menu/bake_lod_dialog.gd.uid new file mode 100644 index 0000000..08c194f --- /dev/null +++ b/addons/terrain_3d/menu/bake_lod_dialog.gd.uid @@ -0,0 +1 @@ +uid://cqmt8f5x5c2ad diff --git a/addons/terrain_3d/menu/bake_lod_dialog.tscn b/addons/terrain_3d/menu/bake_lod_dialog.tscn new file mode 100644 index 0000000..bad26f1 --- /dev/null +++ b/addons/terrain_3d/menu/bake_lod_dialog.tscn @@ -0,0 +1,43 @@ +[gd_scene load_steps=2 format=3 uid="uid://bhvrrmb8bk1bt"] + +[ext_resource type="Script" path="res://addons/terrain_3d/menu/bake_lod_dialog.gd" id="1_sf76d"] + +[node name="bake_lod_dialog" type="ConfirmationDialog"] +title = "Bake Terrain3D Mesh" +position = Vector2i(0, 36) +size = Vector2i(400, 155) +visible = true +script = ExtResource("1_sf76d") + +[node name="MarginContainer" type="MarginContainer" parent="."] +offset_left = 8.0 +offset_top = 8.0 +offset_right = 392.0 +offset_bottom = 106.0 +theme_override_constants/margin_left = 10 +theme_override_constants/margin_top = 10 +theme_override_constants/margin_right = 10 +theme_override_constants/margin_bottom = 10 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"] +layout_mode = 2 + +[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer"] +layout_mode = 2 +theme_override_constants/separation = 20 + +[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/HBoxContainer"] +layout_mode = 2 +text = "LOD:" + +[node name="LodBox" type="SpinBox" parent="MarginContainer/VBoxContainer/HBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +max_value = 8.0 +value = 4.0 + +[node name="DescriptionLabel" type="Label" parent="MarginContainer/VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +autowrap_mode = 2 diff --git a/addons/terrain_3d/menu/baker.gd b/addons/terrain_3d/menu/baker.gd new file mode 100644 index 0000000..08064b6 --- /dev/null +++ b/addons/terrain_3d/menu/baker.gd @@ -0,0 +1,398 @@ +# Copyright © 2025 Cory Petkovsek, Roope Palmroos, and Contributors. +# Baker for Terrain3D +extends Node + +const BakeLodDialog: PackedScene = preload("res://addons/terrain_3d/menu/bake_lod_dialog.tscn") +const BAKE_MESH_DESCRIPTION: String = "This will create a child MeshInstance3D. LOD4+ is recommended. LOD0 is slow and dense with vertices every 1 unit. It is not an optimal mesh." +const BAKE_OCCLUDER_DESCRIPTION: String = "This will create a child OccluderInstance3D. LOD4+ is recommended and will take 5+ seconds per region to generate. LOD0 is unnecessarily dense and slow." +const SET_UP_NAVIGATION_DESCRIPTION: String = "This operation will: + +- Create a NavigationRegion3D node, +- Assign it a blank NavigationMesh resource, +- Move the Terrain3D node to be a child of the new node, +- And bake the nav mesh. + +Once setup is complete, you can modify the settings on your nav mesh, and rebake +without having to run through the setup again. + +If preferred, this setup can be canceled and the steps performed manually. For +the best results, adjust the settings on the NavigationMesh resource to match +the settings of your navigation agents and collisions." + +var plugin: EditorPlugin +var bake_method: Callable +var bake_lod_dialog: ConfirmationDialog +var confirm_dialog: ConfirmationDialog + + +func _enter_tree() -> void: + bake_lod_dialog = BakeLodDialog.instantiate() + bake_lod_dialog.hide() + bake_lod_dialog.confirmed.connect(func(): bake_method.call()) + bake_lod_dialog.set_unparent_when_invisible(true) + + confirm_dialog = ConfirmationDialog.new() + confirm_dialog.hide() + confirm_dialog.confirmed.connect(func(): bake_method.call()) + confirm_dialog.set_unparent_when_invisible(true) + + +func _exit_tree() -> void: + bake_lod_dialog.queue_free() + confirm_dialog.queue_free() + + +func bake_mesh_popup() -> void: + if plugin.terrain: + bake_method = _bake_mesh + bake_lod_dialog.description = BAKE_MESH_DESCRIPTION + EditorInterface.popup_dialog_centered(bake_lod_dialog) + + +func _bake_mesh() -> void: + if plugin.terrain.data.get_region_count() == 0: + push_error("Terrain3D has no active regions to bake") + return + var mesh: Mesh = plugin.terrain.bake_mesh(bake_lod_dialog.lod, Terrain3DData.HEIGHT_FILTER_NEAREST) + if !mesh: + push_error("Failed to bake mesh from Terrain3D") + return + + var undo: EditorUndoRedoManager = plugin.get_undo_redo() + undo.create_action("Terrain3D Bake ArrayMesh") + + var mesh_instance := plugin.terrain.get_node_or_null(^"MeshInstance3D") as MeshInstance3D + if !mesh_instance: + mesh_instance = MeshInstance3D.new() + mesh_instance.name = &"MeshInstance3D" + mesh_instance.set_skeleton_path(NodePath()) + mesh_instance.mesh = mesh + + undo.add_do_method(plugin.terrain, &"add_child", mesh_instance, true) + undo.add_undo_method(plugin.terrain, &"remove_child", mesh_instance) + undo.add_do_property(mesh_instance, &"owner", EditorInterface.get_edited_scene_root()) + undo.add_do_reference(mesh_instance) + + else: + undo.add_do_property(mesh_instance, &"mesh", mesh) + undo.add_undo_property(mesh_instance, &"mesh", mesh_instance.mesh) + + if mesh_instance.mesh.resource_path: + var path := mesh_instance.mesh.resource_path + undo.add_do_method(mesh, &"take_over_path", path) + undo.add_undo_method(mesh_instance.mesh, &"take_over_path", path) + undo.add_do_method(ResourceSaver, &"save", mesh) + undo.add_undo_method(ResourceSaver, &"save", mesh_instance.mesh) + + undo.commit_action() + + +func bake_occluder_popup() -> void: + if plugin.terrain: + bake_method = _bake_occluder + bake_lod_dialog.description = BAKE_OCCLUDER_DESCRIPTION + EditorInterface.popup_dialog_centered(bake_lod_dialog) + + +func _bake_occluder() -> void: + if plugin.terrain.data.get_region_count() == 0: + push_error("Terrain3D has no active regions to bake") + return + var mesh: Mesh = plugin.terrain.bake_mesh(bake_lod_dialog.lod, Terrain3DData.HEIGHT_FILTER_MINIMUM) + if !mesh: + push_error("Failed to bake mesh from Terrain3D") + return + assert(mesh.get_surface_count() == 1) + + var undo: EditorUndoRedoManager = plugin.get_undo_redo() + undo.create_action("Terrain3D Bake Occluder3D") + + var occluder := ArrayOccluder3D.new() + var arrays: Array = mesh.surface_get_arrays(0) + assert(arrays.size() > Mesh.ARRAY_INDEX) + assert(arrays[Mesh.ARRAY_INDEX] != null) + occluder.set_arrays(arrays[Mesh.ARRAY_VERTEX], arrays[Mesh.ARRAY_INDEX]) + + var occluder_instance := plugin.terrain.get_node_or_null(^"OccluderInstance3D") as OccluderInstance3D + if !occluder_instance: + occluder_instance = OccluderInstance3D.new() + occluder_instance.name = &"OccluderInstance3D" + occluder_instance.occluder = occluder + + undo.add_do_method(plugin.terrain, &"add_child", occluder_instance, true) + undo.add_undo_method(plugin.terrain, &"remove_child", occluder_instance) + undo.add_do_property(occluder_instance, &"owner", EditorInterface.get_edited_scene_root()) + undo.add_do_reference(occluder_instance) + + else: + undo.add_do_property(occluder_instance, &"occluder", occluder) + undo.add_undo_property(occluder_instance, &"occluder", occluder_instance.occluder) + + if occluder_instance.occluder.resource_path: + var path := occluder_instance.occluder.resource_path + undo.add_do_method(occluder, &"take_over_path", path) + undo.add_undo_method(occluder_instance.occluder, &"take_over_path", path) + undo.add_do_method(ResourceSaver, &"save", occluder) + undo.add_undo_method(ResourceSaver, &"save", occluder_instance.occluder) + + undo.commit_action() + + +func find_nav_region_terrains(p_nav_region: NavigationRegion3D) -> Array[Terrain3D]: + var result: Array[Terrain3D] = [] + if not p_nav_region.navigation_mesh: + return result + + var source_mode: NavigationMesh.SourceGeometryMode + source_mode = p_nav_region.navigation_mesh.geometry_source_geometry_mode + if source_mode == NavigationMesh.SOURCE_GEOMETRY_ROOT_NODE_CHILDREN: + result.append_array(p_nav_region.find_children("", "Terrain3D", true, true)) + return result + + var group_nodes: Array = p_nav_region.get_tree().get_nodes_in_group(p_nav_region.navigation_mesh.geometry_source_group_name) + for node in group_nodes: + if node is Terrain3D: + result.push_back(node) + if source_mode == NavigationMesh.SOURCE_GEOMETRY_GROUPS_WITH_CHILDREN: + result.append_array(node.find_children("", "Terrain3D", true, true)) + + return result + + +func find_terrain_nav_regions(p_terrain: Terrain3D) -> Array[NavigationRegion3D]: + var result: Array[NavigationRegion3D] = [] + var root: Node = EditorInterface.get_edited_scene_root() + if not root: + return result + for nav_region in root.find_children("", "NavigationRegion3D", true, true): + if find_nav_region_terrains(nav_region).has(p_terrain): + result.push_back(nav_region) + return result + + +func bake_nav_mesh() -> void: + if plugin.nav_region: + # A NavigationRegion3D is selected. We only need to bake that one navmesh. + _bake_nav_region_nav_mesh(plugin.nav_region) + print("Terrain3DNavigation: Finished baking 1 NavigationMesh.") + + elif plugin.terrain: + if plugin.terrain.data.get_region_count() == 0: + push_error("Terrain3D has no active regions to bake") + return + # A Terrain3D is selected. There are potentially multiple navmeshes to bake and we need to + # find them all. (The multiple navmesh use-case is likely on very large scenes with lots of + # geometry. Each navmesh in this case would define its own, non-overlapping, baking AABB, to + # cut down on the amount of geometry to bake. In a large open-world RPG, for instance, there + # could be a navmesh for each town.) + var nav_regions: Array[NavigationRegion3D] = find_terrain_nav_regions(plugin.terrain) + for nav_region in nav_regions: + _bake_nav_region_nav_mesh(nav_region) + print("Terrain3DNavigation: Finished baking %d NavigationMesh(es)." % nav_regions.size()) + + +func _bake_nav_region_nav_mesh(p_nav_region: NavigationRegion3D) -> void: + var nav_mesh: NavigationMesh = p_nav_region.navigation_mesh + assert(nav_mesh != null) + + var source_geometry_data := NavigationMeshSourceGeometryData3D.new() + NavigationServer3D.parse_source_geometry_data(nav_mesh, source_geometry_data, p_nav_region) + + for terrain in find_nav_region_terrains(p_nav_region): + var aabb: AABB = nav_mesh.filter_baking_aabb + aabb.position += nav_mesh.filter_baking_aabb_offset + aabb = p_nav_region.global_transform * aabb + var faces: PackedVector3Array = terrain.generate_nav_mesh_source_geometry(aabb) + if not faces.is_empty(): + source_geometry_data.add_faces(faces, Transform3D.IDENTITY) + + NavigationServer3D.bake_from_source_geometry_data(nav_mesh, source_geometry_data) + + _postprocess_nav_mesh(nav_mesh) + + # Assign null first to force the debug display to actually update: + p_nav_region.set_navigation_mesh(null) + p_nav_region.set_navigation_mesh(nav_mesh) + + # Trigger save to disk if it is saved as an external file + if not nav_mesh.get_path().is_empty(): + ResourceSaver.save(nav_mesh, nav_mesh.get_path(), ResourceSaver.FLAG_COMPRESS) + + # Let other editor plugins and tool scripts know the nav mesh was just baked: + p_nav_region.bake_finished.emit() + + +func _postprocess_nav_mesh(p_nav_mesh: NavigationMesh) -> void: + # Post-process the nav mesh to work around Godot issue #85548 + + # Round all the vertices in the nav_mesh to the nearest cell_size/cell_height so that it doesn't + # contain any edges shorter than cell_size/cell_height (one cause of #85548). + var vertices: PackedVector3Array = _postprocess_nav_mesh_round_vertices(p_nav_mesh) + + # Rounding vertices can collapse some edges to 0 length. We remove these edges, and any polygons + # that have been reduced to 0 area. + var polygons: Array[PackedInt32Array] = _postprocess_nav_mesh_remove_empty_polygons(p_nav_mesh, vertices) + + # Another cause of #85548 is baking producing overlapping polygons. We remove these. + _postprocess_nav_mesh_remove_overlapping_polygons(p_nav_mesh, vertices, polygons) + + p_nav_mesh.clear_polygons() + p_nav_mesh.set_vertices(vertices) + for polygon in polygons: + p_nav_mesh.add_polygon(polygon) + + +func _postprocess_nav_mesh_round_vertices(p_nav_mesh: NavigationMesh) -> PackedVector3Array: + assert(p_nav_mesh != null) + assert(p_nav_mesh.cell_size > 0.0) + assert(p_nav_mesh.cell_height > 0.0) + + var cell_size: Vector3 = Vector3(p_nav_mesh.cell_size, p_nav_mesh.cell_height, p_nav_mesh.cell_size) + + # Round a little harder to avoid rounding errors with non-power-of-two cell_size/cell_height + # causing the navigation map to put two non-matching edges in the same cell: + var round_factor := cell_size * 1.001 + + var vertices: PackedVector3Array = p_nav_mesh.get_vertices() + for i in range(vertices.size()): + vertices[i] = (vertices[i] / round_factor).floor() * round_factor + return vertices + + +func _postprocess_nav_mesh_remove_empty_polygons(p_nav_mesh: NavigationMesh, p_vertices: PackedVector3Array) -> Array[PackedInt32Array]: + var polygons: Array[PackedInt32Array] = [] + + for i in range(p_nav_mesh.get_polygon_count()): + var old_polygon: PackedInt32Array = p_nav_mesh.get_polygon(i) + var new_polygon: PackedInt32Array = [] + + # Remove duplicate vertices (introduced by rounding) from the polygon: + var polygon_vertices: PackedVector3Array = [] + for index in old_polygon: + var vertex: Vector3 = p_vertices[index] + if polygon_vertices.has(vertex): + continue + polygon_vertices.push_back(vertex) + new_polygon.push_back(index) + + # If we removed some vertices, we might be able to remove the polygon too: + if new_polygon.size() <= 2: + continue + polygons.push_back(new_polygon) + + return polygons + + +func _postprocess_nav_mesh_remove_overlapping_polygons(p_nav_mesh: NavigationMesh, p_vertices: PackedVector3Array, p_polygons: Array[PackedInt32Array]) -> void: + # Occasionally, a baked nav mesh comes out with overlapping polygons: + # https://github.com/godotengine/godot/issues/85548#issuecomment-1839341071 + # Until the bug is fixed in the engine, this function attempts to detect and remove overlapping + # polygons. + + # This function has to make a choice of which polygon to remove when an overlap is detected, + # because in this case the nav mesh is ambiguous. To do this it uses a heuristic: + # (1) an 'overlap' is defined as an edge that is shared by 3 or more polygons. + # (2) a 'bad polygon' is defined as a polygon that contains 2 or more 'overlaps'. + # The function removes the 'bad polygons', which in practice seems to be enough to remove all + # overlaps without creating holes in the nav mesh. + + var cell_size: Vector3 = Vector3(p_nav_mesh.cell_size, p_nav_mesh.cell_height, p_nav_mesh.cell_size) + + # `edges` is going to map edges (vertex pairs) to arrays of polygons that contain that edge. + var edges: Dictionary = {} + + for polygon_index in range(p_polygons.size()): + var polygon: PackedInt32Array = p_polygons[polygon_index] + for j in range(polygon.size()): + var vertex: Vector3 = p_vertices[polygon[j]] + var next_vertex: Vector3 = p_vertices[polygon[(j + 1) % polygon.size()]] + + # edge_key is a key we can use in the edges dictionary that uniquely identifies the + # edge. We use cell coordinates here (Vector3i) because with a non-power-of-two + # cell_size, rounding errors can cause Vector3 vertices to not be equal. + # Array.sort IS defined for vector types - see the Godot docs. It's necessary here + # because polygons that share an edge can have their vertices in a different order. + var edge_key: Array = [Vector3i(vertex / cell_size), Vector3i(next_vertex / cell_size)] + edge_key.sort() + + if !edges.has(edge_key): + edges[edge_key] = [] + edges[edge_key].push_back(polygon_index) + + var overlap_count: Dictionary = {} + for connections in edges.values(): + if connections.size() <= 2: + continue + for polygon_index in connections: + overlap_count[polygon_index] = overlap_count.get(polygon_index, 0) + 1 + + var bad_polygons: Array = [] + for polygon_index in overlap_count.keys(): + if overlap_count[polygon_index] >= 2: + bad_polygons.push_back(polygon_index) + + bad_polygons.sort() + for i in range(bad_polygons.size() - 1, -1, -1): + p_polygons.remove_at(bad_polygons[i]) + + +func set_up_navigation_popup() -> void: + if plugin.terrain: + bake_method = _set_up_navigation + confirm_dialog.dialog_text = SET_UP_NAVIGATION_DESCRIPTION + EditorInterface.popup_dialog_centered(confirm_dialog) + + +func _set_up_navigation() -> void: + assert(plugin.terrain) + if plugin.terrain == EditorInterface.get_edited_scene_root(): + push_error("Terrain3D Navigation setup not possible if Terrain3D node is scene root") + return + if plugin.terrain.data.get_region_count() == 0: + push_error("Terrain3D has no active regions") + return + var terrain: Terrain3D = plugin.terrain + + var nav_region := NavigationRegion3D.new() + nav_region.name = &"NavigationRegion3D" + nav_region.navigation_mesh = NavigationMesh.new() + + var undo_redo: EditorUndoRedoManager = plugin.get_undo_redo() + + undo_redo.create_action("Terrain3D Set up Navigation") + undo_redo.add_do_method(self, &"_do_set_up_navigation", nav_region, terrain) + undo_redo.add_undo_method(self, &"_undo_set_up_navigation", nav_region, terrain) + undo_redo.add_do_reference(nav_region) + undo_redo.commit_action() + + EditorInterface.inspect_object(nav_region) + assert(plugin.nav_region == nav_region) + + bake_nav_mesh() + + +func _do_set_up_navigation(p_nav_region: NavigationRegion3D, p_terrain: Terrain3D) -> void: + var parent: Node = p_terrain.get_parent() + var index: int = p_terrain.get_index() + var t_owner: Node = p_terrain.owner + + parent.add_child(p_nav_region, true) + p_terrain.reparent(p_nav_region) + parent.move_child(p_nav_region, index) + + p_nav_region.owner = t_owner + p_terrain.owner = t_owner + + +func _undo_set_up_navigation(p_nav_region: NavigationRegion3D, p_terrain: Terrain3D) -> void: + assert(p_terrain.get_parent() == p_nav_region) + + var parent: Node = p_nav_region.get_parent() + var index: int = p_nav_region.get_index() + var t_owner: Node = p_nav_region.get_owner() + + p_terrain.reparent(parent) + parent.remove_child(p_nav_region) + parent.move_child(p_terrain, index) + + p_terrain.owner = t_owner diff --git a/addons/terrain_3d/menu/baker.gd.uid b/addons/terrain_3d/menu/baker.gd.uid new file mode 100644 index 0000000..0f2ed94 --- /dev/null +++ b/addons/terrain_3d/menu/baker.gd.uid @@ -0,0 +1 @@ +uid://4ulaeevj5jvi diff --git a/addons/terrain_3d/menu/channel_packer.gd b/addons/terrain_3d/menu/channel_packer.gd new file mode 100644 index 0000000..f2699d7 --- /dev/null +++ b/addons/terrain_3d/menu/channel_packer.gd @@ -0,0 +1,463 @@ +# Copyright © 2025 Cory Petkovsek, Roope Palmroos, and Contributors. +# Channel Packer for Terrain3D +extends RefCounted + +const WINDOW_SCENE: String = "res://addons/terrain_3d/menu/channel_packer.tscn" +const TEMPLATE_PATH: String = "res://addons/terrain_3d/menu/channel_packer_import_template.txt" +const DRAG_DROP_SCRIPT: String = "res://addons/terrain_3d/menu/channel_packer_dragdrop.gd" +enum { + INFO, + WARN, + ERROR, +} + +enum { + IMAGE_ALBEDO, + IMAGE_HEIGHT, + IMAGE_NORMAL, + IMAGE_ROUGHNESS +} + +var plugin: EditorPlugin +var window: Window +var save_file_dialog: EditorFileDialog +var open_file_dialog: EditorFileDialog +var invert_green_checkbox: CheckBox +var invert_smooth_checkbox: CheckBox +var invert_height_checkbox: CheckBox +var lumin_height_button: Button +var generate_mipmaps_checkbox: CheckBox +var high_quality_checkbox: CheckBox +var align_normals_checkbox: CheckBox +var resize_toggle_checkbox: CheckBox +var resize_option_box: SpinBox +var height_channel: Array[Button] +var height_channel_selected: int = 0 +var roughness_channel: Array[Button] +var roughness_channel_selected: int = 0 +var last_opened_directory: String +var last_saved_directory: String +var packing_albedo: bool = false +var queue_pack_normal_roughness: bool = false +var images: Array[Image] = [null, null, null, null] +var status_label: Label +var no_op: Callable = func(): pass +var last_file_selected_fn: Callable = no_op +var normal_vector: Vector3 + + +func pack_textures_popup() -> void: + if window != null: + window.show() + window.grab_focus() + window.move_to_center() + return + window = (load(WINDOW_SCENE) as PackedScene).instantiate() + window.close_requested.connect(_on_close_requested) + window.window_input.connect(func(event:InputEvent): + if event is InputEventKey: + if event.pressed and event.keycode == KEY_ESCAPE: + _on_close_requested() + ) + window.find_child("CloseButton").pressed.connect(_on_close_requested) + + status_label = window.find_child("StatusLabel") as Label + invert_green_checkbox = window.find_child("InvertGreenChannelCheckBox") as CheckBox + invert_smooth_checkbox = window.find_child("InvertSmoothCheckBox") as CheckBox + invert_height_checkbox = window.find_child("ConvertDepthToHeight") as CheckBox + lumin_height_button = window.find_child("LuminanceAsHeightButton") as Button + generate_mipmaps_checkbox = window.find_child("GenerateMipmapsCheckBox") as CheckBox + high_quality_checkbox = window.find_child("HighQualityCheckBox") as CheckBox + align_normals_checkbox = window.find_child("AlignNormalsCheckBox") as CheckBox + resize_toggle_checkbox = window.find_child("ResizeToggle") as CheckBox + resize_option_box = window.find_child("ResizeOptionButton") as SpinBox + height_channel = [ + window.find_child("HeightChannelR") as Button, + window.find_child("HeightChannelG") as Button, + window.find_child("HeightChannelB") as Button, + window.find_child("HeightChannelA") as Button + ] + roughness_channel = [ + window.find_child("RoughnessChannelR") as Button, + window.find_child("RoughnessChannelG") as Button, + window.find_child("RoughnessChannelB") as Button, + window.find_child("RoughnessChannelA") as Button + ] + + height_channel[0].pressed.connect(func() -> void: height_channel_selected = 0) + height_channel[1].pressed.connect(func() -> void: height_channel_selected = 1) + height_channel[2].pressed.connect(func() -> void: height_channel_selected = 2) + height_channel[3].pressed.connect(func() -> void: height_channel_selected = 3) + + roughness_channel[0].pressed.connect(func() -> void: roughness_channel_selected = 0) + roughness_channel[1].pressed.connect(func() -> void: roughness_channel_selected = 1) + roughness_channel[2].pressed.connect(func() -> void: roughness_channel_selected = 2) + roughness_channel[3].pressed.connect(func() -> void: roughness_channel_selected = 3) + + plugin.add_child(window) + _init_file_dialogs() + + # the dialog disables the parent window "on top" so, restore it after 1 frame to alow the dialog to clear. + var set_on_top_fn: Callable = func(_file: String = "") -> void: + await RenderingServer.frame_post_draw + window.always_on_top = true + save_file_dialog.file_selected.connect(set_on_top_fn) + save_file_dialog.canceled.connect(set_on_top_fn) + open_file_dialog.file_selected.connect(set_on_top_fn) + open_file_dialog.canceled.connect(set_on_top_fn) + + _init_texture_picker(window.find_child("AlbedoVBox"), IMAGE_ALBEDO) + _init_texture_picker(window.find_child("HeightVBox"), IMAGE_HEIGHT) + _init_texture_picker(window.find_child("NormalVBox"), IMAGE_NORMAL) + _init_texture_picker(window.find_child("RoughnessVBox"), IMAGE_ROUGHNESS) + + (window.find_child("PackButton") as Button).pressed.connect(_on_pack_button_pressed) + + +func _on_close_requested() -> void: + last_file_selected_fn = no_op + images = [null, null, null, null] + window.queue_free() + window = null + + +func _init_file_dialogs() -> void: + save_file_dialog = EditorFileDialog.new() + save_file_dialog.set_filters(PackedStringArray(["*.png"])) + save_file_dialog.set_file_mode(EditorFileDialog.FILE_MODE_SAVE_FILE) + save_file_dialog.access = EditorFileDialog.ACCESS_FILESYSTEM + save_file_dialog.file_selected.connect(_on_save_file_selected) + save_file_dialog.ok_button_text = "Save" + save_file_dialog.size = Vector2i(550, 550) + #save_file_dialog.transient = false + #save_file_dialog.exclusive = false + #save_file_dialog.popup_window = true + + open_file_dialog = EditorFileDialog.new() + open_file_dialog.set_filters(PackedStringArray( + ["*.png", "*.bmp", "*.exr", "*.hdr", "*.jpg", "*.jpeg", "*.tga", "*.svg", "*.webp", "*.ktx", "*.dds"])) + open_file_dialog.set_file_mode(EditorFileDialog.FILE_MODE_OPEN_FILE) + open_file_dialog.access = EditorFileDialog.ACCESS_FILESYSTEM + open_file_dialog.ok_button_text = "Open" + open_file_dialog.size = Vector2i(550, 550) + #open_file_dialog.transient = false + #open_file_dialog.exclusive = false + #open_file_dialog.popup_window = true + + window.add_child(save_file_dialog) + window.add_child(open_file_dialog) + + +func _init_texture_picker(p_parent: Node, p_image_index: int) -> void: + var line_edit: LineEdit = p_parent.find_child("LineEdit") as LineEdit + var file_pick_button: Button = p_parent.find_child("PickButton") as Button + var clear_button: Button = p_parent.find_child("ClearButton") as Button + var texture_rect: TextureRect = p_parent.find_child("TextureRect") as TextureRect + var texture_button: Button = p_parent.find_child("TextureButton") as Button + texture_button.set_script(load(DRAG_DROP_SCRIPT) as GDScript) + + var set_channel_fn: Callable = func(used_channels: int) -> void: + var channel_count: int = 4 + # enum Image.UsedChannels + match used_channels: + Image.USED_CHANNELS_L, Image.USED_CHANNELS_R: channel_count = 1 + Image.USED_CHANNELS_LA, Image.USED_CHANNELS_RG: channel_count = 2 + Image.USED_CHANNELS_RGB: channel_count = 3 + Image.USED_CHANNELS_RGBA: channel_count = 4 + if p_image_index == IMAGE_HEIGHT: + for i in 4: + height_channel[i].visible = i < channel_count + height_channel[0].button_pressed = true + height_channel[0].pressed.emit() + elif p_image_index == IMAGE_ROUGHNESS: + for i in 4: + roughness_channel[i].visible = i < channel_count + roughness_channel[0].button_pressed = true + roughness_channel[0].pressed.emit() + + var load_image_fn: Callable = func(path: String): + var image: Image = Image.new() + var error: int = OK + # Special case for dds files + if path.get_extension() == "dds": + image = ResourceLoader.load(path).get_image() + if not image.is_empty(): + # if the dds file is loaded, we must clear any mipmaps and + # decompress if needed in order to do per pixel operations. + image.clear_mipmaps() + image.decompress() + else: + error = FAILED + else: + error = image.load(path) + if error != OK: + _show_message(ERROR, "Failed to load texture '" + path + "'") + texture_rect.texture = null + images[p_image_index] = null + else: + _show_message(INFO, "Loaded texture '" + path + "'") + texture_rect.texture = ImageTexture.create_from_image(image) + images[p_image_index] = image + _set_wh_labels(p_image_index, image.get_width(), image.get_height()) + if p_image_index == IMAGE_NORMAL: + _set_normal_vector(image) + if p_image_index == IMAGE_HEIGHT or p_image_index == IMAGE_ROUGHNESS: + set_channel_fn.call(image.detect_used_channels()) + + var os_drop_fn: Callable = func(files: PackedStringArray) -> void: + # OS drag drop holds mouse focus until released, + # Get mouse pos and check directly if inside texture_rect + var rect = texture_button.get_global_rect() + var mouse_position = texture_button.get_global_mouse_position() + if rect.has_point(mouse_position): + if files.size() != 1: + _show_message(ERROR, "Cannot load multiple files") + else: + line_edit.text = files[0] + load_image_fn.call(files[0]) + + var godot_drop_fn: Callable = func(path: String) -> void: + path = ProjectSettings.globalize_path(path) + line_edit.text = path + load_image_fn.call(path) + + var open_fn: Callable = func() -> void: + open_file_dialog.current_path = last_opened_directory + if last_file_selected_fn != no_op: + open_file_dialog.file_selected.disconnect(last_file_selected_fn) + last_file_selected_fn = func(path: String) -> void: + line_edit.text = path + load_image_fn.call(path) + open_file_dialog.file_selected.connect(last_file_selected_fn) + open_file_dialog.popup_centered_ratio() + + var line_edit_submit_fn: Callable = func(path: String) -> void: + line_edit.text = path + load_image_fn.call(path) + + var clear_fn: Callable = func() -> void: + line_edit.text = "" + texture_rect.texture = null + images[p_image_index] = null + _set_wh_labels(p_image_index, -1, -1) + + line_edit.text_submitted.connect(line_edit_submit_fn) + file_pick_button.pressed.connect(open_fn) + texture_button.pressed.connect(open_fn) + clear_button.pressed.connect(clear_fn) + texture_button.dropped.connect(godot_drop_fn) + window.files_dropped.connect(os_drop_fn) + + if p_image_index == IMAGE_HEIGHT: + var lumin_fn: Callable = func() -> void: + if !images[IMAGE_ALBEDO]: + _show_message(ERROR, "Albedo Image Required for Operation") + else: + line_edit.text = "Generated Height" + var height_texture: Image = Terrain3DUtil.luminance_to_height(images[IMAGE_ALBEDO]) + if height_texture.is_empty(): + _show_message(ERROR, "Height Texture Generation error") + # blur the image by resizing down and back.. + var w: int = height_texture.get_width() + var h: int = height_texture.get_height() + height_texture.resize(w / 4, h / 4) + height_texture.resize(w, h, Image.INTERPOLATE_CUBIC) + # "Load" the height texture + images[IMAGE_HEIGHT] = height_texture + texture_rect.texture = ImageTexture.create_from_image(images[IMAGE_HEIGHT]) + _set_wh_labels(IMAGE_HEIGHT, height_texture.get_width(), height_texture.get_height()) + set_channel_fn.call(Image.USED_CHANNELS_R) + _show_message(INFO, "Height Texture generated sucsessfully") + lumin_height_button.pressed.connect(lumin_fn) + plugin.ui.set_button_editor_icon(file_pick_button, "Folder") + plugin.ui.set_button_editor_icon(clear_button, "Remove") + + +func _set_wh_labels(p_image_index: int, width: int, height: int) -> void: + var w: String = "" + var h: String = "" + if width > 0 and height > 0: + w = "w: " + str(width) + h = "h: " + str(height) + match p_image_index: + 0: + window.find_child("AlbedoW").text = w + window.find_child("AlbedoH").text = h + 1: + window.find_child("HeightW").text = w + window.find_child("HeightH").text = h + 2: + window.find_child("NormalW").text = w + window.find_child("NormalH").text = h + 3: + window.find_child("RoughnessW").text = w + window.find_child("RoughnessH").text = h + + +func _show_message(p_level: int, p_text: String) -> void: + status_label.text = p_text + match p_level: + INFO: + print("Terrain3DChannelPacker: " + p_text) + status_label.add_theme_color_override("font_color", Color(0, 0.82, 0.14)) + WARN: + push_warning("Terrain3DChannelPacker: " + p_text) + status_label.add_theme_color_override("font_color", Color(0.9, 0.9, 0)) + ERROR,_: + push_error("Terrain3DChannelPacker: " + p_text) + status_label.add_theme_color_override("font_color", Color(0.9, 0, 0)) + + +func _create_import_file(png_path: String) -> void: + var dst_import_path: String = png_path + ".import" + var file: FileAccess = FileAccess.open(TEMPLATE_PATH, FileAccess.READ) + var template_content: String = file.get_as_text() + file.close() + template_content = template_content.replace( + "$SOURCE_FILE", png_path).replace( + "$HIGH_QUALITY", str(high_quality_checkbox.button_pressed)).replace( + "$GENERATE_MIPMAPS", str(generate_mipmaps_checkbox.button_pressed) + ) + var import_content: String = template_content + file = FileAccess.open(dst_import_path, FileAccess.WRITE) + file.store_string(import_content) + file.close() + + +func _on_pack_button_pressed() -> void: + packing_albedo = images[IMAGE_ALBEDO] != null and images[IMAGE_HEIGHT] != null + var packing_normal_roughness: bool = images[IMAGE_NORMAL] != null and images[IMAGE_ROUGHNESS] != null + + if not packing_albedo and not packing_normal_roughness: + _show_message(WARN, "Please select an albedo and height texture or a normal and roughness texture") + return + if packing_albedo: + save_file_dialog.current_path = last_saved_directory + "packed_albedo_height" + save_file_dialog.title = "Save Packed Albedo/Height Texture" + save_file_dialog.popup_centered_ratio() + if packing_normal_roughness: + queue_pack_normal_roughness = true + return + if packing_normal_roughness: + save_file_dialog.current_path = last_saved_directory + "packed_normal_roughness" + save_file_dialog.title = "Save Packed Normal/Roughness Texture" + save_file_dialog.popup_centered_ratio() + + +func _on_save_file_selected(p_dst_path) -> void: + last_saved_directory = p_dst_path.get_base_dir() + "/" + var error: int + if packing_albedo: + error = _pack_textures(images[IMAGE_ALBEDO], images[IMAGE_HEIGHT], p_dst_path, false, + invert_height_checkbox.button_pressed, false, height_channel_selected) + else: + error = _pack_textures(images[IMAGE_NORMAL], images[IMAGE_ROUGHNESS], p_dst_path, + invert_green_checkbox.button_pressed, invert_smooth_checkbox.button_pressed, + align_normals_checkbox.button_pressed, roughness_channel_selected) + + if error == OK: + EditorInterface.get_resource_filesystem().scan() + if window.visible: + window.hide() + await EditorInterface.get_resource_filesystem().resources_reimported + # wait 1 extra frame, to ensure the UI is responsive. + await RenderingServer.frame_post_draw + window.show() + + if queue_pack_normal_roughness: + queue_pack_normal_roughness = false + packing_albedo = false + save_file_dialog.current_path = last_saved_directory + "packed_normal_roughness" + save_file_dialog.title = "Save Packed Normal/Roughness Texture" + + save_file_dialog.call_deferred("popup_centered_ratio") + save_file_dialog.call_deferred("grab_focus") + + +func _alignment_basis(normal: Vector3) -> Basis: + var up: Vector3 = Vector3(0, 0, 1) + var v: Vector3 = normal.cross(up) + var c: float = normal.dot(up) + var k: float = 1.0 / (1.0 + c) + + var vxy: float = v.x * v.y * k + var vxz: float = v.x * v.z * k + var vyz: float = v.y * v.z * k + + return Basis(Vector3(v.x * v.x * k + c, vxy - v.z, vxz + v.y), + Vector3(vxy + v.z, v.y * v.y * k + c, vyz - v.x), + Vector3(vxz - v.y, vyz + v.x, v.z * v.z * k + c) + ) + + +func _set_normal_vector(source: Image, quiet: bool = false) -> void: + # Calculate texture normal sum direction + var normal: Image = source + var sum: Color = Color(0.0, 0.0, 0.0, 0.0) + for x in normal.get_width(): + for y in normal.get_height(): + sum += normal.get_pixel(x, y) + var div: float = normal.get_height() * normal.get_width() + sum /= Color(div, div, div) + sum *= 2.0 + sum -= Color(1.0, 1.0, 1.0) + normal_vector = Vector3(sum.r, sum.g, sum.b).normalized() + if normal_vector.dot(Vector3(0.0, 0.0, 1.0)) < 0.999 && !quiet: + _show_message(WARN, "Normal Texture Not Orthoganol to UV plane.\nFor Compatability with Detiling and Rotation, Select Orthoganolize Normals") + + +func _align_normals(source: Image, iteration: int = 0) -> void: + # generate matrix to re-align the normalmap + var mat3: Basis = _alignment_basis(normal_vector) + # re-align the normal map pixels + for x in source.get_width(): + for y in source.get_height(): + var old_pixel: Color = source.get_pixel(x, y) + var vector_pixel: Vector3 = Vector3(old_pixel.r, old_pixel.g, old_pixel.b) + vector_pixel *= 2.0 + vector_pixel -= Vector3.ONE + vector_pixel = vector_pixel.normalized() + vector_pixel = vector_pixel * mat3 + vector_pixel += Vector3.ONE + vector_pixel *= 0.5 + var new_pixel: Color = Color(vector_pixel.x, vector_pixel.y, vector_pixel.z, old_pixel.a) + source.set_pixel(x, y, new_pixel) + _set_normal_vector(source, true) + if normal_vector.dot(Vector3(0.0, 0.0, 1.0)) < 0.999 && iteration < 3: + ++iteration + _align_normals(source, iteration) + + +func _pack_textures(p_rgb_image: Image, p_a_image: Image, p_dst_path: String, p_invert_green: bool, + p_invert_smooth: bool, p_align_normals : bool, p_alpha_channel: int) -> Error: + if p_rgb_image and p_a_image: + if p_rgb_image.get_size() != p_a_image.get_size() and !resize_toggle_checkbox.button_pressed: + _show_message(ERROR, "Textures must be the same size.\nEnable resize to override image dimensions") + return FAILED + + if resize_toggle_checkbox.button_pressed: + var size: int = max(128, resize_option_box.value) + p_rgb_image.resize(size, size, Image.INTERPOLATE_CUBIC) + p_a_image.resize(size, size, Image.INTERPOLATE_CUBIC) + + if p_align_normals and normal_vector.dot(Vector3(0.0, 0.0, 1.0)) < 0.999: + _align_normals(p_rgb_image) + elif p_align_normals: + _show_message(INFO, "Alignment OK, skipping Normal Orthogonalization") + + var output_image: Image = Terrain3DUtil.pack_image(p_rgb_image, p_a_image, + p_invert_green, p_invert_smooth, p_alpha_channel) + + if not output_image: + _show_message(ERROR, "Failed to pack textures") + return FAILED + if output_image.detect_alpha() != Image.ALPHA_BLEND: + _show_message(WARN, "Warning, Alpha channel empty") + + output_image.save_png(p_dst_path) + _create_import_file(p_dst_path) + _show_message(INFO, "Packed to " + p_dst_path + ".") + return OK + else: + _show_message(ERROR, "Failed to load one or more textures") + return FAILED diff --git a/addons/terrain_3d/menu/channel_packer.gd.uid b/addons/terrain_3d/menu/channel_packer.gd.uid new file mode 100644 index 0000000..564c78d --- /dev/null +++ b/addons/terrain_3d/menu/channel_packer.gd.uid @@ -0,0 +1 @@ +uid://bwldx4itd58o7 diff --git a/addons/terrain_3d/menu/channel_packer.tscn b/addons/terrain_3d/menu/channel_packer.tscn new file mode 100644 index 0000000..11de99a --- /dev/null +++ b/addons/terrain_3d/menu/channel_packer.tscn @@ -0,0 +1,530 @@ +[gd_scene load_steps=7 format=3 uid="uid://nud6dwjcnj5v"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ysabf"] +bg_color = Color(0.211765, 0.239216, 0.290196, 1) + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_lcvna"] +bg_color = Color(0.168627, 0.211765, 0.266667, 1) +border_width_left = 3 +border_width_top = 3 +border_width_right = 3 +border_width_bottom = 3 +border_color = Color(0.270588, 0.435294, 0.580392, 1) +corner_radius_top_left = 5 +corner_radius_top_right = 5 +corner_radius_bottom_right = 5 +corner_radius_bottom_left = 5 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_cb0xf"] +bg_color = Color(0.137255, 0.137255, 0.137255, 1) +draw_center = false +border_width_left = 3 +border_width_top = 3 +border_width_right = 3 +border_width_bottom = 3 +border_color = Color(0.784314, 0.784314, 0.784314, 1) +corner_radius_top_left = 5 +corner_radius_top_right = 5 +corner_radius_bottom_right = 5 +corner_radius_bottom_left = 5 + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_7qdas"] + +[sub_resource type="ButtonGroup" id="ButtonGroup_wnxik"] + +[sub_resource type="ButtonGroup" id="ButtonGroup_bs6ki"] + +[node name="Window" type="Window"] +title = "Terrain3D Channel Packer" +initial_position = 1 +size = Vector2i(583, 856) +wrap_controls = true +always_on_top = true + +[node name="PanelContainer" type="PanelContainer" parent="."] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +theme_override_styles/panel = SubResource("StyleBoxFlat_ysabf") + +[node name="MarginContainer" type="MarginContainer" parent="PanelContainer"] +layout_mode = 2 +theme_override_constants/margin_left = 5 +theme_override_constants/margin_top = 5 +theme_override_constants/margin_right = 5 +theme_override_constants/margin_bottom = 5 + +[node name="VBoxContainer" type="VBoxContainer" parent="PanelContainer/MarginContainer"] +layout_mode = 2 +theme_override_constants/separation = 10 + +[node name="AlbedoHeightPanel" type="PanelContainer" parent="PanelContainer/MarginContainer/VBoxContainer"] +layout_mode = 2 +theme_override_styles/panel = SubResource("StyleBoxFlat_lcvna") + +[node name="MarginContainer" type="MarginContainer" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel"] +layout_mode = 2 +theme_override_constants/margin_left = 10 +theme_override_constants/margin_top = 10 +theme_override_constants/margin_right = 10 +theme_override_constants/margin_bottom = 10 + +[node name="HBoxContainer" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer"] +layout_mode = 2 + +[node name="AlbedoVBox" type="VBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="AlbedoLabel" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox"] +layout_mode = 2 +text = "Albedo texture" + +[node name="AlbedoHBox" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox"] +layout_mode = 2 + +[node name="LineEdit" type="LineEdit" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox/AlbedoHBox"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="PickButton" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox/AlbedoHBox"] +layout_mode = 2 + +[node name="ClearButton" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox/AlbedoHBox"] +layout_mode = 2 + +[node name="MarginContainer" type="MarginContainer" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox"] +layout_mode = 2 +size_flags_vertical = 4 +theme_override_constants/margin_top = 10 + +[node name="Panel" type="Panel" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox/MarginContainer"] +custom_minimum_size = Vector2(110, 110) +layout_mode = 2 +size_flags_horizontal = 4 +size_flags_vertical = 4 +theme_override_styles/panel = SubResource("StyleBoxFlat_cb0xf") + +[node name="TextureRect" type="TextureRect" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox/MarginContainer/Panel"] +layout_mode = 1 +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = -50.0 +offset_top = -50.0 +offset_right = 50.0 +offset_bottom = 50.0 +grow_horizontal = 2 +grow_vertical = 2 +expand_mode = 1 + +[node name="TextureButton" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox/MarginContainer/Panel"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +theme_override_styles/normal = SubResource("StyleBoxEmpty_7qdas") + +[node name="AlbedoWHHBox" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox"] +layout_mode = 2 +alignment = 1 + +[node name="AlbedoW" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox/AlbedoWHHBox"] +layout_mode = 2 +horizontal_alignment = 1 + +[node name="AlbedoH" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox/AlbedoWHHBox"] +layout_mode = 2 +horizontal_alignment = 1 + +[node name="HBoxContainer2" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox"] +layout_mode = 2 +alignment = 1 + +[node name="LuminanceAsHeightButton" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox/HBoxContainer2"] +layout_mode = 2 +text = " Generate Height from Luminance" +icon_alignment = 2 + +[node name="HeightVBox" type="VBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="HeightLabel" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox"] +layout_mode = 2 +text = "Height texture" + +[node name="HeightHBox" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox"] +layout_mode = 2 + +[node name="LineEdit" type="LineEdit" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/HeightHBox"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="PickButton" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/HeightHBox"] +layout_mode = 2 + +[node name="ClearButton" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/HeightHBox"] +layout_mode = 2 + +[node name="MarginContainer" type="MarginContainer" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox"] +layout_mode = 2 +size_flags_vertical = 4 +theme_override_constants/margin_top = 10 + +[node name="Panel" type="Panel" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/MarginContainer"] +custom_minimum_size = Vector2(110, 110) +layout_mode = 2 +size_flags_horizontal = 4 +size_flags_vertical = 4 +theme_override_styles/panel = SubResource("StyleBoxFlat_cb0xf") + +[node name="TextureRect" type="TextureRect" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/MarginContainer/Panel"] +layout_mode = 1 +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = -50.0 +offset_top = -50.0 +offset_right = 50.0 +offset_bottom = 50.0 +grow_horizontal = 2 +grow_vertical = 2 +expand_mode = 1 + +[node name="TextureButton" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/MarginContainer/Panel"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +theme_override_styles/normal = SubResource("StyleBoxEmpty_7qdas") + +[node name="HeightWHHBox" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox"] +layout_mode = 2 +alignment = 1 + +[node name="HeightW" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/HeightWHHBox"] +layout_mode = 2 +horizontal_alignment = 1 + +[node name="HeightH" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/HeightWHHBox"] +layout_mode = 2 +horizontal_alignment = 1 + +[node name="HBoxContainer2" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox"] +layout_mode = 2 +alignment = 1 + +[node name="ConvertDepthToHeight" type="CheckBox" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/HBoxContainer2"] +layout_mode = 2 +text = " Convert Depth to Height" +icon_alignment = 2 + +[node name="HBoxContainer" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox"] +layout_mode = 2 +alignment = 1 + +[node name="HeightChannelLabel" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/HBoxContainer"] +layout_mode = 2 +text = " Source Channel: " +horizontal_alignment = 2 + +[node name="HeightChannelR" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/HBoxContainer"] +layout_mode = 2 +toggle_mode = true +button_pressed = true +button_group = SubResource("ButtonGroup_wnxik") +text = "R" + +[node name="HeightChannelB" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/HBoxContainer"] +layout_mode = 2 +toggle_mode = true +button_group = SubResource("ButtonGroup_wnxik") +text = "G" + +[node name="HeightChannelG" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/HBoxContainer"] +layout_mode = 2 +toggle_mode = true +button_group = SubResource("ButtonGroup_wnxik") +text = "B" + +[node name="HeightChannelA" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/HBoxContainer"] +layout_mode = 2 +toggle_mode = true +button_group = SubResource("ButtonGroup_wnxik") +text = "A" + +[node name="NormalRoughnessPanel" type="PanelContainer" parent="PanelContainer/MarginContainer/VBoxContainer"] +layout_mode = 2 +theme_override_styles/panel = SubResource("StyleBoxFlat_lcvna") + +[node name="MarginContainer" type="MarginContainer" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel"] +layout_mode = 2 +theme_override_constants/margin_left = 10 +theme_override_constants/margin_top = 10 +theme_override_constants/margin_right = 10 +theme_override_constants/margin_bottom = 10 + +[node name="HBoxContainer" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer"] +layout_mode = 2 + +[node name="NormalVBox" type="VBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="NormalLabel" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox"] +layout_mode = 2 +text = "Normal texture" + +[node name="NormalHBox" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox"] +layout_mode = 2 + +[node name="LineEdit" type="LineEdit" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox/NormalHBox"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="PickButton" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox/NormalHBox"] +layout_mode = 2 + +[node name="ClearButton" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox/NormalHBox"] +layout_mode = 2 + +[node name="MarginContainer" type="MarginContainer" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox"] +layout_mode = 2 +size_flags_vertical = 4 +theme_override_constants/margin_top = 10 + +[node name="Panel" type="Panel" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox/MarginContainer"] +custom_minimum_size = Vector2(110, 110) +layout_mode = 2 +size_flags_horizontal = 4 +size_flags_vertical = 4 +theme_override_styles/panel = SubResource("StyleBoxFlat_cb0xf") + +[node name="TextureRect" type="TextureRect" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox/MarginContainer/Panel"] +layout_mode = 1 +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = -50.0 +offset_top = -50.0 +offset_right = 50.0 +offset_bottom = 50.0 +grow_horizontal = 2 +grow_vertical = 2 +expand_mode = 1 + +[node name="TextureButton" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox/MarginContainer/Panel"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +theme_override_styles/normal = SubResource("StyleBoxEmpty_7qdas") + +[node name="NormalWHHBox" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox"] +layout_mode = 2 +alignment = 1 + +[node name="NormalW" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox/NormalWHHBox"] +layout_mode = 2 +horizontal_alignment = 1 + +[node name="NormalH" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox/NormalWHHBox"] +layout_mode = 2 +horizontal_alignment = 1 + +[node name="HBoxContainer" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox"] +layout_mode = 2 +alignment = 1 + +[node name="InvertGreenChannelCheckBox" type="CheckBox" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox/HBoxContainer"] +layout_mode = 2 +text = " Convert DirectX to OpenGL" + +[node name="HBoxContainer2" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox"] +layout_mode = 2 +alignment = 1 + +[node name="AlignNormalsCheckBox" type="CheckBox" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox/HBoxContainer2"] +layout_mode = 2 +text = " Orthoganolise Normals" + +[node name="RoughnessVBox" type="VBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="RoughnessLabel" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox"] +layout_mode = 2 +text = "Roughness texture" + +[node name="RoughnessHBox" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox"] +layout_mode = 2 + +[node name="LineEdit" type="LineEdit" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/RoughnessHBox"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="PickButton" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/RoughnessHBox"] +layout_mode = 2 + +[node name="ClearButton" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/RoughnessHBox"] +layout_mode = 2 + +[node name="MarginContainer" type="MarginContainer" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox"] +layout_mode = 2 +size_flags_vertical = 4 +theme_override_constants/margin_top = 10 + +[node name="Panel" type="Panel" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/MarginContainer"] +custom_minimum_size = Vector2(110, 110) +layout_mode = 2 +size_flags_horizontal = 4 +size_flags_vertical = 4 +theme_override_styles/panel = SubResource("StyleBoxFlat_cb0xf") + +[node name="TextureRect" type="TextureRect" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/MarginContainer/Panel"] +layout_mode = 1 +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = -50.0 +offset_top = -50.0 +offset_right = 50.0 +offset_bottom = 50.0 +grow_horizontal = 2 +grow_vertical = 2 +expand_mode = 1 + +[node name="TextureButton" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/MarginContainer/Panel"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +theme_override_styles/normal = SubResource("StyleBoxEmpty_7qdas") + +[node name="RoughnessWHHBox" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox"] +layout_mode = 2 +alignment = 1 + +[node name="RoughnessW" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/RoughnessWHHBox"] +layout_mode = 2 +horizontal_alignment = 1 + +[node name="RoughnessH" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/RoughnessWHHBox"] +layout_mode = 2 +horizontal_alignment = 1 + +[node name="HBoxContainer2" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox"] +layout_mode = 2 +alignment = 1 + +[node name="InvertSmoothCheckBox" type="CheckBox" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/HBoxContainer2"] +layout_mode = 2 +text = " Convert Smoothness to Roughness" + +[node name="HBoxContainer" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox"] +layout_mode = 2 +alignment = 1 + +[node name="RoughnessChannelLabel" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/HBoxContainer"] +layout_mode = 2 +text = " Source Channel: " +horizontal_alignment = 2 + +[node name="RoughnessChannelR" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/HBoxContainer"] +layout_mode = 2 +toggle_mode = true +button_pressed = true +button_group = SubResource("ButtonGroup_bs6ki") +text = "R" + +[node name="RoughnessChannelG" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/HBoxContainer"] +layout_mode = 2 +toggle_mode = true +button_group = SubResource("ButtonGroup_bs6ki") +text = "G" + +[node name="RoughnessChannelB" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/HBoxContainer"] +layout_mode = 2 +toggle_mode = true +button_group = SubResource("ButtonGroup_bs6ki") +text = "B" + +[node name="RoughnessChannelA" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/HBoxContainer"] +layout_mode = 2 +toggle_mode = true +button_group = SubResource("ButtonGroup_bs6ki") +text = "A" + +[node name="GeneralOptionsLabel" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer"] +layout_mode = 2 +text = "General Options" +horizontal_alignment = 1 +vertical_alignment = 1 + +[node name="GeneralOptionsHBox" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer"] +layout_mode = 2 +alignment = 1 + +[node name="ResizeToggle" type="CheckBox" parent="PanelContainer/MarginContainer/VBoxContainer/GeneralOptionsHBox"] +layout_mode = 2 +text = " Resize Packed Image" + +[node name="ResizeOptionButton" type="SpinBox" parent="PanelContainer/MarginContainer/VBoxContainer/GeneralOptionsHBox"] +visible = false +layout_mode = 2 +tooltip_text = "A value of 0 disables resizing." +min_value = 128.0 +max_value = 4096.0 +step = 128.0 +value = 1024.0 + +[node name="VSeparator" type="VSeparator" parent="PanelContainer/MarginContainer/VBoxContainer/GeneralOptionsHBox"] +layout_mode = 2 + +[node name="GenerateMipmapsCheckBox" type="CheckBox" parent="PanelContainer/MarginContainer/VBoxContainer/GeneralOptionsHBox"] +layout_mode = 2 +button_pressed = true +text = "Generate Mipmaps" + +[node name="HighQualityCheckBox" type="CheckBox" parent="PanelContainer/MarginContainer/VBoxContainer/GeneralOptionsHBox"] +layout_mode = 2 +text = "Import High Quality" + +[node name="PackButton" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer"] +layout_mode = 2 +text = "Pack textures as..." + +[node name="StatusLabel" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer"] +custom_minimum_size = Vector2(0, 60) +layout_mode = 2 +horizontal_alignment = 1 +autowrap_mode = 3 + +[node name="HBoxContainer" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer"] +layout_mode = 2 +alignment = 1 + +[node name="CloseButton" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/HBoxContainer"] +layout_mode = 2 +text = "Close" + +[connection signal="toggled" from="PanelContainer/MarginContainer/VBoxContainer/GeneralOptionsHBox/ResizeToggle" to="PanelContainer/MarginContainer/VBoxContainer/GeneralOptionsHBox/ResizeOptionButton" method="set_visible"] diff --git a/addons/terrain_3d/menu/channel_packer_dragdrop.gd b/addons/terrain_3d/menu/channel_packer_dragdrop.gd new file mode 100644 index 0000000..154c8ad --- /dev/null +++ b/addons/terrain_3d/menu/channel_packer_dragdrop.gd @@ -0,0 +1,17 @@ +# Copyright © 2025 Cory Petkovsek, Roope Palmroos, and Contributors. +# Channel Packer Dragdropper for Terrain3D +@tool +extends Button + +signal dropped + +func _can_drop_data(p_position, p_data) -> bool: + if typeof(p_data) == TYPE_DICTIONARY: + if p_data.files.size() == 1: + match p_data.files[0].get_extension(): + "png", "bmp", "exr", "hdr", "jpg", "jpeg", "tga", "svg", "webp", "ktx", "dds": + return true + return false + +func _drop_data(p_position, p_data) -> void: + dropped.emit(p_data.files[0]) diff --git a/addons/terrain_3d/menu/channel_packer_dragdrop.gd.uid b/addons/terrain_3d/menu/channel_packer_dragdrop.gd.uid new file mode 100644 index 0000000..d9a59e3 --- /dev/null +++ b/addons/terrain_3d/menu/channel_packer_dragdrop.gd.uid @@ -0,0 +1 @@ +uid://br45krrqbw8bg diff --git a/addons/terrain_3d/menu/channel_packer_import_template.txt b/addons/terrain_3d/menu/channel_packer_import_template.txt new file mode 100644 index 0000000..55003e3 --- /dev/null +++ b/addons/terrain_3d/menu/channel_packer_import_template.txt @@ -0,0 +1,32 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +metadata={ +"imported_formats": ["s3tc_bptc"], +"vram_texture": true +} + +[deps] + +source_file="$SOURCE_FILE" + +[params] + +compress/mode=2 +compress/high_quality=$HIGH_QUALITY +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=2 +compress/channel_pack=0 +mipmaps/generate=$GENERATE_MIPMAPS +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/addons/terrain_3d/menu/directory_setup.gd b/addons/terrain_3d/menu/directory_setup.gd new file mode 100644 index 0000000..b851747 --- /dev/null +++ b/addons/terrain_3d/menu/directory_setup.gd @@ -0,0 +1,86 @@ +# Copyright © 2025 Cory Petkovsek, Roope Palmroos, and Contributors. +# Directory Setup for Terrain3D +extends Node + +const DIRECTORY_SETUP: String = "res://addons/terrain_3d/menu/directory_setup.tscn" + +var plugin: EditorPlugin +var dialog: ConfirmationDialog +var select_dir_btn: Button +var selected_dir_le: LineEdit +var editor_file_dialog: EditorFileDialog + + +func _init() -> void: + editor_file_dialog = EditorFileDialog.new() + editor_file_dialog.set_filters(PackedStringArray(["*.res"])) + editor_file_dialog.set_file_mode(EditorFileDialog.FILE_MODE_SAVE_FILE) + editor_file_dialog.access = EditorFileDialog.ACCESS_RESOURCES + editor_file_dialog.ok_button_text = "Open" + editor_file_dialog.title = "Open a folder or file" + editor_file_dialog.dir_selected.connect(_on_dir_selected) + editor_file_dialog.size = Vector2i(850, 550) + editor_file_dialog.transient = false + editor_file_dialog.exclusive = false + editor_file_dialog.popup_window = true + add_child(editor_file_dialog) + + +func directory_setup_popup() -> void: + dialog = load(DIRECTORY_SETUP).instantiate() + dialog.hide() + + # Nodes + select_dir_btn = dialog.get_node("Margin/VBox/DirHBox/SelectDir") + selected_dir_le = dialog.get_node("Margin/VBox/DirHBox/LineEdit") + + if plugin.terrain.data_directory: + selected_dir_le.text = plugin.terrain.data_directory + + # Icons + plugin.ui.set_button_editor_icon(select_dir_btn, "Folder") + + #Signals + select_dir_btn.pressed.connect(_on_select_file_pressed.bind(EditorFileDialog.FILE_MODE_OPEN_DIR)) + dialog.confirmed.connect(_on_close_requested) + dialog.canceled.connect(_on_close_requested) + dialog.get_ok_button().pressed.connect(_on_ok_pressed) + + # Popup + EditorInterface.popup_dialog_centered(dialog) + + +func _on_close_requested() -> void: + dialog.queue_free() + dialog = null + + +func _on_select_file_pressed(file_mode: EditorFileDialog.FileMode) -> void: + editor_file_dialog.file_mode = file_mode + editor_file_dialog.popup_centered() + + +func _on_dir_selected(path: String) -> void: + selected_dir_le.text = path + + +func _on_ok_pressed() -> void: + if not plugin.terrain: + push_error("Not connected terrain. Click the Terrain3D node first") + return + if selected_dir_le.text.is_empty(): + push_error("No data directory specified") + return + if not DirAccess.dir_exists_absolute(selected_dir_le.text): + push_error("Directory doesn't exist: ", selected_dir_le.text) + return + # Check if directory empty of terrain files + var data_found: bool = false + var files: Array = DirAccess.get_files_at(selected_dir_le.text) + for file in files: + if file.begins_with("terrain3d") || file.ends_with(".res"): + data_found = true + break + + print("Setting terrain directory: ", selected_dir_le.text) + plugin.terrain.data_directory = selected_dir_le.text diff --git a/addons/terrain_3d/menu/directory_setup.gd.uid b/addons/terrain_3d/menu/directory_setup.gd.uid new file mode 100644 index 0000000..de0b12f --- /dev/null +++ b/addons/terrain_3d/menu/directory_setup.gd.uid @@ -0,0 +1 @@ +uid://0034ukv2mngn diff --git a/addons/terrain_3d/menu/directory_setup.tscn b/addons/terrain_3d/menu/directory_setup.tscn new file mode 100644 index 0000000..d5dabc5 --- /dev/null +++ b/addons/terrain_3d/menu/directory_setup.tscn @@ -0,0 +1,48 @@ +[gd_scene format=3 uid="uid://by3kr2nqbqr67"] + +[node name="DirectorySetup" type="ConfirmationDialog"] +title = "Terrain3D Data Directory Setup" +position = Vector2i(0, 36) +size = Vector2i(750, 330) +visible = true + +[node name="Margin" type="MarginContainer" parent="."] +offset_left = 8.0 +offset_top = 8.0 +offset_right = 742.0 +offset_bottom = 281.0 +theme_override_constants/margin_left = 20 +theme_override_constants/margin_top = 20 +theme_override_constants/margin_right = 20 +theme_override_constants/margin_bottom = 20 + +[node name="VBox" type="VBoxContainer" parent="Margin"] +layout_mode = 2 + +[node name="Instructions" type="Label" parent="Margin/VBox"] +custom_minimum_size = Vector2(400, 0) +layout_mode = 2 +text = "Terrain3D now stores data in a directory instead of a single file. Each region is stored in a separate file named `terrain3d[-_]##[-_]##.res`. For instance, the region at location (-1, 1) would be named `terrain3d-01_01.res`. Enable Terrain3D / Debug / Show Region Labels for a visual display." +autowrap_mode = 3 + +[node name="DirectoryLabel" type="Label" parent="Margin/VBox"] +custom_minimum_size = Vector2(400, 0) +layout_mode = 2 +text = " +Specify the directory to store your data. Any existing region files will be loaded." +autowrap_mode = 3 + +[node name="DirHBox" type="HBoxContainer" parent="Margin/VBox"] +layout_mode = 2 + +[node name="LineEdit" type="LineEdit" parent="Margin/VBox/DirHBox"] +layout_mode = 2 +size_flags_horizontal = 3 +placeholder_text = "Data directory" + +[node name="SelectDir" type="Button" parent="Margin/VBox/DirHBox"] +layout_mode = 2 + +[node name="Spacer" type="Control" parent="Margin/VBox"] +custom_minimum_size = Vector2(0, 40) +layout_mode = 2 diff --git a/addons/terrain_3d/menu/terrain_menu.gd b/addons/terrain_3d/menu/terrain_menu.gd new file mode 100644 index 0000000..c53656f --- /dev/null +++ b/addons/terrain_3d/menu/terrain_menu.gd @@ -0,0 +1,83 @@ +# Copyright © 2025 Cory Petkovsek, Roope Palmroos, and Contributors. +# Menu for Terrain3D +extends HBoxContainer + + +const DirectoryWizard: Script = preload("res://addons/terrain_3d/menu/directory_setup.gd") +const Packer: Script = preload("res://addons/terrain_3d/menu/channel_packer.gd") +const Baker: Script = preload("res://addons/terrain_3d/menu/baker.gd") + +var plugin: EditorPlugin +var menu_button: MenuButton = MenuButton.new() +var directory_setup: DirectoryWizard = DirectoryWizard.new() +var packer: Packer = Packer.new() +var baker: Baker = Baker.new() + +# These are IDs and order must be consistent with add_item and set_disabled IDs +enum { + MENU_DIRECTORY_SETUP, + MENU_PACK_TEXTURES, + MENU_SEPARATOR, + MENU_BAKE_ARRAY_MESH, + MENU_BAKE_OCCLUDER, + MENU_SEPARATOR2, + MENU_SET_UP_NAVIGATION, + MENU_BAKE_NAV_MESH, +} + + +func _enter_tree() -> void: + directory_setup.plugin = plugin + packer.plugin = plugin + baker.plugin = plugin + add_child(directory_setup) + add_child(baker) + + menu_button.text = "Terrain3D" + menu_button.get_popup().add_item("Directory Setup...", MENU_DIRECTORY_SETUP) + menu_button.get_popup().add_item("Pack Textures...", MENU_PACK_TEXTURES) + menu_button.get_popup().add_separator("", MENU_SEPARATOR) + menu_button.get_popup().add_item("Bake ArrayMesh...", MENU_BAKE_ARRAY_MESH) + menu_button.get_popup().add_item("Bake Occluder3D...", MENU_BAKE_OCCLUDER) + menu_button.get_popup().add_separator("", MENU_SEPARATOR2) + menu_button.get_popup().add_item("Set up Navigation...", MENU_SET_UP_NAVIGATION) + menu_button.get_popup().add_item("Bake NavMesh...", MENU_BAKE_NAV_MESH) + + menu_button.get_popup().id_pressed.connect(_on_menu_pressed) + menu_button.about_to_popup.connect(_on_menu_about_to_popup) + add_child(menu_button) + + +func _on_menu_pressed(p_id: int) -> void: + match p_id: + MENU_DIRECTORY_SETUP: + directory_setup.directory_setup_popup() + MENU_PACK_TEXTURES: + packer.pack_textures_popup() + MENU_BAKE_ARRAY_MESH: + baker.bake_mesh_popup() + MENU_BAKE_OCCLUDER: + baker.bake_occluder_popup() + MENU_SET_UP_NAVIGATION: + baker.set_up_navigation_popup() + MENU_BAKE_NAV_MESH: + baker.bake_nav_mesh() + + +func _on_menu_about_to_popup() -> void: + menu_button.get_popup().set_item_disabled(MENU_DIRECTORY_SETUP, not plugin.terrain) + menu_button.get_popup().set_item_disabled(MENU_PACK_TEXTURES, not plugin.terrain) + menu_button.get_popup().set_item_disabled(MENU_BAKE_ARRAY_MESH, not plugin.terrain) + menu_button.get_popup().set_item_disabled(MENU_BAKE_OCCLUDER, not plugin.terrain) + + if plugin.terrain: + var nav_regions: Array[NavigationRegion3D] = baker.find_terrain_nav_regions(plugin.terrain) + menu_button.get_popup().set_item_disabled(MENU_BAKE_NAV_MESH, nav_regions.size() == 0) + menu_button.get_popup().set_item_disabled(MENU_SET_UP_NAVIGATION, nav_regions.size() != 0) + elif plugin.nav_region: + var terrains: Array[Terrain3D] = baker.find_nav_region_terrains(plugin.nav_region) + menu_button.get_popup().set_item_disabled(MENU_BAKE_NAV_MESH, terrains.size() == 0) + menu_button.get_popup().set_item_disabled(MENU_SET_UP_NAVIGATION, true) + else: + menu_button.get_popup().set_item_disabled(MENU_BAKE_NAV_MESH, true) + menu_button.get_popup().set_item_disabled(MENU_SET_UP_NAVIGATION, true) diff --git a/addons/terrain_3d/menu/terrain_menu.gd.uid b/addons/terrain_3d/menu/terrain_menu.gd.uid new file mode 100644 index 0000000..b1b1cea --- /dev/null +++ b/addons/terrain_3d/menu/terrain_menu.gd.uid @@ -0,0 +1 @@ +uid://3gxvahogxa10 diff --git a/addons/terrain_3d/plugin.cfg b/addons/terrain_3d/plugin.cfg new file mode 100644 index 0000000..8371f4e --- /dev/null +++ b/addons/terrain_3d/plugin.cfg @@ -0,0 +1,7 @@ +[plugin] + +name="Terrain3D" +description="A high performance, editable terrain system for Godot 4." +author="Cory Petkovsek & Roope Palmroos" +version="1.0.0" +script="src/editor_plugin.gd" diff --git a/addons/terrain_3d/src/asset_dock.gd b/addons/terrain_3d/src/asset_dock.gd new file mode 100644 index 0000000..cfa28fc --- /dev/null +++ b/addons/terrain_3d/src/asset_dock.gd @@ -0,0 +1,883 @@ +# Copyright © 2025 Cory Petkovsek, Roope Palmroos, and Contributors. +# Asset Dock for Terrain3D +@tool +extends PanelContainer + +signal confirmation_closed +signal confirmation_confirmed +signal confirmation_canceled + +const ES_DOCK_SLOT: String = "terrain3d/dock/slot" +const ES_DOCK_TILE_SIZE: String = "terrain3d/dock/tile_size" +const ES_DOCK_FLOATING: String = "terrain3d/dock/floating" +const ES_DOCK_PINNED: String = "terrain3d/dock/always_on_top" +const ES_DOCK_WINDOW_POSITION: String = "terrain3d/dock/window_position" +const ES_DOCK_WINDOW_SIZE: String = "terrain3d/dock/window_size" +const ES_DOCK_TAB: String = "terrain3d/dock/tab" + +var texture_list: ListContainer +var mesh_list: ListContainer +var _current_list: ListContainer +var _last_thumb_update_time: int = 0 +const MAX_UPDATE_TIME: int = 1000 + +var placement_opt: OptionButton +var floating_btn: Button +var pinned_btn: Button +var size_slider: HSlider +var box: BoxContainer +var buttons: BoxContainer +var textures_btn: Button +var meshes_btn: Button +var asset_container: ScrollContainer +var confirm_dialog: ConfirmationDialog +var _confirmed: bool = false + +# Used only for editor, so change to single visible/hiddden +enum { + HIDDEN = -1, + SIDEBAR = 0, + BOTTOM = 1, + WINDOWED = 2, +} +var state: int = HIDDEN + +enum { + POS_LEFT_UL = 0, + POS_LEFT_BL = 1, + POS_LEFT_UR = 2, + POS_LEFT_BR = 3, + POS_RIGHT_UL = 4, + POS_RIGHT_BL = 5, + POS_RIGHT_UR = 6, + POS_RIGHT_BR = 7, + POS_BOTTOM = 8, + POS_MAX = 9, +} +var slot: int = POS_RIGHT_BR +var _initialized: bool = false +var plugin: EditorPlugin +var window: Window +var _godot_last_state: Window.Mode = Window.MODE_FULLSCREEN + + +func initialize(p_plugin: EditorPlugin) -> void: + if p_plugin: + plugin = p_plugin + + _godot_last_state = plugin.godot_editor_window.mode + placement_opt = $Box/Buttons/PlacementOpt + pinned_btn = $Box/Buttons/Pinned + floating_btn = $Box/Buttons/Floating + floating_btn.owner = null + size_slider = $Box/Buttons/SizeSlider + size_slider.owner = null + box = $Box + buttons = $Box/Buttons + textures_btn = $Box/Buttons/TexturesBtn + meshes_btn = $Box/Buttons/MeshesBtn + asset_container = $Box/ScrollContainer + + texture_list = ListContainer.new() + texture_list.plugin = plugin + texture_list.type = Terrain3DAssets.TYPE_TEXTURE + asset_container.add_child(texture_list) + mesh_list = ListContainer.new() + mesh_list.plugin = plugin + mesh_list.type = Terrain3DAssets.TYPE_MESH + mesh_list.visible = false + asset_container.add_child(mesh_list) + _current_list = texture_list + + load_editor_settings() + + # Connect signals + resized.connect(update_layout) + textures_btn.pressed.connect(_on_textures_pressed) + meshes_btn.pressed.connect(_on_meshes_pressed) + placement_opt.item_selected.connect(set_slot) + floating_btn.pressed.connect(make_dock_float) + pinned_btn.toggled.connect(_on_pin_changed) + pinned_btn.visible = ( window != null ) + size_slider.value_changed.connect(_on_slider_changed) + plugin.ui.toolbar.tool_changed.connect(_on_tool_changed) + + meshes_btn.add_theme_font_size_override("font_size", 16 * EditorInterface.get_editor_scale()) + textures_btn.add_theme_font_size_override("font_size", 16 * EditorInterface.get_editor_scale()) + + _initialized = true + update_dock() + update_layout() + + +func _ready() -> void: + if not _initialized: + return + + # Setup styles + set("theme_override_styles/panel", get_theme_stylebox("panel", "Panel")) + # Avoid saving icon resources in tscn when editing w/ a tool script + if EditorInterface.get_edited_scene_root() != self: + pinned_btn.icon = get_theme_icon("Pin", "EditorIcons") + pinned_btn.text = "" + floating_btn.icon = get_theme_icon("MakeFloating", "EditorIcons") + floating_btn.text = "" + + update_thumbnails() + confirm_dialog = ConfirmationDialog.new() + add_child(confirm_dialog) + confirm_dialog.hide() + confirm_dialog.confirmed.connect(func(): _confirmed = true; \ + emit_signal("confirmation_closed"); \ + emit_signal("confirmation_confirmed") ) + confirm_dialog.canceled.connect(func(): _confirmed = false; \ + emit_signal("confirmation_closed"); \ + emit_signal("confirmation_canceled") ) + + +func get_current_list() -> ListContainer: + return _current_list + + +## Dock placement + +func set_slot(p_slot: int) -> void: + p_slot = clamp(p_slot, 0, POS_MAX-1) + + if slot != p_slot: + slot = p_slot + placement_opt.selected = slot + save_editor_settings() + plugin.select_terrain() + update_dock() + + +func remove_dock(p_force: bool = false) -> void: + if state == SIDEBAR: + plugin.remove_control_from_docks(self) + state = HIDDEN + + elif state == BOTTOM: + plugin.remove_control_from_bottom_panel(self) + state = HIDDEN + + # If windowed and destination is not window or final exit, otherwise leave + elif state == WINDOWED and p_force and window: + var parent: Node = get_parent() + if parent: + parent.remove_child(self) + plugin.godot_editor_window.mouse_entered.disconnect(_on_godot_window_entered) + plugin.godot_editor_window.focus_entered.disconnect(_on_godot_focus_entered) + plugin.godot_editor_window.focus_exited.disconnect(_on_godot_focus_exited) + window.hide() + window.queue_free() + window = null + floating_btn.button_pressed = false + floating_btn.visible = true + pinned_btn.visible = false + placement_opt.visible = true + state = HIDDEN + update_dock() # return window to side/bottom + + +func update_dock() -> void: + if not _initialized or window: + return + + update_assets() + + # Move dock to new destination + remove_dock() + # Sidebar + if slot < POS_BOTTOM: + state = SIDEBAR + plugin.add_control_to_dock(slot, self) + # Bottom + elif slot == POS_BOTTOM: + state = BOTTOM + plugin.add_control_to_bottom_panel(self, "Terrain3D") + plugin.make_bottom_panel_item_visible(self) + + +func update_layout() -> void: + if not _initialized: + return + + # Detect if we have a new window from Make floating, grab it so we can free it properly + if not window and get_parent() and get_parent().get_parent() is Window: + window = get_parent().get_parent() + make_dock_float() + return # Will call this function again upon display + + var size_parent: Control = size_slider.get_parent() + # Vertical layout in window / sidebar + if window or slot < POS_BOTTOM: + box.vertical = true + buttons.vertical = false + + if size.x >= 500 and size_parent != buttons: + size_slider.reparent(buttons) + buttons.move_child(size_slider, 3) + elif size.x < 500 and size_parent != box: + size_slider.reparent(box) + box.move_child(size_slider, 1) + floating_btn.reparent(buttons) + buttons.move_child(floating_btn, 4) + + # Wide layout on bottom bar + else: + size_slider.reparent(buttons) + buttons.move_child(size_slider, 3) + floating_btn.reparent(box) + box.vertical = false + buttons.vertical = true + + save_editor_settings() + + +func update_thumbnails() -> void: + if not is_instance_valid(plugin.terrain): + return + if _current_list.type == Terrain3DAssets.TYPE_MESH and \ + Time.get_ticks_msec() - _last_thumb_update_time > MAX_UPDATE_TIME: + plugin.terrain.assets.create_mesh_thumbnails() + _last_thumb_update_time = Time.get_ticks_msec() + for mesh_asset in mesh_list.entries: + mesh_asset.queue_redraw() + + +## Dock Button handlers + + +func _on_pin_changed(toggled: bool) -> void: + if window: + window.always_on_top = pinned_btn.button_pressed + save_editor_settings() + + +func _on_slider_changed(value: float) -> void: + if texture_list: + texture_list.set_entry_width(value) + if mesh_list: + mesh_list.set_entry_width(value) + save_editor_settings() + + +func _on_textures_pressed() -> void: + _current_list = texture_list + texture_list.update_asset_list() + texture_list.visible = true + mesh_list.visible = false + textures_btn.button_pressed = true + meshes_btn.button_pressed = false + texture_list.set_selected_id(texture_list.selected_id) + if plugin.is_terrain_valid(): + EditorInterface.edit_node(plugin.terrain) + save_editor_settings() + + +func _on_meshes_pressed() -> void: + _current_list = mesh_list + mesh_list.update_asset_list() + mesh_list.visible = true + texture_list.visible = false + meshes_btn.button_pressed = true + textures_btn.button_pressed = false + mesh_list.set_selected_id(mesh_list.selected_id) + if plugin.is_terrain_valid(): + EditorInterface.edit_node(plugin.terrain) + update_thumbnails() + save_editor_settings() + + +func _on_tool_changed(p_tool: Terrain3DEditor.Tool, p_operation: Terrain3DEditor.Operation) -> void: + if p_tool == Terrain3DEditor.INSTANCER: + _on_meshes_pressed() + elif p_tool in [ Terrain3DEditor.TEXTURE, Terrain3DEditor.COLOR, Terrain3DEditor.ROUGHNESS ]: + _on_textures_pressed() + + +## Update Dock Contents + + +func update_assets() -> void: + if not _initialized: + return + + # Verify signals to individual lists + if plugin.is_terrain_valid() and plugin.terrain.assets: + if not plugin.terrain.assets.textures_changed.is_connected(texture_list.update_asset_list): + plugin.terrain.assets.textures_changed.connect(texture_list.update_asset_list) + if not plugin.terrain.assets.meshes_changed.is_connected(mesh_list.update_asset_list): + plugin.terrain.assets.meshes_changed.connect(mesh_list.update_asset_list) + + _current_list.update_asset_list() + + +## Window Management + + +func make_dock_float() -> void: + # If not already created (eg from editor panel 'Make Floating' button) + if not window: + remove_dock() + create_window() + + state = WINDOWED + visible = true # Asset dock contents are hidden when popping out of the bottom! + pinned_btn.visible = true + floating_btn.visible = false + placement_opt.visible = false + window.title = "Terrain3D Asset Dock" + window.always_on_top = pinned_btn.button_pressed + window.close_requested.connect(remove_dock.bind(true)) + window.window_input.connect(_on_window_input) + window.focus_exited.connect(save_editor_settings) + window.mouse_exited.connect(save_editor_settings) + window.size_changed.connect(save_editor_settings) + plugin.godot_editor_window.mouse_entered.connect(_on_godot_window_entered) + plugin.godot_editor_window.focus_entered.connect(_on_godot_focus_entered) + plugin.godot_editor_window.focus_exited.connect(_on_godot_focus_exited) + plugin.godot_editor_window.grab_focus() + update_assets() + save_editor_settings() + + +func create_window() -> void: + window = Window.new() + window.wrap_controls = true + var mc := MarginContainer.new() + mc.set_anchors_preset(PRESET_FULL_RECT, false) + mc.add_child(self) + window.add_child(mc) + window.set_transient(false) + window.set_size(plugin.get_setting(ES_DOCK_WINDOW_SIZE, Vector2i(512, 512))) + window.set_position(plugin.get_setting(ES_DOCK_WINDOW_POSITION, Vector2i(704, 284))) + plugin.add_child(window) + window.show() + + +func clamp_window_position() -> void: + if window and window.visible: + var bounds: Vector2i + if EditorInterface.get_editor_settings().get_setting("interface/editor/single_window_mode"): + bounds = EditorInterface.get_base_control().size + else: + bounds = DisplayServer.screen_get_position(window.current_screen) + bounds += DisplayServer.screen_get_size(window.current_screen) + var margin: int = 40 + window.position.x = clamp(window.position.x, -window.size.x + 2*margin, bounds.x - margin) + window.position.y = clamp(window.position.y, 25, bounds.y - margin) + + +func _on_window_input(event: InputEvent) -> void: + # Capture CTRL+S when doc focused to save scene + if event is InputEventKey and event.keycode == KEY_S and event.pressed and event.is_command_or_control_pressed(): + save_editor_settings() + EditorInterface.save_scene() + + +func _on_godot_window_entered() -> void: + if is_instance_valid(window) and window.has_focus(): + plugin.godot_editor_window.grab_focus() + + +func _on_godot_focus_entered() -> void: + # If asset dock is windowed, and Godot was minimized, and now is not, restore asset dock window + if is_instance_valid(window): + if _godot_last_state == Window.MODE_MINIMIZED and plugin.godot_editor_window.mode != Window.MODE_MINIMIZED: + window.show() + _godot_last_state = plugin.godot_editor_window.mode + plugin.godot_editor_window.grab_focus() + + +func _on_godot_focus_exited() -> void: + if is_instance_valid(window) and plugin.godot_editor_window.mode == Window.MODE_MINIMIZED: + window.hide() + _godot_last_state = plugin.godot_editor_window.mode + + +## Manage Editor Settings + +func load_editor_settings() -> void: + floating_btn.button_pressed = plugin.get_setting(ES_DOCK_FLOATING, false) + pinned_btn.button_pressed = plugin.get_setting(ES_DOCK_PINNED, true) + size_slider.value = plugin.get_setting(ES_DOCK_TILE_SIZE, 83) + _on_slider_changed(size_slider.value) + set_slot(plugin.get_setting(ES_DOCK_SLOT, POS_BOTTOM)) + if floating_btn.button_pressed: + make_dock_float() + # TODO Don't save tab until thumbnail generation more reliable + #if plugin.get_setting(ES_DOCK_TAB, 0) == 1: + # _on_meshes_pressed() + + +func save_editor_settings() -> void: + if not _initialized: + return + clamp_window_position() + plugin.set_setting(ES_DOCK_SLOT, slot) + plugin.set_setting(ES_DOCK_TILE_SIZE, size_slider.value) + plugin.set_setting(ES_DOCK_FLOATING, floating_btn.button_pressed) + plugin.set_setting(ES_DOCK_PINNED, pinned_btn.button_pressed) + # TODO Don't save tab until thumbnail generation more reliable + # plugin.set_setting(ES_DOCK_TAB, 0 if _current_list == texture_list else 1) + if window: + plugin.set_setting(ES_DOCK_WINDOW_SIZE, window.size) + plugin.set_setting(ES_DOCK_WINDOW_POSITION, window.position) + + +############################################################## +## class ListContainer +############################################################## + + +class ListContainer extends Container: + var plugin: EditorPlugin + var type := Terrain3DAssets.TYPE_TEXTURE + var entries: Array[ListEntry] + var selected_id: int = 0 + var height: float = 0 + var width: float = 83 + var focus_style: StyleBox + + + func _ready() -> void: + set_v_size_flags(SIZE_EXPAND_FILL) + set_h_size_flags(SIZE_EXPAND_FILL) + + + func clear() -> void: + for e in entries: + e.get_parent().remove_child(e) + e.queue_free() + entries.clear() + + + func update_asset_list() -> void: + clear() + + # Grab terrain + var t: Terrain3D + if plugin.is_terrain_valid(): + t = plugin.terrain + elif is_instance_valid(plugin._last_terrain) and plugin.is_terrain_valid(plugin._last_terrain): + t = plugin._last_terrain + else: + return + + if not t.assets: + return + + if type == Terrain3DAssets.TYPE_TEXTURE: + var texture_count: int = t.assets.get_texture_count() + for i in texture_count: + var texture: Terrain3DTextureAsset = t.assets.get_texture(i) + add_item(texture) + if texture_count < Terrain3DAssets.MAX_TEXTURES: + add_item() + else: + var mesh_count: int = t.assets.get_mesh_count() + for i in mesh_count: + var mesh: Terrain3DMeshAsset = t.assets.get_mesh_asset(i) + add_item(mesh, t.assets) + if mesh_count < Terrain3DAssets.MAX_MESHES: + add_item() + if selected_id >= mesh_count or selected_id < 0: + set_selected_id(0) + + + func add_item(p_resource: Resource = null, p_assets: Terrain3DAssets = null) -> void: + var entry: ListEntry = ListEntry.new() + entry.focus_style = focus_style + var id: int = entries.size() + + entry.set_edited_resource(p_resource) + entry.hovered.connect(_on_resource_hovered.bind(id)) + entry.selected.connect(set_selected_id.bind(id)) + entry.inspected.connect(_on_resource_inspected) + entry.changed.connect(_on_resource_changed.bind(id)) + entry.type = type + entry.asset_list = p_assets + add_child(entry) + entries.push_back(entry) + + if p_resource: + entry.set_selected(id == selected_id) + if not p_resource.id_changed.is_connected(set_selected_after_swap): + p_resource.id_changed.connect(set_selected_after_swap) + + + func _on_resource_hovered(p_id: int): + if type == Terrain3DAssets.TYPE_MESH: + if plugin.terrain: + plugin.terrain.assets.create_mesh_thumbnails(p_id) + + + func set_selected_after_swap(p_type: Terrain3DAssets.AssetType, p_old_id: int, p_new_id: int) -> void: + set_selected_id(clamp(p_new_id, 0, entries.size() - 2)) + + + func set_selected_id(p_id: int) -> void: + selected_id = p_id + + for i in entries.size(): + var entry: ListEntry = entries[i] + entry.set_selected(i == selected_id) + + plugin.select_terrain() + + # Select Paint tool if clicking a texture + if type == Terrain3DAssets.TYPE_TEXTURE and \ + not plugin.editor.get_tool() in [ Terrain3DEditor.TEXTURE, Terrain3DEditor.COLOR, Terrain3DEditor.ROUGHNESS ]: + var paint_btn: Button = plugin.ui.toolbar.get_node_or_null("PaintBaseTexture") + if paint_btn: + paint_btn.set_pressed(true) + plugin.ui._on_tool_changed(Terrain3DEditor.TEXTURE, Terrain3DEditor.REPLACE) + + elif type == Terrain3DAssets.TYPE_MESH and plugin.editor.get_tool() != Terrain3DEditor.INSTANCER: + var instancer_btn: Button = plugin.ui.toolbar.get_node_or_null("InstanceMeshes") + if instancer_btn: + instancer_btn.set_pressed(true) + plugin.ui._on_tool_changed(Terrain3DEditor.INSTANCER, Terrain3DEditor.ADD) + + # Update editor with selected brush + plugin.ui._on_setting_changed() + + + func _on_resource_inspected(p_resource: Resource) -> void: + await get_tree().create_timer(.01).timeout + EditorInterface.edit_resource(p_resource) + + + func _on_resource_changed(p_resource: Resource, p_id: int) -> void: + if not p_resource: + var asset_dock: Control = get_parent().get_parent().get_parent() + if type == Terrain3DAssets.TYPE_TEXTURE: + asset_dock.confirm_dialog.dialog_text = "Are you sure you want to clear this texture?" + else: + asset_dock.confirm_dialog.dialog_text = "Are you sure you want to clear this mesh and delete all instances?" + asset_dock.confirm_dialog.popup_centered() + await asset_dock.confirmation_closed + if not asset_dock._confirmed: + update_asset_list() + return + + if not plugin.is_terrain_valid(): + plugin.select_terrain() + await get_tree().create_timer(.01).timeout + + if plugin.is_terrain_valid(): + if type == Terrain3DAssets.TYPE_TEXTURE: + plugin.terrain.get_assets().set_texture(p_id, p_resource) + else: + plugin.terrain.get_assets().set_mesh_asset(p_id, p_resource) + await get_tree().create_timer(.01).timeout + plugin.terrain.assets.create_mesh_thumbnails(p_id) + + # If removing an entry, clear inspector + if not p_resource: + EditorInterface.inspect_object(null) + + # If null resource, remove last + if not p_resource: + var last_offset: int = 2 + if p_id == entries.size()-2: + last_offset = 3 + set_selected_id(clamp(selected_id, 0, entries.size() - last_offset)) + + + func get_selected_id() -> int: + return selected_id + + + func set_entry_width(value: float) -> void: + width = clamp(value, 66, 230) + redraw() + + + func get_entry_width() -> float: + return width + + + func redraw() -> void: + height = 0 + var id: int = 0 + var separation: float = 4 + var columns: int = 3 + columns = clamp(size.x / width, 1, 100) + + for c in get_children(): + if is_instance_valid(c): + c.size = Vector2(width, width) - Vector2(separation, separation) + c.position = Vector2(id % columns, id / columns) * width + \ + Vector2(separation / columns, separation / columns) + height = max(height, c.position.y + width) + id += 1 + + + # Needed to enable ScrollContainer scroll bar + func _get_minimum_size() -> Vector2: + return Vector2(0, height) + + + func _notification(p_what) -> void: + if p_what == NOTIFICATION_SORT_CHILDREN: + redraw() + + +############################################################## +## class ListEntry +############################################################## + + +class ListEntry extends VBoxContainer: + signal hovered() + signal selected() + signal changed(resource: Resource) + signal inspected(resource: Resource) + + var resource: Resource + var type := Terrain3DAssets.TYPE_TEXTURE + var _thumbnail: Texture2D + var drop_data: bool = false + var is_hovered: bool = false + var is_selected: bool = false + var asset_list: Terrain3DAssets + + @onready var button_row := HBoxContainer.new() + @onready var button_clear := TextureButton.new() + @onready var button_edit := TextureButton.new() + @onready var spacer := Control.new() + @onready var button_enabled := TextureButton.new() + @onready var clear_icon: Texture2D = get_theme_icon("Close", "EditorIcons") + @onready var edit_icon: Texture2D = get_theme_icon("Edit", "EditorIcons") + @onready var enabled_icon: Texture2D = get_theme_icon("GuiVisibilityVisible", "EditorIcons") + @onready var disabled_icon: Texture2D = get_theme_icon("GuiVisibilityHidden", "EditorIcons") + + var name_label: Label + @onready var add_icon: Texture2D = get_theme_icon("Add", "EditorIcons") + @onready var background: StyleBox = get_theme_stylebox("pressed", "Button") + @onready var focus_style: StyleBox = get_theme_stylebox("focus", "Button").duplicate() + + + func _ready() -> void: + setup_buttons() + setup_label() + focus_style.set_border_width_all(2) + focus_style.set_border_color(Color(1, 1, 1, .67)) + + + func setup_buttons() -> void: + var icon_size: Vector2 = Vector2(12, 12) + var margin_container := MarginContainer.new() + margin_container.mouse_filter = Control.MOUSE_FILTER_PASS + margin_container.add_theme_constant_override("margin_top", 5) + margin_container.add_theme_constant_override("margin_left", 5) + margin_container.add_theme_constant_override("margin_right", 5) + add_child(margin_container) + + button_row.size_flags_horizontal = Control.SIZE_EXPAND_FILL + button_row.alignment = BoxContainer.ALIGNMENT_CENTER + button_row.mouse_filter = Control.MOUSE_FILTER_PASS + margin_container.add_child(button_row) + + if type == Terrain3DAssets.TYPE_MESH: + button_enabled.set_texture_normal(enabled_icon) + button_enabled.set_texture_pressed(disabled_icon) + button_enabled.set_custom_minimum_size(icon_size) + button_enabled.set_h_size_flags(Control.SIZE_SHRINK_END) + button_enabled.set_visible(resource != null) + button_enabled.toggle_mode = true + button_enabled.mouse_filter = Control.MOUSE_FILTER_PASS + button_enabled.pressed.connect(enable) + button_row.add_child(button_enabled) + + button_edit.set_texture_normal(edit_icon) + button_edit.set_custom_minimum_size(icon_size) + button_edit.set_h_size_flags(Control.SIZE_SHRINK_END) + button_edit.set_visible(resource != null) + button_edit.mouse_filter = Control.MOUSE_FILTER_PASS + button_edit.pressed.connect(edit) + button_row.add_child(button_edit) + + spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL + spacer.mouse_filter = Control.MOUSE_FILTER_PASS + button_row.add_child(spacer) + + button_clear.set_texture_normal(clear_icon) + button_clear.set_custom_minimum_size(icon_size) + button_clear.set_h_size_flags(Control.SIZE_SHRINK_END) + button_clear.set_visible(resource != null) + button_clear.mouse_filter = Control.MOUSE_FILTER_PASS + button_clear.pressed.connect(clear) + button_row.add_child(button_clear) + + + func setup_label() -> void: + name_label = Label.new() + add_child(name_label, true) + name_label.visible = false + name_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + name_label.vertical_alignment = VERTICAL_ALIGNMENT_BOTTOM + name_label.size_flags_vertical = Control.SIZE_EXPAND_FILL + name_label.add_theme_color_override("font_color", Color.WHITE) + name_label.add_theme_color_override("font_shadow_color", Color.BLACK) + name_label.add_theme_constant_override("shadow_offset_x", 1.) + name_label.add_theme_constant_override("shadow_offset_y", 1.) + name_label.add_theme_font_size_override("font_size", 15) + name_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + name_label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS + if type == Terrain3DAssets.TYPE_TEXTURE: + name_label.text = "Add Texture" + else: + name_label.text = "Add Mesh" + + + func _notification(p_what) -> void: + match p_what: + NOTIFICATION_DRAW: + # Hide spacer if icons are crowding small textures + spacer.visible = size.x > 70 or type == Terrain3DAssets.TYPE_TEXTURE + + var rect: Rect2 = Rect2(Vector2.ZERO, get_size()) + if !resource: + draw_style_box(background, rect) + draw_texture(add_icon, (get_size() / 2) - (add_icon.get_size() / 2)) + else: + if type == Terrain3DAssets.TYPE_TEXTURE: + name_label.text = (resource as Terrain3DTextureAsset).get_name() + self_modulate = resource.get_albedo_color() + _thumbnail = resource.get_albedo_texture() + if _thumbnail: + draw_texture_rect(_thumbnail, rect, false) + texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST_WITH_MIPMAPS + else: + name_label.text = (resource as Terrain3DMeshAsset).get_name() + var id: int = (resource as Terrain3DMeshAsset).get_id() + _thumbnail = resource.get_thumbnail() + if _thumbnail: + draw_texture_rect(_thumbnail, rect, false) + texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS + else: + draw_rect(rect, Color(.15, .15, .15, 1.)) + button_enabled.set_pressed_no_signal(!resource.is_enabled()) + name_label.add_theme_font_size_override("font_size", 4 + rect.size.x/10) + if drop_data: + draw_style_box(focus_style, rect) + if is_hovered: + draw_rect(rect, Color(1, 1, 1, 0.2)) + if is_selected: + draw_style_box(focus_style, rect) + NOTIFICATION_MOUSE_ENTER: + is_hovered = true + name_label.visible = true + emit_signal("hovered") + queue_redraw() + NOTIFICATION_MOUSE_EXIT: + is_hovered = false + name_label.visible = false + drop_data = false + queue_redraw() + + + func _gui_input(p_event: InputEvent) -> void: + if p_event is InputEventMouseButton: + if p_event.is_pressed(): + match p_event.get_button_index(): + MOUSE_BUTTON_LEFT: + # If `Add new` is clicked + if !resource: + if type == Terrain3DAssets.TYPE_TEXTURE: + set_edited_resource(Terrain3DTextureAsset.new(), false) + else: + set_edited_resource(Terrain3DMeshAsset.new(), false) + edit() + else: + emit_signal("selected") + MOUSE_BUTTON_RIGHT: + if resource: + edit() + MOUSE_BUTTON_MIDDLE: + if resource: + clear() + + + func _can_drop_data(p_at_position: Vector2, p_data: Variant) -> bool: + drop_data = false + if typeof(p_data) == TYPE_DICTIONARY: + if p_data.files.size() == 1: + queue_redraw() + drop_data = true + return drop_data + + + func _drop_data(p_at_position: Vector2, p_data: Variant) -> void: + if typeof(p_data) == TYPE_DICTIONARY: + var res: Resource = load(p_data.files[0]) + if res is Texture2D and type == Terrain3DAssets.TYPE_TEXTURE: + var ta := Terrain3DTextureAsset.new() + if resource is Terrain3DTextureAsset: + ta.id = resource.id + ta.set_albedo_texture(res) + set_edited_resource(ta, false) + resource = ta + elif res is Terrain3DTextureAsset and type == Terrain3DAssets.TYPE_TEXTURE: + if resource is Terrain3DTextureAsset: + res.id = resource.id + set_edited_resource(res, false) + elif res is PackedScene and type == Terrain3DAssets.TYPE_MESH: + var ma := Terrain3DMeshAsset.new() + if resource is Terrain3DMeshAsset: + ma.id = resource.id + set_edited_resource(ma, false) + ma.set_scene_file(res) + resource = ma + elif res is Terrain3DMeshAsset and type == Terrain3DAssets.TYPE_MESH: + if resource is Terrain3DMeshAsset: + res.id = resource.id + set_edited_resource(res, false) + emit_signal("selected") + emit_signal("inspected", resource) + + + + func set_edited_resource(p_res: Resource, p_no_signal: bool = true) -> void: + resource = p_res + if resource: + resource.setting_changed.connect(_on_resource_changed) + resource.file_changed.connect(_on_resource_changed) + if resource is Terrain3DMeshAsset: + resource.instancer_setting_changed.connect(_on_resource_changed) + + if button_clear: + button_clear.set_visible(resource != null) + + queue_redraw() + if !p_no_signal: + emit_signal("changed", resource) + + + func _on_resource_changed() -> void: + queue_redraw() + emit_signal("changed", resource) + + + func set_selected(value: bool) -> void: + is_selected = value + queue_redraw() + + + func clear() -> void: + if resource: + set_edited_resource(null, false) + + + func edit() -> void: + emit_signal("selected") + emit_signal("inspected", resource) + + + func enable() -> void: + if resource is Terrain3DMeshAsset: + resource.set_enabled(!resource.is_enabled()) diff --git a/addons/terrain_3d/src/asset_dock.gd.uid b/addons/terrain_3d/src/asset_dock.gd.uid new file mode 100644 index 0000000..8c39f29 --- /dev/null +++ b/addons/terrain_3d/src/asset_dock.gd.uid @@ -0,0 +1 @@ +uid://bgoifepft1hjw diff --git a/addons/terrain_3d/src/asset_dock.tscn b/addons/terrain_3d/src/asset_dock.tscn new file mode 100644 index 0000000..f88c8c0 --- /dev/null +++ b/addons/terrain_3d/src/asset_dock.tscn @@ -0,0 +1,92 @@ +[gd_scene load_steps=2 format=3 uid="uid://dkb6hii5e48m2"] + +[ext_resource type="Script" path="res://addons/terrain_3d/src/asset_dock.gd" id="1_e23pg"] + +[node name="Terrain3D" type="PanelContainer"] +custom_minimum_size = Vector2(256, 95) +offset_right = 766.0 +offset_bottom = 100.0 +script = ExtResource("1_e23pg") + +[node name="Box" type="BoxContainer" parent="."] +layout_mode = 2 +size_flags_vertical = 3 +vertical = true + +[node name="Buttons" type="BoxContainer" parent="Box"] +layout_mode = 2 + +[node name="TexturesBtn" type="Button" parent="Box/Buttons"] +custom_minimum_size = Vector2(80, 30) +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 0 +theme_override_font_sizes/font_size = 16 +toggle_mode = true +button_pressed = true +text = "Textures" + +[node name="MeshesBtn" type="Button" parent="Box/Buttons"] +custom_minimum_size = Vector2(80, 30) +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 0 +theme_override_font_sizes/font_size = 16 +toggle_mode = true +text = "Meshes" + +[node name="PlacementOpt" type="OptionButton" parent="Box/Buttons"] +custom_minimum_size = Vector2(80, 30) +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 0 +selected = 7 +item_count = 9 +popup/item_0/text = "Left_UL" +popup/item_1/text = "Left_BL" +popup/item_1/id = 1 +popup/item_2/text = "Left_UR" +popup/item_2/id = 2 +popup/item_3/text = "Left_BR" +popup/item_3/id = 3 +popup/item_4/text = "Right_UL" +popup/item_4/id = 4 +popup/item_5/text = "Right_BL " +popup/item_5/id = 5 +popup/item_6/text = "Right_UR" +popup/item_6/id = 6 +popup/item_7/text = "Right_BR" +popup/item_7/id = 7 +popup/item_8/text = "Bottom" +popup/item_8/id = 8 + +[node name="SizeSlider" type="HSlider" parent="Box/Buttons"] +custom_minimum_size = Vector2(80, 10) +layout_mode = 2 +size_flags_horizontal = 3 +min_value = 66.0 +max_value = 230.0 +value = 83.0 + +[node name="Floating" type="Button" parent="Box/Buttons"] +layout_mode = 2 +size_flags_horizontal = 0 +size_flags_vertical = 0 +tooltip_text = "Pop this dock out to a floating window." +toggle_mode = true +text = "F" +flat = true + +[node name="Pinned" type="Button" parent="Box/Buttons"] +layout_mode = 2 +size_flags_horizontal = 0 +size_flags_vertical = 0 +tooltip_text = "Make this window \"Always on top\"." +toggle_mode = true +text = "P" +flat = true + +[node name="ScrollContainer" type="ScrollContainer" parent="Box"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 diff --git a/addons/terrain_3d/src/double_slider.gd b/addons/terrain_3d/src/double_slider.gd new file mode 100644 index 0000000..8119456 --- /dev/null +++ b/addons/terrain_3d/src/double_slider.gd @@ -0,0 +1,164 @@ +# Copyright © 2025 Cory Petkovsek, Roope Palmroos, and Contributors. +# DoubleSlider for Terrain3D +# Should work for other UIs +@tool +class_name DoubleSlider +extends Control + +signal value_changed(Vector2) +var label: Label +var suffix: String +var grabbed_handle: int = 0 # -1 left, 0 none, 1 right +var min_value: float = 0.0 +var max_value: float = 100.0 +var step: float = 1.0 +var range := Vector2(0, 100) +var display_scale: float = 1. +var position_x: float = 0. +var minimum_x: float = 60. + + +func _ready() -> void: + # Setup Display Scale + # 0 auto, 1 75%, 2 100%, 3 125%, 4 150%, 5 175%, 6 200%, 7 custom + var es: EditorSettings = EditorInterface.get_editor_settings() + var ds: int = es.get_setting("interface/editor/display_scale") + if ds == 0: + ds = 2 + elif ds == 7: + display_scale = es.get_setting("interface/editor/custom_display_scale") + else: + display_scale = float(ds + 2) * .25 + + update_label() + + +func set_min(p_value: float) -> void: + min_value = p_value + if range.x <= min_value: + range.x = min_value + set_value(range) + update_label() + + +func get_min() -> float: + return min_value + + +func set_max(p_value: float) -> void: + max_value = p_value + if range.y == 0 or range.y >= max_value: + range.y = max_value + set_value(range) + update_label() + + +func get_max() -> float: + return max_value + + +func set_step(p_step: float) -> void: + step = p_step + + +func get_step() -> float: + return step + + +func set_value(p_range: Vector2) -> void: + range.x = clamp(p_range.x, min_value, max_value) + range.y = clamp(p_range.y, min_value, max_value) + if range.y < range.x: + var tmp: float = range.x + range.x = range.y + range.y = tmp + + update_label() + emit_signal("value_changed", Vector2(range.x, range.y)) + queue_redraw() + + +func get_value() -> Vector2: + return range + + +func update_label() -> void: + if label: + label.set_text(str(range.x) + suffix + "/" + str(range.y) + suffix) + if position_x == 0: + position_x = label.position.x + else: + label.position.x = position_x + 5 * display_scale + label.custom_minimum_size.x = minimum_x + 5 * display_scale + + +func _get_handle() -> int: + return 1 + + +func _gui_input(p_event: InputEvent) -> void: + if p_event is InputEventMouseButton: + var button: int = p_event.get_button_index() + if button in [ MOUSE_BUTTON_LEFT, MOUSE_BUTTON_WHEEL_UP, MOUSE_BUTTON_WHEEL_DOWN ]: + if p_event.is_pressed(): + var mid_point = (range.x + range.y) / 2.0 + var xpos: float = p_event.get_position().x * 2.0 + if xpos >= mid_point: + grabbed_handle = 1 + else: + grabbed_handle = -1 + match button: + MOUSE_BUTTON_LEFT: + set_slider(p_event.get_position().x) + MOUSE_BUTTON_WHEEL_DOWN: + set_slider(-1., true) + MOUSE_BUTTON_WHEEL_UP: + set_slider(1., true) + else: + grabbed_handle = 0 + + if p_event is InputEventMouseMotion: + if grabbed_handle != 0: + set_slider(p_event.get_position().x) + + +func set_slider(p_xpos: float, p_relative: bool = false) -> void: + if grabbed_handle == 0: + return + var xpos_step: float = clamp(snappedf((p_xpos / size.x) * max_value, step), min_value, max_value) + if(grabbed_handle < 0): + if p_relative: + range.x += p_xpos + else: + range.x = xpos_step + else: + if p_relative: + range.y += p_xpos + else: + range.y = xpos_step + set_value(range) + + +func _notification(p_what: int) -> void: + if p_what == NOTIFICATION_DRAW: + # Draw background bar + var bg: StyleBox = get_theme_stylebox("slider", "HSlider") + var bg_height: float = bg.get_minimum_size().y + var mid_y: float = (size.y - bg_height) / 2.0 + draw_style_box(bg, Rect2(Vector2(0, mid_y), Vector2(size.x, bg_height))) + + # Draw foreground bar + var handle: Texture2D = get_theme_icon("grabber", "HSlider") + var area: StyleBox = get_theme_stylebox("grabber_area", "HSlider") + var startx: float = (range.x / max_value) * size.x + var endx: float = (range.y / max_value) * size.x + draw_style_box(area, Rect2(Vector2(startx, mid_y), Vector2(endx - startx, bg_height))) + + # Draw handles, slightly in so they don't get on the outside edges + var handle_pos: Vector2 + handle_pos.x = clamp(startx - handle.get_size().x/2, -10, size.x) + handle_pos.y = clamp(endx - handle.get_size().x/2, 0, size.x - 10) + draw_texture(handle, Vector2(handle_pos.x, -mid_y - 10 * (display_scale - 1.))) + draw_texture(handle, Vector2(handle_pos.y, -mid_y - 10 * (display_scale - 1.))) + + update_label() diff --git a/addons/terrain_3d/src/double_slider.gd.uid b/addons/terrain_3d/src/double_slider.gd.uid new file mode 100644 index 0000000..1e73752 --- /dev/null +++ b/addons/terrain_3d/src/double_slider.gd.uid @@ -0,0 +1 @@ +uid://stro0p1oawfb diff --git a/addons/terrain_3d/src/editor_plugin.gd b/addons/terrain_3d/src/editor_plugin.gd new file mode 100644 index 0000000..e1934cc --- /dev/null +++ b/addons/terrain_3d/src/editor_plugin.gd @@ -0,0 +1,397 @@ +# Copyright © 2025 Cory Petkovsek, Roope Palmroos, and Contributors. +# Editor Plugin for Terrain3D +@tool +extends EditorPlugin + + +# Includes +const UI: Script = preload("res://addons/terrain_3d/src/ui.gd") +const RegionGizmo: Script = preload("res://addons/terrain_3d/src/region_gizmo.gd") +const ASSET_DOCK: String = "res://addons/terrain_3d/src/asset_dock.tscn" + +var modifier_ctrl: bool +var modifier_alt: bool +var modifier_shift: bool +var _last_modifiers: int = 0 +var _input_mode: int = 0 # -1: camera move, 0: none, 1: operating +var _use_meta: bool = false + +var terrain: Terrain3D +var _last_terrain: Terrain3D +var nav_region: NavigationRegion3D + +var editor: Terrain3DEditor +var editor_settings: EditorSettings +var ui: Node # Terrain3DUI see Godot #75388 +var asset_dock: PanelContainer +var region_gizmo: RegionGizmo +var current_region_position: Vector2 +var mouse_global_position: Vector3 = Vector3.ZERO +var godot_editor_window: Window # The Godot Editor window + + +func _init() -> void: + if OS.get_name() == "macOS": + _use_meta = true + + # Get the Godot Editor window. Structure is root:Window/EditorNode/Base Control + godot_editor_window = EditorInterface.get_base_control().get_parent().get_parent() + godot_editor_window.focus_entered.connect(_on_godot_focus_entered) + + +func _enter_tree() -> void: + editor = Terrain3DEditor.new() + setup_editor_settings() + ui = UI.new() + ui.plugin = self + add_child(ui) + + region_gizmo = RegionGizmo.new() + + scene_changed.connect(_on_scene_changed) + + asset_dock = load(ASSET_DOCK).instantiate() + asset_dock.initialize(self) + + +func _exit_tree() -> void: + asset_dock.remove_dock(true) + asset_dock.queue_free() + ui.queue_free() + editor.free() + + scene_changed.disconnect(_on_scene_changed) + godot_editor_window.focus_entered.disconnect(_on_godot_focus_entered) + + +func _on_godot_focus_entered() -> void: + _read_input() + ui.update_decal() + + +## EditorPlugin selection function call chain isn't consistent. Here's the map of calls: +## Assume we handle Terrain3D and NavigationRegion3D +# Click Terrain3D: _handles(Terrain3D), _make_visible(true), _edit(Terrain3D) +# Deselect: _make_visible(false), _edit(null) +# Click other node: _handles(OtherNode) +# Click NavRegion3D: _handles(NavReg3D), _make_visible(true), _edit(NavReg3D) +# Click NavRegion3D, Terrain3D: _handles(Terrain3D), _edit(Terrain3D) +# Click Terrain3D, NavRegion3D: _handles(NavReg3D), _edit(NavReg3D) +func _handles(p_object: Object) -> bool: + if p_object is Terrain3D: + return true + elif p_object is NavigationRegion3D and is_instance_valid(_last_terrain): + return true + + # Terrain3DObjects requires access to EditorUndoRedoManager. The only way to make sure it + # always has it, is to pass it in here. _edit is NOT called if the node is cut and pasted. + elif p_object is Terrain3DObjects: + p_object.editor_setup(self) + elif p_object is Node3D and p_object.get_parent() is Terrain3DObjects: + p_object.get_parent().editor_setup(self) + + return false + + +func _make_visible(p_visible: bool, p_redraw: bool = false) -> void: + if p_visible and is_selected(): + ui.set_visible(true) + asset_dock.update_dock() + else: + ui.set_visible(false) + + +func _edit(p_object: Object) -> void: + if !p_object: + _clear() + + if p_object is Terrain3D: + if p_object == terrain: + return + terrain = p_object + _last_terrain = terrain + terrain.set_plugin(self) + terrain.set_editor(editor) + editor.set_terrain(terrain) + region_gizmo.set_node_3d(terrain) + terrain.add_gizmo(region_gizmo) + ui.set_visible(true) + terrain.set_meta("_edit_lock_", true) + + # Get alerted when a new asset list is loaded + if not terrain.assets_changed.is_connected(asset_dock.update_assets): + terrain.assets_changed.connect(asset_dock.update_assets) + asset_dock.update_assets() + # Get alerted when the region map changes + if not terrain.data.region_map_changed.is_connected(update_region_grid): + terrain.data.region_map_changed.connect(update_region_grid) + update_region_grid() + else: + _clear() + + if is_terrain_valid(_last_terrain): + if p_object is NavigationRegion3D: + ui.set_visible(true, true) + nav_region = p_object + else: + nav_region = null + + +func _clear() -> void: + if is_terrain_valid(): + if terrain.data.region_map_changed.is_connected(update_region_grid): + terrain.data.region_map_changed.disconnect(update_region_grid) + + terrain.clear_gizmos() + terrain = null + editor.set_terrain(null) + + ui.clear_picking() + + region_gizmo.clear() + + +func _forward_3d_gui_input(p_viewport_camera: Camera3D, p_event: InputEvent) -> int: + if not is_terrain_valid(): + return AFTER_GUI_INPUT_PASS + + _read_input(p_event) + ui.update_decal() + + ## Setup active camera & viewport + # Always update this for all inputs, as the mouse position can move without + # necessarily being a InputEventMouseMotion object. get_intersection() also + # returns the last frame position, and should be updated more frequently. + + # Snap terrain to current camera + terrain.set_camera(p_viewport_camera) + + # Detect if viewport is set to half_resolution + # Structure is: Node3DEditorViewportContainer/Node3DEditorViewport(4)/SubViewportContainer/SubViewport/Camera3D + var editor_vpc: SubViewportContainer = p_viewport_camera.get_parent().get_parent() + var full_resolution: bool = false if editor_vpc.stretch_shrink == 2 else true + + ## Get mouse location on terrain + # Project 2D mouse position to 3D position and direction + var vp_mouse_pos: Vector2 = editor_vpc.get_local_mouse_position() + var mouse_pos: Vector2 = vp_mouse_pos if full_resolution else vp_mouse_pos / 2 + var camera_pos: Vector3 = p_viewport_camera.project_ray_origin(mouse_pos) + var camera_dir: Vector3 = p_viewport_camera.project_ray_normal(mouse_pos) + + # If region tool, grab mouse position without considering height + if editor.get_tool() == Terrain3DEditor.REGION: + var t = -Vector3(0, 1, 0).dot(camera_pos) / Vector3(0, 1, 0).dot(camera_dir) + mouse_global_position = (camera_pos + t * camera_dir) + else: + #Else look for intersection with terrain + var intersection_point: Vector3 = terrain.get_intersection(camera_pos, camera_dir, true) + if intersection_point.z > 3.4e38 or is_nan(intersection_point.y): # max double or nan + return AFTER_GUI_INPUT_PASS + mouse_global_position = intersection_point + + ## Handle mouse movement + if p_event is InputEventMouseMotion: + + if _input_mode != -1: # Not cam rotation + ## Update region highlight + var region_position: Vector2 = ( Vector2(mouse_global_position.x, mouse_global_position.z) \ + / (terrain.get_region_size() * terrain.get_vertex_spacing()) ).floor() + if current_region_position != region_position: + current_region_position = region_position + update_region_grid() + + if _input_mode > 0 and editor.is_operating(): + # Inject pressure - Relies on C++ set_brush_data() using same dictionary instance + ui.brush_data["mouse_pressure"] = p_event.pressure + + editor.operate(mouse_global_position, p_viewport_camera.rotation.y) + return AFTER_GUI_INPUT_STOP + + return AFTER_GUI_INPUT_PASS + + if p_event is InputEventMouseButton and _input_mode > 0: + if p_event.is_pressed(): + # If picking + if ui.is_picking(): + ui.pick(mouse_global_position) + if not ui.operation_builder or not ui.operation_builder.is_ready(): + return AFTER_GUI_INPUT_STOP + + if modifier_ctrl and editor.get_tool() == Terrain3DEditor.HEIGHT: + var height: float = terrain.data.get_height(mouse_global_position) + ui.brush_data["height"] = height + ui.tool_settings.set_setting("height", height) + + # If adjusting regions + if editor.get_tool() == Terrain3DEditor.REGION: + # Skip regions that already exist or don't + var has_region: bool = terrain.data.has_regionp(mouse_global_position) + var op: int = editor.get_operation() + if ( has_region and op == Terrain3DEditor.ADD) or \ + ( not has_region and op == Terrain3DEditor.SUBTRACT ): + return AFTER_GUI_INPUT_STOP + + # If an automatic operation is ready to go (e.g. gradient) + if ui.operation_builder and ui.operation_builder.is_ready(): + ui.operation_builder.apply_operation(editor, mouse_global_position, p_viewport_camera.rotation.y) + return AFTER_GUI_INPUT_STOP + + # Mouse clicked, start editing + editor.start_operation(mouse_global_position) + editor.operate(mouse_global_position, p_viewport_camera.rotation.y) + return AFTER_GUI_INPUT_STOP + + # _input_apply released, save undo data + elif editor.is_operating(): + editor.stop_operation() + return AFTER_GUI_INPUT_STOP + + return AFTER_GUI_INPUT_PASS + + +func _read_input(p_event: InputEvent = null) -> void: + ## Determine if user is moving camera or applying + if Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT) or \ + p_event is InputEventMouseButton and p_event.is_released() and \ + p_event.get_button_index() == MOUSE_BUTTON_LEFT: + _input_mode = 1 + else: + _input_mode = 0 + + match get_setting("editors/3d/navigation/navigation_scheme", 0): + 2, 1: # Modo, Maya + if Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT) or \ + ( Input.is_key_pressed(KEY_ALT) and Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT) ): + _input_mode = -1 + if p_event is InputEventMouseButton and p_event.is_released() and \ + ( p_event.get_button_index() == MOUSE_BUTTON_RIGHT or \ + ( Input.is_key_pressed(KEY_ALT) and p_event.get_button_index() == MOUSE_BUTTON_LEFT )): + ui.last_rmb_time = Time.get_ticks_msec() + 0, _: # Godot + if Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT) or \ + Input.is_mouse_button_pressed(MOUSE_BUTTON_MIDDLE): + _input_mode = -1 + if p_event is InputEventMouseButton and p_event.is_released() and \ + ( p_event.get_button_index() == MOUSE_BUTTON_RIGHT or \ + p_event.get_button_index() == MOUSE_BUTTON_MIDDLE ): + ui.last_rmb_time = Time.get_ticks_msec() + if _input_mode < 0: + return + + ## Determine modifiers pressed + modifier_shift = Input.is_key_pressed(KEY_SHIFT) + modifier_ctrl = Input.is_key_pressed(KEY_META) if _use_meta else Input.is_key_pressed(KEY_CTRL) + # Keybind enum: Alt,Space,Meta,Capslock + var alt_key: int + match get_setting("terrain3d/config/alt_key_bind", 0): + 3: alt_key = KEY_CAPSLOCK + 2: alt_key = KEY_META + 1: alt_key = KEY_SPACE + 0, _: alt_key = KEY_ALT + modifier_alt = Input.is_key_pressed(alt_key) + + # Return if modifiers haven't changed AND brush_data has them; + # modifiers disappear from brush_data when clicking asset_dock (Why?) + var current_mods: int = int(modifier_shift) | int(modifier_ctrl) << 1 | int(modifier_alt) << 2 + if _last_modifiers == current_mods and ui.brush_data.has("modifier_shift"): + return + + _last_modifiers = current_mods + ui.brush_data["modifier_shift"] = modifier_shift + ui.brush_data["modifier_ctrl"] = modifier_ctrl + ui.brush_data["modifier_alt"] = modifier_alt + ui.update_modifiers() + + +func update_region_grid() -> void: + if not region_gizmo: + return + region_gizmo.set_hidden(not ui.visible) + + if is_terrain_valid(): + region_gizmo.show_rect = editor.get_tool() == Terrain3DEditor.REGION + region_gizmo.use_secondary_color = editor.get_operation() == Terrain3DEditor.SUBTRACT + region_gizmo.region_position = current_region_position + region_gizmo.region_size = terrain.get_region_size() * terrain.get_vertex_spacing() + region_gizmo.grid = terrain.get_data().get_region_locations() + + terrain.update_gizmos() + return + + region_gizmo.show_rect = false + region_gizmo.region_size = 1024 + region_gizmo.grid = [Vector2i.ZERO] + + +func _on_scene_changed(scene_root: Node) -> void: + if not scene_root: + return + + for node in scene_root.find_children("", "Terrain3DObjects"): + node.editor_setup(self) + + asset_dock.update_assets() + await get_tree().create_timer(2).timeout + asset_dock.update_thumbnails() + + +func is_terrain_valid(p_terrain: Terrain3D = null) -> bool: + var t: Terrain3D + if p_terrain: + t = p_terrain + else: + t = terrain + if is_instance_valid(t) and t.is_inside_tree() and t.data: + return true + return false + + +func is_selected() -> bool: + var selected: Array[Node] = EditorInterface.get_selection().get_selected_nodes() + for node in selected: + if ( is_instance_valid(_last_terrain) and node.get_instance_id() == _last_terrain.get_instance_id() ) or \ + node is Terrain3D: + return true + return false + + +func select_terrain() -> void: + if is_instance_valid(_last_terrain) and is_terrain_valid(_last_terrain) and not is_selected(): + var es: EditorSelection = EditorInterface.get_selection() + es.clear() + es.add_node(_last_terrain) + + +## Editor Settings + + +func setup_editor_settings() -> void: + editor_settings = EditorInterface.get_editor_settings() + if not editor_settings.has_setting("terrain3d/config/alt_key_bind"): + editor_settings.set("terrain3d/config/alt_key_bind", 0) + var property_info = { + "name": "terrain3d/config/alt_key_bind", + "type": TYPE_INT, + "hint": PROPERTY_HINT_ENUM, + "hint_string": "Alt,Space,Meta,Capslock" + } + editor_settings.add_property_info(property_info) + + +func set_setting(p_str: String, p_value: Variant) -> void: + editor_settings.set_setting(p_str, p_value) + + +func get_setting(p_str: String, p_default: Variant) -> Variant: + if editor_settings.has_setting(p_str): + return editor_settings.get_setting(p_str) + else: + return p_default + + +func has_setting(p_str: String) -> bool: + return editor_settings.has_setting(p_str) + + +func erase_setting(p_str: String) -> void: + editor_settings.erase(p_str) diff --git a/addons/terrain_3d/src/editor_plugin.gd.uid b/addons/terrain_3d/src/editor_plugin.gd.uid new file mode 100644 index 0000000..fb3e1ea --- /dev/null +++ b/addons/terrain_3d/src/editor_plugin.gd.uid @@ -0,0 +1 @@ +uid://bsgxo1qywjdf3 diff --git a/addons/terrain_3d/src/gradient_operation_builder.gd b/addons/terrain_3d/src/gradient_operation_builder.gd new file mode 100644 index 0000000..8bbafd4 --- /dev/null +++ b/addons/terrain_3d/src/gradient_operation_builder.gd @@ -0,0 +1,56 @@ +# Copyright © 2025 Cory Petkovsek, Roope Palmroos, and Contributors. +# Gradient Operation Builder for Terrain3D +extends "res://addons/terrain_3d/src/operation_builder.gd" + + +const MultiPicker: Script = preload("res://addons/terrain_3d/src/multi_picker.gd") + + +func _get_point_picker() -> MultiPicker: + return tool_settings.settings["gradient_points"] + + +func _get_brush_size() -> float: + return tool_settings.get_setting("size") + + +func _is_drawable() -> bool: + return tool_settings.get_setting("drawable") + + +func is_picking() -> bool: + return not _get_point_picker().all_points_selected() + + +func pick(p_global_position: Vector3, p_terrain: Terrain3D) -> void: + if not _get_point_picker().all_points_selected(): + _get_point_picker().add_point(p_global_position) + + +func is_ready() -> bool: + return _get_point_picker().all_points_selected() and not _is_drawable() + + +func apply_operation(p_editor: Terrain3DEditor, p_global_position: Vector3, p_camera_direction: float) -> void: + var points: PackedVector3Array = _get_point_picker().get_points() + assert(points.size() == 2) + assert(not _is_drawable()) + + var brush_size: float = _get_brush_size() + assert(brush_size > 0.0) + + var start: Vector3 = points[0] + var end: Vector3 = points[1] + + p_editor.start_operation(start) + + var dir: Vector3 = (end - start).normalized() + + var pos: Vector3 = start + while dir.dot(end - pos) > 0.0: + p_editor.operate(pos, p_camera_direction) + pos += dir * brush_size * 0.2 + + p_editor.stop_operation() + + _get_point_picker().clear() diff --git a/addons/terrain_3d/src/gradient_operation_builder.gd.uid b/addons/terrain_3d/src/gradient_operation_builder.gd.uid new file mode 100644 index 0000000..e747d25 --- /dev/null +++ b/addons/terrain_3d/src/gradient_operation_builder.gd.uid @@ -0,0 +1 @@ +uid://def7sych6dp8b diff --git a/addons/terrain_3d/src/multi_picker.gd b/addons/terrain_3d/src/multi_picker.gd new file mode 100644 index 0000000..717ca7b --- /dev/null +++ b/addons/terrain_3d/src/multi_picker.gd @@ -0,0 +1,88 @@ +# Copyright © 2025 Cory Petkovsek, Roope Palmroos, and Contributors. +# Multipicker for Terrain3D +extends HBoxContainer + + +signal pressed +signal value_changed + + +const ICON_PICKER_CHECKED: String = "res://addons/terrain_3d/icons/picker_checked.svg" +const MAX_POINTS: int = 2 + + +var icon_picker: Texture2D +var icon_picker_checked: Texture2D +var points: PackedVector3Array +var picking_index: int = -1 + + +func _enter_tree() -> void: + icon_picker = get_theme_icon("ColorPick", "EditorIcons") + icon_picker_checked = load(ICON_PICKER_CHECKED) + + points.resize(MAX_POINTS) + + for i in range(MAX_POINTS): + var button := Button.new() + button.icon = icon_picker + button.tooltip_text = "Pick point on the Terrain" + button.set_meta(&"point_index", i) + button.pressed.connect(_on_button_pressed.bind(i)) + add_child(button) + + _update_buttons() + + +func _on_button_pressed(button_index: int) -> void: + points[button_index] = Vector3.ZERO + picking_index = button_index + _update_buttons() + pressed.emit() + + +func _update_buttons() -> void: + for child in get_children(): + if child is Button: + _update_button(child) + + +func _update_button(button: Button) -> void: + var index: int = button.get_meta(&"point_index") + + if points[index] != Vector3.ZERO: + button.icon = icon_picker_checked + else: + button.icon = icon_picker + + +func clear() -> void: + points.fill(Vector3.ZERO) + _update_buttons() + value_changed.emit() + + +func all_points_selected() -> bool: + return points.count(Vector3.ZERO) == 0 + + +func add_point(p_value: Vector3) -> void: + if points.has(p_value): + return + + # If manually selecting a point individually + if picking_index != -1: + points[picking_index] = p_value + picking_index = -1 + else: + # Else picking a sequence of points (non-drawable) + for i in range(MAX_POINTS): + if points[i] == Vector3.ZERO: + points[i] = p_value + break + _update_buttons() + value_changed.emit() + + +func get_points() -> PackedVector3Array: + return points diff --git a/addons/terrain_3d/src/multi_picker.gd.uid b/addons/terrain_3d/src/multi_picker.gd.uid new file mode 100644 index 0000000..6db5cb7 --- /dev/null +++ b/addons/terrain_3d/src/multi_picker.gd.uid @@ -0,0 +1 @@ +uid://dvdtoa32h6xdn diff --git a/addons/terrain_3d/src/operation_builder.gd b/addons/terrain_3d/src/operation_builder.gd new file mode 100644 index 0000000..2a558be --- /dev/null +++ b/addons/terrain_3d/src/operation_builder.gd @@ -0,0 +1,25 @@ +# Copyright © 2025 Cory Petkovsek, Roope Palmroos, and Contributors. +# Operation Builder for Terrain3D +extends RefCounted + + +const ToolSettings: Script = preload("res://addons/terrain_3d/src/tool_settings.gd") + + +var tool_settings: ToolSettings + + +func is_picking() -> bool: + return false + + +func pick(p_global_position: Vector3, p_terrain: Terrain3D) -> void: + pass + + +func is_ready() -> bool: + return false + + +func apply_operation(editor: Terrain3DEditor, p_global_position: Vector3, p_camera_direction: float) -> void: + pass diff --git a/addons/terrain_3d/src/operation_builder.gd.uid b/addons/terrain_3d/src/operation_builder.gd.uid new file mode 100644 index 0000000..f14e1fe --- /dev/null +++ b/addons/terrain_3d/src/operation_builder.gd.uid @@ -0,0 +1 @@ +uid://bu5cm0eh052rm diff --git a/addons/terrain_3d/src/region_gizmo.gd b/addons/terrain_3d/src/region_gizmo.gd new file mode 100644 index 0000000..c74c8f5 --- /dev/null +++ b/addons/terrain_3d/src/region_gizmo.gd @@ -0,0 +1,68 @@ +# Copyright © 2025 Cory Petkovsek, Roope Palmroos, and Contributors. +# Editor Region Gizmos for Terrain3D +extends EditorNode3DGizmo + +var material: StandardMaterial3D +var selection_material: StandardMaterial3D +var region_position: Vector2 +var region_size: float +var grid: Array[Vector2i] +var use_secondary_color: bool = false +var show_rect: bool = true + +var main_color: Color = Color.GREEN_YELLOW +var secondary_color: Color = Color.RED +var grid_color: Color = Color.WHITE +var border_color: Color = Color.BLUE + + +func _init() -> void: + material = StandardMaterial3D.new() + material.set_flag(BaseMaterial3D.FLAG_DISABLE_DEPTH_TEST, true) + material.set_flag(BaseMaterial3D.FLAG_ALBEDO_FROM_VERTEX_COLOR, true) + material.set_shading_mode(BaseMaterial3D.SHADING_MODE_UNSHADED) + material.set_albedo(Color.WHITE) + + selection_material = material.duplicate() + selection_material.set_render_priority(0) + + +func _redraw() -> void: + clear() + + var rect_position = region_position * region_size + + if show_rect: + var modulate: Color = main_color if !use_secondary_color else secondary_color + if abs(region_position.x) > Terrain3DData.REGION_MAP_SIZE*.5 or abs(region_position.y) > Terrain3DData.REGION_MAP_SIZE*.5: + modulate = Color.GRAY + draw_rect(Vector2(region_size,region_size)*.5 + rect_position, region_size, selection_material, modulate) + + for pos in grid: + var grid_tile_position = Vector2(pos) * region_size + if show_rect and grid_tile_position == rect_position: + # Skip this one, otherwise focused region borders are not always visible due to draw order + continue + + draw_rect(Vector2(region_size,region_size)*.5 + grid_tile_position, region_size, material, grid_color) + + draw_rect(Vector2.ZERO, region_size * Terrain3DData.REGION_MAP_SIZE, material, border_color) + + +func draw_rect(p_pos: Vector2, p_size: float, p_material: StandardMaterial3D, p_modulate: Color) -> void: + var lines: PackedVector3Array = [ + Vector3(-1, 0, -1), + Vector3(-1, 0, 1), + Vector3(1, 0, 1), + Vector3(1, 0, -1), + Vector3(-1, 0, 1), + Vector3(1, 0, 1), + Vector3(1, 0, -1), + Vector3(-1, 0, -1), + ] + + for i in lines.size(): + lines[i] = ((lines[i] / 2.0) * p_size) + Vector3(p_pos.x, 0, p_pos.y) + + add_lines(lines, p_material, false, p_modulate) + diff --git a/addons/terrain_3d/src/region_gizmo.gd.uid b/addons/terrain_3d/src/region_gizmo.gd.uid new file mode 100644 index 0000000..346a404 --- /dev/null +++ b/addons/terrain_3d/src/region_gizmo.gd.uid @@ -0,0 +1 @@ +uid://bh6qwe1ok4cx3 diff --git a/addons/terrain_3d/src/tool_settings.gd b/addons/terrain_3d/src/tool_settings.gd new file mode 100644 index 0000000..6145c9b --- /dev/null +++ b/addons/terrain_3d/src/tool_settings.gd @@ -0,0 +1,692 @@ +# Copyright © 2025 Cory Petkovsek, Roope Palmroos, and Contributors. +# Tool settings bar for Terrain3D +extends PanelContainer + +signal picking(type, callback) +signal setting_changed + +enum Layout { + HORIZONTAL, + VERTICAL, + GRID, +} + +enum SettingType { + CHECKBOX, + COLOR_SELECT, + DOUBLE_SLIDER, + OPTION, + PICKER, + MULTI_PICKER, + SLIDER, + LABEL, + TYPE_MAX, +} + +const MultiPicker: Script = preload("res://addons/terrain_3d/src/multi_picker.gd") +const DEFAULT_BRUSH: String = "circle0.exr" +const BRUSH_PATH: String = "res://addons/terrain_3d/brushes" +const ES_TOOL_SETTINGS: String = "terrain3d/tool_settings/" + +# Add settings flags +const NONE: int = 0x0 +const ALLOW_LARGER: int = 0x1 +const ALLOW_SMALLER: int = 0x2 +const ALLOW_OUT_OF_BOUNDS: int = 0x3 # LARGER|SMALLER +const NO_LABEL: int = 0x4 +const ADD_SEPARATOR: int = 0x8 # Add a vertical line before this entry +const ADD_SPACER: int = 0x10 # Add a space before this entry +const NO_SAVE: int = 0x20 # Don't save this in EditorSettings + +var plugin: EditorPlugin # Actually Terrain3DEditorPlugin, but Godot still has CRC errors +var brush_preview_material: ShaderMaterial +var select_brush_button: Button +var selected_brush_imgs: Array +var main_list: HFlowContainer +var advanced_list: VBoxContainer +var height_list: VBoxContainer +var scale_list: VBoxContainer +var rotation_list: VBoxContainer +var color_list: VBoxContainer +var settings: Dictionary = {} + + +func _ready() -> void: + # Remove old editor settings + for setting in ["lift_floor", "flatten_peaks", "lift_flatten", "automatic_regions"]: + plugin.erase_setting(ES_TOOL_SETTINGS + setting) + + # Setup buttons + main_list = HFlowContainer.new() + add_child(main_list, true) + + add_brushes(main_list) + + add_setting({ "name":"instructions", "label":"Click the terrain to add a region. CTRL+Click to remove. Or select another tool on the left.", + "type":SettingType.LABEL, "list":main_list, "flags":NO_LABEL|NO_SAVE }) + + add_setting({ "name":"size", "type":SettingType.SLIDER, "list":main_list, "default":20, "unit":"m", + "range":Vector3(0.1, 200, 1), "flags":ALLOW_LARGER|ADD_SPACER }) + + add_setting({ "name":"strength", "type":SettingType.SLIDER, "list":main_list, "default":33, + "unit":"%", "range":Vector3(1, 100, 1), "flags":ALLOW_LARGER }) + + add_setting({ "name":"height", "type":SettingType.SLIDER, "list":main_list, "default":20, + "unit":"m", "range":Vector3(-500, 500, 0.1), "flags":ALLOW_OUT_OF_BOUNDS }) + add_setting({ "name":"height_picker", "type":SettingType.PICKER, "list":main_list, + "default":Terrain3DEditor.HEIGHT, "flags":NO_LABEL }) + + add_setting({ "name":"color", "type":SettingType.COLOR_SELECT, "list":main_list, + "default":Color.WHITE, "flags":ADD_SEPARATOR }) + add_setting({ "name":"color_picker", "type":SettingType.PICKER, "list":main_list, + "default":Terrain3DEditor.COLOR, "flags":NO_LABEL }) + + add_setting({ "name":"roughness", "type":SettingType.SLIDER, "list":main_list, "default":-65, + "unit":"%", "range":Vector3(-100, 100, 1), "flags":ADD_SEPARATOR }) + add_setting({ "name":"roughness_picker", "type":SettingType.PICKER, "list":main_list, + "default":Terrain3DEditor.ROUGHNESS, "flags":NO_LABEL }) + + add_setting({ "name":"enable_texture", "label":"Texture", "type":SettingType.CHECKBOX, + "list":main_list, "default":true, "flags":ADD_SEPARATOR }) + + add_setting({ "name":"texture_filter", "label":"Texture Filter", "type":SettingType.CHECKBOX, + "list":main_list, "default":false, "flags":ADD_SEPARATOR }) + + add_setting({ "name":"margin", "type":SettingType.SLIDER, "list":main_list, "default":0, + "unit":"", "range":Vector3(-50, 50, 1), "flags":ALLOW_OUT_OF_BOUNDS }) + + # Slope painting filter + add_setting({ "name":"slope", "type":SettingType.DOUBLE_SLIDER, "list":main_list, "default":Vector2(0, 90), + "unit":"°", "range":Vector3(0, 90, 1), "flags":ADD_SEPARATOR }) + + add_setting({ "name":"enable_angle", "label":"Angle", "type":SettingType.CHECKBOX, + "list":main_list, "default":true, "flags":ADD_SEPARATOR }) + add_setting({ "name":"angle", "type":SettingType.SLIDER, "list":main_list, "default":0, + "unit":"%", "range":Vector3(0, 337.5, 22.5), "flags":NO_LABEL }) + add_setting({ "name":"angle_picker", "type":SettingType.PICKER, "list":main_list, + "default":Terrain3DEditor.ANGLE, "flags":NO_LABEL }) + add_setting({ "name":"dynamic_angle", "label":"Dynamic", "type":SettingType.CHECKBOX, + "list":main_list, "default":false, "flags":ADD_SPACER }) + + add_setting({ "name":"enable_scale", "label":"Scale", "type":SettingType.CHECKBOX, + "list":main_list, "default":true, "flags":ADD_SEPARATOR }) + add_setting({ "name":"scale", "label":"±", "type":SettingType.SLIDER, "list":main_list, "default":0, + "unit":"%", "range":Vector3(-60, 80, 20), "flags":NO_LABEL }) + add_setting({ "name":"scale_picker", "type":SettingType.PICKER, "list":main_list, + "default":Terrain3DEditor.SCALE, "flags":NO_LABEL }) + + ## Slope sculpting brush + add_setting({ "name":"gradient_points", "type":SettingType.MULTI_PICKER, "label":"Points", + "list":main_list, "default":Terrain3DEditor.SCULPT, "flags":ADD_SEPARATOR }) + add_setting({ "name":"drawable", "type":SettingType.CHECKBOX, "list":main_list, "default":false, + "flags":ADD_SEPARATOR }) + settings["drawable"].toggled.connect(_on_drawable_toggled) + + ## Instancer + height_list = create_submenu(main_list, "Height", Layout.VERTICAL) + add_setting({ "name":"height_offset", "type":SettingType.SLIDER, "list":height_list, "default":0, + "unit":"m", "range":Vector3(-10, 10, 0.05), "flags":ALLOW_OUT_OF_BOUNDS }) + add_setting({ "name":"random_height", "label":"Random Height ±", "type":SettingType.SLIDER, + "list":height_list, "default":0, "unit":"m", "range":Vector3(0, 10, 0.05), + "flags":ALLOW_OUT_OF_BOUNDS }) + + scale_list = create_submenu(main_list, "Scale", Layout.VERTICAL) + add_setting({ "name":"fixed_scale", "type":SettingType.SLIDER, "list":scale_list, "default":100, + "unit":"%", "range":Vector3(1, 1000, 1), "flags":ALLOW_OUT_OF_BOUNDS }) + add_setting({ "name":"random_scale", "label":"Random Scale ±", "type":SettingType.SLIDER, "list":scale_list, + "default":20, "unit":"%", "range":Vector3(0, 99, 1), "flags":ALLOW_OUT_OF_BOUNDS }) + + rotation_list = create_submenu(main_list, "Rotation", Layout.VERTICAL) + add_setting({ "name":"fixed_spin", "label":"Fixed Spin (Around Y)", "type":SettingType.SLIDER, "list":rotation_list, + "default":0, "unit":"°", "range":Vector3(0, 360, 1) }) + add_setting({ "name":"random_spin", "type":SettingType.SLIDER, "list":rotation_list, "default":360, + "unit":"°", "range":Vector3(0, 360, 1) }) + add_setting({ "name":"fixed_tilt", "label":"Fixed Tilt", "type":SettingType.SLIDER, "list":rotation_list, + "default":0, "unit":"°", "range":Vector3(-85, 85, 1), "flags":ALLOW_OUT_OF_BOUNDS }) + add_setting({ "name":"random_tilt", "label":"Random Tilt ±", "type":SettingType.SLIDER, "list":rotation_list, + "default":10, "unit":"°", "range":Vector3(0, 85, 1), "flags":ALLOW_OUT_OF_BOUNDS }) + add_setting({ "name":"align_to_normal", "type":SettingType.CHECKBOX, "list":rotation_list, "default":false }) + + color_list = create_submenu(main_list, "Color", Layout.VERTICAL) + add_setting({ "name":"vertex_color", "type":SettingType.COLOR_SELECT, "list":color_list, + "default":Color.WHITE }) + add_setting({ "name":"random_hue", "label":"Random Hue Shift ±", "type":SettingType.SLIDER, + "list":color_list, "default":0, "unit":"°", "range":Vector3(0, 360, 1) }) + add_setting({ "name":"random_darken", "type":SettingType.SLIDER, "list":color_list, "default":50, + "unit":"%", "range":Vector3(0, 100, 1) }) + #add_setting({ "name":"blend_mode", "type":SettingType.OPTION, "list":color_list, "default":0, + #"range":Vector3(0, 3, 1) }) + + if DisplayServer.is_touchscreen_available(): + add_setting({ "name":"remove", "label":"Invert", "type":SettingType.CHECKBOX, "list":main_list, "default":false, "flags":ADD_SEPARATOR }) + + var spacer: Control = Control.new() + spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL + main_list.add_child(spacer, true) + + ## Advanced Settings Menu + advanced_list = create_submenu(main_list, "", Layout.VERTICAL, false) + add_setting({ "name":"auto_regions", "label":"Add regions while sculpting", "type":SettingType.CHECKBOX, + "list":advanced_list, "default":true }) + add_setting({ "name":"align_to_view", "type":SettingType.CHECKBOX, "list":advanced_list, + "default":true }) + add_setting({ "name":"show_cursor_while_painting", "type":SettingType.CHECKBOX, "list":advanced_list, + "default":true }) + advanced_list.add_child(HSeparator.new(), true) + add_setting({ "name":"gamma", "type":SettingType.SLIDER, "list":advanced_list, "default":1.0, + "unit":"γ", "range":Vector3(0.1, 2.0, 0.01) }) + add_setting({ "name":"jitter", "type":SettingType.SLIDER, "list":advanced_list, "default":50, + "unit":"%", "range":Vector3(0, 100, 1) }) + add_setting({ "name":"crosshair_threshold", "type":SettingType.SLIDER, "list":advanced_list, "default":16., + "unit":"m", "range":Vector3(0, 200, 1) }) + + +func create_submenu(p_parent: Control, p_button_name: String, p_layout: Layout, p_hover_pop: bool = true) -> Container: + var menu_button: Button = Button.new() + if p_button_name.is_empty(): + menu_button.icon = get_theme_icon("GuiTabMenuHl", "EditorIcons") + else: + menu_button.set_text(p_button_name) + menu_button.set_toggle_mode(true) + menu_button.set_v_size_flags(SIZE_SHRINK_CENTER) + menu_button.toggled.connect(_on_show_submenu.bind(menu_button)) + + var submenu: PopupPanel = PopupPanel.new() + submenu.popup_hide.connect(menu_button.set_pressed.bind(false)) + var panel_style: StyleBox = get_theme_stylebox("panel", "PopupMenu").duplicate() + panel_style.set_content_margin_all(10) + submenu.set("theme_override_styles/panel", panel_style) + submenu.add_to_group("terrain3d_submenus") + + # Pop up menu on hover, hide on exit + if p_hover_pop: + menu_button.mouse_entered.connect(_on_show_submenu.bind(true, menu_button)) + + submenu.mouse_entered.connect(func(): submenu.set_meta("mouse_entered", true)) + + submenu.mouse_exited.connect(func(): + # On mouse_exit, hide popup unless LineEdit focused + var focused_element: Control = submenu.gui_get_focus_owner() + if not focused_element is LineEdit: + _on_show_submenu(false, menu_button) + submenu.set_meta("mouse_entered", false) + return + + focused_element.focus_exited.connect(func(): + # Close submenu once lineedit loses focus + if not submenu.get_meta("mouse_entered"): + _on_show_submenu(false, menu_button) + submenu.set_meta("mouse_entered", false) + ) + ) + + var sublist: Container + match(p_layout): + Layout.GRID: + sublist = GridContainer.new() + Layout.VERTICAL: + sublist = VBoxContainer.new() + Layout.HORIZONTAL, _: + sublist = HBoxContainer.new() + + p_parent.add_child(menu_button, true) + menu_button.add_child(submenu, true) + submenu.add_child(sublist, true) + + return sublist + + +func _on_show_submenu(p_toggled: bool, p_button: Button) -> void: + # Don't show if mouse already down (from painting) + if p_toggled and Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT): + return + + # Hide menu if mouse is not in button or panel + var button_rect: Rect2 = Rect2(p_button.get_screen_transform().origin, p_button.get_global_rect().size) + var in_button: bool = button_rect.has_point(DisplayServer.mouse_get_position()) + var popup: PopupPanel = p_button.get_child(0) + var popup_rect: Rect2 = Rect2(popup.position, popup.size) + var in_popup: bool = popup_rect.has_point(DisplayServer.mouse_get_position()) + if not p_toggled and ( in_button or in_popup ): + return + + # Hide all submenus before possibly enabling the current one + get_tree().call_group("terrain3d_submenus", "set_visible", false) + popup.set_visible(p_toggled) + var popup_pos: Vector2 = p_button.get_screen_transform().origin + popup_pos.y -= popup.size.y + if popup.get_child_count()>0 and popup.get_child(0) == advanced_list: + popup_pos.x -= popup.size.x - p_button.size.x + popup.set_position(popup_pos) + + +func add_brushes(p_parent: Control) -> void: + var brush_list: GridContainer = create_submenu(p_parent, "Brush", Layout.GRID) + brush_list.name = "BrushList" + + var brush_button_group: ButtonGroup = ButtonGroup.new() + brush_button_group.pressed.connect(_on_setting_changed) + var default_brush_btn: Button + + var dir: DirAccess = DirAccess.open(BRUSH_PATH) + if dir: + dir.list_dir_begin() + var file_name = dir.get_next() + while file_name != "": + if !dir.current_is_dir() and file_name.ends_with(".exr"): + var img: Image = Image.load_from_file(BRUSH_PATH + "/" + file_name) + var thumbimg: Image = img.duplicate() + img.convert(Image.FORMAT_RF) + + if thumbimg.get_width() != 100 and thumbimg.get_height() != 100: + thumbimg.resize(100, 100, Image.INTERPOLATE_CUBIC) + thumbimg = Terrain3DUtil.black_to_alpha(thumbimg) + thumbimg.convert(Image.FORMAT_LA8) + var thumbtex: ImageTexture = ImageTexture.create_from_image(thumbimg) + + var brush_btn: Button = Button.new() + brush_btn.set_custom_minimum_size(Vector2.ONE * 100) + brush_btn.set_button_icon(thumbtex) + brush_btn.set_meta("image", img) + brush_btn.set_expand_icon(true) + brush_btn.set_material(_get_brush_preview_material()) + brush_btn.set_toggle_mode(true) + brush_btn.set_button_group(brush_button_group) + brush_btn.mouse_entered.connect(_on_brush_hover.bind(true, brush_btn)) + brush_btn.mouse_exited.connect(_on_brush_hover.bind(false, brush_btn)) + brush_list.add_child(brush_btn, true) + if file_name == DEFAULT_BRUSH: + default_brush_btn = brush_btn + + var lbl: Label = Label.new() + brush_btn.name = file_name.get_basename().to_pascal_case() + brush_btn.add_child(lbl, true) + lbl.text = brush_btn.name + lbl.visible = false + lbl.position.y = 70 + lbl.add_theme_color_override("font_shadow_color", Color.BLACK) + lbl.add_theme_constant_override("shadow_offset_x", 1) + lbl.add_theme_constant_override("shadow_offset_y", 1) + lbl.add_theme_font_size_override("font_size", 16) + + file_name = dir.get_next() + + brush_list.columns = sqrt(brush_list.get_child_count()) + 2 + + if not default_brush_btn: + default_brush_btn = brush_button_group.get_buttons()[0] + default_brush_btn.set_pressed(true) + _generate_brush_texture(default_brush_btn) + + settings["brush"] = brush_button_group + + select_brush_button = brush_list.get_parent().get_parent() + # Optionally erase the main brush button text and replace it with the texture + select_brush_button.set_text("") + select_brush_button.set_button_icon(default_brush_btn.get_button_icon()) + select_brush_button.set_custom_minimum_size(Vector2.ONE * 36) + select_brush_button.set_icon_alignment(HORIZONTAL_ALIGNMENT_CENTER) + select_brush_button.set_expand_icon(true) + + +func _on_brush_hover(p_hovering: bool, p_button: Button) -> void: + if p_button.get_child_count() > 0: + var child = p_button.get_child(0) + if child is Label: + if p_hovering: + child.visible = true + else: + child.visible = false + + +func _on_pick(p_type: Terrain3DEditor.Tool) -> void: + emit_signal("picking", p_type, _on_picked) + + +func _on_picked(p_type: Terrain3DEditor.Tool, p_color: Color, p_global_position: Vector3) -> void: + match p_type: + Terrain3DEditor.HEIGHT: + settings["height"].value = p_color.r if not is_nan(p_color.r) else 0 + Terrain3DEditor.COLOR: + settings["color"].color = p_color if not is_nan(p_color.r) else Color.WHITE + Terrain3DEditor.ROUGHNESS: + # 200... -.5 converts 0,1 to -100,100 + settings["roughness"].value = round(200 * (p_color.a - 0.5)) if not is_nan(p_color.r) else 0.499 + Terrain3DEditor.ANGLE: + settings["angle"].value = p_color.r + Terrain3DEditor.SCALE: + settings["scale"].value = p_color.r + _on_setting_changed() + + +func _on_point_pick(p_type: Terrain3DEditor.Tool, p_name: String) -> void: + assert(p_type == Terrain3DEditor.SCULPT) + emit_signal("picking", p_type, _on_point_picked.bind(p_name)) + + +func _on_point_picked(p_type: Terrain3DEditor.Tool, p_color: Color, p_global_position: Vector3, p_name: String) -> void: + assert(p_type == Terrain3DEditor.SCULPT) + var point: Vector3 = p_global_position + point.y = p_color.r + settings[p_name].add_point(point) + _on_setting_changed() + + +func add_setting(p_args: Dictionary) -> void: + var p_name: StringName = p_args.get("name", "") + var p_label: String = p_args.get("label", "") # Optional replacement for name + var p_type: SettingType = p_args.get("type", SettingType.TYPE_MAX) + var p_list: Control = p_args.get("list") + var p_default: Variant = p_args.get("default") + var p_suffix: String = p_args.get("unit", "") + var p_range: Vector3 = p_args.get("range", Vector3(0, 0, 1)) + var p_minimum: float = p_range.x + var p_maximum: float = p_range.y + var p_step: float = p_range.z + var p_flags: int = p_args.get("flags", NONE) + + if p_name.is_empty() or p_type == SettingType.TYPE_MAX: + return + + var container: HBoxContainer = HBoxContainer.new() + container.set_v_size_flags(SIZE_EXPAND_FILL) + var control: Control # Houses the setting to be saved + var pending_children: Array[Control] + + match p_type: + SettingType.LABEL: + var label := Label.new() + label.set_text(p_label) + pending_children.push_back(label) + control = label + + SettingType.CHECKBOX: + var checkbox := CheckBox.new() + if !(p_flags & NO_SAVE): + checkbox.set_pressed_no_signal(plugin.get_setting(ES_TOOL_SETTINGS + p_name, p_default)) + checkbox.toggled.connect( ( + func(value, path): + plugin.set_setting(path, value) + ).bind(ES_TOOL_SETTINGS + p_name) ) + else: + checkbox.set_pressed_no_signal(p_default) + checkbox.pressed.connect(_on_setting_changed) + pending_children.push_back(checkbox) + control = checkbox + + SettingType.COLOR_SELECT: + var picker := ColorPickerButton.new() + picker.set_custom_minimum_size(Vector2(100, 25)) + picker.edit_alpha = false + picker.get_picker().set_color_mode(ColorPicker.MODE_HSV) + if !(p_flags & NO_SAVE): + picker.set_pick_color(plugin.get_setting(ES_TOOL_SETTINGS + p_name, p_default)) + picker.color_changed.connect( ( + func(value, path): + plugin.set_setting(path, value) + ).bind(ES_TOOL_SETTINGS + p_name) ) + else: + picker.set_pick_color(p_default) + picker.color_changed.connect(_on_setting_changed) + pending_children.push_back(picker) + control = picker + + SettingType.PICKER: + var button := Button.new() + button.set_v_size_flags(SIZE_SHRINK_CENTER) + button.icon = get_theme_icon("ColorPick", "EditorIcons") + button.tooltip_text = "Pick value from the Terrain" + button.pressed.connect(_on_pick.bind(p_default)) + pending_children.push_back(button) + control = button + + SettingType.MULTI_PICKER: + var multi_picker: HBoxContainer = MultiPicker.new() + multi_picker.pressed.connect(_on_point_pick.bind(p_default, p_name)) + multi_picker.value_changed.connect(_on_setting_changed) + pending_children.push_back(multi_picker) + control = multi_picker + + SettingType.OPTION: + var option := OptionButton.new() + for i in int(p_maximum): + option.add_item("a", i) + option.selected = p_minimum + option.item_selected.connect(_on_setting_changed) + pending_children.push_back(option) + control = option + + SettingType.SLIDER, SettingType.DOUBLE_SLIDER: + var slider: Control + if p_type == SettingType.SLIDER: + # Create an editable value box + var spin_slider := EditorSpinSlider.new() + spin_slider.set_flat(false) + spin_slider.set_hide_slider(true) + spin_slider.value_changed.connect(_on_setting_changed) + spin_slider.set_max(p_maximum) + spin_slider.set_min(p_minimum) + spin_slider.set_step(p_step) + spin_slider.set_suffix(p_suffix) + spin_slider.set_v_size_flags(SIZE_SHRINK_CENTER) + spin_slider.set_custom_minimum_size(Vector2(65, 0)) + + # Create horizontal slider linked to the above box + slider = HSlider.new() + slider.share(spin_slider) + if p_flags & ALLOW_LARGER: + slider.set_allow_greater(true) + if p_flags & ALLOW_SMALLER: + slider.set_allow_lesser(true) + + pending_children.push_back(slider) + pending_children.push_back(spin_slider) + control = spin_slider + + else: # DOUBLE_SLIDER + var label := Label.new() + label.set_custom_minimum_size(Vector2(60, 0)) + label.set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER) + slider = DoubleSlider.new() + slider.label = label + slider.suffix = p_suffix + slider.value_changed.connect(_on_setting_changed) + pending_children.push_back(slider) + pending_children.push_back(label) + control = slider + + slider.set_min(p_minimum) + slider.set_max(p_maximum) + slider.set_step(p_step) + slider.set_value(p_default) + slider.set_v_size_flags(SIZE_SHRINK_CENTER) + slider.set_custom_minimum_size(Vector2(50, 10)) + + if !(p_flags & NO_SAVE): + slider.set_value(plugin.get_setting(ES_TOOL_SETTINGS + p_name, p_default)) + slider.value_changed.connect( ( + func(value, path): + plugin.set_setting(path, value) + ).bind(ES_TOOL_SETTINGS + p_name) ) + else: + slider.set_value(p_default) + + control.name = p_name.to_pascal_case() + settings[p_name] = control + + # Setup button labels + if not (p_flags & NO_LABEL): + # Labels are actually buttons styled to look like labels + var label := Button.new() + label.set("theme_override_styles/normal", get_theme_stylebox("normal", "Label")) + label.set("theme_override_styles/hover", get_theme_stylebox("normal", "Label")) + label.set("theme_override_styles/pressed", get_theme_stylebox("normal", "Label")) + label.set("theme_override_styles/focus", get_theme_stylebox("normal", "Label")) + label.pressed.connect(_on_label_pressed.bind(p_name, p_default)) + if p_label.is_empty(): + label.set_text(p_name.capitalize() + ": ") + else: + label.set_text(p_label.capitalize() + ": ") + pending_children.push_front(label) + + # Add separators to front + if p_flags & ADD_SEPARATOR: + pending_children.push_front(VSeparator.new()) + if p_flags & ADD_SPACER: + var spacer := Control.new() + spacer.set_custom_minimum_size(Vector2(5, 0)) + pending_children.push_front(spacer) + + # Add all children to container and list + for child in pending_children: + container.add_child(child, true) + p_list.add_child(container, true) + + +# If label button is pressed, reset value to default or toggle checkbox +func _on_label_pressed(p_name: String, p_default: Variant) -> void: + var control: Control = settings.get(p_name) + if not control: + return + if control is CheckBox: + set_setting(p_name, !control.button_pressed) + elif p_default != null: + set_setting(p_name, p_default) + + +func get_settings() -> Dictionary: + var dict: Dictionary + for key in settings.keys(): + dict[key] = get_setting(key) + return dict + + +func get_setting(p_setting: String) -> Variant: + var object: Object = settings.get(p_setting) + var value: Variant + if object is Range: + value = object.get_value() + # Adjust widths of all sliders on update of values + var digits: float = count_digits(value) + var width: float = clamp( (1 + count_digits(value)) * 19., 50, 80) * clamp(EditorInterface.get_editor_scale(), .9, 2) + object.set_custom_minimum_size(Vector2(width, 0)) + elif object is DoubleSlider: + value = object.get_value() + elif object is ButtonGroup: # "brush" + value = selected_brush_imgs + elif object is CheckBox: + value = object.is_pressed() + elif object is ColorPickerButton: + value = object.color + elif object is MultiPicker: + value = object.get_points() + if value == null: + value = 0 + return value + + +func set_setting(p_setting: String, p_value: Variant) -> void: + var object: Object = settings.get(p_setting) + if object is DoubleSlider: # Expects p_value is Vector2 + object.set_value(p_value) + elif object is Range: + object.set_value(p_value) + elif object is ButtonGroup: # Expects p_value is Array [ "button name", boolean ] + if p_value is Array and p_value.size() == 2: + for button in object.get_buttons(): + if button.name == p_value[0]: + button.button_pressed = p_value[1] + elif object is CheckBox: + object.button_pressed = p_value + elif object is ColorPickerButton: + object.color = p_value + plugin.set_setting(ES_TOOL_SETTINGS + p_setting, p_value) # Signal doesn't fire on CPB + elif object is MultiPicker: # Expects p_value is PackedVector3Array + object.points = p_value + _on_setting_changed(object) + + +func show_settings(p_settings: PackedStringArray) -> void: + for setting in settings.keys(): + var object: Object = settings[setting] + if object is Control: + if setting in p_settings: + object.get_parent().show() + else: + object.get_parent().hide() + if select_brush_button: + if not "brush" in p_settings: + select_brush_button.hide() + else: + select_brush_button.show() + + +func _on_setting_changed(p_object: Variant = null) -> void: + # If a brush was selected + if p_object is Button and p_object.get_parent().name == "BrushList": + _generate_brush_texture(p_object) + # Optionally Set selected brush texture in main brush button + if select_brush_button: + select_brush_button.set_button_icon(p_object.get_button_icon()) + # Hide popup + p_object.get_parent().get_parent().set_visible(false) + # Hide label + if p_object.get_child_count() > 0: + p_object.get_child(0).visible = false + emit_signal("setting_changed") + + +func _generate_brush_texture(p_btn: Button) -> void: + if p_btn is Button: + var img: Image = p_btn.get_meta("image") + if img.get_width() < 1024 and img.get_height() < 1024: + img = img.duplicate() + img.resize(1024, 1024, Image.INTERPOLATE_CUBIC) + var tex: ImageTexture = ImageTexture.create_from_image(img) + selected_brush_imgs = [ img, tex ] + + +func _on_drawable_toggled(p_button_pressed: bool) -> void: + if not p_button_pressed: + settings["gradient_points"].clear() + + +func _get_brush_preview_material() -> ShaderMaterial: + if !brush_preview_material: + brush_preview_material = ShaderMaterial.new() + var shader: Shader = Shader.new() + var code: String = "shader_type canvas_item;\n" + code += "varying vec4 v_vertex_color;\n" + code += "void vertex() {\n" + code += " v_vertex_color = COLOR;\n" + code += "}\n" + code += "void fragment(){\n" + code += " vec4 tex = texture(TEXTURE, UV);\n" + code += " COLOR.a *= pow(tex.r, 0.666);\n" + code += " COLOR.rgb = v_vertex_color.rgb;\n" + code += "}\n" + shader.set_code(code) + brush_preview_material.set_shader(shader) + return brush_preview_material + + +# Counts digits of a number including negative sign, decimal points, and up to 3 decimals +func count_digits(p_value: float) -> int: + var count: int = 1 + for i in range(5, 0, -1): + if abs(p_value) >= pow(10, i): + count = i+1 + break + if p_value - floor(p_value) >= .1: + count += 1 # For the decimal + if p_value*10 - floor(p_value*10.) >= .1: + count += 1 + if p_value*100 - floor(p_value*100.) >= .1: + count += 1 + if p_value*1000 - floor(p_value*1000.) >= .1: + count += 1 + # Negative sign + if p_value < 0: + count += 1 + return count + diff --git a/addons/terrain_3d/src/tool_settings.gd.uid b/addons/terrain_3d/src/tool_settings.gd.uid new file mode 100644 index 0000000..b6c65fb --- /dev/null +++ b/addons/terrain_3d/src/tool_settings.gd.uid @@ -0,0 +1 @@ +uid://ciskaaennrffu diff --git a/addons/terrain_3d/src/toolbar.gd b/addons/terrain_3d/src/toolbar.gd new file mode 100644 index 0000000..0cc80fd --- /dev/null +++ b/addons/terrain_3d/src/toolbar.gd @@ -0,0 +1,145 @@ +# Copyright © 2025 Cory Petkovsek, Roope Palmroos, and Contributors. +# Toolbar for Terrain3D +extends VFlowContainer + +signal tool_changed(p_tool: Terrain3DEditor.Tool, p_operation: Terrain3DEditor.Operation) + +const ICON_REGION_ADD: String = "res://addons/terrain_3d/icons/region_add.svg" +const ICON_REGION_REMOVE: String = "res://addons/terrain_3d/icons/region_remove.svg" +const ICON_HEIGHT_ADD: String = "res://addons/terrain_3d/icons/height_add.svg" +const ICON_HEIGHT_SUB: String = "res://addons/terrain_3d/icons/height_sub.svg" +const ICON_HEIGHT_FLAT: String = "res://addons/terrain_3d/icons/height_flat.svg" +const ICON_HEIGHT_SLOPE: String = "res://addons/terrain_3d/icons/height_slope.svg" +const ICON_HEIGHT_SMOOTH: String = "res://addons/terrain_3d/icons/height_smooth.svg" +const ICON_PAINT_TEXTURE: String = "res://addons/terrain_3d/icons/texture_paint.svg" +const ICON_SPRAY_TEXTURE: String = "res://addons/terrain_3d/icons/texture_spray.svg" +const ICON_COLOR: String = "res://addons/terrain_3d/icons/color_paint.svg" +const ICON_WETNESS: String = "res://addons/terrain_3d/icons/wetness.svg" +const ICON_AUTOSHADER: String = "res://addons/terrain_3d/icons/autoshader.svg" +const ICON_HOLES: String = "res://addons/terrain_3d/icons/holes.svg" +const ICON_NAVIGATION: String = "res://addons/terrain_3d/icons/navigation.svg" +const ICON_INSTANCER: String = "res://addons/terrain_3d/icons/multimesh.svg" + +var add_tool_group: ButtonGroup = ButtonGroup.new() +var sub_tool_group: ButtonGroup = ButtonGroup.new() + + +func _init() -> void: + set_custom_minimum_size(Vector2(20, 0)) + +func _ready() -> void: + add_tool_group.pressed.connect(_on_tool_selected) + sub_tool_group.pressed.connect(_on_tool_selected) + + add_tool_button({ "tool":Terrain3DEditor.REGION, + "add_text":"Add Region", "add_op":Terrain3DEditor.ADD, "add_icon":ICON_REGION_ADD, + "sub_text":"Remove Region", "sub_op":Terrain3DEditor.SUBTRACT, "sub_icon":ICON_REGION_REMOVE }) + + add_child(HSeparator.new()) + + add_tool_button({ "tool":Terrain3DEditor.SCULPT, + "add_text":"Raise", "add_op":Terrain3DEditor.ADD, "add_icon":ICON_HEIGHT_ADD, + "sub_text":"Lower", "sub_op":Terrain3DEditor.SUBTRACT, "sub_icon":ICON_HEIGHT_SUB }) + + add_tool_button({ "tool":Terrain3DEditor.SCULPT, + "add_text":"Smooth", "add_op":Terrain3DEditor.AVERAGE, "add_icon":ICON_HEIGHT_SMOOTH }) + + add_tool_button({ "tool":Terrain3DEditor.HEIGHT, + "add_text":"Height", "add_op":Terrain3DEditor.ADD, "add_icon":ICON_HEIGHT_FLAT, + "sub_text":"Zero", "sub_op":Terrain3DEditor.SUBTRACT, "sub_icon":ICON_HEIGHT_FLAT }) + + add_tool_button({ "tool":Terrain3DEditor.SCULPT, + "add_text":"Slope", "add_op":Terrain3DEditor.GRADIENT, "add_icon":ICON_HEIGHT_SLOPE }) + + add_child(HSeparator.new()) + + add_tool_button({ "tool":Terrain3DEditor.TEXTURE, + "add_text":"Paint Base Texture", "add_op":Terrain3DEditor.REPLACE, "add_icon":ICON_PAINT_TEXTURE }) + + add_tool_button({ "tool":Terrain3DEditor.TEXTURE, + "add_text":"Spray Overlay Texture", "add_op":Terrain3DEditor.ADD, "add_icon":ICON_SPRAY_TEXTURE }) + + add_tool_button({ "tool":Terrain3DEditor.AUTOSHADER, + "add_text":"Enable Autoshader", "add_op":Terrain3DEditor.ADD, "add_icon":ICON_AUTOSHADER, + "sub_text":"Disable Autoshader", "sub_op":Terrain3DEditor.SUBTRACT }) + + add_child(HSeparator.new()) + + add_tool_button({ "tool":Terrain3DEditor.COLOR, + "add_text":"Paint Color", "add_op":Terrain3DEditor.ADD, "add_icon":ICON_COLOR, + "sub_text":"Remove Color", "sub_op":Terrain3DEditor.SUBTRACT }) + + add_tool_button({ "tool":Terrain3DEditor.ROUGHNESS, + "add_text":"Paint Wetness", "add_op":Terrain3DEditor.ADD, "add_icon":ICON_WETNESS, + "sub_text":"Remove Wetness", "sub_op":Terrain3DEditor.SUBTRACT }) + + add_child(HSeparator.new()) + + add_tool_button({ "tool":Terrain3DEditor.HOLES, + "add_text":"Add Holes", "add_op":Terrain3DEditor.ADD, "add_icon":ICON_HOLES, + "sub_text":"Remove Holes", "sub_op":Terrain3DEditor.SUBTRACT }) + + add_tool_button({ "tool":Terrain3DEditor.NAVIGATION, + "add_text":"Paint Navigable Area", "add_op":Terrain3DEditor.ADD, "add_icon":ICON_NAVIGATION, + "sub_text":"Remove Navigable Area", "sub_op":Terrain3DEditor.SUBTRACT }) + + add_tool_button({ "tool":Terrain3DEditor.INSTANCER, + "add_text":"Instance Meshes", "add_op":Terrain3DEditor.ADD, "add_icon":ICON_INSTANCER, + "sub_text":"Remove Meshes", "sub_op":Terrain3DEditor.SUBTRACT }) + + # Select first button + var buttons: Array[BaseButton] = add_tool_group.get_buttons() + buttons[0].set_pressed(true) + show_add_buttons(true) + + +func add_tool_button(p_params: Dictionary) -> void: + # Additive button + var button := Button.new() + button.set_name(p_params.get("add_text", "blank").to_pascal_case()) + button.set_meta("Tool", p_params.get("tool", 0)) + button.set_meta("Operation", p_params.get("add_op", 0)) + button.set_meta("ID", add_tool_group.get_buttons().size() + 1) + button.set_tooltip_text(p_params.get("add_text", "blank")) + button.set_button_icon(load(p_params.get("add_icon"))) + button.set_flat(true) + button.set_toggle_mode(true) + button.set_h_size_flags(SIZE_SHRINK_END) + button.set_button_group(p_params.get("group", add_tool_group)) + add_child(button, true) + + # Subtractive button + var button2: Button + if p_params.has("sub_text"): + button2 = Button.new() + button2.set_name(p_params.get("sub_text", "blank").to_pascal_case()) + button2.set_meta("Tool", p_params.get("tool", 0)) + button2.set_meta("Operation", p_params.get("sub_op", 0)) + button2.set_meta("ID", button.get_meta("ID")) + button2.set_tooltip_text(p_params.get("sub_text", "blank")) + button2.set_button_icon(load(p_params.get("sub_icon", p_params.get("add_icon")))) + button2.set_flat(true) + button2.set_toggle_mode(true) + button2.set_h_size_flags(SIZE_SHRINK_END) + else: + button2 = button.duplicate() + button2.set_button_group(p_params.get("group", sub_tool_group)) + add_child(button2, true) + + +func show_add_buttons(p_enable: bool) -> void: + for button in add_tool_group.get_buttons(): + button.visible = p_enable + for button in sub_tool_group.get_buttons(): + button.visible = !p_enable + + +func _on_tool_selected(p_button: BaseButton) -> void: + # Select same tool on negative bar + var group: ButtonGroup = p_button.get_button_group() + var change_group: ButtonGroup = add_tool_group if group == sub_tool_group else sub_tool_group + var id: int = p_button.get_meta("ID", -2) + for button in change_group.get_buttons(): + button.set_pressed_no_signal(button.get_meta("ID", -1) == id) + + emit_signal("tool_changed", p_button.get_meta("Tool", Terrain3DEditor.TOOL_MAX), p_button.get_meta("Operation", Terrain3DEditor.OP_MAX)) diff --git a/addons/terrain_3d/src/toolbar.gd.uid b/addons/terrain_3d/src/toolbar.gd.uid new file mode 100644 index 0000000..6ba79d5 --- /dev/null +++ b/addons/terrain_3d/src/toolbar.gd.uid @@ -0,0 +1 @@ +uid://b1j37u6utjbom diff --git a/addons/terrain_3d/src/ui.gd b/addons/terrain_3d/src/ui.gd new file mode 100644 index 0000000..1c16fff --- /dev/null +++ b/addons/terrain_3d/src/ui.gd @@ -0,0 +1,574 @@ +# Copyright © 2025 Cory Petkovsek, Roope Palmroos, and Contributors. +# UI for Terrain3D +extends Node + + +# Includes +const TerrainMenu: Script = preload("res://addons/terrain_3d/menu/terrain_menu.gd") +const Toolbar: Script = preload("res://addons/terrain_3d/src/toolbar.gd") +const ToolSettings: Script = preload("res://addons/terrain_3d/src/tool_settings.gd") +const OperationBuilder: Script = preload("res://addons/terrain_3d/src/operation_builder.gd") +const GradientOperationBuilder: Script = preload("res://addons/terrain_3d/src/gradient_operation_builder.gd") +const COLOR_RAISE := Color.WHITE +const COLOR_LOWER := Color.BLACK +const COLOR_SMOOTH := Color(0.5, 0, .2) +const COLOR_LIFT := Color.ORANGE +const COLOR_FLATTEN := Color.BLUE_VIOLET +const COLOR_HEIGHT := Color(0., 0.32, .4) +const COLOR_SLOPE := Color.YELLOW +const COLOR_PAINT := Color.DARK_GREEN +const COLOR_SPRAY := Color.PALE_GREEN +const COLOR_ROUGHNESS := Color.ROYAL_BLUE +const COLOR_AUTOSHADER := Color.DODGER_BLUE +const COLOR_HOLES := Color.BLACK +const COLOR_NAVIGATION := Color(.28, .0, .25) +const COLOR_INSTANCER := Color.CRIMSON +const COLOR_PICK_COLOR := Color.WHITE +const COLOR_PICK_HEIGHT := Color.DARK_RED +const COLOR_PICK_ROUGH := Color.ROYAL_BLUE + +const OP_NONE: int = 0x0 +const OP_POSITIVE_ONLY: int = 0x01 +const OP_NEGATIVE_ONLY: int = 0x02 + +const RING1: String = "res://addons/terrain_3d/brushes/ring1.exr" +var ring_texture : ImageTexture +@onready var region_texture := ImageTexture.new() : + set(value): + var image: Image = Image.create_empty(1, 1, false, Image.FORMAT_R8) + image.fill(Color.WHITE) + value.create_from_image(image) + region_texture = value +var plugin: EditorPlugin # Actually Terrain3DEditorPlugin, but Godot still has CRC errors +var toolbar: Toolbar +var tool_settings: ToolSettings +var terrain_menu: TerrainMenu +var setting_has_changed: bool = false +var visible: bool = false +var picking: int = Terrain3DEditor.TOOL_MAX +var picking_callback: Callable +var brush_data: Dictionary +var operation_builder: OperationBuilder +var last_tool: Terrain3DEditor.Tool +var last_operation: Terrain3DEditor.Operation +var last_rmb_time: int = 0 # Set in editor.gd + +# Editor decals, indices; 0 = main brush, 1 = slope point A, 2 = slope point B +var mat_rid: RID +var editor_decal_position: Array[Vector2] = [Vector2(), Vector2(), Vector2()] +var editor_decal_rotation: Array[float] = [float(), float(), float()] +var editor_decal_size: Array[float] = [float(), float(), float()] +var editor_decal_color: Array[Color] = [Color(), Color(), Color()] +var editor_decal_visible: Array[bool] = [bool(), bool(), bool()] +var editor_brush_texture_rid: RID = RID() +var editor_decal_timer: Timer +var editor_decal_fade: float : + set(value): + editor_decal_fade = value + if editor_decal_color.size() > 0: + editor_decal_color[0].a = value + if is_shader_valid(): + RenderingServer.material_set_param(mat_rid, "_editor_decal_color", editor_decal_color) + if value < 0.001: + var r_map: PackedInt32Array = plugin.terrain.data.get_region_map() + RenderingServer.material_set_param(mat_rid, "_region_map", r_map) +var editor_ring_texture_rid: RID + + +func _enter_tree() -> void: + toolbar = Toolbar.new() + toolbar.hide() + toolbar.tool_changed.connect(_on_tool_changed) + + tool_settings = ToolSettings.new() + tool_settings.setting_changed.connect(_on_setting_changed) + tool_settings.picking.connect(_on_picking) + tool_settings.plugin = plugin + tool_settings.hide() + + terrain_menu = TerrainMenu.new() + terrain_menu.plugin = plugin + terrain_menu.hide() + + plugin.add_control_to_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_SIDE_LEFT, toolbar) + plugin.add_control_to_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_BOTTOM, tool_settings) + plugin.add_control_to_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_MENU, terrain_menu) + + _on_tool_changed(Terrain3DEditor.REGION, Terrain3DEditor.ADD) + + editor_decal_timer = Timer.new() + editor_decal_timer.wait_time = .5 + editor_decal_timer.one_shot = true + editor_decal_timer.timeout.connect(func(): + get_tree().create_tween().tween_property(self, "editor_decal_fade", 0.0, 0.15)) + add_child(editor_decal_timer) + + +func _ready() -> void: + var img: Image = Image.load_from_file(RING1) + img.convert(Image.FORMAT_R8) + ring_texture = ImageTexture.create_from_image(img) + editor_ring_texture_rid = ring_texture.get_rid() + + +func _exit_tree() -> void: + plugin.remove_control_from_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_SIDE_LEFT, toolbar) + plugin.remove_control_from_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_BOTTOM, tool_settings) + toolbar.queue_free() + tool_settings.queue_free() + terrain_menu.queue_free() + editor_decal_timer.queue_free() + + +func set_visible(p_visible: bool, p_menu_only: bool = false) -> void: + terrain_menu.set_visible(p_visible) + + if p_menu_only: + toolbar.set_visible(false) + tool_settings.set_visible(false) + else: + visible = p_visible + toolbar.set_visible(p_visible) + tool_settings.set_visible(p_visible) + update_decal() + + if(plugin.editor): + if(p_visible): + await get_tree().create_timer(.01).timeout # Won't work, otherwise + _on_tool_changed(last_tool, last_operation) + else: + plugin.editor.set_tool(Terrain3DEditor.TOOL_MAX) + plugin.editor.set_operation(Terrain3DEditor.OP_MAX) + + +func set_menu_visibility(p_list: Control, p_visible: bool) -> void: + if p_list: + p_list.get_parent().get_parent().visible = p_visible + + +func _on_tool_changed(p_tool: Terrain3DEditor.Tool, p_operation: Terrain3DEditor.Operation) -> void: + clear_picking() + set_menu_visibility(tool_settings.advanced_list, true) + set_menu_visibility(tool_settings.scale_list, false) + set_menu_visibility(tool_settings.rotation_list, false) + set_menu_visibility(tool_settings.height_list, false) + set_menu_visibility(tool_settings.color_list, false) + + # Select which settings to show. Options in tool_settings.gd:_ready + var to_show: PackedStringArray = [] + + match p_tool: + Terrain3DEditor.REGION: + to_show.push_back("instructions") + to_show.push_back("remove") + set_menu_visibility(tool_settings.advanced_list, false) + + Terrain3DEditor.SCULPT: + to_show.push_back("brush") + to_show.push_back("size") + to_show.push_back("strength") + if p_operation in [Terrain3DEditor.ADD, Terrain3DEditor.SUBTRACT]: + to_show.push_back("remove") + elif p_operation == Terrain3DEditor.GRADIENT: + to_show.push_back("gradient_points") + to_show.push_back("drawable") + + Terrain3DEditor.HEIGHT: + to_show.push_back("brush") + to_show.push_back("size") + to_show.push_back("strength") + to_show.push_back("height") + to_show.push_back("height_picker") + + Terrain3DEditor.TEXTURE: + to_show.push_back("brush") + to_show.push_back("size") + to_show.push_back("enable_texture") + if p_operation == Terrain3DEditor.ADD: + to_show.push_back("strength") + to_show.push_back("slope") + to_show.push_back("enable_angle") + to_show.push_back("angle") + to_show.push_back("angle_picker") + to_show.push_back("dynamic_angle") + to_show.push_back("enable_scale") + to_show.push_back("scale") + to_show.push_back("scale_picker") + + Terrain3DEditor.COLOR: + to_show.push_back("brush") + to_show.push_back("size") + to_show.push_back("strength") + to_show.push_back("color") + to_show.push_back("color_picker") + to_show.push_back("slope") + to_show.push_back("texture_filter") + to_show.push_back("margin") + to_show.push_back("remove") + + Terrain3DEditor.ROUGHNESS: + to_show.push_back("brush") + to_show.push_back("size") + to_show.push_back("strength") + to_show.push_back("roughness") + to_show.push_back("roughness_picker") + to_show.push_back("slope") + to_show.push_back("texture_filter") + to_show.push_back("margin") + to_show.push_back("remove") + + Terrain3DEditor.AUTOSHADER, Terrain3DEditor.HOLES, Terrain3DEditor.NAVIGATION: + to_show.push_back("brush") + to_show.push_back("size") + to_show.push_back("remove") + + Terrain3DEditor.INSTANCER: + to_show.push_back("size") + to_show.push_back("strength") + to_show.push_back("slope") + set_menu_visibility(tool_settings.height_list, true) + to_show.push_back("height_offset") + to_show.push_back("random_height") + set_menu_visibility(tool_settings.scale_list, true) + to_show.push_back("fixed_scale") + to_show.push_back("random_scale") + set_menu_visibility(tool_settings.rotation_list, true) + to_show.push_back("fixed_spin") + to_show.push_back("random_spin") + to_show.push_back("fixed_tilt") + to_show.push_back("random_tilt") + to_show.push_back("align_to_normal") + set_menu_visibility(tool_settings.color_list, true) + to_show.push_back("vertex_color") + to_show.push_back("random_darken") + to_show.push_back("random_hue") + to_show.push_back("remove") + + _: + pass + + # Advanced menu settings + to_show.push_back("auto_regions") + to_show.push_back("align_to_view") + to_show.push_back("show_cursor_while_painting") + to_show.push_back("gamma") + to_show.push_back("jitter") + to_show.push_back("crosshair_threshold") + tool_settings.show_settings(to_show) + + operation_builder = null + if p_operation == Terrain3DEditor.GRADIENT: + operation_builder = GradientOperationBuilder.new() + operation_builder.tool_settings = tool_settings + + if plugin.editor: + plugin.editor.set_tool(p_tool) + plugin.editor.set_operation(_modify_operation(p_operation)) + last_tool = p_tool + last_operation = p_operation + + _on_setting_changed() + plugin.update_region_grid() + + +func _on_setting_changed() -> void: + if not plugin.asset_dock: + return + brush_data = tool_settings.get_settings() + brush_data["asset_id"] = plugin.asset_dock.get_current_list().get_selected_id() + plugin.editor.set_brush_data(brush_data) + plugin.editor.set_operation(_modify_operation(plugin.editor.get_operation())) + update_decal() + + +func update_modifiers() -> void: + toolbar.show_add_buttons(not plugin.modifier_ctrl) + + if plugin.modifier_shift and not plugin.modifier_ctrl: + plugin.editor.set_tool(Terrain3DEditor.SCULPT) + plugin.editor.set_operation(Terrain3DEditor.AVERAGE) + else: + plugin.editor.set_tool(last_tool) + if plugin.modifier_ctrl: + plugin.editor.set_operation(_modify_operation(last_operation)) + else: + plugin.editor.set_operation(last_operation) + + +func _modify_operation(p_operation: Terrain3DEditor.Operation) -> Terrain3DEditor.Operation: + var remove_checked: bool = false + if DisplayServer.is_touchscreen_available(): + var removable_tools := [Terrain3DEditor.REGION, Terrain3DEditor.SCULPT, Terrain3DEditor.HEIGHT, Terrain3DEditor.AUTOSHADER, + Terrain3DEditor.HOLES, Terrain3DEditor.INSTANCER, Terrain3DEditor.NAVIGATION, + Terrain3DEditor.COLOR, Terrain3DEditor.ROUGHNESS] + remove_checked = brush_data.get("remove", false) && plugin.editor.get_tool() in removable_tools + + if plugin.modifier_ctrl or remove_checked: + return _invert_operation(p_operation, OP_NEGATIVE_ONLY) + return _invert_operation(p_operation, OP_POSITIVE_ONLY) + + +func _invert_operation(p_operation: Terrain3DEditor.Operation, flags: int = OP_NONE) -> Terrain3DEditor.Operation: + if p_operation == Terrain3DEditor.ADD and ! (flags & OP_POSITIVE_ONLY): + return Terrain3DEditor.SUBTRACT + elif p_operation == Terrain3DEditor.SUBTRACT and ! (flags & OP_NEGATIVE_ONLY): + return Terrain3DEditor.ADD + return p_operation + + +func update_decal() -> void: + if not plugin.terrain or brush_data.size() <= 3: + return + mat_rid = plugin.terrain.material.get_material_rid() + editor_decal_timer.start() + + # If not a state that should show the decal, hide everything and return + if not visible or \ + plugin._input_mode < 0 or \ + # Wait for cursor to recenter after moving camera before revealing + # See https://github.com/godotengine/godot/issues/70098 + Time.get_ticks_msec() - last_rmb_time <= 30 or \ + (plugin._input_mode > 0 and not brush_data["show_cursor_while_painting"]): + hide_decal() + return + + reset_decal_arrays() + editor_decal_position[0] = Vector2(plugin.mouse_global_position.x, plugin.mouse_global_position.z) + editor_decal_visible[0] = true + # Set region size, and modify region map for none background mode. + var r_map: PackedInt32Array = plugin.terrain.data.get_region_map() + if plugin.editor.get_tool() == Terrain3DEditor.REGION: + var r_size: float = float(plugin.terrain.get_region_size()) * plugin.terrain.get_vertex_spacing() + var map_size: int = plugin.terrain.data.REGION_MAP_SIZE + var half_r_size: float = r_size * 0.5 + var pos: Vector2 = (Vector2(plugin.mouse_global_position.x, plugin.mouse_global_position.z) + + Vector2(half_r_size, half_r_size)).snappedf(r_size) - Vector2(half_r_size, half_r_size) + editor_brush_texture_rid = region_texture.get_rid() + editor_decal_position[0] = pos + editor_decal_size[0] = r_size + editor_decal_rotation[0] = 0.0 + + var loc: Vector2i = plugin.terrain.data.get_region_location(plugin.mouse_global_position) + loc += Vector2i(map_size / 2, map_size / 2) + if !(loc.x < 0 or loc.x > map_size - 1 or loc.y < 0 or loc.y > map_size - 1): + var index: int = clampi(loc.y * map_size + loc.x, 0, map_size * map_size - 1) + if plugin.terrain.material.get_world_background() == Terrain3DMaterial.WorldBackground.NONE: + if r_map[index] == 0 and plugin.editor.get_operation() == Terrain3DEditor.ADD: + r_map[index] = -index - 1 + else: + r_map[index] = r_map[index] + + match plugin.editor.get_operation(): + Terrain3DEditor.ADD: + if r_map[index] <= 0: + editor_decal_color[0] = Color.WHITE + editor_decal_color[0].a = 0.25 + else: + hide_decal() + + Terrain3DEditor.SUBTRACT: + if r_map[index] > 0: + editor_decal_color[0] = Color.WHITE * .15 + editor_decal_color[0].a = 0.75 + else: + hide_decal() + else: + hide_decal() + # Set texture and color + elif picking != Terrain3DEditor.TOOL_MAX: + editor_brush_texture_rid = ring_texture.get_rid() + editor_decal_size[0] = 10. * plugin.terrain.get_vertex_spacing() + match picking: + Terrain3DEditor.HEIGHT: + editor_decal_color[0] = COLOR_PICK_HEIGHT + Terrain3DEditor.COLOR: + editor_decal_color[0] = COLOR_PICK_COLOR + Terrain3DEditor.ROUGHNESS: + editor_decal_color[0] = COLOR_PICK_ROUGH + editor_decal_color[0].a = 1.0 + else: + editor_brush_texture_rid = brush_data["brush"][1].get_rid() + editor_decal_size[0] = maxf(brush_data["size"], .5) + if brush_data["align_to_view"]: + var cam: Camera3D = plugin.terrain.get_camera(); + if (cam): + editor_decal_rotation[0] = cam.rotation.y + else: + editor_decal_rotation[0] = 0. + match plugin.editor.get_tool(): + Terrain3DEditor.SCULPT: + match plugin.editor.get_operation(): + Terrain3DEditor.ADD: + if plugin.modifier_alt: + editor_decal_color[0] = COLOR_LIFT + editor_decal_color[0].a = clamp(brush_data["strength"], .2, .5) + else: + editor_decal_color[0] = COLOR_RAISE + editor_decal_color[0].a = clamp(brush_data["strength"], .25, .5) + Terrain3DEditor.SUBTRACT: + if plugin.modifier_alt: + editor_decal_color[0] = COLOR_FLATTEN + editor_decal_color[0].a = clamp(brush_data["strength"], .25, .5) + .1 + else: + editor_decal_color[0] = COLOR_LOWER + editor_decal_color[0].a = clamp(brush_data["strength"], .2, .5) + .25 + Terrain3DEditor.AVERAGE: + editor_decal_color[0] = COLOR_SMOOTH + editor_decal_color[0].a = clamp(brush_data["strength"], .2, .5) + .25 + Terrain3DEditor.GRADIENT: + editor_decal_color[0] = COLOR_SLOPE + editor_decal_color[0].a = clamp(brush_data["strength"], .2, .4) + Terrain3DEditor.HEIGHT: + editor_decal_color[0] = COLOR_HEIGHT + editor_decal_color[0].a = clamp(brush_data["strength"], .2, .5) + .25 + Terrain3DEditor.TEXTURE: + match plugin.editor.get_operation(): + Terrain3DEditor.REPLACE: + editor_decal_color[0] = COLOR_PAINT + editor_decal_color[0].a = .6 + Terrain3DEditor.SUBTRACT: + editor_decal_color[0] = COLOR_PAINT + editor_decal_color[0].a = clamp(brush_data["strength"], .2, .5) + .1 + Terrain3DEditor.ADD: + editor_decal_color[0] = COLOR_SPRAY + editor_decal_color[0].a = clamp(brush_data["strength"], .15, .4) + Terrain3DEditor.COLOR: + editor_decal_color[0] = brush_data["color"].srgb_to_linear() + editor_decal_color[0].a *= clamp(brush_data["strength"], .2, .5) + Terrain3DEditor.ROUGHNESS: + editor_decal_color[0] = COLOR_ROUGHNESS + editor_decal_color[0].a = clamp(brush_data["strength"], .2, .5) + .1 + Terrain3DEditor.AUTOSHADER: + editor_decal_color[0] = COLOR_AUTOSHADER + editor_decal_color[0].a = .6 + Terrain3DEditor.HOLES: + editor_decal_color[0] = COLOR_HOLES + editor_decal_color[0].a = .75 + Terrain3DEditor.NAVIGATION: + editor_decal_color[0] = COLOR_NAVIGATION + editor_decal_color[0].a = .80 + Terrain3DEditor.INSTANCER: + editor_brush_texture_rid = ring_texture.get_rid() + editor_decal_color[0] = COLOR_INSTANCER + editor_decal_color[0].a = .75 + + editor_decal_visible[1] = false + editor_decal_visible[2] = false + + if plugin.editor.get_operation() == Terrain3DEditor.GRADIENT: + var point1: Vector3 = brush_data["gradient_points"][0] + if point1 != Vector3.ZERO: + editor_decal_color[1] = COLOR_SLOPE + editor_decal_size[1] = 10. * plugin.terrain.get_vertex_spacing() + editor_decal_visible[1] = true + editor_decal_position[1] = Vector2(point1.x, point1.z) + var point2: Vector3 = brush_data["gradient_points"][1] + if point2 != Vector3.ZERO: + editor_decal_color[2] = COLOR_SLOPE + editor_decal_size[2] = 10. * plugin.terrain.get_vertex_spacing() + editor_decal_visible[2] = true + editor_decal_position[2] = Vector2(point2.x, point2.z) + + if RenderingServer.get_current_rendering_method().contains("gl_compatibility"): + for i in editor_decal_color.size(): + editor_decal_color[i].a = maxf(0.1, editor_decal_color[i].a - .25) + + editor_decal_fade = editor_decal_color[0].a + # Update Shader params + if is_shader_valid(): + RenderingServer.material_set_param(mat_rid, "_editor_brush_texture", editor_brush_texture_rid) + RenderingServer.material_set_param(mat_rid, "_editor_ring_texture", editor_ring_texture_rid) + RenderingServer.material_set_param(mat_rid, "_editor_decal_position", editor_decal_position) + RenderingServer.material_set_param(mat_rid, "_editor_decal_rotation", editor_decal_rotation) + RenderingServer.material_set_param(mat_rid, "_editor_decal_size", editor_decal_size) + RenderingServer.material_set_param(mat_rid, "_editor_decal_color", editor_decal_color) + RenderingServer.material_set_param(mat_rid, "_editor_decal_visible", editor_decal_visible) + RenderingServer.material_set_param(mat_rid, "_editor_crosshair_threshold", brush_data["crosshair_threshold"] + 0.1) + RenderingServer.material_set_param(mat_rid, "_region_map", r_map) + + +func is_shader_valid() -> bool: + # As long as the compiled shader contains at least 1 uniform, we can use it to check + # if the shader compilation has failed, as this will then return an empty dictionary. + if not plugin.terrain: + return false + var params = RenderingServer.get_shader_parameter_list(plugin.terrain.material.get_shader_rid()) + if params.is_empty(): + return false + else: + return true + + +func hide_decal() -> void: + editor_decal_visible = [false, false, false] + if is_shader_valid(): + var r_map: PackedInt32Array = plugin.terrain.data.get_region_map() + RenderingServer.material_set_param(mat_rid, "_editor_decal_visible", editor_decal_visible) + RenderingServer.material_set_param(mat_rid, "_region_map", r_map) + + +# These array sizes are reset to 0 when closing scenes for some unknown reason, so check and reset +func reset_decal_arrays() -> void: + if editor_decal_color.size() < 3: + editor_decal_position = [Vector2(), Vector2(), Vector2()] + editor_decal_rotation = [float(), float(), float()] + editor_decal_size = [float(), float(), float()] + editor_decal_color = [Color(), Color(), Color()] + editor_decal_visible = [false, false, false] + editor_brush_texture_rid = RID() + + +func set_decal_rotation(p_rot: float) -> void: + editor_decal_rotation[0] = p_rot + if is_shader_valid(): + RenderingServer.material_set_param(mat_rid, "_editor_decal_rotation", editor_decal_rotation) + + +func _on_picking(p_type: int, p_callback: Callable) -> void: + picking = p_type + picking_callback = p_callback + update_decal() + + +func clear_picking() -> void: + picking = Terrain3DEditor.TOOL_MAX + + +func is_picking() -> bool: + if picking != Terrain3DEditor.TOOL_MAX: + return true + + if operation_builder and operation_builder.is_picking(): + return true + + return false + + +func pick(p_global_position: Vector3) -> void: + if picking != Terrain3DEditor.TOOL_MAX: + var color: Color + match picking: + Terrain3DEditor.HEIGHT, Terrain3DEditor.SCULPT: + color = Color(plugin.terrain.data.get_height(p_global_position), 0., 0., 1.) + Terrain3DEditor.ROUGHNESS: + color = plugin.terrain.data.get_pixel(Terrain3DRegion.TYPE_COLOR, p_global_position) + Terrain3DEditor.COLOR: + color = plugin.terrain.data.get_color(p_global_position) + Terrain3DEditor.ANGLE: + color = Color(plugin.terrain.data.get_control_angle(p_global_position), 0., 0., 1.) + Terrain3DEditor.SCALE: + color = Color(plugin.terrain.data.get_control_scale(p_global_position), 0., 0., 1.) + _: + push_error("Unsupported picking type: ", picking) + return + if picking_callback.is_valid(): + picking_callback.call(picking, color, p_global_position) + picking_callback = Callable() + picking = Terrain3DEditor.TOOL_MAX + + elif operation_builder and operation_builder.is_picking(): + operation_builder.pick(p_global_position, plugin.terrain) + + +func set_button_editor_icon(p_button: Button, p_icon_name: String) -> void: + p_button.icon = EditorInterface.get_base_control().get_theme_icon(p_icon_name, "EditorIcons") diff --git a/addons/terrain_3d/src/ui.gd.uid b/addons/terrain_3d/src/ui.gd.uid new file mode 100644 index 0000000..bc20eee --- /dev/null +++ b/addons/terrain_3d/src/ui.gd.uid @@ -0,0 +1 @@ +uid://bpad72s36mwkx diff --git a/addons/terrain_3d/terrain.gdextension b/addons/terrain_3d/terrain.gdextension new file mode 100644 index 0000000..000d085 --- /dev/null +++ b/addons/terrain_3d/terrain.gdextension @@ -0,0 +1,32 @@ +[configuration] + +entry_symbol = "terrain_3d_init" +compatibility_minimum = 4.4 + +[icons] + +Terrain3D = "res://addons/terrain_3d/icons/terrain3d.svg" + +[libraries] + +windows.debug.x86_64 = "res://addons/terrain_3d/bin/libterrain.windows.debug.x86_64.dll" +windows.release.x86_64 = "res://addons/terrain_3d/bin/libterrain.windows.release.x86_64.dll" + +linux.debug.x86_64 = "res://addons/terrain_3d/bin/libterrain.linux.debug.x86_64.so" +linux.release.x86_64 = "res://addons/terrain_3d/bin/libterrain.linux.release.x86_64.so" +linux.debug.arm64 = "res://addons/terrain_3d/bin/libterrain.linux.debug.arm64.so" +linux.release.arm64 = "res://addons/terrain_3d/bin/libterrain.linux.release.arm64.so" +linux.debug.rv64 = "res://addons/terrain_3d/bin/libterrain.linux.debug.rv64.so" +linux.release.rv64 = "res://addons/terrain_3d/bin/libterrain.linux.release.rv64.so" + +macos.debug = "res://addons/terrain_3d/bin/libterrain.macos.debug.framework" +macos.release = "res://addons/terrain_3d/bin/libterrain.macos.release.framework" + +android.debug.arm64 = "res://addons/terrain_3d/bin/libterrain.android.debug.arm64.so" +android.release.arm64 = "res://addons/terrain_3d/bin/libterrain.android.release.arm64.so" + +ios.debug = "res://addons/terrain_3d/bin/libterrain.ios.debug.universal.dylib" +ios.release = "res://addons/terrain_3d/bin/libterrain.ios.release.universal.dylib" + +web.debug = "res://addons/terrain_3d/bin/libterrain.web.debug.wasm32.wasm" +web.release = "res://addons/terrain_3d/bin/libterrain.web.release.wasm32.wasm" diff --git a/addons/terrain_3d/terrain.gdextension.uid b/addons/terrain_3d/terrain.gdextension.uid new file mode 100644 index 0000000..ef58a55 --- /dev/null +++ b/addons/terrain_3d/terrain.gdextension.uid @@ -0,0 +1 @@ +uid://bdn2ygldx56a8 diff --git a/addons/terrain_3d/tools/importer.gd b/addons/terrain_3d/tools/importer.gd new file mode 100644 index 0000000..3dc0c10 --- /dev/null +++ b/addons/terrain_3d/tools/importer.gd @@ -0,0 +1,107 @@ +# Copyright © 2025 Cory Petkovsek, Roope Palmroos, and Contributors. +# Importer for Terrain3D +@tool +extends Terrain3D + + +@export var clear_all: bool = false : set = reset_settings +@export var clear_terrain: bool = false : set = reset_terrain +@export var update_height_range: bool = false : set = update_heights + + +func reset_settings(p_value) -> void: + if p_value: + height_file_name = "" + control_file_name = "" + color_file_name = "" + destination_directory = "" + import_position = Vector2i.ZERO + height_offset = 0.0 + import_scale = 1.0 + r16_range = Vector2(0, 1) + r16_size = Vector2i(1024, 1024) + material = null + assets = null + reset_terrain(true) + + +func reset_terrain(p_value) -> void: + data_directory = "" + for region:Terrain3DRegion in data.get_regions_active(): + data.remove_region(region, false) + data.update_maps(Terrain3DRegion.TYPE_MAX, true, false) + + +func update_heights(p_value) -> void: + if p_value and data: + data.update_height_range() + + +@export_group("Import File") +@export_global_file var height_file_name: String = "" +@export_global_file var control_file_name: String = "" +@export_global_file var color_file_name: String = "" +@export var import_position: Vector2i = Vector2i(0, 0) : set = set_import_position +@export var import_scale: float = 1.0 +@export var height_offset: float = 0.0 +@export var r16_range: Vector2 = Vector2(0, 1) +@export var r16_size: Vector2i = Vector2i(1024, 1024) : set = set_r16_size +@export var run_import: bool = false : set = start_import + +@export_dir var destination_directory: String = "" +@export var save_to_disk: bool = false : set = save_data + + +func set_import_position(p_value: Vector2i) -> void: + import_position.x = clamp(p_value.x, -8192, 8192) + import_position.y = clamp(p_value.y, -8192, 8192) + + +func set_r16_size(p_value: Vector2i) -> void: + r16_size.x = clamp(p_value.x, 0, 16384) + r16_size.y = clamp(p_value.y, 0, 16384) + + +func start_import(p_value: bool) -> void: + if p_value: + print("Terrain3DImporter: Importing files:\n\t%s\n\t%s\n\t%s" % [ height_file_name, control_file_name, color_file_name]) + + var imported_images: Array[Image] + imported_images.resize(Terrain3DRegion.TYPE_MAX) + var min_max := Vector2(0, 1) + var img: Image + if height_file_name: + img = Terrain3DUtil.load_image(height_file_name, ResourceLoader.CACHE_MODE_IGNORE, r16_range, r16_size) + min_max = Terrain3DUtil.get_min_max(img) + imported_images[Terrain3DRegion.TYPE_HEIGHT] = img + if control_file_name: + img = Terrain3DUtil.load_image(control_file_name, ResourceLoader.CACHE_MODE_IGNORE) + imported_images[Terrain3DRegion.TYPE_CONTROL] = img + if color_file_name: + img = Terrain3DUtil.load_image(color_file_name, ResourceLoader.CACHE_MODE_IGNORE) + imported_images[Terrain3DRegion.TYPE_COLOR] = img + if assets.get_texture_count() == 0: + material.show_checkered = false + material.show_colormap = true + var pos := Vector3(import_position.x, 0, import_position.y) + data.import_images(imported_images, pos, height_offset, import_scale) + print("Terrain3DImporter: Import finished") + + +func save_data(p_value: bool) -> void: + if destination_directory.is_empty(): + push_error("Set destination directory first") + return + data.save_directory(destination_directory) + + +@export_group("Export File") +enum { TYPE_HEIGHT, TYPE_CONTROL, TYPE_COLOR } +@export_enum("Height:0", "Control:1", "Color:2") var map_type: int = TYPE_HEIGHT +@export var file_name_out: String = "" +@export var run_export: bool = false : set = start_export + +func start_export(p_value: bool) -> void: + var err: int = data.export_image(file_name_out, map_type) + print("Terrain3DImporter: Export error status: ", err, " ", error_string(err)) + diff --git a/addons/terrain_3d/tools/importer.gd.uid b/addons/terrain_3d/tools/importer.gd.uid new file mode 100644 index 0000000..7a59538 --- /dev/null +++ b/addons/terrain_3d/tools/importer.gd.uid @@ -0,0 +1 @@ +uid://cib2rig7vup10 diff --git a/addons/terrain_3d/tools/importer.tscn b/addons/terrain_3d/tools/importer.tscn new file mode 100644 index 0000000..4bc43c3 --- /dev/null +++ b/addons/terrain_3d/tools/importer.tscn @@ -0,0 +1,63 @@ +[gd_scene load_steps=9 format=3 uid="uid://blaieaqp413k7"] + +[ext_resource type="Script" path="res://addons/terrain_3d/tools/importer.gd" id="1_60b8f"] + +[sub_resource type="Gradient" id="Gradient_88f3t"] +offsets = PackedFloat32Array(0.2, 1) +colors = PackedColorArray(1, 1, 1, 1, 0, 0, 0, 1) + +[sub_resource type="FastNoiseLite" id="FastNoiseLite_muvel"] +noise_type = 2 +frequency = 0.03 +cellular_jitter = 3.0 +cellular_return_type = 0 +domain_warp_enabled = true +domain_warp_type = 1 +domain_warp_amplitude = 50.0 +domain_warp_fractal_type = 2 +domain_warp_fractal_lacunarity = 1.5 +domain_warp_fractal_gain = 1.0 + +[sub_resource type="NoiseTexture2D" id="NoiseTexture2D_ve0yk"] +seamless = true +color_ramp = SubResource("Gradient_88f3t") +noise = SubResource("FastNoiseLite_muvel") + +[sub_resource type="Terrain3DMaterial" id="Terrain3DMaterial_p55u0"] +_shader_parameters = { +"blend_sharpness": 0.87, +"height_blending": true, +"macro_variation1": Color(1, 1, 1, 1), +"macro_variation2": Color(1, 1, 1, 1), +"noise1_angle": 0.0, +"noise1_offset": Vector2(0.5, 0.5), +"noise1_scale": 0.04, +"noise2_scale": 0.076, +"noise3_scale": 0.225, +"noise_texture": SubResource("NoiseTexture2D_ve0yk"), +"vertex_normals_distance": 128.0 +} +show_checkered = true + +[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_8rvqy"] +cull_mode = 2 +vertex_color_use_as_albedo = true +backlight_enabled = true +backlight = Color(0.5, 0.5, 0.5, 1) + +[sub_resource type="Terrain3DMeshAsset" id="Terrain3DMeshAsset_7je72"] +height_offset = 0.5 +density = 10.0 +material_override = SubResource("StandardMaterial3D_8rvqy") +generated_type = 1 + +[sub_resource type="Terrain3DAssets" id="Terrain3DAssets_op32e"] +mesh_list = Array[Terrain3DMeshAsset]([SubResource("Terrain3DMeshAsset_7je72")]) + +[node name="Importer" type="Terrain3D"] +material = SubResource("Terrain3DMaterial_p55u0") +assets = SubResource("Terrain3DAssets_op32e") +mesh_lods = 8 +top_level = true +script = ExtResource("1_60b8f") +metadata/_edit_lock_ = true diff --git a/addons/terrain_3d/utils/terrain_3d_objects.gd b/addons/terrain_3d/utils/terrain_3d_objects.gd new file mode 100644 index 0000000..3a19f6d --- /dev/null +++ b/addons/terrain_3d/utils/terrain_3d_objects.gd @@ -0,0 +1,191 @@ +# Copyright © 2025 Cory Petkovsek, Roope Palmroos, and Contributors. +# Objects parent for Terrain3D +# Children nodes get transform updates on sculpting +@tool +extends Node3D +class_name Terrain3DObjects + +const TransformChangedNotifier: Script = preload("res://addons/terrain_3d/utils/transform_changed_notifier.gd") + +const CHILD_HELPER_NAME: StringName = &"TransformChangedSignaller" +const CHILD_HELPER_PATH: NodePath = ^"TransformChangedSignaller" + +var _undo_redo = null +var _terrain_id: int +var _offsets: Dictionary # Object ID -> Vector3(X, Y offset relative to terrain height, Z) +var _ignore_transform_change: bool = false + + +func _enter_tree() -> void: + if not Engine.is_editor_hint(): + return + + for child in get_children(): + _on_child_entered_tree(child) + + child_entered_tree.connect(_on_child_entered_tree) + child_exiting_tree.connect(_on_child_exiting_tree) + + +func _exit_tree() -> void: + if not Engine.is_editor_hint(): + return + + child_entered_tree.disconnect(_on_child_entered_tree) + child_exiting_tree.disconnect(_on_child_exiting_tree) + + for child in get_children(): + _on_child_exiting_tree(child) + + +func editor_setup(p_plugin) -> void: + _undo_redo = p_plugin.get_undo_redo() + + +func get_terrain() -> Terrain3D: + var terrain := instance_from_id(_terrain_id) as Terrain3D + if not terrain or terrain.is_queued_for_deletion() or not terrain.is_inside_tree(): + var terrains: Array[Node] = Engine.get_singleton(&"EditorInterface").get_edited_scene_root().find_children("", "Terrain3D") + if terrains.size() > 0: + terrain = terrains[0] + _terrain_id = terrain.get_instance_id() if terrain else 0 + + if terrain and terrain.data and not terrain.data.maps_edited.is_connected(_on_maps_edited): + terrain.data.maps_edited.connect(_on_maps_edited) + + return terrain + + +func _get_terrain_height(p_global_position: Vector3) -> float: + var terrain: Terrain3D = get_terrain() + if not terrain or not terrain.data: + return 0.0 + var height: float = terrain.data.get_height(p_global_position) + if is_nan(height): + return 0.0 + return height + + +func _on_child_entered_tree(p_node: Node) -> void: + if not (p_node is Node3D): + return + + assert(p_node.get_parent() == self) + + var helper: TransformChangedNotifier = p_node.get_node_or_null(CHILD_HELPER_PATH) + if not helper: + helper = TransformChangedNotifier.new() + helper.name = CHILD_HELPER_NAME + p_node.add_child(helper, true, INTERNAL_MODE_BACK) + assert(p_node.has_node(CHILD_HELPER_PATH)) + + # When reparenting a Node3D, Godot changes its transform _after_ reparenting it. So here, + # we must use call_deferred, to avoid receiving transform_changed as a result of reparenting. + _setup_child_signal.call_deferred(p_node, helper) + + +func _setup_child_signal(p_node: Node, helper: TransformChangedNotifier) -> void: + if not p_node.is_inside_tree(): + return + if helper.transform_changed.is_connected(_on_child_transform_changed): + return + + helper.transform_changed.connect(_on_child_transform_changed.bind(p_node)) + _update_child_offset(p_node) + + +func _on_child_exiting_tree(p_node: Node) -> void: + if not (p_node is Node3D) or not p_node.has_node(CHILD_HELPER_PATH): + return + + var helper: TransformChangedNotifier = p_node.get_node_or_null(CHILD_HELPER_PATH) + if helper: + if helper.transform_changed.is_connected(_on_child_transform_changed): + helper.transform_changed.disconnect(_on_child_transform_changed) + p_node.remove_child(helper) + helper.queue_free() + + _offsets.erase(p_node.get_instance_id()) + + +func _is_node_selected(p_node: Node) -> bool: + var editor_sel = Engine.get_singleton(&"EditorInterface").get_selection() + return editor_sel.get_transformable_selected_nodes().has(p_node) + + +func _on_child_transform_changed(p_node: Node3D) -> void: + if _ignore_transform_change: + return + + var lmb_down := Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT) + if lmb_down and (_is_node_selected(p_node) or _is_node_selected(self)): + # The user may be moving the node using gizmos. + # We should wait until they're done before updating otherwise gizmos + this node conflict. + return + + if not _offsets.has(p_node.get_instance_id()): + return + + var old_offset: Vector3 = _offsets[p_node.get_instance_id()] + var old_h: float = _get_terrain_height(old_offset) + var old_position: Vector3 = old_offset + Vector3(0, old_h, 0) + var new_position: Vector3 = p_node.global_position + if old_position.is_equal_approx(new_position): + return + var new_h: float = _get_terrain_height(new_position) + var new_offset: Vector3 = new_position - Vector3(0, new_h, 0) + + var translate_without_reposition: bool = Input.is_key_pressed(KEY_SHIFT) + var y_changed: bool = not is_equal_approx(old_position.y, p_node.global_position.y) + if not y_changed and not translate_without_reposition: + new_offset.y = old_offset.y + new_position = new_offset + Vector3(0, new_h, 0) + + # Make sure that when the user undo's the translation, the offset change gets undone too! + _undo_redo.create_action("Translate", UndoRedo.MERGE_ALL) + _undo_redo.add_do_method(self, &"_set_offset_and_position", p_node.get_instance_id(), new_offset, new_position) + _undo_redo.add_undo_method(self, &"_set_offset_and_position", p_node.get_instance_id(), old_offset, old_position) + _undo_redo.commit_action() + + +func _set_offset_and_position(p_id: int, p_offset: Vector3, p_position: Vector3) -> void: + var node := instance_from_id(p_id) as Node + if not is_instance_valid(node): + return + + _ignore_transform_change = true + node.global_position = p_position + _offsets[p_id] = p_offset + _ignore_transform_change = false + + +# Overwrite current offset stored for node with its current Y position relative to the terrain +func _update_child_offset(p_node: Node3D) -> void: + var position: Vector3 = global_transform * p_node.position + var h: float = _get_terrain_height(position) + var offset: Vector3 = position - Vector3(0, h, 0) + _offsets[p_node.get_instance_id()] = offset + + +# Overwrite node's current position with terrain height + stored offset for this node +func _update_child_position(p_node: Node3D) -> void: + if not _offsets.has(p_node.get_instance_id()): + return + + var position: Vector3 = global_transform * p_node.position + var h: float = _get_terrain_height(position) + var offset: Vector3 = _offsets[p_node.get_instance_id()] + var new_position: Vector3 = global_transform.inverse() * (offset + Vector3(0, h, 0)) + if not p_node.position.is_equal_approx(new_position): + p_node.position = new_position + + +func _on_maps_edited(p_edited_aabb: AABB) -> void: + var edited_area: AABB = p_edited_aabb.grow(1) + edited_area.position.y = -INF + edited_area.end.y = INF + + for child in get_children(): + var node := child as Node3D + if node && edited_area.has_point(node.global_position): + _update_child_position(node) diff --git a/addons/terrain_3d/utils/terrain_3d_objects.gd.uid b/addons/terrain_3d/utils/terrain_3d_objects.gd.uid new file mode 100644 index 0000000..3a1e873 --- /dev/null +++ b/addons/terrain_3d/utils/terrain_3d_objects.gd.uid @@ -0,0 +1 @@ +uid://dbndw8p05yam7 diff --git a/addons/terrain_3d/utils/transform_changed_notifier.gd b/addons/terrain_3d/utils/transform_changed_notifier.gd new file mode 100644 index 0000000..5feaae8 --- /dev/null +++ b/addons/terrain_3d/utils/transform_changed_notifier.gd @@ -0,0 +1,16 @@ +# Copyright © 2025 Cory Petkovsek, Roope Palmroos, and Contributors. +# Transform Changed Notifier for Terrain3D +@tool +extends Node3D + +signal transform_changed + + +func _ready() -> void: + assert(Engine.is_editor_hint()) + set_notify_transform(true) + + +func _notification(what: int) -> void: + if what == NOTIFICATION_TRANSFORM_CHANGED: + transform_changed.emit() diff --git a/addons/terrain_3d/utils/transform_changed_notifier.gd.uid b/addons/terrain_3d/utils/transform_changed_notifier.gd.uid new file mode 100644 index 0000000..fc43825 --- /dev/null +++ b/addons/terrain_3d/utils/transform_changed_notifier.gd.uid @@ -0,0 +1 @@ +uid://claxtgppe8keq diff --git a/assets/models/environment/fire_pit_env.glb.import b/assets/models/environment/fire_pit_env.glb.import index c8bff7f..63a6ea9 100644 --- a/assets/models/environment/fire_pit_env.glb.import +++ b/assets/models/environment/fire_pit_env.glb.import @@ -32,6 +32,12 @@ animation/trimming=false animation/remove_immutable_tracks=true animation/import_rest_as_RESET=false import_script/path="" -_subresources={} +_subresources={ +"nodes": { +"PATH:Fire_Pit_Env": { +"generate/physics": true +} +} +} gltf/naming_version=1 gltf/embedded_image_handling=1 diff --git a/assets/textures/smoke_texture.png b/assets/textures/smoke_texture.png new file mode 100644 index 0000000..c71421e --- /dev/null +++ b/assets/textures/smoke_texture.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:18fa7bc7d5bbc4700978b4ebaac277e32262bcbb038953924c1b2895549978e6 +size 188346 diff --git a/assets/textures/smoke_texture.png.import b/assets/textures/smoke_texture.png.import new file mode 100644 index 0000000..422c81d --- /dev/null +++ b/assets/textures/smoke_texture.png.import @@ -0,0 +1,35 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://0acyl8dfoi6i" +path.s3tc="res://.godot/imported/smoke_texture.png-9fda8d109e95054a6c1cc37a1bdbbd90.s3tc.ctex" +metadata={ +"imported_formats": ["s3tc_bptc"], +"vram_texture": true +} + +[deps] + +source_file="res://assets/textures/smoke_texture.png" +dest_files=["res://.godot/imported/smoke_texture.png-9fda8d109e95054a6c1cc37a1bdbbd90.s3tc.ctex"] + +[params] + +compress/mode=2 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=true +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/project.godot b/project.godot index 958ee3f..731f163 100644 --- a/project.godot +++ b/project.godot @@ -15,6 +15,10 @@ run/main_scene="uid://dw0lbkh31rofd" config/features=PackedStringArray("4.4", "Forward Plus") config/icon="res://icon.svg" +[editor_plugins] + +enabled=PackedStringArray("res://addons/sky_3d/plugin.cfg", "res://addons/terrain_3d/plugin.cfg") + [global_group] Player="" diff --git a/src/scenes/characters/enemy.tscn b/src/scenes/characters/enemy.tscn index e5ed951..c446cae 100644 --- a/src/scenes/characters/enemy.tscn +++ b/src/scenes/characters/enemy.tscn @@ -73,10 +73,7 @@ bones/26/position = Vector3(2.04767e-08, 0.127245, 1.84988e-08) bones/26/rotation = Quaternion(-0.00454016, 0.003573, -0.0595397, 0.998209) bones/28/rotation = Quaternion(0.702583, -0.000939752, -0.00133551, 0.7116) bones/29/rotation = Quaternion(0.741121, 0.00218287, 0.00265386, 0.671363) -bones/30/rotation = Quaternion(0.32527, 0.890055, 0.295977, -0.119997) -bones/31/rotation = Quaternion(0.514882, -0.114149, 0.114482, 0.841879) bones/32/rotation = Quaternion(0.592003, -0.0381348, -0.0906989, 0.799907) -bones/34/position = Vector3(-0.00316938, 0.0987206, 0.0147918) bones/34/rotation = Quaternion(-0.06747, 0.718446, 0.0663966, 0.689112) bones/35/rotation = Quaternion(0.580803, -0.000892082, -0.00149751, 0.814042) bones/36/rotation = Quaternion(0.682777, 0.00174512, 0.002566, 0.73062) @@ -87,10 +84,7 @@ bones/39/rotation = Quaternion(0.630039, 0.00151958, 0.00237936, 0.776559) bones/40/rotation = Quaternion(0.652643, -0.00308136, -0.004211, 0.757647) bones/41/rotation = Quaternion(-0.0684174, 0.72411, 0.0332338, 0.685478) bones/42/position = Vector3(-0.00715364, 0.0679857, -0.0481983) -bones/42/rotation = Quaternion(-0.120913, 0.799109, 0.0358921, 0.587807) bones/43/rotation = Quaternion(0.498693, -0.0202705, -0.0256952, 0.866161) -bones/44/rotation = Quaternion(0.715385, 0.035306, 0.0573145, 0.695481) -bones/45/rotation = Quaternion(-0.129059, 0.728936, -0.0250259, 0.67184) bones/46/position = Vector3(-0.00763592, 1.41808, 0.0529702) bones/46/rotation = Quaternion(-0.509575, 0.218287, 0.374832, 0.743092) bones/47/position = Vector3(-0.163133, 1.45507, -0.0294819) @@ -106,24 +100,12 @@ bones/51/rotation = Quaternion(-0.00835387, -0.00217926, 0.0268349, 0.999603) bones/52/rotation = Quaternion(-0.2563, 0.855859, 0.0299185, -0.448241) bones/53/rotation = Quaternion(0.694912, 0.107145, -0.103548, 0.703488) bones/54/rotation = Quaternion(0.74112, -0.00218243, -0.00265405, 0.671363) -bones/55/rotation = Quaternion(-0.325269, 0.890055, 0.295977, 0.119998) -bones/56/rotation = Quaternion(0.514882, 0.11415, -0.114483, 0.841879) -bones/57/rotation = Quaternion(0.592004, 0.0381346, 0.0906992, 0.799907) bones/59/position = Vector3(0.00316941, 0.0987207, 0.0147918) -bones/59/rotation = Quaternion(0.138771, -0.704916, 0.15571, 0.677932) bones/60/rotation = Quaternion(0.579984, 0.0462614, -0.0308755, 0.812727) bones/61/rotation = Quaternion(0.682777, -0.00174717, -0.0025645, 0.73062) -bones/62/rotation = Quaternion(-0.00426532, -0.736714, -0.0394788, 0.675038) bones/63/position = Vector3(0.00622955, 0.0926416, -0.0236344) -bones/63/rotation = Quaternion(0.104095, -0.719607, 0.186262, 0.660784) -bones/64/rotation = Quaternion(0.670693, 0.0163391, -0.0185663, 0.741323) bones/65/rotation = Quaternion(0.692499, 0.00330364, 0.00403915, 0.7214) -bones/66/rotation = Quaternion(-0.0684178, -0.72411, -0.0332339, 0.685477) bones/67/position = Vector3(0.0071537, 0.0679858, -0.0481984) -bones/67/rotation = Quaternion(0.108364, -0.768224, 0.166291, 0.608635) -bones/68/rotation = Quaternion(0.629954, 0.0295149, 0.0177509, 0.775869) -bones/69/rotation = Quaternion(0.816678, -0.0439534, -0.0509861, 0.573155) -bones/70/rotation = Quaternion(-0.12906, -0.728937, 0.0250258, 0.67184) bones/71/position = Vector3(0.131284, 1.33873, -0.0170255) bones/71/rotation = Quaternion(-0.00161625, 0.60429, 0.796762, -0.00107789) bones/72/position = Vector3(-0.109309, 1.33794, -0.0175146) diff --git a/src/scenes/levels/level.tscn b/src/scenes/levels/level.tscn index 2ce2abf..6ac0657 100644 --- a/src/scenes/levels/level.tscn +++ b/src/scenes/levels/level.tscn @@ -1,37 +1,167 @@ -[gd_scene load_steps=11 format=3 uid="uid://dw0lbkh31rofd"] +[gd_scene load_steps=17 format=3 uid="uid://dw0lbkh31rofd"] +[ext_resource type="Script" uid="uid://bmywk4wvcp0lr" path="res://addons/sky_3d/src/Sky3D.gd" id="1_1jhfv"] [ext_resource type="PackedScene" uid="uid://nsv4lbw7j8mi" path="res://src/scenes/characters/player.tscn" id="1_crbv0"] [ext_resource type="PackedScene" uid="uid://b1fe4n68iivfm" path="res://assets/models/environment/rock_cliff_env_03.glb" id="2_d2tjv"] +[ext_resource type="Script" uid="uid://27fj74ofndim" path="res://addons/sky_3d/src/Skydome.gd" id="2_juj6f"] +[ext_resource type="Script" uid="uid://bm0hx4mklpml" path="res://addons/sky_3d/src/TimeOfDay.gd" id="3_0eo66"] [ext_resource type="PackedScene" uid="uid://duotcsmd2fwkk" path="res://assets/models/environment/bush_vege_2.glb" id="3_fqsoq"] [ext_resource type="PackedScene" uid="uid://bdg8366pt8iu7" path="res://assets/models/environment/topiary_tree_vege_01.glb" id="4_uvsco"] [ext_resource type="PackedScene" uid="uid://ddh3p0sg6i080" path="res://assets/models/environment/tree_group_vege.glb" id="5_eyekk"] [ext_resource type="PackedScene" uid="uid://cwvrti8hmxj0k" path="res://src/scenes/characters/enemy.tscn" id="6_uvsco"] +[ext_resource type="PackedScene" uid="uid://6dmbuecqolod" path="res://src/scenes/structures/fire_pit.tscn" id="10_juj6f"] +[ext_resource type="PackedScene" uid="uid://rkhiaud0rlq6" path="res://src/scenes/structures/lamp.tscn" id="11_0eo66"] -[sub_resource type="ProceduralSkyMaterial" id="ProceduralSkyMaterial_amufq"] -sky_horizon_color = Color(0.662243, 0.671743, 0.686743, 1) -ground_horizon_color = Color(0.662243, 0.671743, 0.686743, 1) +[sub_resource type="PhysicalSkyMaterial" id="PhysicalSkyMaterial_dn2un"] +use_debanding = false -[sub_resource type="Sky" id="Sky_crbv0"] -sky_material = SubResource("ProceduralSkyMaterial_amufq") +[sub_resource type="Sky" id="Sky_7iny7"] +sky_material = SubResource("PhysicalSkyMaterial_dn2un") -[sub_resource type="Environment" id="Environment_d2tjv"] +[sub_resource type="Environment" id="Environment_cxs0p"] background_mode = 2 -sky = SubResource("Sky_crbv0") -tonemap_mode = 2 -glow_enabled = true +sky = SubResource("Sky_7iny7") +ambient_light_source = 3 +ambient_light_color = Color(0.235156, 0.278907, 0.35, 1) +ambient_light_sky_contribution = 0.7 +reflected_light_source = 2 +tonemap_mode = 3 +tonemap_white = 6.0 + +[sub_resource type="CameraAttributesPractical" id="CameraAttributesPractical_0slur"] [sub_resource type="StandardMaterial3D" id="StandardMaterial3D_d2tjv"] albedo_color = Color(0.321569, 0.317647, 0.117647, 1) [node name="Level" type="Node3D"] -[node name="WorldEnvironment" type="WorldEnvironment" parent="."] -environment = SubResource("Environment_d2tjv") +[node name="Sky3D" type="WorldEnvironment" parent="."] +environment = SubResource("Environment_cxs0p") +camera_attributes = SubResource("CameraAttributesPractical_0slur") +script = ExtResource("1_1jhfv") +current_time = 2.13 +metadata/_custom_type_script = "uid://bmywk4wvcp0lr" -[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."] -transform = Transform3D(-0.866025, -0.433013, 0.25, 0, 0.5, 0.866025, -0.5, 0.75, -0.433013, 0, 0, 0) +[node name="SunLight" type="DirectionalLight3D" parent="Sky3D"] +transform = Transform3D(0, -0.423059, 0.906102, 0.999297, -0.0339567, -0.0158544, 0.0374756, 0.905466, 0.422762, 0.906102, -0.0158543, 0.422762) +light_color = Color(0.98, 0.523, 0.294, 1) +light_energy = 0.0 shadow_enabled = true +[node name="MoonLight" type="DirectionalLight3D" parent="Sky3D"] +transform = Transform3D(0, -0.956847, -0.290593, -0.200031, -0.28472, 0.937509, -0.97979, 0.0581276, -0.191399, -0.290592, 0.937509, -0.191399) +light_color = Color(0.572549, 0.776471, 0.956863, 1) +light_energy = 0.281253 +shadow_enabled = true + +[node name="Skydome" type="Node" parent="Sky3D"] +script = ExtResource("2_juj6f") +sky_visible = true +dome_radius = 10.0 +tonemap_level = 0.0 +exposure = 1.3 +ground_color = Color(0.3, 0.3, 0.3, 1) +sky_layers = 4 +sky_render_priority = -128 +horizon_level = 0.0 +sun_altitude = -147.012 +sun_azimuth = -107.961 +sun_disk_color = Color(0.996094, 0.541334, 0.140076, 1) +sun_disk_intensity = 2.0 +sun_disk_size = 0.015 +sun_light_path = NodePath("../SunLight") +sun_light_color = Color(1, 1, 1, 1) +sun_horizon_light_color = Color(0.98, 0.523, 0.294, 1) +sun_light_energy = 1.0 +moon_altitude = -20.3627 +moon_azimuth = -303.371 +moon_color = Color(1, 1, 1, 1) +moon_size = 0.07 +enable_set_moon_texture = false +moon_resolution = 2 +moon_light_path = NodePath("../MoonLight") +moon_light_color = Color(0.572549, 0.776471, 0.956863, 1) +moon_light_energy = 0.3 +deep_space_euler = Vector3(1.29154, 3.14159, -2.30693) +background_color = Color(0.709804, 0.709804, 0.709804, 0.854902) +set_background_texture = false +stars_field_color = Color(1, 1, 1, 1) +set_stars_field_texture = false +stars_scintillation = 0.75 +stars_scintillation_speed = 0.01 +atm_quality = 1 +atm_wavelenghts = Vector3(680, 550, 440) +atm_darkness = 0.5 +atm_sun_intensity = 18.0 +atm_day_tint = Color(0.807843, 0.909804, 1, 1) +atm_horizon_light_tint = Color(0.980392, 0.635294, 0.462745, 1) +atm_enable_moon_scatter_mode = false +atm_night_tint = Color(0.168627, 0.2, 0.25098, 1) +atm_level_params = Vector3(1, 0, 0) +atm_thickness = 0.7 +atm_mie = 0.07 +atm_turbidity = 0.001 +atm_sun_mie_tint = Color(1, 1, 1, 1) +atm_sun_mie_intensity = 1.0 +atm_sun_mie_anisotropy = 0.8 +atm_moon_mie_tint = Color(0.137255, 0.184314, 0.292196, 1) +atm_moon_mie_intensity = 0.7 +atm_moon_mie_anisotropy = 0.8 +fog_visible = true +fog_atm_level_params_offset = Vector3(0, 0, -1) +fog_density = 0.00015 +fog_rayleigh_depth = 0.116 +fog_mie_depth = 0.0001 +fog_falloff = 3.0 +fog_start = 0.0 +fog_end = 1000.0 +fog_layers = 524288 +fog_render_priority = 123 +clouds_thickness = 1.7 +clouds_coverage = 0.5 +clouds_absorption = 2.0 +clouds_sky_tint_fade = 0.5 +clouds_intensity = 10.0 +clouds_size = 2.0 +clouds_uv = Vector2(0.16, 0.11) +clouds_direction = Vector2(0.25, 0.25) +clouds_speed = 0.07 +set_clouds_texture = false +clouds_cumulus_visible = true +clouds_cumulus_day_color = Color(0.823529, 0.87451, 1, 1) +clouds_cumulus_horizon_light_color = Color(0.98, 0.43, 0.15, 1) +clouds_cumulus_night_color = Color(0.090196, 0.094118, 0.129412, 1) +clouds_cumulus_thickness = 0.0243 +clouds_cumulus_coverage = 0.55 +clouds_cumulus_absorption = 2.0 +clouds_cumulus_noise_freq = 2.7 +clouds_cumulus_intensity = 1.0 +clouds_cumulus_mie_intensity = 1.0 +clouds_cumulus_mie_anisotropy = 0.206 +clouds_cumulus_size = 0.5 +clouds_cumulus_direction = Vector3(0.25, 0.1, 0.25) +clouds_cumulus_speed = 0.05 +set_clouds_cumulus_texture = false +environment = SubResource("Environment_cxs0p") + +[node name="TimeOfDay" type="Node" parent="Sky3D"] +script = ExtResource("3_0eo66") +update_in_game = false +update_in_editor = false +dome_path = NodePath("../Skydome") +system_sync = false +total_cycle_in_minutes = 15.0 +total_hours = 2.13 +day = 1 +month = 1 +year = 2025 +celestials_calculations = 1 +compute_moon_coords = true +compute_deep_space_coords = true +latitude = 16.0 +longitude = 108.0 +utc = 7.0 + [node name="Floor" type="CSGBox3D" parent="."] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.5, 0) use_collision = true @@ -109,3 +239,9 @@ transform = Transform3D(0.910684, 0, 0.413104, 0, 1, 0, -0.413104, 0, 0.910684, [node name="Enemy4" parent="Enemies" instance=ExtResource("6_uvsco")] transform = Transform3D(-0.503623, 0, 0.863923, 0, 1, 0, -0.863923, 0, -0.503623, -5.81376, 1, 15.3224) + +[node name="FirePit" parent="." instance=ExtResource("10_juj6f")] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 9.96342, 0, 3.24593) + +[node name="Lamp" parent="." instance=ExtResource("11_0eo66")] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -16.5327, -3.57628e-07, -11.138) diff --git a/src/scenes/structures/fire_pit.tscn b/src/scenes/structures/fire_pit.tscn new file mode 100644 index 0000000..a95cfba --- /dev/null +++ b/src/scenes/structures/fire_pit.tscn @@ -0,0 +1,48 @@ +[gd_scene load_steps=8 format=3 uid="uid://6dmbuecqolod"] + +[ext_resource type="PackedScene" uid="uid://sh4wfmyjjve" path="res://assets/models/environment/fire_pit_env.glb" id="1_klngy"] +[ext_resource type="Texture2D" uid="uid://0acyl8dfoi6i" path="res://assets/textures/smoke_texture.png" id="2_kfkh6"] + +[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_klngy"] +transparency = 1 +vertex_color_use_as_albedo = true +albedo_color = Color(0.936458, 0.662528, 0.102757, 1) +albedo_texture = ExtResource("2_kfkh6") +emission_enabled = true +emission = Color(0.647612, 0.431395, 0.0657308, 1) +emission_energy_multiplier = 5.0 +billboard_mode = 1 + +[sub_resource type="Gradient" id="Gradient_b2ijf"] +offsets = PackedFloat32Array(0, 0.527197, 1) +colors = PackedColorArray(0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0) + +[sub_resource type="GradientTexture1D" id="GradientTexture1D_magng"] +gradient = SubResource("Gradient_b2ijf") + +[sub_resource type="ParticleProcessMaterial" id="ParticleProcessMaterial_kfkh6"] +emission_shape_scale = Vector3(0.5, 0.5, 0.5) +emission_shape = 1 +emission_sphere_radius = 1.0 +gravity = Vector3(0, 3, 0) +color_ramp = SubResource("GradientTexture1D_magng") + +[sub_resource type="QuadMesh" id="QuadMesh_b2ijf"] + +[node name="FirePit" type="Node3D"] + +[node name="fire_pit_env" parent="." instance=ExtResource("1_klngy")] + +[node name="Fire" type="GPUParticles3D" parent="fire_pit_env"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.615283, 0) +material_override = SubResource("StandardMaterial3D_klngy") +cast_shadow = 0 +lifetime = 1.25 +process_material = SubResource("ParticleProcessMaterial_kfkh6") +draw_pass_1 = SubResource("QuadMesh_b2ijf") + +[node name="OmniLight3D" type="OmniLight3D" parent="fire_pit_env"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.645023, 0) +light_color = Color(0.900353, 0.606455, 0.0980959, 1) +light_energy = 10.0 +omni_range = 6.56384 diff --git a/src/scenes/structures/lamp.tscn b/src/scenes/structures/lamp.tscn new file mode 100644 index 0000000..882be07 --- /dev/null +++ b/src/scenes/structures/lamp.tscn @@ -0,0 +1,13 @@ +[gd_scene load_steps=2 format=3 uid="uid://rkhiaud0rlq6"] + +[ext_resource type="PackedScene" uid="uid://dolbrwcjgq3yb" path="res://assets/models/environment/lamppost_furn.glb" id="1_mmulg"] + +[node name="Lamp" type="Node3D"] + +[node name="lamppost_furn" parent="." instance=ExtResource("1_mmulg")] + +[node name="OmniLight3D" type="OmniLight3D" parent="lamppost_furn"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.471, 2.92497, 0) +light_color = Color(0.900353, 0.606455, 0.0980959, 1) +light_energy = 10.0 +omni_range = 10.6869 diff --git a/src/scripts/characters/area_attack.gd b/src/scripts/characters/area_attack.gd index 806b6ba..8bc3898 100644 --- a/src/scripts/characters/area_attack.gd +++ b/src/scripts/characters/area_attack.gd @@ -1,7 +1,9 @@ extends ShapeCast3D -func deal_damage(damage: float) -> void: +func deal_damage(damage: float, crit_chance: float) -> void: for collision_id in get_collision_count(): + var is_critical = randf() <= crit_chance var collider = get_collider(collision_id) + if collider is Player or collider is Enemy: - collider.health_component.take_damage(damage) + collider.health_component.take_damage(damage, is_critical) diff --git a/src/scripts/characters/attack_cast.gd b/src/scripts/characters/attack_cast.gd index 0ad1f0e..f53921a 100644 --- a/src/scripts/characters/attack_cast.gd +++ b/src/scripts/characters/attack_cast.gd @@ -1,10 +1,12 @@ extends RayCast3D -func deal_damage(damage: float) -> void: +func deal_damage(damage: float, crit_chance: float) -> void: if !is_colliding(): return + var is_critical = randf() <= crit_chance var collider = get_collider() + if collider is Enemy: - collider.health_component.take_damage(damage) + collider.health_component.take_damage(damage, is_critical) add_exception(collider) diff --git a/src/scripts/characters/enemy.gd b/src/scripts/characters/enemy.gd index 630490d..aff81fb 100644 --- a/src/scripts/characters/enemy.gd +++ b/src/scripts/characters/enemy.gd @@ -1,8 +1,9 @@ -extends CharacterBody3D -class_name Enemy +class_name Enemy extends CharacterBody3D @export var max_health: float = 20.0 @export var xp_value: int = 30 +@export var damage: float = 20.0 +@export var crit_chance: float = 0.05 @onready var rig: Node3D = $Rig @onready var health_component: HealthComponent = $HealthComponent @@ -16,7 +17,7 @@ func _ready() -> void: rig.set_active_mesh(mesh) health_component.update_max_health(max_health) -func _physics_process(delta: float) -> void: +func _physics_process(_delta: float) -> void: if rig.is_idle(): check_for_attacks() @@ -33,4 +34,4 @@ func _on_health_component_defeat() -> void: set_physics_process(false) func _on_rig_heavy_attack() -> void: - area_attack.deal_damage(20.0) + area_attack.deal_damage(damage, crit_chance) diff --git a/src/scripts/characters/health_component.gd b/src/scripts/characters/health_component.gd index 875be17..6fe9648 100644 --- a/src/scripts/characters/health_component.gd +++ b/src/scripts/characters/health_component.gd @@ -16,5 +16,9 @@ func update_max_health(max_hp_in: float) -> void: max_health = max_hp_in current_health = max_health -func take_damage(damage_in: float) -> void: - current_health -= damage_in +func take_damage(damage_in: float, is_critical: bool) -> void: + var damage = damage_in + if is_critical: + damage *= 2.0 + print("Is critical hit") + current_health -= damage diff --git a/src/scripts/characters/player.gd b/src/scripts/characters/player.gd index 52a6bd5..49e04bb 100644 --- a/src/scripts/characters/player.gd +++ b/src/scripts/characters/player.gd @@ -121,7 +121,8 @@ func handle_slashing_physics_frame(delta: float) -> void: velocity.x = _attack_direction.x * attack_move_speed velocity.z = _attack_direction.z * attack_move_speed look_toward_direction(_attack_direction, delta) - attack_cast.deal_damage(10.0 + stats.strength.get_modifier()) + var damage = 10.0 + stats.strength.get_modifier() + attack_cast.deal_damage(damage, stats.agility.get_modifier()) func handle_overhead_physics_frame() -> void: if !rig.is_overhead(): @@ -135,4 +136,5 @@ func _on_health_component_defeat() -> void: set_physics_process(false) func _on_rig_heavy_attack() -> void: - area_attack.deal_damage(10.0 + stats.strength.get_modifier()) + var damage = 10.0 + stats.strength.get_modifier() + area_attack.deal_damage(damage, stats.agility.get_modifier()) diff --git a/src/shaders/fire.tres b/src/shaders/fire.tres new file mode 100644 index 0000000..086426b --- /dev/null +++ b/src/shaders/fire.tres @@ -0,0 +1,9 @@ +[gd_resource type="VisualShader" format=3 uid="uid://ck7nn7v3pytdr"] + +[resource] +code = "shader_type spatial; +render_mode blend_mix, depth_draw_opaque, cull_back, diffuse_lambert, specular_schlick_ggx; + + + +"