mirror of
https://github.com/beestat/app.git
synced 2026-02-26 13:10:23 -05:00
794 lines
28 KiB
JavaScript
794 lines
28 KiB
JavaScript
/**
|
|
* Scene methods split from scene.js.
|
|
*/
|
|
|
|
|
|
/**
|
|
* Add multiple directional lights from different angles to create definition
|
|
* and depth without harsh shadows. This three-point lighting setup gives
|
|
* surfaces varied illumination for better visual depth.
|
|
*/
|
|
beestat.component.scene.prototype.add_directional_lights_ = function() {
|
|
// Prevent re-initialization if lights already exist
|
|
if (this.directional_lights_ !== undefined) {
|
|
return;
|
|
}
|
|
|
|
this.directional_lights_ = [];
|
|
|
|
// Key light: Main light from upper front-right (strongest)
|
|
const key_light = new THREE.DirectionalLight(0xffffff, beestat.component.scene.directional_light_intensity);
|
|
key_light.position.set(2000, 3200, 2000);
|
|
this.static_light_group_.add(key_light);
|
|
this.directional_lights_.push(key_light);
|
|
|
|
// Fill light: Softer light from upper front-left (balances key light)
|
|
const fill_light = new THREE.DirectionalLight(0xffffff, beestat.component.scene.directional_light_intensity);
|
|
fill_light.position.set(-2000, 2400, 2000);
|
|
this.static_light_group_.add(fill_light);
|
|
this.directional_lights_.push(fill_light);
|
|
|
|
// Back light: Mild light from behind (creates rim lighting on edges)
|
|
const back_light = new THREE.DirectionalLight(0xffffff, beestat.component.scene.directional_light_intensity);
|
|
back_light.position.set(0, 2000, -2000);
|
|
this.static_light_group_.add(back_light);
|
|
this.directional_lights_.push(back_light);
|
|
|
|
// Top light: Gentle overhead light for roof definition
|
|
const top_light = new THREE.DirectionalLight(0xffffff, beestat.component.scene.directional_light_intensity);
|
|
top_light.position.set(0, 4000, 0);
|
|
this.static_light_group_.add(top_light);
|
|
this.directional_lights_.push(top_light);
|
|
|
|
};
|
|
|
|
|
|
/**
|
|
* Create static lights group containing ambient and directional fill lights.
|
|
* These lights are always on and provide base illumination.
|
|
*/
|
|
beestat.component.scene.prototype.add_static_lights_ = function() {
|
|
// Prevent re-initialization
|
|
if (this.static_light_group_ !== undefined) {
|
|
return;
|
|
}
|
|
|
|
// Initialize layers object if not already done
|
|
if (this.layers_ === undefined) {
|
|
this.layers_ = {};
|
|
}
|
|
|
|
this.static_light_group_ = new THREE.Group();
|
|
this.scene_.add(this.static_light_group_);
|
|
this.layers_['static_lights'] = this.static_light_group_;
|
|
|
|
// Add ambient light
|
|
this.ambient_light_ = new THREE.AmbientLight(
|
|
0xffffff,
|
|
beestat.component.scene.ambient_light_intensity
|
|
);
|
|
this.static_light_group_.add(this.ambient_light_);
|
|
|
|
// Add directional fill lights
|
|
this.add_directional_lights_();
|
|
};
|
|
|
|
|
|
/**
|
|
* Directional sun and moon lights that provide natural lighting. Only
|
|
* visible when the environment layer is enabled. Positions are calculated based
|
|
* on time of day and location.
|
|
*/
|
|
beestat.component.scene.prototype.get_celestial_shadow_frustum_extent_ = function() {
|
|
const bounding_box = this.get_scene_bounding_box_();
|
|
const plan_width = Math.max(1, Number(bounding_box.right - bounding_box.left));
|
|
const plan_height = Math.max(1, Number(bounding_box.bottom - bounding_box.top));
|
|
const half_span = Math.max(plan_width, plan_height) / 2;
|
|
const environment_padding = Math.max(0, beestat.component.scene.environment_padding || 0);
|
|
const caster_margin = 420;
|
|
// Extra scale keeps long low-angle shadows inside the ortho shadow camera.
|
|
return Math.max(1000, (half_span + environment_padding + caster_margin) * 1.45);
|
|
};
|
|
|
|
|
|
/**
|
|
* Configure a directional light shadow camera to cover the current scene size.
|
|
*
|
|
* @param {THREE.DirectionalLight} light
|
|
*/
|
|
beestat.component.scene.prototype.configure_celestial_shadow_camera_ = function(light) {
|
|
const extent = this.get_celestial_shadow_frustum_extent_();
|
|
light.shadow.camera.left = -extent;
|
|
light.shadow.camera.right = extent;
|
|
light.shadow.camera.top = extent;
|
|
light.shadow.camera.bottom = -extent;
|
|
light.shadow.camera.near = 0.5;
|
|
light.shadow.camera.far = Math.max(5000, extent * 6);
|
|
light.shadow.camera.updateProjectionMatrix();
|
|
};
|
|
|
|
|
|
/**
|
|
* Directional sun and moon lights that provide natural lighting. Only
|
|
* visible when the environment layer is enabled. Positions are calculated based
|
|
* on time of day and location.
|
|
*/
|
|
beestat.component.scene.prototype.add_celestial_lights_ = function() {
|
|
// Prevent re-initialization if lights already exist
|
|
if (this.sun_light_ !== undefined) {
|
|
return;
|
|
}
|
|
|
|
// Create celestial group if it doesn't exist
|
|
if (this.celestial_light_group_ === undefined) {
|
|
this.celestial_light_group_ = new THREE.Group();
|
|
this.scene_.add(this.celestial_light_group_);
|
|
this.layers_['celestial'] = this.celestial_light_group_;
|
|
}
|
|
|
|
// Sun light
|
|
this.sun_light_ = new THREE.DirectionalLight(
|
|
0xffffdd, // Slightly warm color for sunlight
|
|
beestat.component.scene.sun_light_intensity
|
|
);
|
|
|
|
// Initial position (will be updated by update_celestial_lights_)
|
|
this.sun_light_.position.set(500, 500, -500);
|
|
|
|
// Enable shadow casting
|
|
this.sun_light_.castShadow = true;
|
|
this.sun_light_.shadow.mapSize.set(2048, 2048);
|
|
this.sun_light_.shadow.bias = -0.001;
|
|
|
|
// Configure shadow camera frustum based on scene size.
|
|
this.configure_celestial_shadow_camera_(this.sun_light_);
|
|
|
|
// Set target to world origin (0,0,0) so light always points there
|
|
this.sun_light_.target.position.set(0, 0, 0);
|
|
this.scene_.add(this.sun_light_.target);
|
|
|
|
this.celestial_light_group_.add(this.sun_light_);
|
|
|
|
// Faint arc showing the sun's path across the sky.
|
|
this.sun_path_line_ = new THREE.Line(
|
|
new THREE.BufferGeometry(),
|
|
new THREE.LineBasicMaterial({
|
|
'color': 0xffe7aa,
|
|
'vertexColors': true,
|
|
'transparent': true,
|
|
'opacity': 0.14,
|
|
'depthWrite': false
|
|
})
|
|
);
|
|
this.sun_path_line_.layers.set(beestat.component.scene.layer_visible);
|
|
this.celestial_light_group_.add(this.sun_path_line_);
|
|
|
|
// Visible sun body and glow
|
|
this.sun_visual_group_ = new THREE.Group();
|
|
this.sun_visual_group_.layers.set(beestat.component.scene.layer_visible);
|
|
this.celestial_light_group_.add(this.sun_visual_group_);
|
|
|
|
this.sun_core_texture_ = this.create_sun_core_texture_();
|
|
const sun_core_material = new THREE.SpriteMaterial({
|
|
'map': this.sun_core_texture_,
|
|
'color': 0xffffff,
|
|
'transparent': true,
|
|
'depthWrite': false,
|
|
'depthTest': true,
|
|
'opacity': 1
|
|
});
|
|
this.sun_core_mesh_ = new THREE.Sprite(sun_core_material);
|
|
this.sun_core_mesh_.userData.is_celestial_object = true;
|
|
this.sun_core_mesh_.scale.set(320, 320, 1);
|
|
this.sun_core_mesh_.renderOrder = 12;
|
|
this.sun_visual_group_.add(this.sun_core_mesh_);
|
|
|
|
this.sun_glow_texture_ = this.create_sun_glow_texture_();
|
|
const sun_glow_material = new THREE.SpriteMaterial({
|
|
'map': this.sun_glow_texture_,
|
|
'color': 0xfff0b0,
|
|
'transparent': true,
|
|
'blending': THREE.AdditiveBlending,
|
|
'depthWrite': false,
|
|
'depthTest': true,
|
|
'opacity': 1
|
|
});
|
|
this.sun_glow_sprite_ = new THREE.Sprite(sun_glow_material);
|
|
this.sun_glow_sprite_.userData.is_celestial_object = true;
|
|
this.sun_glow_sprite_.scale.set(1037, 1037, 1);
|
|
this.sun_glow_sprite_.renderOrder = 11;
|
|
this.sun_visual_group_.add(this.sun_glow_sprite_);
|
|
|
|
// Moon light
|
|
this.moon_light_ = new THREE.DirectionalLight(
|
|
0xaaccff, // Cool bluish color for moonlight
|
|
beestat.component.scene.moon_light_intensity
|
|
);
|
|
|
|
// Initial position (will be updated by update_celestial_lights_)
|
|
this.moon_light_.position.set(-500, 500, 500);
|
|
|
|
// Enable shadow casting
|
|
this.moon_light_.castShadow = true;
|
|
this.moon_light_.shadow.mapSize.set(2048, 2048);
|
|
this.moon_light_.shadow.bias = -0.001;
|
|
|
|
// Configure shadow camera frustum based on scene size.
|
|
this.configure_celestial_shadow_camera_(this.moon_light_);
|
|
|
|
// Set target to world origin
|
|
this.moon_light_.target.position.set(0, 0, 0);
|
|
this.scene_.add(this.moon_light_.target);
|
|
|
|
this.celestial_light_group_.add(this.moon_light_);
|
|
|
|
// Visible moon disk with procedural phase texture.
|
|
this.moon_visual_group_ = new THREE.Group();
|
|
this.moon_visual_group_.layers.set(beestat.component.scene.layer_visible);
|
|
this.celestial_light_group_.add(this.moon_visual_group_);
|
|
|
|
this.update_moon_phase_texture_(0);
|
|
const moon_material = new THREE.SpriteMaterial({
|
|
'map': this.moon_phase_texture_,
|
|
'transparent': true,
|
|
'depthWrite': false,
|
|
'depthTest': true,
|
|
'opacity': 1
|
|
});
|
|
this.moon_sprite_ = new THREE.Sprite(moon_material);
|
|
this.moon_sprite_.userData.is_celestial_object = true;
|
|
this.moon_sprite_.scale.set(405, 405, 1);
|
|
this.moon_visual_group_.add(this.moon_sprite_);
|
|
|
|
this.add_stars_();
|
|
};
|
|
|
|
|
|
/**
|
|
* Add stars as non-lighting visual sprites in the sky.
|
|
*/
|
|
beestat.component.scene.prototype.add_stars_ = function() {
|
|
if (this.star_texture_ === undefined) {
|
|
this.star_texture_ = this.create_star_texture_();
|
|
}
|
|
|
|
this.star_group_ = new THREE.Group();
|
|
this.star_group_.layers.set(beestat.component.scene.layer_visible);
|
|
this.celestial_light_group_.add(this.star_group_);
|
|
|
|
this.stars_ = [];
|
|
|
|
const radius = 4200;
|
|
const star_density = Math.max(0, Number(this.get_scene_setting_('star_density') || 0));
|
|
const star_count = Math.max(0, Math.round(1000 * star_density));
|
|
for (let i = 0; i < star_count; i++) {
|
|
const theta = Math.random() * Math.PI * 2;
|
|
const phi = Math.acos((Math.random() * 2) - 1);
|
|
|
|
const x = radius * Math.sin(phi) * Math.cos(theta);
|
|
const y = radius * Math.cos(phi);
|
|
const z = radius * Math.sin(phi) * Math.sin(theta);
|
|
|
|
const size =
|
|
beestat.component.scene.star_min_size +
|
|
(Math.pow(Math.random(), 1.7) * (beestat.component.scene.star_max_size - beestat.component.scene.star_min_size));
|
|
const is_twinkle = size >= 20;
|
|
const base_opacity = 0.3 + (Math.random() * 0.7);
|
|
|
|
const material = new THREE.SpriteMaterial({
|
|
'map': this.star_texture_,
|
|
'transparent': true,
|
|
'opacity': 0,
|
|
'depthWrite': false,
|
|
'depthTest': true,
|
|
'blending': THREE.AdditiveBlending
|
|
});
|
|
const star = new THREE.Sprite(material);
|
|
star.position.set(x, y, z);
|
|
star.scale.set(size, size, 1);
|
|
star.userData.is_celestial_object = true;
|
|
this.star_group_.add(star);
|
|
|
|
this.stars_.push({
|
|
'sprite': star,
|
|
'base_opacity': base_opacity,
|
|
'twinkle': is_twinkle,
|
|
'twinkle_amount': is_twinkle ? (0.06 + (Math.random() * 0.1)) : 0,
|
|
'twinkle_speed': is_twinkle ? (0.8 + (Math.random() * 1.2)) : 0,
|
|
'phase': Math.random() * Math.PI * 2
|
|
});
|
|
}
|
|
|
|
this.star_visibility_ = 0;
|
|
this.target_star_visibility_ = 0;
|
|
};
|
|
|
|
|
|
/**
|
|
* Update a faint line representing the sun's path for the current date and
|
|
* location.
|
|
*
|
|
* @param {moment} date
|
|
* @param {number} latitude
|
|
* @param {number} longitude
|
|
*/
|
|
beestat.component.scene.prototype.update_sun_path_arc_ = function(date, latitude, longitude) {
|
|
if (this.sun_path_line_ === undefined) {
|
|
return;
|
|
}
|
|
|
|
const rotation_radians = (this.get_appearance_value_('rotation') * Math.PI) / 180;
|
|
const sun_distance = 4000;
|
|
const start_of_day = date.clone().startOf('day');
|
|
const end_of_day = start_of_day.clone().add(1, 'day');
|
|
const start_ms = start_of_day.valueOf();
|
|
const end_ms = end_of_day.valueOf();
|
|
const sample_count = 72;
|
|
const points = [];
|
|
|
|
for (let i = 0; i < sample_count; i++) {
|
|
const t = i / (sample_count - 1);
|
|
const sample_date = new Date(start_ms + (end_ms - start_ms) * t);
|
|
|
|
const sun_pos = SunCalc.getPosition(sample_date, latitude, longitude);
|
|
// Convert SunCalc azimuth (south-origin, west-positive) to a north-origin,
|
|
// clockwise bearing, then apply floor-plan north rotation.
|
|
const rotated_azimuth = sun_pos.azimuth + Math.PI + rotation_radians;
|
|
|
|
// Extend slightly below horizon so the path doesn't hard-stop at horizon.
|
|
if (sun_pos.altitude > -0.22) {
|
|
points.push(new THREE.Vector3(
|
|
sun_distance * Math.cos(sun_pos.altitude) * Math.sin(rotated_azimuth),
|
|
sun_distance * Math.sin(sun_pos.altitude),
|
|
-sun_distance * Math.cos(sun_pos.altitude) * Math.cos(rotated_azimuth)
|
|
));
|
|
}
|
|
}
|
|
|
|
if (points.length >= 2) {
|
|
const geometry = this.sun_path_line_.geometry;
|
|
geometry.setFromPoints(points);
|
|
|
|
// Fade the ends by dimming vertex colors toward each endpoint.
|
|
const colors = [];
|
|
const base = new THREE.Color(0xffe7aa);
|
|
for (let i = 0; i < points.length; i++) {
|
|
const t = i / (points.length - 1);
|
|
const center_weight = Math.pow(Math.sin(Math.PI * t), 1.2);
|
|
const intensity = 0.2 + (0.8 * center_weight);
|
|
const color = base.clone().multiplyScalar(intensity);
|
|
colors.push(color.r, color.g, color.b);
|
|
}
|
|
geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
|
|
|
|
this.sun_path_line_.visible = true;
|
|
} else {
|
|
this.sun_path_line_.geometry.setFromPoints([]);
|
|
this.sun_path_line_.visible = false;
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Build target sun colors from altitude.
|
|
* Warmer near horizon and more neutral when the sun is high.
|
|
*
|
|
* @param {number} sun_altitude_radians
|
|
*
|
|
* @return {{light_color: THREE.Color, core_color: THREE.Color, glow_color: THREE.Color}}
|
|
*/
|
|
beestat.component.scene.prototype.get_sun_color_profile_ = function(sun_altitude_radians) {
|
|
const warm_start_altitude = THREE.MathUtils.degToRad(-2);
|
|
const warm_end_altitude = THREE.MathUtils.degToRad(26);
|
|
const daylight_t = Math.max(
|
|
0,
|
|
Math.min(
|
|
1,
|
|
(sun_altitude_radians - warm_start_altitude) / Math.max(0.0001, warm_end_altitude - warm_start_altitude)
|
|
)
|
|
);
|
|
const warmth = 1 - daylight_t;
|
|
|
|
const light_day = new THREE.Color(0xfff5de);
|
|
const light_horizon = new THREE.Color(0xffa66c);
|
|
const core_day = new THREE.Color(0xffffff);
|
|
const core_horizon = new THREE.Color(0xffcf98);
|
|
const glow_day = new THREE.Color(0xfff0b0);
|
|
const glow_horizon = new THREE.Color(0xff8a4f);
|
|
|
|
return {
|
|
'light_color': light_day.clone().lerp(light_horizon, warmth),
|
|
'core_color': core_day.clone().lerp(core_horizon, warmth * 0.9),
|
|
'glow_color': glow_day.clone().lerp(glow_horizon, warmth)
|
|
};
|
|
};
|
|
|
|
|
|
/**
|
|
* Update sun and moon light positions based on date and location using SunCalc.
|
|
* Adjusts light intensities based on altitude and moon phase.
|
|
*
|
|
* @param {moment} date The date/time to calculate positions for
|
|
* @param {number} latitude Location latitude
|
|
* @param {number} longitude Location longitude
|
|
*
|
|
* @link https://www.earthspacelab.com/app/solar-time/
|
|
*/
|
|
beestat.component.scene.prototype.update_celestial_lights_ = function(date, latitude, longitude) {
|
|
const sun_distance = 4000;
|
|
const moon_distance = 3920;
|
|
const js_date = date.toDate();
|
|
const rotation_radians = (this.get_appearance_value_('rotation') * Math.PI) / 180;
|
|
|
|
// Sun
|
|
const sun_pos = SunCalc.getPosition(js_date, latitude, longitude);
|
|
// Convert SunCalc azimuth (south-origin, west-positive) to a north-origin,
|
|
// clockwise bearing, then apply floor-plan north rotation.
|
|
const rotated_sun_azimuth = sun_pos.azimuth + Math.PI + rotation_radians;
|
|
this.sun_light_.position.set(
|
|
sun_distance * Math.cos(sun_pos.altitude) * Math.sin(rotated_sun_azimuth), // East-West
|
|
sun_distance * Math.sin(sun_pos.altitude), // Up-Down (altitude)
|
|
-sun_distance * Math.cos(sun_pos.altitude) * Math.cos(rotated_sun_azimuth) // North-South
|
|
);
|
|
|
|
if (this.sun_visual_group_ !== undefined) {
|
|
this.sun_visual_group_.position.copy(this.sun_light_.position);
|
|
this.sun_visual_group_.visible = true;
|
|
this.sun_visual_horizon_fade_ = Math.max(0, Math.min(1, (sun_pos.altitude + 0.15) / 0.3));
|
|
}
|
|
const sun_color_profile = this.get_sun_color_profile_(sun_pos.altitude);
|
|
this.target_sun_light_color_ = sun_color_profile.light_color;
|
|
this.target_sun_core_color_ = sun_color_profile.core_color;
|
|
this.target_sun_glow_color_ = sun_color_profile.glow_color;
|
|
|
|
const cloud_dimming = this.get_cloud_dimming_factor_();
|
|
|
|
// Calculate target intensity for smooth transitions.
|
|
// Keep the transition tight around the horizon so sunrise "pops in" with
|
|
// the same quick behavior as sunset "drops out".
|
|
const sun_transition_start_altitude = -0.015;
|
|
const sun_transition_end_altitude = 0.075;
|
|
const sun_horizon_visibility = Math.max(
|
|
0,
|
|
Math.min(
|
|
1,
|
|
(sun_pos.altitude - sun_transition_start_altitude) /
|
|
Math.max(0.0001, sun_transition_end_altitude - sun_transition_start_altitude)
|
|
)
|
|
);
|
|
const sun_intensity_factor = Math.pow(sun_horizon_visibility, 2.4);
|
|
this.target_sun_intensity_ =
|
|
beestat.component.scene.sun_light_intensity * sun_intensity_factor;
|
|
this.target_sun_intensity_ *= cloud_dimming;
|
|
|
|
// Fade stars out at day and in at night.
|
|
this.target_star_visibility_ = Math.max(
|
|
0,
|
|
Math.min(1, (-sun_pos.altitude - 0.05) / 0.25)
|
|
);
|
|
|
|
const max_sun_intensity = Math.max(0.0001, Number(beestat.component.scene.sun_light_intensity || 0.0001));
|
|
const normalized_sun_brightness = Math.max(
|
|
0,
|
|
Math.min(1, this.target_sun_intensity_ / max_sun_intensity)
|
|
);
|
|
const user_light_on_brightness_threshold = 0.34;
|
|
const user_light_off_brightness_threshold = 0.42;
|
|
if (this.user_lights_on_ === undefined) {
|
|
this.user_lights_on_ = normalized_sun_brightness <= user_light_on_brightness_threshold;
|
|
}
|
|
if (this.user_lights_on_ === true) {
|
|
if (normalized_sun_brightness >= user_light_off_brightness_threshold) {
|
|
this.user_lights_on_ = false;
|
|
}
|
|
} else if (normalized_sun_brightness <= user_light_on_brightness_threshold) {
|
|
this.user_lights_on_ = true;
|
|
}
|
|
this.target_light_source_intensity_multiplier_ = this.user_lights_on_ === true ? 1 : 0;
|
|
|
|
// Moon
|
|
const moon_pos = SunCalc.getMoonPosition(js_date, latitude, longitude);
|
|
// Keep moon conversion consistent with the sun conversion.
|
|
const rotated_moon_azimuth = moon_pos.azimuth + Math.PI + rotation_radians;
|
|
const moon_illumination = SunCalc.getMoonIllumination(js_date);
|
|
const moon_fraction = moon_illumination.fraction;
|
|
const moon_phase = moon_illumination.phase;
|
|
this.current_moon_fraction_ = moon_fraction;
|
|
this.moon_light_.position.set(
|
|
moon_distance * Math.cos(moon_pos.altitude) * Math.sin(rotated_moon_azimuth), // East-West
|
|
moon_distance * Math.sin(moon_pos.altitude), // Up-Down (altitude)
|
|
-moon_distance * Math.cos(moon_pos.altitude) * Math.cos(rotated_moon_azimuth) // North-South
|
|
);
|
|
if (this.moon_visual_group_ !== undefined) {
|
|
let moon_front_direction;
|
|
if (this.camera_ !== undefined) {
|
|
moon_front_direction = this.camera_.position.clone().sub(this.moon_light_.position).normalize();
|
|
} else {
|
|
moon_front_direction = this.moon_light_.position.clone().normalize().negate();
|
|
}
|
|
this.moon_visual_group_.position.copy(
|
|
this.moon_light_.position.clone().add(moon_front_direction.multiplyScalar(20))
|
|
);
|
|
this.moon_visual_group_.visible = true;
|
|
this.moon_visual_horizon_fade_ = Math.max(0, Math.min(1, (moon_pos.altitude + 0.12) / 0.24));
|
|
|
|
if (this.last_moon_phase_ === undefined || Math.abs(this.last_moon_phase_ - moon_phase) > 0.002) {
|
|
this.last_moon_phase_ = moon_phase;
|
|
this.update_moon_phase_texture_(moon_phase);
|
|
}
|
|
}
|
|
const moon_intensity = beestat.component.scene.moon_light_intensity * moon_fraction;
|
|
|
|
this.update_sun_path_arc_(date, latitude, longitude);
|
|
|
|
// Calculate target intensity for smooth transitions
|
|
// Moon is only visible when sun is below horizon
|
|
if (sun_pos.altitude >= 0) {
|
|
this.target_moon_intensity_ = 0;
|
|
} else {
|
|
this.target_moon_intensity_ = moon_pos.altitude < 0
|
|
? Math.max(0, moon_intensity * (1 + moon_pos.altitude / (Math.PI / 6)))
|
|
: moon_intensity;
|
|
}
|
|
this.target_moon_intensity_ *= cloud_dimming;
|
|
};
|
|
|
|
|
|
/**
|
|
* Smoothly interpolate celestial light intensities towards their targets.
|
|
* Called every frame to create smooth transitions instead of instant jumps.
|
|
*/
|
|
beestat.component.scene.prototype.update_celestial_light_intensities_ = function() {
|
|
if (this.sun_light_ === undefined || this.moon_light_ === undefined) {
|
|
return;
|
|
}
|
|
|
|
// Initialize current intensities if not set
|
|
if (this.target_sun_intensity_ === undefined) {
|
|
this.target_sun_intensity_ = 0;
|
|
}
|
|
if (this.target_moon_intensity_ === undefined) {
|
|
this.target_moon_intensity_ = 0;
|
|
}
|
|
if (this.target_light_source_intensity_multiplier_ === undefined) {
|
|
const hour = this.date_ !== undefined ? Number(this.date_.format('H')) : 12;
|
|
this.target_light_source_intensity_multiplier_ = (hour >= 19 || hour <= 5) ? 1 : 0;
|
|
}
|
|
if (this.target_sun_light_color_ === undefined) {
|
|
this.target_sun_light_color_ = new THREE.Color(0xffffdd);
|
|
}
|
|
if (this.target_sun_core_color_ === undefined) {
|
|
this.target_sun_core_color_ = new THREE.Color(0xffffff);
|
|
}
|
|
if (this.target_sun_glow_color_ === undefined) {
|
|
this.target_sun_glow_color_ = new THREE.Color(0xfff0b0);
|
|
}
|
|
|
|
// Lerp factor - lower = smoother but slower, higher = faster but jumpier
|
|
const lerp_factor = 0.05;
|
|
|
|
// Lerp sun intensity
|
|
this.sun_light_.intensity += (this.target_sun_intensity_ - this.sun_light_.intensity) * lerp_factor;
|
|
|
|
// Lerp moon intensity
|
|
this.moon_light_.intensity += (this.target_moon_intensity_ - this.moon_light_.intensity) * lerp_factor;
|
|
const color_lerp_factor = 0.08;
|
|
this.sun_light_.color.lerp(this.target_sun_light_color_, color_lerp_factor);
|
|
|
|
if (Array.isArray(this.light_sources_) === true) {
|
|
this.light_sources_.forEach((light) => {
|
|
const base_intensity = Number(light.userData.base_intensity || 0);
|
|
const target_intensity = base_intensity * this.target_light_source_intensity_multiplier_;
|
|
light.intensity += (target_intensity - light.intensity) * lerp_factor;
|
|
});
|
|
}
|
|
// Match visible sun brightness to actual sun light intensity, with smooth
|
|
// fade at/under the horizon.
|
|
if (this.sun_core_mesh_ !== undefined && this.sun_glow_sprite_ !== undefined) {
|
|
const max_sun_intensity = beestat.component.scene.sun_light_intensity;
|
|
const intensity_ratio = max_sun_intensity > 0
|
|
? Math.max(0, Math.min(1, this.sun_light_.intensity / max_sun_intensity))
|
|
: 0;
|
|
const horizon_fade = this.sun_visual_horizon_fade_ !== undefined
|
|
? this.sun_visual_horizon_fade_
|
|
: 1;
|
|
const visual_strength = intensity_ratio * horizon_fade;
|
|
|
|
this.sun_core_mesh_.material.opacity = Math.min(1, (0.65 + visual_strength * 0.8) * visual_strength);
|
|
this.sun_glow_sprite_.material.opacity = Math.min(1, (0.45 + visual_strength * 1.4) * visual_strength);
|
|
this.sun_core_mesh_.material.color.lerp(this.target_sun_core_color_, color_lerp_factor);
|
|
this.sun_glow_sprite_.material.color.lerp(this.target_sun_glow_color_, color_lerp_factor);
|
|
}
|
|
|
|
if (this.moon_sprite_ !== undefined) {
|
|
// Keep moon visible at all times. This visual opacity is phase-driven and
|
|
// intentionally decoupled from moon light intensity and horizon fade.
|
|
const moon_phase_visibility = Math.max(
|
|
0.25,
|
|
Math.min(1, Number(this.current_moon_fraction_ === undefined ? 1 : this.current_moon_fraction_))
|
|
);
|
|
this.moon_sprite_.material.opacity = Math.min(1, 0.08 + (moon_phase_visibility * 0.28));
|
|
if (this.moon_visual_group_ !== undefined) {
|
|
this.moon_visual_group_.visible = true;
|
|
}
|
|
}
|
|
|
|
this.update_stars_();
|
|
};
|
|
|
|
|
|
/**
|
|
* Update star visibility and subtle twinkle.
|
|
*/
|
|
beestat.component.scene.prototype.update_stars_ = function() {
|
|
if (this.stars_ === undefined || this.stars_.length === 0) {
|
|
return;
|
|
}
|
|
|
|
if (this.target_star_visibility_ === undefined) {
|
|
this.target_star_visibility_ = 0;
|
|
}
|
|
if (this.star_visibility_ === undefined) {
|
|
this.star_visibility_ = 0;
|
|
}
|
|
|
|
this.star_visibility_ += (this.target_star_visibility_ - this.star_visibility_) * 0.06;
|
|
const visibility = Math.max(0, Math.min(1, this.star_visibility_));
|
|
const now_seconds = window.performance.now() / 1000;
|
|
|
|
if (this.star_group_ !== undefined) {
|
|
if (this.date_ !== undefined && typeof this.date_.valueOf === 'function') {
|
|
// Apparent star motion is westward due to Earth's eastward rotation.
|
|
const sidereal_phase = (
|
|
(this.date_.valueOf() / 1000) % beestat.component.scene.sidereal_day_seconds
|
|
) / beestat.component.scene.sidereal_day_seconds;
|
|
this.star_group_.rotation.y = -sidereal_phase * Math.PI * 2 * beestat.component.scene.star_drift_visual_factor;
|
|
}
|
|
this.star_group_.visible = visibility > 0.005;
|
|
}
|
|
|
|
for (let i = 0; i < this.stars_.length; i++) {
|
|
const star = this.stars_[i];
|
|
let twinkle = 1;
|
|
if (star.twinkle === true) {
|
|
twinkle = 1 + (Math.sin((now_seconds * star.twinkle_speed) + star.phase) * star.twinkle_amount);
|
|
}
|
|
star.sprite.material.opacity = Math.max(
|
|
0,
|
|
Math.min(
|
|
1,
|
|
star.base_opacity * visibility * twinkle
|
|
)
|
|
);
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Convert color temperature in Kelvin to RGB color.
|
|
*
|
|
* @param {number} temperature_k
|
|
*
|
|
* @return {THREE.Color}
|
|
*/
|
|
beestat.component.scene.prototype.get_light_color_from_temperature_ = function(temperature_k) {
|
|
const kelvin = Math.max(1000, Math.min(12000, Number(temperature_k || 4000)));
|
|
const temp = kelvin / 100;
|
|
|
|
let red;
|
|
let green;
|
|
let blue;
|
|
|
|
if (temp <= 66) {
|
|
red = 255;
|
|
green = 99.4708025861 * Math.log(temp) - 161.1195681661;
|
|
blue = temp <= 19 ? 0 : (138.5177312231 * Math.log(temp - 10) - 305.0447927307);
|
|
} else {
|
|
red = 329.698727446 * Math.pow(temp - 60, -0.1332047592);
|
|
green = 288.1221695283 * Math.pow(temp - 60, -0.0755148492);
|
|
blue = 255;
|
|
}
|
|
|
|
const clamp_channel = function(value) {
|
|
return Math.max(0, Math.min(255, Number(value || 0)));
|
|
};
|
|
|
|
return new THREE.Color(
|
|
clamp_channel(red) / 255,
|
|
clamp_channel(green) / 255,
|
|
clamp_channel(blue) / 255
|
|
);
|
|
};
|
|
|
|
|
|
/**
|
|
* Add floor-plan light sources.
|
|
*
|
|
* @param {THREE.Group} layer The layer to add light sources to.
|
|
* @param {object} group The floor plan group.
|
|
*/
|
|
beestat.component.scene.prototype.add_light_sources_ = function(layer, group) {
|
|
if (this.get_scene_setting_('light_user_enabled') !== true) {
|
|
return;
|
|
}
|
|
|
|
if (Array.isArray(group.light_sources) !== true || group.light_sources.length === 0) {
|
|
return;
|
|
}
|
|
if (Array.isArray(this.light_sources_) !== true) {
|
|
this.light_sources_ = [];
|
|
}
|
|
|
|
const group_elevation = Number(group.elevation || 0);
|
|
const floor_thickness = Number(beestat.component.scene.room_floor_thickness || 0);
|
|
const user_light_cast_shadows = this.get_scene_setting_('light_user_cast_shadows') === true;
|
|
|
|
group.light_sources.forEach(function(light_source) {
|
|
const x = Number(light_source.x || 0);
|
|
const y = Number(light_source.y || 0);
|
|
const elevation = Number(light_source.elevation !== undefined ? light_source.elevation : 72);
|
|
const z = -group_elevation - floor_thickness - elevation;
|
|
let intensity_level = 2;
|
|
if (light_source.intensity === 'dim') {
|
|
intensity_level = 1;
|
|
} else if (light_source.intensity === 'bright') {
|
|
intensity_level = 3;
|
|
}
|
|
const light_intensity = 0.9 * intensity_level;
|
|
const light_color = this.get_light_color_from_temperature_(light_source.temperature_k);
|
|
|
|
const light = new THREE.PointLight(light_color, light_intensity, 240, 2);
|
|
light.userData.base_intensity = light_intensity;
|
|
light.intensity = light_intensity * Number(this.target_light_source_intensity_multiplier_ || 0);
|
|
light.position.set(x, y, z);
|
|
light.castShadow = user_light_cast_shadows;
|
|
if (user_light_cast_shadows === true) {
|
|
light.shadow.mapSize.width = 512;
|
|
light.shadow.mapSize.height = 512;
|
|
light.shadow.bias = -0.0012;
|
|
light.shadow.normalBias = 0.025;
|
|
light.shadow.radius = 2;
|
|
light.shadow.camera.near = 1;
|
|
light.shadow.camera.far = 240;
|
|
}
|
|
light.userData.is_light_source = true;
|
|
layer.add(light);
|
|
this.light_sources_.push(light);
|
|
}, this);
|
|
};
|
|
|
|
|
|
/**
|
|
* Apply the current user-light shadow setting to existing user lights.
|
|
*/
|
|
beestat.component.scene.prototype.update_user_light_shadow_settings_ = function() {
|
|
if (Array.isArray(this.light_sources_) !== true) {
|
|
return;
|
|
}
|
|
|
|
const user_light_cast_shadows = this.get_scene_setting_('light_user_cast_shadows') === true;
|
|
this.light_sources_.forEach(function(light) {
|
|
if (light === undefined || light === null || light.isPointLight !== true) {
|
|
return;
|
|
}
|
|
|
|
light.castShadow = user_light_cast_shadows;
|
|
if (user_light_cast_shadows === true) {
|
|
light.shadow.mapSize.width = 512;
|
|
light.shadow.mapSize.height = 512;
|
|
light.shadow.bias = -0.0012;
|
|
light.shadow.normalBias = 0.025;
|
|
light.shadow.radius = 2;
|
|
light.shadow.camera.near = 1;
|
|
light.shadow.camera.far = 240;
|
|
}
|
|
});
|
|
|
|
if (this.renderer_ !== undefined && this.renderer_.shadowMap !== undefined) {
|
|
this.renderer_.shadowMap.needsUpdate = true;
|
|
}
|
|
};
|
|
|
|
|
|
|