diff --git a/dynamic-sky/README.md b/dynamic-sky/README.md new file mode 100644 index 0000000..7dfca57 --- /dev/null +++ b/dynamic-sky/README.md @@ -0,0 +1,283 @@ +# Dynamic Sky System for Godot 4.6 + +A stylized dynamic sky system with day/night cycles, weather, clouds, and VFX. + +## Setup Instructions + +### Step 1: Open Project in Godot +1. Open your Godot 4.6 project +2. Wait for the editor to import all scripts (check the bottom progress bar) +3. If you see script errors, click **Project > Reload Current Project** + +### Step 2: Add to Your Scene +1. Open the scene where you want the sky system +2. Drag `dynamic-sky/dynamic_sky.tscn` into your scene tree +3. Or right-click in the scene tree → **Instance Child Scene** → select `dynamic_sky.tscn` + +### Step 3: Configure the Environment +The DynamicSky node auto-creates required children. To link an existing environment: + +1. Select the **DynamicSky** node +2. In the Inspector, under **Environment**: + - Assign your existing `WorldEnvironment` node (or leave empty to auto-create) + - Assign existing `DirectionalLight3D` for sun/moon (or leave empty) + +### Step 4: Generate Cloud Textures +Clouds need textures. Use the procedural generator: + +```gdscript +# In a tool script or _ready(): +var cloud_tex = ProceduralCloudTexture.create_default_cloud_texture(1) +$DynamicSky/CloudSystem.layer_configs[0].texture = cloud_tex +``` + +Or assign existing cloud textures to each `CloudLayerConfig` in the Inspector. + +### Step 5: Configure Shadow Texture +```gdscript +var shadow_tex = ProceduralCloudTexture.create_shadow_texture() +$DynamicSky/ShadowProxySystem.shadow_texture = shadow_tex +``` + +## Testing the System + +### Test 1: Basic Scene Test +1. Create a new 3D scene with a `Camera3D` and a `MeshInstance3D` (e.g., plane for ground) +2. Instance `dynamic_sky.tscn` +3. Run the scene (F5) +4. You should see a gradient sky with sun light + +### Test 2: Time of Day +Add this script to test time changes: + +```gdscript +extends Node + +@onready var sky = $DynamicSky + +func _ready(): + # Start at morning + sky.set_time_of_day(0.25) + +func _process(delta): + # Press keys to change time + if Input.is_action_just_pressed("ui_right"): + sky.set_time_of_day(sky.get_time_of_day() + 0.05) + if Input.is_action_just_pressed("ui_left"): + sky.set_time_of_day(sky.get_time_of_day() - 0.05) + + # Print current time + if Input.is_action_just_pressed("ui_accept"): + print("Time: ", sky.get_time_of_day()) +``` + +### Test 3: Weather System +```gdscript +func _input(event): + if event.is_action_pressed("ui_up"): + $DynamicSky.set_weather(WeatherProfile.WeatherState.RAIN) + if event.is_action_pressed("ui_down"): + $DynamicSky.set_weather(WeatherProfile.WeatherState.CLEAR) +``` + +### Test 4: Debug Panel +1. Add a `CanvasLayer` to your scene +2. Add a `Control` node as child +3. Attach the `sky_debug_panel.gd` script +4. In Inspector, assign the `DynamicSky` node to the `dynamic_sky` property +5. Run and use the UI controls + +### Test 5: Meteor Shower +```gdscript +func _ready(): + # Ensure it's night for visibility + $DynamicSky.set_time_of_day(0.0) + # Wait a moment then trigger + await get_tree().create_timer(1.0).timeout + $DynamicSky.trigger_meteor_shower(30.0) +``` + +### Test 6: Kill Volumes +1. Add an `Area3D` to your scene +2. Attach `weather_kill_volume.gd` script +3. Configure the shape and size in Inspector +4. Precipitation will be blocked inside this volume + +## Complete Test Scene Example + +Create a new scene with this structure: + +``` +TestScene (Node3D) +├── DynamicSky (instance of dynamic_sky.tscn) +├── Camera3D +├── Ground (MeshInstance3D with PlaneMesh) +├── CanvasLayer +│ └── DebugPanel (Control with sky_debug_panel.gd) +└── TestScript (Node with test script below) +``` + +Test script: +```gdscript +extends Node + +@onready var sky = $"../DynamicSky" + +func _ready(): + # Generate cloud textures + _setup_clouds() + + # Start at mid-morning + sky.set_time_of_day(0.35) + + print("Dynamic Sky Test Ready!") + print("Arrow keys: Change time") + print("1-6: Change weather") + print("M: Meteor shower") + print("P: Pause/Resume time") + +func _setup_clouds(): + var cloud_system = sky.cloud_system + if cloud_system: + for i in cloud_system.layer_configs.size(): + var tex = ProceduralCloudTexture.create_default_cloud_texture(i) + cloud_system.layer_configs[i].texture = tex + cloud_system.rebuild_layers() + + var shadow_system = sky.shadow_proxy_system + if shadow_system: + shadow_system.shadow_texture = ProceduralCloudTexture.create_shadow_texture() + +func _input(event): + if event is InputEventKey and event.pressed: + match event.keycode: + KEY_RIGHT: + sky.set_time_of_day(wrapf(sky.get_time_of_day() + 0.02, 0, 1)) + KEY_LEFT: + sky.set_time_of_day(wrapf(sky.get_time_of_day() - 0.02, 0, 1)) + KEY_1: + sky.set_weather(WeatherProfile.WeatherState.CLEAR) + KEY_2: + sky.set_weather(WeatherProfile.WeatherState.CLOUDY) + KEY_3: + sky.set_weather(WeatherProfile.WeatherState.OVERCAST) + KEY_4: + sky.set_weather(WeatherProfile.WeatherState.RAIN) + KEY_5: + sky.set_weather(WeatherProfile.WeatherState.STORM) + KEY_6: + sky.set_weather(WeatherProfile.WeatherState.SNOW) + KEY_M: + sky.trigger_meteor_shower(30.0) + KEY_P: + if sky.day_night_controller.paused: + sky.resume_time() + else: + sky.pause_time() +``` + +## Troubleshooting + +| Issue | Solution | +|-------|----------| +| Script errors on load | Reload project: Project → Reload Current Project | +| No sky visible | Ensure camera is looking up; check WorldEnvironment exists | +| Clouds not visible | Assign textures to CloudLayerConfig or generate procedurally | +| Shadows not visible | Assign shadow_texture to ShadowProxySystem | +| Stars not visible | Set time to night (0.0 or 0.9+); check stars_visibility curve | +| Time not advancing | Check `paused` is false on DayNightController | +| Weather not changing | Check WeatherController has profiles; look at transition progress | + +## Features + +### Preset Skybox Nodes +- 6 presets: Clear Day, Sunset, Night, Overcast, Storm, Dawn +- Runtime preset switching with smooth transitions +- Non-destructive overrides + +### Day/Night Cycle +- Configurable day length (1-1440 minutes) +- Sun/moon arc movement +- Color/intensity curves for sun, moon, ambient, fog +- Star visibility at night +- Time events (sunrise, sunset, noon) + +### Weather System +- States: Clear, Cloudy, Overcast, Rain, Storm, Snow +- Smooth transitions between states +- Per-state cloud, wind, and VFX settings +- Random or scripted weather timelines + +### 2D Cloudscapes +- 4 independent layers (low, mid, high, wisps) +- Per-layer texture, tiling, opacity, color +- Flow animation with wind influence +- Evolution/morphing over time +- Reactive to sun/moon lighting + +### Post-Processing +- Sky gradient shading +- Height and distance fog +- Sun glow halo +- Dawn/dusk enhancement +- Procedural stars + +### VFX +- Shooting stars (randomized) +- Meteor showers (event-triggered) +- Configurable appearance + +### Cloud Shadows +- Projected shadow overlay +- Syncs with cloud movement +- Blur and opacity controls + +### Kill Volumes +- Block precipitation in indoor areas +- Box, sphere, cylinder shapes +- Fade-out margins + +## Quick Start + +1. Add `dynamic_sky.tscn` to your scene +2. Reference the DynamicSky node in your scripts +3. Use the API: + +```gdscript +# Get reference +@onready var sky = $DynamicSky + +# Set time (0.0-1.0, where 0.5 = noon) +sky.set_time_of_day(0.5) + +# Change weather +sky.set_weather(WeatherProfile.WeatherState.RAIN) + +# Apply preset +sky.apply_preset("Sunset") + +# Trigger meteor shower +sky.trigger_meteor_shower(60.0) +``` + +## Debug Panel + +Add `SkyDebugPanel` to your UI and assign the DynamicSky reference to see: +- Current time and weather +- Cloud coverage +- Controls for testing + +## Resources + +- `SkyConfig`: Global sky parameters +- `SkyPreset`: Complete preset configuration +- `WeatherProfile`: Per-weather-state settings +- `CloudLayerConfig`: Per-layer cloud settings +- `PostProcessProfile`: Post-processing parameters + +## Performance + +- Disable unused cloud layers +- Reduce cloud texture resolution +- Lower VFX update rates +- Disable shadow blur on low-end hardware diff --git a/dynamic-sky/SPEC.md b/dynamic-sky/SPEC.md new file mode 100644 index 0000000..468a443 --- /dev/null +++ b/dynamic-sky/SPEC.md @@ -0,0 +1,180 @@ +# Stylized Dynamic Sky Specification (Godot 4.6) + +## Overview +This document specifies a stylized dynamic sky system for Godot 4.6. It defines the required features, behavior, data structures, and integration points without prescribing implementation code. + +## Goals +- Provide a visually stylized, dynamic sky suitable for stylized or semi-realistic games. +- Offer modular components that can be combined or replaced. +- Enable deterministic playback for cutscenes and synchronized multiplayer. +- Support art-direction via presets and editor-friendly controls. + +## Non-Goals +- Physically accurate atmospheric scattering. +- Real-world meteorology simulation. +- Automated terrain-aware shadow projections. + +## System Architecture +- **DynamicSkyRoot**: Top-level scene controlling time, weather, and global parameters. +- **SkyboxPresetLibrary**: Collection of preset skybox nodes (configured resources/scenes). +- **DayNightController**: Controls sun/moon movement, ambient light, and time-of-day curves. +- **WeatherController**: Drives weather states, transitions, and VFX spawning. +- **CloudSystem2D**: Handles layered 2D cloudscapes, flow, animation, and evolution. +- **PostProcessController**: Applies stylized gradients, fog shaping, and sun glow. +- **VFXController**: Shooting stars and meteor showers. +- **ShadowProxySystem**: Faked cloud shadows projected onto world. + +## Feature Specifications + +### Preset Skybox Nodes +**Description**: Pre-authored skybox configurations that can be swapped at runtime. + +**Requirements**: +- At least 6 presets: Clear Day, Sunset, Night, Overcast, Storm, Dawn. +- Each preset includes: + - Skybox texture/gradient resources. + - Sun and moon color presets. + - Ambient and fog color presets. + - Default cloud layers and post-process settings. +- Presets can be overridden by runtime controllers without losing base values. +- Preset application is non-destructive and reversible. + +**Editor UX**: +- Preset picker dropdown. +- “Apply Preset” button and “Revert to Preset” button. + +### Day/Night Cycle +**Description**: Stylized, configurable cycle controlling lighting and sky appearance. + +**Requirements**: +- Time scale with configurable length (e.g., 10–240 minutes per day). +- Supports manual time-of-day override and pause. +- Sun and moon directional motion (arc across sky dome). +- Color and intensity curves: + - Sun color and intensity. + - Moon color and intensity. + - Ambient color and intensity. + - Fog color and density. +- Smooth transitions between time states. +- Optional star visibility curve. + +**Data**: +- TimeOfDay (0.0–1.0 normalized) +- Curves for color/intensity across time. + +### Weather System +**Description**: State-based weather controller with transitions and parameters. + +**Weather States**: +- Clear +- Cloudy +- Overcast +- Rain +- Storm +- Snow (optional, can be disabled) + +**Requirements**: +- State machine with configurable transition durations. +- Each state defines: + - Cloud density, opacity, and coverage. + - Wind speed and direction (affects cloud flow). + - VFX sets (rain, lightning, snow). + - Ambient and fog adjustments. +- Randomized or scripted weather timeline support. +- Events/callbacks on state enter/exit. + +### 2D Customizable Cloudscapes +**Description**: Layered 2D clouds rendered on the sky dome or as screen-space quads. + +**Requirements**: +- Up to 4 independent layers: low, mid, high, wisps. +- Per-layer settings: + - Texture/atlas selection. + - Tiling and scale. + - Opacity and color tint. + - Parallax depth factor. + - Flow direction and speed. +- Global cloud coverage control. + +### Cloud Flow, Animation, and Evolution +**Description**: Dynamic motion and shape changes over time. + +**Requirements**: +- Continuous UV flow with wind influence. +- Optional noise-based distortion for stylized movement. +- Evolution parameter to morph coverage and shape (low-frequency drift). +- Time-based variation seed to keep deterministic playback when seeded. + +### Cloud Lighting Reacts with Sun and Moon +**Description**: Cloud shading changes based on celestial light direction and intensity. + +**Requirements**: +- Light direction taken from sun/moon controller. +- Bright side and shadowed side color control. +- Night-time soft illumination from moon. +- Optional rim glow at dawn/dusk. + +### Weather Particle Kill Volumes +**Description**: Volumes that stop precipitation or weather particles in indoor or sheltered spaces. + +**Requirements**: +- Axis-aligned and/or custom-shaped kill volumes. +- Particles inside kill volume are disabled or instantly culled. +- Priority/stacking rules when volumes overlap. +- Optional fade-out margin to avoid hard cutoffs. + +### Stylized Post Process for Fog and Sun Gradients +**Description**: Screen-space effects to enhance gradients and fog stylization. + +**Requirements**: +- Adjustable sky gradient curve blending. +- Fog shaping controls (height fog, distance fog). +- Sun glow halo with stylized falloff. +- Dusk/dawn gradient emphasis. +- Option to toggle for performance profiles. + +### Shooting Star and Meteor Shower VFX +**Description**: Procedural or timed visual effects visible in the sky. + +**Requirements**: +- Occasional shooting stars (randomized frequency). +- Meteor shower events with configurable duration and intensity. +- Trail length, brightness, and color control. +- Optional audio event hook for meteor shower start. + +### Faked Cloud Shadows +**Description**: Stylized cloud shadow overlay projected onto the world. + +**Requirements**: +- Shadow texture projected in world space or onto a large quad. +- Movement matches cloud flow direction and speed. +- Darkness/opacity controls and color tint. +- Optional blur for softer look. + +## Data and Configuration +- **SkyConfig Resource**: Stores defaults for all controllers. +- **Preset Resource**: Encapsulates a SkyConfig plus skybox assets. +- **WeatherProfile**: Per-state settings and VFX parameters. +- **CloudLayerConfig**: Texture/flow/lighting settings per layer. +- **PostProcessProfile**: Gradient, fog, and sun glow parameters. + +## Performance Targets +- Target 60 FPS on mid-range hardware. +- Cloud layers should allow selective disabling. +- VFX update rates configurable. + +## Debug and Tooling +- On-screen debug panel showing time, weather state, cloud parameters. +- Seed display for deterministic playback. +- Editor gizmos for kill volumes. + +## Acceptance Criteria +- All required features are present and configurable in editor. +- Presets can be applied and reverted without data loss. +- Day/night transitions are smooth with no visible jumps. +- Weather transitions alter clouds, lighting, and VFX consistently. +- Cloud lighting visibly reacts to sun/moon changes. +- Kill volumes prevent precipitation in indoor spaces. +- Post process improves gradients and fog stylization. +- Shooting stars and meteor showers visible at night. +- Cloud shadows visibly move across the world. diff --git a/dynamic-sky/dynamic_sky.tscn b/dynamic-sky/dynamic_sky.tscn new file mode 100644 index 0000000..cbd808d --- /dev/null +++ b/dynamic-sky/dynamic_sky.tscn @@ -0,0 +1,394 @@ +[gd_scene format=3 uid="uid://doiojb8xxlp2s"] + +[ext_resource type="Script" uid="uid://8r3do6ow5nmn" path="res://dynamic-sky/scripts/dynamic_sky_root.gd" id="1_dynamic_sky"] +[ext_resource type="Script" uid="uid://cb8c6ug44jx4t" path="res://dynamic-sky/scripts/skybox_preset_library.gd" id="2_preset_library"] +[ext_resource type="Script" uid="uid://c48eq2y4ikok8" path="res://dynamic-sky/resources/sky_preset.gd" id="3_c6jpa"] +[ext_resource type="Script" uid="uid://gypnoh5ikwjn" path="res://dynamic-sky/scripts/day_night_controller.gd" id="3_day_night"] +[ext_resource type="Script" uid="uid://beu0xhk0rvpky" path="res://dynamic-sky/resources/cloud_layer_config.gd" id="4_ssi15"] +[ext_resource type="Script" uid="uid://blyx8v2gq8nlw" path="res://dynamic-sky/scripts/weather_controller.gd" id="4_weather"] +[ext_resource type="Script" uid="uid://c3iniqgu2l0ek" path="res://dynamic-sky/resources/sky_config.gd" id="5_2w5ig"] +[ext_resource type="Script" uid="uid://cwplvsarljsf" path="res://dynamic-sky/scripts/cloud_system_2d.gd" id="5_clouds"] +[ext_resource type="Script" uid="uid://cwqg0bg2m2bme" path="res://dynamic-sky/scripts/post_process_controller.gd" id="6_post_process"] +[ext_resource type="Texture2D" uid="uid://crrj260kiyoy" path="res://dynamic-sky/sky_44_2k.png" id="6_ssi15"] +[ext_resource type="Script" uid="uid://cnvj3m5fpsdtj" path="res://dynamic-sky/scripts/vfx_controller.gd" id="7_vfx"] +[ext_resource type="Script" uid="uid://d2srir3wsi045" path="res://dynamic-sky/resources/weather_profile.gd" id="8_cnx2n"] +[ext_resource type="Script" uid="uid://hvs2g6acqryq" path="res://dynamic-sky/scripts/shadow_proxy_system.gd" id="8_shadows"] +[ext_resource type="Script" uid="uid://ctrw2ycud36yd" path="res://dynamic-sky/resources/post_process_profile.gd" id="11_djhca"] +[ext_resource type="Environment" uid="uid://dq64sos14f8hu" path="res://dynamic-sky/environment.tres" id="13_2w5ig"] + +[sub_resource type="Resource" id="Resource_c27no"] +script = ExtResource("5_2w5ig") +sun_color = Color(1, 0.95, 0.85, 1) +sun_intensity = 1.2 +moon_intensity = 0.0 +ambient_color = Color(0.5, 0.6, 0.8, 1) +ambient_intensity = 0.6 +fog_color = Color(0.7, 0.8, 0.95, 1) +fog_density = 0.005 +sky_top_color = Color(0.2, 0.4, 0.85, 1) +sky_horizon_color = Color(0.6, 0.75, 0.95, 1) +sky_bottom_color = Color(0.5, 0.6, 0.7, 1) +cloud_coverage = 0.2 +cloud_opacity = 0.7 +sun_glow_intensity = 0.3 + +[sub_resource type="Resource" id="Resource_ssi15"] +script = ExtResource("11_djhca") +metadata/_custom_type_script = "uid://ctrw2ycud36yd" + +[sub_resource type="Resource" id="Resource_ajt3x"] +script = ExtResource("3_c6jpa") +preset_name = "Clear Day" +preset_type = 0 +config = SubResource("Resource_c27no") +skybox_texture = ExtResource("6_ssi15") +post_process_profile = SubResource("Resource_ssi15") + +[sub_resource type="Resource" id="Resource_cp2fs"] +script = ExtResource("5_2w5ig") +sun_color = Color(1, 0.5, 0.2, 1) +sun_intensity = 0.8 +moon_intensity = 0.0 +ambient_color = Color(0.8, 0.5, 0.4, 1) +ambient_intensity = 0.4 +fog_color = Color(0.9, 0.6, 0.4, 1) +fog_density = 0.015 +sky_top_color = Color(0.3, 0.2, 0.5, 1) +sky_horizon_color = Color(1, 0.5, 0.3, 1) +sky_bottom_color = Color(0.9, 0.4, 0.2, 1) +stars_visibility = 0.1 +cloud_opacity = 0.9 +cloud_color = Color(1, 0.7, 0.5, 1) +cloud_shadow_color = Color(0.4, 0.2, 0.3, 1) +sun_glow_intensity = 0.8 +sun_glow_size = 0.4 + +[sub_resource type="Resource" id="Resource_sptlx"] +script = ExtResource("3_c6jpa") +preset_name = "Sunset" +preset_type = 1 +config = SubResource("Resource_cp2fs") + +[sub_resource type="Resource" id="Resource_bmqu2"] +script = ExtResource("5_2w5ig") +sun_intensity = 0.0 +moon_color = Color(0.7, 0.8, 1, 1) +moon_intensity = 0.4 +ambient_color = Color(0.1, 0.15, 0.25, 1) +ambient_intensity = 0.2 +fog_color = Color(0.1, 0.12, 0.2, 1) +fog_density = 0.02 +sky_top_color = Color(0.02, 0.03, 0.08, 1) +sky_horizon_color = Color(0.1, 0.12, 0.2, 1) +sky_bottom_color = Color(0.05, 0.06, 0.1, 1) +stars_visibility = 1.0 +cloud_coverage = 0.15 +cloud_opacity = 0.5 +cloud_color = Color(0.2, 0.22, 0.3, 1) +cloud_shadow_color = Color(0.05, 0.05, 0.1, 1) +sun_glow_intensity = 0.0 + +[sub_resource type="Resource" id="Resource_fqsrh"] +script = ExtResource("3_c6jpa") +preset_name = "Night" +preset_type = 2 +config = SubResource("Resource_bmqu2") + +[sub_resource type="Resource" id="Resource_4i5ra"] +script = ExtResource("5_2w5ig") +sun_color = Color(0.9, 0.9, 0.85, 1) +sun_intensity = 0.4 +moon_intensity = 0.0 +ambient_color = Color(0.5, 0.5, 0.55, 1) +ambient_intensity = 0.7 +fog_color = Color(0.6, 0.62, 0.65, 1) +fog_density = 0.03 +sky_top_color = Color(0.45, 0.48, 0.55, 1) +sky_horizon_color = Color(0.6, 0.62, 0.65, 1) +sky_bottom_color = Color(0.5, 0.52, 0.55, 1) +cloud_coverage = 0.9 +cloud_opacity = 1.0 +cloud_color = Color(0.7, 0.72, 0.75, 1) +cloud_shadow_color = Color(0.4, 0.42, 0.45, 1) +sun_glow_intensity = 0.1 + +[sub_resource type="Resource" id="Resource_drex5"] +script = ExtResource("3_c6jpa") +preset_name = "Overcast" +preset_type = 3 +config = SubResource("Resource_4i5ra") + +[sub_resource type="Resource" id="Resource_gm4lj"] +script = ExtResource("5_2w5ig") +sun_color = Color(0.7, 0.7, 0.75, 1) +sun_intensity = 0.2 +moon_intensity = 0.0 +ambient_color = Color(0.35, 0.38, 0.45, 1) +fog_color = Color(0.4, 0.42, 0.5, 1) +fog_density = 0.05 +sky_top_color = Color(0.25, 0.28, 0.35, 1) +sky_horizon_color = Color(0.4, 0.42, 0.48, 1) +sky_bottom_color = Color(0.35, 0.38, 0.42, 1) +cloud_coverage = 1.0 +cloud_opacity = 1.0 +cloud_color = Color(0.4, 0.42, 0.5, 1) +cloud_shadow_color = Color(0.2, 0.22, 0.28, 1) +sun_glow_intensity = 0.0 + +[sub_resource type="Resource" id="Resource_5lttf"] +script = ExtResource("3_c6jpa") +preset_name = "Storm" +preset_type = 4 +config = SubResource("Resource_gm4lj") + +[sub_resource type="Resource" id="Resource_ym26c"] +script = ExtResource("5_2w5ig") +sun_color = Color(1, 0.7, 0.5, 1) +sun_intensity = 0.5 +moon_intensity = 0.1 +ambient_color = Color(0.4, 0.35, 0.45, 1) +ambient_intensity = 0.35 +fog_color = Color(0.6, 0.5, 0.55, 1) +fog_density = 0.025 +sky_top_color = Color(0.15, 0.2, 0.45, 1) +sky_horizon_color = Color(0.9, 0.6, 0.5, 1) +sky_bottom_color = Color(0.7, 0.45, 0.4, 1) +stars_visibility = 0.3 +cloud_coverage = 0.25 +cloud_color = Color(0.95, 0.7, 0.6, 1) +cloud_shadow_color = Color(0.3, 0.25, 0.35, 1) +sun_glow_intensity = 0.6 +sun_glow_size = 0.35 + +[sub_resource type="Resource" id="Resource_umd7u"] +script = ExtResource("3_c6jpa") +preset_name = "Dawn" +preset_type = 5 +config = SubResource("Resource_ym26c") + +[sub_resource type="Gradient" id="Gradient_sxvon"] +offsets = PackedFloat32Array(0, 0.25, 0.5, 0.75, 1) +colors = PackedColorArray(1, 0.6, 0.4, 1, 1, 0.5, 0.3, 1, 1, 1, 0.95, 1, 1, 0.7, 0.4, 1, 1, 1, 1, 1) + +[sub_resource type="Curve" id="Curve_fm80q"] +_data = [Vector2(0, 0), 0.0, 0.0, 0, 0, Vector2(0.2, 0), 0.0, 0.0, 0, 0, Vector2(0.3, 0.8), 0.0, 0.0, 0, 0, Vector2(0.5, 1), 0.0, 0.0, 0, 0, Vector2(0.7, 0.8), 0.0, 0.0, 0, 0, Vector2(0.85, 0), 0.0, 0.0, 0, 0, Vector2(1, 0), 0.0, 0.0, 0, 0] +point_count = 7 + +[sub_resource type="Gradient" id="Gradient_kcyr7"] +colors = PackedColorArray(0.7, 0.8, 1, 1, 0.7, 0.8, 1, 1) + +[sub_resource type="Curve" id="Curve_qti3v"] +_data = [Vector2(0, 0.4), 0.0, 0.0, 0, 0, Vector2(0.2, 0.3), 0.0, 0.0, 0, 0, Vector2(0.25, 0), 0.0, 0.0, 0, 0, Vector2(0.75, 0), 0.0, 0.0, 0, 0, Vector2(0.85, 0.3), 0.0, 0.0, 0, 0, Vector2(1, 0.4), 0.0, 0.0, 0, 0] +point_count = 6 + +[sub_resource type="Gradient" id="Gradient_bq8yq"] +offsets = PackedFloat32Array(0, 0.25, 0.5, 0.75, 1) +colors = PackedColorArray(0.15, 0.18, 0.3, 1, 0.15, 0.18, 0.3, 1, 0.6, 0.65, 0.75, 1, 0.55, 0.5, 0.55, 1, 1, 1, 1, 1) + +[sub_resource type="Curve" id="Curve_x6xf2"] +_data = [Vector2(0, 0.2), 0.0, 0.0, 0, 0, Vector2(0.25, 0.5), 0.0, 0.0, 0, 0, Vector2(0.5, 0.7), 0.0, 0.0, 0, 0, Vector2(0.75, 0.5), 0.0, 0.0, 0, 0, Vector2(1, 0.2), 0.0, 0.0, 0, 0] +point_count = 5 + +[sub_resource type="Gradient" id="Gradient_tn5up"] +offsets = PackedFloat32Array(0, 0.25, 0.5, 0.75, 1) +colors = PackedColorArray(0.1, 0.12, 0.2, 1, 0.1, 0.12, 0.2, 1, 0.7, 0.75, 0.85, 1, 0.7, 0.55, 0.5, 1, 1, 1, 1, 1) + +[sub_resource type="Curve" id="Curve_ej5u1"] +_data = [Vector2(0, 0.02), 0.0, 0.0, 0, 0, Vector2(0.25, 0.015), 0.0, 0.0, 0, 0, Vector2(0.5, 0.01), 0.0, 0.0, 0, 0, Vector2(0.75, 0.015), 0.0, 0.0, 0, 0, Vector2(1, 0.02), 0.0, 0.0, 0, 0] +point_count = 5 + +[sub_resource type="Curve" id="Curve_ta3fj"] +_data = [Vector2(0, 1), 0.0, 0.0, 0, 0, Vector2(0.2, 0.8), 0.0, 0.0, 0, 0, Vector2(0.3, 0), 0.0, 0.0, 0, 0, Vector2(0.7, 0), 0.0, 0.0, 0, 0, Vector2(0.85, 0.8), 0.0, 0.0, 0, 0, Vector2(1, 1), 0.0, 0.0, 0, 0] +point_count = 6 + +[sub_resource type="Resource" id="Resource_dfjcy"] +script = ExtResource("8_cnx2n") +cloud_density = 0.1 +cloud_opacity = 0.6 +cloud_coverage = 0.2 +wind_speed = 0.5 + +[sub_resource type="Resource" id="Resource_sjmt0"] +script = ExtResource("8_cnx2n") +weather_state = 1 +display_name = "Cloudy" +cloud_density = 0.5 +cloud_opacity = 0.8 +cloud_coverage = 0.5 +ambient_intensity_multiplier = 0.9 + +[sub_resource type="Resource" id="Resource_uap2q"] +script = ExtResource("8_cnx2n") +weather_state = 2 +display_name = "Overcast" +cloud_density = 0.9 +cloud_opacity = 1.0 +cloud_coverage = 0.9 +cloud_color = Color(0.7, 0.72, 0.75, 1) +wind_speed = 1.5 +ambient_intensity_multiplier = 0.7 +fog_density_multiplier = 1.5 + +[sub_resource type="Resource" id="Resource_mcoyn"] +script = ExtResource("8_cnx2n") +weather_state = 3 +display_name = "Rain" +cloud_density = 0.85 +cloud_opacity = 0.95 +cloud_coverage = 0.85 +cloud_color = Color(0.55, 0.58, 0.65, 1) +wind_speed = 2.0 +ambient_intensity_multiplier = 0.6 +fog_density_multiplier = 2.0 +rain_intensity = 0.7 +default_transition_duration = 8.0 + +[sub_resource type="Resource" id="Resource_mr14h"] +script = ExtResource("8_cnx2n") +weather_state = 4 +display_name = "Storm" +cloud_density = 1.0 +cloud_opacity = 1.0 +cloud_coverage = 1.0 +cloud_color = Color(0.4, 0.42, 0.5, 1) +cloud_shadow_color = Color(0.2, 0.22, 0.28, 1) +wind_speed = 4.0 +wind_turbulence = 0.4 +ambient_intensity_multiplier = 0.4 +fog_density_multiplier = 3.0 +rain_intensity = 1.0 +lightning_enabled = true +lightning_frequency = 0.15 +default_transition_duration = 10.0 + +[sub_resource type="Resource" id="Resource_ps44b"] +script = ExtResource("8_cnx2n") +weather_state = 5 +display_name = "Snow" +cloud_density = 0.8 +cloud_opacity = 0.9 +cloud_coverage = 0.8 +cloud_color = Color(0.85, 0.87, 0.92, 1) +ambient_color_multiplier = Color(0.9, 0.92, 1, 1) +ambient_intensity_multiplier = 0.75 +fog_color_multiplier = Color(0.95, 0.97, 1, 1) +fog_density_multiplier = 1.8 +snow_intensity = 0.8 +default_transition_duration = 12.0 + +[sub_resource type="Curve" id="Curve_uhdsh"] +_data = [Vector2(0, 0), 0.0, 0.0, 0, 0, Vector2(0.5, 0.5), 0.0, 0.0, 0, 0, Vector2(1, 1), 0.0, 0.0, 0, 0] +point_count = 3 + +[sub_resource type="Resource" id="Resource_xh4oc"] +script = ExtResource("4_ssi15") +layer_type = 0 +tiling = Vector2(2, 2) +opacity = 0.9 +flow_speed = 0.03 +parallax_depth = 0.3 + +[sub_resource type="Resource" id="Resource_ahi10"] +script = ExtResource("4_ssi15") +tiling = Vector2(3, 3) +opacity = 0.7 +parallax_depth = 0.6 + +[sub_resource type="Resource" id="Resource_a5xai"] +script = ExtResource("4_ssi15") +layer_type = 2 +opacity = 0.5 +flow_speed = 0.01 +parallax_depth = 0.9 + +[sub_resource type="Resource" id="Resource_uxs4q"] +script = ExtResource("4_ssi15") +layer_type = 3 +tiling = Vector2(6, 6) +opacity = 0.3 +flow_speed = 0.015 + +[sub_resource type="Compositor" id="Compositor_ssi15"] + +[node name="DynamicSky" type="Node3D" unique_id=516593260 node_paths=PackedStringArray("preset_library", "day_night_controller", "weather_controller", "cloud_system", "post_process_controller", "vfx_controller", "shadow_proxy_system", "world_environment", "sun_light", "moon_light")] +script = ExtResource("1_dynamic_sky") +preset_library = NodePath("PresetLibrary") +day_night_controller = NodePath("DayNightController") +weather_controller = NodePath("WeatherController") +cloud_system = NodePath("CloudSystem") +post_process_controller = NodePath("PostProcessController") +vfx_controller = NodePath("VFXController") +shadow_proxy_system = NodePath("ShadowProxySystem") +world_environment = NodePath("WorldEnvironment") +sun_light = NodePath("SunLight") +moon_light = NodePath("MoonLight") + +[node name="PresetLibrary" type="Node" parent="." unique_id=1947927418] +script = ExtResource("2_preset_library") +presets = Array[ExtResource("3_c6jpa")]([SubResource("Resource_ajt3x"), SubResource("Resource_sptlx"), SubResource("Resource_fqsrh"), SubResource("Resource_drex5"), SubResource("Resource_5lttf"), SubResource("Resource_umd7u")]) + +[node name="DayNightController" type="Node" parent="." unique_id=1083149574] +script = ExtResource("3_day_night") +time_of_day = 0.1482273141975634 +current_day = 4 +sun_color_curve = SubResource("Gradient_sxvon") +sun_intensity_curve = SubResource("Curve_fm80q") +moon_color_curve = SubResource("Gradient_kcyr7") +moon_intensity_curve = SubResource("Curve_qti3v") +ambient_color_curve = SubResource("Gradient_bq8yq") +ambient_intensity_curve = SubResource("Curve_x6xf2") +fog_color_curve = SubResource("Gradient_tn5up") +fog_density_curve = SubResource("Curve_ej5u1") +stars_visibility_curve = SubResource("Curve_ta3fj") + +[node name="WeatherController" type="Node" parent="." unique_id=1440281936] +script = ExtResource("4_weather") +weather_profiles = Array[ExtResource("8_cnx2n")]([SubResource("Resource_dfjcy"), SubResource("Resource_sjmt0"), SubResource("Resource_uap2q"), SubResource("Resource_mcoyn"), SubResource("Resource_mr14h"), SubResource("Resource_ps44b")]) +transition_curve = SubResource("Curve_uhdsh") + +[node name="CloudSystem" type="Node3D" parent="." unique_id=680200253] +script = ExtResource("5_clouds") +layer_configs = Array[ExtResource("4_ssi15")]([SubResource("Resource_xh4oc"), SubResource("Resource_ahi10"), SubResource("Resource_a5xai"), SubResource("Resource_uxs4q")]) +global_coverage = 0.2 +global_wind_speed = 0.5 +sun_direction = Vector3(0.6647294, -0.6939493, -0.2767116) +sun_color = Color(1, 0.55136895, 0.3513689, 1) +sun_intensity = 0.0 +moon_direction = Vector3(0.6799649, 0.7098545, 0.18372385) +moon_intensity = 0.33406785 + +[node name="PostProcessController" type="Node" parent="." unique_id=186747041] +script = ExtResource("6_post_process") +profile = SubResource("Resource_ssi15") +target_environment = ExtResource("13_2w5ig") +sky_top_color = Color(0.2, 0.4, 0.85, 1) +sky_horizon_color = Color(0.6, 0.75, 0.95, 1) +sky_bottom_color = Color(0.5, 0.6, 0.7, 1) +skybox_texture = ExtResource("6_ssi15") +fog_color = Color(0.1, 0.12, 0.2, 1) +fog_density = 0.017602645 +sun_direction = Vector3(0.6647294, -0.6939493, -0.2767116) + +[node name="VFXController" type="Node3D" parent="." unique_id=207251300] +script = ExtResource("7_vfx") + +[node name="ShadowProxySystem" type="Node3D" parent="." unique_id=1982828842] +script = ExtResource("8_shadows") +shadow_opacity = 0.1 +flow_speed = 2.5 + +[node name="WorldEnvironment" type="WorldEnvironment" parent="." unique_id=700760798] +environment = ExtResource("13_2w5ig") +compositor = SubResource("Compositor_ssi15") + +[node name="SunLight" type="DirectionalLight3D" parent="." unique_id=1278065589] +transform = Transform3D(-0.38430893, -0.9231061, 0.0134779755, 0, 0.014599117, 0.99989367, -0.9232045, 0.384268, -0.0056105736, 0, 50, 0) +visible = false +light_color = Color(1, 0.55136895, 0.3513689, 1) +light_energy = 0.0 +shadow_enabled = true + +[node name="MoonLight" type="DirectionalLight3D" parent="." unique_id=52072807] +transform = Transform3D(0.2608423, -0.9652886, 0.013407094, 0, 0.013887873, 0.9999038, -0.9653814, -0.26081723, 0.003622545, 0, 50, 0) +light_color = Color(0.7, 0.8, 1, 1) +light_energy = 0.33406785 diff --git a/dynamic-sky/dynamic_sky_toy.tscn b/dynamic-sky/dynamic_sky_toy.tscn new file mode 100644 index 0000000..726f784 --- /dev/null +++ b/dynamic-sky/dynamic_sky_toy.tscn @@ -0,0 +1,25 @@ +[gd_scene format=3 uid="uid://c0752ygxeq4rc"] + +[ext_resource type="PackedScene" uid="uid://doiojb8xxlp2s" path="res://dynamic-sky/dynamic_sky.tscn" id="1_cuadp"] +[ext_resource type="Script" uid="uid://cimhpqq1qo80b" path="res://dynamic-sky/scripts/sky_debug_panel.gd" id="2_o3fwy"] + +[node name="DynamicSkyToy" type="Node3D" unique_id=1968105337] + +[node name="DynamicSky" parent="." unique_id=516593260 instance=ExtResource("1_cuadp")] + +[node name="Camera3D" type="Camera3D" parent="." unique_id=805686804] + +[node name="Control" type="Control" parent="." unique_id=83422594] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="SkyDebugPanel" type="Control" parent="Control" unique_id=1408698131] +anchors_preset = 0 +offset_right = 40.0 +offset_bottom = 40.0 +script = ExtResource("2_o3fwy") +metadata/_custom_type_script = "uid://cimhpqq1qo80b" diff --git a/dynamic-sky/environment.tres b/dynamic-sky/environment.tres new file mode 100644 index 0000000..9bb3467 --- /dev/null +++ b/dynamic-sky/environment.tres @@ -0,0 +1,136 @@ +[gd_resource type="Environment" format=3 uid="uid://dq64sos14f8hu"] + +[ext_resource type="Texture2D" uid="uid://crrj260kiyoy" path="res://dynamic-sky/sky_44_2k.png" id="1_vd8dq"] + +[sub_resource type="Shader" id="Shader_porxs"] +code = " +shader_type sky; + +uniform sampler2D skybox_texture : source_color, filter_linear_mipmap, repeat_enable; +uniform bool skybox_enabled = false; +uniform float skybox_blend = 1.0; + +uniform vec4 sky_top_color : source_color = vec4(0.2, 0.4, 0.8, 1.0); +uniform vec4 sky_horizon_color : source_color = vec4(0.6, 0.7, 0.9, 1.0); +uniform vec4 sky_bottom_color : source_color = vec4(0.4, 0.5, 0.6, 1.0); +uniform float gradient_intensity = 1.0; +uniform float horizon_blend = 0.5; + +uniform bool sun_glow_enabled = true; +uniform vec3 sun_direction = vec3(0.0, 1.0, 0.0); +uniform vec4 sun_glow_color : source_color = vec4(1.0, 0.95, 0.8, 1.0); +uniform float sun_glow_intensity = 0.5; +uniform float sun_glow_size = 0.2; +uniform float sun_glow_falloff = 2.0; + +uniform bool dawn_dusk_enabled = true; +uniform vec4 dawn_dusk_color : source_color = vec4(1.0, 0.6, 0.4, 1.0); +uniform float dawn_dusk_intensity = 0.5; +uniform float dawn_dusk_blend = 0.0; + +uniform float stars_visibility = 0.0; +uniform vec4 stars_tint : source_color = vec4(1.0); + +float hash(vec2 p) { + return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); +} + +float stars(vec3 dir) { + vec2 uv = dir.xz / (abs(dir.y) + 0.001) * 50.0; + vec2 cell = floor(uv); + vec2 local = fract(uv) - 0.5; + + float star = 0.0; + float h = hash(cell); + if (h > 0.97) { + float dist = length(local); + star = smoothstep(0.1, 0.0, dist) * (h - 0.97) / 0.03; + star *= step(0.0, dir.y); + } + return star; +} + +vec2 pano_uv(vec3 dir) { + dir = normalize(dir); + float u = atan(dir.x, dir.z) / (2.0 * PI) + 0.5; + float v = acos(clamp(dir.y, -1.0, 1.0)) / PI; + return vec2(u, v); +} + +void sky() { + vec3 dir = EYEDIR; + float y = dir.y; + + vec3 color; + if (y > 0.0) { + float t = pow(y, horizon_blend); + color = mix(sky_horizon_color.rgb, sky_top_color.rgb, t); + } else { + float t = pow(-y, horizon_blend); + color = mix(sky_horizon_color.rgb, sky_bottom_color.rgb, t); + } + + color = mix(vec3(0.5), color, gradient_intensity); + + if (skybox_enabled) { + vec3 pano_color = texture(skybox_texture, pano_uv(dir)).rgb; + color = mix(color, pano_color, skybox_blend); + } + + if (sun_glow_enabled && sun_direction.y > -0.2) { + float sun_dot = max(0.0, dot(dir, normalize(sun_direction))); + float glow = pow(sun_dot, 1.0 / max(0.001, sun_glow_size)); + glow = pow(glow, sun_glow_falloff); + color += sun_glow_color.rgb * glow * sun_glow_intensity; + } + + if (dawn_dusk_enabled && dawn_dusk_blend > 0.0) { + float horizon_factor = 1.0 - abs(y); + horizon_factor = pow(horizon_factor, 2.0); + color = mix(color, dawn_dusk_color.rgb, horizon_factor * dawn_dusk_intensity * dawn_dusk_blend); + } + + if (stars_visibility > 0.0 && y > 0.0) { + float star_value = stars(dir) * stars_visibility; + color += stars_tint.rgb * star_value; + } + + COLOR = color; +} +" + +[sub_resource type="ShaderMaterial" id="ShaderMaterial_jmmec"] +shader = SubResource("Shader_porxs") +shader_parameter/skybox_texture = ExtResource("1_vd8dq") +shader_parameter/skybox_enabled = true +shader_parameter/skybox_blend = 1.0 +shader_parameter/sky_top_color = Color(0.2, 0.4, 0.85, 1) +shader_parameter/sky_horizon_color = Color(0.6, 0.75, 0.95, 1) +shader_parameter/sky_bottom_color = Color(0.5, 0.6, 0.7, 1) +shader_parameter/gradient_intensity = 1.0 +shader_parameter/horizon_blend = 0.5 +shader_parameter/sun_glow_enabled = true +shader_parameter/sun_direction = Vector3(0.9079945, -0.38918287, -0.1551863) +shader_parameter/sun_glow_color = Color(1, 0.5257787, 0.32577872, 1) +shader_parameter/sun_glow_intensity = 0.0 +shader_parameter/sun_glow_size = 0.2 +shader_parameter/sun_glow_falloff = 2.0 +shader_parameter/dawn_dusk_enabled = true +shader_parameter/dawn_dusk_color = Color(1, 0.6, 0.4, 1) +shader_parameter/dawn_dusk_intensity = 0.5 +shader_parameter/dawn_dusk_blend = 0.0 +shader_parameter/stars_visibility = 0.80297995 +shader_parameter/stars_tint = Color(1, 1, 1, 1) + +[sub_resource type="Sky" id="Sky_ssi15"] +sky_material = SubResource("ShaderMaterial_jmmec") + +[resource] +background_mode = 2 +sky = SubResource("Sky_ssi15") +ambient_light_color = Color(0.15, 0.18, 0.3, 1) +ambient_light_energy = 0.4504698 +fog_enabled = true +fog_light_color = Color(0.1, 0.12, 0.2, 1) +fog_density = 0.015825503 +fog_height_density = 0.1 diff --git a/dynamic-sky/presets/clear_day.tres b/dynamic-sky/presets/clear_day.tres new file mode 100644 index 0000000..6a446ac --- /dev/null +++ b/dynamic-sky/presets/clear_day.tres @@ -0,0 +1,17 @@ +[gd_resource type="Resource" script_class="SkyPreset" format=3 uid="uid://dose3mxe2snku"] + +[ext_resource type="Script" uid="uid://c48eq2y4ikok8" path="res://dynamic-sky/resources/sky_preset.gd" id="1"] +[ext_resource type="Script" uid="uid://beu0xhk0rvpky" path="res://dynamic-sky/resources/cloud_layer_config.gd" id="1_5fmul"] +[ext_resource type="Script" uid="uid://c3iniqgu2l0ek" path="res://dynamic-sky/resources/sky_config.gd" id="2_2a7hp"] +[ext_resource type="Texture2D" uid="uid://crrj260kiyoy" path="res://dynamic-sky/sky_44_2k.png" id="4_a3ytv"] + +[sub_resource type="Resource" id="Resource_ktpc1"] +script = ExtResource("2_2a7hp") +metadata/_custom_type_script = "uid://c3iniqgu2l0ek" + +[resource] +script = ExtResource("1") +preset_name = "Clear Day" +preset_type = 0 +config = SubResource("Resource_ktpc1") +skybox_texture = ExtResource("4_a3ytv") diff --git a/dynamic-sky/presets/dawn.tres b/dynamic-sky/presets/dawn.tres new file mode 100644 index 0000000..9dd1c13 --- /dev/null +++ b/dynamic-sky/presets/dawn.tres @@ -0,0 +1,17 @@ +[gd_resource type="Resource" script_class="SkyPreset" format=3 uid="uid://cg0qi1v475fvo"] + +[ext_resource type="Script" uid="uid://c48eq2y4ikok8" path="res://dynamic-sky/resources/sky_preset.gd" id="1"] +[ext_resource type="Script" uid="uid://beu0xhk0rvpky" path="res://dynamic-sky/resources/cloud_layer_config.gd" id="1_etfyk"] +[ext_resource type="Script" uid="uid://c3iniqgu2l0ek" path="res://dynamic-sky/resources/sky_config.gd" id="2_yd2ys"] +[ext_resource type="Texture2D" uid="uid://sl180wnwa3su" path="res://dynamic-sky/sky_25_2k.png" id="4_ckylf"] + +[sub_resource type="Resource" id="Resource_o0y23"] +script = ExtResource("2_yd2ys") +metadata/_custom_type_script = "uid://c3iniqgu2l0ek" + +[resource] +script = ExtResource("1") +preset_name = "Dawn" +preset_type = 5 +config = SubResource("Resource_o0y23") +skybox_texture = ExtResource("4_ckylf") diff --git a/dynamic-sky/presets/night.tres b/dynamic-sky/presets/night.tres new file mode 100644 index 0000000..d31f527 --- /dev/null +++ b/dynamic-sky/presets/night.tres @@ -0,0 +1,11 @@ +[gd_resource type="Resource" script_class="SkyPreset" format=3 uid="uid://b08fj288b38q6"] + +[ext_resource type="Script" uid="uid://c48eq2y4ikok8" path="res://dynamic-sky/resources/sky_preset.gd" id="1"] +[ext_resource type="Script" uid="uid://beu0xhk0rvpky" path="res://dynamic-sky/resources/cloud_layer_config.gd" id="1_eyyte"] +[ext_resource type="Texture2D" uid="uid://cu4ogwg6537jc" path="res://dynamic-sky/sky_16_2k.png" id="3_75xut"] + +[resource] +script = ExtResource("1") +preset_name = "Night" +preset_type = 2 +skybox_texture = ExtResource("3_75xut") diff --git a/dynamic-sky/presets/overcast.tres b/dynamic-sky/presets/overcast.tres new file mode 100644 index 0000000..1e5b99e --- /dev/null +++ b/dynamic-sky/presets/overcast.tres @@ -0,0 +1,15 @@ +[gd_resource type="Resource" script_class="SkyPreset" format=3 uid="uid://b6hytb5q01khi"] + +[ext_resource type="Script" uid="uid://c48eq2y4ikok8" path="res://dynamic-sky/resources/sky_preset.gd" id="1"] +[ext_resource type="Script" uid="uid://beu0xhk0rvpky" path="res://dynamic-sky/resources/cloud_layer_config.gd" id="1_vtd2i"] +[ext_resource type="Script" uid="uid://c3iniqgu2l0ek" path="res://dynamic-sky/resources/sky_config.gd" id="2_1tetv"] + +[sub_resource type="Resource" id="Resource_k34u2"] +script = ExtResource("2_1tetv") +metadata/_custom_type_script = "uid://c3iniqgu2l0ek" + +[resource] +script = ExtResource("1") +preset_name = "Overcast" +preset_type = 3 +config = SubResource("Resource_k34u2") diff --git a/dynamic-sky/presets/storm.tres b/dynamic-sky/presets/storm.tres new file mode 100644 index 0000000..811af15 --- /dev/null +++ b/dynamic-sky/presets/storm.tres @@ -0,0 +1,8 @@ +[gd_resource type="Resource" script_class="SkyPreset" load_steps=2 format=3] + +[ext_resource type="Script" path="res://dynamic-sky/resources/sky_preset.gd" id="1"] + +[resource] +script = ExtResource("1") +preset_name = "Storm" +preset_type = 4 diff --git a/dynamic-sky/presets/sunset.tres b/dynamic-sky/presets/sunset.tres new file mode 100644 index 0000000..40f0ca8 --- /dev/null +++ b/dynamic-sky/presets/sunset.tres @@ -0,0 +1,8 @@ +[gd_resource type="Resource" script_class="SkyPreset" load_steps=2 format=3] + +[ext_resource type="Script" path="res://dynamic-sky/resources/sky_preset.gd" id="1"] + +[resource] +script = ExtResource("1") +preset_name = "Sunset" +preset_type = 1 diff --git a/dynamic-sky/resources/cloud_layer_config.gd b/dynamic-sky/resources/cloud_layer_config.gd new file mode 100644 index 0000000..a38cf3b --- /dev/null +++ b/dynamic-sky/resources/cloud_layer_config.gd @@ -0,0 +1,65 @@ +@tool +class_name CloudLayerConfig +extends Resource + +enum LayerType { + LOW, + MID, + HIGH, + WISPS +} + +@export var layer_type: LayerType = LayerType.MID +@export var enabled: bool = true + +@export_group("Texture") +@export var texture: Texture2D +@export var texture_scale: Vector2 = Vector2(1.0, 1.0) +@export var tiling: Vector2 = Vector2(4.0, 4.0) + +@export_group("Appearance") +@export var opacity: float = 0.8 +@export var color_tint: Color = Color.WHITE +@export var shadow_color: Color = Color(0.6, 0.6, 0.7) + +@export_group("Motion") +@export var flow_direction: Vector2 = Vector2(1.0, 0.0) +@export var flow_speed: float = 0.02 +@export var parallax_depth: float = 1.0 + +@export_group("Lighting") +@export var receive_light: bool = true +@export var light_influence: float = 1.0 +@export var rim_glow_intensity: float = 0.3 + + +func get_layer_height() -> float: + match layer_type: + LayerType.LOW: + return 0.2 + LayerType.MID: + return 0.5 + LayerType.HIGH: + return 0.8 + LayerType.WISPS: + return 0.9 + return 0.5 + + +func duplicate_config() -> CloudLayerConfig: + var copy := CloudLayerConfig.new() + copy.layer_type = layer_type + copy.enabled = enabled + copy.texture = texture + copy.texture_scale = texture_scale + copy.tiling = tiling + copy.opacity = opacity + copy.color_tint = color_tint + copy.shadow_color = shadow_color + copy.flow_direction = flow_direction + copy.flow_speed = flow_speed + copy.parallax_depth = parallax_depth + copy.receive_light = receive_light + copy.light_influence = light_influence + copy.rim_glow_intensity = rim_glow_intensity + return copy diff --git a/dynamic-sky/resources/cloud_layer_config.gd.uid b/dynamic-sky/resources/cloud_layer_config.gd.uid new file mode 100644 index 0000000..329f1f0 --- /dev/null +++ b/dynamic-sky/resources/cloud_layer_config.gd.uid @@ -0,0 +1 @@ +uid://beu0xhk0rvpky diff --git a/dynamic-sky/resources/post_process_profile.gd b/dynamic-sky/resources/post_process_profile.gd new file mode 100644 index 0000000..3403d5a --- /dev/null +++ b/dynamic-sky/resources/post_process_profile.gd @@ -0,0 +1,55 @@ +@tool +class_name PostProcessProfile +extends Resource + +@export_group("Sky Gradient") +@export var gradient_enabled: bool = true +@export var gradient_intensity: float = 1.0 +@export var gradient_curve: Curve + +@export_group("Fog Shaping") +@export var fog_enabled: bool = true +@export var height_fog_enabled: bool = true +@export var height_fog_start: float = 0.0 +@export var height_fog_end: float = 100.0 +@export var height_fog_curve: float = 1.0 +@export var distance_fog_enabled: bool = true +@export var distance_fog_start: float = 50.0 +@export var distance_fog_end: float = 500.0 + +@export_group("Sun Glow") +@export var sun_glow_enabled: bool = true +@export var sun_glow_intensity: float = 0.5 +@export var sun_glow_size: float = 0.2 +@export var sun_glow_falloff: float = 2.0 +@export var sun_glow_color: Color = Color(1.0, 0.95, 0.8) + +@export_group("Dawn/Dusk Enhancement") +@export var dawn_dusk_enabled: bool = true +@export var dawn_dusk_intensity: float = 0.5 +@export var dawn_dusk_color: Color = Color(1.0, 0.6, 0.4) +@export var dawn_dusk_blend_range: float = 0.15 + + +func lerp_to(other: PostProcessProfile, weight: float) -> PostProcessProfile: + var result := PostProcessProfile.new() + result.gradient_enabled = gradient_enabled if weight < 0.5 else other.gradient_enabled + result.gradient_intensity = lerpf(gradient_intensity, other.gradient_intensity, weight) + result.fog_enabled = fog_enabled if weight < 0.5 else other.fog_enabled + result.height_fog_enabled = height_fog_enabled if weight < 0.5 else other.height_fog_enabled + result.height_fog_start = lerpf(height_fog_start, other.height_fog_start, weight) + result.height_fog_end = lerpf(height_fog_end, other.height_fog_end, weight) + result.height_fog_curve = lerpf(height_fog_curve, other.height_fog_curve, weight) + result.distance_fog_enabled = distance_fog_enabled if weight < 0.5 else other.distance_fog_enabled + result.distance_fog_start = lerpf(distance_fog_start, other.distance_fog_start, weight) + result.distance_fog_end = lerpf(distance_fog_end, other.distance_fog_end, weight) + result.sun_glow_enabled = sun_glow_enabled if weight < 0.5 else other.sun_glow_enabled + result.sun_glow_intensity = lerpf(sun_glow_intensity, other.sun_glow_intensity, weight) + result.sun_glow_size = lerpf(sun_glow_size, other.sun_glow_size, weight) + result.sun_glow_falloff = lerpf(sun_glow_falloff, other.sun_glow_falloff, weight) + result.sun_glow_color = sun_glow_color.lerp(other.sun_glow_color, weight) + result.dawn_dusk_enabled = dawn_dusk_enabled if weight < 0.5 else other.dawn_dusk_enabled + result.dawn_dusk_intensity = lerpf(dawn_dusk_intensity, other.dawn_dusk_intensity, weight) + result.dawn_dusk_color = dawn_dusk_color.lerp(other.dawn_dusk_color, weight) + result.dawn_dusk_blend_range = lerpf(dawn_dusk_blend_range, other.dawn_dusk_blend_range, weight) + return result diff --git a/dynamic-sky/resources/post_process_profile.gd.uid b/dynamic-sky/resources/post_process_profile.gd.uid new file mode 100644 index 0000000..45ad061 --- /dev/null +++ b/dynamic-sky/resources/post_process_profile.gd.uid @@ -0,0 +1 @@ +uid://ctrw2ycud36yd diff --git a/dynamic-sky/resources/sky_config.gd b/dynamic-sky/resources/sky_config.gd new file mode 100644 index 0000000..6dbb21f --- /dev/null +++ b/dynamic-sky/resources/sky_config.gd @@ -0,0 +1,104 @@ +@tool +class_name SkyConfig +extends Resource + +@export_group("Sun Settings") +@export var sun_color: Color = Color(1.0, 0.95, 0.8) +@export var sun_intensity: float = 1.0 +@export var sun_size: float = 0.05 + +@export_group("Moon Settings") +@export var moon_color: Color = Color(0.8, 0.85, 1.0) +@export var moon_intensity: float = 0.3 +@export var moon_size: float = 0.03 + +@export_group("Ambient Settings") +@export var ambient_color: Color = Color(0.4, 0.5, 0.6) +@export var ambient_intensity: float = 0.5 + +@export_group("Fog Settings") +@export var fog_color: Color = Color(0.7, 0.8, 0.9) +@export var fog_density: float = 0.01 +@export var fog_height: float = 0.0 +@export var fog_height_density: float = 0.1 + +@export_group("Sky Gradient") +@export var sky_top_color: Color = Color(0.2, 0.4, 0.8) +@export var sky_horizon_color: Color = Color(0.6, 0.7, 0.9) +@export var sky_bottom_color: Color = Color(0.4, 0.5, 0.6) +@export var horizon_blend: float = 0.5 + +@export_group("Stars") +@export var stars_visibility: float = 0.0 +@export var stars_tint: Color = Color.WHITE + +@export_group("Cloud Defaults") +@export var cloud_coverage: float = 0.3 +@export var cloud_opacity: float = 0.8 +@export var cloud_color: Color = Color.WHITE +@export var cloud_shadow_color: Color = Color(0.6, 0.6, 0.7) + +@export_group("Post Process") +@export var sun_glow_intensity: float = 0.5 +@export var sun_glow_size: float = 0.2 +@export var gradient_intensity: float = 1.0 + + +func lerp_to(other: SkyConfig, weight: float) -> SkyConfig: + var result := SkyConfig.new() + result.sun_color = sun_color.lerp(other.sun_color, weight) + result.sun_intensity = lerpf(sun_intensity, other.sun_intensity, weight) + result.sun_size = lerpf(sun_size, other.sun_size, weight) + result.moon_color = moon_color.lerp(other.moon_color, weight) + result.moon_intensity = lerpf(moon_intensity, other.moon_intensity, weight) + result.moon_size = lerpf(moon_size, other.moon_size, weight) + result.ambient_color = ambient_color.lerp(other.ambient_color, weight) + result.ambient_intensity = lerpf(ambient_intensity, other.ambient_intensity, weight) + result.fog_color = fog_color.lerp(other.fog_color, weight) + result.fog_density = lerpf(fog_density, other.fog_density, weight) + result.fog_height = lerpf(fog_height, other.fog_height, weight) + result.fog_height_density = lerpf(fog_height_density, other.fog_height_density, weight) + result.sky_top_color = sky_top_color.lerp(other.sky_top_color, weight) + result.sky_horizon_color = sky_horizon_color.lerp(other.sky_horizon_color, weight) + result.sky_bottom_color = sky_bottom_color.lerp(other.sky_bottom_color, weight) + result.horizon_blend = lerpf(horizon_blend, other.horizon_blend, weight) + result.stars_visibility = lerpf(stars_visibility, other.stars_visibility, weight) + result.stars_tint = stars_tint.lerp(other.stars_tint, weight) + result.cloud_coverage = lerpf(cloud_coverage, other.cloud_coverage, weight) + result.cloud_opacity = lerpf(cloud_opacity, other.cloud_opacity, weight) + result.cloud_color = cloud_color.lerp(other.cloud_color, weight) + result.cloud_shadow_color = cloud_shadow_color.lerp(other.cloud_shadow_color, weight) + result.sun_glow_intensity = lerpf(sun_glow_intensity, other.sun_glow_intensity, weight) + result.sun_glow_size = lerpf(sun_glow_size, other.sun_glow_size, weight) + result.gradient_intensity = lerpf(gradient_intensity, other.gradient_intensity, weight) + return result + + +func duplicate_config() -> SkyConfig: + var copy := SkyConfig.new() + copy.sun_color = sun_color + copy.sun_intensity = sun_intensity + copy.sun_size = sun_size + copy.moon_color = moon_color + copy.moon_intensity = moon_intensity + copy.moon_size = moon_size + copy.ambient_color = ambient_color + copy.ambient_intensity = ambient_intensity + copy.fog_color = fog_color + copy.fog_density = fog_density + copy.fog_height = fog_height + copy.fog_height_density = fog_height_density + copy.sky_top_color = sky_top_color + copy.sky_horizon_color = sky_horizon_color + copy.sky_bottom_color = sky_bottom_color + copy.horizon_blend = horizon_blend + copy.stars_visibility = stars_visibility + copy.stars_tint = stars_tint + copy.cloud_coverage = cloud_coverage + copy.cloud_opacity = cloud_opacity + copy.cloud_color = cloud_color + copy.cloud_shadow_color = cloud_shadow_color + copy.sun_glow_intensity = sun_glow_intensity + copy.sun_glow_size = sun_glow_size + copy.gradient_intensity = gradient_intensity + return copy diff --git a/dynamic-sky/resources/sky_config.gd.uid b/dynamic-sky/resources/sky_config.gd.uid new file mode 100644 index 0000000..0d3e828 --- /dev/null +++ b/dynamic-sky/resources/sky_config.gd.uid @@ -0,0 +1 @@ +uid://c3iniqgu2l0ek diff --git a/dynamic-sky/resources/sky_preset.gd b/dynamic-sky/resources/sky_preset.gd new file mode 100644 index 0000000..68032eb --- /dev/null +++ b/dynamic-sky/resources/sky_preset.gd @@ -0,0 +1,164 @@ +@tool +class_name SkyPreset +extends Resource + +enum PresetType { + CLEAR_DAY, + SUNSET, + NIGHT, + OVERCAST, + STORM, + DAWN, + CUSTOM +} + +@export var preset_name: String = "Custom" +@export var preset_type: PresetType = PresetType.CUSTOM +@export var config: SkyConfig +@export var skybox_texture: Texture2D +@export var cloud_layer_configs: Array[CloudLayerConfig] +@export var post_process_profile: PostProcessProfile + + +static func create_clear_day() -> SkyPreset: + var preset := SkyPreset.new() + preset.preset_name = "Clear Day" + preset.preset_type = PresetType.CLEAR_DAY + preset.config = SkyConfig.new() + preset.config.sun_color = Color(1.0, 0.95, 0.85) + preset.config.sun_intensity = 1.2 + preset.config.moon_intensity = 0.0 + preset.config.ambient_color = Color(0.5, 0.6, 0.8) + preset.config.ambient_intensity = 0.6 + preset.config.fog_color = Color(0.7, 0.8, 0.95) + preset.config.fog_density = 0.005 + preset.config.sky_top_color = Color(0.2, 0.4, 0.85) + preset.config.sky_horizon_color = Color(0.6, 0.75, 0.95) + preset.config.sky_bottom_color = Color(0.5, 0.6, 0.7) + preset.config.stars_visibility = 0.0 + preset.config.cloud_coverage = 0.2 + preset.config.cloud_opacity = 0.7 + preset.config.sun_glow_intensity = 0.3 + return preset + + +static func create_sunset() -> SkyPreset: + var preset := SkyPreset.new() + preset.preset_name = "Sunset" + preset.preset_type = PresetType.SUNSET + preset.config = SkyConfig.new() + preset.config.sun_color = Color(1.0, 0.5, 0.2) + preset.config.sun_intensity = 0.8 + preset.config.moon_intensity = 0.0 + preset.config.ambient_color = Color(0.8, 0.5, 0.4) + preset.config.ambient_intensity = 0.4 + preset.config.fog_color = Color(0.9, 0.6, 0.4) + preset.config.fog_density = 0.015 + preset.config.sky_top_color = Color(0.3, 0.2, 0.5) + preset.config.sky_horizon_color = Color(1.0, 0.5, 0.3) + preset.config.sky_bottom_color = Color(0.9, 0.4, 0.2) + preset.config.stars_visibility = 0.1 + preset.config.cloud_coverage = 0.3 + preset.config.cloud_opacity = 0.9 + preset.config.cloud_color = Color(1.0, 0.7, 0.5) + preset.config.cloud_shadow_color = Color(0.4, 0.2, 0.3) + preset.config.sun_glow_intensity = 0.8 + preset.config.sun_glow_size = 0.4 + return preset + + +static func create_night() -> SkyPreset: + var preset := SkyPreset.new() + preset.preset_name = "Night" + preset.preset_type = PresetType.NIGHT + preset.config = SkyConfig.new() + preset.config.sun_intensity = 0.0 + preset.config.moon_color = Color(0.7, 0.8, 1.0) + preset.config.moon_intensity = 0.4 + preset.config.ambient_color = Color(0.1, 0.15, 0.25) + preset.config.ambient_intensity = 0.2 + preset.config.fog_color = Color(0.1, 0.12, 0.2) + preset.config.fog_density = 0.02 + preset.config.sky_top_color = Color(0.02, 0.03, 0.08) + preset.config.sky_horizon_color = Color(0.1, 0.12, 0.2) + preset.config.sky_bottom_color = Color(0.05, 0.06, 0.1) + preset.config.stars_visibility = 1.0 + preset.config.cloud_coverage = 0.15 + preset.config.cloud_opacity = 0.5 + preset.config.cloud_color = Color(0.2, 0.22, 0.3) + preset.config.cloud_shadow_color = Color(0.05, 0.05, 0.1) + preset.config.sun_glow_intensity = 0.0 + return preset + + +static func create_overcast() -> SkyPreset: + var preset := SkyPreset.new() + preset.preset_name = "Overcast" + preset.preset_type = PresetType.OVERCAST + preset.config = SkyConfig.new() + preset.config.sun_color = Color(0.9, 0.9, 0.85) + preset.config.sun_intensity = 0.4 + preset.config.moon_intensity = 0.0 + preset.config.ambient_color = Color(0.5, 0.5, 0.55) + preset.config.ambient_intensity = 0.7 + preset.config.fog_color = Color(0.6, 0.62, 0.65) + preset.config.fog_density = 0.03 + preset.config.sky_top_color = Color(0.45, 0.48, 0.55) + preset.config.sky_horizon_color = Color(0.6, 0.62, 0.65) + preset.config.sky_bottom_color = Color(0.5, 0.52, 0.55) + preset.config.stars_visibility = 0.0 + preset.config.cloud_coverage = 0.9 + preset.config.cloud_opacity = 1.0 + preset.config.cloud_color = Color(0.7, 0.72, 0.75) + preset.config.cloud_shadow_color = Color(0.4, 0.42, 0.45) + preset.config.sun_glow_intensity = 0.1 + return preset + + +static func create_storm() -> SkyPreset: + var preset := SkyPreset.new() + preset.preset_name = "Storm" + preset.preset_type = PresetType.STORM + preset.config = SkyConfig.new() + preset.config.sun_color = Color(0.7, 0.7, 0.75) + preset.config.sun_intensity = 0.2 + preset.config.moon_intensity = 0.0 + preset.config.ambient_color = Color(0.35, 0.38, 0.45) + preset.config.ambient_intensity = 0.5 + preset.config.fog_color = Color(0.4, 0.42, 0.5) + preset.config.fog_density = 0.05 + preset.config.sky_top_color = Color(0.25, 0.28, 0.35) + preset.config.sky_horizon_color = Color(0.4, 0.42, 0.48) + preset.config.sky_bottom_color = Color(0.35, 0.38, 0.42) + preset.config.stars_visibility = 0.0 + preset.config.cloud_coverage = 1.0 + preset.config.cloud_opacity = 1.0 + preset.config.cloud_color = Color(0.4, 0.42, 0.5) + preset.config.cloud_shadow_color = Color(0.2, 0.22, 0.28) + preset.config.sun_glow_intensity = 0.0 + return preset + + +static func create_dawn() -> SkyPreset: + var preset := SkyPreset.new() + preset.preset_name = "Dawn" + preset.preset_type = PresetType.DAWN + preset.config = SkyConfig.new() + preset.config.sun_color = Color(1.0, 0.7, 0.5) + preset.config.sun_intensity = 0.5 + preset.config.moon_intensity = 0.1 + preset.config.ambient_color = Color(0.4, 0.35, 0.45) + preset.config.ambient_intensity = 0.35 + preset.config.fog_color = Color(0.6, 0.5, 0.55) + preset.config.fog_density = 0.025 + preset.config.sky_top_color = Color(0.15, 0.2, 0.45) + preset.config.sky_horizon_color = Color(0.9, 0.6, 0.5) + preset.config.sky_bottom_color = Color(0.7, 0.45, 0.4) + preset.config.stars_visibility = 0.3 + preset.config.cloud_coverage = 0.25 + preset.config.cloud_opacity = 0.8 + preset.config.cloud_color = Color(0.95, 0.7, 0.6) + preset.config.cloud_shadow_color = Color(0.3, 0.25, 0.35) + preset.config.sun_glow_intensity = 0.6 + preset.config.sun_glow_size = 0.35 + return preset diff --git a/dynamic-sky/resources/sky_preset.gd.uid b/dynamic-sky/resources/sky_preset.gd.uid new file mode 100644 index 0000000..cbb09a6 --- /dev/null +++ b/dynamic-sky/resources/sky_preset.gd.uid @@ -0,0 +1 @@ +uid://c48eq2y4ikok8 diff --git a/dynamic-sky/resources/weather_profile.gd b/dynamic-sky/resources/weather_profile.gd new file mode 100644 index 0000000..1dbac94 --- /dev/null +++ b/dynamic-sky/resources/weather_profile.gd @@ -0,0 +1,158 @@ +@tool +class_name WeatherProfile +extends Resource + +enum WeatherState { + CLEAR, + CLOUDY, + OVERCAST, + RAIN, + STORM, + SNOW +} + +@export var weather_state: WeatherState = WeatherState.CLEAR +@export var display_name: String = "Clear" + +@export_group("Clouds") +@export var cloud_density: float = 0.2 +@export var cloud_opacity: float = 0.7 +@export var cloud_coverage: float = 0.3 +@export var cloud_color: Color = Color.WHITE +@export var cloud_shadow_color: Color = Color(0.6, 0.6, 0.7) + +@export_group("Wind") +@export var wind_speed: float = 1.0 +@export var wind_direction: Vector2 = Vector2(1.0, 0.0) +@export var wind_turbulence: float = 0.1 + +@export_group("Ambient Adjustments") +@export var ambient_color_multiplier: Color = Color.WHITE +@export var ambient_intensity_multiplier: float = 1.0 + +@export_group("Fog Adjustments") +@export var fog_color_multiplier: Color = Color.WHITE +@export var fog_density_multiplier: float = 1.0 + +@export_group("VFX") +@export var rain_intensity: float = 0.0 +@export var snow_intensity: float = 0.0 +@export var lightning_enabled: bool = false +@export var lightning_frequency: float = 0.1 + +@export_group("Transition") +@export var default_transition_duration: float = 5.0 + + +static func create_clear() -> WeatherProfile: + var profile := WeatherProfile.new() + profile.weather_state = WeatherState.CLEAR + profile.display_name = "Clear" + profile.cloud_density = 0.1 + profile.cloud_opacity = 0.6 + profile.cloud_coverage = 0.2 + profile.wind_speed = 0.5 + return profile + + +static func create_cloudy() -> WeatherProfile: + var profile := WeatherProfile.new() + profile.weather_state = WeatherState.CLOUDY + profile.display_name = "Cloudy" + profile.cloud_density = 0.5 + profile.cloud_opacity = 0.8 + profile.cloud_coverage = 0.5 + profile.wind_speed = 1.0 + profile.ambient_intensity_multiplier = 0.9 + return profile + + +static func create_overcast() -> WeatherProfile: + var profile := WeatherProfile.new() + profile.weather_state = WeatherState.OVERCAST + profile.display_name = "Overcast" + profile.cloud_density = 0.9 + profile.cloud_opacity = 1.0 + profile.cloud_coverage = 0.9 + profile.cloud_color = Color(0.7, 0.72, 0.75) + profile.wind_speed = 1.5 + profile.ambient_intensity_multiplier = 0.7 + profile.fog_density_multiplier = 1.5 + return profile + + +static func create_rain() -> WeatherProfile: + var profile := WeatherProfile.new() + profile.weather_state = WeatherState.RAIN + profile.display_name = "Rain" + profile.cloud_density = 0.85 + profile.cloud_opacity = 0.95 + profile.cloud_coverage = 0.85 + profile.cloud_color = Color(0.55, 0.58, 0.65) + profile.wind_speed = 2.0 + profile.ambient_intensity_multiplier = 0.6 + profile.fog_density_multiplier = 2.0 + profile.rain_intensity = 0.7 + profile.default_transition_duration = 8.0 + return profile + + +static func create_storm() -> WeatherProfile: + var profile := WeatherProfile.new() + profile.weather_state = WeatherState.STORM + profile.display_name = "Storm" + profile.cloud_density = 1.0 + profile.cloud_opacity = 1.0 + profile.cloud_coverage = 1.0 + profile.cloud_color = Color(0.4, 0.42, 0.5) + profile.cloud_shadow_color = Color(0.2, 0.22, 0.28) + profile.wind_speed = 4.0 + profile.wind_turbulence = 0.4 + profile.ambient_intensity_multiplier = 0.4 + profile.fog_density_multiplier = 3.0 + profile.rain_intensity = 1.0 + profile.lightning_enabled = true + profile.lightning_frequency = 0.15 + profile.default_transition_duration = 10.0 + return profile + + +static func create_snow() -> WeatherProfile: + var profile := WeatherProfile.new() + profile.weather_state = WeatherState.SNOW + profile.display_name = "Snow" + profile.cloud_density = 0.8 + profile.cloud_opacity = 0.9 + profile.cloud_coverage = 0.8 + profile.cloud_color = Color(0.85, 0.87, 0.92) + profile.wind_speed = 1.0 + profile.ambient_color_multiplier = Color(0.9, 0.92, 1.0) + profile.ambient_intensity_multiplier = 0.75 + profile.fog_color_multiplier = Color(0.95, 0.97, 1.0) + profile.fog_density_multiplier = 1.8 + profile.snow_intensity = 0.8 + profile.default_transition_duration = 12.0 + return profile + + +func lerp_to(other: WeatherProfile, weight: float) -> WeatherProfile: + var result := WeatherProfile.new() + result.weather_state = weather_state if weight < 0.5 else other.weather_state + result.display_name = display_name if weight < 0.5 else other.display_name + result.cloud_density = lerpf(cloud_density, other.cloud_density, weight) + result.cloud_opacity = lerpf(cloud_opacity, other.cloud_opacity, weight) + result.cloud_coverage = lerpf(cloud_coverage, other.cloud_coverage, weight) + result.cloud_color = cloud_color.lerp(other.cloud_color, weight) + result.cloud_shadow_color = cloud_shadow_color.lerp(other.cloud_shadow_color, weight) + result.wind_speed = lerpf(wind_speed, other.wind_speed, weight) + result.wind_direction = wind_direction.lerp(other.wind_direction, weight).normalized() + result.wind_turbulence = lerpf(wind_turbulence, other.wind_turbulence, weight) + result.ambient_color_multiplier = ambient_color_multiplier.lerp(other.ambient_color_multiplier, weight) + result.ambient_intensity_multiplier = lerpf(ambient_intensity_multiplier, other.ambient_intensity_multiplier, weight) + result.fog_color_multiplier = fog_color_multiplier.lerp(other.fog_color_multiplier, weight) + result.fog_density_multiplier = lerpf(fog_density_multiplier, other.fog_density_multiplier, weight) + result.rain_intensity = lerpf(rain_intensity, other.rain_intensity, weight) + result.snow_intensity = lerpf(snow_intensity, other.snow_intensity, weight) + result.lightning_enabled = lightning_enabled or other.lightning_enabled + result.lightning_frequency = lerpf(lightning_frequency, other.lightning_frequency, weight) + return result diff --git a/dynamic-sky/resources/weather_profile.gd.uid b/dynamic-sky/resources/weather_profile.gd.uid new file mode 100644 index 0000000..ee08019 --- /dev/null +++ b/dynamic-sky/resources/weather_profile.gd.uid @@ -0,0 +1 @@ +uid://d2srir3wsi045 diff --git a/dynamic-sky/scripts/cloud_system_2d.gd b/dynamic-sky/scripts/cloud_system_2d.gd new file mode 100644 index 0000000..83ec9f0 --- /dev/null +++ b/dynamic-sky/scripts/cloud_system_2d.gd @@ -0,0 +1,301 @@ +@tool +class_name CloudSystem2D +extends Node3D + +signal cloud_coverage_changed(coverage: float) + +const MAX_LAYERS := 4 + +@export_group("Cloud Layers") +@export var layer_configs: Array[CloudLayerConfig] = [] +@export var global_coverage: float = 0.5: + set(value): + global_coverage = clampf(value, 0.0, 1.0) + _update_coverage() + cloud_coverage_changed.emit(global_coverage) + +@export_group("Evolution") +@export var evolution_enabled: bool = true +@export var evolution_speed: float = 0.01 +@export var evolution_scale: float = 1.0 +@export var evolution_seed: int = 0 + +@export_group("Wind Override") +@export var use_global_wind: bool = true +@export var global_wind_direction: Vector2 = Vector2(1.0, 0.0) +@export var global_wind_speed: float = 1.0 + +@export_group("Lighting") +@export var sun_direction: Vector3 = Vector3(0.0, 1.0, 0.0) +@export var sun_color: Color = Color.WHITE +@export var sun_intensity: float = 1.0 +@export var moon_direction: Vector3 = Vector3(0.0, -1.0, 0.0) +@export var moon_color: Color = Color(0.7, 0.8, 1.0) +@export var moon_intensity: float = 0.3 + +@export_group("Performance") +@export var update_rate: float = 60.0 + +var _cloud_materials: Array[ShaderMaterial] = [] +var _cloud_meshes: Array[MeshInstance3D] = [] +var _time_accumulator: float = 0.0 +var _evolution_offset: float = 0.0 +var _update_timer: float = 0.0 + + +func _ready() -> void: + if layer_configs.is_empty(): + _initialize_default_layers() + _create_cloud_meshes() + _update_all_layers() + + +func _process(delta: float) -> void: + _update_timer += delta + var update_interval := 1.0 / update_rate + + if _update_timer >= update_interval: + _update_timer = 0.0 + _time_accumulator += delta * update_rate * update_interval + + if evolution_enabled: + _evolution_offset += delta * evolution_speed + + _update_flow(delta) + _update_lighting() + + +func _initialize_default_layers() -> void: + layer_configs.clear() + + var low_layer := CloudLayerConfig.new() + low_layer.layer_type = CloudLayerConfig.LayerType.LOW + low_layer.opacity = 0.9 + low_layer.flow_speed = 0.03 + low_layer.parallax_depth = 0.3 + low_layer.tiling = Vector2(2.0, 2.0) + layer_configs.append(low_layer) + + var mid_layer := CloudLayerConfig.new() + mid_layer.layer_type = CloudLayerConfig.LayerType.MID + mid_layer.opacity = 0.7 + mid_layer.flow_speed = 0.02 + mid_layer.parallax_depth = 0.6 + mid_layer.tiling = Vector2(3.0, 3.0) + layer_configs.append(mid_layer) + + var high_layer := CloudLayerConfig.new() + high_layer.layer_type = CloudLayerConfig.LayerType.HIGH + high_layer.opacity = 0.5 + high_layer.flow_speed = 0.01 + high_layer.parallax_depth = 0.9 + high_layer.tiling = Vector2(4.0, 4.0) + layer_configs.append(high_layer) + + var wisps_layer := CloudLayerConfig.new() + wisps_layer.layer_type = CloudLayerConfig.LayerType.WISPS + wisps_layer.opacity = 0.3 + wisps_layer.flow_speed = 0.015 + wisps_layer.parallax_depth = 1.0 + wisps_layer.tiling = Vector2(6.0, 6.0) + layer_configs.append(wisps_layer) + + +func _create_cloud_meshes() -> void: + for mesh in _cloud_meshes: + if is_instance_valid(mesh): + mesh.queue_free() + _cloud_meshes.clear() + _cloud_materials.clear() + + for i in layer_configs.size(): + var config := layer_configs[i] + if not config.enabled: + continue + + var mesh_instance := MeshInstance3D.new() + mesh_instance.name = "CloudLayer_%d" % i + + var quad := QuadMesh.new() + quad.size = Vector2(1000.0, 1000.0) + quad.orientation = PlaneMesh.FACE_Y + mesh_instance.mesh = quad + + var material := _create_cloud_material(config) + mesh_instance.material_override = material + + var height := config.get_layer_height() * 100.0 + 50.0 + mesh_instance.position.y = height + + add_child(mesh_instance) + _cloud_meshes.append(mesh_instance) + _cloud_materials.append(material) + + +func _create_cloud_material(config: CloudLayerConfig) -> ShaderMaterial: + var material := ShaderMaterial.new() + material.shader = _get_cloud_shader() + + material.set_shader_parameter("tiling", config.tiling) + material.set_shader_parameter("opacity", config.opacity * global_coverage) + material.set_shader_parameter("color_tint", config.color_tint) + material.set_shader_parameter("shadow_color", config.shadow_color) + material.set_shader_parameter("flow_direction", config.flow_direction) + material.set_shader_parameter("flow_speed", config.flow_speed) + material.set_shader_parameter("light_influence", config.light_influence) + material.set_shader_parameter("rim_glow_intensity", config.rim_glow_intensity) + + if config.texture: + material.set_shader_parameter("cloud_texture", config.texture) + + return material + + +func _get_cloud_shader() -> Shader: + var shader := Shader.new() + shader.code = """ +shader_type spatial; +render_mode unshaded, depth_draw_never, cull_disabled; + +uniform sampler2D cloud_texture : source_color, filter_linear_mipmap, repeat_enable; +uniform vec2 tiling = vec2(4.0, 4.0); +uniform float opacity = 0.8; +uniform vec4 color_tint : source_color = vec4(1.0); +uniform vec4 shadow_color : source_color = vec4(0.6, 0.6, 0.7, 1.0); +uniform vec2 flow_direction = vec2(1.0, 0.0); +uniform float flow_speed = 0.02; +uniform float flow_offset = 0.0; +uniform vec3 light_direction = vec3(0.0, 1.0, 0.0); +uniform vec4 light_color : source_color = vec4(1.0); +uniform float light_intensity = 1.0; +uniform float light_influence = 1.0; +uniform float rim_glow_intensity = 0.3; +uniform float evolution_offset = 0.0; +uniform float coverage = 1.0; + +varying vec2 world_uv; + +void vertex() { + world_uv = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xz; +} + +void fragment() { + vec2 uv = world_uv * tiling * 0.001; + uv += flow_direction * flow_offset; + + vec2 distorted_uv = uv + vec2(sin(uv.y * 10.0 + evolution_offset) * 0.01, cos(uv.x * 10.0 + evolution_offset) * 0.01); + + vec4 cloud_sample = texture(cloud_texture, distorted_uv); + float cloud_alpha = cloud_sample.r * opacity * coverage; + + float light_dot = dot(normalize(light_direction), vec3(0.0, 1.0, 0.0)); + float light_factor = mix(0.5, 1.0, (light_dot + 1.0) * 0.5) * light_influence; + + vec3 lit_color = mix(shadow_color.rgb, color_tint.rgb, light_factor); + lit_color *= light_color.rgb * light_intensity; + + float rim = pow(1.0 - cloud_sample.r, 2.0) * rim_glow_intensity; + lit_color += light_color.rgb * rim; + + ALBEDO = lit_color; + ALPHA = cloud_alpha; +} +""" + return shader + + +func _update_flow(delta: float) -> void: + for i in _cloud_materials.size(): + if i >= layer_configs.size(): + continue + + var config := layer_configs[i] + var material := _cloud_materials[i] + + var wind_dir := global_wind_direction if use_global_wind else config.flow_direction + var wind_spd := global_wind_speed if use_global_wind else 1.0 + #var flow_offset: float = material.get_shader_parameter("flow_offset") + #flow_offset += config.flow_speed * wind_spd * delta + # + #material.set_shader_parameter("flow_offset", flow_offset) + #material.set_shader_parameter("flow_direction", wind_dir) + #smaterial.set_shader_parameter("evolution_offset", _evolution_offset) + + +func _update_lighting() -> void: + for material in _cloud_materials: + material.set_shader_parameter("light_direction", sun_direction) + material.set_shader_parameter("light_color", sun_color) + material.set_shader_parameter("light_intensity", sun_intensity) + + +func _update_coverage() -> void: + for i in _cloud_materials.size(): + if i >= layer_configs.size(): + continue + var config := layer_configs[i] + _cloud_materials[i].set_shader_parameter("coverage", global_coverage) + _cloud_materials[i].set_shader_parameter("opacity", config.opacity) + + +func _update_all_layers() -> void: + for i in _cloud_materials.size(): + if i >= layer_configs.size(): + continue + var config := layer_configs[i] + var material := _cloud_materials[i] + + material.set_shader_parameter("tiling", config.tiling) + material.set_shader_parameter("opacity", config.opacity) + material.set_shader_parameter("color_tint", config.color_tint) + material.set_shader_parameter("shadow_color", config.shadow_color) + material.set_shader_parameter("flow_direction", config.flow_direction) + material.set_shader_parameter("flow_speed", config.flow_speed) + material.set_shader_parameter("light_influence", config.light_influence) + material.set_shader_parameter("rim_glow_intensity", config.rim_glow_intensity) + material.set_shader_parameter("coverage", global_coverage) + + +func set_layer_enabled(layer_index: int, enabled: bool) -> void: + if layer_index < 0 or layer_index >= _cloud_meshes.size(): + return + _cloud_meshes[layer_index].visible = enabled + + +func set_layer_opacity(layer_index: int, opacity: float) -> void: + if layer_index < 0 or layer_index >= _cloud_materials.size(): + return + if layer_index < layer_configs.size(): + layer_configs[layer_index].opacity = opacity + _cloud_materials[layer_index].set_shader_parameter("opacity", opacity) + + +func set_layer_color(layer_index: int, color: Color) -> void: + if layer_index < 0 or layer_index >= _cloud_materials.size(): + return + if layer_index < layer_configs.size(): + layer_configs[layer_index].color_tint = color + _cloud_materials[layer_index].set_shader_parameter("color_tint", color) + + +func set_global_wind(direction: Vector2, speed: float) -> void: + global_wind_direction = direction.normalized() + global_wind_speed = speed + + +func set_sun_lighting(direction: Vector3, color: Color, intensity: float) -> void: + sun_direction = direction.normalized() + sun_color = color + sun_intensity = intensity + _update_lighting() + + +func set_moon_lighting(direction: Vector3, color: Color, intensity: float) -> void: + moon_direction = direction.normalized() + moon_color = color + moon_intensity = intensity + + +func rebuild_layers() -> void: + _create_cloud_meshes() + _update_all_layers() diff --git a/dynamic-sky/scripts/cloud_system_2d.gd.uid b/dynamic-sky/scripts/cloud_system_2d.gd.uid new file mode 100644 index 0000000..94fd8f7 --- /dev/null +++ b/dynamic-sky/scripts/cloud_system_2d.gd.uid @@ -0,0 +1 @@ +uid://cwplvsarljsf diff --git a/dynamic-sky/scripts/day_night_controller.gd b/dynamic-sky/scripts/day_night_controller.gd new file mode 100644 index 0000000..466b8b3 --- /dev/null +++ b/dynamic-sky/scripts/day_night_controller.gd @@ -0,0 +1,313 @@ +@tool +class_name DayNightController +extends Node + +signal time_changed(time_of_day: float) +signal hour_changed(hour: int) +signal day_changed(day: int) +signal sunrise_started() +signal sunset_started() +signal night_started() +signal noon_reached() + +const HOURS_PER_DAY := 24.0 +const SUNRISE_START := 0.2 # ~5 AM +const SUNRISE_END := 0.3 # ~7 AM +const SUNSET_START := 0.75 # ~6 PM +const SUNSET_END := 0.85 # ~8 PM +const NOON := 0.5 + +@export_group("Time Settings") +@export var time_of_day: float = 0.25: + set(value): + var old_hour := get_current_hour() + time_of_day = wrapf(value, 0.0, 1.0) + var new_hour := get_current_hour() + if old_hour != new_hour: + hour_changed.emit(new_hour) + time_changed.emit(time_of_day) + _check_time_events(old_hour, new_hour) + +@export var day_length_minutes: float = 30.0: + set(value): + day_length_minutes = clampf(value, 1.0, 1440.0) + +@export var paused: bool = false +@export var time_scale: float = 1.0 +@export var current_day: int = 1 + +@export_group("Sun Arc") +@export var sun_arc_tilt: float = 23.5 +@export var sun_arc_offset: float = 0.0 + +@export_group("Moon Arc") +@export var moon_arc_tilt: float = 15.0 +@export var moon_arc_offset: float = 180.0 +@export var moon_phase: float = 0.0 + +@export_group("Sun Curves") +@export var sun_color_curve: Gradient +@export var sun_intensity_curve: Curve + +@export_group("Moon Curves") +@export var moon_color_curve: Gradient +@export var moon_intensity_curve: Curve + +@export_group("Ambient Curves") +@export var ambient_color_curve: Gradient +@export var ambient_intensity_curve: Curve + +@export_group("Fog Curves") +@export var fog_color_curve: Gradient +@export var fog_density_curve: Curve + +@export_group("Stars") +@export var stars_visibility_curve: Curve + +var _last_hour: int = -1 +var _sunrise_emitted := false +var _sunset_emitted := false +var _night_emitted := false +var _noon_emitted := false + + +func _ready() -> void: + _initialize_default_curves() + _last_hour = get_current_hour() + + +func _process(delta: float) -> void: + if paused: + return + + var time_increment := delta / (day_length_minutes * 60.0) * time_scale + var old_time := time_of_day + time_of_day += time_increment + + if time_of_day < old_time: + current_day += 1 + day_changed.emit(current_day) + _reset_time_events() + + +func _initialize_default_curves() -> void: + if sun_color_curve == null: + sun_color_curve = Gradient.new() + sun_color_curve.set_color(0, Color(1.0, 0.6, 0.4)) # Dawn + sun_color_curve.add_point(0.25, Color(1.0, 0.95, 0.85)) # Morning + sun_color_curve.add_point(0.5, Color(1.0, 1.0, 0.95)) # Noon + sun_color_curve.add_point(0.75, Color(1.0, 0.7, 0.4)) # Sunset + sun_color_curve.set_color(1, Color(1.0, 0.5, 0.3)) # Dusk + + if sun_intensity_curve == null: + sun_intensity_curve = Curve.new() + sun_intensity_curve.add_point(Vector2(0.0, 0.0)) + sun_intensity_curve.add_point(Vector2(0.2, 0.0)) + sun_intensity_curve.add_point(Vector2(0.3, 0.8)) + sun_intensity_curve.add_point(Vector2(0.5, 1.0)) + sun_intensity_curve.add_point(Vector2(0.7, 0.8)) + sun_intensity_curve.add_point(Vector2(0.85, 0.0)) + sun_intensity_curve.add_point(Vector2(1.0, 0.0)) + + if moon_color_curve == null: + moon_color_curve = Gradient.new() + moon_color_curve.set_color(0, Color(0.7, 0.8, 1.0)) + moon_color_curve.set_color(1, Color(0.7, 0.8, 1.0)) + + if moon_intensity_curve == null: + moon_intensity_curve = Curve.new() + moon_intensity_curve.add_point(Vector2(0.0, 0.4)) + moon_intensity_curve.add_point(Vector2(0.2, 0.3)) + moon_intensity_curve.add_point(Vector2(0.25, 0.0)) + moon_intensity_curve.add_point(Vector2(0.75, 0.0)) + moon_intensity_curve.add_point(Vector2(0.85, 0.3)) + moon_intensity_curve.add_point(Vector2(1.0, 0.4)) + + if ambient_color_curve == null: + ambient_color_curve = Gradient.new() + ambient_color_curve.set_color(0, Color(0.15, 0.18, 0.3)) # Night + ambient_color_curve.add_point(0.25, Color(0.5, 0.55, 0.65)) # Morning + ambient_color_curve.add_point(0.5, Color(0.6, 0.65, 0.75)) # Noon + ambient_color_curve.add_point(0.75, Color(0.55, 0.5, 0.55)) # Evening + ambient_color_curve.set_color(1, Color(0.15, 0.18, 0.3)) # Night + + if ambient_intensity_curve == null: + ambient_intensity_curve = Curve.new() + ambient_intensity_curve.add_point(Vector2(0.0, 0.2)) + ambient_intensity_curve.add_point(Vector2(0.25, 0.5)) + ambient_intensity_curve.add_point(Vector2(0.5, 0.7)) + ambient_intensity_curve.add_point(Vector2(0.75, 0.5)) + ambient_intensity_curve.add_point(Vector2(1.0, 0.2)) + + if fog_color_curve == null: + fog_color_curve = Gradient.new() + fog_color_curve.set_color(0, Color(0.1, 0.12, 0.2)) + fog_color_curve.add_point(0.25, Color(0.6, 0.65, 0.75)) + fog_color_curve.add_point(0.5, Color(0.7, 0.75, 0.85)) + fog_color_curve.add_point(0.75, Color(0.7, 0.55, 0.5)) + fog_color_curve.set_color(1, Color(0.1, 0.12, 0.2)) + + if fog_density_curve == null: + fog_density_curve = Curve.new() + fog_density_curve.add_point(Vector2(0.0, 0.02)) + fog_density_curve.add_point(Vector2(0.25, 0.015)) + fog_density_curve.add_point(Vector2(0.5, 0.01)) + fog_density_curve.add_point(Vector2(0.75, 0.015)) + fog_density_curve.add_point(Vector2(1.0, 0.02)) + + if stars_visibility_curve == null: + stars_visibility_curve = Curve.new() + stars_visibility_curve.add_point(Vector2(0.0, 1.0)) + stars_visibility_curve.add_point(Vector2(0.2, 0.8)) + stars_visibility_curve.add_point(Vector2(0.3, 0.0)) + stars_visibility_curve.add_point(Vector2(0.7, 0.0)) + stars_visibility_curve.add_point(Vector2(0.85, 0.8)) + stars_visibility_curve.add_point(Vector2(1.0, 1.0)) + + +func _check_time_events(old_hour: int, new_hour: int) -> void: + if time_of_day >= SUNRISE_START and time_of_day < SUNRISE_END and not _sunrise_emitted: + _sunrise_emitted = true + sunrise_started.emit() + + if time_of_day >= SUNSET_START and time_of_day < SUNSET_END and not _sunset_emitted: + _sunset_emitted = true + sunset_started.emit() + + if time_of_day >= SUNSET_END and not _night_emitted: + _night_emitted = true + night_started.emit() + + if time_of_day >= NOON - 0.01 and time_of_day < NOON + 0.01 and not _noon_emitted: + _noon_emitted = true + noon_reached.emit() + + +func _reset_time_events() -> void: + _sunrise_emitted = false + _sunset_emitted = false + _night_emitted = false + _noon_emitted = false + + +func get_current_hour() -> int: + return int(time_of_day * HOURS_PER_DAY) + + +func get_current_hour_float() -> float: + return time_of_day * HOURS_PER_DAY + + +func set_time_from_hours(hours: float) -> void: + time_of_day = fmod(hours, HOURS_PER_DAY) / HOURS_PER_DAY + + +func get_sun_direction() -> Vector3: + var angle := (time_of_day - 0.25) * TAU + var tilt_rad := deg_to_rad(sun_arc_tilt) + var offset_rad := deg_to_rad(sun_arc_offset) + + var direction := Vector3( + cos(angle + offset_rad), + sin(angle), + sin(angle) * sin(tilt_rad) + ).normalized() + + return direction + + +func get_moon_direction() -> Vector3: + var angle := (time_of_day + 0.25) * TAU + var tilt_rad := deg_to_rad(moon_arc_tilt) + var offset_rad := deg_to_rad(moon_arc_offset) + + var direction := Vector3( + cos(angle + offset_rad), + sin(angle), + sin(angle) * sin(tilt_rad) + ).normalized() + + return direction + + +func get_sun_color() -> Color: + if sun_color_curve: + return sun_color_curve.sample(time_of_day) + return Color.WHITE + + +func get_sun_intensity() -> float: + if sun_intensity_curve: + return sun_intensity_curve.sample(time_of_day) + return 1.0 + + +func get_moon_color() -> Color: + if moon_color_curve: + return moon_color_curve.sample(time_of_day) + return Color(0.7, 0.8, 1.0) + + +func get_moon_intensity() -> float: + var base_intensity := 1.0 + if moon_intensity_curve: + base_intensity = moon_intensity_curve.sample(time_of_day) + var phase_factor := 0.5 + 0.5 * cos(moon_phase * TAU) + return base_intensity * phase_factor + + +func get_ambient_color() -> Color: + if ambient_color_curve: + return ambient_color_curve.sample(time_of_day) + return Color(0.5, 0.55, 0.65) + + +func get_ambient_intensity() -> float: + if ambient_intensity_curve: + return ambient_intensity_curve.sample(time_of_day) + return 0.5 + + +func get_fog_color() -> Color: + if fog_color_curve: + return fog_color_curve.sample(time_of_day) + return Color(0.7, 0.75, 0.85) + + +func get_fog_density() -> float: + if fog_density_curve: + return fog_density_curve.sample(time_of_day) + return 0.01 + + +func get_stars_visibility() -> float: + if stars_visibility_curve: + return stars_visibility_curve.sample(time_of_day) + return 0.0 + + +func is_daytime() -> bool: + return time_of_day >= SUNRISE_END and time_of_day < SUNSET_START + + +func is_nighttime() -> bool: + return time_of_day < SUNRISE_START or time_of_day >= SUNSET_END + + +func is_dawn() -> bool: + return time_of_day >= SUNRISE_START and time_of_day < SUNRISE_END + + +func is_dusk() -> bool: + return time_of_day >= SUNSET_START and time_of_day < SUNSET_END + + +func get_time_period() -> String: + if is_dawn(): + return "dawn" + elif is_dusk(): + return "dusk" + elif is_daytime(): + return "day" + else: + return "night" diff --git a/dynamic-sky/scripts/day_night_controller.gd.uid b/dynamic-sky/scripts/day_night_controller.gd.uid new file mode 100644 index 0000000..11270da --- /dev/null +++ b/dynamic-sky/scripts/day_night_controller.gd.uid @@ -0,0 +1 @@ +uid://gypnoh5ikwjn diff --git a/dynamic-sky/scripts/dynamic_sky_root.gd b/dynamic-sky/scripts/dynamic_sky_root.gd new file mode 100644 index 0000000..5dff677 --- /dev/null +++ b/dynamic-sky/scripts/dynamic_sky_root.gd @@ -0,0 +1,399 @@ +@tool +class_name DynamicSkyRoot +extends Node3D + +signal time_of_day_changed(time: float) +signal weather_changed(state: WeatherProfile.WeatherState) +signal preset_changed(preset: SkyPreset) + +@export_group("Controllers") +@export var preset_library: SkyboxPresetLibrary +@export var day_night_controller: DayNightController +@export var weather_controller: WeatherController +@export var cloud_system: CloudSystem2D +@export var post_process_controller: PostProcessController +@export var vfx_controller: VFXController +@export var shadow_proxy_system: ShadowProxySystem + +@export_group("Environment") +@export var world_environment: WorldEnvironment +@export var sun_light: DirectionalLight3D +@export var moon_light: DirectionalLight3D + +@export_group("Global Settings") +@export var auto_create_controllers: bool = true +@export var deterministic_seed: int = 0: + set(value): + deterministic_seed = value + _apply_seed() + +@export_group("Debug") +@export var show_debug_panel: bool = false +@export var debug_time_override: float = -1.0 + +var _current_config: SkyConfig +var preset_name: String = "" +var _updating_preset_name: bool = false + + +func _ready() -> void: + if auto_create_controllers: + _ensure_controllers_exist() + + _connect_signals() + property_list_changed_notify() + _sync_preset_name() + _apply_seed() + _sync_all_systems() + + +func _process(delta: float) -> void: + _sync_all_systems() + + if show_debug_panel: + _update_debug_info() + + +func _ensure_controllers_exist() -> void: + if preset_library == null: + preset_library = SkyboxPresetLibrary.new() + preset_library.name = "PresetLibrary" + add_child(preset_library) + + if day_night_controller == null: + day_night_controller = DayNightController.new() + day_night_controller.name = "DayNightController" + add_child(day_night_controller) + + if weather_controller == null: + weather_controller = WeatherController.new() + weather_controller.name = "WeatherController" + add_child(weather_controller) + + if cloud_system == null: + cloud_system = CloudSystem2D.new() + cloud_system.name = "CloudSystem" + add_child(cloud_system) + + if post_process_controller == null: + post_process_controller = PostProcessController.new() + post_process_controller.name = "PostProcessController" + add_child(post_process_controller) + + if vfx_controller == null: + vfx_controller = VFXController.new() + vfx_controller.name = "VFXController" + add_child(vfx_controller) + + if shadow_proxy_system == null: + shadow_proxy_system = ShadowProxySystem.new() + shadow_proxy_system.name = "ShadowProxySystem" + add_child(shadow_proxy_system) + + if world_environment == null: + _create_default_environment() + + if sun_light == null: + _create_sun_light() + + if moon_light == null: + _create_moon_light() + + +func _create_default_environment() -> void: + world_environment = WorldEnvironment.new() + world_environment.name = "WorldEnvironment" + world_environment.environment = Environment.new() + world_environment.environment.background_mode = Environment.BG_SKY + world_environment.environment.sky = Sky.new() + add_child(world_environment) + + if post_process_controller: + post_process_controller.target_environment = world_environment.environment + + +func _create_sun_light() -> void: + sun_light = DirectionalLight3D.new() + sun_light.name = "SunLight" + sun_light.light_color = Color(1.0, 0.95, 0.85) + sun_light.light_energy = 1.0 + sun_light.shadow_enabled = true + add_child(sun_light) + + +func _create_moon_light() -> void: + moon_light = DirectionalLight3D.new() + moon_light.name = "MoonLight" + moon_light.light_color = Color(0.7, 0.8, 1.0) + moon_light.light_energy = 0.3 + moon_light.shadow_enabled = false + add_child(moon_light) + + +func _connect_signals() -> void: + if day_night_controller: + day_night_controller.time_changed.connect(_on_time_changed) + + if weather_controller: + weather_controller.weather_state_changed.connect(_on_weather_state_changed) + + if preset_library: + preset_library.preset_changed.connect(_on_preset_changed) + + +func _on_time_changed(time: float) -> void: + time_of_day_changed.emit(time) + + if vfx_controller: + vfx_controller.set_night_mode(day_night_controller.is_nighttime()) + + +func _on_weather_state_changed(old_state: WeatherProfile.WeatherState, new_state: WeatherProfile.WeatherState) -> void: + weather_changed.emit(new_state) + + +func _on_preset_changed(preset: SkyPreset) -> void: + preset_changed.emit(preset) + if preset.config: + _current_config = preset.config + _sync_preset_name() + + _apply_preset_assets(preset) + + +func _apply_seed() -> void: + if deterministic_seed != 0: + seed(deterministic_seed) + + +func _sync_all_systems() -> void: + if day_night_controller == null: + return + + var time := day_night_controller.time_of_day + if debug_time_override >= 0: + time = debug_time_override + + _sync_sun_moon() + _sync_clouds() + _sync_post_process() + _sync_shadows() + + +func _sync_sun_moon() -> void: + if day_night_controller == null: + return + + var sun_dir := day_night_controller.get_sun_direction() + var moon_dir := day_night_controller.get_moon_direction() + + if sun_light: + sun_light.look_at(global_position - sun_dir, Vector3.UP) + sun_light.light_color = day_night_controller.get_sun_color() + sun_light.light_energy = day_night_controller.get_sun_intensity() + sun_light.visible = day_night_controller.get_sun_intensity() > 0.01 + + if moon_light: + moon_light.look_at(global_position - moon_dir, Vector3.UP) + moon_light.light_color = day_night_controller.get_moon_color() + moon_light.light_energy = day_night_controller.get_moon_intensity() + moon_light.visible = day_night_controller.get_moon_intensity() > 0.01 + + if world_environment and world_environment.environment: + world_environment.environment.ambient_light_color = day_night_controller.get_ambient_color() + world_environment.environment.ambient_light_energy = day_night_controller.get_ambient_intensity() + + +func _sync_clouds() -> void: + if cloud_system == null: + return + + if day_night_controller: + cloud_system.set_sun_lighting( + day_night_controller.get_sun_direction(), + day_night_controller.get_sun_color(), + day_night_controller.get_sun_intensity() + ) + cloud_system.set_moon_lighting( + day_night_controller.get_moon_direction(), + day_night_controller.get_moon_color(), + day_night_controller.get_moon_intensity() + ) + + if weather_controller: + cloud_system.global_coverage = weather_controller.get_cloud_coverage() + cloud_system.set_global_wind( + weather_controller.get_wind_direction(), + weather_controller.get_wind_speed() + ) + + +func _sync_post_process() -> void: + if post_process_controller == null or day_night_controller == null: + return + + var sun_dir := day_night_controller.get_sun_direction() + var sun_color := day_night_controller.get_sun_color() + + post_process_controller.set_sun_glow( + sun_dir, + sun_color, + day_night_controller.get_sun_intensity() * 0.5, + 0.2 + ) + + post_process_controller.set_fog_settings( + day_night_controller.get_fog_color(), + day_night_controller.get_fog_density() + ) + + var dawn_dusk_blend := 0.0 + if day_night_controller.is_dawn() or day_night_controller.is_dusk(): + dawn_dusk_blend = 1.0 + post_process_controller.set_dawn_dusk_blend(dawn_dusk_blend) + + post_process_controller.set_stars_visibility( + day_night_controller.get_stars_visibility() + ) + + if _current_config: + post_process_controller.set_sky_colors( + _current_config.sky_top_color, + _current_config.sky_horizon_color, + _current_config.sky_bottom_color + ) + post_process_controller.gradient_intensity = _current_config.gradient_intensity + post_process_controller.apply_config(_current_config) + + +func _apply_preset_assets(preset: SkyPreset) -> void: + if preset == null: + return + + if post_process_controller: + post_process_controller.skybox_texture = preset.skybox_texture + if preset.post_process_profile: + post_process_controller.profile = preset.post_process_profile + + if cloud_system and preset.cloud_layer_configs.size() > 0: + cloud_system.layer_configs = preset.cloud_layer_configs + cloud_system.rebuild_layers() + + +func _get_property_list() -> Array: + var properties := [] + var names: PackedStringArray = [] + if preset_library: + names = preset_library.get_preset_names() + + var hint_string := "" + if names.size() > 0: + hint_string = ",".join(names) + + properties.append({ + "name": "preset_name", + "type": TYPE_STRING, + "usage": PROPERTY_USAGE_EDITOR, + "hint": PROPERTY_HINT_ENUM, + "hint_string": hint_string, + }) + return properties + + +func _set(property: StringName, value: Variant) -> bool: + if property == "preset_name": + if preset_name == value: + return true + preset_name = value + if _updating_preset_name: + return true + if preset_library and String(preset_name) != "": + preset_library.apply_preset(String(preset_name)) + return true + return false + + +func _get(property: StringName) -> Variant: + if property == "preset_name": + return preset_name + return null + + +func _sync_preset_name() -> void: + if preset_library == null: + return + var current := preset_library.get_current_preset() + if current == null: + return + _updating_preset_name = true + preset_name = current.preset_name + _updating_preset_name = false + + +func _sync_shadows() -> void: + if shadow_proxy_system == null: + return + + if weather_controller: + shadow_proxy_system.set_cloud_coverage(weather_controller.get_cloud_coverage()) + + if cloud_system: + shadow_proxy_system.sync_with_cloud_system(cloud_system) + + +func _update_debug_info() -> void: + pass + + +func set_time_of_day(time: float) -> void: + if day_night_controller: + day_night_controller.time_of_day = time + + +func set_weather(state: WeatherProfile.WeatherState, transition_time: float = -1.0) -> void: + if weather_controller: + weather_controller.set_weather(state, transition_time) + + +func apply_preset(preset_name: String) -> void: + if preset_library: + preset_library.apply_preset(preset_name) + + +func pause_time() -> void: + if day_night_controller: + day_night_controller.paused = true + + +func resume_time() -> void: + if day_night_controller: + day_night_controller.paused = false + + +func trigger_meteor_shower(duration: float = 60.0) -> void: + if vfx_controller: + vfx_controller.start_meteor_shower(duration) + + +func get_time_of_day() -> float: + if day_night_controller: + return day_night_controller.time_of_day + return 0.0 + + +func get_current_weather() -> WeatherProfile.WeatherState: + if weather_controller: + return weather_controller.current_weather + return WeatherProfile.WeatherState.CLEAR + + +func is_daytime() -> bool: + if day_night_controller: + return day_night_controller.is_daytime() + return true + + +func is_nighttime() -> bool: + if day_night_controller: + return day_night_controller.is_nighttime() + return false diff --git a/dynamic-sky/scripts/dynamic_sky_root.gd.uid b/dynamic-sky/scripts/dynamic_sky_root.gd.uid new file mode 100644 index 0000000..a4b3930 --- /dev/null +++ b/dynamic-sky/scripts/dynamic_sky_root.gd.uid @@ -0,0 +1 @@ +uid://8r3do6ow5nmn diff --git a/dynamic-sky/scripts/post_process_controller.gd b/dynamic-sky/scripts/post_process_controller.gd new file mode 100644 index 0000000..8fa5275 --- /dev/null +++ b/dynamic-sky/scripts/post_process_controller.gd @@ -0,0 +1,310 @@ +@tool +class_name PostProcessController +extends Node + +@export_group("Profile") +@export var profile: PostProcessProfile: + set(value): + profile = value + _apply_profile() + +@export_group("Environment") +@export var target_environment: Environment + +@export_group("Sky Gradient") +@export var sky_top_color: Color = Color(0.2, 0.4, 0.8) +@export var sky_horizon_color: Color = Color(0.6, 0.7, 0.9) +@export var sky_bottom_color: Color = Color(0.4, 0.5, 0.6) +@export var gradient_intensity: float = 1.0 + +@export_group("Skybox") +@export var skybox_texture: Texture2D: + set(value): + skybox_texture = value + _update_sky_material() +@export var skybox_blend: float = 1.0: + set(value): + skybox_blend = clampf(value, 0.0, 1.0) + _update_sky_material() + +@export_group("Fog Shaping") +@export var fog_enabled: bool = true +@export var fog_color: Color = Color(0.7, 0.8, 0.9) +@export var fog_density: float = 0.01 +@export var height_fog_enabled: bool = true +@export var height_fog_min: float = 0.0 +@export var height_fog_max: float = 100.0 +@export var height_fog_curve: float = 1.0 + +@export_group("Sun Glow") +@export var sun_glow_enabled: bool = true +@export var sun_glow_intensity: float = 0.5 +@export var sun_glow_size: float = 0.2 +@export var sun_glow_falloff: float = 2.0 +@export var sun_glow_color: Color = Color(1.0, 0.95, 0.8) +@export var sun_direction: Vector3 = Vector3(0.0, 1.0, 0.0) + +@export_group("Dawn/Dusk Enhancement") +@export var dawn_dusk_enabled: bool = true +@export var dawn_dusk_intensity: float = 0.5 +@export var dawn_dusk_blend: float = 0.0 + +@export_group("Performance") +@export var effects_enabled: bool = true + +var _sky_material: ShaderMaterial +var _post_process_quad: MeshInstance3D + + +func _ready() -> void: + if profile == null: + profile = PostProcessProfile.new() + _apply_profile() + _setup_environment() + + +func _apply_profile() -> void: + if profile == null: + return + + gradient_intensity = profile.gradient_intensity + fog_enabled = profile.fog_enabled + height_fog_enabled = profile.height_fog_enabled + height_fog_min = profile.height_fog_start + height_fog_max = profile.height_fog_end + height_fog_curve = profile.height_fog_curve + sun_glow_enabled = profile.sun_glow_enabled + sun_glow_intensity = profile.sun_glow_intensity + sun_glow_size = profile.sun_glow_size + sun_glow_falloff = profile.sun_glow_falloff + sun_glow_color = profile.sun_glow_color + dawn_dusk_enabled = profile.dawn_dusk_enabled + dawn_dusk_intensity = profile.dawn_dusk_intensity + + +func _setup_environment() -> void: + if target_environment == null: + return + + _update_fog() + _update_sky() + + +func _update_fog() -> void: + if target_environment == null: + return + + target_environment.fog_enabled = fog_enabled and effects_enabled + + if fog_enabled: + target_environment.fog_light_color = fog_color + target_environment.fog_density = fog_density + + if height_fog_enabled: + target_environment.fog_height = height_fog_min + target_environment.fog_height_density = height_fog_curve * 0.1 + + +func _update_sky() -> void: + if target_environment == null: + return + + if target_environment.sky == null: + target_environment.sky = Sky.new() + + if target_environment.sky.sky_material == null or not target_environment.sky.sky_material is ShaderMaterial: + _create_sky_material() + + _sky_material = target_environment.sky.sky_material as ShaderMaterial + if _sky_material: + _update_sky_material() + + +func _create_sky_material() -> void: + _sky_material = ShaderMaterial.new() + _sky_material.shader = _get_sky_shader() + + if target_environment and target_environment.sky: + target_environment.sky.sky_material = _sky_material + + +func _get_sky_shader() -> Shader: + var shader := Shader.new() + shader.code = """ +shader_type sky; + +uniform sampler2D skybox_texture : source_color, filter_linear_mipmap, repeat_enable; +uniform bool skybox_enabled = false; +uniform float skybox_blend = 1.0; + +uniform vec4 sky_top_color : source_color = vec4(0.2, 0.4, 0.8, 1.0); +uniform vec4 sky_horizon_color : source_color = vec4(0.6, 0.7, 0.9, 1.0); +uniform vec4 sky_bottom_color : source_color = vec4(0.4, 0.5, 0.6, 1.0); +uniform float gradient_intensity = 1.0; +uniform float horizon_blend = 0.5; + +uniform bool sun_glow_enabled = true; +uniform vec3 sun_direction = vec3(0.0, 1.0, 0.0); +uniform vec4 sun_glow_color : source_color = vec4(1.0, 0.95, 0.8, 1.0); +uniform float sun_glow_intensity = 0.5; +uniform float sun_glow_size = 0.2; +uniform float sun_glow_falloff = 2.0; + +uniform bool dawn_dusk_enabled = true; +uniform vec4 dawn_dusk_color : source_color = vec4(1.0, 0.6, 0.4, 1.0); +uniform float dawn_dusk_intensity = 0.5; +uniform float dawn_dusk_blend = 0.0; + +uniform float stars_visibility = 0.0; +uniform vec4 stars_tint : source_color = vec4(1.0); + +float hash(vec2 p) { + return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); +} + +float stars(vec3 dir) { + vec2 uv = dir.xz / (abs(dir.y) + 0.001) * 50.0; + vec2 cell = floor(uv); + vec2 local = fract(uv) - 0.5; + + float star = 0.0; + float h = hash(cell); + if (h > 0.97) { + float dist = length(local); + star = smoothstep(0.1, 0.0, dist) * (h - 0.97) / 0.03; + star *= step(0.0, dir.y); + } + return star; +} + +vec2 pano_uv(vec3 dir) { + dir = normalize(dir); + float u = atan(dir.x, dir.z) / (2.0 * PI) + 0.5; + float v = acos(clamp(dir.y, -1.0, 1.0)) / PI; + return vec2(u, v); +} + +void sky() { + vec3 dir = EYEDIR; + float y = dir.y; + + vec3 color; + if (y > 0.0) { + float t = pow(y, horizon_blend); + color = mix(sky_horizon_color.rgb, sky_top_color.rgb, t); + } else { + float t = pow(-y, horizon_blend); + color = mix(sky_horizon_color.rgb, sky_bottom_color.rgb, t); + } + + color = mix(vec3(0.5), color, gradient_intensity); + + if (skybox_enabled) { + vec3 pano_color = texture(skybox_texture, pano_uv(dir)).rgb; + color = mix(color, pano_color, skybox_blend); + } + + if (sun_glow_enabled && sun_direction.y > -0.2) { + float sun_dot = max(0.0, dot(dir, normalize(sun_direction))); + float glow = pow(sun_dot, 1.0 / max(0.001, sun_glow_size)); + glow = pow(glow, sun_glow_falloff); + color += sun_glow_color.rgb * glow * sun_glow_intensity; + } + + if (dawn_dusk_enabled && dawn_dusk_blend > 0.0) { + float horizon_factor = 1.0 - abs(y); + horizon_factor = pow(horizon_factor, 2.0); + color = mix(color, dawn_dusk_color.rgb, horizon_factor * dawn_dusk_intensity * dawn_dusk_blend); + } + + if (stars_visibility > 0.0 && y > 0.0) { + float star_value = stars(dir) * stars_visibility; + color += stars_tint.rgb * star_value; + } + + COLOR = color; +} +""" + return shader + + +func _update_sky_material() -> void: + if _sky_material == null: + return + + if skybox_texture: + _sky_material.set_shader_parameter("skybox_enabled", true) + _sky_material.set_shader_parameter("skybox_texture", skybox_texture) + _sky_material.set_shader_parameter("skybox_blend", skybox_blend) + else: + _sky_material.set_shader_parameter("skybox_enabled", false) + _sky_material.set_shader_parameter("skybox_blend", skybox_blend) + + _sky_material.set_shader_parameter("sky_top_color", sky_top_color) + _sky_material.set_shader_parameter("sky_horizon_color", sky_horizon_color) + _sky_material.set_shader_parameter("sky_bottom_color", sky_bottom_color) + _sky_material.set_shader_parameter("gradient_intensity", gradient_intensity) + + _sky_material.set_shader_parameter("sun_glow_enabled", sun_glow_enabled and effects_enabled) + _sky_material.set_shader_parameter("sun_direction", sun_direction) + _sky_material.set_shader_parameter("sun_glow_color", sun_glow_color) + _sky_material.set_shader_parameter("sun_glow_intensity", sun_glow_intensity) + _sky_material.set_shader_parameter("sun_glow_size", sun_glow_size) + _sky_material.set_shader_parameter("sun_glow_falloff", sun_glow_falloff) + + _sky_material.set_shader_parameter("dawn_dusk_enabled", dawn_dusk_enabled and effects_enabled) + _sky_material.set_shader_parameter("dawn_dusk_blend", dawn_dusk_blend) + _sky_material.set_shader_parameter("dawn_dusk_intensity", dawn_dusk_intensity) + + +func set_sky_colors(top: Color, horizon: Color, bottom: Color) -> void: + sky_top_color = top + sky_horizon_color = horizon + sky_bottom_color = bottom + _update_sky_material() + + +func set_fog_settings(color: Color, density: float) -> void: + fog_color = color + fog_density = density + _update_fog() + + +func set_sun_glow(direction: Vector3, color: Color, intensity: float, size: float) -> void: + sun_direction = direction.normalized() + sun_glow_color = color + sun_glow_intensity = intensity + sun_glow_size = size + _update_sky_material() + + +func set_dawn_dusk_blend(blend: float) -> void: + dawn_dusk_blend = clampf(blend, 0.0, 1.0) + _update_sky_material() + + +func set_stars_visibility(visibility: float, tint: Color = Color.WHITE) -> void: + if _sky_material: + _sky_material.set_shader_parameter("stars_visibility", visibility) + _sky_material.set_shader_parameter("stars_tint", tint) + + +func set_effects_enabled(enabled: bool) -> void: + effects_enabled = enabled + _update_fog() + _update_sky_material() + + +func apply_config(config: SkyConfig) -> void: + sky_top_color = config.sky_top_color + sky_horizon_color = config.sky_horizon_color + sky_bottom_color = config.sky_bottom_color + fog_color = config.fog_color + fog_density = config.fog_density + sun_glow_intensity = config.sun_glow_intensity + sun_glow_size = config.sun_glow_size + gradient_intensity = config.gradient_intensity + + _update_fog() + _update_sky_material() + set_stars_visibility(config.stars_visibility, config.stars_tint) diff --git a/dynamic-sky/scripts/post_process_controller.gd.uid b/dynamic-sky/scripts/post_process_controller.gd.uid new file mode 100644 index 0000000..12d396b --- /dev/null +++ b/dynamic-sky/scripts/post_process_controller.gd.uid @@ -0,0 +1 @@ +uid://cwqg0bg2m2bme diff --git a/dynamic-sky/scripts/procedural_cloud_texture.gd b/dynamic-sky/scripts/procedural_cloud_texture.gd new file mode 100644 index 0000000..ad40d20 --- /dev/null +++ b/dynamic-sky/scripts/procedural_cloud_texture.gd @@ -0,0 +1,93 @@ +@tool +class_name ProceduralCloudTexture +extends Resource + +@export var size: int = 512 +@export var octaves: int = 6 +@export var persistence: float = 0.5 +@export var lacunarity: float = 2.0 +@export var scale: float = 4.0 +@export var seed_value: int = 0 +@export var cloud_threshold: float = 0.4 +@export var cloud_softness: float = 0.3 + +var _noise: FastNoiseLite +var _texture: ImageTexture + + +func _init() -> void: + _setup_noise() + + +func _setup_noise() -> void: + _noise = FastNoiseLite.new() + _noise.noise_type = FastNoiseLite.TYPE_SIMPLEX + _noise.seed = seed_value + _noise.frequency = 1.0 / (scale * 64.0) + _noise.fractal_type = FastNoiseLite.FRACTAL_FBM + _noise.fractal_octaves = octaves + _noise.fractal_lacunarity = lacunarity + _noise.fractal_gain = persistence + + +func generate() -> ImageTexture: + _setup_noise() + + var image := Image.create(size, size, false, Image.FORMAT_RGBA8) + + for y in size: + for x in size: + var noise_value := (_noise.get_noise_2d(float(x), float(y)) + 1.0) * 0.5 + + var cloud_value := smoothstep(cloud_threshold - cloud_softness, cloud_threshold + cloud_softness, noise_value) + + var color := Color(cloud_value, cloud_value, cloud_value, cloud_value) + image.set_pixel(x, y, color) + + _texture = ImageTexture.create_from_image(image) + return _texture + + +func get_texture() -> ImageTexture: + if _texture == null: + return generate() + return _texture + + +static func create_default_cloud_texture(layer_type: int = 0) -> ImageTexture: + var generator := ProceduralCloudTexture.new() + generator.size = 512 + + match layer_type: + 0: # Low clouds - larger, denser + generator.scale = 3.0 + generator.octaves = 4 + generator.cloud_threshold = 0.35 + generator.cloud_softness = 0.25 + 1: # Mid clouds - medium + generator.scale = 4.0 + generator.octaves = 5 + generator.cloud_threshold = 0.4 + generator.cloud_softness = 0.3 + 2: # High clouds - wispy + generator.scale = 6.0 + generator.octaves = 6 + generator.cloud_threshold = 0.5 + generator.cloud_softness = 0.35 + 3: # Wisps - very thin + generator.scale = 8.0 + generator.octaves = 7 + generator.cloud_threshold = 0.55 + generator.cloud_softness = 0.4 + + return generator.generate() + + +static func create_shadow_texture() -> ImageTexture: + var generator := ProceduralCloudTexture.new() + generator.size = 256 + generator.scale = 4.0 + generator.octaves = 4 + generator.cloud_threshold = 0.3 + generator.cloud_softness = 0.4 + return generator.generate() diff --git a/dynamic-sky/scripts/procedural_cloud_texture.gd.uid b/dynamic-sky/scripts/procedural_cloud_texture.gd.uid new file mode 100644 index 0000000..83f674b --- /dev/null +++ b/dynamic-sky/scripts/procedural_cloud_texture.gd.uid @@ -0,0 +1 @@ +uid://dyjrdeqft0xlk diff --git a/dynamic-sky/scripts/shadow_proxy_system.gd b/dynamic-sky/scripts/shadow_proxy_system.gd new file mode 100644 index 0000000..a8df5db --- /dev/null +++ b/dynamic-sky/scripts/shadow_proxy_system.gd @@ -0,0 +1,220 @@ +@tool +class_name ShadowProxySystem +extends Node3D + +@export_group("Shadow Settings") +@export var enabled: bool = true: + set(value): + enabled = value + if _shadow_mesh: + _shadow_mesh.visible = enabled + +@export var shadow_texture: Texture2D: + set(value): + shadow_texture = value + _update_shadow_texture() + +@export var shadow_opacity: float = 0.3: + set(value): + shadow_opacity = clampf(value, 0.0, 1.0) + _update_shadow_material() + +@export var shadow_color: Color = Color(0.0, 0.0, 0.1): + set(value): + shadow_color = value + _update_shadow_material() + +@export_group("Movement") +@export var flow_direction: Vector2 = Vector2(1.0, 0.0) +@export var flow_speed: float = 5.0 +@export var sync_with_clouds: bool = true + +@export_group("Projection") +@export var projection_size: float = 500.0: + set(value): + projection_size = value + _update_projection_size() + +@export var projection_height: float = 0.1 +@export var tiling: Vector2 = Vector2(2.0, 2.0): + set(value): + tiling = value + _update_shadow_material() + +@export_group("Blur") +@export var blur_enabled: bool = true +@export var blur_amount: float = 0.5: + set(value): + blur_amount = clampf(value, 0.0, 1.0) + _update_shadow_material() + +@export_group("Performance") +@export var follow_camera: bool = true +@export var update_distance: float = 50.0 + +var _shadow_mesh: MeshInstance3D +var _shadow_material: ShaderMaterial +var _uv_offset: Vector2 = Vector2.ZERO +var _last_camera_position: Vector3 = Vector3.ZERO + + +func _ready() -> void: + _create_shadow_mesh() + _create_shadow_material() + _update_shadow_material() + + +func _process(delta: float) -> void: + if not enabled: + return + + _update_flow(delta) + + if follow_camera: + _follow_camera() + + +func _create_shadow_mesh() -> void: + if _shadow_mesh: + _shadow_mesh.queue_free() + + _shadow_mesh = MeshInstance3D.new() + _shadow_mesh.name = "CloudShadowMesh" + + var plane := PlaneMesh.new() + plane.size = Vector2(projection_size, projection_size) + _shadow_mesh.mesh = plane + + _shadow_mesh.position.y = projection_height + _shadow_mesh.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF + + add_child(_shadow_mesh) + + +func _create_shadow_material() -> void: + _shadow_material = ShaderMaterial.new() + _shadow_material.shader = _get_shadow_shader() + + if _shadow_mesh: + _shadow_mesh.material_override = _shadow_material + + +func _get_shadow_shader() -> Shader: + var shader := Shader.new() + shader.code = """ +shader_type spatial; +render_mode unshaded, depth_draw_opaque, cull_disabled, shadows_disabled; + +uniform sampler2D shadow_texture : source_color, filter_linear_mipmap, repeat_enable; +uniform vec4 shadow_color : source_color = vec4(0.0, 0.0, 0.1, 1.0); +uniform float opacity = 0.3; +uniform vec2 tiling = vec2(2.0, 2.0); +uniform vec2 uv_offset = vec2(0.0, 0.0); +uniform float blur_amount = 0.5; +uniform float edge_fade = 0.1; + +varying vec2 world_uv; + +void vertex() { + world_uv = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xz; +} + +void fragment() { + vec2 uv = world_uv * tiling * 0.001 + uv_offset; + + vec4 shadow_sample; + if (blur_amount > 0.01) { + float blur = blur_amount * 0.01; + shadow_sample = texture(shadow_texture, uv) * 0.25; + shadow_sample += texture(shadow_texture, uv + vec2(blur, 0.0)) * 0.125; + shadow_sample += texture(shadow_texture, uv - vec2(blur, 0.0)) * 0.125; + shadow_sample += texture(shadow_texture, uv + vec2(0.0, blur)) * 0.125; + shadow_sample += texture(shadow_texture, uv - vec2(0.0, blur)) * 0.125; + shadow_sample += texture(shadow_texture, uv + vec2(blur, blur)) * 0.0625; + shadow_sample += texture(shadow_texture, uv - vec2(blur, blur)) * 0.0625; + shadow_sample += texture(shadow_texture, uv + vec2(blur, -blur)) * 0.0625; + shadow_sample += texture(shadow_texture, uv + vec2(-blur, blur)) * 0.0625; + } else { + shadow_sample = texture(shadow_texture, uv); + } + + float shadow_value = shadow_sample.r; + + vec2 local_uv = fract(world_uv * 0.001); + float edge = smoothstep(0.0, edge_fade, local_uv.x); + edge *= smoothstep(0.0, edge_fade, local_uv.y); + edge *= smoothstep(0.0, edge_fade, 1.0 - local_uv.x); + edge *= smoothstep(0.0, edge_fade, 1.0 - local_uv.y); + + ALBEDO = shadow_color.rgb; + ALPHA = shadow_value * opacity * edge; +} +""" + return shader + + +func _update_shadow_material() -> void: + if _shadow_material == null: + return + + _shadow_material.set_shader_parameter("shadow_color", shadow_color) + _shadow_material.set_shader_parameter("opacity", shadow_opacity) + _shadow_material.set_shader_parameter("tiling", tiling) + _shadow_material.set_shader_parameter("blur_amount", blur_amount if blur_enabled else 0.0) + + +func _update_shadow_texture() -> void: + if _shadow_material and shadow_texture: + _shadow_material.set_shader_parameter("shadow_texture", shadow_texture) + + +func _update_projection_size() -> void: + if _shadow_mesh and _shadow_mesh.mesh is PlaneMesh: + var plane := _shadow_mesh.mesh as PlaneMesh + plane.size = Vector2(projection_size, projection_size) + + +func _update_flow(delta: float) -> void: + _uv_offset += flow_direction * flow_speed * delta * 0.001 + _uv_offset.x = fmod(_uv_offset.x, 1.0) + _uv_offset.y = fmod(_uv_offset.y, 1.0) + + if _shadow_material: + _shadow_material.set_shader_parameter("uv_offset", _uv_offset) + + +func _follow_camera() -> void: + var camera := get_viewport().get_camera_3d() + if camera == null: + return + + var camera_pos := camera.global_position + var distance := camera_pos.distance_to(_last_camera_position) + + if distance >= update_distance: + _last_camera_position = camera_pos + global_position.x = camera_pos.x + global_position.z = camera_pos.z + + +func set_flow(direction: Vector2, speed: float) -> void: + flow_direction = direction.normalized() + flow_speed = speed + + +func set_shadow_appearance(color: Color, opacity_value: float, blur: float) -> void: + shadow_color = color + shadow_opacity = opacity_value + blur_amount = blur + _update_shadow_material() + + +func sync_with_cloud_system(cloud_system: CloudSystem2D) -> void: + if sync_with_clouds and cloud_system: + flow_direction = cloud_system.global_wind_direction + flow_speed = cloud_system.global_wind_speed * 5.0 + + +func set_cloud_coverage(coverage: float) -> void: + shadow_opacity = coverage * 0.5 + _update_shadow_material() diff --git a/dynamic-sky/scripts/shadow_proxy_system.gd.uid b/dynamic-sky/scripts/shadow_proxy_system.gd.uid new file mode 100644 index 0000000..5e5156d --- /dev/null +++ b/dynamic-sky/scripts/shadow_proxy_system.gd.uid @@ -0,0 +1 @@ +uid://hvs2g6acqryq diff --git a/dynamic-sky/scripts/sky_debug_panel.gd b/dynamic-sky/scripts/sky_debug_panel.gd new file mode 100644 index 0000000..b55c1d1 --- /dev/null +++ b/dynamic-sky/scripts/sky_debug_panel.gd @@ -0,0 +1,171 @@ +@tool +class_name SkyDebugPanel +extends Control + +@export var dynamic_sky: DynamicSkyRoot + +var _panel: PanelContainer +var _vbox: VBoxContainer +var _time_label: Label +var _weather_label: Label +var _cloud_label: Label +var _seed_label: Label +var _time_slider: HSlider +var _weather_dropdown: OptionButton +var _pause_button: Button + + +func _ready() -> void: + _create_ui() + _connect_signals() + + +func _process(_delta: float) -> void: + if dynamic_sky == null: + return + _update_labels() + + +func _create_ui() -> void: + _panel = PanelContainer.new() + _panel.name = "DebugPanel" + _panel.custom_minimum_size = Vector2(300, 200) + add_child(_panel) + + var margin := MarginContainer.new() + margin.add_theme_constant_override("margin_left", 10) + margin.add_theme_constant_override("margin_right", 10) + margin.add_theme_constant_override("margin_top", 10) + margin.add_theme_constant_override("margin_bottom", 10) + _panel.add_child(margin) + + _vbox = VBoxContainer.new() + _vbox.add_theme_constant_override("separation", 8) + margin.add_child(_vbox) + + var title := Label.new() + title.text = "Dynamic Sky Debug" + title.add_theme_font_size_override("font_size", 18) + _vbox.add_child(title) + + _vbox.add_child(HSeparator.new()) + + _time_label = Label.new() + _time_label.text = "Time: 00:00 (0.00)" + _vbox.add_child(_time_label) + + var time_hbox := HBoxContainer.new() + _vbox.add_child(time_hbox) + + var time_label := Label.new() + time_label.text = "Time:" + time_label.custom_minimum_size.x = 60 + time_hbox.add_child(time_label) + + _time_slider = HSlider.new() + _time_slider.min_value = 0.0 + _time_slider.max_value = 1.0 + _time_slider.step = 0.001 + _time_slider.size_flags_horizontal = Control.SIZE_EXPAND_FILL + time_hbox.add_child(_time_slider) + + _pause_button = Button.new() + _pause_button.text = "Pause" + _pause_button.toggle_mode = true + _vbox.add_child(_pause_button) + + _vbox.add_child(HSeparator.new()) + + _weather_label = Label.new() + _weather_label.text = "Weather: Clear" + _vbox.add_child(_weather_label) + + var weather_hbox := HBoxContainer.new() + _vbox.add_child(weather_hbox) + + var weather_label := Label.new() + weather_label.text = "Set:" + weather_label.custom_minimum_size.x = 60 + weather_hbox.add_child(weather_label) + + _weather_dropdown = OptionButton.new() + _weather_dropdown.size_flags_horizontal = Control.SIZE_EXPAND_FILL + for state_name in WeatherProfile.WeatherState.keys(): + _weather_dropdown.add_item(state_name) + weather_hbox.add_child(_weather_dropdown) + + _vbox.add_child(HSeparator.new()) + + _cloud_label = Label.new() + _cloud_label.text = "Clouds: 0%" + _vbox.add_child(_cloud_label) + + _seed_label = Label.new() + _seed_label.text = "Seed: 0" + _vbox.add_child(_seed_label) + + var meteor_button := Button.new() + meteor_button.text = "Trigger Meteor Shower" + meteor_button.pressed.connect(_on_meteor_pressed) + _vbox.add_child(meteor_button) + + +func _connect_signals() -> void: + _time_slider.value_changed.connect(_on_time_slider_changed) + _pause_button.toggled.connect(_on_pause_toggled) + _weather_dropdown.item_selected.connect(_on_weather_selected) + + +func _update_labels() -> void: + if dynamic_sky == null: + return + + var time := dynamic_sky.get_time_of_day() + var hours := int(time * 24.0) + var minutes := int(fmod(time * 24.0 * 60.0, 60.0)) + _time_label.text = "Time: %02d:%02d (%.3f)" % [hours, minutes, time] + + if not _time_slider.has_focus(): + _time_slider.set_value_no_signal(time) + + var weather := dynamic_sky.get_current_weather() + var weather_name: String = WeatherProfile.WeatherState.keys()[weather] + _weather_label.text = "Weather: %s" % weather_name + + if dynamic_sky.weather_controller: + var coverage := dynamic_sky.weather_controller.get_cloud_coverage() + _cloud_label.text = "Clouds: %d%%" % int(coverage * 100) + + if dynamic_sky.weather_controller.is_transitioning(): + var progress := dynamic_sky.weather_controller.get_transition_progress() + _weather_label.text += " (transitioning %.0f%%)" % (progress * 100) + + _seed_label.text = "Seed: %d" % dynamic_sky.deterministic_seed + + if dynamic_sky.day_night_controller: + _pause_button.set_pressed_no_signal(dynamic_sky.day_night_controller.paused) + + +func _on_time_slider_changed(value: float) -> void: + if dynamic_sky: + dynamic_sky.set_time_of_day(value) + + +func _on_pause_toggled(pressed: bool) -> void: + if dynamic_sky: + if pressed: + dynamic_sky.pause_time() + else: + dynamic_sky.resume_time() + _pause_button.text = "Resume" if pressed else "Pause" + + +func _on_weather_selected(index: int) -> void: + if dynamic_sky: + var state: WeatherProfile.WeatherState = index as WeatherProfile.WeatherState + dynamic_sky.set_weather(state) + + +func _on_meteor_pressed() -> void: + if dynamic_sky: + dynamic_sky.trigger_meteor_shower(30.0) diff --git a/dynamic-sky/scripts/sky_debug_panel.gd.uid b/dynamic-sky/scripts/sky_debug_panel.gd.uid new file mode 100644 index 0000000..3fffc9c --- /dev/null +++ b/dynamic-sky/scripts/sky_debug_panel.gd.uid @@ -0,0 +1 @@ +uid://cimhpqq1qo80b diff --git a/dynamic-sky/scripts/skybox_preset_library.gd b/dynamic-sky/scripts/skybox_preset_library.gd new file mode 100644 index 0000000..7aa0662 --- /dev/null +++ b/dynamic-sky/scripts/skybox_preset_library.gd @@ -0,0 +1,131 @@ +@tool +class_name SkyboxPresetLibrary +extends Node + +signal preset_changed(preset: SkyPreset) +signal preset_applied(preset_name: String) +signal preset_reverted(preset_name: String) + +@export var presets: Array[SkyPreset] = [] +@export var current_preset_index: int = 0: + set(value): + current_preset_index = clampi(value, 0, maxi(0, presets.size() - 1)) + if current_preset_index < presets.size(): + _on_preset_selected(presets[current_preset_index]) + +var _base_preset: SkyPreset +var _runtime_overrides: SkyConfig + + +func _ready() -> void: + if presets.is_empty(): + _initialize_default_presets() + if current_preset_index < presets.size(): + _base_preset = presets[current_preset_index] + + +func _initialize_default_presets() -> void: + presets.clear() + presets.append(SkyPreset.create_clear_day()) + presets.append(SkyPreset.create_sunset()) + presets.append(SkyPreset.create_night()) + presets.append(SkyPreset.create_overcast()) + presets.append(SkyPreset.create_storm()) + presets.append(SkyPreset.create_dawn()) + + +func get_preset_names() -> PackedStringArray: + var names := PackedStringArray() + for preset in presets: + names.append(preset.preset_name) + return names + + +func get_current_preset() -> SkyPreset: + if current_preset_index < presets.size(): + return presets[current_preset_index] + return null + + +func get_preset_by_name(preset_name: String) -> SkyPreset: + for preset in presets: + if preset.preset_name == preset_name: + return preset + return null + + +func get_preset_by_type(preset_type: SkyPreset.PresetType) -> SkyPreset: + for preset in presets: + if preset.preset_type == preset_type: + return preset + return null + + +func apply_preset(preset_name: String) -> bool: + var preset := get_preset_by_name(preset_name) + if preset == null: + push_warning("SkyboxPresetLibrary: Preset '%s' not found" % preset_name) + return false + + for i in presets.size(): + if presets[i] == preset: + current_preset_index = i + break + + _base_preset = preset + _runtime_overrides = null + preset_applied.emit(preset_name) + preset_changed.emit(preset) + return true + + +func apply_preset_by_type(preset_type: SkyPreset.PresetType) -> bool: + var preset := get_preset_by_type(preset_type) + if preset == null: + push_warning("SkyboxPresetLibrary: Preset type '%s' not found" % SkyPreset.PresetType.keys()[preset_type]) + return false + return apply_preset(preset.preset_name) + + +func revert_to_preset() -> void: + _runtime_overrides = null + if _base_preset: + preset_reverted.emit(_base_preset.preset_name) + preset_changed.emit(_base_preset) + + +func set_runtime_override(config: SkyConfig) -> void: + _runtime_overrides = config + + +func get_effective_config() -> SkyConfig: + if _runtime_overrides != null: + return _runtime_overrides + if _base_preset != null and _base_preset.config != null: + return _base_preset.config + return SkyConfig.new() + + +func has_runtime_overrides() -> bool: + return _runtime_overrides != null + + +func add_preset(preset: SkyPreset) -> void: + if preset not in presets: + presets.append(preset) + + +func remove_preset(preset_name: String) -> bool: + for i in presets.size(): + if presets[i].preset_name == preset_name: + presets.remove_at(i) + if current_preset_index >= presets.size(): + current_preset_index = maxi(0, presets.size() - 1) + return true + return false + + +func _on_preset_selected(preset: SkyPreset) -> void: + _base_preset = preset + _runtime_overrides = null + preset_changed.emit(preset) diff --git a/dynamic-sky/scripts/skybox_preset_library.gd.uid b/dynamic-sky/scripts/skybox_preset_library.gd.uid new file mode 100644 index 0000000..ecc6f41 --- /dev/null +++ b/dynamic-sky/scripts/skybox_preset_library.gd.uid @@ -0,0 +1 @@ +uid://cb8c6ug44jx4t diff --git a/dynamic-sky/scripts/vfx_controller.gd b/dynamic-sky/scripts/vfx_controller.gd new file mode 100644 index 0000000..e8bbac3 --- /dev/null +++ b/dynamic-sky/scripts/vfx_controller.gd @@ -0,0 +1,299 @@ +@tool +class_name VFXController +extends Node3D + +signal shooting_star_spawned(position: Vector3, direction: Vector3) +signal meteor_shower_started() +signal meteor_shower_ended() + +@export_group("Shooting Stars") +@export var shooting_stars_enabled: bool = true +@export var shooting_star_frequency: float = 0.05 +@export var min_spawn_interval: float = 5.0 +@export var max_spawn_interval: float = 30.0 + +@export_group("Shooting Star Appearance") +@export var trail_length: float = 2.0 +@export var trail_width: float = 0.05 +@export var star_brightness: float = 1.5 +@export var star_color: Color = Color(1.0, 1.0, 0.9) +@export var trail_fade_time: float = 0.5 + +@export_group("Meteor Shower") +@export var meteor_shower_active: bool = false +@export var meteor_shower_intensity: float = 1.0 +@export var meteors_per_second: float = 3.0 +@export var meteor_shower_duration: float = 60.0 + +@export_group("Meteor Appearance") +@export var meteor_trail_length: float = 4.0 +@export var meteor_brightness: float = 2.0 +@export var meteor_color: Color = Color(1.0, 0.8, 0.5) + +@export_group("Spawn Area") +@export var spawn_radius: float = 500.0 +@export var spawn_height_min: float = 100.0 +@export var spawn_height_max: float = 200.0 +@export var spawn_direction_variance: float = 30.0 + +@export_group("Audio") +@export var meteor_shower_audio_event: String = "" + +var _spawn_timer: float = 0.0 +var _next_spawn_time: float = 0.0 +var _meteor_timer: float = 0.0 +var _meteor_spawn_accumulator: float = 0.0 +var _meteor_shower_elapsed: float = 0.0 +var _active_stars: Array[Dictionary] = [] +var _star_mesh: MeshInstance3D +var _star_material: ShaderMaterial +var _is_night: bool = false + + +func _ready() -> void: + _setup_star_mesh() + _schedule_next_shooting_star() + + +func _process(delta: float) -> void: + if not _is_night: + return + + if shooting_stars_enabled: + _process_shooting_stars(delta) + + if meteor_shower_active: + _process_meteor_shower(delta) + + _update_active_stars(delta) + + +func _setup_star_mesh() -> void: + _star_material = ShaderMaterial.new() + _star_material.shader = _get_star_shader() + _star_material.set_shader_parameter("color", star_color) + _star_material.set_shader_parameter("brightness", star_brightness) + + +func _get_star_shader() -> Shader: + var shader := Shader.new() + shader.code = """ +shader_type spatial; +render_mode unshaded, depth_draw_never, cull_disabled, blend_add; + +uniform vec4 color : source_color = vec4(1.0, 1.0, 0.9, 1.0); +uniform float brightness = 1.5; +uniform float fade = 1.0; +uniform float trail_progress = 0.0; + +void vertex() { + VERTEX.y *= mix(0.1, 1.0, trail_progress); +} + +void fragment() { + float gradient = UV.y; + float alpha = gradient * fade; + ALBEDO = color.rgb * brightness; + ALPHA = alpha * color.a; +} +""" + return shader + + +func _schedule_next_shooting_star() -> void: + _next_spawn_time = randf_range(min_spawn_interval, max_spawn_interval) + _next_spawn_time /= maxf(0.01, shooting_star_frequency) + _spawn_timer = 0.0 + + +func _process_shooting_stars(delta: float) -> void: + _spawn_timer += delta + + if _spawn_timer >= _next_spawn_time: + _spawn_shooting_star() + _schedule_next_shooting_star() + + +func _process_meteor_shower(delta: float) -> void: + _meteor_shower_elapsed += delta + + if _meteor_shower_elapsed >= meteor_shower_duration: + stop_meteor_shower() + return + + _meteor_spawn_accumulator += meteors_per_second * meteor_shower_intensity * delta + + while _meteor_spawn_accumulator >= 1.0: + _meteor_spawn_accumulator -= 1.0 + _spawn_meteor() + + +func _spawn_shooting_star() -> void: + var spawn_pos := _get_random_sky_position() + var direction := _get_random_fall_direction() + + var star_data := { + "position": spawn_pos, + "direction": direction, + "speed": randf_range(50.0, 100.0), + "lifetime": 0.0, + "max_lifetime": trail_fade_time + randf_range(0.2, 0.5), + "trail_length": trail_length * randf_range(0.8, 1.2), + "brightness": star_brightness * randf_range(0.8, 1.2), + "color": star_color, + "mesh": _create_star_trail(spawn_pos, direction), + "is_meteor": false + } + + _active_stars.append(star_data) + shooting_star_spawned.emit(spawn_pos, direction) + + +func _spawn_meteor() -> void: + var spawn_pos := _get_random_sky_position() + var direction := _get_random_fall_direction() + + var star_data := { + "position": spawn_pos, + "direction": direction, + "speed": randf_range(80.0, 150.0), + "lifetime": 0.0, + "max_lifetime": trail_fade_time + randf_range(0.3, 0.7), + "trail_length": meteor_trail_length * randf_range(0.8, 1.2), + "brightness": meteor_brightness * randf_range(0.8, 1.2), + "color": meteor_color, + "mesh": _create_star_trail(spawn_pos, direction, true), + "is_meteor": true + } + + _active_stars.append(star_data) + + +func _get_random_sky_position() -> Vector3: + var angle := randf() * TAU + var radius := randf_range(spawn_radius * 0.3, spawn_radius) + var height := randf_range(spawn_height_min, spawn_height_max) + + return Vector3( + cos(angle) * radius, + height, + sin(angle) * radius + ) + + +func _get_random_fall_direction() -> Vector3: + var base_dir := Vector3(-0.5, -0.8, -0.3).normalized() + + var variance := deg_to_rad(spawn_direction_variance) + var rotation := Basis(Vector3.UP, randf() * TAU) + rotation = rotation.rotated(Vector3.RIGHT, randf_range(-variance, variance)) + + return (rotation * base_dir).normalized() + + +func _create_star_trail(position: Vector3, direction: Vector3, is_meteor: bool = false) -> MeshInstance3D: + var mesh_instance := MeshInstance3D.new() + + var length := meteor_trail_length if is_meteor else trail_length + var width := trail_width * (1.5 if is_meteor else 1.0) + + var quad := QuadMesh.new() + quad.size = Vector2(width, length) + mesh_instance.mesh = quad + + var material := _star_material.duplicate() as ShaderMaterial + material.set_shader_parameter("color", meteor_color if is_meteor else star_color) + material.set_shader_parameter("brightness", meteor_brightness if is_meteor else star_brightness) + mesh_instance.material_override = material + + mesh_instance.position = position + mesh_instance.look_at(position + direction, Vector3.UP) + mesh_instance.rotate_object_local(Vector3.RIGHT, PI / 2.0) + + add_child(mesh_instance) + return mesh_instance + + +func _update_active_stars(delta: float) -> void: + var stars_to_remove: Array[int] = [] + + for i in _active_stars.size(): + var star: Dictionary = _active_stars[i] + star["lifetime"] += delta + + var progress: float = star["lifetime"] / star["max_lifetime"] + + if progress >= 1.0: + stars_to_remove.append(i) + continue + + var mesh: MeshInstance3D = star["mesh"] + if is_instance_valid(mesh): + star["position"] += star["direction"] * star["speed"] * delta + mesh.position = star["position"] + + var fade := 1.0 - smoothstep(0.5, 1.0, progress) + var material := mesh.material_override as ShaderMaterial + if material: + material.set_shader_parameter("fade", fade) + material.set_shader_parameter("trail_progress", minf(progress * 3.0, 1.0)) + + for i in range(stars_to_remove.size() - 1, -1, -1): + var idx: int = stars_to_remove[i] + var star: Dictionary = _active_stars[idx] + var mesh: MeshInstance3D = star["mesh"] + if is_instance_valid(mesh): + mesh.queue_free() + _active_stars.remove_at(idx) + + +func set_night_mode(is_night: bool) -> void: + _is_night = is_night + + if not is_night: + _clear_all_stars() + + +func _clear_all_stars() -> void: + for star in _active_stars: + var mesh: MeshInstance3D = star["mesh"] + if is_instance_valid(mesh): + mesh.queue_free() + _active_stars.clear() + + +func start_meteor_shower(duration: float = -1.0) -> void: + if duration > 0: + meteor_shower_duration = duration + + meteor_shower_active = true + _meteor_shower_elapsed = 0.0 + _meteor_spawn_accumulator = 0.0 + + meteor_shower_started.emit() + + if meteor_shower_audio_event != "": + pass + + +func stop_meteor_shower() -> void: + meteor_shower_active = false + _meteor_shower_elapsed = 0.0 + meteor_shower_ended.emit() + + +func trigger_shooting_star() -> void: + if _is_night: + _spawn_shooting_star() + + +func set_shooting_star_appearance(color: Color, brightness: float, length: float) -> void: + star_color = color + star_brightness = brightness + trail_length = length + + +func set_meteor_appearance(color: Color, brightness: float, length: float) -> void: + meteor_color = color + meteor_brightness = brightness + meteor_trail_length = length diff --git a/dynamic-sky/scripts/vfx_controller.gd.uid b/dynamic-sky/scripts/vfx_controller.gd.uid new file mode 100644 index 0000000..c3a4c9f --- /dev/null +++ b/dynamic-sky/scripts/vfx_controller.gd.uid @@ -0,0 +1 @@ +uid://cnvj3m5fpsdtj diff --git a/dynamic-sky/scripts/weather_controller.gd b/dynamic-sky/scripts/weather_controller.gd new file mode 100644 index 0000000..aea4eaa --- /dev/null +++ b/dynamic-sky/scripts/weather_controller.gd @@ -0,0 +1,278 @@ +@tool +class_name WeatherController +extends Node + +signal weather_state_changed(old_state: WeatherProfile.WeatherState, new_state: WeatherProfile.WeatherState) +signal weather_transition_started(from: WeatherProfile.WeatherState, to: WeatherProfile.WeatherState) +signal weather_transition_completed(state: WeatherProfile.WeatherState) +signal weather_state_entered(state: WeatherProfile.WeatherState) +signal weather_state_exited(state: WeatherProfile.WeatherState) + +@export_group("Weather Profiles") +@export var weather_profiles: Array[WeatherProfile] = [] +@export var current_weather: WeatherProfile.WeatherState = WeatherProfile.WeatherState.CLEAR: + set(value): + if value != current_weather: + _request_weather_change(value) + +@export_group("Transition Settings") +@export var default_transition_duration: float = 5.0 +@export var transition_curve: Curve + +@export_group("Random Weather") +@export var enable_random_weather: bool = false +@export var min_weather_duration: float = 60.0 +@export var max_weather_duration: float = 300.0 +@export var snow_enabled: bool = true + +@export_group("Scripted Timeline") +@export var use_scripted_timeline: bool = false +@export var weather_timeline: Array[Dictionary] = [] + +var _current_profile: WeatherProfile +var _target_profile: WeatherProfile +var _transition_weight: float = 1.0 +var _is_transitioning: bool = false +var _transition_duration: float = 0.0 +var _transition_elapsed: float = 0.0 +var _random_weather_timer: float = 0.0 +var _next_weather_change: float = 0.0 +var _timeline_index: int = 0 +var _timeline_timer: float = 0.0 + + +func _ready() -> void: + if weather_profiles.is_empty(): + _initialize_default_profiles() + + if transition_curve == null: + transition_curve = Curve.new() + transition_curve.add_point(Vector2(0.0, 0.0)) + transition_curve.add_point(Vector2(0.5, 0.5)) + transition_curve.add_point(Vector2(1.0, 1.0)) + + _current_profile = get_profile_for_state(current_weather) + _target_profile = _current_profile + _schedule_next_random_weather() + + +func _process(delta: float) -> void: + if _is_transitioning: + _process_transition(delta) + + if enable_random_weather and not use_scripted_timeline: + _process_random_weather(delta) + + if use_scripted_timeline: + _process_scripted_timeline(delta) + + +func _initialize_default_profiles() -> void: + weather_profiles.clear() + weather_profiles.append(WeatherProfile.create_clear()) + weather_profiles.append(WeatherProfile.create_cloudy()) + weather_profiles.append(WeatherProfile.create_overcast()) + weather_profiles.append(WeatherProfile.create_rain()) + weather_profiles.append(WeatherProfile.create_storm()) + weather_profiles.append(WeatherProfile.create_snow()) + + +func _process_transition(delta: float) -> void: + _transition_elapsed += delta + var t := clampf(_transition_elapsed / _transition_duration, 0.0, 1.0) + + if transition_curve: + _transition_weight = transition_curve.sample(t) + else: + _transition_weight = t + + if t >= 1.0: + _complete_transition() + + +func _complete_transition() -> void: + _is_transitioning = false + _transition_weight = 1.0 + _current_profile = _target_profile + var new_state := _current_profile.weather_state + weather_transition_completed.emit(new_state) + weather_state_entered.emit(new_state) + + +func _process_random_weather(delta: float) -> void: + _random_weather_timer += delta + + if _random_weather_timer >= _next_weather_change: + _random_weather_timer = 0.0 + _trigger_random_weather_change() + _schedule_next_random_weather() + + +func _schedule_next_random_weather() -> void: + _next_weather_change = randf_range(min_weather_duration, max_weather_duration) + + +func _trigger_random_weather_change() -> void: + var available_states: Array[WeatherProfile.WeatherState] = [] + for state in WeatherProfile.WeatherState.values(): + if state == current_weather: + continue + if state == WeatherProfile.WeatherState.SNOW and not snow_enabled: + continue + available_states.append(state) + + if available_states.is_empty(): + return + + var new_state: WeatherProfile.WeatherState = available_states.pick_random() + set_weather(new_state) + + +func _process_scripted_timeline(delta: float) -> void: + if weather_timeline.is_empty(): + return + + _timeline_timer += delta + + if _timeline_index < weather_timeline.size(): + var entry: Dictionary = weather_timeline[_timeline_index] + var trigger_time: float = entry.get("time", 0.0) + + if _timeline_timer >= trigger_time: + var state_name: String = entry.get("state", "CLEAR") + var duration: float = entry.get("duration", default_transition_duration) + + if WeatherProfile.WeatherState.has(state_name): + var state: WeatherProfile.WeatherState = WeatherProfile.WeatherState.get(state_name) + set_weather(state, duration) + + _timeline_index += 1 + + +func _request_weather_change(new_state: WeatherProfile.WeatherState) -> void: + var profile := get_profile_for_state(new_state) + if profile == null: + push_warning("WeatherController: No profile for state %s" % WeatherProfile.WeatherState.keys()[new_state]) + return + + set_weather(new_state, profile.default_transition_duration) + + +func get_profile_for_state(state: WeatherProfile.WeatherState) -> WeatherProfile: + for profile in weather_profiles: + if profile.weather_state == state: + return profile + return null + + +func set_weather(state: WeatherProfile.WeatherState, transition_duration: float = -1.0) -> void: + var profile := get_profile_for_state(state) + if profile == null: + push_warning("WeatherController: No profile for state %s" % WeatherProfile.WeatherState.keys()[state]) + return + + if _current_profile: + weather_state_exited.emit(_current_profile.weather_state) + + var old_state := current_weather + current_weather = state + _target_profile = profile + + if transition_duration < 0: + transition_duration = profile.default_transition_duration + + _transition_duration = transition_duration + _transition_elapsed = 0.0 + _is_transitioning = true + _transition_weight = 0.0 + + weather_transition_started.emit(old_state, state) + weather_state_changed.emit(old_state, state) + + +func set_weather_immediate(state: WeatherProfile.WeatherState) -> void: + var profile := get_profile_for_state(state) + if profile == null: + return + + if _current_profile: + weather_state_exited.emit(_current_profile.weather_state) + + var old_state := current_weather + current_weather = state + _current_profile = profile + _target_profile = profile + _is_transitioning = false + _transition_weight = 1.0 + + weather_state_changed.emit(old_state, state) + weather_state_entered.emit(state) + + +func get_current_profile() -> WeatherProfile: + if _is_transitioning and _current_profile and _target_profile: + return _current_profile.lerp_to(_target_profile, _transition_weight) + return _current_profile + + +func get_cloud_density() -> float: + var profile := get_current_profile() + return profile.cloud_density if profile else 0.2 + + +func get_cloud_opacity() -> float: + var profile := get_current_profile() + return profile.cloud_opacity if profile else 0.7 + + +func get_cloud_coverage() -> float: + var profile := get_current_profile() + return profile.cloud_coverage if profile else 0.3 + + +func get_cloud_color() -> Color: + var profile := get_current_profile() + return profile.cloud_color if profile else Color.WHITE + + +func get_wind_speed() -> float: + var profile := get_current_profile() + return profile.wind_speed if profile else 1.0 + + +func get_wind_direction() -> Vector2: + var profile := get_current_profile() + return profile.wind_direction if profile else Vector2(1.0, 0.0) + + +func get_rain_intensity() -> float: + var profile := get_current_profile() + return profile.rain_intensity if profile else 0.0 + + +func get_snow_intensity() -> float: + var profile := get_current_profile() + return profile.snow_intensity if profile else 0.0 + + +func is_lightning_active() -> bool: + var profile := get_current_profile() + return profile.lightning_enabled if profile else false + + +func get_lightning_frequency() -> float: + var profile := get_current_profile() + return profile.lightning_frequency if profile else 0.0 + + +func get_transition_progress() -> float: + return _transition_weight + + +func is_transitioning() -> bool: + return _is_transitioning + + +func reset_timeline() -> void: + _timeline_index = 0 + _timeline_timer = 0.0 diff --git a/dynamic-sky/scripts/weather_controller.gd.uid b/dynamic-sky/scripts/weather_controller.gd.uid new file mode 100644 index 0000000..66eeeb4 --- /dev/null +++ b/dynamic-sky/scripts/weather_controller.gd.uid @@ -0,0 +1 @@ +uid://blyx8v2gq8nlw diff --git a/dynamic-sky/scripts/weather_kill_volume.gd b/dynamic-sky/scripts/weather_kill_volume.gd new file mode 100644 index 0000000..1480cb2 --- /dev/null +++ b/dynamic-sky/scripts/weather_kill_volume.gd @@ -0,0 +1,277 @@ +@tool +class_name WeatherKillVolume +extends Area3D + +enum VolumeShape { + BOX, + SPHERE, + CYLINDER +} + +@export_group("Volume Settings") +@export var volume_shape: VolumeShape = VolumeShape.BOX: + set(value): + volume_shape = value + _update_collision_shape() + +@export var volume_size: Vector3 = Vector3(10.0, 5.0, 10.0): + set(value): + volume_size = value + _update_collision_shape() + +@export var volume_radius: float = 5.0: + set(value): + volume_radius = value + _update_collision_shape() + +@export_group("Kill Behavior") +@export var enabled: bool = true +@export var priority: int = 0 +@export var instant_kill: bool = true +@export var fade_out_margin: float = 1.0 +@export var fade_out_duration: float = 0.3 + +@export_group("Particle Types") +@export var block_rain: bool = true +@export var block_snow: bool = true +@export var block_all_precipitation: bool = true + +@export_group("Debug") +@export var show_debug_gizmo: bool = true +@export var debug_color: Color = Color(0.2, 0.5, 1.0, 0.3) + +var _collision_shape: CollisionShape3D +var _particles_inside: Dictionary = {} + + +func _ready() -> void: + _setup_collision() + + body_entered.connect(_on_body_entered) + body_exited.connect(_on_body_exited) + area_entered.connect(_on_area_entered) + area_exited.connect(_on_area_exited) + + +func _setup_collision() -> void: + _collision_shape = CollisionShape3D.new() + _collision_shape.name = "KillVolumeShape" + add_child(_collision_shape) + _update_collision_shape() + + collision_layer = 0 + collision_mask = 1 << 20 + monitorable = true + monitoring = true + + +func _update_collision_shape() -> void: + if _collision_shape == null: + return + + var shape: Shape3D + + match volume_shape: + VolumeShape.BOX: + var box := BoxShape3D.new() + box.size = volume_size + shape = box + + VolumeShape.SPHERE: + var sphere := SphereShape3D.new() + sphere.radius = volume_radius + shape = sphere + + VolumeShape.CYLINDER: + var cylinder := CylinderShape3D.new() + cylinder.radius = volume_radius + cylinder.height = volume_size.y + shape = cylinder + + _collision_shape.shape = shape + + +func _on_body_entered(body: Node3D) -> void: + if not enabled: + return + + if _is_precipitation_particle(body): + _handle_particle_enter(body) + + +func _on_body_exited(body: Node3D) -> void: + _handle_particle_exit(body) + + +func _on_area_entered(area: Area3D) -> void: + if not enabled: + return + + if area.has_meta("precipitation_particle"): + _handle_particle_enter(area) + + +func _on_area_exited(area: Area3D) -> void: + _handle_particle_exit(area) + + +func _is_precipitation_particle(node: Node3D) -> bool: + if node.has_meta("particle_type"): + var particle_type: String = node.get_meta("particle_type") + + if block_all_precipitation: + return particle_type in ["rain", "snow", "hail", "sleet"] + + if block_rain and particle_type == "rain": + return true + + if block_snow and particle_type == "snow": + return true + + return false + + +func _handle_particle_enter(node: Node3D) -> void: + if instant_kill: + _kill_particle(node) + else: + _particles_inside[node.get_instance_id()] = { + "node": node, + "fade_time": 0.0 + } + + +func _handle_particle_exit(node: Node3D) -> void: + var id := node.get_instance_id() + if _particles_inside.has(id): + _particles_inside.erase(id) + + +func _kill_particle(node: Node3D) -> void: + if node.has_method("kill"): + node.call("kill") + elif node is GPUParticles3D: + node.emitting = false + elif node is CPUParticles3D: + node.emitting = false + + +func _process(delta: float) -> void: + if instant_kill or _particles_inside.is_empty(): + return + + var to_remove: Array[int] = [] + + for id in _particles_inside: + var data: Dictionary = _particles_inside[id] + var node: Node3D = data["node"] + + if not is_instance_valid(node): + to_remove.append(id) + continue + + data["fade_time"] += delta + var fade_progress: float = data["fade_time"] / fade_out_duration + + if fade_progress >= 1.0: + _kill_particle(node) + to_remove.append(id) + else: + _apply_fade(node, 1.0 - fade_progress) + + for id in to_remove: + _particles_inside.erase(id) + + +func _apply_fade(node: Node3D, alpha: float) -> void: + if node.has_method("set_alpha"): + node.call("set_alpha", alpha) + elif node is GPUParticles3D or node is CPUParticles3D: + pass + + +func is_point_inside(point: Vector3) -> bool: + var local_point := to_local(point) + + match volume_shape: + VolumeShape.BOX: + var half_size := volume_size * 0.5 + return abs(local_point.x) <= half_size.x and \ + abs(local_point.y) <= half_size.y and \ + abs(local_point.z) <= half_size.z + + VolumeShape.SPHERE: + return local_point.length() <= volume_radius + + VolumeShape.CYLINDER: + var horizontal_dist := Vector2(local_point.x, local_point.z).length() + var half_height := volume_size.y * 0.5 + return horizontal_dist <= volume_radius and abs(local_point.y) <= half_height + + return false + + +func is_point_in_fade_margin(point: Vector3) -> bool: + if fade_out_margin <= 0: + return false + + var local_point := to_local(point) + + match volume_shape: + VolumeShape.BOX: + var half_size := volume_size * 0.5 + var inner_half := half_size - Vector3(fade_out_margin, fade_out_margin, fade_out_margin) + + var in_outer := abs(local_point.x) <= half_size.x and \ + abs(local_point.y) <= half_size.y and \ + abs(local_point.z) <= half_size.z + + var in_inner := abs(local_point.x) <= inner_half.x and \ + abs(local_point.y) <= inner_half.y and \ + abs(local_point.z) <= inner_half.z + + return in_outer and not in_inner + + VolumeShape.SPHERE: + var dist := local_point.length() + return dist <= volume_radius and dist >= volume_radius - fade_out_margin + + VolumeShape.CYLINDER: + var horizontal_dist := Vector2(local_point.x, local_point.z).length() + var half_height := volume_size.y * 0.5 + + var in_outer := horizontal_dist <= volume_radius and abs(local_point.y) <= half_height + var in_inner := horizontal_dist <= volume_radius - fade_out_margin and \ + abs(local_point.y) <= half_height - fade_out_margin + + return in_outer and not in_inner + + return false + + +func get_fade_factor(point: Vector3) -> float: + if not is_point_inside(point): + return 1.0 + + if not is_point_in_fade_margin(point): + return 0.0 + + var local_point := to_local(point) + var min_distance_to_edge := fade_out_margin + + match volume_shape: + VolumeShape.BOX: + var half_size := volume_size * 0.5 + min_distance_to_edge = minf(min_distance_to_edge, half_size.x - abs(local_point.x)) + min_distance_to_edge = minf(min_distance_to_edge, half_size.y - abs(local_point.y)) + min_distance_to_edge = minf(min_distance_to_edge, half_size.z - abs(local_point.z)) + + VolumeShape.SPHERE: + min_distance_to_edge = volume_radius - local_point.length() + + VolumeShape.CYLINDER: + var horizontal_dist := Vector2(local_point.x, local_point.z).length() + var half_height := volume_size.y * 0.5 + min_distance_to_edge = minf(volume_radius - horizontal_dist, half_height - abs(local_point.y)) + + return clampf(min_distance_to_edge / fade_out_margin, 0.0, 1.0) diff --git a/dynamic-sky/scripts/weather_kill_volume.gd.uid b/dynamic-sky/scripts/weather_kill_volume.gd.uid new file mode 100644 index 0000000..3023536 --- /dev/null +++ b/dynamic-sky/scripts/weather_kill_volume.gd.uid @@ -0,0 +1 @@ +uid://burw0wjl85d8a diff --git a/dynamic-sky/sky_04_2k.png b/dynamic-sky/sky_04_2k.png new file mode 100644 index 0000000..bc635dc Binary files /dev/null and b/dynamic-sky/sky_04_2k.png differ diff --git a/dynamic-sky/sky_06_2k.png b/dynamic-sky/sky_06_2k.png new file mode 100644 index 0000000..3279247 Binary files /dev/null and b/dynamic-sky/sky_06_2k.png differ diff --git a/dynamic-sky/sky_11_2k.png b/dynamic-sky/sky_11_2k.png new file mode 100644 index 0000000..5e51b89 Binary files /dev/null and b/dynamic-sky/sky_11_2k.png differ diff --git a/dynamic-sky/sky_16_2k.png b/dynamic-sky/sky_16_2k.png new file mode 100644 index 0000000..3a0ea1a Binary files /dev/null and b/dynamic-sky/sky_16_2k.png differ diff --git a/dynamic-sky/sky_25_2k.png b/dynamic-sky/sky_25_2k.png new file mode 100644 index 0000000..ee8dcd4 Binary files /dev/null and b/dynamic-sky/sky_25_2k.png differ diff --git a/dynamic-sky/sky_44_2k.png b/dynamic-sky/sky_44_2k.png new file mode 100644 index 0000000..5812ab7 Binary files /dev/null and b/dynamic-sky/sky_44_2k.png differ