364 lines
11 KiB
Rust
364 lines
11 KiB
Rust
use glam::{Quat, Vec2, Vec3};
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use super::{BoundingSphere, Transform};
|
|
|
|
/// IDs share one namespace so an editor can never confuse a primitive and a
|
|
/// light after save/load or duplication.
|
|
pub type EntityId = u32;
|
|
pub type PrimitiveId = EntityId;
|
|
pub type LightId = EntityId;
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
#[serde(default)]
|
|
pub struct Material {
|
|
pub albedo: Vec3,
|
|
pub roughness: f32,
|
|
pub metallic: f32,
|
|
pub emissive_color: Vec3,
|
|
pub emission_strength: f32,
|
|
}
|
|
|
|
impl Default for Material {
|
|
fn default() -> Self {
|
|
Self {
|
|
albedo: Vec3::splat(0.7),
|
|
roughness: 0.6,
|
|
metallic: 0.0,
|
|
emissive_color: Vec3::ONE,
|
|
emission_strength: 0.0,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Material {
|
|
pub(crate) fn validate(&self) -> Result<(), String> {
|
|
if !self.albedo.is_finite() || self.albedo.min_element() < 0.0 {
|
|
return Err("material albedo must be finite and non-negative".into());
|
|
}
|
|
if !self.emissive_color.is_finite() || self.emissive_color.min_element() < 0.0 {
|
|
return Err("material emissive_color must be finite and non-negative".into());
|
|
}
|
|
if !self.roughness.is_finite() || !(0.0..=1.0).contains(&self.roughness) {
|
|
return Err("material roughness must be in 0..=1".into());
|
|
}
|
|
if !self.metallic.is_finite() || !(0.0..=1.0).contains(&self.metallic) {
|
|
return Err("material metallic must be in 0..=1".into());
|
|
}
|
|
if !self.emission_strength.is_finite() || self.emission_strength < 0.0 {
|
|
return Err("material emission_strength must be finite and non-negative".into());
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
|
|
#[serde(tag = "type", rename_all = "snake_case")]
|
|
pub enum LightKind {
|
|
DirectionalSun,
|
|
Point {
|
|
range: f32,
|
|
},
|
|
Spot {
|
|
range: f32,
|
|
inner_angle_radians: f32,
|
|
outer_angle_radians: f32,
|
|
},
|
|
}
|
|
|
|
impl LightKind {
|
|
pub fn point_default() -> Self {
|
|
Self::Point { range: 25.0 }
|
|
}
|
|
|
|
pub fn spot_default() -> Self {
|
|
Self::Spot {
|
|
range: 25.0,
|
|
inner_angle_radians: 20.0_f32.to_radians(),
|
|
outer_angle_radians: 30.0_f32.to_radians(),
|
|
}
|
|
}
|
|
|
|
pub fn display_name(&self) -> &'static str {
|
|
match self {
|
|
Self::DirectionalSun => "Солнце",
|
|
Self::Point { .. } => "Точечный свет",
|
|
Self::Spot { .. } => "Прожектор",
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
#[serde(default)]
|
|
pub struct SceneLight {
|
|
pub id: LightId,
|
|
pub name: String,
|
|
pub transform: Transform,
|
|
pub kind: LightKind,
|
|
pub color: Vec3,
|
|
pub intensity: f32,
|
|
pub enabled: bool,
|
|
}
|
|
|
|
impl Default for SceneLight {
|
|
fn default() -> Self {
|
|
Self {
|
|
id: 0,
|
|
name: "Солнце".into(),
|
|
transform: Transform::default(),
|
|
kind: LightKind::DirectionalSun,
|
|
color: Vec3::ONE,
|
|
intensity: 4.0,
|
|
enabled: true,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl SceneLight {
|
|
/// Directional and spot lights point down their local -Z axis. A point
|
|
/// light has no direction, but returning the same stable vector keeps
|
|
/// editor helpers and the fixed GPU ABI uniform across all light kinds.
|
|
pub fn direction(&self) -> Vec3 {
|
|
let rotation = if self.transform.rotation.is_finite()
|
|
&& self.transform.rotation.length_squared() > Transform::MIN_SCALE
|
|
{
|
|
self.transform.rotation.normalize()
|
|
} else {
|
|
Quat::IDENTITY
|
|
};
|
|
rotation * -Vec3::Z
|
|
}
|
|
|
|
pub fn position(&self) -> Vec3 {
|
|
self.transform.translation
|
|
}
|
|
|
|
pub fn selection_sphere(&self, icon_radius: f32) -> BoundingSphere {
|
|
BoundingSphere {
|
|
center: self.position(),
|
|
radius: icon_radius.abs().max(0.01),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn validate(&self) -> Result<(), String> {
|
|
if !self.transform.is_finite() {
|
|
return Err("light transform contains invalid values".into());
|
|
}
|
|
if !self.color.is_finite() || self.color.min_element() < 0.0 {
|
|
return Err("light color must be finite and non-negative".into());
|
|
}
|
|
if !self.intensity.is_finite() || self.intensity < 0.0 {
|
|
return Err("light intensity must be finite and non-negative".into());
|
|
}
|
|
match self.kind {
|
|
LightKind::DirectionalSun => {}
|
|
LightKind::Point { range } => {
|
|
if !range.is_finite() || range <= 0.0 {
|
|
return Err("point range must be finite and greater than zero".into());
|
|
}
|
|
}
|
|
LightKind::Spot {
|
|
range,
|
|
inner_angle_radians,
|
|
outer_angle_radians,
|
|
} => {
|
|
if !range.is_finite() || range <= 0.0 {
|
|
return Err("spot range must be finite and greater than zero".into());
|
|
}
|
|
if !inner_angle_radians.is_finite()
|
|
|| !outer_angle_radians.is_finite()
|
|
|| inner_angle_radians < 0.0
|
|
|| outer_angle_radians <= 0.0
|
|
|| inner_angle_radians > outer_angle_radians
|
|
|| outer_angle_radians >= core::f32::consts::FRAC_PI_2
|
|
{
|
|
return Err("spot cone angles must satisfy 0 <= inner <= outer < pi/2".into());
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
#[serde(default)]
|
|
pub struct LightingSettings {
|
|
pub ambient_color: Vec3,
|
|
pub ambient_intensity: f32,
|
|
pub shadow_softness: f32,
|
|
pub max_shadow_distance: f32,
|
|
}
|
|
|
|
impl Default for LightingSettings {
|
|
fn default() -> Self {
|
|
Self {
|
|
ambient_color: Vec3::new(0.35, 0.42, 0.55),
|
|
ambient_intensity: 0.12,
|
|
shadow_softness: 12.0,
|
|
max_shadow_distance: 200.0,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl LightingSettings {
|
|
pub(crate) fn validate(&self) -> Result<(), String> {
|
|
if !self.ambient_color.is_finite() || self.ambient_color.min_element() < 0.0 {
|
|
return Err("ambient color must be finite and non-negative".into());
|
|
}
|
|
for (name, value) in [
|
|
("ambient_intensity", self.ambient_intensity),
|
|
("shadow_softness", self.shadow_softness),
|
|
("max_shadow_distance", self.max_shadow_distance),
|
|
] {
|
|
if !value.is_finite() || value < 0.0 {
|
|
return Err(format!("{name} must be finite and non-negative"));
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
#[serde(default)]
|
|
pub struct CloudSettings {
|
|
pub enabled: bool,
|
|
pub base_height: f32,
|
|
pub thickness: f32,
|
|
pub coverage: f32,
|
|
pub density: f32,
|
|
pub scale: f32,
|
|
pub wind_direction: Vec2,
|
|
pub wind_speed: f32,
|
|
pub color: Vec3,
|
|
pub absorption: f32,
|
|
}
|
|
|
|
impl Default for CloudSettings {
|
|
fn default() -> Self {
|
|
Self {
|
|
enabled: true,
|
|
base_height: 80.0,
|
|
thickness: 35.0,
|
|
coverage: 0.45,
|
|
density: 0.7,
|
|
scale: 0.012,
|
|
wind_direction: Vec2::new(1.0, 0.2).normalize(),
|
|
wind_speed: 1.5,
|
|
color: Vec3::ONE,
|
|
absorption: 0.6,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl CloudSettings {
|
|
pub(crate) fn validate(&self) -> Result<(), String> {
|
|
for (name, value) in [
|
|
("base_height", self.base_height),
|
|
("thickness", self.thickness),
|
|
("density", self.density),
|
|
("scale", self.scale),
|
|
("wind_speed", self.wind_speed),
|
|
("absorption", self.absorption),
|
|
] {
|
|
if !value.is_finite() || value < 0.0 {
|
|
return Err(format!("cloud {name} must be finite and non-negative"));
|
|
}
|
|
}
|
|
if !self.coverage.is_finite() || !(0.0..=1.0).contains(&self.coverage) {
|
|
return Err("cloud coverage must be in 0..=1".into());
|
|
}
|
|
if !self.wind_direction.is_finite() || self.wind_direction.length_squared() < 1.0e-6 {
|
|
return Err("cloud wind_direction must be finite and non-zero".into());
|
|
}
|
|
if !self.color.is_finite() || self.color.min_element() < 0.0 {
|
|
return Err("cloud color must be finite and non-negative".into());
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
#[serde(default)]
|
|
pub struct EditorCamera {
|
|
pub position: Vec3,
|
|
pub yaw_radians: f32,
|
|
pub pitch_radians: f32,
|
|
pub movement_speed: f32,
|
|
pub orbit_pivot: Vec3,
|
|
pub field_of_view_y_radians: f32,
|
|
pub near_plane: f32,
|
|
pub far_plane: f32,
|
|
}
|
|
|
|
impl Default for EditorCamera {
|
|
fn default() -> Self {
|
|
Self {
|
|
position: Vec3::new(8.0, -8.0, 6.0),
|
|
yaw_radians: -45.0_f32.to_radians(),
|
|
pitch_radians: -27.938_353_f32.to_radians(),
|
|
movement_speed: 8.0,
|
|
orbit_pivot: Vec3::ZERO,
|
|
field_of_view_y_radians: 60.0_f32.to_radians(),
|
|
near_plane: 0.01,
|
|
far_plane: 2_000.0,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl EditorCamera {
|
|
pub(crate) fn validate(&self) -> Result<(), String> {
|
|
if !self.position.is_finite() || !self.orbit_pivot.is_finite() {
|
|
return Err("editor camera vectors must be finite".into());
|
|
}
|
|
for (name, value) in [
|
|
("yaw_radians", self.yaw_radians),
|
|
("pitch_radians", self.pitch_radians),
|
|
("movement_speed", self.movement_speed),
|
|
("field_of_view_y_radians", self.field_of_view_y_radians),
|
|
("near_plane", self.near_plane),
|
|
("far_plane", self.far_plane),
|
|
] {
|
|
if !value.is_finite() {
|
|
return Err(format!("editor camera {name} must be finite"));
|
|
}
|
|
}
|
|
if self.movement_speed <= 0.0
|
|
|| self.field_of_view_y_radians <= 0.0
|
|
|| self.field_of_view_y_radians >= core::f32::consts::PI
|
|
|| self.near_plane <= 0.0
|
|
|| self.far_plane <= self.near_plane
|
|
{
|
|
return Err("editor camera projection or movement settings are invalid".into());
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn point_light_has_stable_serde_shape_and_round_trips() {
|
|
let kind = LightKind::Point { range: 17.5 };
|
|
let json = serde_json::to_string(&kind).unwrap();
|
|
assert_eq!(json, r#"{"type":"point","range":17.5}"#);
|
|
assert_eq!(serde_json::from_str::<LightKind>(&json).unwrap(), kind);
|
|
}
|
|
|
|
#[test]
|
|
fn point_light_range_must_be_positive_and_finite() {
|
|
let mut light = SceneLight {
|
|
kind: LightKind::Point { range: 0.0 },
|
|
..SceneLight::default()
|
|
};
|
|
assert!(light.validate().is_err());
|
|
|
|
light.kind = LightKind::Point { range: f32::NAN };
|
|
assert!(light.validate().is_err());
|
|
|
|
light.kind = LightKind::point_default();
|
|
assert!(light.validate().is_ok());
|
|
}
|
|
}
|