diff --git a/js/beestat/weather.js b/js/beestat/weather.js index 0e50ad2..ca61758 100644 --- a/js/beestat/weather.js +++ b/js/beestat/weather.js @@ -9,6 +9,8 @@ beestat.weather = {}; * `icon_color`: UI accent color for the weather icon. * `cloud_density`: Controls cloud particle count/intensity in the scene. * `cloud_darkness`: Controls cloud shading/dimming (0 clear -> 2 very dark). + * `fog_density`: Controls low-altitude volumetric fog cloud density. + * `fog_color`: Hex color used to tint low-altitude fog volumes. * `rain_density`: Controls rain particle count/intensity. * `snow_density`: Controls snow particle count/intensity and snow-cover blend. * `lightning_frequency`: Controls frequency/intensity of lightning effects. @@ -221,6 +223,8 @@ beestat.weather.settings_ = { 'icon_color': beestat.style.color.gray.base, 'cloud_density': 0.6, 'cloud_darkness': 0.2, + 'fog_density': 1.2, + 'fog_color': '#d6dde8', 'rain_density': 0, 'snow_density': 0, 'lightning_frequency': 0, @@ -232,6 +236,8 @@ beestat.weather.settings_ = { 'icon_color': beestat.style.color.gray.base, 'cloud_density': 0.45, 'cloud_darkness': 0.35, + 'fog_density': 0.8, + 'fog_color': '#d9d4c8', 'rain_density': 0, 'snow_density': 0, 'lightning_frequency': 0, @@ -243,6 +249,8 @@ beestat.weather.settings_ = { 'icon_color': beestat.style.color.gray.base, 'cloud_density': 0.45, 'cloud_darkness': 0.35, + 'fog_density': 1.05, + 'fog_color': '#cbc7c3', 'rain_density': 0, 'snow_density': 0, 'lightning_frequency': 0, @@ -254,6 +262,8 @@ beestat.weather.settings_ = { 'icon_color': beestat.style.color.gray.base, 'cloud_density': 0.45, 'cloud_darkness': 0.35, + 'fog_density': 0.95, + 'fog_color': '#d9c7a8', 'rain_density': 0, 'snow_density': 0, 'lightning_frequency': 0, @@ -271,6 +281,8 @@ beestat.weather.default_settings_ = { 'icon_color': beestat.style.color.gray.base, 'cloud_density': 0.03, 'cloud_darkness': 0, + 'fog_density': 0, + 'fog_color': '#d6dde8', 'rain_density': 0, 'snow_density': 0, 'lightning_frequency': 0, @@ -337,6 +349,37 @@ beestat.weather.get_cloud_darkness = function(condition) { return beestat.weather.get_settings_(condition).cloud_darkness; }; +/** + * Get fog density for a condition. + * Higher values increase low-altitude volumetric fog presence. + * + * @param {string} condition + * + * @return {number} + */ +beestat.weather.get_fog_density = function(condition) { + const fog_density = beestat.weather.get_settings_(condition).fog_density; + if (fog_density !== undefined) { + return fog_density; + } + return beestat.weather.default_settings_.fog_density; +}; + +/** + * Get fog color tint for a condition. + * + * @param {string} condition + * + * @return {string} + */ +beestat.weather.get_fog_color = function(condition) { + const fog_color = beestat.weather.get_settings_(condition).fog_color; + if (typeof fog_color === 'string' && fog_color.length > 0) { + return fog_color; + } + return beestat.weather.default_settings_.fog_color; +}; + /** * Get rain density for a condition. * Higher values increase rain particle count and rainfall intensity. diff --git a/js/component/card/three_d.js b/js/component/card/three_d.js index a9f6974..862018d 100644 --- a/js/component/card/three_d.js +++ b/js/component/card/three_d.js @@ -76,7 +76,7 @@ beestat.component.card.three_d = function() { if (self.get_weather_() !== 'auto') { return; } - self.apply_weather_setting_to_scene_(); + self.apply_weather_setting_to_scene_(false); self.decorate_toolbar_(); }; beestat.dispatcher.addEventListener('cache.thermostat', this.handle_thermostat_cache_change_); @@ -112,6 +112,15 @@ beestat.component.card.three_d.rerender_delay_scene_setting_ms = 1000; */ beestat.component.card.three_d.rerender_loading_min_visible_ms = 350; +/** + * Debug-only weather override. + * Set to `null` to use the scene-selected weather mode. + * Set to a weather condition string (e.g. `fog`, `rain`) to force it. + * + * @type {?string} + */ +beestat.component.card.three_d.debug_weather_override = null; + /** * Scene setting keys that require a full rerender. * @@ -666,6 +675,26 @@ beestat.component.card.three_d.prototype.get_auto_weather_from_thermostat_ = fun return beestat.weather.get_settings_(thermostat?.weather?.condition).condition; }; +/** + * Get a normalized debug weather override condition. + * + * @return {?string} + */ +beestat.component.card.three_d.prototype.get_debug_weather_override_condition_ = function() { + const override = beestat.component.card.three_d.debug_weather_override; + if (typeof override !== 'string') { + return null; + } + const normalized_override = override.trim().toLowerCase(); + if (normalized_override.length === 0) { + return null; + } + if (normalized_override === 'auto') { + return this.get_auto_weather_from_thermostat_(); + } + return beestat.weather.get_settings_(normalized_override).condition; +}; + /** * Resolve selected weather mode into a weather condition. * @@ -674,6 +703,10 @@ beestat.component.card.three_d.prototype.get_auto_weather_from_thermostat_ = fun * @return {string} */ beestat.component.card.three_d.prototype.get_weather_condition_from_mode_ = function(weather) { + const debug_override_condition = this.get_debug_weather_override_condition_(); + if (debug_override_condition !== null) { + return debug_override_condition; + } if (weather === 'auto') { return this.get_auto_weather_from_thermostat_(); } @@ -798,6 +831,8 @@ beestat.component.card.three_d.prototype.set_show_group_ = function(group_id, vi * @return {{ * cloud_density: number, * cloud_darkness: number, + * fog_density: number, + * fog_color: string, * rain_density: number, * snow_density: number, * lightning_frequency: number, @@ -809,6 +844,8 @@ beestat.component.card.three_d.prototype.get_weather_settings_from_weather_ = fu return { 'cloud_density': beestat.weather.get_cloud_density(condition), 'cloud_darkness': beestat.weather.get_cloud_darkness(condition), + 'fog_density': beestat.weather.get_fog_density(condition), + 'fog_color': beestat.weather.get_fog_color(condition), 'rain_density': beestat.weather.get_rain_density(condition), 'snow_density': beestat.weather.get_snow_density(condition), 'lightning_frequency': beestat.weather.get_lightning_frequency(condition), @@ -818,8 +855,10 @@ beestat.component.card.three_d.prototype.get_weather_settings_from_weather_ = fu /** * Apply current weather settings to the scene. + * + * @param {boolean=} opt_persist Persist weather settings to floor-plan scene data. */ -beestat.component.card.three_d.prototype.apply_weather_setting_to_scene_ = function() { +beestat.component.card.three_d.prototype.apply_weather_setting_to_scene_ = function(opt_persist) { if (this.scene_ === undefined) { return; } @@ -827,6 +866,18 @@ beestat.component.card.three_d.prototype.apply_weather_setting_to_scene_ = funct this.ensure_scene_settings_values_(); const weather_settings = this.get_weather_settings_from_weather_(this.get_weather_()); Object.assign(this.scene_settings_values_, weather_settings); + const persist = opt_persist === true; + if (persist === true) { + const scene_visualize = this.get_scene_visualize_state_(); + if (scene_visualize !== null) { + const previous_settings_json = JSON.stringify(scene_visualize.settings || {}); + Object.assign(scene_visualize.settings, weather_settings); + const next_settings_json = JSON.stringify(scene_visualize.settings); + if (previous_settings_json !== next_settings_json) { + this.save_scene_visualize_state_(); + } + } + } this.scene_.set_scene_settings(weather_settings, { 'rerender': false }); @@ -871,10 +922,6 @@ beestat.component.card.three_d.prototype.ensure_scene_settings_values_ = functio this.save_scene_visualize_state_(); } } - Object.assign( - this.scene_settings_values_, - this.get_weather_settings_from_weather_(this.get_weather_()) - ); }; /** @@ -1205,6 +1252,7 @@ beestat.component.card.three_d.prototype.decorate_scene_settings_panel_ = functi add_section_title('Weather'); add_number_setting(get_title_case_label('cloud_density'), 'cloud_density', 0, 2, 0.1); add_number_setting(get_title_case_label('cloud_darkness'), 'cloud_darkness', 0, 2, 0.1); + add_number_setting(get_title_case_label('fog_density'), 'fog_density', 0, 2, 0.1); add_number_setting(get_title_case_label('rain_density'), 'rain_density', 0, 2, 0.1); add_number_setting(get_title_case_label('snow_density'), 'snow_density', 0, 2, 0.1); add_number_setting(get_title_case_label('lightning_frequency'), 'lightning_frequency', 0, 2, 0.1); @@ -1960,7 +2008,7 @@ beestat.component.card.three_d.prototype.decorate_toolbar_ = function(parent) { tile.addEventListener('click', (e) => { e.stopPropagation(); this.set_weather_(mode.value); - this.apply_weather_setting_to_scene_(); + this.apply_weather_setting_to_scene_(true); this.weather_menu_open_ = false; this.decorate_toolbar_(); }); @@ -1981,7 +2029,7 @@ beestat.component.card.three_d.prototype.decorate_toolbar_ = function(parent) { auto_tile.addEventListener('click', (e) => { e.stopPropagation(); this.set_weather_('auto'); - this.apply_weather_setting_to_scene_(); + this.apply_weather_setting_to_scene_(true); this.weather_menu_open_ = false; this.decorate_toolbar_(); }); diff --git a/js/component/scene.js b/js/component/scene.js index bfd803e..7bdee37 100644 --- a/js/component/scene.js +++ b/js/component/scene.js @@ -90,6 +90,13 @@ beestat.component.scene.weather_snow_max_count = 1500; */ beestat.component.scene.weather_cloud_max_count = 140; +/** + * Maximum low-altitude fog volume sprite count at full fog intensity. + * + * @type {number} + */ +beestat.component.scene.weather_fog_max_count = 18; + /** * Time in seconds for weather effects to fully transition to a new mode. * @@ -222,16 +229,18 @@ beestat.component.scene.sidereal_day_seconds = 86164.0905; beestat.component.scene.star_drift_visual_factor = 0.12; /** - * Runtime scene settings exposed through the scene settings panel. + * Runtime scene settings used by environment rendering. * * @type {{ * cloud_density: number, * cloud_darkness: number, + * fog_density: number, * rain_density: number, * snow_density: number, * lightning_frequency: number, * wind_speed: number, * wind_direction: number, + * fog_color: string, * tree_wobble: boolean, * tree_enabled: boolean, * star_density: number, @@ -243,11 +252,13 @@ beestat.component.scene.star_drift_visual_factor = 0.12; beestat.component.scene.default_settings = { 'cloud_density': 1, 'cloud_darkness': 0, + 'fog_density': 0, 'rain_density': 1, 'snow_density': 1, 'lightning_frequency': 0, 'wind_speed': 0.4, 'wind_direction': 0, + 'fog_color': '#d6dde8', 'tree_wobble': true, 'tree_enabled': true, 'star_density': 1, @@ -465,6 +476,11 @@ beestat.component.scene.prototype.reset_runtime_scene_references_for_rerender_ = delete this.snow_particles_; delete this.cloud_sprites_; delete this.cloud_motion_; + delete this.fog_sprites_; + delete this.fog_motion_; + delete this.fog_bounds_; + delete this.current_fog_count_; + delete this.current_fog_density_; delete this.weather_profile_target_; delete this.weather_transition_start_profile_; delete this.lightning_flash_light_; @@ -1346,6 +1362,9 @@ beestat.component.scene.prototype.dispose = function() { if (this.cloud_texture_ !== undefined) { this.cloud_texture_.dispose(); } + if (this.fog_volume_texture_ !== undefined) { + this.fog_volume_texture_.dispose(); + } if (this.moon_phase_texture_ !== undefined) { this.moon_phase_texture_.dispose(); } diff --git a/js/component/scene/environment.js b/js/component/scene/environment.js index 720ada8..b5d48e4 100644 --- a/js/component/scene/environment.js +++ b/js/component/scene/environment.js @@ -210,7 +210,8 @@ beestat.component.scene.prototype.add_environment_ = function() { if (index === 0) { mesh.userData.is_ground = true; } - mesh.receiveShadow = true; + mesh.castShadow = true; + mesh.receiveShadow = index === 0; this.environment_group_.add(mesh); current_z += stratum.thickness; diff --git a/js/component/scene/texture.js b/js/component/scene/texture.js index 1be7f56..ac4def8 100644 --- a/js/component/scene/texture.js +++ b/js/component/scene/texture.js @@ -251,6 +251,49 @@ beestat.component.scene.prototype.create_cloud_texture_ = function() { }; +/** + * Create a broad soft texture used for low-altitude fog volume sprites. + * + * @return {THREE.Texture} + */ +beestat.component.scene.prototype.create_fog_volume_texture_ = function() { + const size = 320; + const canvas = document.createElement('canvas'); + canvas.width = size; + canvas.height = size; + const context = canvas.getContext('2d'); + + const lobes = [ + {'x': 0.26, 'y': 0.56, 'r': 0.27, 'alpha': 0.5}, + {'x': 0.46, 'y': 0.5, 'r': 0.3, 'alpha': 0.6}, + {'x': 0.66, 'y': 0.57, 'r': 0.28, 'alpha': 0.54}, + {'x': 0.52, 'y': 0.67, 'r': 0.31, 'alpha': 0.42} + ]; + + lobes.forEach(function(lobe) { + const gradient = context.createRadialGradient( + size * lobe.x, + size * lobe.y, + 0, + size * lobe.x, + size * lobe.y, + size * lobe.r + ); + gradient.addColorStop(0.0, `rgba(255,255,255,${lobe.alpha})`); + gradient.addColorStop(0.55, `rgba(245,249,255,${lobe.alpha * 0.5})`); + gradient.addColorStop(1.0, 'rgba(245,249,255,0.0)'); + context.fillStyle = gradient; + context.beginPath(); + context.arc(size * lobe.x, size * lobe.y, size * lobe.r, 0, Math.PI * 2); + context.fill(); + }); + + const texture = new THREE.CanvasTexture(canvas); + texture.needsUpdate = true; + return texture; +}; + + /** * Draw the moon phase into the reusable moon canvas texture. * diff --git a/js/component/scene/weather.js b/js/component/scene/weather.js index bbd9251..81f92f6 100644 --- a/js/component/scene/weather.js +++ b/js/component/scene/weather.js @@ -18,6 +18,8 @@ beestat.component.scene.prototype.get_weather_design_count_ = function(density_k return Math.max(1, Number(beestat.component.scene.weather_rain_max_count || 1)); case 'snow_density': return Math.max(1, Number(beestat.component.scene.weather_snow_max_count || 1)); + case 'fog_density': + return Math.max(1, Number(beestat.component.scene.weather_fog_max_count || 1)); default: return 1; } @@ -69,6 +71,9 @@ beestat.component.scene.prototype.get_weather_count_from_density_ = function(den beestat.component.scene.prototype.get_weather_profile_ = function() { return { 'cloud_count': this.get_weather_count_from_density_('cloud_density'), + // Fog uses fixed sprite population; density controls opacity. + 'fog_count': this.get_weather_design_capacity_count_('fog_density'), + 'fog_density': Math.max(0, Math.min(2, Number(this.get_scene_setting_('fog_density') || 0))), 'rain_count': this.get_weather_count_from_density_('rain_density'), 'snow_count': this.get_weather_count_from_density_('snow_density') }; @@ -113,6 +118,22 @@ beestat.component.scene.prototype.get_cloud_color_ = function() { return base_color.lerp(dark_gray_color, blend); }; +/** + * Get low-altitude fog volume sprite tint color. + * + * @return {THREE.Color} + */ +beestat.component.scene.prototype.get_fog_color_ = function() { + const fog_color = this.get_scene_setting_('fog_color'); + if ( + typeof fog_color === 'string' || + typeof fog_color === 'number' + ) { + return new THREE.Color(fog_color); + } + return new THREE.Color(beestat.component.scene.default_settings.fog_color); +}; + /** * Update weather transition targets based on appearance weather. @@ -122,6 +143,8 @@ beestat.component.scene.prototype.update_weather_targets_ = function() { this.weather_transition_start_profile_ = { 'cloud_count': this.current_cloud_count_ === undefined ? 0 : this.current_cloud_count_, + 'fog_count': this.current_fog_count_ === undefined ? 0 : this.current_fog_count_, + 'fog_density': this.current_fog_density_ === undefined ? 0 : this.current_fog_density_, 'rain_count': this.current_rain_count_ === undefined ? 0 : this.current_rain_count_, 'snow_count': this.current_snow_count_ === undefined ? 0 : this.current_snow_count_ }; @@ -130,7 +153,7 @@ beestat.component.scene.prototype.update_weather_targets_ = function() { /** - * Get current snow cover blend amount (0-1) from precipitation transition. + * Get current snow cover blend amount (0-2) from precipitation transition. * * @return {number} */ @@ -146,7 +169,7 @@ beestat.component.scene.prototype.get_snow_cover_blend_ = function() { return Math.max( 0, Math.min( - 1, + 2, this.current_snow_count_ / configured_snow_count ) ); @@ -165,14 +188,16 @@ beestat.component.scene.prototype.update_snow_surface_colors_ = function(snow_bl // Keep a small amount of base color visible at peak snow for definition. const normalized_blend = Math.max(0, Math.min(1, snow_blend)); + const grass_normalized_blend = Math.max(0, Math.min(1, snow_blend * 0.5)); const blend = normalized_blend * 0.9; + const grass_blend = grass_normalized_blend * 0.9; const foliage_blend = normalized_blend * 0.75; const snow_color = new THREE.Color(beestat.component.scene.snow_surface_color); const base_roof_color = new THREE.Color(this.get_appearance_value_('roof_color')); const base_ground_color = new THREE.Color(this.get_appearance_value_('ground_color')); const roof_color = base_roof_color.clone().lerp(snow_color, blend); - const ground_color = base_ground_color.clone().lerp(snow_color, blend); + const ground_color = base_ground_color.clone().lerp(snow_color, grass_blend); if (this.layers_.roof !== undefined) { this.layers_.roof.traverse(function(object) { @@ -287,7 +312,8 @@ beestat.component.scene.prototype.create_precipitation_system_ = function(bounds 'static_opacity': config.static_opacity === true, 'max_wind_angle': config.max_wind_angle || 0, 'max_wind_speed_scale': config.max_wind_speed_scale || 2, - 'wind_motion_multiplier': config.wind_motion_multiplier || 1 + 'wind_motion_multiplier': config.wind_motion_multiplier || 1, + 'wind_speed_curve': config.wind_speed_curve || 'linear' }; }; @@ -357,13 +383,21 @@ beestat.component.scene.prototype.update_precipitation_system_ = function( ); const wind_speed_scale = 1 + ((clamped_wind_speed / 2) * (max_wind_speed_scale - 1)); const wind_motion_multiplier = Math.max(0, Number(precipitation.wind_motion_multiplier || 1)); + const wind_speed_curve_scale = precipitation.wind_speed_curve === 'half_same_double' + ? Math.pow(2, clamped_wind_speed - 1) + : 1; const direction_velocity_x = horizontal_scale * wind_x; const direction_velocity_y = horizontal_scale * wind_y; const direction_velocity_z = vertical_scale; for (let i = 0; i < clamped_count; i++) { const offset = i * 3; - const speed = precipitation.speeds[i] * delta_seconds * wind_speed_scale * wind_motion_multiplier; + const speed = + precipitation.speeds[i] * + delta_seconds * + wind_speed_scale * + wind_motion_multiplier * + wind_speed_curve_scale; positions[offset + 2] += speed * direction_velocity_z; positions[offset] += speed * direction_velocity_x; positions[offset + 1] += speed * direction_velocity_y; @@ -688,6 +722,9 @@ beestat.component.scene.prototype.add_weather_ = function(center_x, center_y, pl if (this.cloud_texture_ === undefined) { this.cloud_texture_ = this.create_cloud_texture_(); } + if (this.fog_volume_texture_ === undefined) { + this.fog_volume_texture_ = this.create_fog_volume_texture_(); + } if (this.snow_particle_texture_ === undefined) { this.snow_particle_texture_ = this.create_snow_particle_texture_(); @@ -761,6 +798,65 @@ beestat.component.scene.prototype.add_weather_ = function(center_x, center_y, pl }); } + const fog_capacity = this.get_weather_design_capacity_count_('fog_density', this.weather_area_); + const fog_bounds = { + 'min_x': bounds.min_x - 140, + 'max_x': bounds.max_x + 140, + 'min_y': bounds.min_y - 140, + 'max_y': bounds.max_y + 140, + // Keep fog cards well above the ground plane to avoid seam artifacts. + 'min_z': -420, + 'max_z': -120 + }; + + this.fog_bounds_ = fog_bounds; + this.fog_sprites_ = []; + this.fog_motion_ = []; + + // Pre-build low-altitude volumetric fog sprites. + for (let i = 0; i < fog_capacity; i++) { + const fog_material = new THREE.SpriteMaterial({ + 'map': this.fog_volume_texture_, + 'color': 0xd6dde8, + 'transparent': true, + 'opacity': 0, + 'depthWrite': false, + 'depthTest': true + }); + + const fog = new THREE.Sprite(fog_material); + fog.position.set( + fog_bounds.min_x + Math.random() * (fog_bounds.max_x - fog_bounds.min_x), + fog_bounds.min_y + Math.random() * (fog_bounds.max_y - fog_bounds.min_y), + fog_bounds.min_z + Math.random() * (fog_bounds.max_z - fog_bounds.min_z) + ); + const fog_size = 680 + Math.random() * 980; + fog.scale.set(fog_size * 1.7, fog_size * 0.45, 1); + fog.layers.set(beestat.component.scene.layer_visible); + fog.userData.is_environment = true; + this.weather_group_.add(fog); + this.fog_sprites_.push(fog); + this.fog_motion_.push({ + 'base_x': fog.position.x, + 'base_y': fog.position.y, + 'base_z': fog.position.z, + 'base_scale_x': fog.scale.x, + 'base_scale_y': fog.scale.y, + 'base_opacity': 0.34 + (Math.random() * 0.12), + 'phase': Math.random() * Math.PI * 2, + 'pulse_speed': 0.12 + (Math.random() * 0.1), + 'scale_wobble_x': 0.03 + (Math.random() * 0.02), + 'scale_wobble_y': 0.02 + (Math.random() * 0.02), + 'opacity_wobble': 0.04 + (Math.random() * 0.03), + 'wiggle_x': 18 + (Math.random() * 26), + 'wiggle_y': 16 + (Math.random() * 24), + 'wiggle_z': 4 + (Math.random() * 8), + 'wiggle_freq_x': 0.55 + (Math.random() * 0.4), + 'wiggle_freq_y': 0.45 + (Math.random() * 0.35), + 'wiggle_freq_z': 0.35 + (Math.random() * 0.3) + }); + } + // Build precipitation systems (rain and snow) at design-capacity scale. this.rain_particles_ = this.create_precipitation_system_( bounds, @@ -773,8 +869,8 @@ beestat.component.scene.prototype.add_weather_ = function(center_x, center_y, pl 'color': 0xa8c7ff, 'opacity': 0.5, 'static_opacity': true, - 'speed_min': 280, - 'speed_max': 430, + 'speed_min': 560, + 'speed_max': 860, 'drift': 28, 'texture': this.rain_particle_texture_, 'max_wind_angle': 45, @@ -799,7 +895,8 @@ beestat.component.scene.prototype.add_weather_ = function(center_x, center_y, pl 'texture': this.snow_particle_texture_, 'max_wind_angle': 75, 'max_wind_speed_scale': 3, - 'wind_motion_multiplier': 2.5 + 'wind_motion_multiplier': 2.5, + 'wind_speed_curve': 'half_same_double' } ); this.weather_group_.add(this.snow_particles_.points); @@ -816,6 +913,8 @@ beestat.component.scene.prototype.add_weather_ = function(center_x, center_y, pl const initial_weather_profile = this.get_weather_profile_(); this.weather_profile_target_ = initial_weather_profile; this.current_cloud_count_ = initial_weather_profile.cloud_count; + this.current_fog_count_ = initial_weather_profile.fog_count; + this.current_fog_density_ = initial_weather_profile.fog_density; this.current_rain_count_ = initial_weather_profile.rain_count; this.current_snow_count_ = initial_weather_profile.snow_count; this.update_weather_targets_(); @@ -852,6 +951,8 @@ beestat.component.scene.prototype.update_weather_ = function() { if (this.weather_transition_start_profile_ === undefined) { this.weather_transition_start_profile_ = { 'cloud_count': this.current_cloud_count_ === undefined ? 0 : this.current_cloud_count_, + 'fog_count': this.current_fog_count_ === undefined ? 0 : this.current_fog_count_, + 'fog_density': this.current_fog_density_ === undefined ? 0 : this.current_fog_density_, 'rain_count': this.current_rain_count_ === undefined ? 0 : this.current_rain_count_, 'snow_count': this.current_snow_count_ === undefined ? 0 : this.current_snow_count_ }; @@ -880,6 +981,14 @@ beestat.component.scene.prototype.update_weather_ = function() { this.weather_transition_start_profile_.cloud_count, this.weather_profile_target_.cloud_count ); + this.current_fog_count_ = transition( + this.weather_transition_start_profile_.fog_count, + this.weather_profile_target_.fog_count + ); + this.current_fog_density_ = transition( + this.weather_transition_start_profile_.fog_density, + this.weather_profile_target_.fog_density + ); this.current_rain_count_ = transition( this.weather_transition_start_profile_.rain_count, this.weather_profile_target_.rain_count @@ -940,6 +1049,51 @@ beestat.component.scene.prototype.update_weather_ = function() { } } + // Update low-altitude volumetric fog sprites. + if (this.fog_sprites_ !== undefined && this.fog_motion_ !== undefined) { + const now_seconds = now_ms / 1000; + const fog_color = this.get_fog_color_(); + const fog_density = Math.max( + 0, + Math.min( + 1, + (this.current_fog_density_ === undefined ? 0 : this.current_fog_density_) / 2 + ) + ); + + for (let i = 0; i < this.fog_sprites_.length; i++) { + const sprite = this.fog_sprites_[i]; + const motion = this.fog_motion_[i]; + const phase = now_seconds * motion.pulse_speed + motion.phase; + + const scale_x_wobble = 1 + (Math.sin(phase) * motion.scale_wobble_x); + const scale_y_wobble = 1 + (Math.cos(phase * 0.88) * motion.scale_wobble_y); + const fog_scale_transition = 0.62 + (0.38 * fog_density); + sprite.scale.set( + motion.base_scale_x * scale_x_wobble * fog_scale_transition, + motion.base_scale_y * scale_y_wobble * fog_scale_transition, + 1 + ); + + sprite.position.x = motion.base_x + Math.sin(phase * motion.wiggle_freq_x) * motion.wiggle_x; + sprite.position.y = motion.base_y + Math.cos(phase * motion.wiggle_freq_y) * motion.wiggle_y; + sprite.position.z = motion.base_z + Math.sin(phase * motion.wiggle_freq_z) * motion.wiggle_z; + + if (sprite.material !== undefined) { + if (sprite.material.color !== undefined) { + sprite.material.color.copy(fog_color); + } + sprite.material.opacity = Math.max( + 0, + Math.min( + 1, + (motion.base_opacity + Math.sin(phase * 0.61) * motion.opacity_wobble) * fog_density + ) + ); + } + } + } + // Update precipitation + lightning systems, then re-apply snow cover tinting. this.update_precipitation_system_( this.rain_particles_,