This commit is contained in:
2026-10-08 17:38:08 +08:00
parent 4765dfaa02
commit 27b9788a04
78 changed files with 24546 additions and 1 deletions
+26
View File
@@ -0,0 +1,26 @@
MIT License
Copyright (c) 2023-2026 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.
+73
View File
@@ -0,0 +1,73 @@
![image](https://github.com/TokisanGames/Sky3D/blob/main/screenshots/oota-windmill.jpg)
# Sky3D
A dynamic day/night cycle for Godot Engine 4, written in GDScript.
## Features
* Supports Godot 4.3+, Forward, Mobile, and Compatibility renderers
* Automatically rotating sun, moon, and stars, with moon phases
* Dynamic atmosphere, fog, and clouds that change with the day cycle
* Consolidated controls to manage lighting and camera exposure
* Management of game time: current time, day length, day or night
## Screenshots
![image](https://github.com/TokisanGames/Sky3D/blob/main/screenshots/sky3d.jpg)
![image](https://github.com/TokisanGames/Sky3D/blob/main/screenshots/oota-forest.jpg)
## Installation
* Clone or download the repository.
* Open the project in Godot and run `demo/Sky3DDemo.tscn` to test it.
* Copy `addons/sky_3d` into your project `addons` directory. Create the folder if missing.
* Open `Project -> Project Settings -> Plugins` and enable the plugin.
## Usage
* Create or open a Scene.
* Remove any existing `WorldEnvironment` node.
* Create a new `Sky3D` node.
* Customize the settings of the `Sky3D`, `Sky3D/Environment`, `TimeOfDay`, `SkyDome`, `SunLight`, and `MoonLight` nodes. Some settings like light energy, color, and angle are driven by Sky3D and not directly changeable on the light nodes. You'll know if they are reset on time updates. Adjust those settings in `Sky3D` or `SkyDome`.
## Compatibility Renderer Note
This render needs a bit of adjustment to show similar results to Vulkan:
* `Sky3D / Sky Contribution = 0.75`
* `SkyDome / Fog / Fog Density = 0.01`
## Documentation
The documentation is built in to Godot. Look for tooltips in the inspector where available, or press F1 and search for `Sky3D`, `SkyDome`, `TimeOfDay`.
## Support
For support, join our [Discord server](https://tokisan.com/discord).
## Credit
Developed for the Godot community by:
|||
|--|--|
| **Cory Petkovsek, Tokisan Games** | [<img src="https://github.com/dmhendricks/signature-social-icons/blob/master/icons/round-flat-filled/35px/twitter.png?raw=true" width="24"/>](https://twitter.com/TokisanGames) [<img src="https://github.com/dmhendricks/signature-social-icons/blob/master/icons/round-flat-filled/35px/github.png?raw=true" width="24"/>](https://github.com/TokisanGames) [<img src="https://github.com/dmhendricks/signature-social-icons/blob/master/icons/round-flat-filled/35px/www.png?raw=true" width="24"/>](https://tokisan.com/) [<img src="https://github.com/dmhendricks/signature-social-icons/blob/master/icons/round-flat-filled/35px/discord.png?raw=true" width="24"/>](https://tokisan.com/discord) [<img src="https://github.com/dmhendricks/signature-social-icons/blob/master/icons/round-flat-filled/35px/youtube.png?raw=true" width="24"/>](https://www.youtube.com/@TokisanGames)|
**And all of the wonderful contributors shown in the right sidebar of the [github repository](https://github.com/TokisanGames/Sky3D/).**
The original version of this plugin, *TimeOfDay v1*, was made by [J. Cuéllar](https://twitter.com/JayKuellar) for Godot 3. You can find it in the `godot3` branch. The original repository was deleted. We revived it, ported the GDScript version to Godot 4, and have continued to build on it to produce Sky3D.
## License
This addon has been released under the [MIT License](LICENSE.txt).
If using the star map assets, you must [credit the author](https://github.com/TokisanGames/Sky3D/blob/main/addons/sky_3d/assets/thirdparty/textures/milkyway/LICENSE.md).
+8
View File
@@ -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)
@@ -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")
@@ -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
Binary file not shown.

After

Width:  |  Height:  |  Size: 860 B

@@ -0,0 +1,40 @@
[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/uastc_level=0
compress/rdo_quality_loss=0.0
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/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
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
Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

@@ -0,0 +1,41 @@
[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/uastc_level=0
compress/rdo_quality_loss=0.0
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/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
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
Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

@@ -0,0 +1,41 @@
[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/uastc_level=0
compress/rdo_quality_loss=0.0
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/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
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
Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

@@ -0,0 +1,41 @@
[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/uastc_level=0
compress/rdo_quality_loss=0.0
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/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
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
@@ -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.
```
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 MiB

@@ -0,0 +1,41 @@
[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/uastc_level=0
compress/rdo_quality_loss=0.0
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/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
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
Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

@@ -0,0 +1,41 @@
[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/uastc_level=0
compress/rdo_quality_loss=0.0
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/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
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
@@ -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
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 MiB

@@ -0,0 +1,41 @@
[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/uastc_level=0
compress/rdo_quality_loss=0.0
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/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
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
+7
View File
@@ -0,0 +1,7 @@
[plugin]
name="Sky3D"
description="Atmospheric Day/Night Cycle"
author="Cory Petkovsek, J. Cuéllar, and Contributors"
version="2.1"
script="src/Plugin.gd"
+161
View File
@@ -0,0 +1,161 @@
// Copyright (c) 2023-2025 Cory Petkovsek and Contributors
// Copyright (c) 2021 J. Cuellar
shader_type spatial;
render_mode blend_mix, cull_front, unshaded, fog_disabled;
uniform vec2 color_correction;
uniform float sea_level: hint_range(-2048,2048,.01) = 0.0;
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;
varying vec4 v_angle_mult;
#include "Common.gdshaderinc"
// Atmosphere
vec3 calc_atmospheric_scatter(float sr, float sm, vec2 mu, vec3 mult, float depth) {
vec3 beta_mie = atm_beta_mie;
vec3 beta_ray = atm_beta_ray * atm_thickness;
vec3 extc_factor = clamp_rgb_to_unit(exp(-(beta_ray * sr + beta_mie * sm)));
float extc_ff = mix(clamp_to_unit(atm_thickness * 0.5), 1.0, mult.x);
vec3 final_extc_factor = mix(1.0 - extc_factor, (1.0 - extc_factor) * extc_factor, extc_ff);
float rayleigh_phase = calc_rayleigh_phase(mu.x);
vec3 BRT = beta_ray * rayleigh_phase * clamp_to_unit(depth * fog_rayleigh_depth);
vec3 BMT = beta_mie * calc_mie_phase(mu.x, atm_sun_partial_mie_phase);
BMT *= atm_sun_mie_intensity * atm_sun_mie_tint.rgb * clamp_to_unit(depth * fog_mie_depth);
vec3 BRMT = (BRT + BMT) / (beta_ray + beta_mie);
vec3 scatter = atm_sun_intensity * (BRMT * final_extc_factor) * atm_day_tint.rgb * mult.y;
scatter = mix(scatter, scatter * (1.0 - extc_factor), atm_darkness);
vec3 lcol = mix(atm_day_tint.rgb, atm_horizon_light_tint.rgb, mult.x);
vec3 nscatter = (1.0 - extc_factor) * atm_night_tint.rgb * clamp_to_unit(depth * fog_rayleigh_depth);
nscatter += calc_mie_phase(mu.y, atm_moon_partial_mie_phase) *
atm_moon_mie_tint.rgb * atm_moon_mie_intensity * 0.005 * clamp_to_unit(depth * fog_mie_depth);
return (scatter * lcol) + nscatter;
}
// Fog
float calc_fog_exp(float depth, float density) {
return 1.0 - clamp_to_unit(exp2(-depth * density));
}
float calc_fog_falloff(float y, float zeroLevel, float falloff) {
return clamp_to_unit(exp(-(y + zeroLevel) * falloff));
}
float calc_fog_distance(float depth) {
float d = depth;
d = (fog_end - d) / (fog_end - fog_start);
return clamp_to_unit(1.0 - d);
}
// Main
void vertex() {
POSITION = vec4(VERTEX.xy, 1.0, 1.0);
v_angle_mult = vec4(
clamp_to_unit(1.0 - sun_direction.y),
clamp_to_unit(sun_direction.y + 0.45),
clamp_to_unit(-sun_direction.y + 0.30),
clamp_to_unit(-sun_direction.y + 0.60));
}
void fragment() {
// Calculate world pixel position from depth
float depth_raw = texture(DEPTH_TEXTURE, SCREEN_UV).r;
#ifndef CURRENT_RENDERER
// For 4.3 or earlier, will break if far plane is less than 1m.
depth_raw = PROJECTION_MATRIX[3][2] > PROJECTION_MATRIX[2][2] ? depth_raw : depth_raw * 2.0 - 1.0;
#else
#if CURRENT_RENDERER == RENDERER_COMPATIBILITY
depth_raw = depth_raw * 2.0 - 1.0;
#endif
#endif
vec3 ndc = vec3(SCREEN_UV * 2.0 - 1.0, depth_raw);
vec4 clip_pos = INV_PROJECTION_MATRIX * vec4(ndc, 1.0);
vec3 view_pos = clip_pos.xyz / clip_pos.w;
vec4 world_pos_hom = INV_VIEW_MATRIX * vec4(view_pos, 1.0);
vec3 world_position = world_pos_hom.xyz / world_pos_hom.w;
// Ray from camera to world intersection point
vec3 intersection_ray = world_position - CAMERA_POSITION_WORLD;
// Unit ray direction
vec3 ray_direction = normalize(intersection_ray);
//float linear_depth = -view_pos.z; // Approximate ray distance
//float ray_distance = length(intersection_ray); // Exact ray distance
// Cap ray if there's a sea_level plane intersection
float original_distance = length(intersection_ray);
// Y difference from camera to sea_level
float dy = sea_level - CAMERA_POSITION_WORLD.y;
// Distance along ray to sea_level plane
float plane_distance = dy / (ray_direction.y + 1e-6 * sign(ray_direction.y)); // avoid divide by zero
float plane_hit = float(ray_direction.y < 0.) *
float(plane_distance > 0.) *
float(plane_distance < original_distance);
// Use final distance or t_plane distance Final distance: mix original vs plane distance
float ray_distance = mix(original_distance, plane_distance, plane_hit);
// Capped world position
//world_position = CAMERA_POSITION_WORLD + ray_distance * ray_direction;
float fog_factor = calc_fog_exp(ray_distance, fog_density);
fog_factor *= calc_fog_falloff(ray_direction.y, 0.0, fog_falloff);
fog_factor *= calc_fog_distance(ray_distance);
vec2 mu = vec2(
dot(sun_direction, ray_direction),
dot(moon_direction, ray_direction)
);
float sr, sm;
calc_simple_optical_depth(ray_direction.y + atm_level_params.z, sr, sm, atm_level_params.xy);
vec3 scatter = calc_atmospheric_scatter(sr, sm, mu, v_angle_mult.xyz, ray_distance);
vec4 fog_color = vec4(scatter, fog_factor);
fog_color.a = clamp(fog_color.a, 0.0, 1.0);
fog_color.rgb = apply_photo_tonemap(fog_color.rgb, color_correction.y, color_correction.x);
ALBEDO = fog_color.rgb;
ALPHA = fog_color.a;
// View depth map
//ALPHA = 1.;
//ALBEDO = vec3(ray_distance*.0001);
}
@@ -0,0 +1 @@
uid://bqaetpq827vg6
+64
View File
@@ -0,0 +1,64 @@
// Copyright (c) 2023-2025 Cory Petkovsek and Contributors
// Copyright (c) 2021 J. Cuellar
const float INV_PI = 1. / PI;
const float HALF_PI = PI / 2.;
const float INV_HALF_PI = 1. / HALF_PI;
const float QRT_PI = PI / 4.;
const float INV_QRT_PI = 1. / QRT_PI;
const float PI4 = PI * 4.;
const float INV_PI4 = 1. / PI4;
const float INV_TAU = 1. / TAU;
const float RAYLEIGH_ZENITH_LENGTH = 8.4e3;
const float MIE_ZENITH_LENGTH = 1.25e3;
const float THREE_OVER_EIGHT_PI = 3.0 / (8.0 * PI);
float clamp_to_unit(float value) {
return clamp(value, 0.0, 1.0);
}
vec3 clamp_rgb_to_unit(vec3 value) {
return clamp(value.rgb, 0.0, 1.0);
}
vec3 adjust_contrast(vec3 vec, float level) {
return mix(vec, vec * vec * vec, level); // pow3
}
vec3 apply_photo_tonemap(vec3 color, float exposure, float level) {
color.rgb *= exposure;
return mix(color.rgb, 1.0 - exp(-color.rgb), level);
}
float calc_rayleigh_phase(float mu) {
return THREE_OVER_EIGHT_PI * (1.0 + mu * mu);
}
float calc_mie_phase(float mu, vec3 partial) {
float denominator = partial.y - partial.z * mu;
float safe_denominator = max(denominator, 0.0001); // Prevent division by zero on negative powers
return PI4 * partial.x * pow(safe_denominator, -1.5);
}
// Simplifield for more performance
void calc_simple_optical_depth(float y, out float sr, out float sm, vec2 params) {
y = max(0.03, y + 0.03) + params.y;
y = 1.0 / (y * params.x);
sr = y * RAYLEIGH_ZENITH_LENGTH;
sm = y * MIE_ZENITH_LENGTH;
}
// Paper based (Unused)
void calc_optical_depth(float y, out float sr, out float sm, vec2 params) {
y = max(0.0, y);
y = clamp_to_unit(y * params.x);
float zenith = acos(y);
zenith = cos(zenith) + 0.15 * pow(93.885 - ((zenith * 180.0) * INV_PI), -1.253);
zenith = 1.0 / (zenith + params.y);
sr = zenith * RAYLEIGH_ZENITH_LENGTH;
sm = zenith * MIE_ZENITH_LENGTH;
}
@@ -0,0 +1 @@
uid://0auvnvj85575
+21
View File
@@ -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.
+517
View File
@@ -0,0 +1,517 @@
// Copyright (c) 2023-2025 Cory Petkovsek and Contributors
// Copyright (c) 2021 J. Cuellar
shader_type sky;
render_mode use_debanding;
group_uniforms general;
uniform bool sky_visible = true;
uniform vec2 color_correction = vec2(0.0, 1.0);
uniform vec4 ground_color: source_color = vec4(0.3, 0.3, 0.3, 1.0);
uniform float horizon_offset = 0.0;
group_uniforms atmosphere;
uniform float atm_darkness = 0.5;
uniform float atm_sun_intensity = 18.0;
uniform vec4 atm_day_tint: source_color = vec4(0.808, 0.91, 1.0, 1.0);
uniform vec4 atm_horizon_light_tint: source_color = vec4(0.98, 0.635, 0.463, 1.0);
uniform vec4 atm_night_tint: source_color = vec4 (0.041, 0.049, 0.061, 0.245);
uniform vec3 atm_level_params = vec3(1.0, 0.0, 0.0);
uniform float atm_thickness = 0.7;
uniform vec3 atm_beta_ray = vec3(0.00001, 0.00001, 0.00003);
uniform vec3 atm_beta_mie = vec3(0.00000003038); // Value computed by multiplying Mie, turbidity, and 0.000434.
group_uniforms sun_disk;
uniform vec4 sun_light_color: source_color = vec4(1.0);
uniform vec4 sun_disk_color: source_color = vec4(0.996, 0.541, 0.14, 1.0);
uniform float sun_disk_intensity = 2.0;
uniform float sun_disk_size = 0.015;
group_uniforms sun_mie;
uniform vec4 atm_sun_mie_tint: source_color = vec4(1.0);
uniform float atm_sun_mie_intensity = 1.0;
uniform vec3 atm_sun_partial_mie_phase = vec3(0.36, 1.64, 1.6);
group_uniforms moon;
uniform vec4 moon_color: source_color = vec4(1.0);
uniform sampler2D moon_texture: source_color, repeat_disable;
uniform vec3 moon_texture_alignment = vec3(7.0, 1.4, 4.8);
uniform bool moon_texture_flip_u;
uniform bool moon_texture_flip_v;
uniform float moon_size = 0.07;
group_uniforms moon_mie;
uniform vec4 atm_moon_mie_tint: source_color = vec4(0.137, 0.184, 0.292, 1.0);
uniform float atm_moon_mie_intensity = 0.17152;
uniform vec3 atm_moon_partial_mie_phase = vec3(0.36, 1.64, 1.6);
group_uniforms star_map;
uniform vec4 starmap_color: source_color = vec4(0.71, 0.71, 0.71, 0.855);
uniform sampler2D starmap_texture: source_color;
uniform bool starmap_flip_u = false;
uniform bool starmap_flip_v = false;
uniform vec3 starmap_alignment = vec3(2.68288, -0.25891, 0.40101);
uniform float star_rotation_offset = 9.38899;
uniform float star_rotation = 0.0; // For simulating Earth rotation.
uniform float star_tilt = 0.0; // For tilting the sky according to observer latitude in radians. (90° - latitude)
group_uniforms star_field;
uniform vec4 star_field_color: source_color = vec4(1.0);
uniform sampler2D star_field_texture: source_color;
uniform float star_scintillation = 0.75;
uniform float star_scintillation_speed = 0.01;
uniform sampler2D noise_tex: source_color;
/* Sky alignment values for a star map that is in galactic coordinates. Works for most star maps.
X: 2.6555
Y: -0.23935
Z: 0.4505
These alignment values are consistent with a sky view from the North Pole (latitude zero)
at midnight. The positions of Polaris, Vega, Rigel, and Alpha Centauri confirmed
through various sources and calculations.
*/
group_uniforms clouds;
uniform vec4 clouds_night_color: source_color = vec4(0.09, 0.094, 0.129, 1.0);
group_uniforms cirrus_clouds;
uniform bool cirrus_visible = true;
uniform float cirrus_intensity = 2.0;
uniform float cirrus_coverage = 0.5;
uniform float cirrus_thickness = 10.0;
uniform float cirrus_absorption = 2.0;
uniform float cirrus_sky_tint_fade = 0.5;
uniform float cirrus_size = 1.0;
uniform vec2 cirrus_uv = vec2(0.16, 0.11);
uniform vec2 cirrus_position1 = vec2(0.0);
uniform vec2 cirrus_position2 = vec2(0.0);
uniform sampler2D cirrus_texture;
group_uniforms cumulus_clouds;
uniform bool cumulus_visible = true;
uniform float cumulus_intensity = 0.6;
uniform float cumulus_coverage = 0.55;
uniform float cumulus_thickness = 0.0243;
uniform float cumulus_absorption = 2.0;
uniform float cumulus_noise_freq = 2.7;
uniform float cumulus_sky_tint_fade = 0.0;
uniform float cumulus_size = 0.5;
uniform vec2 cumulus_position = vec2(0.0);
uniform sampler2D cumulus_texture;
uniform vec3 cumulus_partial_mie_phase = vec3(0.95756, 1.04244, 0.412);
uniform float cumulus_mie_intensity = 1.0;
group_uniforms overlays;
uniform bool show_azimuthal_grid = false;
uniform vec4 azimuthal_grid_color : source_color = vec4(.871, .722, .529, 1.);
uniform float azimuthal_grid_rotation_offset = 0.03;
uniform bool show_equatorial_grid = false;
uniform vec4 equatorial_grid_color : source_color = vec4(0., .75, 1., 1.);
uniform float equatorial_grid_rotation_offset = 0.03;
group_uniforms transform_matrices;
uniform mat3 moon_matrix; // Calculated externally via GDScript.
#include "Common.gdshaderinc"
// Provides a transformation matrix when passed a vec3.
mat3 create_rotation_matrix(vec3 r) {
float c1 = cos(r.x), s1 = sin(r.x);
float c2 = cos(r.y), s2 = sin(r.y);
float c3 = cos(r.z), s3 = sin(r.z);
mat3 rotX = mat3(
vec3(1.0, 0.0, 0.0),
vec3(0.0, c1, -s1),
vec3(0.0, s1, c1)
);
mat3 rotY = mat3(
vec3(c2, 0.0, s2),
vec3(0.0, 1.0, 0.0),
vec3(-s2, 0.0, c2)
);
mat3 rotZ = mat3(
vec3(c3, -s3, 0.0),
vec3(s3, c3, 0.0),
vec3(0.0, 0.0, 1.0)
);
return rotZ * rotY * rotX; // Combine rotations in ZYX order
}
vec2 calc_equirect_uv(vec3 norm) {
float flipu = starmap_flip_u ? -1.0 : 1.0;
float flipv = starmap_flip_v ? -1.0 : 1.0;
float u = flipu * ((atan(norm.y, norm.x) + PI) * INV_TAU); // 0 to 1 horizontally
float v = -flipv * (acos(norm.z) * INV_PI); // 0 (north pole) to 1 (south pole)
return vec2(u, v);
}
vec3 sample_moon_texture(vec3 dir) {
dir = moon_matrix * dir * create_rotation_matrix(moon_texture_alignment);
dir.x = -dir.x;
dir.z = -dir.z;
float flipu = moon_texture_flip_u ? 1.0 : -1.0;
float flipv = moon_texture_flip_v ? 1.0 : -1.0;
float u = 0.5 + flipu * atan(dir.z, dir.x) * INV_TAU; // Longitude
float v = 0.5 + flipv * asin(dir.y) * INV_PI; // Latitude
return texture(moon_texture, vec2(u, v)).rgb;
}
float calc_disk_mask(vec3 norm, vec3 coords, lowp float size) {
float dist = length(norm - coords);
return 1.0 - step(size, dist);
}
float map_range(float value, float from_min, float from_max, float to_min, float to_max) {
return to_min + (value - from_min) * (to_max - to_min) / (from_max - from_min);
}
bool ray_sphere_intersect(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;
float valid = step(0.0, d); // 1.0 if d >= 0.0, else 0.0
d = sqrt(max(d, 0.0)); // Prevent sqrt of negative numbers
a *= 2.0;
float t1 = 0.5 * (-b + d);
float t2 = 0.5 * (-b - d);
float t_min = min(t1, t2); // Select the smallest intersection
t = mix(t2, t1, step(t2, t1) * step(0.0, t1)); // Use t1 if t2 < t1 and t1 >= 0, else t2
t = mix(0.0, t, step(0.0, t) * valid); // Invalidate if t < 0 or d < 0
nrm = mix(vec3(0.0), origin + t * dir, step(0.0, t) * valid); // Set nrm only if valid
return bool(step(0.0, t) * valid);
}
// For rendering the moon. This function courtesy of Kelvin van Hoorn.
// https://kelvinvanhoorn.com/2024/12/07/skybox-tutorial-godot/
float simple_sphere_intersect(vec3 ray_dir, vec3 sphere_pos, float radius) {
vec3 oc = -sphere_pos;
float b = dot(oc, ray_dir);
float c = dot(oc, oc) - radius * radius;
float h = b * b - c;
float valid = step(0.0, h); // 1.0 if h >= 0.0, else 0.0
h = sqrt(max(h, 0.0)); // Ensure sqrt is non-negative
return mix(-1.0, -b - h, valid);
}
// Atmosphere
//------------------------------------------------------------------------------
vec3 calc_atmospheric_scatter(float sr, float sm, vec2 mu, vec3 mult) {
vec3 beta_mie = atm_beta_mie;
vec3 beta_ray = atm_beta_ray * atm_thickness;
vec3 extc_factor = clamp_rgb_to_unit(exp(-(beta_ray * sr + beta_mie * sm)));
float extc_ff = mix(clamp_to_unit(atm_thickness * 0.5), 1.0, mult.x);
vec3 final_extc_factor = mix(1.0 - extc_factor, (1.0 - extc_factor) * extc_factor, extc_ff);
float rayleigh_phase = calc_rayleigh_phase(mu.x);
vec3 BRT = beta_ray * rayleigh_phase;
vec3 BMT = beta_mie * calc_mie_phase(mu.x, atm_sun_partial_mie_phase);
BMT *= atm_sun_mie_intensity * atm_sun_mie_tint.rgb;
vec3 BRMT = (BRT + BMT) / (beta_ray + beta_mie);
vec3 scatter = atm_sun_intensity * (BRMT * final_extc_factor) * atm_day_tint.rgb * mult.y;
scatter = mix(scatter, scatter * (1.0 - extc_factor), atm_darkness);
vec3 lcol = mix(atm_day_tint.rgb, atm_horizon_light_tint.rgb, mult.x);
vec3 nscatter = (1.0 - extc_factor) * atm_night_tint.rgb;
nscatter += calc_mie_phase(mu.y, atm_moon_partial_mie_phase) *
atm_moon_mie_tint.rgb * atm_moon_mie_intensity * 0.005;
return (scatter * lcol) + nscatter;
}
// Clouds
//------------------------------------------------------------------------------
const int CUMULUS_STEP = 10;
float sample_cirrus_noise(vec2 coords) {
float a = textureLod(cirrus_texture, coords.xy * cirrus_uv + cirrus_position1, 0.0).r;
float b = textureLod(cirrus_texture, coords.xy * cirrus_uv + cirrus_position2, 0.0).r;
return ((a + b) * 0.5);
}
float sample_cumulus_noise(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(cumulus_texture, uv.xy, 0.0).r;
float x2 = textureLod(cumulus_texture, uv.zw, 0.0).r;
return mix(x1, x2, fract(pos.z));
}
float calc_cumulus_fbm(vec3 p, float l) {
float ret;
ret = 0.51749673 * sample_cumulus_noise(p);
p *= l;
ret += 0.25584929 * sample_cumulus_noise(p);
p *= l;
ret += 0.12527603 * sample_cumulus_noise(p);
p *= l;
ret += 0.06255931 * sample_cumulus_noise(p);
return ret;
}
float calc_cirrus_density(vec2 p) {
float d = sample_cirrus_noise(p);
float c = 1.0 - cirrus_coverage;
d = d - c;
return clamp_to_unit(d);
}
float calc_cumulus_density(vec3 p, vec3 offset, float t) {
vec3 pos = p * 0.0212242 - offset;
float dens = calc_cumulus_fbm(pos, cumulus_noise_freq);
dens += dens;
float cov = 1.0 - cumulus_coverage;
cov = smoothstep(0.00, (cov * 3.5) + t, dens);
dens *= cov;
dens = map_range(dens, 1.0 - cov, 1.0, 0.0, 1.0);
return clamp_to_unit(dens);
}
vec4 render_cirrus_clouds(vec3 pos) {
pos.xy = pos.xz / pos.y;
pos *= cirrus_size;
float density = calc_cirrus_density(pos.xy);
float sh = clamp_to_unit(exp(-cirrus_absorption * density));
float a = clamp_to_unit(density * cirrus_thickness);
return vec4(vec3(density*sh) * cirrus_intensity, a);
}
vec4 render_cumulus_clouds(vec3 ro, vec3 rd, float am, vec3 sun_pos, vec3 moon_pos) {
vec4 ret = vec4(0.);
vec3 wind = -vec3(cumulus_position.x, 0.0, cumulus_position.y);
float a = 0.0;
// n and tt doesnt need to be initialized since it would be set by ray_sphere_intersect
vec3 n; float tt;
if (ray_sphere_intersect(500., ro, rd, tt, n)) {
float march_step = float(CUMULUS_STEP) * cumulus_thickness;
vec3 dir_step = rd / rd.y * march_step;
vec3 pos = n * cumulus_size;
vec2 mu = vec2(dot(sun_pos, rd), dot(moon_pos, rd));
vec3 mph = ((calc_mie_phase(mu.x, cumulus_partial_mie_phase) * atm_sun_mie_tint.rgb) +
calc_mie_phase(mu.y, cumulus_partial_mie_phase) * am);
vec4 t = vec4(1.0);
t.rgb += (mph.rgb * cumulus_mie_intensity);
for(int i = 0; i < CUMULUS_STEP; i++) {
float h = float(i) * 0.1; // / float(kCLOUDS_STEP);
float density = calc_cumulus_density(pos, wind, h);
float sh = clamp_to_unit(exp(-cumulus_absorption * density * march_step));
t *= sh;
ret += (t * (exp(h) * 0.571428571) * density * march_step);
a += (1.0 - sh) * (1.0 - a);
pos += dir_step;
}
return vec4(ret.rgb * cumulus_intensity, a);
}
return vec4(ret.rgb * cumulus_intensity, a);
}
// Main
//------------------------------------------------------------------------------
vec3 render_sky(vec3 world_pos, vec3 clouds_pos, vec3 sun_pos, vec3 moon_pos, float p_time) {
vec3 col = vec3(0.0);
vec4 angle_mult;
angle_mult.x = clamp_to_unit(1.0 - sun_pos.y);
angle_mult.y = clamp_to_unit(sun_pos.y + 0.45);
angle_mult.z = clamp_to_unit(-sun_pos.y + 0.30);
angle_mult.w = clamp_to_unit(-sun_pos.y + 0.60);
// Atmosphere
vec2 mu = vec2(dot(sun_pos, world_pos), dot(moon_pos, world_pos));
float sr, sm;
calc_simple_optical_depth(world_pos.y + atm_level_params.z - horizon_offset, sr, sm, atm_level_params.xy);
world_pos.y -= horizon_offset;
float horizonBlend = clamp_to_unit((world_pos.y - 0.03) * 3.0);
float horizonBlendCumulus = clamp_to_unit((world_pos.y + 0.01) * 50.0);
vec3 scatter = calc_atmospheric_scatter(sr, sm, mu.xy, angle_mult.xyz);
col.rgb += scatter.rgb;
// Sun
vec3 sun_disk = calc_disk_mask(world_pos, sun_pos, sun_disk_size) * sun_disk_color.rgb * scatter.rgb;
sun_disk *= sun_disk_intensity;
// Moon
float moon_intersect = simple_sphere_intersect(world_pos, moon_pos, moon_size);
float moon_mask = moon_intersect > (-1. + sqrt(moon_size)) ? 1.0 : 0.0;
vec3 moon_normal = normalize(world_pos * moon_intersect - moon_pos);
float moon_ndotl = clamp(dot(moon_normal, sun_pos), 0.0, 1.0);
vec3 moon_tex = sample_moon_texture(moon_normal);
vec3 moon_output = moon_mask * moon_ndotl * exp2(1.0) * moon_tex * moon_color.rgb;
float moonMask = (1.0 - moon_mask);
// Deep space
// Tilt the sky along the X-axis.
mat3 tilt = create_rotation_matrix(vec3(star_tilt, 0.0, 0.0));
// Rotation around Y-axis. For simulating Earth's rotation.
mat3 rotation = create_rotation_matrix(vec3(0.0, star_rotation + star_rotation_offset, 0.0));
// Initial XYZ rotation for alignment.
mat3 alignment_xyz = create_rotation_matrix(starmap_alignment);
// Combine everything together to get the desired alignment.
vec3 final_alignment = normalize(alignment_xyz * rotation * tilt * world_pos);
// Now generate UV coordinates for the texture.
vec2 deep_space_uv = calc_equirect_uv(normalize(final_alignment));
// Star map
vec3 deep_space_background = textureLod(starmap_texture, deep_space_uv, 0.0).rgb;
deep_space_background *= starmap_color.rgb;
deep_space_background = adjust_contrast(deep_space_background, starmap_color.a);
vec3 deep_space = deep_space_background.rgb * moonMask + moon_output;
// Star Field
float star_scint = textureLod(noise_tex, deep_space_uv + (p_time * star_scintillation_speed), 0.0).r;
star_scint = mix(1.0, star_scint * 1.5, star_scintillation);
vec3 star_field = textureLod(star_field_texture, deep_space_uv, 0.0).rgb * star_field_color.rgb;
star_field = clamp_rgb_to_unit(mix(star_field.rgb, star_field.rgb * star_scint, star_scintillation));
star_field = clamp_rgb_to_unit(star_field.rgb);
//deep_space.rgb -= clamp_to_unit(star_field.r * 10.0);
deep_space.rgb += star_field.rgb * moonMask;
deep_space.rgb *= angle_mult.z;
col.rgb += sun_disk.rgb + deep_space.rgb * horizonBlend;
// Clouds
if (cirrus_visible) {
vec4 cirrus_clouds = render_cirrus_clouds(clouds_pos);
cirrus_clouds.a = clamp_to_unit(cirrus_clouds.a);
vec3 cloud_tint = sun_light_color.rgb;
cloud_tint = mix(cloud_tint, clouds_night_color.rgb, angle_mult.w);
cirrus_clouds.rgb *= cloud_tint * cirrus_intensity;
cirrus_clouds.a = mix(0.0, cirrus_clouds.a, horizonBlend);
col.rgb = mix(col.rgb, cirrus_clouds.rgb + mix(vec3(0.0), scatter, cirrus_sky_tint_fade), cirrus_clouds.a);
}
if (cumulus_visible) {
vec4 cumulus_clouds = render_cumulus_clouds(vec3(0.0), clouds_pos, angle_mult.z, sun_pos, moon_pos);
cumulus_clouds.a = clamp_to_unit(cumulus_clouds.a);
vec3 cloud_tint = sun_light_color.rgb;
cloud_tint = mix(cloud_tint, clouds_night_color.rgb, angle_mult.w);
cumulus_clouds.rgb *= cloud_tint * cumulus_intensity;
cumulus_clouds.a = mix(0.0, cumulus_clouds.a, horizonBlend);
col.rgb = mix(col.rgb, cumulus_clouds.rgb + mix(vec3(0.0), scatter, cumulus_sky_tint_fade), cumulus_clouds.a);
}
col.rgb = mix(col.rgb, ground_color.rgb * scatter, clamp_to_unit((-world_pos.y - atm_level_params.z) * 100.0));
col.rgb = apply_photo_tonemap(col.rgb, color_correction.y, color_correction.x);
return col;
}
vec4 create_axis_angle_quat(vec3 axis, float angle) {
float s = sin(angle * 0.5);
return vec4(axis * s, cos(angle * 0.5));
}
vec3 rotate_vector_by_quat(vec3 v, vec4 q) {
vec3 t = 2.0 * cross(q.xyz, v);
return v + q.w * t + cross(q.xyz, t);
}
vec3 align_and_rotate_vector(vec3 dir, vec3 target, float rotation_angle) {
vec3 z_axis = vec3(0.0, 0.0, 1.0); // Original grid alignment
// First, align Z-axis with target (Polaris)
target = normalize(target);
vec3 axis = cross(z_axis, target);
float angle = acos(dot(z_axis, target));
float valid = step(0.0001, length(axis)); // 1.0 if length(axis) >= 0.0001
axis = normalize(axis);
vec4 align_quat = create_axis_angle_quat(axis, angle);
dir = mix(dir, rotate_vector_by_quat(dir, align_quat), valid);
vec4 rot_quat = create_axis_angle_quat(vec3(0.0, 0.0, 1.0), rotation_angle);
dir = mix(dir, rotate_vector_by_quat(dir, rot_quat), valid);
return dir;
}
void render_grid(vec3 dir, vec3 pole_dir, float rotation_angle, vec4 line_color, float grid_spacing, float line_thickness, inout vec3 color) {
float spacing = max(grid_spacing, 0.001);
float thickness = max(line_thickness, 0.0001);
// Rotate dir with sidereal motion and align with pole_dir
dir = align_and_rotate_vector(dir, pole_dir, rotation_angle);
// Latitude / Circles / Theta Spherical coordinates
float theta = acos(dir.z);
float theta_deriv = fwidth(theta);
float circle_thickness = max(thickness, theta_deriv);
float circle = mod(theta, spacing);
float circle_line = 1.0 - smoothstep(0.0, circle_thickness * 1.5, abs(circle - circle_thickness * 0.5));
circle_line *= clamp_to_unit(thickness / circle_thickness);
// Add poles
float pole_thickness = circle_thickness;
float north_pole = 1.0 - smoothstep(0.0, pole_thickness, theta);
float south_pole = 1.0 - smoothstep(0.0, pole_thickness, abs(theta - PI));
circle_line = max(circle_line, max(north_pole, south_pole));
// Darken Lat at poles
float circle_adjust = sin(theta);
circle_line *= clamp(circle_adjust, 0.2, 1.0);
// Longitude / Radial lines / Phi Spherical coordinates
float phi = atan(dir.y, dir.x);
float phi_deriv = fwidth(phi);
float radial_thickness = max(thickness, phi_deriv);
float radial = mod(phi + PI, spacing); // PI w/ less precision to remove moire
float radial_line = 1.0 - smoothstep(0.0, radial_thickness * 1.5, abs(radial - radial_thickness));
radial_line *= clamp_to_unit(thickness / radial_thickness);
float grid = max(radial_line, circle_line);
color += line_color.rgb * line_color.a * grid;
}
void sky() {
float current_time = TIME;
vec3 col = vec3(0.0); // Set up the initial sky color which will be added to later to produce the final result.
vec3 world_pos = EYEDIR; // Original shader set this up in the vertex() function. For sky shaders the overall equivalent is EYEDIR.
vec3 clouds_pos = EYEDIR; // The same is used for cloud rendering.
vec3 sun_pos = LIGHT0_DIRECTION; // Redefined from now-deprecated _sun_direction.
vec3 moon_pos = LIGHT1_DIRECTION; // Redefined from now-deprecated _moon_direction.
if (sky_visible) {
col = render_sky(world_pos, clouds_pos, sun_pos, moon_pos, current_time);
}
// Draw overlay grids
{
float grid_thickness = 0.0002;
float grid_spacing = 0.261799; // 15 degrees
vec3 zenith_dir = vec3(0.0, 1.0, 0.0); // Y-axis aligned azimuthal
vec3 north_celestial_pole = create_rotation_matrix(vec3(star_tilt, 0.0, 0.0)) * zenith_dir;
float rotation_angle = star_rotation + star_rotation_offset;
if (show_azimuthal_grid) {
render_grid(EYEDIR, zenith_dir, azimuthal_grid_rotation_offset, azimuthal_grid_color, grid_spacing, grid_thickness, col);
}
if (show_equatorial_grid) {
render_grid(EYEDIR, north_celestial_pole, rotation_angle + equatorial_grid_rotation_offset, equatorial_grid_color, grid_spacing, grid_thickness, col);
}
}
COLOR = col.rgb;
}
@@ -0,0 +1 @@
uid://cvd0x7lon1ur6
+31
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
uid://4ka6iw74pja3
+24
View File
@@ -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")
+1
View File
@@ -0,0 +1 @@
uid://ri5mkwojuh33
+50
View File
@@ -0,0 +1,50 @@
# 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_wavelenghts(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
ret.y = 1.0 + g2
ret.z = 2.0 * g
return ret
+1
View File
@@ -0,0 +1 @@
uid://dc70wk0t64fwe
+549
View File
@@ -0,0 +1,549 @@
# Copyright (c) 2023-2025 Cory Petkovsek and Contributors
# Copyright (c) 2021 J. Cuellar
## Sky3D is an Atmosphereic Day/Night Cycle for Godot 4.
##
## This plugin 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, [SkyDome], and [TimeOfDay] nodes.
@tool
class_name Sky3D
extends WorldEnvironment
## Emitted when the environment has changed to a new resource.
signal environment_changed
const SKY_SHADER: String = "res://addons/sky_3d/shaders/SkyMaterial.gdshader"
@export_custom(PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_READ_ONLY)
var version: String = "2.1"
## The Sun DirectionalLight.
var sun: DirectionalLight3D
## The Moon DirectionalLight.
var moon: DirectionalLight3D
## The TimeOfDay node.
var tod: TimeOfDay
## The SkyDome node.
var sky: SkyDome
## The Sky shader.
var sky_material: ShaderMaterial
## Enables all rendering and time tracking.
@export var sky3d_enabled: bool = true :
set(value):
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 [method hide_sky].
@export var sky_enabled: bool = true :
set(value):
sky_enabled = value
if sky and sky_material:
sky_material.set_shader_parameter("sky_visible", value)
sky.cumulus_visible = clouds_enabled and value
sky.cirrus_visible = clouds_enabled and value
## Enables both 2D and cumulus cloud layers.
@export var clouds_enabled: bool = true :
set(value):
clouds_enabled = value
if sky:
sky.cumulus_visible = value
sky.cirrus_visible = value
## Enables the Sun and Moon [DirectionalLight3D]s.
@export var lights_enabled: bool = true :
set(value):
lights_enabled = value
if sky:
sky.sun_light_enabled = value
sky.moon_light_enabled = value
## Enables the screen space fog shader. Sky3D also works with the other two fog methods built into Godot.
@export var fog_enabled: bool = true :
set(value):
fog_enabled = value
if sky:
sky.fog_visible = value
## Disables rendering of sky, fog, and lights.
func hide_sky() -> void:
sky_enabled = false
lights_enabled = false
fog_enabled = false
clouds_enabled = false
## Enables rendering of sky, fog, and lights.
func show_sky() -> void:
sky_enabled = true
lights_enabled = true
fog_enabled = true
clouds_enabled = true
#####################
## Time
#####################
@export_group("Time")
## Allows time to progress in the editor. Alias for [member TimeOfDay.editor_time_enabled].
@export var editor_time_enabled: bool = true :
set(value):
if tod:
tod.editor_time_enabled = value
get:
return tod.editor_time_enabled if tod else editor_time_enabled
## Allows time to progress in game. Alias for [member TimeOfDay.game_time_enabled].
@export var game_time_enabled: bool = true :
set(value):
if tod:
tod.game_time_enabled = value
get:
return tod.game_time_enabled if tod else game_time_enabled
## A readable game date string, eg. '2025-01-01'. Alias for [member TimeOfDay.game_date].
@export_custom(PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_READ_ONLY)
var game_date: String = "" :
get:
return tod.game_date if tod else game_date
## A readable game time string, e.g. '08:00:00'. Alias for [member TimeOfDay.game_time].
@export_custom(PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_READ_ONLY)
var game_time: String = "" :
get:
return tod.game_time if tod else game_time
## The current in-game time in hours from 0.0 to 23.99. Smaller or larger values than the range will wrap.
## Alias for [member TimeOfDay.current_time].
@export_range(0.0, 23.9998, 0.01) var current_time: float = 8.0 :
set(value):
if tod:
tod.current_time = value
get:
return tod.current_time if tod else current_time
## The total length of time for a complete day and night cycle in real world minutes. Setting this to
## [param 15] means a full in-game day takes 15 real-world minutes. [member game_time_enabled] must be
## enabled for this to work. Negative values moves time backwards. The Witcher 3 uses a 96 minute cycle.
## Adjust [member update_interval] to match. Shorter days needs more updates. Longer days need less.
## Alias for [member TimeOfDay.minutes_per_day].
@export_range(-1440, 1440, 0.1) var minutes_per_day: float = 15.0 :
set(value):
if tod:
tod.minutes_per_day = value
get:
return tod.minutes_per_day if tod else minutes_per_day
## Frequency of sky updates, per second. The smaller the number, the more frequent the updates and
## the smoother the animation. Set to [param 0.016] for 60fps, for example.[br][br]
## [b]Note:[/b] Setting this value too small may cause unwanted behavior. See [member Timer.wait_time].
## Alias for [member TimeOfDay.update_interval].
@export_range(0.016, 10.0) var update_interval: float = 0.016 :
set(value):
if tod:
tod.update_interval = value
get:
return tod.update_interval if tod else update_interval
## Returns true if the sun is above the horizon.
func is_day() -> bool:
return sky and sky.is_day()
## Returns true if the sun is below the horizon.
func is_night() -> bool:
return sky and not sky.is_day()
## Pauses time calculation. Alias for [method TimeOfDay.pause].
func pause() -> void:
if tod:
tod.pause()
## Resumes time calculation. Alias for [method TimeOfDay.resume].
func resume() -> void:
if tod:
tod.resume()
var _contrib_tween: Tween
# Adjusts sky contribution if transitioning to day or night.
func _start_sky_contrib_tween(daytime: bool = is_day()) -> void:
if not (sky and environment and is_inside_tree()):
return
if _contrib_tween:
_contrib_tween.kill()
_contrib_tween = get_tree().create_tween()
_contrib_tween.set_parallel(true)
if daytime:
_contrib_tween.tween_property(environment, "ambient_light_sky_contribution", sky_contribution, contribution_tween_time)
else:
var night_contrib: float = minf(night_sky_contribution, sky_contribution) if night_ambient_boost else sky_contribution
_contrib_tween.tween_property(environment, "ambient_light_sky_contribution", night_contrib, contribution_tween_time)
#####################
## Lighting
#####################
@export_group("Lighting")
## Light intensity scaled before the tonemapper. Softer highlights.
## Alias for [member Environment.camera_attributes].
## Connect this same resource to your [member Camera3D.attributes].
@export_range(0, 16, 0.005) var camera_exposure: float = 1.0 :
set(value):
if camera_attributes:
camera_attributes.exposure_multiplier = value
get:
return camera_attributes.exposure_multiplier if camera_attributes else camera_exposure
## Light intensity scaled in post processing. Hotter highlights.
## Alias for [member Environment.tonemap_exposure].
## Connect this same resource to your [member Camera3D.environment].
@export_range(0, 16, 0.005) var tonemap_exposure: float = 1.0 :
set(value):
if environment:
environment.tonemap_exposure = value
get:
return environment.tonemap_exposure if environment else tonemap_exposure
## Light energy coming from the sky shader. Alias for [member SkyDome.exposure].
@export_range(0, 16, 0.005) var skydome_energy: float = 1.0 :
set(value):
if sky:
sky.exposure = value
get:
return sky.exposure if sky else skydome_energy
## Brightness of and light energy coming from the clouds. Alias for [member SkyDome.cumulus_intensity].
@export_range(0, 16, 0.005) var cloud_intensity: float = 0.6 :
set(value):
if sky:
sky.cumulus_intensity = value
get:
return sky.cumulus_intensity if sky else cloud_intensity
## Maximum brightness of the Sun DirectionalLight, visible during the day.
## Alias for [member SkyDome.sun_light_energy].
@export_range(0, 16, 0.005) var sun_energy: float = 1.0 :
set(value):
if sky:
sky.sun_light_energy = value
get:
return sky.sun_light_energy if sky else sun_energy
## Opacity of Sun DirectionalLight shadow. Alias for [member DirectionalLight3D.shadow_opacity].
@export_range(0, 1, 0.005) var sun_shadow_opacity: float = 1.0 :
set(value):
if sun:
sun.shadow_opacity = value
get:
return sun.shadow_opacity if sun else sun_shadow_opacity
## Ratio of ambient light to sky light. Works when there are no Reflection Probes or GI.
## Sets the target for [member Environment.ambient_light_sky_contribution], which may change at night
## depending on [member night_ambient_boost] and [member night_sky_contribution].
@export_range(0, 1, 0.005) var sky_contribution: float = 1.0 :
set(value):
if environment:
sky_contribution = value
environment.ambient_light_sky_contribution = value
_start_sky_contrib_tween()
## Strength of ambient light. Works when there are no Reflection Probes or GI, and
## [member sky_contribution] < 1. Alias for [member Environment.ambient_light_energy].
@export_range(0, 16, 0.005) var ambient_energy: float = 1.0 :
set(value):
environment.ambient_light_energy = value
_start_sky_contrib_tween()
get:
return environment.ambient_light_energy if environment else ambient_energy
@export_subgroup("Night")
## Maximum strength of Moon DirectionalLight, visible at night. Alias for [member SkyDome.moon_light_energy].
@export_range(0, 16, 0.005) var moon_energy: float = 0.3 :
set(value):
if sky:
sky.moon_light_energy = value
get:
return sky.moon_light_energy if sky else moon_energy
## Opacity of Moon DirectionalLight shadow. Alias for [member DirectionalLight3D.shadow_opacity].
@export_range(0, 1, 0.005) var moon_shadow_opacity: float = 1.0 :
set(value):
if moon:
moon.shadow_opacity = value
get:
return moon.shadow_opacity if moon else moon_shadow_opacity
## Enables a lower sky_contribution at night, which allows more ambient energy to show.
## To use, ensure there are no ReflectionProbes or GI. Set [member ambient_energy] > 0.
## Set [member night_sky_contribution] < [member sky_contribution].
## Then at night, [member Environment.ambient_light_sky_contribution] will be set lower, which
## will show more [member ambient_energy].
@export var night_ambient_boost: bool = true :
set(value):
night_ambient_boost = value
_start_sky_contrib_tween()
## Sets [member Environment.ambient_light_sky_contribution] at night if [member night_ambient_boost] is enabled.
## See [member night_ambient_boost] and [member sky_contribution].
@export_range(0, 1, 0.005) var night_sky_contribution: float = 0.7 :
set(value):
night_sky_contribution = value
if night_ambient_boost:
_start_sky_contrib_tween()
## Transition time for changing sky contribution when shifting between day and night.
@export_range(0, 30, 0.05) var contribution_tween_time: float = 3.0
@export_subgroup("Auto Exposure")
## Alias for [member CameraAttributes.auto_exposure_enabled].
@export var auto_exposure: bool = false :
set(value):
if camera_attributes:
camera_attributes.auto_exposure_enabled = value
get:
return camera_attributes.auto_exposure_enabled if camera_attributes else auto_exposure
## Alias for [member CameraAttributes.auto_exposure_scale].
@export_range(0.01, 16, 0.005) var auto_exposure_scale: float = 0.4 :
set(value):
if camera_attributes:
camera_attributes.auto_exposure_scale = value
get:
return camera_attributes.auto_exposure_scale if camera_attributes else auto_exposure_scale
## Alias for [member CameraAttributesPractical.auto_exposure_min_sensitivity].
@export_range(0, 1600, 0.5) var auto_exposure_min: float = 0.0 :
set(value):
if camera_attributes:
camera_attributes.auto_exposure_min_sensitivity = value
if value > auto_exposure_max:
auto_exposure_max = value
get:
return camera_attributes.auto_exposure_min_sensitivity if camera_attributes else auto_exposure_min
## Alias for [member CameraAttributesPractical.auto_exposure_max_sensitivity].
@export_range(30, 64000, 0.5) var auto_exposure_max: float = 800.0 :
set(value):
if camera_attributes:
camera_attributes.auto_exposure_max_sensitivity = value
if value < auto_exposure_min:
auto_exposure_min = value
get:
return camera_attributes.auto_exposure_max_sensitivity if camera_attributes else auto_exposure_max
## Alias for [member CameraAttributes.auto_exposure_speed].
@export_range(0.1, 64, 0.1) var auto_exposure_speed: float = 0.5 :
set(value):
if camera_attributes:
camera_attributes.auto_exposure_speed = value
get:
return camera_attributes.auto_exposure_speed if camera_attributes else auto_exposure_speed
#####################
## Weather
#####################
@export_group("Weather")
## Sets the wind speed. Alias for [member SkyDome.wind_speed].
@export_custom(PROPERTY_HINT_RANGE, "0,120,0.1,or_greater,or_less,suffix:m/s") var wind_speed: float = 1.0 :
set(value):
if sky:
sky.wind_speed = value
get:
return sky.wind_speed if sky else wind_speed
## Sets the wind direction. Zero means the wind is coming from the north, 90 from the east,
## 180 from the south and 270 (or -90) from the west. Alias for [member SkyDome.wind_direction].
@export_custom(PROPERTY_HINT_RANGE, "-180,180,0.1,radians_as_degrees") var wind_direction: float = 0.0 :
set(value):
if sky:
sky.wind_direction = value
get:
return sky.wind_direction if sky else wind_direction
#####################
## Overlays
#####################
@export_group("Overlays")
## Overlays a grid aligned to the horizon and the sky zenith.
## Change color in SkyDome. Alias for [member SkyDome.show_azimuthal_grid].
@export var show_azimuthal_grid: bool = false :
set(value):
if sky:
sky.show_azimuthal_grid = value
get:
return sky.show_azimuthal_grid if sky else show_azimuthal_grid
## Overlays a grid aligned to the celestial equator and the north celestial pole (near Polaris).
## Change color in SkyDome. Alias for [member SkyDome.show_equatorial_grid].
@export var show_equatorial_grid: bool = false :
set(value):
if sky:
sky.show_equatorial_grid = value
get:
return sky.show_equatorial_grid if sky else show_equatorial_grid
#####################
## 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.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)
# Setup Sky material & Upgrade old
if environment.sky == null or environment.sky.sky_material is PhysicalSkyMaterial:
environment.sky = Sky.new()
environment.sky.sky_material = ShaderMaterial.new()
environment.sky.sky_material.shader = load(SKY_SHADER)
# Set a reference to the sky material for easy access.
sky_material = environment.sky.sky_material
# 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
# DEPRECATED - Remove 2.2
if has_node("Skydome"):
$Skydome.name = "SkyDome"
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 sky and not sky.day_night_changed.is_connected(_start_sky_contrib_tween):
sky.day_night_changed.connect(_start_sky_contrib_tween)
func _enter_tree() -> void:
_start_sky_contrib_tween()
func _set(property: StringName, value: Variant) -> bool:
match property:
"environment":
sky.environment = value
environment = value
emit_signal("environment_changed", environment)
return true
return false
+1
View File
@@ -0,0 +1 @@
uid://deo0gxkmk2ahd
File diff suppressed because it is too large Load Diff
+1
View File
@@ -0,0 +1 @@
uid://b4tkplvh7fq40
+18
View File
@@ -0,0 +1,18 @@
# Copyright (c) 2023-2025 Cory Petkovsek and Contributors
# Copyright (c) 2021 J. Cuellar
class_name TOD_Math
static func rev(val: float) -> float:
return val - int(floor(val / 360.0)) * 360.0
static func spherical_to_cartesian(theta: float, azimuth: float, radius: float = 1.0) -> Vector3:
var ret: Vector3
var sinTheta: float = sin(theta)
ret.x = sinTheta * sin(azimuth)
ret.y = cos(theta)
ret.z = sinTheta * cos(azimuth)
return ret * radius
+1
View File
@@ -0,0 +1 @@
uid://couf46uxtsrhg
+593
View File
@@ -0,0 +1,593 @@
# Copyright (c) 2023-2025 Cory Petkovsek and Contributors
# Copyright (c) 2021 J. Cuellar
## TimeOfDay is a component of [Sky3D].
##
## This class tracks the progress of time, as well as the planetary calculations. See [Sky3D].
@tool
class_name TimeOfDay
extends Node
signal time_changed(value)
signal minute_changed(value)
signal hour_changed(value)
signal day_changed(value)
signal month_changed(value)
signal year_changed(value)
var _previous_minute: int = -1
var _previous_hour: int = -1
const HOURS_PER_DAY: int = 24
const RADIANS_PER_HOUR: float = PI / 12.0
const HALFPI: float = PI / 2.0
func _init() -> void:
_update_celestial_coords()
func _ready() -> void:
dome_path = dome_path
_previous_minute = floori(fmod(current_time, 1.0) * 60.0)
_previous_hour = floori(current_time)
_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:
_update_time_from_os()
else:
var delta: float = 0.001 * (Time.get_ticks_msec() - _last_update)
_progress_time(delta)
_update_celestial_coords()
_last_update = Time.get_ticks_msec()
## Halts the automatic progression of in-game time until resumed.
func pause() -> void:
if is_instance_valid(_update_timer):
_update_timer.stop()
## Restarts the automatic progression of in-game time.
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 editor_time_enabled) or \
(not Engine.is_editor_hint() and game_time_enabled):
_update_timer.start()
#####################
## General
#####################
var _update_timer: Timer
var _last_update: int = 0
var _sky_dome: SkyDome
@export_group("General")
## Allows time to progress in the editor.
@export var editor_time_enabled: bool = true :
set(value):
editor_time_enabled = value
if Engine.is_editor_hint():
if editor_time_enabled:
resume()
else:
pause()
## Allows time to progress in game.
@export var game_time_enabled: bool = true :
set(value):
game_time_enabled = value
if not Engine.is_editor_hint():
if game_time_enabled:
resume()
else:
pause()
## NodePath to SkyDome
@export var dome_path: NodePath :
set(value):
dome_path = value
if dome_path:
# DEPRECATED - Remove 2.2
if dome_path == NodePath("../Skydome"):
dome_path = NodePath("../SkyDome")
_sky_dome = get_node_or_null(dome_path)
_update_celestial_coords()
## The total length of time for a complete day and night cycle in real world minutes. Setting this to
## [param 15] means a full in-game day takes 15 real-world minutes. [member game_time_enabled] must be
## enabled for this to work. Negative values moves time backwards. The Witcher 3 uses a 96 minute cycle.
## Adjust [member update_interval] to match. Shorter days needs more updates. Longer days need less.
@export var minutes_per_day: float = 15.0
## Celestial coordinates are updated based upon a timer, which continuously fires based on
## this interval: [0.016, 10s]. Set to the lowest, 0.016 (60fps) if your [member minutes_per_day] is short,
## such as less than 15 minutes. The Witcher 3 uses a 96 minute day cycle, so 0.1 (10fps) is adequate.
@export_range(0.016, 10) var update_interval: float = 0.016 :
set(value):
update_interval = clamp(value, .016, 10)
if is_instance_valid(_update_timer):
_update_timer.wait_time = update_interval
resume()
#####################
## DateTime
#####################
@export_group("Current Time")
## Syncronize all of Sky3D with your system clock for a realtime sky, time, and date.
@export var system_sync: bool = false
## A readable game date string, eg. '2025-01-01'. Alias for [member TimeOfDay.game_date].
@export_custom(PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_READ_ONLY)
var game_date: String = "" :
get():
return "%04d-%02d-%02d" % [ year, month, day ]
## A readable game time string, e.g. '08:00:00'. Alias for [member TimeOfDay.game_time].
@export_custom(PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_READ_ONLY)
var game_time: String = "" :
get():
return "%02d:%02d:%02d" % [ floor(current_time), floor(fmod(current_time, 1.0) * 60.0),
floor(fmod(current_time * 60.0, 1.0) * 60.0) ]
## The current in-game time in hours, in the range [0.0 .. 23.9998]
@export_range(0.,23.9998) var current_time: float = 8.0 :
set(value):
if current_time != value:
current_time = value
while current_time > 23.9999:
current_time -= 24
day += 1
while current_time < 0.0000:
current_time += 24
day -= 1
emit_signal("time_changed", current_time)
var new_hour: int = floori(current_time)
var new_minute: int = floori(fmod(current_time, 1.0) * 60.0)
if new_hour != _previous_hour:
_previous_hour = new_hour
emit_signal("hour_changed", new_hour)
if new_minute != _previous_minute:
_previous_minute = new_minute
emit_signal("minute_changed", new_minute)
_update_celestial_coords()
## The current day of the month
@export_range(0,31) var day: int = 1 :
set(value):
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()
## The current month of the year
@export_range(0,12) var month: int = 1 :
set(value):
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()
## The current year
@export_range(-9999,9999) var year: int = 2025 :
set(value):
if year != value:
year = value
emit_signal("year_changed", year)
_update_celestial_coords()
## Determines whether the current year has 366 days.
func is_leap_year() -> bool:
return (year % 4 == 0) && (year % 100 != 0) || (year % 400 == 0)
## Calculates the number of days in the current month, including adjustments for February during leap years.
func max_days_per_month() -> int:
match month:
1, 3, 5, 7, 8, 10, 12:
return 31
2:
return 29 if is_leap_year() else 28
return 30
## Sets [member current_time] from parameters.
func set_time(hour: int, minute: int, second: int) -> void:
current_time = float(hour) + float(minute) / 60.0 + float(second) / 3600.0
## Sets the in-game date and time from a dictionary.
func set_from_datetime_dict(datetime_dict: Dictionary) -> void:
year = datetime_dict.year
month = datetime_dict.month
day = datetime_dict.day
set_time(datetime_dict.hour, datetime_dict.minute, datetime_dict.second)
## Returns the current date and time as a Godot Time format compatible dictionary.
func get_datetime_dict() -> Dictionary:
var datetime_dict: Dictionary = {
"year": year,
"month": month,
"day": day,
"hour": floor(current_time),
"minute": floor(fmod(current_time, 1.0) * 60.0),
"second": floor(fmod(current_time * 60.0, 1.0) * 60.0)
}
return datetime_dict
## Initializes datetime from Unix timestamp, used by users' save/load extentions.
func set_from_unix_timestamp(timestamp: int) -> void:
set_from_datetime_dict(Time.get_datetime_dict_from_unix_time(timestamp))
## Converts current datetime to Unix timestamp, used by users' save/load extentions.
func get_unix_timestamp() -> int:
return Time.get_unix_time_from_datetime_dict(get_datetime_dict())
func _progress_time(delta: float) -> void:
var hours_per_real_second: float = 0. if is_zero_approx(minutes_per_day) else HOURS_PER_DAY / (minutes_per_day * 60.0)
current_time += delta * hours_per_real_second
func _update_time_from_os() -> void:
var date_time_os: Dictionary = Time.get_datetime_dict_from_system()
set_time(date_time_os.hour, date_time_os.minute, date_time_os.second)
day = date_time_os.day
month = date_time_os.month
year = date_time_os.year
#####################
## Planetary
#####################
@export_group("Planetary And Location")
var _sun_coords: Vector2 = Vector2.ZERO
var _moon_coords: Vector2 = Vector2.ZERO
var _sun_distance: 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()
enum CelestialMode { SIMPLE, REALISTIC }
## Selects between simple trigonometric approximations or full orbital mechanics for computing sun and moon positions.
@export var celestials_calculations := CelestialMode.REALISTIC:
set(value):
celestials_calculations = value
_update_celestial_coords()
notify_property_list_changed()
## The observer's position on Earth, north or south of the equator (0°=equator, +90°=North Pole, -90°=South Pole).
@export_range(-90, 90, 0.00001, "radians_as_degrees") var latitude: float = deg_to_rad(16.) :
set(value):
latitude = value
_update_celestial_coords()
## The observer's position on Earth, east or west of the prime meridian (0°=Greenwich, +180°=International Date Line).
@export_range(-180, 180, 0.00001, "radians_as_degrees") var longitude: float = deg_to_rad(108.) :
set(value):
longitude = value
_update_celestial_coords()
## Specifies the time zone offset from UTC in hours to adjust local time calculations.
@export_range(-12,14,.25) var utc: float = 7.0 :
set(value):
utc = value
_update_celestial_coords()
## Calculate moon orbital position. Disable for performance if moon lighting not needed.
@export var compute_moon_coords: bool = true :
set(value):
compute_moon_coords = value
_update_celestial_coords()
notify_property_list_changed()
## Toggles the calculation of sidereal time and star field rotation for realistic deep space alignments.
@export var compute_deep_space_coords: bool = true :
set(value):
compute_deep_space_coords = value
_update_celestial_coords()
## Offset the moon azimuth and altitude. Simple celestial calculation mode only.
@export var moon_coords_offset: Vector2 = Vector2(0.0, 0.0) :
set(value):
moon_coords_offset = value
_update_celestial_coords()
## Returns the current time at UTC 0
func get_current_time_utc0() -> float:
return current_time - utc
func _get_time_scale() -> float:
# All divisions must be integer divisions, see: https://stjarnhimlen.se/comp/ppcomp.html#3
var leap_term: int = int(7 * int((year + int((month + 9) / 12))) / 4)
var century_term: int = int(3 * int((int((year + int((month - 9) / 7))) / 100) + 1) / 4)
var month_term: int = int(275 * month / 9)
var time_scale: int = 367 * year - leap_term - century_term + month_term + day - 730515
return time_scale + current_time / 24.0
func _get_oblecl() -> float:
# From: https://stjarnhimlen.se/comp/ppcomp.html#4
# Another quantity we will need is ecl, the obliquity of the ecliptic,
# i.e. the "tilt" of the Earth's axis of rotation (currently 23.4 degrees
# and slowly decreasing). First, compute the "d" of the moment of interest
# (section 3). Then, compute the obliquity of the ecliptic:
return deg_to_rad(23.4393 - 3.563e-7 * _get_time_scale())
func _update_celestial_coords() -> void:
if not _sky_dome:
return
match celestials_calculations:
CelestialMode.SIMPLE:
_compute_simple_sun_coords()
_sky_dome.sun_altitude = _sun_coords.y
_sky_dome.sun_azimuth = _sun_coords.x
if compute_moon_coords:
_compute_simple_moon_coords()
_sky_dome.moon_altitude = _moon_coords.y
_sky_dome.moon_azimuth = _moon_coords.x
# Celestial North Pole (SIMPLE convention: +Z = north)
_sky_dome.celestial_north_pole = Vector3(0.0, sin(latitude), cos(latitude))
if compute_deep_space_coords:
if _sky_dome.is_scene_built:
_sky_dome.sky_material.set_shader_parameter("star_tilt", HALFPI - latitude)
CelestialMode.REALISTIC:
_compute_realistic_sun_coords()
_sky_dome.sun_altitude = -_sun_coords.y
_sky_dome.sun_azimuth = -_sun_coords.x
if compute_moon_coords:
_compute_realistic_moon_coords()
_sky_dome.moon_altitude = -_moon_coords.y
_sky_dome.moon_azimuth = -_moon_coords.x
# Celestial North Pole (REALISTIC convention: -Z = north)
_sky_dome.celestial_north_pole = Vector3(0.0, sin(latitude), -cos(latitude))
if compute_deep_space_coords:
if _sky_dome.is_scene_built:
_sky_dome.sky_material.set_shader_parameter("star_tilt", latitude - HALFPI)
_sky_dome.sky_material.set_shader_parameter("star_rotation", -_local_sideral_time)
_sky_dome.update_moon_coords()
func _compute_simple_sun_coords() -> void:
# PI/12.0 radians = 15 degrees => 1 hour is 15 degrees of rotation
var altitude: float = (get_current_time_utc0() * RADIANS_PER_HOUR) + longitude
_sun_coords.y = PI - altitude
_sun_coords.x = latitude
func _compute_simple_moon_coords() -> void:
_moon_coords.y = (PI - _sun_coords.y) + moon_coords_offset.y
_moon_coords.x = (PI + _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 = deg_to_rad(_sun_orbital_elements.M)
# Eccentric Anomaly
var E: float = _sun_orbital_elements.M + rad_to_deg(_sun_orbital_elements.e * sin(MRad) * (1 + _sun_orbital_elements.e * cos(MRad)))
var ERad: float = deg_to_rad(E)
# 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 = atan2(yv, xv)
_sun_distance = r
# True longitude
var lonSun: float = v + deg_to_rad(_sun_orbital_elements.w)
lonSun = fmod(lonSun + TAU, TAU) # Wrap to [0, 2π)
## Ecliptic and ecuatorial coords
# Ecliptic rectangular coords
var xs: float = r * cos(lonSun)
var ys: float = r * sin(lonSun)
# 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 = atan2(ye, xe) # right ascension.
var decl: float = atan2(ze, sqrt(xe * xe + ye * ye)) # declination
# Mean longitude
var L: float = deg_to_rad(_sun_orbital_elements.w + _sun_orbital_elements.M)
L = fmod(L + TAU, TAU) # Wrap to [0, 2π)
_mean_sun_longitude = L
var GMST0: float = L + PI
# Sidereal, see: https://stjarnhimlen.se/comp/ppcomp.html#5b
# Local Sidereal Time = GMST0 + UT + longitude
_sideral_time = GMST0 + get_current_time_utc0() * RADIANS_PER_HOUR + longitude
_local_sideral_time = _sideral_time
# Hour Angle = Sidereal Time - Right Ascension
# See: https://stjarnhimlen.se/comp/ppcomp.html#12b
var HA: float = _sideral_time - RA
# Hour angle and declination in rectangular coords
var declCos: float = cos(decl)
var x: float = cos(HA) * declCos # X Axis points to the celestial equator in the south
var y: float = sin(HA) * declCos # Y axis points to the horizon in the west
var z: float = 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)
var cosLat: float = cos(latitude)
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)
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 = deg_to_rad(_moon_orbital_elements.N)
var IRad: float = deg_to_rad(_moon_orbital_elements.i)
var MRad: float = deg_to_rad(_moon_orbital_elements.M)
# Eccentric anomaly
var E: float = _moon_orbital_elements.M + rad_to_deg(_moon_orbital_elements.e * sin(MRad) * (1 + _moon_orbital_elements.e * cos(MRad)))
var ERad: float = deg_to_rad(E)
# See: https://stjarnhimlen.se/comp/ppcomp.html#6
var xv: float = _moon_orbital_elements.a * (cos(ERad) - _moon_orbital_elements.e)
var yv: float = _moon_orbital_elements.a * 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 = atan2(yv, xv)
var l: float = v + deg_to_rad(_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))
# Position in space: https://stjarnhimlen.se/comp/ppcomp.html#7
# Geocentric position for the moon and Heliocentric position for the planets
var lonecl: float = atan2(yeclip, xeclip) # Ecliptic longitude
var latecl: float = atan2(zeclip, sqrt(xeclip * xeclip + yeclip * yeclip)) # Ecliptic latitude
var nr: float = 1.0
var xh: float = nr * cos(lonecl) * cos(latecl)
var yh: float = nr * sin(lonecl) * cos(latecl)
var zh: float = nr * sin(latecl)
# 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
# Convert xg, yg to 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 = atan2(ye, xe)
var decl: float = atan2(ze, sqrt(xe * xe + ye * ye))
# Sideral time and hour angle
var HA: float = ((_sideral_time) - RA)
# HA y Decl in rectangular coordinates
var declCos: float = cos(decl)
var xr: float = cos(HA) * declCos
var yr: float = sin(HA) * declCos
var zr: float = sin(decl)
# Rotate the rectangualar coordinates system along of the Y axis(radians)
var sinLat: float = sin(latitude)
var cosLat: float = cos(latitude)
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)
+1
View File
@@ -0,0 +1 @@
uid://cjmkcj8hwm7ts