supports resources based on masks.

This commit is contained in:
2026-03-04 13:45:52 -08:00
parent a4cc5e8f5f
commit a04ae0edd9
20 changed files with 449 additions and 18 deletions

1
.gitignore vendored
View File

@@ -1,4 +1,5 @@
.godot/** .godot/**
tools/venv/**
.import .import
addons addons
build/ build/

4
PolygonPointsResource.gd Normal file
View File

@@ -0,0 +1,4 @@
extends Resource
class_name PolygonPointsResource
@export var points: PackedVector2Array

View File

@@ -0,0 +1 @@
uid://dtemboas3bi8y

17
ResourcePolygon2D.gd Normal file
View File

@@ -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()

1
ResourcePolygon2D.gd.uid Normal file
View File

@@ -0,0 +1 @@
uid://dcg8kp6a4rarx

View File

@@ -2,5 +2,3 @@ extends Node2D
class_name ScalePoint class_name ScalePoint
@export var target_scale: float @export var target_scale: float

View File

@@ -10,6 +10,7 @@ extends Polygon2D
# Called when the node enters the scene tree for the first time. # Called when the node enters the scene tree for the first time.
func _ready(): func _ready():
_update_polygon_from_resource()
self.color.a = 0.25 self.color.a = 0.25
if Engine.is_editor_hint(): if Engine.is_editor_hint():
self.color.a = 0.25 self.color.a = 0.25
@@ -17,7 +18,12 @@ func _ready():
pass pass
else: else:
hide() 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 interacted
signal walked signal walked
@@ -28,6 +34,10 @@ signal entered(lab)
signal exited signal exited
@export var label: String @export var label: String
@export var points_resource: PolygonPointsResource:
set(value):
points_resource = value
_update_polygon_from_resource()
var is_in = false var is_in = false
# Called every frame. 'delta' is the elapsed time since the previous frame. # 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 Geometry2D.is_point_in_polygon(to_local(get_global_mouse_position()), self.polygon):
if is_in == false: if is_in == false:
is_in = true is_in = true
print("ENTERED0", label)
emit_signal("entered", label) emit_signal("entered", label)
else: else:
if is_in == true: if is_in == true:

View File

@@ -69,6 +69,7 @@ func _process(delta):
func _on_setpiece_entered(lab): func _on_setpiece_entered(lab):
print("LABEL ENTERED ", lab)
$label.show() $label.show()
$label.text = lab $label.text = lab

BIN
mask.png LFS Normal file

Binary file not shown.

40
mask.png.import Normal file
View File

@@ -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

7
new_resource.tres Normal file
View File

@@ -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"

View File

@@ -28,7 +28,6 @@ CameraTransition="*res://camera_transition.tscn"
window/size/viewport_width=1920 window/size/viewport_width=1920
window/size/viewport_height=1080 window/size/viewport_height=1080
window/size/mode=4
window/size/initial_position_type=0 window/size/initial_position_type=0
window/stretch/mode="viewport" window/stretch/mode="viewport"
mouse_cursor/custom_image="res://boot_icon.png" mouse_cursor/custom_image="res://boot_icon.png"

View File

@@ -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="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="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="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="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"] [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"] [node name="exit" parent="kq4_027_forest_path" index="1"]
position = Vector2(396, 438) position = Vector2(396, 438)
[node name="pool" type="Polygon2D" parent="."] [node name="columns" type="Polygon2D" parent="." unique_id=896145355]
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="."]
position = Vector2(-43, 689) position = Vector2(-43, 689)
polygon = PackedVector2Array(398, -10, 402, -118, -4, -128, 0, -19)
color = Color(0.7, 0.7, 0.7, 0.25) color = Color(0.7, 0.7, 0.7, 0.25)
polygon = PackedVector2Array(398, -10, 402, -118, -4, -128, 0, -19)
script = ExtResource("6_n67q9") script = ExtResource("6_n67q9")
label = "Columns" label = "Columns"
[node name="stairs" type="Polygon2D" parent="."] [node name="stairs" type="Polygon2D" parent="." unique_id=737804296]
position = Vector2(1526, 653) position = Vector2(1526, 653)
polygon = PackedVector2Array(129, 92, 534, 73, 498, -75, 5, -56)
color = Color(0.5, 0.5, 0.5, 0.25) color = Color(0.5, 0.5, 0.5, 0.25)
polygon = PackedVector2Array(129, 92, 534, 73, 498, -75, 5, -56)
script = ExtResource("6_n67q9") script = ExtResource("6_n67q9")
label = "Stairs" label = "Stairs"
[node name="ground" type="Polygon2D" parent="."] [node name="ground" type="Polygon2D" parent="." unique_id=1868664008]
position = Vector2(-28.5625, 1097.27) 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) 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") script = ExtResource("6_n67q9")
label = "Ground" label = "Ground"
@@ -125,14 +119,22 @@ scale = Vector2(1.163, 1.179)
texture = ExtResource("5_cu368") texture = ExtResource("5_cu368")
centered = false 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_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_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_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="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="columns" to="." method="_on_columns_looked"]
[connection signal="looked" from="stairs" to="." method="_on_stairs_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="ground" to="." method="_on_ground_looked"]
[connection signal="looked" from="pool" to="." method="_on_pool_looked"]
[editable path="kq4_002_meadow"] [editable path="kq4_002_meadow"]
[editable path="kq4_004_ogres_cottage"] [editable path="kq4_004_ogres_cottage"]

View File

@@ -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"

View File

@@ -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"

BIN
test.png LFS Normal file

Binary file not shown.

40
test.png.import Normal file
View File

@@ -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

0
tools/.gdignore Normal file
View File

284
tools/mask_to_polygon.py Executable file
View File

@@ -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: <image_stem>.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()

3
tools/requirements.txt Normal file
View File

@@ -0,0 +1,3 @@
opencv-python
numpy
shapely