diff --git a/src/scene/camera_ops.rs b/src/scene/camera_ops.rs index dce2bbac6..75d15dcb0 100644 --- a/src/scene/camera_ops.rs +++ b/src/scene/camera_ops.rs @@ -122,9 +122,8 @@ impl Scene { pub fn restore_named_view(&mut self, view: &acadrust::tables::View) { use glam::Vec3; let cam = &mut *self.camera.borrow_mut(); - // view.target is the look-at point; view.direction is eye→target direction. + // The stored direction points from the target toward the eye. cam.target = glam::DVec3::new(view.target.x, view.target.y, view.target.z); - // direction in acadrust = from-target-to-eye (same as AutoCAD convention). let eye_dir = Vec3::new( view.direction.x as f32, view.direction.y as f32, @@ -720,16 +719,13 @@ impl Scene { ) } - /// Reverse of `camera_from_vport`: write `cam`'s view target / direction - /// / height onto a fresh VPort entry with the given `name` and screen - /// rectangle (0..1 normalized, DXF bottom-left origin convention). - fn vport_from_camera( + fn apply_camera_to_vport( &self, - name: &str, + entry: &mut acadrust::tables::VPort, cam: &Camera, lower_left: acadrust::types::Vector2, upper_right: acadrust::types::Vector2, - ) -> acadrust::tables::VPort { + ) { let view_dir = cam.rotation * glam::Vec3::Z; let view_height = cam.ortho_size() * 2.0; let target_wcs = acadrust::types::Vector3 { @@ -737,7 +733,6 @@ impl Scene { y: (cam.target.y as f64) + [0.0_f64; 3][1], z: (cam.target.z as f64) + [0.0_f64; 3][2], }; - let mut entry = acadrust::tables::VPort::new(name); entry.lower_left = lower_left; entry.upper_right = upper_right; entry.view_target = target_wcs; @@ -747,12 +742,43 @@ impl Scene { z: view_dir.z as f64, }; entry.view_height = view_height as f64; + let (canvas_width, canvas_height) = self.selection.borrow().vp_size; + let viewport_width = (upper_right.x - lower_left.x).abs() * canvas_width as f64; + let viewport_height = (upper_right.y - lower_left.y).abs() * canvas_height as f64; + if viewport_width > 1e-9 && viewport_height > 1e-9 { + entry.aspect_ratio = viewport_width / viewport_height; + } entry.perspective = cam.projection == view::camera::Projection::Perspective; entry.lens_length = (12.0 / (cam.fov_y * 0.5).tan().max(1e-6)) as f64; entry.view_center = acadrust::types::Vector2::ZERO; // Stored twist = -roll, matching the decoder (roll = -twist). entry.view_twist = -cam.roll() as f64; - entry + } + + fn vport_rect_matches( + entry: &acadrust::tables::VPort, + lower_left: acadrust::types::Vector2, + upper_right: acadrust::types::Vector2, + ) -> bool { + const EPSILON: f64 = 1e-5; + (entry.lower_left.x - lower_left.x).abs() <= EPSILON + && (entry.lower_left.y - lower_left.y).abs() <= EPSILON + && (entry.upper_right.x - upper_right.x).abs() <= EPSILON + && (entry.upper_right.y - upper_right.y).abs() <= EPSILON + } + + fn vport_contains_rect_center( + entry: &acadrust::tables::VPort, + lower_left: acadrust::types::Vector2, + upper_right: acadrust::types::Vector2, + ) -> bool { + const EPSILON: f64 = 1e-5; + let center_x = (lower_left.x + upper_right.x) * 0.5; + let center_y = (lower_left.y + upper_right.y) * 0.5; + center_x >= entry.lower_left.x - EPSILON + && center_x <= entry.upper_right.x + EPSILON + && center_y >= entry.lower_left.y - EPSILON + && center_y <= entry.upper_right.y + EPSILON } /// Convert a `ModelTile`'s normalized iced rectangle (top-left origin) to @@ -780,9 +806,7 @@ impl Scene { } } - /// Restore `model_tiles` from VPort entries that a previous save left in - /// the document. Native AutoCAD tiled model-space layouts are represented - /// by duplicate `*Active` VPort entries. + /// Restore model tiles from duplicate `*Active` VPort entries. /// Returns true on success — the caller skips `apply_active_vport_camera` /// in that case because the active tile's camera has already been loaded /// into `self.camera`. @@ -828,8 +852,7 @@ impl Scene { true } - /// Persist `model_tiles` to the VPort table. Native AutoCAD tiled model - /// viewports are written as duplicate `*Active` entries. + /// Persist model tiles as duplicate `*Active` VPort entries. fn save_model_tiles_to_vports(&mut self) { // Stash the live camera into the active tile so the about-to-write // snapshot reflects the user's most recent orbit / pan / zoom. @@ -843,6 +866,15 @@ impl Scene { } let table_handle = self.document.vports.handle(); + let mut active_vports: Vec = self + .document + .vports + .iter() + .filter(|v| is_active_vport_name(&v.name)) + .cloned() + .collect(); + let source_vports = active_vports.clone(); + let inherited_vport = active_vports.first().cloned(); let preserved_vps: Vec = self .document .vports @@ -870,16 +902,88 @@ impl Scene { } } + let mut scene_objects_changed = false; for tile in ordered_tiles { let (ll, ur) = Self::tile_rect_to_vport(tile.rect); - let mut entry = self.vport_from_camera("*Active", &tile.camera, ll, ur); + let exact = active_vports + .iter() + .position(|entry| Self::vport_rect_matches(entry, ll, ur)); + let inherited = exact.or_else(|| { + active_vports + .iter() + .enumerate() + .filter(|(_, entry)| Self::vport_contains_rect_center(entry, ll, ur)) + .min_by(|(_, left), (_, right)| { + let left_area = (left.upper_right.x - left.lower_left.x) + * (left.upper_right.y - left.lower_left.y); + let right_area = (right.upper_right.x - right.lower_left.x) + * (right.upper_right.y - right.lower_left.y); + left_area.total_cmp(&right_area) + }) + .map(|(index, _)| index) + }); + let mut entry = inherited + .map(|index| active_vports.remove(index)) + .or_else(|| { + source_vports + .iter() + .filter(|entry| Self::vport_contains_rect_center(entry, ll, ur)) + .min_by(|left, right| { + let left_area = (left.upper_right.x - left.lower_left.x) + * (left.upper_right.y - left.lower_left.y); + let right_area = (right.upper_right.x - right.lower_left.x) + * (right.upper_right.y - right.lower_left.y); + left_area.total_cmp(&right_area) + }) + .cloned() + }) + .or_else(|| inherited_vport.clone()) + .unwrap_or_else(|| acadrust::tables::VPort::new("*Active")); + let cloned = inherited.is_none(); + if cloned { + entry.handle = acadrust::Handle::NULL; + } + entry.name = "*Active".to_string(); + self.apply_camera_to_vport(&mut entry, &tile.camera, ll, ur); entry.render_mode = tile.render_mode; // Each viewport persists its own grid display + grid-snap (#121). entry.grid_on = tile.grid_on; entry.snap_on = tile.snap_on; - entry.handle = self.document.allocate_handle(); + if !entry.handle.is_valid() { + entry.handle = self.document.allocate_handle(); + } + if cloned && entry.sun_handle.is_valid() { + let source_sun = self.document.objects.get(&entry.sun_handle).and_then(|object| { + match object { + acadrust::objects::ObjectType::ClassObject(value) + if matches!( + &value.data, + acadrust::objects::ClassObjectData::Sun(_) + ) => Some(value.clone()), + _ => None, + } + }); + if let Some(mut sun) = source_sun { + let handle = self.document.allocate_handle(); + sun.handle = handle; + sun.owner = entry.handle; + sun.reactors.clear(); + sun.xdictionary_handle = None; + self.document.objects.insert( + handle, + acadrust::objects::ObjectType::ClassObject(sun), + ); + entry.sun_handle = handle; + scene_objects_changed = true; + } + } self.document.vports.add_allow_duplicate(entry); } + if scene_objects_changed { + self.object_data_cache = + crate::entities::object_data::build_cache(&self.document); + self.lighting_cache.borrow_mut().clear(); + } } /// Mirror the live grid/snap toggles onto the active view's own store so the @@ -1000,31 +1104,6 @@ impl Scene { }; if self.current_layout == "Model" { - let target_wcs = acadrust::types::Vector3 { - x: (cam.target.x as f64) + [0.0_f64; 3][0], - y: (cam.target.y as f64) + [0.0_f64; 3][1], - z: (cam.target.z as f64) + [0.0_f64; 3][2], - }; - - // Write back to the *Active VPort entry (may be overridden by DWG writer). - if let Some(vp) = self - .document - .vports - .iter_mut() - .find(|v| is_active_vport_name(&v.name)) - { - vp.view_target = target_wcs; - vp.view_center = acadrust::types::Vector2::ZERO; - vp.view_direction = vd3; - vp.view_height = view_height as f64; - vp.view_twist = twist; - vp.perspective = cam.projection == view::camera::Projection::Perspective; - vp.lens_length = (12.0 / (cam.fov_y * 0.5).tan().max(1e-6)) as f64; - } - - // Persist the tiled layout as duplicate `*Active` VPort entries. - // The view lives entirely in the standard VPORT table now — no - // app-specific View record. self.save_model_tiles_to_vports(); true } else { @@ -1040,11 +1119,7 @@ impl Scene { if let Some(EntityType::Viewport(vp)) = self.document.get_entity_mut(sheet_handle) { - // AutoCAD stores the paper-space view position in - // `view_center` (DCS) with `view_target` at the origin — - // writing it the other way round shifts the layout and - // crashes nothing but renders the sheet off-place. Paper - // space is always a plan view, so DCS == WCS XY here. + // Paper-space position is stored in view_center (DCS). vp.view_center = acadrust::types::Vector3::new(target_wcs.x, target_wcs.y, 0.0); vp.view_target = acadrust::types::Vector3::ZERO; diff --git a/src/scene/view/render.rs b/src/scene/view/render.rs index d92c79b85..b07b9bc12 100644 --- a/src/scene/view/render.rs +++ b/src/scene/view/render.rs @@ -34,6 +34,7 @@ struct ViewportLightingSettings { force_default: bool, default_type: i16, ambient: [f32; 3], + sun_handle: Handle, } #[derive(Clone)] @@ -94,6 +95,7 @@ impl Default for ViewportLightingSettings { force_default: true, default_type: 1, ambient: [0.18; 3], + sun_handle: Handle::NULL, } } } @@ -1426,9 +1428,148 @@ fn crop_view_proj(view_proj: glam::Mat4, uo: f32, vo: f32, us: f32, vs: f32) -> crop * view_proj } +fn normalized_direction(value: [f64; 3], fallback: [f32; 3]) -> [f32; 3] { + let length = (value[0] * value[0] + value[1] * value[1] + value[2] * value[2]).sqrt(); + if length <= 1e-12 { + fallback + } else { + [ + (value[0] / length) as f32, + (value[1] / length) as f32, + (value[2] / length) as f32, + ] + } +} + +fn solar_direction( + sun: &acadrust::objects::Sun, + geo: &acadrust::objects::GeoData, +) -> Option<[f32; 3]> { + if sun.julian_day < 1_000_000 { + return None; + } + let daylight_ms = if sun.is_daylight_savings_on { + 3_600_000.0 + } else { + 0.0 + }; + let jd = sun.julian_day as f64 + + (sun.milliseconds as f64 - daylight_ms) / 86_400_000.0; + let days = jd - 2_451_545.0; + let mean_longitude = (280.460 + 0.985_647_4 * days).to_radians(); + let mean_anomaly = (357.528 + 0.985_600_3 * days).to_radians(); + let ecliptic_longitude = mean_longitude + + (1.915 * mean_anomaly.sin() + 0.020 * (2.0 * mean_anomaly).sin()).to_radians(); + let obliquity = (23.439 - 0.000_000_4 * days).to_radians(); + let right_ascension = + (obliquity.cos() * ecliptic_longitude.sin()).atan2(ecliptic_longitude.cos()); + let declination = (obliquity.sin() * ecliptic_longitude.sin()).asin(); + let local_sidereal = + (280.460_618_37 + 360.985_647_366_29 * days + geo.reference_point.x).to_radians(); + let hour_angle = (local_sidereal - right_ascension + std::f64::consts::PI) + .rem_euclid(std::f64::consts::TAU) + - std::f64::consts::PI; + let latitude = geo.reference_point.y.to_radians(); + let east_component = -declination.cos() * hour_angle.sin(); + let north_component = declination.sin() * latitude.cos() + - declination.cos() * hour_angle.cos() * latitude.sin(); + let up_component = declination.sin() * latitude.sin() + + declination.cos() * hour_angle.cos() * latitude.cos(); + if up_component <= 0.0 { + return None; + } + let north = normalized_direction( + [geo.north_direction.x, geo.north_direction.y, 0.0], + [0.0, 1.0, 0.0], + ); + let east = [north[1], -north[0], 0.0]; + let up = normalized_direction( + [geo.up_direction.x, geo.up_direction.y, geo.up_direction.z], + [0.0, 0.0, 1.0], + ); + Some(normalized_direction( + [ + -(east[0] as f64 * east_component + + north[0] as f64 * north_component + + up[0] as f64 * up_component), + -(east[1] as f64 * east_component + + north[1] as f64 * north_component + + up[1] as f64 * up_component), + -(east[2] as f64 * east_component + + north[2] as f64 * north_component + + up[2] as f64 * up_component), + ], + [0.0, 0.0, -1.0], + )) +} + // ── Render-style helpers (impl Scene) ──────────────────────────────────── impl Scene { + fn model_tile_vport(&self, index: usize) -> Option<&acadrust::tables::VPort> { + let rect = self.model_tiles.borrow().get(index)?.rect; + let lower_left = [rect.x as f64, (1.0 - rect.y - rect.height) as f64]; + let upper_right = [ + (rect.x + rect.width) as f64, + (1.0 - rect.y) as f64, + ]; + const EPSILON: f64 = 1e-5; + let exact = self + .document + .vports + .iter() + .filter(|value| value.name.eq_ignore_ascii_case("*Active")) + .find(|value| { + (value.lower_left.x - lower_left[0]).abs() <= EPSILON + && (value.lower_left.y - lower_left[1]).abs() <= EPSILON + && (value.upper_right.x - upper_right[0]).abs() <= EPSILON + && (value.upper_right.y - upper_right[1]).abs() <= EPSILON + }); + exact.or_else(|| { + let center_x = (lower_left[0] + upper_right[0]) * 0.5; + let center_y = (lower_left[1] + upper_right[1]) * 0.5; + self.document + .vports + .iter() + .filter(|value| { + value.name.eq_ignore_ascii_case("*Active") + && center_x >= value.lower_left.x - EPSILON + && center_x <= value.upper_right.x + EPSILON + && center_y >= value.lower_left.y - EPSILON + && center_y <= value.upper_right.y + EPSILON + }) + .min_by(|left, right| { + let left_area = (left.upper_right.x - left.lower_left.x) + * (left.upper_right.y - left.lower_left.y); + let right_area = (right.upper_right.x - right.lower_left.x) + * (right.upper_right.y - right.lower_left.y); + left_area.total_cmp(&right_area) + }) + .or_else(|| { + self.document + .vports + .iter() + .find(|value| value.name.eq_ignore_ascii_case("*Active")) + }) + }) + } + + fn geolocation(&self) -> Option<&acadrust::objects::GeoData> { + use acadrust::objects::ObjectType; + + crate::entities::object_data::geo_objects(&self.object_data_cache) + .iter() + .find_map(|handle| match self.document.objects.get(handle) { + Some(ObjectType::GeoData(value)) + if value.coordinate_type == 3 + && value.reference_point.x.is_finite() + && value.reference_point.y.is_finite() + && value.reference_point.x.abs() <= 180.0 + && value.reference_point.y.abs() <= 90.0 => Some(value), + _ => None, + }) + } + fn build_lighting_cache( &self, target_block: Handle, @@ -1436,92 +1577,11 @@ impl Scene { ) -> Vec { use acadrust::objects::{ClassObjectData, ObjectType}; - fn normalized(value: [f64; 3], fallback: [f32; 3]) -> [f32; 3] { - let length = - (value[0] * value[0] + value[1] * value[1] + value[2] * value[2]).sqrt(); - if length <= 1e-12 { - fallback - } else { - [ - (value[0] / length) as f32, - (value[1] / length) as f32, - (value[2] / length) as f32, - ] - } - } - - fn solar_direction( - sun: &acadrust::objects::Sun, - geo: &acadrust::objects::GeoData, - ) -> Option<[f32; 3]> { - if sun.julian_day < 1_000_000 { - return None; - } - let daylight_ms = if sun.is_daylight_savings_on { - 3_600_000.0 - } else { - 0.0 - }; - let jd = sun.julian_day as f64 - + (sun.milliseconds as f64 - daylight_ms) / 86_400_000.0; - let days = jd - 2_451_545.0; - let mean_longitude = (280.460 + 0.985_647_4 * days).to_radians(); - let mean_anomaly = (357.528 + 0.985_600_3 * days).to_radians(); - let ecliptic_longitude = mean_longitude - + (1.915 * mean_anomaly.sin() - + 0.020 * (2.0 * mean_anomaly).sin()) - .to_radians(); - let obliquity = (23.439 - 0.000_000_4 * days).to_radians(); - let right_ascension = (obliquity.cos() * ecliptic_longitude.sin()) - .atan2(ecliptic_longitude.cos()); - let declination = - (obliquity.sin() * ecliptic_longitude.sin()).asin(); - let local_sidereal = (280.460_618_37 - + 360.985_647_366_29 * days - + geo.reference_point.x) - .to_radians(); - let hour_angle = (local_sidereal - right_ascension + std::f64::consts::PI) - .rem_euclid(std::f64::consts::TAU) - - std::f64::consts::PI; - let latitude = geo.reference_point.y.to_radians(); - let east_component = -declination.cos() * hour_angle.sin(); - let north_component = declination.sin() * latitude.cos() - - declination.cos() * hour_angle.cos() * latitude.sin(); - let up_component = declination.sin() * latitude.sin() - + declination.cos() * hour_angle.cos() * latitude.cos(); - if up_component <= 0.0 { - return None; - } - let north = normalized( - [geo.north_direction.x, geo.north_direction.y, 0.0], - [0.0, 1.0, 0.0], - ); - let east = [north[1], -north[0], 0.0]; - let up = normalized( - [geo.up_direction.x, geo.up_direction.y, geo.up_direction.z], - [0.0, 0.0, 1.0], - ); - Some(normalized( - [ - -(east[0] as f64 * east_component - + north[0] as f64 * north_component - + up[0] as f64 * up_component), - -(east[1] as f64 * east_component - + north[1] as f64 * north_component - + up[1] as f64 * up_component), - -(east[2] as f64 * east_component - + north[2] as f64 * north_component - + up[2] as f64 * up_component), - ], - [0.0, 0.0, -1.0], - )) - } - fn converted(scene: &Scene, light: &acadrust::entities::Light) -> Option { if !light.status { return None; } - let mut direction = normalized( + let mut direction = normalized_direction( [ light.target.x - light.position.x, light.target.y - light.position.y, @@ -1687,19 +1747,7 @@ impl Scene { } } - // SUN is document-global. Keep it after entity lights so the four-light - // shader limit prefers visible lights owned by this viewport's block. - let geo = crate::entities::object_data::geo_objects(&self.object_data_cache) - .iter() - .find_map(|handle| match self.document.objects.get(handle) { - Some(ObjectType::GeoData(value)) - if value.coordinate_type == 3 - && value.reference_point.x.is_finite() - && value.reference_point.y.is_finite() - && value.reference_point.x.abs() <= 180.0 - && value.reference_point.y.abs() <= 90.0 => Some(value), - _ => None, - }); + let geo = self.geolocation(); for handle in crate::entities::object_data::sun_objects(&self.object_data_cache) { let Some(ObjectType::ClassObject(value)) = self.document.objects.get(handle) else { continue; @@ -1711,10 +1759,10 @@ impl Scene { continue; } let Some(geo) = geo else { - break; + continue; }; let Some(direction) = solar_direction(sun, geo) else { - break; + continue; }; let rgba = tess_util::aci_to_rgba(&sun.color); lights.push(SceneLight { @@ -1737,7 +1785,6 @@ impl Scene { web_rotation: [0.0; 3], web_enabled: false, }); - break; } lights } @@ -1756,6 +1803,7 @@ impl Scene { } let cache = self.lighting_cache.borrow(); let lights = cache.get(&key).map(Vec::as_slice).unwrap_or_default(); + let settings = self.viewport_lighting_settings(viewport); let visible_lights: Vec<&SceneLight> = lights .iter() .filter(|light| match self.document.get_entity(light.handle) { @@ -1777,12 +1825,12 @@ impl Scene { object, acadrust::objects::ObjectType::ClassObject(value) if matches!(&value.data, acadrust::objects::ClassObjectData::Sun(_)) - ) + ) && (!settings.sun_handle.is_valid() + || settings.sun_handle == light.handle) }), }) .take(4) .collect(); - let settings = self.viewport_lighting_settings(viewport); uniforms.lighting[1..4].copy_from_slice(&settings.ambient); if settings.force_default || visible_lights.is_empty() { Self::apply_default_lighting(uniforms, &viewport.camera, settings.default_type); @@ -1933,11 +1981,13 @@ impl Scene { force_default: value.default_lighting, default_type: value.default_lighting_type, ambient: ambient(&value.ambient_color), + sun_handle: value.sun_handle, }; let from_table = |value: &acadrust::tables::VPort| ViewportLightingSettings { force_default: value.use_default_lights, default_type: value.default_lighting_type, ambient: ambient(&value.ambient_color), + sun_handle: value.sun_handle, }; if viewport.paper_sheet { @@ -1951,13 +2001,7 @@ impl Scene { } } else if self.current_layout == "Model" { let index = viewport.tile_idx.unwrap_or(0); - if let Some(value) = self - .document - .vports - .iter() - .filter(|value| value.name.eq_ignore_ascii_case("*Active")) - .nth(index) - { + if let Some(value) = self.model_tile_vport(index) { return from_table(value); } } @@ -2025,13 +2069,7 @@ impl Scene { } } else if self.current_layout == "Model" { let index = viewport.tile_idx.unwrap_or(0); - if let Some(value) = self - .document - .vports - .iter() - .filter(|value| value.name.eq_ignore_ascii_case("*Active")) - .nth(index) - { + if let Some(value) = self.model_tile_vport(index) { return build( value.visual_style_handle, value.brightness, @@ -2079,7 +2117,16 @@ impl Scene { candidates.push(base.join(&source)); if let Some(name) = source.file_name() { candidates.push(base.join(name)); - for folder in ["Textures", "textures", "Materials", "materials"] { + for folder in [ + "Textures", + "textures", + "Materials", + "materials", + "Environments", + "environments", + "Render", + "render", + ] { candidates.push(base.join(folder).join(name)); } } @@ -2373,12 +2420,38 @@ impl Scene { } result } - ClassObjectData::SkyLightBackground(_) => { + ClassObjectData::SkyLightBackground(background) => { let mut result = ViewportBackgroundSettings::canvas(canvas); result.colors[0] = [0.18, 0.42, 0.78, 1.0]; + result.colors[1] = [1.0, 0.92, 0.72, 0.0]; result.colors[2] = [0.78, 0.86, 0.95, 1.0]; result.base = result.colors[2]; result.params[0] = 6.0; + if let Some(ObjectType::ClassObject(value)) = + self.document.objects.get(&background.sun) + { + if let ClassObjectData::Sun(sun) = &value.data { + if sun.is_on { + if let Some(direction) = self + .geolocation() + .and_then(|geo| solar_direction(sun, geo)) + { + result.params[1..4].copy_from_slice(&[ + -direction[0], + -direction[1], + -direction[2], + ]); + let color = tess_util::aci_to_rgba(&sun.color); + result.colors[1] = [ + color[0], + color[1], + color[2], + sun.intensity.max(0.0) as f32, + ]; + } + } + } + } result } _ => ViewportBackgroundSettings::canvas(canvas), @@ -3467,7 +3540,13 @@ impl Scene { uniforms.background_image_params = display.background.image_params; uniforms.background_image_transform = display.background.image_transform; uniforms.environment_params = display.background.environment_params; - uniforms.environment_view = glam::Mat4::from_quat(inst.camera.rotation); + let environment_scale = (inst.camera.fov_y * 0.5).tan().max(1e-4); + uniforms.environment_view = glam::Mat4::from_quat(inst.camera.rotation) + * glam::Mat4::from_scale(glam::Vec3::new( + visible_w / visible_h.max(1.0) * environment_scale, + environment_scale, + 1.0, + )); uniforms.fog_color = display.background.fog_color; uniforms.fog_params = display.background.fog_params; uniforms.fog_distances = display.background.fog_distances; diff --git a/src/shaders/background.wgsl b/src/shaders/background.wgsl index 12b33205d..3c933c0a9 100644 --- a/src/shaders/background.wgsl +++ b/src/shaders/background.wgsl @@ -66,8 +66,12 @@ fn vs_main(@builtin(vertex_index) index: u32) -> VertexOutput { return out; } +fn world_direction(ndc: vec2) -> vec3 { + return normalize((u.environment_view * vec4(ndc, -1.0, 0.0)).xyz); +} + fn equirect_uv(ndc: vec2, rotation: f32) -> vec2 { - let direction = normalize((u.environment_view * vec4(ndc, -1.0, 0.0)).xyz); + let direction = world_direction(ndc); let longitude = atan2(direction.x, -direction.z) + rotation; let latitude = asin(clamp(direction.y, -1.0, 1.0)); return vec2(fract(longitude / (2.0 * 3.14159265359) + 0.5), @@ -75,8 +79,11 @@ fn equirect_uv(ndc: vec2, rotation: f32) -> vec2 { } fn image_uv(source_uv: vec2) -> vec2 { - var uv = (source_uv - u.background_image_transform.xy) - / max(abs(u.background_image_transform.zw), vec2(1e-6)); + var uv = source_uv; + if u.background_image_params.x < 0.5 { + uv = (source_uv - u.background_image_transform.xy) + / max(abs(u.background_image_transform.zw), vec2(1e-6)); + } if u.background_image_params.y > 0.5 { let image_aspect = max(u.background_image_params.w, 1e-6); let viewport_aspect = max(u.viewport_size.x / max(u.viewport_size.y, 1.0), 1e-6); @@ -91,7 +98,7 @@ fn image_uv(source_uv: vec2) -> vec2 { if u.background_image_params.z > 0.5 { return fract(uv); } - return clamp(uv, vec2(0.0), vec2(1.0)); + return uv; } @fragment @@ -111,24 +118,41 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4 { let low_color = mix(u.background_bottom.rgb, u.background_middle.rgb, lower); color = mix(low_color, u.background_top.rgb, upper); } else if mode == 3u { - let horizon = 0.5; - if in.uv.y <= horizon { - let sky = smoothstep(0.0, horizon, in.uv.y); - color = mix(u.background_top.rgb, u.background_middle.rgb, sky); + let direction = world_direction(in.ndc); + if direction.z >= 0.0 { + let sky = smoothstep(0.0, 1.0, direction.z); + color = mix(u.background_middle.rgb, u.background_top.rgb, sky); } else { - let ground = smoothstep(horizon, 1.0, in.uv.y); - let underground = mix(u.background_bottom.rgb, u.background_aux0.rgb, ground); - let distance_tint = mix(u.background_aux1.rgb, u.viewport_background.rgb, ground); - color = mix(underground, distance_tint, 0.35); + let depth = smoothstep(0.0, 1.0, -direction.z); + let underground = mix(u.background_bottom.rgb, u.background_aux0.rgb, depth); + let ground = mix(u.viewport_background.rgb, u.background_aux1.rgb, depth); + color = mix(underground, ground, 0.65); } } else if mode == 4u { - color = textureSample(background_texture, background_sampler, image_uv(in.uv)).rgb; + let uv = image_uv(in.uv); + if u.background_image_params.z < 0.5 + && (any(uv < vec2(0.0)) || any(uv > vec2(1.0))) { + color = u.viewport_background.rgb; + } else { + color = textureSample(background_texture, background_sampler, uv).rgb; + } } else if mode == 5u { color = textureSample(background_texture, background_sampler, equirect_uv(in.ndc, u.background_params.w)).rgb; } else if mode == 6u { - let t = smoothstep(0.0, 1.0, in.uv.y); - color = mix(u.background_bottom.rgb, u.background_top.rgb, t); + let direction = world_direction(in.ndc); + let elevation = clamp(direction.z, 0.0, 1.0); + color = mix(u.background_bottom.rgb, u.background_top.rgb, + smoothstep(0.0, 1.0, elevation)); + let sun_length = length(u.background_params.yzw); + if sun_length > 0.5 { + let sun_direction = u.background_params.yzw / sun_length; + let alignment = dot(direction, sun_direction); + let glow = smoothstep(0.96, 1.0, alignment) * 0.18; + let disk = smoothstep(0.9997, 0.99995, alignment); + let intensity = max(u.background_middle.a, 0.0); + color += u.background_middle.rgb * intensity * (glow + disk); + } } else { color = u.viewport_background.rgb; }