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:
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user