feat: Enhance Bevel & Emboss effect with contour support and algorithm improvements

- Implemented contour lookup functionality for Bevel & Emboss effect to allow for custom contour curves.
- Added contour parameter to LayerStyle and updated related structures and functions.
- Improved emboss algorithm by fixing height field calculations and ensuring proper handling of outside pixels.
- Introduced tilt component for 3D emboss effect, enhancing visual depth.
- Optimized MAE grid search parameters for better performance and accuracy.
- Expanded Iced GUI panel for Bevel & Emboss to include contour selection and additional styling options.
- Updated example scripts to reflect changes in the API and new features.
This commit is contained in:
2026-07-14 00:36:09 +03:00
parent 5e03065165
commit 58599bf8f9
11 changed files with 521 additions and 81 deletions
+2 -1
View File
@@ -64,7 +64,7 @@ pub fn apply_layer_effects(
if let Some(LayerEffect::BevelEmboss {
enabled: true, style, technique, depth, direction, size, soften,
angle, altitude, highlight_blend, highlight_color, highlight_opacity,
shadow_blend, shadow_color, shadow_opacity, ..
shadow_blend, shadow_color, shadow_opacity, contour, ..
}) = effects.iter().find(|e| matches!(e, LayerEffect::BevelEmboss { .. })) {
// Emboss is rendered on top of the original layer fill; do not replace
// it with a hard-coded grey. Photoshop keeps the original pixel color
@@ -74,6 +74,7 @@ pub fn apply_layer_effects(
*depth, *size, *soften, *angle, *altitude,
*direction, *technique, *style,
*highlight_color, *shadow_color,
contour,
);
// For Emboss and OuterBevel, the effect extends outside the shape.
+50 -13
View File
@@ -1,6 +1,31 @@
use crate::types;
use crate::helpers::box_blur_f32;
/// Look up a value in a contour curve using linear interpolation.
/// input is in [0.0, 1.0], output is remapped through the curve.
fn contour_lookup(contour: &types::ContourCurve, input: f32) -> f32 {
let input = input.clamp(0.0, 1.0);
let points = &contour.points;
if points.is_empty() {
return input;
}
// Find the two points that bracket the input
for i in 0..points.len() - 1 {
let (x0, y0) = points[i];
let (x1, y1) = points[i + 1];
if input >= x0 && input <= x1 {
let t = if (x1 - x0).abs() < 1e-6 {
0.0
} else {
(input - x0) / (x1 - x0)
};
return y0 + t * (y1 - y0);
}
}
// If input is beyond the last point, return the last y value
points.last().map(|p| p.1).unwrap_or(input)
}
/// Generate bevel & emboss highlight and shadow buffers.
///
/// **Purpose:** Renders Photoshop-style Bevel & Emboss layer effect. Produces
@@ -53,6 +78,7 @@ pub fn generate_bevel_emboss(
style: types::BevelStyle,
highlight_color: [u8; 4],
shadow_color: [u8; 4],
contour: &Option<types::ContourCurve>,
) -> (Vec<u8>, Vec<u8>) {
let px = (w * h) as usize;
let mut highlight_buf = vec![0u8; px * 4];
@@ -112,7 +138,9 @@ pub fn generate_bevel_emboss(
let val = match style {
types::BevelStyle::InnerBevel => {
if alpha[i] > 0 {
(d_in / radius).min(1.0) * radius
// Use linear ramp (d_in) instead of saturating at radius.
// This prevents plateau on large shapes.
d_in
} else {
0.0
}
@@ -126,21 +154,21 @@ pub fn generate_bevel_emboss(
}
types::BevelStyle::Emboss => {
// Emboss = inner bevel (inside) + outer bevel (outside) + tilt.
// tilt_scale controls the global slope for 3D emboss effect.
// Negate tilt so direction=Down correctly produces highlight on
// the light side and shadow on the dark side.
// Use linear d_in (no saturation) to avoid plateau on large shapes.
let px_x = (i % iw) as f32;
let px_y = (i / iw) as f32;
let tilt = ((px_x - cx) * light_x + (px_y - cy) * light_y) * radius * 0.05;
if alpha[i] > 0 {
(d_in / radius).min(1.0) * radius + tilt
d_in + tilt
} else {
((radius - d_out).max(0.0) / radius) * radius + tilt
// Outside: limit to half-radius to avoid excessive external effects
let outside_h = ((radius - d_out).max(0.0) / radius) * radius;
outside_h * 0.5 + tilt
}
}
_ => {
if alpha[i] > 0 {
(d_in / radius).min(1.0) * radius
d_in
} else {
0.0
}
@@ -150,8 +178,10 @@ pub fn generate_bevel_emboss(
}
// Apply soften box-blur on the height field.
// Even at soften=0, apply minimal 1px blur for smoother gradient estimation at edges.
let soften_radius = (soften.max(0.0).round() as i32).max(1);
// Even at soften=0, apply minimal 2px blur for smoother gradient estimation at edges.
// Emboss benefits from more blur to reduce wrinkled edges on large shapes.
let base_blur = if matches!(style, types::BevelStyle::Emboss) { 2 } else { 1 };
let soften_radius = (soften.max(0.0).round() as i32).max(base_blur);
height = box_blur_f32(&height, w, h, soften_radius);
// Depth controls how prominent the bevel appears.
@@ -250,12 +280,19 @@ pub fn generate_bevel_emboss(
}
let (hl_scale, sh_scale) = if matches!(style, crate::types::BevelStyle::Emboss) {
(0.05, 16.00)
(0.5, 16.00)
} else {
(0.05, 2.50)
(0.5, 5.0)
};
let hl_pred = (ndl * hl_scale).clamp(0.0, 1.0);
let sh_pred = (-ndl * sh_scale).clamp(0.0, 1.0);
let mut hl_pred = (ndl * hl_scale).clamp(0.0, 1.0);
let mut sh_pred = (-ndl * sh_scale).clamp(0.0, 1.0);
// Apply contour remapping if a contour curve is provided.
// The contour remaps the normalized lighting value [0,1] to a new profile.
if let Some(curve) = contour {
hl_pred = contour_lookup(curve, hl_pred);
sh_pred = contour_lookup(curve, sh_pred);
}
// For outside pixels (alpha[i]==0), use the nearest opaque pixel's
// alpha as the strength base. This is what Photoshop does: the outer
+24 -2
View File
@@ -21,6 +21,28 @@ pub struct ContourCurve {
pub points: Vec<(f32, f32)>,
}
/// Convert a contour name string to a ContourCurve with lookup points.
pub fn contour_name_to_curve(name: &str) -> Option<ContourCurve> {
match name {
"Linear" => None, // Linear is the default (no remapping needed)
"Cone" => Some(ContourCurve { points: vec![(0.0, 0.0), (0.5, 1.0), (1.0, 0.0)] }),
"Cone-Inverted" => Some(ContourCurve { points: vec![(0.0, 1.0), (0.5, 0.0), (1.0, 1.0)] }),
"Gaussian" => Some(ContourCurve { points: vec![(0.0, 0.0), (0.5, 0.85), (1.0, 1.0)] }),
"Half Round" => Some(ContourCurve { points: vec![(0.0, 0.0), (0.25, 0.75), (0.5, 1.0), (0.75, 0.75), (1.0, 0.0)] }),
"Round" => Some(ContourCurve { points: vec![(0.0, 0.0), (0.5, 1.0), (1.0, 0.0)] }),
"Ring" => Some(ContourCurve { points: vec![(0.0, 0.0), (0.25, 1.0), (0.5, 0.0), (0.75, 1.0), (1.0, 0.0)] }),
"Ring-Double" => Some(ContourCurve { points: vec![(0.0, 0.0), (0.125, 1.0), (0.25, 0.0), (0.375, 1.0), (0.5, 0.0), (0.625, 1.0), (0.75, 0.0), (0.875, 1.0), (1.0, 0.0)] }),
"Sawtooth" => Some(ContourCurve { points: vec![(0.0, 0.0), (0.5, 1.0), (0.5, 0.0), (1.0, 1.0)] }),
"Square" => Some(ContourCurve { points: vec![(0.0, 0.0), (0.5, 0.0), (0.5, 1.0), (1.0, 1.0)] }),
"Valley" => Some(ContourCurve { points: vec![(0.0, 1.0), (0.5, 0.0), (1.0, 1.0)] }),
"Shallow-Slope" => Some(ContourCurve { points: vec![(0.0, 0.0), (0.5, 0.25), (1.0, 1.0)] }),
"Wave" => Some(ContourCurve { points: vec![(0.0, 0.0), (0.25, 1.0), (0.5, 0.5), (0.75, 1.0), (1.0, 0.0)] }),
"Cove" => Some(ContourCurve { points: vec![(0.0, 0.0), (0.25, 0.5), (0.5, 1.0), (0.75, 0.5), (1.0, 0.0)] }),
"Washboard" => Some(ContourCurve { points: vec![(0.0, 0.0), (0.125, 1.0), (0.25, 0.0), (0.375, 1.0), (0.5, 0.0), (0.625, 1.0), (0.75, 0.0), (0.875, 1.0), (1.0, 0.0)] }),
_ => None, // Default to linear
}
}
#[derive(Debug, Clone)]
pub struct GradientDef {
pub color_stops: Vec<(u32, [u8; 4])>,
@@ -468,7 +490,7 @@ pub fn layer_style_to_effect(style: &hcie_protocol::LayerStyle) -> Option<LayerE
contour: None,
source: 0,
}),
LayerStyle::BevelEmboss { enabled, depth, size, angle, altitude, highlight_opacity, shadow_opacity, direction, style, technique, soften, highlight_blend_mode, highlight_color, shadow_blend_mode, shadow_color } => Some(LayerEffect::BevelEmboss {
LayerStyle::BevelEmboss { enabled, depth, size, angle, altitude, highlight_opacity, shadow_opacity, direction, style, technique, soften, highlight_blend_mode, highlight_color, shadow_blend_mode, shadow_color, contour } => Some(LayerEffect::BevelEmboss {
enabled: *enabled,
style: match style.as_str() {
"InnerBevel" => BevelStyle::InnerBevel,
@@ -500,7 +522,7 @@ pub fn layer_style_to_effect(style: &hcie_protocol::LayerStyle) -> Option<LayerE
shadow_blend: blend_mode_from_string(shadow_blend_mode),
shadow_color: *shadow_color,
shadow_opacity: *shadow_opacity,
contour: None,
contour: contour_name_to_curve(contour),
}),
LayerStyle::Satin { enabled, opacity, angle, distance, size, color, invert } => Some(LayerEffect::Satin {
enabled: *enabled,