feat(jump-hop): optimize generated assets and runtime background

This commit is contained in:
2026-06-04 22:34:19 +08:00
parent c442c3c3f0
commit 0041b95f72
17 changed files with 1160 additions and 200 deletions

View File

@@ -2,13 +2,80 @@ use super::color::{
GENERATED_ASSET_SHEET_GREEN_SCREEN_MIN_SCORE, GENERATED_ASSET_SHEET_GREEN_SCREEN_SOFT_SCORE,
GENERATED_ASSET_SHEET_HARD_GREEN_SCREEN_SCORE, clamp_generated_asset_sheet_unit,
compute_generated_asset_sheet_green_screen_score,
compute_generated_asset_sheet_key_color_score,
compute_generated_asset_sheet_white_screen_score,
is_generated_asset_sheet_soft_green_matte_pixel, lerp_generated_asset_sheet_channel,
touches_generated_asset_sheet_background_mask,
};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct GeneratedAssetSheetKeyColor {
pub red: u8,
pub green: u8,
pub blue: u8,
}
impl GeneratedAssetSheetKeyColor {
pub const GREEN_SCREEN: Self = Self {
red: 0,
green: 255,
blue: 0,
};
pub const MAGENTA_SCREEN: Self = Self {
red: 255,
green: 0,
blue: 255,
};
pub fn is_green_screen(self) -> bool {
self == Self::GREEN_SCREEN
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct GeneratedAssetSheetAlphaOptions {
pub key_color: GeneratedAssetSheetKeyColor,
pub remove_near_white_background: bool,
pub remove_disconnected_hard_key_background: bool,
}
impl GeneratedAssetSheetAlphaOptions {
pub const fn green_screen() -> Self {
Self {
key_color: GeneratedAssetSheetKeyColor::GREEN_SCREEN,
remove_near_white_background: true,
remove_disconnected_hard_key_background: true,
}
}
pub const fn jump_hop_magenta_screen() -> Self {
Self {
key_color: GeneratedAssetSheetKeyColor::MAGENTA_SCREEN,
remove_near_white_background: false,
remove_disconnected_hard_key_background: false,
}
}
}
impl Default for GeneratedAssetSheetAlphaOptions {
fn default() -> Self {
Self::green_screen()
}
}
pub fn apply_generated_asset_sheet_green_screen_alpha(
source: image::DynamicImage,
) -> image::DynamicImage {
apply_generated_asset_sheet_alpha_with_options(
source,
GeneratedAssetSheetAlphaOptions::default(),
)
}
pub fn apply_generated_asset_sheet_alpha_with_options(
source: image::DynamicImage,
options: GeneratedAssetSheetAlphaOptions,
) -> image::DynamicImage {
let mut image = source.to_rgba8();
let (width, height) = image.dimensions();
@@ -16,6 +83,7 @@ pub fn apply_generated_asset_sheet_green_screen_alpha(
image.as_mut(),
width as usize,
height as usize,
options,
);
image::DynamicImage::ImageRgba8(image)
}
@@ -24,13 +92,14 @@ fn remove_generated_asset_sheet_green_screen_background(
pixels: &mut [u8],
width: usize,
height: usize,
options: GeneratedAssetSheetAlphaOptions,
) -> bool {
let pixel_count = width.saturating_mul(height);
if pixel_count == 0 || pixels.len() < pixel_count.saturating_mul(4) {
return false;
}
let mut green_scores = vec![0.0f32; pixel_count];
let mut key_scores = vec![0.0f32; pixel_count];
let mut white_scores = vec![0.0f32; pixel_count];
let mut background_hints = vec![0.0f32; pixel_count];
let mut background_mask = vec![0u8; pixel_count];
@@ -43,16 +112,19 @@ fn remove_generated_asset_sheet_green_screen_background(
let green = pixels[offset + 1];
let blue = pixels[offset + 2];
let alpha = pixels[offset + 3];
let green_score =
compute_generated_asset_sheet_green_screen_score([red, green, blue, alpha]);
let white_score =
compute_generated_asset_sheet_white_screen_score([red, green, blue, alpha]);
let key_score =
compute_generated_asset_sheet_key_score([red, green, blue, alpha], options.key_color);
let white_score = if options.remove_near_white_background {
compute_generated_asset_sheet_white_screen_score([red, green, blue, alpha])
} else {
0.0
};
let transparency_hint =
clamp_generated_asset_sheet_unit((56.0 - alpha as f32) / 56.0) * 0.75;
green_scores[pixel_index] = green_score;
key_scores[pixel_index] = key_score;
white_scores[pixel_index] = white_score;
background_hints[pixel_index] = green_score.max(white_score).max(transparency_hint);
background_hints[pixel_index] = key_score.max(white_score).max(transparency_hint);
}
let seed_background_pixel =
@@ -62,10 +134,10 @@ fn remove_generated_asset_sheet_green_screen_background(
}
let alpha = pixels[pixel_index * 4 + 3];
let strong_candidate = alpha < 40
|| green_scores[pixel_index] >= GENERATED_ASSET_SHEET_HARD_GREEN_SCREEN_SCORE
|| key_scores[pixel_index] >= GENERATED_ASSET_SHEET_HARD_GREEN_SCREEN_SCORE
|| (alpha < 224
&& green_scores[pixel_index] > GENERATED_ASSET_SHEET_GREEN_SCREEN_MIN_SCORE)
|| white_scores[pixel_index] > 0.32;
&& key_scores[pixel_index] > GENERATED_ASSET_SHEET_GREEN_SCREEN_MIN_SCORE)
|| (options.remove_near_white_background && white_scores[pixel_index] > 0.32);
if !strong_candidate {
return;
}
@@ -113,26 +185,34 @@ fn remove_generated_asset_sheet_green_screen_background(
}
let next_offset = next_pixel_index * 4;
let alpha = pixels[next_offset + 3];
let green_score = green_scores[next_pixel_index];
let key_score = key_scores[next_pixel_index];
let white_score = white_scores[next_pixel_index];
let hint = background_hints[next_pixel_index];
let reachable_soft_edge = hint > 0.08
&& alpha < 224
&& (green_score > 0.04 || white_score > 0.08 || alpha < 180);
let green_background = green_score >= GENERATED_ASSET_SHEET_HARD_GREEN_SCREEN_SCORE
|| (alpha < 224 && green_score > GENERATED_ASSET_SHEET_GREEN_SCREEN_MIN_SCORE);
if alpha < 40 || green_background || white_score > 0.32 || reachable_soft_edge {
&& (key_score > 0.04
|| (options.remove_near_white_background && white_score > 0.08)
|| alpha < 180);
let key_background = key_score >= GENERATED_ASSET_SHEET_HARD_GREEN_SCREEN_SCORE
|| (alpha < 224 && key_score > GENERATED_ASSET_SHEET_GREEN_SCREEN_MIN_SCORE);
if alpha < 40
|| key_background
|| (options.remove_near_white_background && white_score > 0.32)
|| reachable_soft_edge
{
background_mask[next_pixel_index] = 1;
queue.push(next_pixel_index);
}
}
}
for pixel_index in 0..pixel_count {
if background_mask[pixel_index] == 0
&& green_scores[pixel_index] >= GENERATED_ASSET_SHEET_HARD_GREEN_SCREEN_SCORE
{
background_mask[pixel_index] = 1;
if options.remove_disconnected_hard_key_background {
for pixel_index in 0..pixel_count {
if background_mask[pixel_index] == 0
&& key_scores[pixel_index] >= GENERATED_ASSET_SHEET_HARD_GREEN_SCREEN_SCORE
{
background_mask[pixel_index] = 1;
}
}
}
@@ -153,10 +233,14 @@ fn remove_generated_asset_sheet_green_screen_background(
pixels[offset + 2],
pixels[offset + 3],
];
let green_score = green_scores[pixel_index];
let key_score = key_scores[pixel_index];
let white_score = white_scores[pixel_index];
if !is_generated_asset_sheet_soft_green_matte_pixel(pixel, green_score, white_score)
{
if !is_generated_asset_sheet_soft_key_matte_pixel(
pixel,
key_score,
white_score,
options,
) {
continue;
}
if !touches_generated_asset_sheet_background_mask(
@@ -188,12 +272,12 @@ fn remove_generated_asset_sheet_green_screen_background(
continue;
}
let alpha = pixels[pixel_index * 4 + 3];
let green_score = green_scores[pixel_index];
let key_score = key_scores[pixel_index];
let white_score = white_scores[pixel_index];
let hint = background_hints[pixel_index];
let soft_matte_candidate = alpha < 224
|| white_score > 0.10
|| green_score >= GENERATED_ASSET_SHEET_HARD_GREEN_SCREEN_SCORE;
|| (options.remove_near_white_background && white_score > 0.10)
|| key_score >= GENERATED_ASSET_SHEET_HARD_GREEN_SCREEN_SCORE;
if hint < GENERATED_ASSET_SHEET_GREEN_SCREEN_SOFT_SCORE || !soft_matte_candidate {
continue;
}
@@ -278,9 +362,9 @@ fn remove_generated_asset_sheet_green_screen_background(
continue;
}
let green_score = green_scores[pixel_index];
let key_score = key_scores[pixel_index];
let white_score = white_scores[pixel_index];
let contamination = green_score.max(white_score).max(if alpha < 220 {
let contamination = key_score.max(white_score).max(if alpha < 220 {
((220 - alpha) as f32 / 220.0) * 0.25
} else {
0.0
@@ -308,23 +392,23 @@ fn remove_generated_asset_sheet_green_screen_background(
green = lerp_generated_asset_sheet_channel(green, sample_green as f32, blend);
blue = lerp_generated_asset_sheet_channel(blue, sample_blue as f32, blend);
if green_score > 0.04 {
if options.key_color.is_green_screen() && key_score > 0.04 {
green = green.min(sample_green as f32 + 18.0);
}
if white_score > 0.1 {
if options.remove_near_white_background && white_score > 0.1 {
red = red.min(sample_red as f32 + 26.0);
green = green.min(sample_green as f32 + 26.0);
blue = blue.min(sample_blue as f32 + 26.0);
}
} else {
if green_score > 0.04 {
if options.key_color.is_green_screen() && key_score > 0.04 {
let toned_green = (green - (green - red.max(blue)) * 0.78)
.round()
.max(red.max(blue));
green = green.min(toned_green).min(red.max(blue) + 18.0);
}
if white_score > 0.12 {
if options.remove_near_white_background && white_score > 0.12 {
let spread = red.max(green).max(blue) - red.min(green).min(blue);
if spread < 20.0 {
let toned_value = ((red + green + blue) / 3.0 * 0.88).round();
@@ -336,7 +420,7 @@ fn remove_generated_asset_sheet_green_screen_background(
}
let mut next_alpha = alpha;
let edge_fade = (green_score * 0.35).max(white_score * 0.28);
let edge_fade = (key_score * 0.35).max(white_score * 0.28);
if edge_fade > 0.08 {
next_alpha = ((alpha as f32) * (1.0 - edge_fade)).round() as u8;
if next_alpha < 10 {
@@ -364,6 +448,35 @@ fn remove_generated_asset_sheet_green_screen_background(
changed
}
fn compute_generated_asset_sheet_key_score(
pixel: [u8; 4],
key_color: GeneratedAssetSheetKeyColor,
) -> f32 {
if key_color.is_green_screen() {
return compute_generated_asset_sheet_green_screen_score(pixel);
}
compute_generated_asset_sheet_key_color_score(
pixel,
[key_color.red, key_color.green, key_color.blue],
)
}
fn is_generated_asset_sheet_soft_key_matte_pixel(
pixel: [u8; 4],
key_score: f32,
white_score: f32,
options: GeneratedAssetSheetAlphaOptions,
) -> bool {
if options.key_color.is_green_screen() {
return is_generated_asset_sheet_soft_green_matte_pixel(pixel, key_score, white_score);
}
pixel[3] != 0
&& key_score >= GENERATED_ASSET_SHEET_GREEN_SCREEN_MIN_SCORE
&& (!options.remove_near_white_background || white_score < 0.34)
}
fn collect_generated_asset_sheet_foreground_neighbor_color(
pixels: &[u8],
width: usize,