fix(scene): preserve viewport settings
Reuse full VPort records during camera and tile synchronization so navigation cannot replace file-defined display state with defaults. Honor image, ground, IBL, and sun-backed sky parameters in background rendering.
This commit is contained in:
parent
0be9c41773
commit
15ab027d70
3 changed files with 357 additions and 179 deletions
|
|
@ -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<SceneLight> {
|
||||
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<SceneLight> {
|
||||
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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue