From 0edcb1b9f1b7f09a952961a9452a03509260f0cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=94=E9=A6=99=E4=B8=B8=E5=AD=90?= <15518898337@163.com> Date: Fri, 5 Jun 2026 13:23:20 +0800 Subject: [PATCH] fix(jump-hop): clean magenta cutout fringes --- ...玩法创作】平å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md | 2 +- server-rs/crates/api-server/src/jump_hop.rs | 14 +++- .../src/generated_asset_sheets/alpha.rs | 68 +++++++++++++++++- .../src/generated_asset_sheets/sheet.rs | 58 +++++++++++++-- .../tests/generated_asset_sheets.rs | 72 +++++++++++++++++++ 5 files changed, 202 insertions(+), 12 deletions(-) diff --git a/docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md b/docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md index 5d879b8e..47791407 100644 --- a/docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md +++ b/docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md @@ -146,7 +146,7 @@ RPG / 拼图等è¿è¡Œæ€å­˜æ¡£ä»ä»¥ `/api/profile/save-archives` çš„åŽç«¯åˆ— 1. 创作端åªä¿ç•™ä¸»é¢˜è¾“å…¥ï¼Œä½œå“æ ‡é¢˜ã€ç®€ä»‹ã€æ ‡ç­¾å’Œåœ°å—æç¤ºè¯ç”±ç³»ç»Ÿæ´¾ç”Ÿï¼› 2. v1 ä¸å†å•独生æˆè§’色图片,è¿è¡Œæ€å›ºå®šä½¿ç”¨æŠ é™¤ç™½åº•åŽçš„陶泥儿 logo 逿˜Ž PNG 作为玩家角色; -3. 地å—åªè°ƒç”¨ä¸€æ¬¡ image2,输出一张 `5行*5列`ã€`1:1`ã€å•一纯洋红 `#FF00FF` key 背景的主题地å—图集;跳一跳地å—常包å«è‰åœ°ã€èбã€é›ªã€ç™½çŸ³å’Œäº‘朵,åŽç«¯é€æ˜ŽåŒ–必须使用跳一跳专用洋红 key,ä¸å¯ç”¨è¿‘ç™½åº•æ‰£é™¤ï¼Œä¹Ÿä¸æ¸…ç†éžè¾¹ç¼˜è¿žé€šçš„ key 色åƒç´ ï¼Œé¿å…把绿色或白色主体误扣;地å—造型æç¤ºè¯è¦æ±‚以主题物体本身外轮廓为准,å…许苹果近似圆形ã€é¦™è•‰è¿‘ä¼¼é•¿æ¡æˆ–长方形ã€è¥¿ç“œè¿‘似扇形等自然差异,åªç»Ÿä¸€å•格规格ã€å®‰å…¨ç•™ç™½ã€æ­£é¢30度视角和 2D/2.5D 手绘风格包装;所有地å—ç´ æå¿…é¡»ä¿æŒç»Ÿä¸€æ­£é¢30度视角,相机ä½äºŽç‰©ä½“æ­£å‰æ–¹ç•¥é«˜ä½ç½®ã€é•œå¤´å‘下约30度,必须看到清晰正é¢ã€ä¾§å£ã€ä¸‹æ²¿ã€æ˜Žæ˜¾è‡ªèº«åŽšåº¦å’Œå°‘é‡ä¸Šè¡¨é¢ï¼Œä¸»ä½“æ­£é¢æˆ–ä¾§å£å¯è§é¢ç§¯å¿…须接近或大于顶é¢é¢ç§¯ï¼Œé¡¶é¢åªèƒ½ä½œä¸ºè¾…助å¯è§é¢ï¼›æ°´æžœä¸»é¢˜éœ€è¦æ˜Žç¡®è¦æ±‚橙瓣看到橙皮正é¢å¤–ä¾§å’Œæžœè‚‰åŽšåº¦ã€æ¤°å­çœ‹åˆ°å£³çš„æ­£é¢ä¾§å£å’Œåˆ‡å£åŽšåº¦ã€æµ†æžœä¸èƒ½åªæ˜¯ä»Žä¸Šå¾€ä¸‹çœ‹çš„圆形çƒé¡¶ï¼›é¿å…生æˆçº¯ä¿¯è§†ã€æ­£ä¸Šæ–¹ä¿¯æ‹ã€é¸Ÿçž°åœ°å›¾å—ã€å¹³é“ºä¿¯æ‹ã€åœ†å½¢é¡¶è§†å›¾æˆ–æ‰å¹³å›¾æ ‡ï¼›ä¸»é¢˜ç‰©ä½“本身必须是唯一å¯è½è„šä½“,åªèƒ½ç”¨è‡ªèº«åˆ‡é¢ã€è¾¹ç¼˜åŽšåº¦ã€èŠ±ç“£å±‚æˆ–æžœçš®è¾¹è¡¨çŽ°æ‰¿é‡ï¼Œç¦æ­¢åœ¨ä¸»é¢˜ç‰©ä½“下方é¢å¤–垫石å°ã€åœŸå¢©ã€æœ¨æ¿ã€åœ†å°ã€æ‰˜ç›˜ã€å²›å±¿åº•座或通用地æ¿ï¼›å‰ç«¯å’ŒåŽç«¯é»˜è®¤ `tilePrompt` 都必须使用“正é¢30度视角主题物体图集,物体本身作为跳跃è½ç‚¹â€çš„å£å¾„,ä¸å†æäº¤â€œå¹³å°ç´ æ / è·³å° / åœ°å— / 地砖â€ç­‰ä¼šæŠŠæ¨¡åž‹æ‹‰å›žé€šç”¨å¹³å°é€ åž‹çš„è¯ï¼ŒåŽç«¯ç”Ÿæˆå‰ä¹Ÿä¼šæ¸…æ´—æ—§è‰ç¨¿é—留的这些è¯ï¼› +3. 地å—åªè°ƒç”¨ä¸€æ¬¡ image2,输出一张 `5行*5列`ã€`1:1`ã€å•一纯洋红 `#FF00FF` key 背景的主题地å—图集;跳一跳地å—常包å«è‰åœ°ã€èбã€é›ªã€ç™½çŸ³å’Œäº‘朵,åŽç«¯é€æ˜ŽåŒ–必须使用跳一跳专用洋红 key,ä¸å¯ç”¨è¿‘ç™½åº•æ‰£é™¤ï¼Œä¹Ÿä¸æ¸…ç†éžè¾¹ç¼˜è¿žé€šçš„ key 色åƒç´ ï¼Œé¿å…把绿色或白色主体误扣;åŽå¤„ç†å¿…须对边缘连通 key 色åšå®¹å·®æ¸…ç†ã€åŽ»å½©è¾¹ defringe 和底部残影清ç†ï¼Œä¸»ä½“图ä¸å¾—自带洋红阴影ã€ç´«è‰²åº•è¾¹ã€ç²‰è‰²è„è¾¹ã€å½©è‰²å…‰æ™•或å‘光底边,è¿è¡Œæ€é˜´å½±ç»Ÿä¸€ç”± DOM 绘制;地å—造型æç¤ºè¯è¦æ±‚以主题物体本身外轮廓为准,å…许苹果近似圆形ã€é¦™è•‰è¿‘ä¼¼é•¿æ¡æˆ–长方形ã€è¥¿ç“œè¿‘似扇形等自然差异,åªç»Ÿä¸€å•格规格ã€å®‰å…¨ç•™ç™½ã€æ­£é¢30度视角和 2D/2.5D 手绘风格包装;所有地å—ç´ æå¿…é¡»ä¿æŒç»Ÿä¸€æ­£é¢30度视角,相机ä½äºŽç‰©ä½“æ­£å‰æ–¹ç•¥é«˜ä½ç½®ã€é•œå¤´å‘下约30度,必须看到清晰正é¢ã€ä¾§å£ã€ä¸‹æ²¿ã€æ˜Žæ˜¾è‡ªèº«åŽšåº¦å’Œå°‘é‡ä¸Šè¡¨é¢ï¼Œä¸»ä½“æ­£é¢æˆ–ä¾§å£å¯è§é¢ç§¯å¿…须接近或大于顶é¢é¢ç§¯ï¼Œé¡¶é¢åªèƒ½ä½œä¸ºè¾…助å¯è§é¢ï¼›æ°´æžœä¸»é¢˜éœ€è¦æ˜Žç¡®è¦æ±‚橙瓣看到橙皮正é¢å¤–ä¾§å’Œæžœè‚‰åŽšåº¦ã€æ¤°å­çœ‹åˆ°å£³çš„æ­£é¢ä¾§å£å’Œåˆ‡å£åŽšåº¦ã€æµ†æžœä¸èƒ½åªæ˜¯ä»Žä¸Šå¾€ä¸‹çœ‹çš„圆形çƒé¡¶ï¼›é¿å…生æˆçº¯ä¿¯è§†ã€æ­£ä¸Šæ–¹ä¿¯æ‹ã€é¸Ÿçž°åœ°å›¾å—ã€å¹³é“ºä¿¯æ‹ã€åœ†å½¢é¡¶è§†å›¾æˆ–æ‰å¹³å›¾æ ‡ï¼›ä¸»é¢˜ç‰©ä½“本身必须是唯一å¯è½è„šä½“,åªèƒ½ç”¨è‡ªèº«åˆ‡é¢ã€è¾¹ç¼˜åŽšåº¦ã€èŠ±ç“£å±‚æˆ–æžœçš®è¾¹è¡¨çŽ°æ‰¿é‡ï¼Œç¦æ­¢åœ¨ä¸»é¢˜ç‰©ä½“下方é¢å¤–垫石å°ã€åœŸå¢©ã€æœ¨æ¿ã€åœ†å°ã€æ‰˜ç›˜ã€å²›å±¿åº•座或通用地æ¿ï¼›å‰ç«¯å’ŒåŽç«¯é»˜è®¤ `tilePrompt` 都必须使用“正é¢30度视角主题物体图集,物体本身作为跳跃è½ç‚¹â€çš„å£å¾„,ä¸å†æäº¤â€œå¹³å°ç´ æ / è·³å° / åœ°å— / 地砖â€ç­‰ä¼šæŠŠæ¨¡åž‹æ‹‰å›žé€šç”¨å¹³å°é€ åž‹çš„è¯ï¼ŒåŽç«¯ç”Ÿæˆå‰ä¹Ÿä¼šæ¸…æ´—æ—§è‰ç¨¿é—留的这些è¯ï¼› 4. èƒŒæ™¯åº•å›¾åŒæ ·ç”± image2 生æˆï¼Œå¤ç”¨çŽ°æœ‰ `coverComposite` / `coverImageSrc` 作为è¿è¡Œæ€èƒŒæ™¯è¯»å†™å­—段,OSS æ§½ä½å›ºå®šä¸º `background/image.png`ï¼Œä¸æ–°å¢ž SpacetimeDB 字段;æç¤ºè¯å¿…须严格以用户主题关键è¯ä¸ºèƒŒæ™¯ä¸»é¢˜ï¼Œç»“构以左å³ä¸¤ä¾§æ°›å›´ä¸ºä¸»ï¼Œä¸­å¤®çºµè½´ 1/2 åŒºåŸŸä¿æŒå°‘元素ã€ç®€æ´ã€å¯è¯»ä¸”有纵深感,两侧å…许更强立体层次和行进感;背景åªä½œä¸ºåº•å›¾ï¼Œç¦æ­¢ç”Ÿæˆè·³æ¿ã€åœ°å—ã€è½è„šç‰©ã€è§’色ã€UIã€æ–‡å­—ã€è·¯å¾„箭头或海报排版; 5. åŽç«¯æŒ‰ä»Žä¸Šåˆ°ä¸‹ã€ä»Žå·¦åˆ°å³å‡åŒ€åˆ‡åˆ†ä¸º `tile-01` 到 `tile-25` çš„é€æ˜Ž PNG,æ¯ä¸ªåˆ‡ç‰‡å¿…须使用唯一 slot/path æŒä¹…化,ä¸èƒ½æŒ‰é‡å¤çš„ `tileType` å¤ç”¨æ§½ä½ï¼› 6. 结果页åªå±•示陶泥儿 logo 逿˜Žè§’色预览ã€åœ°å—æ± é¢„è§ˆå’Œé¦–å± 3 地å—预览;ä¸å†æä¾›æ—§è§’è‰²å›¾ç”Ÿæˆæ§½ã€‚ diff --git a/server-rs/crates/api-server/src/jump_hop.rs b/server-rs/crates/api-server/src/jump_hop.rs index 69ccf710..dcc06949 100644 --- a/server-rs/crates/api-server/src/jump_hop.rs +++ b/server-rs/crates/api-server/src/jump_hop.rs @@ -636,12 +636,12 @@ fn build_jump_hop_tile_atlas_prompt(theme_text: &str, tile_prompt: &str) -> Stri }; format!( - "生æˆä¸€å¼ 1:1图片,主题为“{theme_text}â€ã€‚\nç”»é¢åªåŒ…å«25个独立的跳跃è½ç‚¹ä¸»é¢˜ç‰©ä½“,按五行五列å‡åŒ€æ‘†æ”¾åœ¨çº¯æ´‹çº¢æŠ å›¾ç”»å¸ƒä¸Šï¼›ä¸è¦ç”»æˆæ¸¸æˆç•Œé¢ã€æ£‹ç›˜ã€èƒŒåŒ…ã€è£…å¤‡æ æˆ–图标集页é¢ã€‚\n视觉方å‘为正é¢30度视角的跳跃游æˆç´ æï¼Œç”»é¢å†…容是{subject_text}。所有è½ç‚¹ç´ æéƒ½å¿…é¡»ä¿æŒç»Ÿä¸€çš„æ­£é¢30度视角:相机ä½äºŽç‰©ä½“æ­£å‰æ–¹ç•¥é«˜ä½ç½®ï¼Œé•œå¤´å‘下约30度,能看到清晰正é¢ã€ä¾§å£ã€ä¸‹æ²¿å’Œå°‘é‡ä¸Šè¡¨é¢ã€‚\næž„å›¾éªŒæ”¶æ ‡å‡†ï¼šä¸»ä½“æ­£é¢æˆ–ä¾§å£å¯è§é¢ç§¯å¿…须接近或大于顶é¢é¢ç§¯ï¼Œé¡¶é¢åªèƒ½ä½œä¸ºè¾…助å¯è§é¢ï¼›ä¸è¦è®©é¡¶é¢å æ®ä¸»è¦è§†è§‰ï¼Œä¸è¦ç”»æˆçº¯ä¿¯è§†ã€æ­£ä¸Šæ–¹ä¿¯æ‹ã€é¸Ÿçž°åœ°å›¾å—ã€å¹³é“ºä¿¯æ‹ã€åœ†å½¢é¡¶è§†å›¾æˆ–æ‰å¹³å›¾æ ‡ã€‚\n水果主题尤其è¦é¿å…俯æ‹ï¼šæ©™ç“£å¿…须看到橙皮正é¢å¤–侧和果肉厚度,椰å­å¿…须看到壳的正é¢ä¾§å£å’Œåˆ‡å£åŽšåº¦ï¼Œæµ†æžœä¸èƒ½åªæ˜¯ä¸€ä¸ªä»Žä¸Šå¾€ä¸‹çœ‹çš„圆形çƒé¡¶ã€‚\næ¯ä¸€ä¸ªè½ç‚¹éƒ½å¿…须直接使用主题物体或åˆç†å‘散物体åšä¸»ä½“造型,主题è¦ä¸€çœ¼å¯è§ï¼›ä¾‹å¦‚主题为水果时,å¯ä»¥æ˜¯è‹¹æžœåˆ‡ç‰‡ã€æ©™ç“£ã€è¥¿ç“œå—ã€è‰èŽ“ã€è èå—ã€é¦™è•‰ã€è‘¡è„串等水果物体,苹果å¯è¿‘似圆,香蕉å¯è¿‘ä¼¼é•¿æ¡æˆ–长方形,西瓜å¯è¿‘似扇形,造型以物体本身外轮廓为准。\n主题物体本身就是唯一å¯è½è„šä½“:雪花è½ç‚¹å°±æ˜¯ä¸€æžšå¸¦åŽšåº¦çš„é›ªèŠ±ï¼Œå‘æ—¥è‘µè½ç‚¹å°±æ˜¯ä¸€æœµå¸¦åŽšåº¦çš„å‘æ—¥è‘µï¼Œæ°´æžœè½ç‚¹å°±æ˜¯æ°´æžœåˆ‡ç‰‡æˆ–水果本体;ä¸è¦åœ¨ä¸»é¢˜ç‰©ä½“下é¢å†åž«ä»»ä½•石头ã€åœŸå—ã€æœ¨æ¿ã€åœ†å°ã€åº•ç›˜ã€æ‰˜ç›˜ã€å²›å±¿ã€èŠ±ç›†ã€åœ°é¢å—或通用承托物。\nåªç”»ä¸»é¢˜ç‰©ä½“裸素æï¼Œä¸ç”»å¤–层颿¿ã€æ£‹ç›˜åº•座ã€èœå•ã€UIæŒ‰é’®ã€æ ‡é¢˜ã€æ–‡å­—ã€è§’æ ‡ã€è£…饰边框ã€å·¥å…·æ ã€è£…备æ ã€å›¾æ ‡å¡ã€è§’色或游æˆç•Œé¢ã€‚\n整体风格为清爽自然的休闲手游主题物体素æï¼Œå2D/2.5D手绘质感,哑光æè´¨ï¼Œå¹²å‡€è‰²å—,轻微主体内部明暗,é¿å…å†™å®žæ‘„å½±ã€æ²¹äº®é«˜å…‰ã€å¡‘æ–™æ„Ÿã€æš—黑幻想风和厚é‡CG渲染。\næ¯ä¸ªè½ç‚¹éƒ½æ˜¯ç¬¦åˆä¸»é¢˜ä¸”有设计感的立体感物体,有清晰轮廓和明显自身厚度;ä¸è¦æŠŠä¸åŒä¸»é¢˜ç‰©ä½“强行改造æˆç»Ÿä¸€åœ°ç –ã€ç»Ÿä¸€æŒ‰é’®æˆ–统一抽象图标。\n造型规则完全由物体本身决定:å…许圆形ã€é•¿æ¡ã€å¼§å½¢ã€ä¸‰è§’ã€æ‰‡å½¢ã€å—çŠ¶ã€æžå¶çжã€å¤šä»¶ç»„åˆã€è½»å¾®å¤¸å¼ å’Œä¸€å®šç¨‹åº¦å‘散;åªåœ¨åŒä¸€2D/2.5Dæ‰‹ç»˜é£Žæ ¼ã€æ­£é¢30åº¦è§†è§’ã€æè´¨åŒ…è£…ã€æ¸…晰轮廓ã€å•æ ¼è§„æ ¼å’Œå®‰å…¨ç•™ç™½ä¸Šä¿æŒä¸€è‡´ã€‚\n25个è½ç‚¹åº”å°½é‡é€‰æ‹©ä¸åŒä¸»é¢˜ç‰©ä½“æˆ–ç›¸å…³å‘æ•£ç‰©ä½“ï¼Œå·®å¼‚ä¸»è¦æ¥è‡ªç‰©ä½“ç§ç±»å’ŒåŽŸç”Ÿè½®å»“ï¼Œä¸ä½¿ç”¨å›ºå®šå½¢çŠ¶è„šæœ¬ï¼›ç›¸é‚»æ ¼å¯ä»¥å½¢çŠ¶ç›¸ä¼¼ï¼Œåªè¦ç‰©ä½“ä¸åŒä¸”主题清楚。\nå…许用主题物体自身的切é¢ã€è¾¹ç¼˜åŽšåº¦ã€èŠ±ç“£å±‚ã€æžœçš®è¾¹ã€é›ªèŠ±åŽšè¾¹æˆ–äº‘æœµä½“ç§¯è¡¨çŽ°å¯è½è„šæ„Ÿï¼›ç¦æ­¢é¢å¤–æ”¯æ’‘å±‚ã€æ‰¿æ‰˜åº•座ã€è„šä¸‹åœ°æ¿ã€ä¸‹æ–¹çŸ³å°ã€ä¸‹æ–¹åœŸå¢©ã€ä¸‹æ–¹åœ†ç›˜ã€ä¸‹æ–¹æ‰˜ç›˜æˆ–“物体摆在平å°ä¸Šâ€çš„画法。\næ¯ä¸ªè½ç‚¹å¿…须居中,视觉尺寸åªå å•æ ¼56%-64%,四周至少ä¿ç•™18%纯洋红安全留白;任何å¶ç‰‡ã€è£…饰ã€è½®å»“和光影都ä¸å¾—è´´è¾¹ã€è·¨æ ¼æˆ–越界。\næ¯ä¸ªè½ç‚¹åªä¿ç•™ä¸»ä½“内部明暗ã€å¤–轮廓和自身厚度,ä¸ç»˜åˆ¶è½åœ°æŠ•å½±ã€æŽ¥è§¦é˜´å½±ã€æ–¹å½¢é˜´å½±ã€åº•æ¿ã€ç™½åº•ã€ç°åº•ã€é»‘底或背景色å—,è¿è¡Œæ€ä¼šç»Ÿä¸€æ·»åŠ é˜´å½±ã€‚\n25个è½ç‚¹åŒä¸€æè´¨ä½“ç³»ã€åŒä¸€å…‰å‘å’ŒåŒä¸€æ­£é¢30度视角,但物体类别ã€å¤–轮廓和细节有å˜åŒ–ï¼›æ¯ä¸ªè½ç‚¹ä¹‹é—´åªèƒ½æ˜¯çº¯æ´‹çº¢ç©ºç™½ï¼Œä¸ç”»åˆ†éš”线ã€ç½‘格线ã€å®¹å™¨æ¡†æˆ–棋盘格。\næ•´å¼ ç”»å¸ƒèƒŒæ™¯ã€æ ¼é—´ç©ºç™½å’Œæ¯æ ¼èƒŒæ™¯éƒ½å¿…须是å•一纯洋红 {JUMP_HOP_TILE_ATLAS_KEY_HEX},背景平整无纹ç†ã€æ— æ¸å˜ã€æ— é˜´å½±ã€æ— é»‘底;主体å…许使用绿色ã€ç™½è‰²ã€é›ªåœ°ã€äº‘朵ã€è‰åœ°å’ŒèŠ±æœµï¼Œä½†ä¸»ä½“è‡ªèº«ä¸å¾—使用接近 {JUMP_HOP_TILE_ATLAS_KEY_HEX} 的洋红色。\nç¦æ­¢è·¨æ ¼ã€è´´è¾¹ã€è¶Šç•Œã€æ–‡å­—ã€æ°´å°ã€UIã€è¾¹æ¡†ã€ç½‘格线ã€è§’色ã€åœºæ™¯ã€æ¸¸æˆé¢æ¿ã€å›¾æ ‡é›†é¡µé¢ã€ç‰©ä½“下方é¢å¤–底座或物体摆在地æ¿ä¸Šã€‚\nEnglish guardrail: isolated front-facing 30-degree camera-pitch theme-object assets only, camera slightly above the object and looking down about 30 degrees from the front; every object must show a clear front face, side wall, lower rim, object thickness, and only a small top surface; visible front/side area must be close to or larger than the top area; never produce top-down, overhead, bird's-eye, flat icon, round top-view disk assets; the theme object itself is the only landing object, each object's native silhouette decides the shape, no extra base under the object, no pedestal, no plinth, no floor slab, consistent 2D/2.5D style wrapper, solid magenta chroma key background {JUMP_HOP_TILE_ATLAS_KEY_HEX}, no text, no poster, no UI screen, no inventory icons." + "生æˆä¸€å¼ 1:1图片,主题为“{theme_text}â€ã€‚\nç”»é¢åªåŒ…å«25个独立的跳跃è½ç‚¹ä¸»é¢˜ç‰©ä½“,按五行五列å‡åŒ€æ‘†æ”¾åœ¨çº¯æ´‹çº¢æŠ å›¾ç”»å¸ƒä¸Šï¼›ä¸è¦ç”»æˆæ¸¸æˆç•Œé¢ã€æ£‹ç›˜ã€èƒŒåŒ…ã€è£…å¤‡æ æˆ–图标集页é¢ã€‚\n视觉方å‘为正é¢30度视角的跳跃游æˆç´ æï¼Œç”»é¢å†…容是{subject_text}。所有è½ç‚¹ç´ æéƒ½å¿…é¡»ä¿æŒç»Ÿä¸€çš„æ­£é¢30度视角:相机ä½äºŽç‰©ä½“æ­£å‰æ–¹ç•¥é«˜ä½ç½®ï¼Œé•œå¤´å‘下约30度,能看到清晰正é¢ã€ä¾§å£ã€ä¸‹æ²¿å’Œå°‘é‡ä¸Šè¡¨é¢ã€‚\næž„å›¾éªŒæ”¶æ ‡å‡†ï¼šä¸»ä½“æ­£é¢æˆ–ä¾§å£å¯è§é¢ç§¯å¿…须接近或大于顶é¢é¢ç§¯ï¼Œé¡¶é¢åªèƒ½ä½œä¸ºè¾…助å¯è§é¢ï¼›ä¸è¦è®©é¡¶é¢å æ®ä¸»è¦è§†è§‰ï¼Œä¸è¦ç”»æˆçº¯ä¿¯è§†ã€æ­£ä¸Šæ–¹ä¿¯æ‹ã€é¸Ÿçž°åœ°å›¾å—ã€å¹³é“ºä¿¯æ‹ã€åœ†å½¢é¡¶è§†å›¾æˆ–æ‰å¹³å›¾æ ‡ã€‚\n水果主题尤其è¦é¿å…俯æ‹ï¼šæ©™ç“£å¿…须看到橙皮正é¢å¤–侧和果肉厚度,椰å­å¿…须看到壳的正é¢ä¾§å£å’Œåˆ‡å£åŽšåº¦ï¼Œæµ†æžœä¸èƒ½åªæ˜¯ä¸€ä¸ªä»Žä¸Šå¾€ä¸‹çœ‹çš„圆形çƒé¡¶ã€‚\næ¯ä¸€ä¸ªè½ç‚¹éƒ½å¿…须直接使用主题物体或åˆç†å‘散物体åšä¸»ä½“造型,主题è¦ä¸€çœ¼å¯è§ï¼›ä¾‹å¦‚主题为水果时,å¯ä»¥æ˜¯è‹¹æžœåˆ‡ç‰‡ã€æ©™ç“£ã€è¥¿ç“œå—ã€è‰èŽ“ã€è èå—ã€é¦™è•‰ã€è‘¡è„串等水果物体,苹果å¯è¿‘似圆,香蕉å¯è¿‘ä¼¼é•¿æ¡æˆ–长方形,西瓜å¯è¿‘似扇形,造型以物体本身外轮廓为准。\n主题物体本身就是唯一å¯è½è„šä½“:雪花è½ç‚¹å°±æ˜¯ä¸€æžšå¸¦åŽšåº¦çš„é›ªèŠ±ï¼Œå‘æ—¥è‘µè½ç‚¹å°±æ˜¯ä¸€æœµå¸¦åŽšåº¦çš„å‘æ—¥è‘µï¼Œæ°´æžœè½ç‚¹å°±æ˜¯æ°´æžœåˆ‡ç‰‡æˆ–水果本体;ä¸è¦åœ¨ä¸»é¢˜ç‰©ä½“下é¢å†åž«ä»»ä½•石头ã€åœŸå—ã€æœ¨æ¿ã€åœ†å°ã€åº•ç›˜ã€æ‰˜ç›˜ã€å²›å±¿ã€èŠ±ç›†ã€åœ°é¢å—或通用承托物。\nåªç”»ä¸»é¢˜ç‰©ä½“裸素æï¼Œä¸ç”»å¤–层颿¿ã€æ£‹ç›˜åº•座ã€èœå•ã€UIæŒ‰é’®ã€æ ‡é¢˜ã€æ–‡å­—ã€è§’æ ‡ã€è£…饰边框ã€å·¥å…·æ ã€è£…备æ ã€å›¾æ ‡å¡ã€è§’色或游æˆç•Œé¢ã€‚\n整体风格为清爽自然的休闲手游主题物体素æï¼Œå2D/2.5D手绘质感,哑光æè´¨ï¼Œå¹²å‡€è‰²å—,轻微主体内部明暗,é¿å…å†™å®žæ‘„å½±ã€æ²¹äº®é«˜å…‰ã€å¡‘æ–™æ„Ÿã€æš—黑幻想风和厚é‡CG渲染。\næ¯ä¸ªè½ç‚¹éƒ½æ˜¯ç¬¦åˆä¸»é¢˜ä¸”有设计感的立体感物体,有清晰轮廓和明显自身厚度;ä¸è¦æŠŠä¸åŒä¸»é¢˜ç‰©ä½“强行改造æˆç»Ÿä¸€åœ°ç –ã€ç»Ÿä¸€æŒ‰é’®æˆ–统一抽象图标。\n造型规则完全由物体本身决定:å…许圆形ã€é•¿æ¡ã€å¼§å½¢ã€ä¸‰è§’ã€æ‰‡å½¢ã€å—çŠ¶ã€æžå¶çжã€å¤šä»¶ç»„åˆã€è½»å¾®å¤¸å¼ å’Œä¸€å®šç¨‹åº¦å‘散;åªåœ¨åŒä¸€2D/2.5Dæ‰‹ç»˜é£Žæ ¼ã€æ­£é¢30åº¦è§†è§’ã€æè´¨åŒ…è£…ã€æ¸…晰轮廓ã€å•æ ¼è§„æ ¼å’Œå®‰å…¨ç•™ç™½ä¸Šä¿æŒä¸€è‡´ã€‚\n25个è½ç‚¹åº”å°½é‡é€‰æ‹©ä¸åŒä¸»é¢˜ç‰©ä½“æˆ–ç›¸å…³å‘æ•£ç‰©ä½“ï¼Œå·®å¼‚ä¸»è¦æ¥è‡ªç‰©ä½“ç§ç±»å’ŒåŽŸç”Ÿè½®å»“ï¼Œä¸ä½¿ç”¨å›ºå®šå½¢çŠ¶è„šæœ¬ï¼›ç›¸é‚»æ ¼å¯ä»¥å½¢çŠ¶ç›¸ä¼¼ï¼Œåªè¦ç‰©ä½“ä¸åŒä¸”主题清楚。\nå…许用主题物体自身的切é¢ã€è¾¹ç¼˜åŽšåº¦ã€èŠ±ç“£å±‚ã€æžœçš®è¾¹ã€é›ªèŠ±åŽšè¾¹æˆ–äº‘æœµä½“ç§¯è¡¨çŽ°å¯è½è„šæ„Ÿï¼›ç¦æ­¢é¢å¤–æ”¯æ’‘å±‚ã€æ‰¿æ‰˜åº•座ã€è„šä¸‹åœ°æ¿ã€ä¸‹æ–¹çŸ³å°ã€ä¸‹æ–¹åœŸå¢©ã€ä¸‹æ–¹åœ†ç›˜ã€ä¸‹æ–¹æ‰˜ç›˜æˆ–“物体摆在平å°ä¸Šâ€çš„画法。\næ¯ä¸ªè½ç‚¹å¿…须居中,视觉尺寸åªå å•æ ¼56%-64%,四周至少ä¿ç•™18%纯洋红安全留白;任何å¶ç‰‡ã€è£…饰ã€è½®å»“和光影都ä¸å¾—è´´è¾¹ã€è·¨æ ¼æˆ–越界。\næ¯ä¸ªè½ç‚¹åªä¿ç•™ä¸»ä½“内部明暗ã€å¤–轮廓和自身厚度,ä¸ç»˜åˆ¶è½åœ°æŠ•å½±ã€æŽ¥è§¦é˜´å½±ã€æ–¹å½¢é˜´å½±ã€æ´‹çº¢é˜´å½±ã€ç´«è‰²åº•è¾¹ã€å½©è‰²å…‰æ™•ã€å‘光底边ã€åº•æ¿ã€ç™½åº•ã€ç°åº•ã€é»‘底或背景色å—,è¿è¡Œæ€ä¼šç»Ÿä¸€æ·»åŠ é˜´å½±ã€‚\n25个è½ç‚¹åŒä¸€æè´¨ä½“ç³»ã€åŒä¸€å…‰å‘å’ŒåŒä¸€æ­£é¢30度视角,但物体类别ã€å¤–轮廓和细节有å˜åŒ–ï¼›æ¯ä¸ªè½ç‚¹ä¹‹é—´åªèƒ½æ˜¯çº¯æ´‹çº¢ç©ºç™½ï¼Œä¸ç”»åˆ†éš”线ã€ç½‘格线ã€å®¹å™¨æ¡†æˆ–棋盘格。\næ•´å¼ ç”»å¸ƒèƒŒæ™¯ã€æ ¼é—´ç©ºç™½å’Œæ¯æ ¼èƒŒæ™¯éƒ½å¿…须是å•一纯洋红 {JUMP_HOP_TILE_ATLAS_KEY_HEX},背景平整无纹ç†ã€æ— æ¸å˜ã€æ— é˜´å½±ã€æ— é»‘底;主体å…许使用绿色ã€ç™½è‰²ã€é›ªåœ°ã€äº‘朵ã€è‰åœ°å’ŒèŠ±æœµï¼Œä½†ä¸»ä½“è‡ªèº«ä¸å¾—使用接近 {JUMP_HOP_TILE_ATLAS_KEY_HEX} 的洋红色,主体边缘ä¸å¾—出现洋红色æè¾¹ã€ç´«è‰²æè¾¹ã€ç²‰è‰²è„边或彩色阴影。\nç¦æ­¢è·¨æ ¼ã€è´´è¾¹ã€è¶Šç•Œã€æ–‡å­—ã€æ°´å°ã€UIã€è¾¹æ¡†ã€ç½‘格线ã€è§’色ã€åœºæ™¯ã€æ¸¸æˆé¢æ¿ã€å›¾æ ‡é›†é¡µé¢ã€ç‰©ä½“下方é¢å¤–底座或物体摆在地æ¿ä¸Šã€‚\nEnglish guardrail: isolated front-facing 30-degree camera-pitch theme-object assets only, camera slightly above the object and looking down about 30 degrees from the front; every object must show a clear front face, side wall, lower rim, object thickness, and only a small top surface; visible front/side area must be close to or larger than the top area; never produce top-down, overhead, bird's-eye, flat icon, round top-view disk assets; the theme object itself is the only landing object, each object's native silhouette decides the shape, no extra base under the object, no pedestal, no plinth, no floor slab, no colored shadow or magenta fringe around objects, consistent 2D/2.5D style wrapper, solid magenta chroma key background {JUMP_HOP_TILE_ATLAS_KEY_HEX}, no text, no poster, no UI screen, no inventory icons." ) } fn build_jump_hop_tile_atlas_negative_prompt() -> &'static str { - "文字ã€Logoã€æ°´å°ã€UI按钮ã€UI å­—ã€æ¸¸æˆç•Œé¢ã€æ£‹ç›˜ã€èƒŒåŒ…ã€è£…备æ ã€å›¾æ ‡é›†é¡µé¢ã€å¤–层颿¿ã€èœå•ã€å·¥å…·æ ã€ä½Žæ¸…晰度ã€ç•¸å½¢è‚¢ä½“ã€å¤šä½™è§’色ã€è£åˆ‡ä¸»ä½“ã€å†™å®žæ‘„å½±ã€æ²¹äº®é«˜å…‰ã€å¡‘æ–™è´¨æ„Ÿã€æš—黑幻想风ã€åŽšé‡CGæ¸²æŸ“ã€æµ·æŠ¥ã€UI图标å¡ã€æ ‡é¢˜ã€è¯´æ˜Žæ–‡å­—ã€è£…饰边框ã€çº¯ä¿¯è§†è§’ã€æ­£ä¸Šæ–¹è§†è§’ã€é¸Ÿçž°è§†è§’ã€å¹³é“ºä¿¯æ‹ã€é¡¶é¢å ä¸»ç”»é¢ã€åªçœ‹é¡¶é¢ã€åœ†å½¢é¡¶è§†å›¾ã€æ‰å¹³å›¾æ ‡ã€è½åœ°æŠ•å½±ã€æŽ¥è§¦é˜´å½±ã€æ–¹å½¢é˜´å½±ã€æ–¹å½¢åº•æ¿ã€é¢å¤–åº•åº§ã€æ‰¿æ‰˜åº•座ã€å°åº§ã€çŸ³å°ã€åœŸå¢©ã€æœ¨æ¿åº•座ã€åœ†å°ã€åº•ç›˜ã€æ‰˜ç›˜ã€å²›å±¿åº•座ã€èŠ±ç›†åº•åº§ã€åœ°é¢å—ã€è„šä¸‹åœ°æ¿ã€ç‰©ä½“摆在平å°ä¸Šã€ç‰©ä½“下方垫地æ¿ã€ç™½åº•ã€ç°åº•ã€é»‘åº•ã€æš—色背景ã€èƒŒæ™¯è‰²å—ã€è´´è¾¹ã€è·¨æ ¼ã€è¶Šç•Œ" + "文字ã€Logoã€æ°´å°ã€UI按钮ã€UI å­—ã€æ¸¸æˆç•Œé¢ã€æ£‹ç›˜ã€èƒŒåŒ…ã€è£…备æ ã€å›¾æ ‡é›†é¡µé¢ã€å¤–层颿¿ã€èœå•ã€å·¥å…·æ ã€ä½Žæ¸…晰度ã€ç•¸å½¢è‚¢ä½“ã€å¤šä½™è§’色ã€è£åˆ‡ä¸»ä½“ã€å†™å®žæ‘„å½±ã€æ²¹äº®é«˜å…‰ã€å¡‘æ–™è´¨æ„Ÿã€æš—黑幻想风ã€åŽšé‡CGæ¸²æŸ“ã€æµ·æŠ¥ã€UI图标å¡ã€æ ‡é¢˜ã€è¯´æ˜Žæ–‡å­—ã€è£…饰边框ã€çº¯ä¿¯è§†è§’ã€æ­£ä¸Šæ–¹è§†è§’ã€é¸Ÿçž°è§†è§’ã€å¹³é“ºä¿¯æ‹ã€é¡¶é¢å ä¸»ç”»é¢ã€åªçœ‹é¡¶é¢ã€åœ†å½¢é¡¶è§†å›¾ã€æ‰å¹³å›¾æ ‡ã€è½åœ°æŠ•å½±ã€æŽ¥è§¦é˜´å½±ã€æ–¹å½¢é˜´å½±ã€æ´‹çº¢é˜´å½±ã€ç´«è‰²åº•è¾¹ã€ç²‰è‰²è„è¾¹ã€æ´‹çº¢è‰²æè¾¹ã€å½©è‰²å…‰æ™•ã€å‘å…‰åº•è¾¹ã€æ–¹å½¢åº•æ¿ã€é¢å¤–åº•åº§ã€æ‰¿æ‰˜åº•座ã€å°åº§ã€çŸ³å°ã€åœŸå¢©ã€æœ¨æ¿åº•座ã€åœ†å°ã€åº•ç›˜ã€æ‰˜ç›˜ã€å²›å±¿åº•座ã€èŠ±ç›†åº•åº§ã€åœ°é¢å—ã€è„šä¸‹åœ°æ¿ã€ç‰©ä½“摆在平å°ä¸Šã€ç‰©ä½“下方垫地æ¿ã€ç™½åº•ã€ç°åº•ã€é»‘åº•ã€æš—色背景ã€èƒŒæ™¯è‰²å—ã€è´´è¾¹ã€è·¨æ ¼ã€è¶Šç•Œ" } fn sanitize_jump_hop_tile_prompt(tile_prompt: &str) -> String { @@ -1325,7 +1325,10 @@ mod tests { assert!(prompt.contains(JUMP_HOP_TILE_ATLAS_KEY_HEX)); assert!(prompt.contains("主体å…许使用绿色ã€ç™½è‰²ã€é›ªåœ°ã€äº‘朵ã€è‰åœ°å’ŒèŠ±æœµ")); assert!(prompt.contains("ä¸ç»˜åˆ¶è½åœ°æŠ•å½±")); + assert!(prompt.contains("ä¸ç»˜åˆ¶è½åœ°æŠ•å½±ã€æŽ¥è§¦é˜´å½±ã€æ–¹å½¢é˜´å½±ã€æ´‹çº¢é˜´å½±")); + assert!(prompt.contains("紫色底边ã€å½©è‰²å…‰æ™•ã€å‘光底边")); assert!(prompt.contains("ä¸ç”»åˆ†éš”线ã€ç½‘格线ã€å®¹å™¨æ¡†æˆ–棋盘格")); + assert!(prompt.contains("主体边缘ä¸å¾—出现洋红色æè¾¹ã€ç´«è‰²æè¾¹ã€ç²‰è‰²è„边或彩色阴影")); assert!(prompt.contains("English guardrail")); assert!(prompt.contains("front-facing 30-degree camera-pitch")); assert!(prompt.contains("camera slightly above the object")); @@ -1337,6 +1340,7 @@ mod tests { assert!(prompt.contains("no extra base under the object")); assert!(prompt.contains("no pedestal")); assert!(prompt.contains("no floor slab")); + assert!(prompt.contains("no colored shadow or magenta fringe around objects")); assert!(!prompt.contains("å¯è½è„šå¹³å°ç´ æ")); assert!(!prompt.contains("å¹³å°è£¸ç´ æ")); assert!(!prompt.contains("æ¯æ ¼ä¸€ä¸ªå®Œæ•´å¹³å°")); @@ -1460,6 +1464,12 @@ mod tests { assert!(negative_prompt.contains("圆形顶视图")); assert!(negative_prompt.contains("æ‰å¹³å›¾æ ‡")); assert!(negative_prompt.contains("方形阴影")); + assert!(negative_prompt.contains("洋红阴影")); + assert!(negative_prompt.contains("紫色底边")); + assert!(negative_prompt.contains("粉色è„è¾¹")); + assert!(negative_prompt.contains("洋红色æè¾¹")); + assert!(negative_prompt.contains("彩色光晕")); + assert!(negative_prompt.contains("å‘光底边")); assert!(negative_prompt.contains("方形底æ¿")); assert!(negative_prompt.contains("é¢å¤–底座")); assert!(negative_prompt.contains("承托底座")); diff --git a/server-rs/crates/platform-image/src/generated_asset_sheets/alpha.rs b/server-rs/crates/platform-image/src/generated_asset_sheets/alpha.rs index 782460ca..d95b4675 100644 --- a/server-rs/crates/platform-image/src/generated_asset_sheets/alpha.rs +++ b/server-rs/crates/platform-image/src/generated_asset_sheets/alpha.rs @@ -385,7 +385,13 @@ fn remove_generated_asset_sheet_green_screen_background( let mut red = pixels[offset] as f32; let mut green = pixels[offset + 1] as f32; let mut blue = pixels[offset + 2] as f32; - let blend = clamp_generated_asset_sheet_unit(contamination.max(0.22)); + let blend = if options.key_color.is_green_screen() { + clamp_generated_asset_sheet_unit(contamination.max(0.22)) + } else { + // 中文注释:洋红 / é’色等éžç»¿å¹• key 的残留更容易表现æˆå½©è¾¹ï¼Œ + // éœ€è¦æ¯”绿幕更强地å‘主体邻近色收敛,é¿å… PNG 边缘继续带 key 色。 + clamp_generated_asset_sheet_unit((key_score * 1.35).max(contamination).max(0.28)) + }; if let Some((sample_red, sample_green, sample_blue)) = sample { red = lerp_generated_asset_sheet_channel(red, sample_red as f32, blend); @@ -400,6 +406,17 @@ fn remove_generated_asset_sheet_green_screen_background( green = green.min(sample_green as f32 + 26.0); blue = blue.min(sample_blue as f32 + 26.0); } + if !options.key_color.is_green_screen() && key_score > 0.04 { + let defringed = suppress_generated_asset_sheet_key_color_fringe( + [red, green, blue], + [sample_red as f32, sample_green as f32, sample_blue as f32], + key_score, + options.key_color, + ); + red = defringed[0]; + green = defringed[1]; + blue = defringed[2]; + } } else { if options.key_color.is_green_screen() && key_score > 0.04 { let toned_green = (green - (green - red.max(blue)) * 0.78) @@ -417,10 +434,26 @@ fn remove_generated_asset_sheet_green_screen_background( blue = blue.min(toned_value); } } + if !options.key_color.is_green_screen() && key_score > 0.04 { + let neutral = (red + green + blue) / 3.0; + let defringed = suppress_generated_asset_sheet_key_color_fringe( + [red, green, blue], + [neutral, neutral, neutral], + key_score, + options.key_color, + ); + red = defringed[0]; + green = defringed[1]; + blue = defringed[2]; + } } let mut next_alpha = alpha; - let edge_fade = (key_score * 0.35).max(white_score * 0.28); + let edge_fade = if options.key_color.is_green_screen() { + (key_score * 0.35).max(white_score * 0.28) + } else { + (key_score * 0.48).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 { @@ -448,6 +481,37 @@ fn remove_generated_asset_sheet_green_screen_background( changed } +pub(super) fn suppress_generated_asset_sheet_key_color_fringe( + color: [f32; 3], + target: [f32; 3], + key_score: f32, + key_color: GeneratedAssetSheetKeyColor, +) -> [f32; 3] { + let strength = clamp_generated_asset_sheet_unit(key_score * 1.18); + let key_channels = [ + key_color.red as f32 / 255.0, + key_color.green as f32 / 255.0, + key_color.blue as f32 / 255.0, + ]; + let mut next = color; + + for index in 0..3 { + if key_channels[index] >= 0.66 { + let cap = target[index] + 18.0 + (1.0 - strength) * 28.0; + next[index] = next[index].min(lerp_generated_asset_sheet_channel( + next[index], + cap, + strength, + )); + } else if key_channels[index] <= 0.34 { + next[index] = + lerp_generated_asset_sheet_channel(next[index], target[index], strength * 0.72); + } + } + + next +} + fn compute_generated_asset_sheet_key_score( pixel: [u8; 4], key_color: GeneratedAssetSheetKeyColor, diff --git a/server-rs/crates/platform-image/src/generated_asset_sheets/sheet.rs b/server-rs/crates/platform-image/src/generated_asset_sheets/sheet.rs index 6bfbf96f..740f4f43 100644 --- a/server-rs/crates/platform-image/src/generated_asset_sheets/sheet.rs +++ b/server-rs/crates/platform-image/src/generated_asset_sheets/sheet.rs @@ -1,13 +1,14 @@ use super::alpha::{ GeneratedAssetSheetAlphaOptions, apply_generated_asset_sheet_green_screen_alpha, + suppress_generated_asset_sheet_key_color_fringe, }; use super::color::{ - compute_generated_asset_sheet_key_color_score, + clamp_generated_asset_sheet_unit, compute_generated_asset_sheet_key_color_score, compute_generated_asset_sheet_white_screen_score, is_generated_asset_sheet_foreground_pixel, is_generated_asset_sheet_green_contaminated_edge_pixel, is_generated_asset_sheet_soft_edge_pixel, is_generated_asset_sheet_strong_green_contamination, is_generated_asset_sheet_view_background_pixel, is_generated_asset_sheet_visible_pixel, - touches_generated_asset_sheet_background_mask, + lerp_generated_asset_sheet_channel, touches_generated_asset_sheet_background_mask, }; use super::error::GeneratedAssetSheetError; use image::{GenericImageView, ImageFormat}; @@ -588,11 +589,54 @@ fn remove_generated_asset_sheet_view_edge_matte( pixels[offset + 1], pixels[offset + 2], )); - let next_red = replacement.0.max(pixels[offset]); - let next_blue = replacement.2.max(pixels[offset + 2]); - let next_green = replacement - .1 - .min(next_red.max(next_blue).saturating_add(12)); + let (next_red, next_green, next_blue) = if options.key_color.is_green_screen() { + let next_red = replacement.0.max(pixels[offset]); + let next_blue = replacement.2.max(pixels[offset + 2]); + let next_green = replacement + .1 + .min(next_red.max(next_blue).saturating_add(12)); + (next_red, next_green, next_blue) + } else { + let key_score = compute_generated_asset_sheet_key_color_score( + pixel, + [ + options.key_color.red, + options.key_color.green, + options.key_color.blue, + ], + ); + let blend = clamp_generated_asset_sheet_unit((key_score * 1.25).max(0.36)); + let red = lerp_generated_asset_sheet_channel( + pixels[offset] as f32, + replacement.0 as f32, + blend, + ); + let green = lerp_generated_asset_sheet_channel( + pixels[offset + 1] as f32, + replacement.1 as f32, + blend, + ); + let blue = lerp_generated_asset_sheet_channel( + pixels[offset + 2] as f32, + replacement.2 as f32, + blend, + ); + let defringed = suppress_generated_asset_sheet_key_color_fringe( + [red, green, blue], + [ + replacement.0 as f32, + replacement.1 as f32, + replacement.2 as f32, + ], + key_score, + options.key_color, + ); + ( + defringed[0].round().clamp(0.0, 255.0) as u8, + defringed[1].round().clamp(0.0, 255.0) as u8, + defringed[2].round().clamp(0.0, 255.0) as u8, + ) + }; if next_red != pixels[offset] || next_green != pixels[offset + 1] || next_blue != pixels[offset + 2] diff --git a/server-rs/crates/platform-image/tests/generated_asset_sheets.rs b/server-rs/crates/platform-image/tests/generated_asset_sheets.rs index 6473f756..40a0c8f0 100644 --- a/server-rs/crates/platform-image/tests/generated_asset_sheets.rs +++ b/server-rs/crates/platform-image/tests/generated_asset_sheets.rs @@ -206,6 +206,78 @@ fn generated_asset_sheet_magenta_edge_matte_does_not_remove_white_subject() { ); } +#[test] +fn generated_asset_sheet_magenta_alpha_defringes_pink_halo() { + let mut sheet = RgbaImage::from_pixel(24, 24, Rgba([255, 0, 255, 255])); + for y in 7..17 { + for x in 7..17 { + sheet.put_pixel(x, y, Rgba([198, 170, 120, 255])); + } + } + for y in 6..18 { + sheet.put_pixel(6, y, Rgba([226, 26, 218, 220])); + sheet.put_pixel(17, y, Rgba([226, 26, 218, 220])); + } + for x in 6..18 { + sheet.put_pixel(x, 6, Rgba([226, 26, 218, 220])); + sheet.put_pixel(x, 17, Rgba([226, 26, 218, 220])); + } + + let cleaned = apply_generated_asset_sheet_alpha_with_options( + DynamicImage::ImageRgba8(sheet), + GeneratedAssetSheetAlphaOptions::jump_hop_magenta_screen(), + ) + .to_rgba8(); + let edge = cleaned.get_pixel(6, 12).0; + + assert_eq!(cleaned.get_pixel(0, 0).0[3], 0); + assert_eq!(cleaned.get_pixel(12, 12).0, [198, 170, 120, 255]); + if edge[3] > 0 { + assert!( + edge[0].saturating_sub(edge[1]) <= 76, + "红色 key é€šé“æ®‹ç•™è¿‡å¼ºï¼š{edge:?}" + ); + assert!( + edge[2].saturating_sub(edge[1]) <= 76, + "è“色 key é€šé“æ®‹ç•™è¿‡å¼ºï¼š{edge:?}" + ); + } +} + +#[test] +fn generated_asset_sheet_magenta_edge_matte_defringes_bottom_shadow() { + let mut sheet = RgbaImage::from_pixel(32, 32, Rgba([0, 0, 0, 0])); + for y in 8..18 { + for x in 10..22 { + sheet.put_pixel(x, y, Rgba([202, 176, 126, 255])); + } + } + for y in 18..22 { + for x in 9..23 { + sheet.put_pixel(x, y, Rgba([224, 30, 220, 186])); + } + } + + let cleaned = crop_generated_asset_sheet_view_edge_matte_with_options( + DynamicImage::ImageRgba8(sheet), + GeneratedAssetSheetAlphaOptions::jump_hop_magenta_screen(), + ) + .to_rgba8(); + + assert!( + cleaned + .pixels() + .any(|pixel| pixel.0 == [202, 176, 126, 255]) + ); + assert!( + !cleaned.pixels().any(|pixel| { + let [red, green, blue, alpha] = pixel.0; + alpha > 0 && red > 200 && blue > 200 && green < 96 + }), + "底部洋红残影应被删除或去彩边" + ); +} + #[test] fn generated_asset_sheet_view_edge_matte_trims_transparent_border() { let mut sheet = RgbaImage::from_pixel(20, 20, Rgba([0, 0, 0, 0]));