Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ categories = ["network-programming", "web-programming"]
exclude.workspace = true

[workspace.package]
version = "0.31.1"
version = "0.31.2"
readme = "README.md"
edition = "2024"
authors = ["root@ltpp.vip"]
Expand All @@ -38,13 +38,13 @@ resolver = "3"
crate-type = ["rlib"]

[workspace.dependencies]
euv = { path = ".", version = "0.31.1" }
euv-ui = { path = "ui", version = "0.31.1" }
euv-cli = { path = "cli", version = "0.31.1" }
euv-core = { path = "core", version = "0.31.1" }
euv-engine = { path = "engine", version = "0.31.1" }
euv-macros = { path = "macros", version = "0.31.1" }
euv-example = { path = "example", version = "0.31.1" }
euv = { path = ".", version = "0.31.2" }
euv-ui = { path = "ui", version = "0.31.2" }
euv-cli = { path = "cli", version = "0.31.2" }
euv-core = { path = "core", version = "0.31.2" }
euv-engine = { path = "engine", version = "0.31.2" }
euv-macros = { path = "macros", version = "0.31.2" }
euv-example = { path = "example", version = "0.31.2" }

log = "0.4.34"
toml = "0.9.12"
Expand Down
126 changes: 66 additions & 60 deletions engine/src/lighting/fn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,47 +26,46 @@ use super::*;
///
/// - `Vector3D` - The diffuse contribution of this light.
pub fn compute_lambert(light: &Light, normal: Vector3D, material: &Material) -> Vector3D {
lambert_term(
light.get_color(),
light.get_intensity(),
light.get_direction(),
normal,
material.get_albedo(),
)
let sample: LightSample = LightSample {
color: light.get_color(),
intensity: light.get_intensity(),
direction: light.get_direction(),
};
lambert_term(&sample, normal, material)
}

/// The Lambertian term shared by [`compute_lambert`] and
/// [`LightingUniforms::shade_with_visibility`].
///
/// Unpacking the light into its colour, intensity, and direction is what
/// lets the shading loop evaluate a light once — one distance, one
/// direction, one falloff — instead of building a mutated copy of the
/// [`Light`] per term.
/// The light arrives as a [`LightSample`] rather than as a [`Light`]
/// because the shading loop has already resolved it — one distance, one
/// direction, one falloff — into the colour, intensity, and direction this
/// term reads. Passing that sample on keeps the [`Light`] un-cloned and
/// stops each term from re-deriving what the loop measured.
///
/// # Arguments
///
/// - `Vector3D` - The light's RGB color.
/// - `f64` - The light's intensity, already scaled by any falloff.
/// - `Vector3D` - The unit direction from the surface toward the light.
/// - `&LightSample` - The resolved colour, intensity, and direction of the
/// light being evaluated.
/// - `Vector3D` - The surface normal (unit length).
/// - `Vector3D` - The material's diffuse albedo.
/// - `&Material` - The material at the shaded point; its diffuse albedo is
/// read here.
///
/// # Returns
///
/// - `Vector3D` - `color * albedo * intensity * max(0, dot(normal, light_dir))`.
pub(crate) fn lambert_term(
light_color: Vector3D,
light_intensity: f64,
light_dir: Vector3D,
sample: &LightSample,
normal: Vector3D,
albedo: Vector3D,
material: &Material,
) -> Vector3D {
let cos: f64 = normal.dot(light_dir).max(0.0);
let k: f64 = light_intensity * cos;
let albedo: Vector3D = material.get_albedo();
let cos: f64 = normal.dot(sample.get_direction()).max(0.0);
let k: f64 = sample.get_intensity() * cos;
Vector3D::new(
light_color.get_x() * albedo.get_x() * k,
light_color.get_y() * albedo.get_y() * k,
light_color.get_z() * albedo.get_z() * k,
sample.get_color().get_x() * albedo.get_x() * k,
sample.get_color().get_y() * albedo.get_y() * k,
sample.get_color().get_z() * albedo.get_z() * k,
)
}

Expand Down Expand Up @@ -99,50 +98,52 @@ pub fn compute_phong(
view_dir: Vector3D,
material: &Material,
) -> Vector3D {
phong_specular_term(
light.get_color(),
light.get_intensity(),
light.get_direction(),
normal,
view_dir,
material.get_specular(),
material.get_shininess(),
)
let sample: LightSample = LightSample {
color: light.get_color(),
intensity: light.get_intensity(),
direction: light.get_direction(),
};
phong_specular_term(&sample, normal, view_dir, material)
}

/// The Phong specular term shared by [`compute_phong`] and
/// [`LightingUniforms::shade_with_visibility`]. Returns the zero vector
/// unless the surface faces the light, so the lobe cannot survive on the
/// back side of a surface.
///
/// Like [`lambert_term`], it reads a [`LightSample`] the caller already
/// resolved and takes the two material scalars through the [`Material`]
/// itself, so neither the light nor the material is rebuilt per term.
///
/// # Arguments
///
/// - `Vector3D` - The light's RGB color.
/// - `f64` - The light's intensity, already scaled by any falloff.
/// - `Vector3D` - The unit direction from the surface toward the light.
/// - `&LightSample` - The resolved colour, intensity, and direction of the
/// light being evaluated.
/// - `Vector3D` - The surface normal (unit length).
/// - `Vector3D` - The unit view direction (from the surface toward the eye).
/// - `f64` - The material's specular intensity.
/// - `f64` - The Phong specular exponent.
/// - `&Material` - The material at the shaded point; its specular intensity
/// and shininess are read here.
///
/// # Returns
///
/// - `Vector3D` - `color * intensity * specular * spec_factor`.
pub(crate) fn phong_specular_term(
light_color: Vector3D,
light_intensity: f64,
light_dir: Vector3D,
sample: &LightSample,
normal: Vector3D,
view_dir: Vector3D,
material_specular: f64,
shininess: f64,
material: &Material,
) -> Vector3D {
let spec_factor: f64 = phong_specular_factor(light_dir, normal, view_dir, shininess);
let k: f64 = light_intensity * spec_factor * material_specular;
let spec_factor: f64 = phong_specular_factor(
sample.get_direction(),
normal,
view_dir,
material.get_shininess(),
);
let k: f64 = sample.get_intensity() * spec_factor * material.get_specular();
Vector3D::new(
light_color.get_x() * k,
light_color.get_y() * k,
light_color.get_z() * k,
sample.get_color().get_x() * k,
sample.get_color().get_y() * k,
sample.get_color().get_z() * k,
)
}

Expand Down Expand Up @@ -476,17 +477,21 @@ pub fn soft_shadow_factor(
if dist < EPSILON {
return 1.0;
}
let dir: Vector3D = to_light.scaled(1.0 / dist);
let segment: ShadowSegment = ShadowSegment {
origin,
direction: to_light.scaled(1.0 / dist),
distance: dist,
};
for &(center, radius) in occluders.iter() {
if segment_crosses_sphere(origin, dir, dist, center, radius) {
if segment_crosses_sphere(&segment, center, radius) {
return 0.0;
}
}
1.0
}

/// Returns whether either ray-sphere root lands strictly inside the open
/// segment `(EPSILON, dist - EPSILON)`.
/// segment `(EPSILON, distance - EPSILON)`.
///
/// **Both roots are tested, not just the near one.** A shadow ray is very
/// often cast from the surface point of one of the occluders themselves:
Expand All @@ -495,26 +500,27 @@ pub fn soft_shadow_factor(
/// the nearest root would report the light as visible through the very
/// sphere the ray starts inside.
///
/// The segment arrives as a [`ShadowSegment`] so the same start, unit
/// direction, and distance measured once by [`soft_shadow_factor`] reach
/// every occluder in the loop instead of being restated per call.
///
/// # Arguments
///
/// - `Vector3D` - The segment start (the shaded point).
/// - `Vector3D` - The unit direction toward the light.
/// - `f64` - The distance from the segment start to the light.
/// - `&ShadowSegment` - The segment start, unit direction toward the light,
/// and distance from the segment start to the light.
/// - `Vector3D` - The sphere center.
/// - `f64` - The sphere radius.
///
/// # Returns
///
/// - `bool` - `true` when the segment passes through the sphere's interior.
pub(crate) fn segment_crosses_sphere(
origin: Vector3D,
dir: Vector3D,
dist: f64,
segment: &ShadowSegment,
center: Vector3D,
radius: f64,
) -> bool {
let oc: Vector3D = origin - center;
let b: f64 = oc.dot(dir);
let oc: Vector3D = segment.get_origin() - center;
let b: f64 = oc.dot(segment.get_direction());
let c: f64 = oc.dot(oc) - radius * radius;
let disc: f64 = b * b - c;
if disc < 0.0 {
Expand All @@ -524,6 +530,6 @@ pub(crate) fn segment_crosses_sphere(
let near: f64 = -b - sq;
let far: f64 = -b + sq;
let inside: f64 = EPSILON;
let outside: f64 = dist - EPSILON;
let outside: f64 = segment.get_distance() - EPSILON;
(near > inside && near < outside) || (far > inside && far < outside)
}
26 changes: 12 additions & 14 deletions engine/src/lighting/impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,6 @@ impl LightingUniforms {
let mut color: Vector3D = self.get_ambient();
let view_dir: Vector3D = unit_vector(self.get_eye() - position);
let material_kind: MaterialKind = material.get_kind();
let albedo: Vector3D = material.get_albedo();
for light in self.get_lights().iter() {
let kind: LightType = light.get_kind();
// Derived once per light and shared by every term below. The
Expand Down Expand Up @@ -375,31 +374,30 @@ impl LightingUniforms {
if shadow <= 0.0 {
continue;
}
let intensity: f64 = light.get_intensity() * falloff_factor;
let light_color: Vector3D = light.get_color();
// One resolved light, shared by every term below: the diffuse
// and specular lobes and the fresnel scale all read this one
// colour, the one attenuated intensity, and the one derived
// direction, instead of each re-reading the `Light`.
let sample: LightSample = LightSample {
color: light.get_color(),
intensity: light.get_intensity() * falloff_factor,
direction: light_dir,
};
let diffuse: Vector3D = match material_kind {
MaterialKind::Pbr => {
let fresnel: f64 = apply_schlick_fresnel(light_dir, view_dir, normal);
lambert_term(light_color, intensity, light_dir, normal, albedo).scaled(
lambert_term(&sample, normal, material).scaled(
LIGHTING_PBR_ENERGY_CONSERVATION
+ (1.0 - LIGHTING_PBR_ENERGY_CONSERVATION) * fresnel,
)
}
MaterialKind::Lambert | MaterialKind::Phong => {
lambert_term(light_color, intensity, light_dir, normal, albedo)
lambert_term(&sample, normal, material)
}
};
let specular: Vector3D = match material_kind {
MaterialKind::Lambert | MaterialKind::Pbr => Vector3D::zero(),
MaterialKind::Phong => phong_specular_term(
light_color,
intensity,
light_dir,
normal,
view_dir,
material.get_specular(),
material.get_shininess(),
),
MaterialKind::Phong => phong_specular_term(&sample, normal, view_dir, material),
};
color += (diffuse + specular).scaled(shadow);
}
Expand Down
62 changes: 62 additions & 0 deletions engine/src/lighting/struct.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,68 @@ pub struct Material {
pub(crate) emissive: Vector3D,
}

/// One light's contribution at a single shaded point, resolved once and
/// then shared by every term that light feeds.
///
/// A [`Light`] stores the **authored** values: the intensity before any
/// falloff, and — for a spot light — the outward cone axis rather than the
/// direction toward the lamp. Shading needs different numbers: the
/// surface-to-light direction and the intensity after falloff. Deriving
/// them into locals keeps the [`Light`] itself un-cloned, but it leaves
/// each term reading a bare pair of `Vector3D` / `f64` parameters whose
/// roles no longer match the field names they came from.
///
/// A `LightSample` names the derived pair next to the colour they apply
/// to, so [`lambert_term`] and [`phong_specular_term`] take one borrowed
/// argument instead of three loose ones and neither rebuilds what the
/// shading loop already measured. The caller still decides the values:
/// [`compute_lambert`] samples the authored direction and intensity
/// directly, while [`LightingUniforms::shade_with_visibility`] samples the
/// derived direction and the falloff-attenuated intensity.
///
/// Every field is a `Copy` scalar or vector, so a sample is built on the
/// stack and never allocates.
#[derive(Clone, Copy, Debug, Getter)]
pub(crate) struct LightSample {
/// The light's RGB color.
#[get(pub(crate), type(copy))]
pub(crate) color: Vector3D,
/// The intensity, already scaled by any falloff the caller applied.
#[get(pub(crate), type(copy))]
pub(crate) intensity: f64,
/// The unit direction from the shaded surface toward the light.
#[get(pub(crate), type(copy))]
pub(crate) direction: Vector3D,
}

/// The bounded segment a shadow ray travels: a start, a unit direction, and
/// the length of the leg it covers.
///
/// [`soft_shadow_factor`] measures all three once per light and then tests
/// every occluder sphere against the same segment, which is what keeps the
/// open-interval bounds identical across the loop — [`EPSILON`] at the near
/// end and `distance - EPSILON` at the far end, so a root landing exactly
/// on an endpoint still counts as a miss.
///
/// The triple is declared here rather than borrowed from the ray tracer. A
/// ray-tracing `Ray` carries `t_min` / `t_max` / `depth` for scene
/// traversal, while a shadow leg here is defined by the two endpoints the
/// light and the shaded point supply; naming that pair as one value keeps
/// `lighting` free of a dependency on `raytracing` and of the cycle a
/// shared `Ray` would risk forming.
#[derive(Clone, Copy, Debug, Getter)]
pub(crate) struct ShadowSegment {
/// The shaded point the segment starts from.
#[get(pub(crate), type(copy))]
pub(crate) origin: Vector3D,
/// The unit direction from `origin` toward the light.
#[get(pub(crate), type(copy))]
pub(crate) direction: Vector3D,
/// The distance from `origin` to the light.
#[get(pub(crate), type(copy))]
pub(crate) distance: f64,
}

/// Bundles of uniforms supplied to lighting and shading routines.
#[derive(Clone, Data, Debug, New, PartialEq)]
pub struct LightingUniforms {
Expand Down
Loading
Loading