From c16657e4d6a52eeac92e91d65f7f2fdcb62503cb Mon Sep 17 00:00:00 2001 From: wusumac <736139669@qq.com> Date: Wed, 23 Sep 2026 21:21:47 +0800 Subject: [PATCH] fix(m110): use sprite-alpha occlusion and self-target exclusion --- .../tests/dev5/battle_live_capture_test.gd | 69 ++++++++++++++----- 1 file changed, 52 insertions(+), 17 deletions(-) diff --git a/runtime/tests/dev5/battle_live_capture_test.gd b/runtime/tests/dev5/battle_live_capture_test.gd index c80d8c04..a4b65789 100644 --- a/runtime/tests/dev5/battle_live_capture_test.gd +++ b/runtime/tests/dev5/battle_live_capture_test.gd @@ -1196,17 +1196,6 @@ func _visible_ui_ink_rects(shell: Control) -> Array: return rects -func _visible_actor_rects(shell: Control) -> Array: - var rects: Array = [] - for actor_group in ["BattleActors", "BattleEnemies"]: - var group := shell.find_child(actor_group, true, false) - if group == null: - continue - for actor in group.get_children(): - rects.append((actor as Control).get_global_rect().grow(1.0)) - return rects - - func _visible_text_nodes(node: Node, out: Array) -> void: for child in node.get_children(): if (child is Label or child is BaseButton) and (child as CanvasItem).is_visible_in_tree(): @@ -1214,6 +1203,50 @@ func _visible_text_nodes(node: Node, out: Array) -> void: _visible_text_nodes(child, out) +func _actor_occlusion_masks(shell: Control) -> Array: + # Sprite-space occlusion: a pixel counts as occluded only when the actor + # portrait texture is actually opaque there (the art layer stays below the + # actor layer, so the path is hidden only behind opaque sprite pixels). + var masks: Array = [] + for group_name in ["BattleActors", "BattleEnemies"]: + var group := shell.find_child(group_name, true, false) + if group == null: + continue + for actor in group.get_children(): + for portrait_name in ["BattlePortrait", "BattleEnemyPortrait"]: + var portrait := actor.find_child(portrait_name, true, false) as TextureRect + if portrait == null or portrait.texture == null or not portrait.is_visible_in_tree(): + continue + var sprite := portrait.texture.get_image() + if sprite == null: + continue + masks.append({"rect": portrait.get_global_rect(), "image": sprite}) + return masks + + +func _pixel_occluded(masks: Array, pixel: Vector2) -> bool: + for mask_value in masks: + var mask: Dictionary = mask_value + var rect: Rect2 = mask.rect + if not rect.has_point(pixel): + continue + var sprite: Image = mask.image + var texture_size := Vector2(float(sprite.get_width()), float(sprite.get_height())) + if texture_size.x <= 0.0 or texture_size.y <= 0.0: + continue + var scale := minf(rect.size.x / texture_size.x, rect.size.y / texture_size.y) + if scale <= 0.0: + continue + var drawn := texture_size * scale + var origin := rect.position + (rect.size - drawn) * 0.5 + var uv := (pixel - origin) / scale + var sx := clampi(int(uv.x), 0, sprite.get_width() - 1) + var sy := clampi(int(uv.y), 0, sprite.get_height() - 1) + if sprite.get_pixel(sx, sy).a > 0.4: + return true + return false + + func _point_in_any_rect(rects: Array, point: Vector2) -> bool: for rect_value in rects: if (rect_value as Rect2).has_point(point): @@ -1263,12 +1296,12 @@ func _is_palette_blend(color: Color, stroke: Color, underlays: Array) -> bool: return false -func _core_carrier_at(image: Image, point: Vector2, normal: Vector2, side: float, stroke: Color, underlays: Array, ui_rects: Array, actor_rects: Array) -> Dictionary: +func _core_carrier_at(image: Image, point: Vector2, normal: Vector2, side: float, stroke: Color, underlays: Array, ui_rects: Array, occlusion_masks: Array) -> Dictionary: # Walk outward from the bright core and take the first non-chrome pixel: # with the A5 underlay in place this is the underlay; without it, the raw # rendered carrier is returned (so the measurement can still fail honestly). - # A first valid pixel inside an actor portrait is a sprite occluder, not a - # carrier: the point is reported as occluded and excluded (M110 A5 rule). + # A first valid pixel covered by an opaque actor sprite is an occluder, not + # a carrier: the point is reported as occluded and excluded (M110 A5 rule). for step in range(7): var distance := 1.6 + 0.5 * float(step) var probe := point + normal * distance * side @@ -1285,7 +1318,7 @@ func _core_carrier_at(image: Image, point: Vector2, normal: Vector2, side: float return { "color": color, "distance": distance, - "occluded": _point_in_any_rect(actor_rects, pixel_position), + "occluded": _pixel_occluded(occlusion_masks, pixel_position), } return {} @@ -1329,6 +1362,8 @@ func _path_contrast(image: Image, shell: Control) -> Dictionary: var height := float(image.get_height()) var source: Vector2 = art.get("source_anchor") * Vector2(width, height) var target: Vector2 = art.get("target_anchor") * Vector2(width, height) + if source.distance_to(target) < 1.0: + return {"status": "NOT_APPLICABLE", "reason": "self-target event: no relation span between distinct actors"} var bend := -height * 0.095 if source.x < target.x else height * 0.075 var midpoint := (source + target) * 0.5 + Vector2(0.0, bend) var points: Array = [] @@ -1336,7 +1371,7 @@ func _path_contrast(image: Image, shell: Control) -> Dictionary: var t := float(index) / 20.0 points.append(source * pow(1.0 - t, 2.0) + midpoint * 2.0 * (1.0 - t) * t + target * pow(t, 2.0)) var ui_rects := _visible_ui_ink_rects(shell) - var actor_rects := _visible_actor_rects(shell) + var occlusion_masks := _actor_occlusion_masks(shell) var measured: Array = [] var skipped := 0 var occluded := 0 @@ -1346,7 +1381,7 @@ func _path_contrast(image: Image, shell: Control) -> Dictionary: var normal := Vector2(-tangent.y, tangent.x) var point_carriers: Array = [] for side in [-1.0, 1.0]: - var entry := _core_carrier_at(image, point, normal, side, stroke, underlays, ui_rects, actor_rects) + var entry := _core_carrier_at(image, point, normal, side, stroke, underlays, ui_rects, occlusion_masks) if entry.is_empty(): continue if bool(entry.get("occluded", false)):