Merge remote-tracking branch 'origin/codex/wooden-fish-template'
This commit is contained in:
@@ -1539,111 +1539,11 @@ pub(super) fn slice_match3d_material_sheet(
|
||||
image: &DownloadedOpenAiImage,
|
||||
item_names: &[String],
|
||||
) -> Result<Vec<Vec<Match3DSlicedItemImage>>, AppError> {
|
||||
// 中文注释:素材图提示词固定要求 5*5 均匀排布;切图也固定按 5 行 5 列定位格子。
|
||||
// 每个格子内再基于前景像素二次校准,避免固定内缩裁断物品边缘。
|
||||
let source = image::load_from_memory(image.bytes.as_slice()).map_err(|error| {
|
||||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": "match3d-assets",
|
||||
"message": format!("抓大鹅素材图解码失败:{error}"),
|
||||
}))
|
||||
})?;
|
||||
// 中文注释:素材图按绿幕背景生成;先把整张 sheet 的绿幕转成 alpha,再进入格子裁切。
|
||||
let source = apply_match3d_material_green_screen_alpha(source);
|
||||
let (width, height) = source.dimensions();
|
||||
let row_count = MATCH3D_MATERIAL_GRID_SIZE;
|
||||
let cell_width = width / MATCH3D_MATERIAL_GRID_SIZE;
|
||||
let cell_height = height / row_count;
|
||||
if cell_width == 0 || cell_height == 0 {
|
||||
return Err(
|
||||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": "match3d-assets",
|
||||
"message": "抓大鹅素材图尺寸过小,无法切割",
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
let mut slices = Vec::with_capacity(item_names.len());
|
||||
for item_index in 0..item_names.len().min(MATCH3D_MATERIAL_ITEM_BATCH_SIZE) {
|
||||
let row = item_index as u32;
|
||||
let mut views = Vec::with_capacity(MATCH3D_ITEM_VIEW_COUNT);
|
||||
for view_index in 0..MATCH3D_ITEM_VIEW_COUNT {
|
||||
let col = view_index as u32;
|
||||
let (crop_x, crop_y, crop_width, crop_height) =
|
||||
resolve_match3d_material_cell_crop(&source, row_count, row, col);
|
||||
let cropped = source.crop_imm(crop_x, crop_y, crop_width, crop_height);
|
||||
let cleaned = crop_match3d_material_view_edge_matte(cropped);
|
||||
let mut cursor = std::io::Cursor::new(Vec::new());
|
||||
cleaned
|
||||
.write_to(&mut cursor, ImageFormat::Png)
|
||||
.map_err(|error| {
|
||||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": "match3d-assets",
|
||||
"message": format!("抓大鹅素材图切割失败:{error}"),
|
||||
}))
|
||||
})?;
|
||||
views.push(Match3DSlicedItemImage {
|
||||
bytes: cursor.into_inner(),
|
||||
});
|
||||
}
|
||||
slices.push(views);
|
||||
}
|
||||
|
||||
Ok(slices)
|
||||
}
|
||||
|
||||
fn resolve_match3d_material_cell_crop(
|
||||
source: &image::DynamicImage,
|
||||
row_count: u32,
|
||||
row: u32,
|
||||
col: u32,
|
||||
) -> (u32, u32, u32, u32) {
|
||||
let (image_width, image_height) = source.dimensions();
|
||||
let cell = resolve_match3d_material_cell_bounds(image_width, image_height, row_count, row, col);
|
||||
let Some(foreground) = detect_match3d_material_foreground_bounds(source, cell) else {
|
||||
return cell.to_crop_tuple();
|
||||
};
|
||||
|
||||
let cell_width = cell.width();
|
||||
let cell_height = cell.height();
|
||||
let pad_x = (cell_width / 16).clamp(4, 16);
|
||||
let pad_y = (cell_height / 16).clamp(4, 16);
|
||||
let crop = Match3DMaterialCellBounds {
|
||||
x0: foreground.x0.saturating_sub(pad_x).max(cell.x0),
|
||||
y0: foreground.y0.saturating_sub(pad_y).max(cell.y0),
|
||||
x1: foreground.x1.saturating_add(pad_x).min(cell.x1),
|
||||
y1: foreground.y1.saturating_add(pad_y).min(cell.y1),
|
||||
};
|
||||
|
||||
crop.to_crop_tuple()
|
||||
}
|
||||
|
||||
pub(super) fn crop_match3d_material_view_edge_matte(
|
||||
image: image::DynamicImage,
|
||||
) -> image::DynamicImage {
|
||||
let mut image = image.to_rgba8();
|
||||
let (width, height) = image.dimensions();
|
||||
remove_match3d_material_view_edge_matte(image.as_mut(), width as usize, height as usize);
|
||||
let bounds = detect_match3d_material_visible_bounds(&image).unwrap_or_else(|| {
|
||||
Match3DMaterialCellBounds {
|
||||
x0: 0,
|
||||
y0: 0,
|
||||
x1: width,
|
||||
y1: height,
|
||||
}
|
||||
});
|
||||
if bounds.x0 == 0 && bounds.y0 == 0 && bounds.x1 == width && bounds.y1 == height {
|
||||
return image::DynamicImage::ImageRgba8(image);
|
||||
}
|
||||
|
||||
image::DynamicImage::ImageRgba8(
|
||||
image::imageops::crop_imm(
|
||||
&image,
|
||||
bounds.x0,
|
||||
bounds.y0,
|
||||
bounds.width(),
|
||||
bounds.height(),
|
||||
)
|
||||
.to_image(),
|
||||
slice_generated_asset_sheet_two_items_per_row(
|
||||
image,
|
||||
item_names,
|
||||
MATCH3D_MATERIAL_GRID_SIZE as usize,
|
||||
MATCH3D_ITEM_VIEW_COUNT,
|
||||
)
|
||||
.map(|rows| {
|
||||
rows.into_iter()
|
||||
|
||||
@@ -716,6 +716,40 @@ pub(super) fn load_match3d_container_reference_image() -> Result<OpenAiReference
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn build_match3d_level_scene_generation_prompt(config: &Match3DConfigJson) -> String {
|
||||
let theme = config.theme_text.trim();
|
||||
let theme = if theme.is_empty() {
|
||||
MATCH3D_DEFAULT_THEME
|
||||
} else {
|
||||
theme
|
||||
};
|
||||
let style_clause = resolve_match3d_asset_style_prompt(config)
|
||||
.map(|style| format!("\n整体美术风格要求:{style}"))
|
||||
.unwrap_or_default();
|
||||
|
||||
format!(
|
||||
concat!(
|
||||
"生成抓大鹅游戏关卡画面,要求画面中所有元素精致且风格高度一致,画面中所有UI细节饱满精致、完成度高、顶级游戏品质\n\n",
|
||||
"抓大鹅主题描述:\n",
|
||||
"{theme}{style_clause}\n\n",
|
||||
"画面元素:\n",
|
||||
"返回按钮位于顶部左上角,顶部中间显示关卡标题“第1关 重庆火锅”和倒计时时间,右上角显示设置按钮\n",
|
||||
"画面中间是一个和主题匹配的容器,宽度与画面宽度同宽,紧贴画面横向边缘\n",
|
||||
"底部还有三个道具按钮分别为“移出”、“凑齐”、“打乱”"
|
||||
),
|
||||
theme = theme,
|
||||
style_clause = style_clause,
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn build_match3d_ui_spritesheet_prompt() -> String {
|
||||
"提取画面中的UI元素,将返回按钮、设置按钮、方格素材(不含边框,仅保留一个)、移出按钮、凑齐按钮、打乱按钮的顺序从上到下从左到右整理成纯绿色绿幕背景spritesheet。背景必须是统一纯绿色绿幕(高饱和亮绿色,接近 #00FF00),背景平整无纹理、无渐变、无阴影、无场景内容,后端会在生图后将绿幕扣成透明并把透明背景 PNG 存到 OSS。UI 素材自身不得使用接近 #00FF00 的高饱和纯绿;绿色题材只能使用深绿、橄榄绿、金绿或蓝绿,并用清晰描边与绿幕区分。".to_string()
|
||||
}
|
||||
|
||||
pub(super) fn build_match3d_background_from_scene_prompt() -> String {
|
||||
"移除画面中的所有UI组件和容器中的内含物,完整保留容器和背景,补全被UI覆盖的背景内容".to_string()
|
||||
}
|
||||
|
||||
pub(super) fn build_match3d_background_generation_prompt(
|
||||
config: &Match3DConfigJson,
|
||||
prompt: &str,
|
||||
|
||||
Reference in New Issue
Block a user