bevel and emboss geliştirme

This commit is contained in:
2026-07-13 20:33:39 +03:00
parent 2284a7d81f
commit 5e03065165
25 changed files with 1165 additions and 405 deletions
+49 -2
View File
@@ -42,6 +42,14 @@ pub fn apply_layer_effects(
let alpha = extract_alpha(pixels, px_count);
// Storage for Emboss/OuterBevel outside portions (applied to `behind` later)
let mut bevel_hl_outside: Option<Vec<u8>> = None;
let mut bevel_sh_outside: Option<Vec<u8>> = None;
let mut bevel_hl_blend: hcie_blend::BlendMode = hcie_blend::BlendMode::Screen;
let mut bevel_sh_blend: hcie_blend::BlendMode = hcie_blend::BlendMode::Multiply;
let mut bevel_hl_op: f32 = 1.0;
let mut bevel_sh_op: f32 = 1.0;
// 1. Prepare inside content (original pixels + inside effects)
let mut inside = vec![0u8; px_count * 4];
inside.copy_from_slice(pixels);
@@ -67,8 +75,38 @@ pub fn apply_layer_effects(
*direction, *technique, *style,
*highlight_color, *shadow_color,
);
composite_effect(&mut inside, &highlight, *highlight_blend, *highlight_opacity, px_count);
composite_effect(&mut inside, &shadow, *shadow_blend, *shadow_opacity, px_count);
// For Emboss and OuterBevel, the effect extends outside the shape.
// Inside pixels go to `inside`, outside pixels go to `behind`.
let has_outer = matches!(*style, crate::types::BevelStyle::Emboss | crate::types::BevelStyle::OuterBevel);
if has_outer {
// Split highlight/shadow into inside and outside portions
let mut hl_inside = vec![0u8; px_count * 4];
let mut hl_outside = vec![0u8; px_count * 4];
let mut sh_inside = vec![0u8; px_count * 4];
let mut sh_outside = vec![0u8; px_count * 4];
for i in 0..px_count {
if alpha[i] > 0 {
hl_inside[i * 4..i * 4 + 4].copy_from_slice(&highlight[i * 4..i * 4 + 4]);
sh_inside[i * 4..i * 4 + 4].copy_from_slice(&shadow[i * 4..i * 4 + 4]);
} else {
hl_outside[i * 4..i * 4 + 4].copy_from_slice(&highlight[i * 4..i * 4 + 4]);
sh_outside[i * 4..i * 4 + 4].copy_from_slice(&shadow[i * 4..i * 4 + 4]);
}
}
composite_effect(&mut inside, &hl_inside, *highlight_blend, *highlight_opacity, px_count);
composite_effect(&mut inside, &sh_inside, *shadow_blend, *shadow_opacity, px_count);
// Store outside portions to apply to `behind` later
bevel_hl_outside = Some(hl_outside);
bevel_sh_outside = Some(sh_outside);
bevel_hl_blend = *highlight_blend;
bevel_sh_blend = *shadow_blend;
bevel_hl_op = *highlight_opacity;
bevel_sh_op = *shadow_opacity;
} else {
composite_effect(&mut inside, &highlight, *highlight_blend, *highlight_opacity, px_count);
composite_effect(&mut inside, &shadow, *shadow_blend, *shadow_opacity, px_count);
}
}
// b. Satin
@@ -159,6 +197,15 @@ pub fn apply_layer_effects(
}
}
// d. Emboss/OuterBevel outside portion (highlight then shadow)
// Applied to `behind` so the outer bevel band renders behind the shape.
if let Some(hl_out) = &bevel_hl_outside {
composite_effect(&mut behind, hl_out, bevel_hl_blend, bevel_hl_op, px_count);
}
if let Some(sh_out) = &bevel_sh_outside {
composite_effect(&mut behind, sh_out, bevel_sh_blend, bevel_sh_op, px_count);
}
// 3. Blend inside ON TOP of behind using Normal blend
for i in 0..px_count {
let b = [behind[i * 4], behind[i * 4 + 1], behind[i * 4 + 2], behind[i * 4 + 3]];
+3 -2
View File
@@ -68,8 +68,6 @@ pub(crate) fn generate_shadow(
// MAE-optimized against Krita ground truth (768 samples, 350 combos, 800x800)
// Best: blur_factor=5.0, passes=5, spread_scale=1.00
// Actually use the spread parameter (0.0 to 100.0) from the user
apply_spread(&mut offset_alpha, spread);
let radius = size.max(0.0).round() as i32;
if radius > 0 {
@@ -81,6 +79,9 @@ pub(crate) fn generate_shadow(
offset_alpha = f32_buf.iter().map(|&a| (a as u8).min(255)).collect();
}
// Actually use the spread parameter (0.0 to 100.0) from the user AFTER the blur!
apply_spread(&mut offset_alpha, spread);
for i in 0..px {
let a = offset_alpha[i];
if a > 0 {
+195 -30
View File
@@ -1,5 +1,44 @@
use crate::types;
use crate::helpers::box_blur_f32;
/// Generate bevel & emboss highlight and shadow buffers.
///
/// **Purpose:** Renders Photoshop-style Bevel & Emboss layer effect. Produces
/// two separate RGBA buffers (highlight + shadow) that the caller composites
/// over the layer content using Screen and Multiply blend modes respectively.
///
/// **Logic & Workflow:**
/// 1. Compute inside/outside Euclidean Distance Transforms (EDT) from the
/// layer alpha mask. For Emboss and OuterBevel, also track which opaque
/// pixel is nearest to each outside pixel so the outer bevel can inherit
/// the alpha of the nearest content pixel.
/// 2. Build a continuous height field from the EDT values. The profile shape
/// depends on the BevelStyle:
/// - InnerBevel: ramps up from edge to center inside the shape.
/// - OuterBevel: ramps down from edge outward outside the shape.
/// - Emboss: combines both — ramps inside (raised) and outside (lowered).
/// 3. Apply a box-blur soften pass on the height field.
/// 4. Compute per-pixel lighting via central-difference normals · light vector.
/// 5. Apply a lighting blur to smooth wrinkles at sharp corners.
/// 6. Map lighting to highlight/shadow alpha. For outside pixels, use the
/// nearest opaque pixel's alpha instead of the (zero) local alpha.
///
/// **Arguments:**
/// * `alpha` — Layer alpha mask (one byte per pixel)
/// * `w`, `h` — Canvas dimensions
/// * `depth` — Bevel depth (01000+, controls slope steepness)
/// * `size` — Bevel size in pixels (radius of the bevel band)
/// * `soften` — Soften amount in pixels (extra blur on height field)
/// * `angle` — Light azimuth angle in degrees
/// * `altitude` — Light altitude angle in degrees
/// * `direction` — Up or Down (flips lighting)
/// * `technique` — Smooth, ChiselHard, ChiselSoft
/// * `style` — InnerBevel, OuterBevel, Emboss, etc.
/// * `highlight_color` — RGBA color for the highlight channel
/// * `shadow_color` — RGBA color for the shadow channel
///
/// **Returns:**
/// `(highlight_buf, shadow_buf)` — two RGBA buffers, same size as canvas.
pub fn generate_bevel_emboss(
alpha: &[u8],
w: u32,
@@ -27,6 +66,16 @@ pub fn generate_bevel_emboss(
let dist_inside_sq = edt_inside(alpha, iw, ih);
let dist_outside_sq = edt_outside(alpha, iw, ih);
// For Emboss and OuterBevel, track the nearest opaque source pixel so
// the outer bevel band can inherit the alpha of the nearest content pixel.
// This is critical: without it, alpha[i]==0 outside the shape produces
// zero-strength highlight/shadow, making Emboss/OuterBevel invisible.
let outside_src = if matches!(style, types::BevelStyle::Emboss | types::BevelStyle::OuterBevel) {
edt_outside_src(alpha, iw, ih)
} else {
vec![0usize; px]
};
// Compute shape center for Emboss tilt
let (cx, cy) = {
let mut min_x = iw;
@@ -76,15 +125,17 @@ 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.
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;
let tilt = ((px_x - cx) * light_x + (px_y - cy) * light_y) * radius * 0.05;
if alpha[i] > 0 {
let edge_factor = (d_in / radius).min(1.0);
radius * edge_factor + tilt
(d_in / radius).min(1.0) * radius + tilt
} else {
let t = (d_out / radius).min(1.0);
(radius * (1.0 - t) * 0.5 + tilt).max(0.0)
((radius - d_out).max(0.0) / radius) * radius + tilt
}
}
_ => {
@@ -155,13 +206,7 @@ pub fn generate_bevel_emboss(
let ndl = nx * light_x + ny * light_y + nz * light_z;
let factor = if matches!(style, types::BevelStyle::Emboss) && is_inner {
let height_factor = height[i] / radius;
let blended = ndl * 0.4 + height_factor * 0.6;
blended * depth_scale
} else {
ndl
};
let factor = ndl;
lighting[i] = match direction {
types::Direction::Up => factor,
@@ -171,46 +216,59 @@ pub fn generate_bevel_emboss(
}
// Smooth the lighting to reduce wrinkles at sharp corners.
// MAE-optimized against Krita ground truth (320 samples, 256×256).
// Best: lighting_blur=5, hl_scale=0.30, sh_scale=2.00, avg_mae=17.18
// Emboss uses tilt for 3D effect and needs sharp lighting (no blur).
// Inner/Outer bevel need blur for smooth corners.
// MAE-optimized against Photoshop ground truth (288 emboss samples, 256×256).
// BEGIN_TUNING_ZONE
let lighting_blur = (soften_radius + 5).max(1);
let lighting_blur = if matches!(style, crate::types::BevelStyle::Emboss) { 0 } else { 5 };
lighting = box_blur_f32(&lighting, w, h, lighting_blur);
for i in 0..px {
let is_inner = alpha[i] > 0;
let show = match style {
types::BevelStyle::InnerBevel => is_inner,
types::BevelStyle::OuterBevel => !is_inner,
types::BevelStyle::Emboss => true,
crate::types::BevelStyle::InnerBevel => is_inner,
crate::types::BevelStyle::OuterBevel => !is_inner,
crate::types::BevelStyle::Emboss => true,
_ => is_inner,
};
if !show { continue; }
let dist_abs = if is_inner { dist_inside_sq[i].sqrt() } else { dist_outside_sq[i].sqrt() };
if !is_inner && dist_abs > radius { continue; }
let technique_show = match technique {
types::Technique::Smooth => true,
crate::types::Technique::Smooth => true,
_ => dist_abs <= radius,
};
if !technique_show { continue; }
let ndl = lighting[i];
let is_flat = is_inner && dist_abs > radius;
// Emboss has tilt so the interior is never truly flat — skip the
// is_flat shortcut for emboss to allow tilt-based highlight/shadow
// across the entire shape interior.
let is_flat = is_inner && dist_abs > radius && !matches!(style, crate::types::BevelStyle::Emboss);
if is_flat {
if style == types::BevelStyle::Emboss && depth > 100.0 {
shadow_buf[i * 4 + 3] = alpha[i];
}
continue;
}
// MAE-optimized against Krita ground truth (320 samples, 256×256).
// Best: lighting_blur=5, hl_scale=0.30, sh_scale=2.00, avg_mae=17.18
let hl_pred = (ndl * 0.30).clamp(0.0, 1.0);
let sh_pred = (-ndl * 2.00).clamp(0.0, 1.0);
let (hl_scale, sh_scale) = if matches!(style, crate::types::BevelStyle::Emboss) {
(0.05, 16.00)
} else {
(0.05, 2.50)
};
let hl_pred = (ndl * hl_scale).clamp(0.0, 1.0);
let sh_pred = (-ndl * sh_scale).clamp(0.0, 1.0);
let hl_a = (alpha[i] as f32 * hl_pred).round() as u8;
let sh_a = (alpha[i] as f32 * sh_pred).round() as u8;
// For outside pixels (alpha[i]==0), use the nearest opaque pixel's
// alpha as the strength base. This is what Photoshop does: the outer
// bevel band fades out based on distance to the shape edge, and the
// effect intensity is proportional to the nearest content alpha.
let base_alpha = if is_inner {
alpha[i] as f32
} else {
alpha[outside_src[i]] as f32
};
let hl_a = (base_alpha * hl_pred).round() as u8;
let sh_a = (base_alpha * sh_pred).round() as u8;
if hl_a > 0 {
highlight_buf[i * 4] = highlight_color[0];
@@ -267,6 +325,33 @@ fn edt_outside(alpha: &[u8], w: usize, h: usize) -> Vec<f32> {
dt
}
/// EDT for outside pixels with source index tracking.
///
/// **Purpose:** Same as `edt_outside` but also returns, for each pixel, the
/// index of the nearest opaque pixel. This is needed by Emboss and OuterBevel
/// to give outside (transparent) pixels the alpha of the nearest content pixel.
///
/// **Returns:** `Vec<usize>` where each element is the pixel index of the
/// nearest opaque source pixel.
fn edt_outside_src(alpha: &[u8], w: usize, h: usize) -> Vec<usize> {
let n = w * h;
let mut dt = vec![1e20f32; n];
let mut src = vec![0usize; n];
for y in 0..h {
for x in 0..w {
let i = y * w + x;
if alpha[i] > 0 {
dt[i] = 0.0;
src[i] = i;
}
}
}
edt_2d_src(&mut dt, &mut src, w, h);
src
}
/// 2D Euclidean Distance Transform using the Felzenszwalb & Huttenlocher algorithm.
fn edt_2d(dt: &mut [f32], w: usize, h: usize) {
let mut col = vec![0.0f32; h];
@@ -336,4 +421,84 @@ fn edt_1d(f: &mut [f32]) {
}
f.copy_from_slice(&d);
}
/// 2D EDT with source index tracking (Felzenszwalb & Huttenlocher).
fn edt_2d_src(dt: &mut [f32], src_idx: &mut [usize], w: usize, h: usize) {
let mut col = vec![0.0f32; h];
let mut col_src = vec![0usize; h];
for x in 0..w {
for y in 0..h {
col[y] = dt[y * w + x];
col_src[y] = src_idx[y * w + x];
}
edt_1d_src(&mut col, &mut col_src);
for y in 0..h {
dt[y * w + x] = col[y];
src_idx[y * w + x] = col_src[y];
}
}
let mut row = vec![0.0f32; w];
let mut row_src = vec![0usize; w];
for y in 0..h {
for x in 0..w {
row[x] = dt[y * w + x];
row_src[x] = src_idx[y * w + x];
}
edt_1d_src(&mut row, &mut row_src);
for x in 0..w {
dt[y * w + x] = row[x];
src_idx[y * w + x] = row_src[x];
}
}
}
/// 1D EDT with source index tracking (Felzenszwalb & Huttenlocher).
fn edt_1d_src(f: &mut [f32], src_idx: &mut [usize]) {
let n = f.len();
if n == 0 { return; }
let mut d = vec![0.0f32; n];
let mut d_src = vec![0usize; n];
let mut v = vec![0usize; n];
let mut z = vec![0.0f32; n + 1];
let mut k = 0usize;
v[0] = 0;
z[0] = f32::NEG_INFINITY;
z[1] = f32::INFINITY;
for q in 1..n {
let qf = q as f32;
let fq = f[q] + qf * qf;
let vk = v[k];
let fv_vk = f[vk] + vk as f32 * vk as f32;
let mut s = (fq - fv_vk) / (2.0 * qf - 2.0 * vk as f32);
while s <= z[k] {
k = k.wrapping_sub(1);
if k == usize::MAX { k = 0; break; }
let vk2 = v[k];
let fv_vk2 = f[vk2] + vk2 as f32 * vk2 as f32;
s = (fq - fv_vk2) / (2.0 * qf - 2.0 * vk2 as f32);
}
k += 1;
v[k] = q;
z[k] = s;
z[k + 1] = f32::INFINITY;
}
k = 0;
for q in 0..n {
let qf = q as f32;
while z[k + 1] < qf { k += 1; }
let vk = v[k];
let diff = qf - vk as f32;
d[q] = diff * diff + f[vk];
d_src[q] = src_idx[v[k]];
}
f.copy_from_slice(&d);
src_idx.copy_from_slice(&d_src);
}
+2 -2
View File
@@ -64,8 +64,6 @@ pub(crate) fn generate_inner_shadow(
}
}
apply_spread(&mut offset_alpha, choke);
let radius = size.max(0.0).round() as i32;
if radius > 0 {
let gaussian_r = (radius as f32 / 10.0).round().max(1.0) as i32;
@@ -75,6 +73,8 @@ pub(crate) fn generate_inner_shadow(
offset_alpha = f32_buf.iter().map(|&a| (a as u8).min(255)).collect();
}
apply_spread(&mut offset_alpha, choke);
for i in 0..px {
let a = ((offset_alpha[i] as f32 * 0.50) as u32 * alpha[i] as u32 / 255) as u8;
if a > 0 {
+35 -17
View File
@@ -19,6 +19,7 @@ pub fn generate_bevel_tuned(
hl_scale: f32,
sh_scale: f32,
profile_exp: f32,
tilt_scale: f32,
) -> (Vec<u8>, Vec<u8>) {
let hl_color = [255u8; 4];
let sh_color = [0u8; 4];
@@ -33,6 +34,14 @@ pub fn generate_bevel_tuned(
let dist_inside_sq = edt_inside(alpha, iw, ih);
let dist_outside_sq = edt_outside(alpha, iw, ih);
// For Emboss and OuterBevel, track the nearest opaque source pixel so
// the outer bevel band can inherit the alpha of the nearest content pixel.
let outside_src = if matches!(*style, types::BevelStyle::Emboss | types::BevelStyle::OuterBevel) {
edt_outside_src(alpha, iw, ih)
} else {
vec![0usize; px]
};
// Compute shape center for Emboss tilt
let (cx, cy) = {
let mut min_x = iw;
@@ -81,15 +90,22 @@ pub fn generate_bevel_tuned(
}
}
types::BevelStyle::Emboss => {
// Emboss = inner bevel (inside) + outer bevel (outside) + tilt.
// tilt_scale controls how much global slope is added across the
// shape based on light direction. The tilt makes one side of
// the shape raised (toward light = highlight) and the other
// lowered (away from light = shadow), producing the 3D effect.
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;
// Negate tilt so direction=Down (default) correctly produces
// highlight on the light side and shadow on the dark side.
let tilt = ((px_x - cx) * light_x + (px_y - cy) * light_y) * radius * tilt_scale;
if alpha[i] > 0 {
let t = (d_in / radius).min(1.0);
radius * t.powf(profile_exp) + tilt
t.powf(profile_exp) * radius + tilt
} else {
let t = (d_out / radius).min(1.0);
(radius * (1.0 - t).powf(profile_exp) * 0.5 + tilt).max(0.0)
let t = ((radius - d_out).max(0.0) / radius).min(1.0);
(1.0 - t).powf(profile_exp) * radius + tilt
}
}
_ => {
@@ -142,13 +158,7 @@ pub fn generate_bevel_tuned(
let ndl = nx * light_x + ny * light_y + nz * light_z;
let factor = if matches!(style, types::BevelStyle::Emboss) && is_inner {
let height_factor = height[i] / radius;
let blended = ndl * 0.4 + height_factor * 0.6;
blended * depth_scale
} else {
ndl
};
let factor = ndl;
lighting[i] = match direction {
types::Direction::Up => factor,
@@ -171,19 +181,27 @@ pub fn generate_bevel_tuned(
let dist_abs = if is_inner { dist_inside_sq[i].sqrt() } else { dist_outside_sq[i].sqrt() };
if !is_inner && dist_abs > radius { continue; }
let ndl = lighting[i];
let is_flat = is_inner && dist_abs > radius;
// Emboss has tilt so the interior is never truly flat — skip the
// is_flat shortcut for emboss to allow tilt-based highlight/shadow
// across the entire shape interior.
let is_flat = is_inner && dist_abs > radius && !matches!(*style, types::BevelStyle::Emboss);
if is_flat {
if *style == types::BevelStyle::Emboss && depth > 100.0 {
shadow_buf[i * 4 + 3] = alpha[i];
}
continue;
}
let hl_pred = (ndl * hl_scale).clamp(0.0, 1.0);
let sh_pred = (-ndl * sh_scale).clamp(0.0, 1.0);
let hl_a = (alpha[i] as f32 * hl_pred).round() as u8;
let sh_a = (alpha[i] as f32 * sh_pred).round() as u8;
// For outside pixels (alpha[i]==0), use the nearest opaque pixel's
// alpha as the strength base, matching Photoshop behavior.
let base_alpha = if is_inner {
alpha[i] as f32
} else {
alpha[outside_src[i]] as f32
};
let hl_a = (base_alpha * hl_pred).round() as u8;
let sh_a = (base_alpha * sh_pred).round() as u8;
if hl_a > 0 {
highlight_buf[i * 4] = hl_color[0];