diff --git a/.gitignore b/.gitignore index 0b99a24..cb47d11 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .godot/** +tools/venv/** .import addons build/ diff --git a/PolygonPointsResource.gd b/PolygonPointsResource.gd new file mode 100644 index 0000000..03fa52a --- /dev/null +++ b/PolygonPointsResource.gd @@ -0,0 +1,4 @@ +extends Resource +class_name PolygonPointsResource + +@export var points: PackedVector2Array diff --git a/PolygonPointsResource.gd.uid b/PolygonPointsResource.gd.uid new file mode 100644 index 0000000..ab29982 --- /dev/null +++ b/PolygonPointsResource.gd.uid @@ -0,0 +1 @@ +uid://dtemboas3bi8y diff --git a/ResourcePolygon2D.gd b/ResourcePolygon2D.gd new file mode 100644 index 0000000..e5671fd --- /dev/null +++ b/ResourcePolygon2D.gd @@ -0,0 +1,17 @@ +@tool +extends Polygon2D +class_name ResourcePolygon2D + +@export var points_resource: PolygonPointsResource: + set(value): + points_resource = value + _update_polygon() + + +func _update_polygon() -> void: + if points_resource and points_resource.points.size() > 0: + polygon = points_resource.points + + +func _ready() -> void: + _update_polygon() diff --git a/ResourcePolygon2D.gd.uid b/ResourcePolygon2D.gd.uid new file mode 100644 index 0000000..cb6ab05 --- /dev/null +++ b/ResourcePolygon2D.gd.uid @@ -0,0 +1 @@ +uid://dcg8kp6a4rarx diff --git a/ScalePoint_.gd b/ScalePoint_.gd index 1e5ca27..f27f92a 100644 --- a/ScalePoint_.gd +++ b/ScalePoint_.gd @@ -2,5 +2,3 @@ extends Node2D class_name ScalePoint @export var target_scale: float - - diff --git a/SetPiece_.gd b/SetPiece_.gd index 1cd21c9..74c9c51 100644 --- a/SetPiece_.gd +++ b/SetPiece_.gd @@ -10,6 +10,7 @@ extends Polygon2D # Called when the node enters the scene tree for the first time. func _ready(): + _update_polygon_from_resource() self.color.a = 0.25 if Engine.is_editor_hint(): self.color.a = 0.25 @@ -17,7 +18,12 @@ func _ready(): pass else: hide() - pass # Replace with function body. + pass # Replace with function body + + +func _update_polygon_from_resource() -> void: + if points_resource and points_resource.points.size() > 0: + polygon = points_resource.points signal interacted signal walked @@ -28,6 +34,10 @@ signal entered(lab) signal exited @export var label: String +@export var points_resource: PolygonPointsResource: + set(value): + points_resource = value + _update_polygon_from_resource() var is_in = false # Called every frame. 'delta' is the elapsed time since the previous frame. @@ -38,6 +48,7 @@ func _process(delta): if Geometry2D.is_point_in_polygon(to_local(get_global_mouse_position()), self.polygon): if is_in == false: is_in = true + print("ENTERED0", label) emit_signal("entered", label) else: if is_in == true: diff --git a/label.gd b/label.gd index 0e420bc..e04868b 100644 --- a/label.gd +++ b/label.gd @@ -69,6 +69,7 @@ func _process(delta): func _on_setpiece_entered(lab): + print("LABEL ENTERED ", lab) $label.show() $label.text = lab diff --git a/mask.png b/mask.png new file mode 100644 index 0000000..6a092d5 --- /dev/null +++ b/mask.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1502d17093c90c4cf370a58d2dc58908df415d181ca78133ff1948835fab2807 +size 75300 diff --git a/mask.png.import b/mask.png.import new file mode 100644 index 0000000..f85d106 --- /dev/null +++ b/mask.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cp5ufx8q34bfi" +path="res://.godot/imported/mask.png-b945516e6475612c1c4c3b4f8dd0bdc6.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://mask.png" +dest_files=["res://.godot/imported/mask.png-b945516e6475612c1c4c3b4f8dd0bdc6.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/new_resource.tres b/new_resource.tres new file mode 100644 index 0000000..a8367e3 --- /dev/null +++ b/new_resource.tres @@ -0,0 +1,7 @@ +[gd_resource type="Resource" script_class="PolygonPointsResource" format=3 uid="uid://c13c8lnru1rsc"] + +[ext_resource type="Script" uid="uid://dtemboas3bi8y" path="res://PolygonPointsResource.gd" id="1_g5ko2"] + +[resource] +script = ExtResource("1_g5ko2") +metadata/_custom_type_script = "uid://dtemboas3bi8y" diff --git a/project.godot b/project.godot index d1ba096..547041b 100644 --- a/project.godot +++ b/project.godot @@ -28,7 +28,6 @@ CameraTransition="*res://camera_transition.tscn" window/size/viewport_width=1920 window/size/viewport_height=1080 -window/size/mode=4 window/size/initial_position_type=0 window/stretch/mode="viewport" mouse_cursor/custom_image="res://boot_icon.png" diff --git a/scenes/kq4_003_fountain_pool/kq4_003_fountain_pool.tscn b/scenes/kq4_003_fountain_pool/kq4_003_fountain_pool.tscn index a8be314..d1fe2d8 100644 --- a/scenes/kq4_003_fountain_pool/kq4_003_fountain_pool.tscn +++ b/scenes/kq4_003_fountain_pool/kq4_003_fountain_pool.tscn @@ -4,6 +4,7 @@ [ext_resource type="Texture2D" uid="uid://nb7ybebcy6ok" path="res://scenes/kq4_003_fountain_pool/caption_1_2884713022_generated.png" id="2_ev2w4"] [ext_resource type="Script" uid="uid://xmphq3i0wbg3" path="res://ScalePoint_.gd" id="3_1g2ot"] [ext_resource type="PackedScene" uid="uid://c4vc1wx7k6cw" path="res://TransitionPiece.tscn" id="4_6r684"] +[ext_resource type="Resource" uid="uid://biiu4g5skgjun" path="res://scenes/kq4_003_fountain_pool/pool_shap2.tres" id="4_nuxlg"] [ext_resource type="Texture2D" uid="uid://bmvcn5pl2qble" path="res://scenes/kq4_003_fountain_pool/fg.png" id="5_cu368"] [ext_resource type="Script" uid="uid://bounwnqg34t5k" path="res://SetPiece_.gd" id="6_n67q9"] @@ -92,31 +93,24 @@ position = Vector2(207, 489) [node name="exit" parent="kq4_027_forest_path" index="1"] position = Vector2(396, 438) -[node name="pool" type="Polygon2D" parent="."] -position = Vector2(577, 565) -polygon = PackedVector2Array(-297, 55, 23, 45, 623, 45, 873, 55, 1023, 115, 1103, 155, 1043, 215, 923, 295, 423, 305, -77, 300, -197, 275, -277, 215, -317, 135, -297, 55) -color = Color(0, 0.407843, 0.776471, 0.25) -script = ExtResource("6_n67q9") -label = "Pool" - -[node name="columns" type="Polygon2D" parent="."] +[node name="columns" type="Polygon2D" parent="." unique_id=896145355] position = Vector2(-43, 689) -polygon = PackedVector2Array(398, -10, 402, -118, -4, -128, 0, -19) color = Color(0.7, 0.7, 0.7, 0.25) +polygon = PackedVector2Array(398, -10, 402, -118, -4, -128, 0, -19) script = ExtResource("6_n67q9") label = "Columns" -[node name="stairs" type="Polygon2D" parent="."] +[node name="stairs" type="Polygon2D" parent="." unique_id=737804296] position = Vector2(1526, 653) -polygon = PackedVector2Array(129, 92, 534, 73, 498, -75, 5, -56) color = Color(0.5, 0.5, 0.5, 0.25) +polygon = PackedVector2Array(129, 92, 534, 73, 498, -75, 5, -56) script = ExtResource("6_n67q9") label = "Stairs" -[node name="ground" type="Polygon2D" parent="."] +[node name="ground" type="Polygon2D" parent="." unique_id=1868664008] position = Vector2(-28.5625, 1097.27) -polygon = PackedVector2Array(17.5625, -399.27, 392.211, -408.512, 261.641, -180.051, 550.586, -98.4498, 573.523, -160.27, 1285.42, -160.27, 1385.22, -90.7104, 1544.09, -439.453, 396.914, -535.582, 17.5547, -1051.08) color = Color(0.4, 0.27, 0.13, 0.25) +polygon = PackedVector2Array(17.5625, -399.27, 392.211, -408.512, 261.641, -180.051, 550.586, -98.4498, 573.523, -160.27, 1285.42, -160.27, 1385.22, -90.7104, 1544.09, -439.453, 396.914, -535.582, 17.5547, -1051.08) script = ExtResource("6_n67q9") label = "Ground" @@ -125,14 +119,22 @@ scale = Vector2(1.163, 1.179) texture = ExtResource("5_cu368") centered = false +[node name="pool" type="Polygon2D" parent="." unique_id=1959309137 groups=["set-piece"]] +scale = Vector2(0.78, 0.777) +color = Color(0, 0.407843, 0.776471, 0.25) +polygon = PackedVector2Array(521.48, 955.4, 550.26, 943.14, 655.6, 946.46, 710.07, 906.29, 1799.47, 902.07, 1850.61, 939.09, 1906.74, 946.14, 1943.33, 962.53, 1953, 978, 1922.91, 1008.61, 1937.93, 1040.47, 1919.35, 1062.35, 1496.34, 1064, 1491.6, 1128.52, 1474.93, 1138.71, 1024.07, 1140.71, 1007.29, 1122.93, 1010.4, 1063.91, 596.53, 1061.93, 577.14, 1045.74, 578.99, 1014.44, 504.65, 1005.35, 492, 990) +script = ExtResource("6_n67q9") +label = "Pool" +points_resource = ExtResource("4_nuxlg") + [connection signal="interacted" from="kq4_002_meadow" to="." method="_on_meadow_interacted"] [connection signal="interacted" from="kq4_004_ogres_cottage" to="." method="_on_ogre_house_interacted"] [connection signal="interacted" from="kq4_009_shady_wooded_area" to="." method="_on_shady_wooded_area_interacted"] [connection signal="interacted" from="kq4_027_forest_path" to="." method="_on_forest_path_27_interacted"] -[connection signal="looked" from="pool" to="." method="_on_pool_looked"] [connection signal="looked" from="columns" to="." method="_on_columns_looked"] [connection signal="looked" from="stairs" to="." method="_on_stairs_looked"] [connection signal="looked" from="ground" to="." method="_on_ground_looked"] +[connection signal="looked" from="pool" to="." method="_on_pool_looked"] [editable path="kq4_002_meadow"] [editable path="kq4_004_ogres_cottage"] diff --git a/scenes/kq4_003_fountain_pool/pool_shap.tres b/scenes/kq4_003_fountain_pool/pool_shap.tres new file mode 100644 index 0000000..706d03a --- /dev/null +++ b/scenes/kq4_003_fountain_pool/pool_shap.tres @@ -0,0 +1,8 @@ +[gd_resource type="PolygonPointsResource" script_class="PolygonPointsResource" format=3 uid="uid://ba5nw7qr18eeq"] + +[ext_resource type="Script" uid="uid://dtemboas3bi8y" path="res://PolygonPointsResource.gd" id="1_ppr"] + +[resource] +script = ExtResource("1_ppr") +points = PackedVector2Array(2456.78, 1299.07, 2360.97, 1342.68, 920, 1454, 308.68, 1279.56, 134.7, 1193.86, 49.31, 1028.52, -14.39, 44.73, 33.27, -11.45, 76.7, -30.49, 955.2, -99.7, 2533.27, -92.39, 2578.15, -55.56, 2595, 0, 2586.34, 1059.38) +metadata/_custom_type_script = "uid://dtemboas3bi8y" diff --git a/scenes/kq4_003_fountain_pool/pool_shap2.tres b/scenes/kq4_003_fountain_pool/pool_shap2.tres new file mode 100644 index 0000000..5acc87e --- /dev/null +++ b/scenes/kq4_003_fountain_pool/pool_shap2.tres @@ -0,0 +1,8 @@ +[gd_resource type="Resource" script_class="PolygonPointsResource" format=3 uid="uid://biiu4g5skgjun"] + +[ext_resource type="Script" uid="uid://dtemboas3bi8y" path="res://PolygonPointsResource.gd" id="1_ppr"] + +[resource] +script = ExtResource("1_ppr") +points = PackedVector2Array(521.48, 955.4, 550.26, 943.14, 655.6, 946.46, 710.07, 906.29, 1799.47, 902.07, 1850.61, 939.09, 1906.74, 946.14, 1943.33, 962.53, 1953, 978, 1922.91, 1008.61, 1937.93, 1040.47, 1919.35, 1062.35, 1496.34, 1064, 1491.6, 1128.52, 1474.93, 1138.71, 1024.07, 1140.71, 1007.29, 1122.93, 1010.4, 1063.91, 596.53, 1061.93, 577.14, 1045.74, 578.99, 1014.44, 504.65, 1005.35, 492, 990) +metadata/_custom_type_script = "uid://dtemboas3bi8y" diff --git a/test.png b/test.png new file mode 100644 index 0000000..5b60e1e --- /dev/null +++ b/test.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1aff2ee32ce1370daf01843144a023ad424cbcec5af3cd9cbdff0785043b89a4 +size 26677 diff --git a/test.png.import b/test.png.import new file mode 100644 index 0000000..701e684 --- /dev/null +++ b/test.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://c1s78mcjg2jb5" +path="res://.godot/imported/test.png-2b0b935732229e5bd5e655f2644b2498.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://test.png" +dest_files=["res://.godot/imported/test.png-2b0b935732229e5bd5e655f2644b2498.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/tools/.gdignore b/tools/.gdignore new file mode 100644 index 0000000..e69de29 diff --git a/tools/mask_to_polygon.py b/tools/mask_to_polygon.py new file mode 100755 index 0000000..164757a --- /dev/null +++ b/tools/mask_to_polygon.py @@ -0,0 +1,284 @@ +#!/usr/bin/env python3 +""" +Convert a black/white mask image to a Godot .tres polygon resource. +""" + +import argparse +import secrets +import sys +from pathlib import Path + +import cv2 +import numpy as np +from shapely.geometry import Polygon +from shapely.ops import unary_union + + +ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyz" + + +def to_base36(num: int) -> str: + """Convert integer to lowercase base36 (like Godot).""" + if num == 0: + return "0" + chars = [] + while num: + num, rem = divmod(num, 36) + chars.append(ALPHABET[rem]) + return "".join(reversed(chars)) + + +def generate_godot_uid() -> str: + """Generate a random Godot-compatible UID.""" + value = secrets.randbits(64) + return to_base36(value) + + +def find_largest_contour(contours: list) -> np.ndarray: + """Find the largest contour by area.""" + if not contours: + return None + return max(contours, key=cv2.contourArea) + + +def contours_to_polygon(contours: list, mode: str) -> np.ndarray: + """ + Convert contours to a single polygon. + + Args: + contours: List of contours from cv2.findContours + mode: "convex_hull" or "largest_only" + + Returns: + Single contour as numpy array + """ + if not contours: + return None + + if mode == "largest_only": + return find_largest_contour(contours) + + if mode == "convex_hull": + all_points = np.vstack(contours) + return cv2.convexHull(all_points) + + raise ValueError(f"Unknown mode: {mode}") + + +def simplify_to_target_points(contour: np.ndarray, target_points: int) -> np.ndarray: + """ + Simplify a contour to approximately target number of points. + Uses Douglas-Peucker algorithm with iterative epsilon adjustment. + """ + if len(contour) <= target_points: + return contour + + contour = contour.squeeze() + if len(contour.shape) == 1: + return contour.reshape(-1, 1, 2) + + perimeter = cv2.arcLength(contour, True) + epsilon_low = 0.0 + epsilon_high = perimeter * 0.1 + + best_result = contour + + for _ in range(50): + epsilon_mid = (epsilon_low + epsilon_high) / 2 + simplified = cv2.approxPolyDP(contour, epsilon_mid, True) + + if len(simplified) == target_points: + return simplified + + if len(simplified) > target_points: + epsilon_low = epsilon_mid + else: + epsilon_high = epsilon_mid + best_result = simplified + + if abs(len(simplified) - target_points) <= 1: + best_result = simplified + break + + return best_result + + +def pad_polygon(points: np.ndarray, padding: float) -> np.ndarray: + """ + Expand polygon outward by padding pixels using Shapely buffer. + """ + if padding <= 0: + return points + + points_2d = points.squeeze() + if len(points_2d.shape) == 1: + return points + + polygon = Polygon(points_2d) + + if not polygon.is_valid: + polygon = polygon.buffer(0) + + buffered = polygon.buffer(padding) + + if buffered.is_empty: + return points + + if buffered.geom_type == "MultiPolygon": + buffered = max(buffered.geoms, key=lambda p: p.area) + + exterior_coords = list(buffered.exterior.coords)[:-1] + return np.array(exterior_coords, dtype=np.float32).reshape(-1, 1, 2) + + +def ensure_clockwise(points: np.ndarray) -> np.ndarray: + """ + Ensure points are in clockwise order (visually). + In image coordinates (Y-down), clockwise means positive area. + """ + points_2d = points.squeeze() + if len(points_2d.shape) == 1: + return points + + area = 0.0 + n = len(points_2d) + for i in range(n): + j = (i + 1) % n + area += points_2d[i][0] * points_2d[j][1] + area -= points_2d[j][0] * points_2d[i][1] + + if area < 0: + return points[::-1] + + return points + + +def write_preview( + original_image: np.ndarray, points: np.ndarray, output_path: Path +) -> None: + """ + Write a preview image showing the polygon drawn on the mask. + """ + preview = cv2.cvtColor(original_image, cv2.COLOR_GRAY2BGR) + pts = points.squeeze().astype(np.int32).reshape(-1, 1, 2) + cv2.polylines(preview, [pts], True, (0, 255, 0), 2) + for i, pt in enumerate(pts): + cv2.circle(preview, tuple(pt[0]), 4, (0, 0, 255), -1) + cv2.putText( + preview, + str(i), + (pt[0][0] + 5, pt[0][1] - 5), + cv2.FONT_HERSHEY_SIMPLEX, + 0.4, + (255, 255, 0), + 1, + ) + cv2.imwrite(str(output_path), preview) + + +def write_tres(points: np.ndarray, output_path: Path, uid: str) -> None: + """ + Write polygon points to a Godot .tres resource file. + """ + points_2d = points.squeeze() + if len(points_2d.shape) == 1: + raise ValueError("Invalid points array") + + coords = [] + for pt in points_2d: + coords.extend([round(pt[0], 2), round(pt[1], 2)]) + + coords_str = ", ".join(str(c) for c in coords) + + script_uid = "uid://dtemboas3bi8y" + + content = f"""[gd_resource type="Resource" script_class="PolygonPointsResource" format=3 uid="uid://{uid}"] + +[ext_resource type="Script" uid="{script_uid}" path="res://PolygonPointsResource.gd" id="1_ppr"] + +[resource] +script = ExtResource("1_ppr") +points = PackedVector2Array({coords_str}) +metadata/_custom_type_script = "{script_uid}" +""" + + output_path.write_text(content) + + +def main(): + parser = argparse.ArgumentParser( + description="Convert a black/white mask to a Godot polygon .tres resource" + ) + parser.add_argument("image", type=Path, help="Input mask image (black/white)") + parser.add_argument( + "--padding", + type=float, + default=0, + help="Pixels to expand polygon outward (default: 0)", + ) + parser.add_argument( + "--target-points", + type=int, + default=10, + help="Target number of polygon points (default: 10)", + ) + parser.add_argument( + "--output", + type=Path, + default=None, + help="Output .tres file path (default: .tres)", + ) + parser.add_argument( + "--mode", + choices=["convex_hull", "largest_only"], + default="convex_hull", + help="How to handle multiple regions (default: convex_hull)", + ) + parser.add_argument( + "--preview", + type=Path, + default=None, + help="Output preview PNG showing polygon on mask", + ) + + args = parser.parse_args() + + if not args.image.exists(): + print(f"Error: Image not found: {args.image}", file=sys.stderr) + sys.exit(1) + + image = cv2.imread(str(args.image), cv2.IMREAD_GRAYSCALE) + if image is None: + print(f"Error: Could not load image: {args.image}", file=sys.stderr) + sys.exit(1) + + _, binary = cv2.threshold(image, 127, 255, cv2.THRESH_BINARY) + contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + + if not contours: + print("Error: No contours found in mask", file=sys.stderr) + sys.exit(1) + + polygon = contours_to_polygon(contours, args.mode) + padded = pad_polygon(polygon, args.padding) + simplified = simplify_to_target_points(padded, args.target_points) + clockwise = ensure_clockwise(simplified) + + output_path = args.output + if output_path is None: + output_path = args.image.with_suffix(".tres") + + uid = generate_godot_uid() + write_tres(clockwise, output_path, uid) + + if args.preview: + write_preview(image, clockwise, args.preview) + print(f"Preview: {args.preview}") + + print(f"Created: {output_path}") + print(f"Points: {len(clockwise.squeeze())}") + print(f"UID: uid://{uid}") + + +if __name__ == "__main__": + main() diff --git a/tools/requirements.txt b/tools/requirements.txt new file mode 100644 index 0000000..09edf7d --- /dev/null +++ b/tools/requirements.txt @@ -0,0 +1,3 @@ +opencv-python +numpy +shapely