概述
Godot4.2提供了一个名叫Geometry2D
的类。它提供了一些用于2D几何图形如多边形(Polygon)、折线(PolyLine)相关的函数,可以方便实现诸如多边形与多边形或多边形与折线的布尔运算、求交点等。
这是一个非常强大的2D几何辅助类,可以方便你基于几何图形的一些复杂操作。
本篇是笔者2023年7月受到B站一个搬运视频启发,然后基于Godot内置文档研究Geometry2D
并逐个尝试方法,从而总结的一份笔记。原始笔记分为两篇,这里合二为一,并可能会做一定的改写和扩充。期望对你的学习或项目有用。
参考视频
- 【转载】【Godot】最被低估的(几何类)工具 - The MOST UNDERRATED Godot tool (GEOMETRY CLASS)】
多边形与多边形的布尔运算
我们创建如下场景:
- 添加3个
Polygon2D
节点,其中:Polygon1
和Polygon2
是用于布尔运算的多边形。PolygonResult
用于显示布尔运算的结果。 - 我们在
Polygon1
和Polygon2
中分别绘制一个多边形,并修改其中一个的颜色。
求交集
我们为根节点添加如下代码:
extends Node2D
@onready var polygon1 = $Polygon1 # 多边形1
@onready var polygon2 = $Polygon2 # 多边形2
@onready var polygon_result = $PolygonResult # 显示布尔运算结果的Polygon2D节点
func _ready():
var p1 = polygon1.polygon
var p2 = polygon2.polygon
# 显示布尔运算后的图形
polygon_result.polygon = Geometry2D.intersect_polygons(p1,p2)[0]
运行场景后可以看到如下的结果:
修改图形:
再次修改:
注意:Polygon1
和Polygon2
的原点需要对齐,否则可能会出现不准确的结果。这也很容易理解,因为原点不对齐,意味着坐标系不重合,那么整个多边形顶点的坐标就相当于发生了偏移。
亦或运算
extends Node2D
@onready var polygon1 = $Polygon1
@onready var polygon2 = $Polygon2
@onready var polygon_result = $PolygonResult
@onready var polygon_result2 = $PolygonResult2
func _ready():
var p1 = polygon1.polygon
var p2 = polygon2.polygon
var result := Geometry2D.exclude_polygons(p1,p2)
print(result.size())
if result.size() == 1:
polygon_result.polygon = result[0]
elif result.size() == 2:
polygon_result.polygon = result[0]
polygon_result2.polygon = result[1]
pass
求差集
extends Node2D
@onready var polygon1 = $Polygon1
@onready var polygon2 = $Polygon2
@onready var polygon_result = $PolygonResult
@onready var polygon_result2 = $PolygonResult2
func _ready():
var p1 = polygon1.polygon
var p2 = polygon2.polygon
var result := Geometry2D.clip_polygons(p1,p2)
print(result.size())
if result.size() == 1:
polygon_result.polygon = result[0]
elif result.size() == 2:
polygon_result.polygon = result[0]
polygon_result2.polygon = result[1]
pass
求并集
extends Node2D
@onready var polygon1 = $Polygon1
@onready var polygon2 = $Polygon2
@onready var polygon_result = $PolygonResult
@onready var polygon_result2 = $PolygonResult2
func _ready():
var p1 = polygon1.polygon
var p2 = polygon2.polygon
var result := Geometry2D.merge_polygons(p1,p2)
print(result.size())
if result.size() == 1:
polygon_result.polygon = result[0]
elif result.size() == 2:
polygon_result.polygon = result[0]
polygon_result2.polygon = result[1]
pass
总结
多边形与多边形可以进行四种布尔运算,返回的结果可能是0个,1个或多个多边形。
布尔运算 | 方法 | 布尔操作 |
---|---|---|
交集 | intersect_polygons | OPERATION_INTERSECTION |
并集 | merge_polygons | OPERATION_UNION |
差集 | clip_polygons | OPERATION_DIFFERENCE |
亦或 | exclude_polygons | OPERATION_XOR |
PolyLine和Polygon的布尔运算
我们更改场景节点如下:
简单绘制一段折线与一个多边形。
求交集
extends Node2D
@onready var path = $Path
@onready var polygon = $Polygon
@onready var polyline_result = $PolylineResult
@onready var polyline_result2 = $PolylineResult2
func _ready():
var l = path.points
var p = polygon.polygon
var result := Geometry2D.intersect_polyline_with_polygon(l,p)
print(result.size())
if result.size() == 1:
polyline_result.points = result[0]
pass
求差集
extends Node2D
@onready var path = $Path
@onready var polygon = $Polygon
@onready var polyline_result = $PolylineResult
@onready var polyline_result2 = $PolylineResult2
func _ready():
var l = path.points
var p = polygon.polygon
var result := Geometry2D.clip_polyline_with_polygon(l,p)
print(result)
if result.size() == 1:
polyline_result.points = result[0]
elif result.size() == 2:
polyline_result.points = result[0]
polyline_result2.points = result[1]
pass
总结
多边形与折线只能求交集或差集,返回的可能是一条或多条折线。
布尔运算 | 方法 | 布尔操作 |
---|---|---|
交集 | intersect_polyline_with_polygon | OPERATION_INTERSECTION |
差集 | clip_polyline_with_polygon | OPERATION_DIFFERENCE |
求交点
判断两条直线是否相交以及获得交点
@tool
extends Control
var line1 = [Vector2(0,0),Vector2(400,400)]
var line2 = [Vector2(350,150),Vector2(10,300)]
func _draw():
draw_line(line1[0],line1[1],Color.GOLDENROD,2)
draw_line(line2[0],line2[1],Color.GREEN_YELLOW,2)
if Geometry2D.line_intersects_line(line1[0],line1[0].direction_to(line1[1]),line2[0],line2[0].direction_to(line2[1])):
var j_point:Vector2 = Geometry2D.line_intersects_line(line1[0],line1[0].direction_to(line1[1]),line2[0],line2[0].direction_to(line2[1]))
draw_circle(j_point,4,Color.BLUE_VIOLET)
判断线段与圆是否相交以及获得焦点
extends Node2D
# 定义圆
var center = Vector2(400,200)
var r = 50
var pos:Vector2
func _process(delta):
pos = get_global_mouse_position()
queue_redraw()
func _draw():
# 绘制圆
draw_circle(center,r,Color.AQUA)
# 绘制线段
var line = [center,pos]
draw_line(line[0],line[1],Color.YELLOW)
var x = Geometry2D.segment_intersects_circle(line[0],line[1],center,r) # 判断与圆是否有交点
if x:
var x_pos = lerp(line[0],line[1],x)
draw_circle(x_pos,5,Color.ORANGE_RED)
segment_intersects_circle
判断一个线段与圆的边界是否有交点- 如果没有交点,则返回
-1
,如果有则返回一个0.0-1.0
之间的数字 - 这个数字返回的是焦点在线段上的偏移值(从起始点到终点)
- 通过
lerp(线段起始点,线段终点,偏移值)
形式,我们就可以获得实际交点的位置
通过观察线段起点不是圆心的情况,可以发现求得的交点是类似于RayCast2D
求得的碰撞点,也就是首先接触的那个交点。
其实通过求反向线段与圆的交点,就可以同时求出两个交点。
# 求反向线段 (颠倒起始点、终点)
var line2 = line.duplicate()
line2.reverse()
效果:
完整代码:
extends Node2D
# 定义圆
var center = Vector2(400,200)
var r = 50
var pos:Vector2
func _process(delta):
pos = get_global_mouse_position()
queue_redraw()
func _draw():
# 绘制圆
draw_circle(center,r,Color.AQUA)
# 绘制线段
var line = [center+Vector2(200,200),pos]
var line2 = line.duplicate()
line2.reverse()
draw_line(line[0],line[1],Color.YELLOW)
var x = Geometry2D.segment_intersects_circle(line[0],line[1],center,r) # 判断与圆是否有交点
if x != -1:
var x_pos = lerp(line[0],line[1],x)
draw_circle(x_pos,5,Color.ORANGE_RED)
var x2 = Geometry2D.segment_intersects_circle(line2[0],line2[1],center,r) # 判断与圆是否有交点
if x2 != -1:
var x2_pos = lerp(line2[0],line2[1],x2)
draw_circle(x2_pos,5,Color.ORANGE_RED)
判断点是否在一个几何图形内
如果你熟悉Rect2
的话,你就会知道它有一个名叫has_point
的方法,可以判断一个点是否在矩形内。最常见的用途就是判断鼠标是否进入矩形区域内或移出。
Geometry2D
则提供了对圆形、多边形和三角形判断一个点是否在其内部的方法。
判断点是否在圆内
@tool
extends Control
var pos:Vector2
func _process(delta):
pos = get_global_mouse_position()
queue_redraw()
func _draw():
var center = Vector2(400,200)
var r = 50
if Geometry2D.is_point_in_circle(pos,center,r): # 鼠标进入圆
draw_circle(center,r,Color.AQUA)
else: # 鼠标在圆外
draw_circle(center,r,Color.AQUAMARINE)
draw_circle(pos,4,Color.ORANGE) # 绘制鼠标位置
判断点是否在多边形内
@tool
extends Control
var polygon:PackedVector2Array = [
Vector2(100,100),Vector2(300,100),
Vector2(450,150),Vector2(300,300),
Vector2(200,200),Vector2(100,100)
]
var pos:Vector2
func _process(delta):
pos = get_global_mouse_position()
queue_redraw()
func _draw():
if Geometry2D.is_point_in_polygon(pos,polygon): # 鼠标进入圆
draw_polygon(polygon,[Color.AQUA])
else: # 鼠标在圆外
draw_polygon(polygon,[Color.AQUAMARINE])
draw_circle(pos,4,Color.ORANGE) # 绘制鼠标位置
判断点是否在三角形内
@tool
extends Control
var polygon:PackedVector2Array = [
Vector2(100,100),Vector2(300,100),
Vector2(250,350)
]
var pos:Vector2
func _process(delta):
pos = get_global_mouse_position()
queue_redraw()
func _draw():
if Geometry2D.point_is_inside_triangle(pos,polygon[0],polygon[1],polygon[2]): # 鼠标进入三角形
draw_polygon(polygon,[Color.AQUA])
else: # 鼠标在三角形外
draw_polygon(polygon,[Color.AQUAMARINE])
draw_circle(pos,4,Color.ORANGE) # 绘制鼠标位置
获取最近点
求取线段上离鼠标位置最近的点
@tool
extends Control
var line1 = [Vector2(0,0),Vector2(400,400)]
var pos:Vector2
func _process(delta):
pos = get_global_mouse_position()
queue_redraw()
func _draw():
draw_line(line1[0],line1[1],Color.GOLDENROD,2)
var c_point = Geometry2D.get_closest_point_to_segment(pos,line1[0],line1[1])
draw_circle(c_point,4,Color.GREEN_YELLOW)
draw_circle(pos,4,Color.BLUE)
可以看到:
- 如果鼠标点在与线段平行的所有线段组成的无限矩形范围内,则最近点就是垂线与线段的交点。
- 如果超出线段端点范围,则最近点停留在端点上。
求取直线上离鼠标最近的点
@tool
extends Control
var line1 = [Vector2(0,0),Vector2(400,400)]
var pos:Vector2
func _process(delta):
pos = get_global_mouse_position()
queue_redraw()
func _draw():
draw_line(line1[0],line1[1],Color.GOLDENROD,2)
var c_point = Geometry2D.get_closest_point_to_segment_uncapped(pos,line1[0],line1[1])
draw_circle(c_point,4,Color.GREEN_YELLOW)
draw_circle(pos,4,Color.BLUE)
与线段的概念不同,直线是一个无限长的几何图形,所以求某个点离直线最近的点,就是经过该点做直线的垂线时,获得的交点。
求取两条线段之间最近的两个点
@tool
extends Control
var line1 = [Vector2(100,100),Vector2(400,100)]
var line2 = [Vector2(200,200),Vector2(500,500)]
var pos:Vector2
func _process(delta):
line2[1] = get_global_mouse_position()
queue_redraw()
func _draw():
draw_line(line1[0],line1[1],Color.GOLDENROD,2)
draw_line(line2[0],line2[1],Color.ORANGE_RED,2)
var c_points:PackedVector2Array = Geometry2D.get_closest_points_between_segments(line1[0],line1[1],line2[0],line2[1])
draw_circle(c_points[0],4,Color.GREEN_YELLOW)
draw_circle(c_points[1],4,Color.GREEN_YELLOW)
两条线段,固定其中一条:
- 如果不相交,则最近点就是求经过动态线段两个端点中离固定线段比较近的一个端点做垂线的交点(。。。我自己也觉得绕)
- 如果相交,就是交点。
膨胀或缩小多边形
圆角化膨胀或缩小
@tool
extends Control
@export var offset:int = 0:
set(val):
offset = val
queue_redraw()
var polygon:PackedVector2Array = ShapePoints.star(0,5,50,30,Vector2(400,200))
var pos:Vector2
func _draw():
var off_polygon = Geometry2D.offset_polygon(polygon,offset,Geometry2D.JOIN_ROUND)[0]
draw_polygon(off_polygon,[Color.AQUAMARINE])
保持尖角的膨胀和缩小
@tool
extends Control
@export var offset:int = 0:
set(val):
offset = val
queue_redraw()
var polygon:PackedVector2Array = ShapePoints.star(0,5,50,30,Vector2(400,200))
var pos:Vector2
func _draw():
var off_polygon = Geometry2D.offset_polygon(polygon,offset,Geometry2D.JOIN_MITER)[0]
draw_polygon(off_polygon,[Color.AQUAMARINE])
切角化的膨胀或缩小
@tool
extends Control
@export var offset:int = 0:
set(val):
offset = val
queue_redraw()
var polygon:PackedVector2Array = ShapePoints.star(0,5,50,30,Vector2(400,200))
var pos:Vector2
func _draw():
var off_polygon = Geometry2D.offset_polygon(polygon,offset,Geometry2D.JOIN_SQUARE)[0]
draw_polygon(off_polygon,[Color.AQUAMARINE])
分解凹多边形为几个凸多边形
var result := Geometry2D.decompose_polygon_in_convex(p1)
可以看到,如果一个多边形是凹多边形(Concave Polygon),那么使用decompose_polygon_in_convex
方法,会将其拆分为若干个凸多边形。
从凹多边形求凸多边形
var result := Geometry2D.convex_hull(p1)
print(p1)
print(result)
[(195, 152), (257, 74), (345, 141), (448, 107), (409, 216), (461, 272), (380, 323), (325, 274), (251, 296), (270, 200)]
[(195, 152), (257, 74), (448, 107), (461, 272), (380, 323), (251, 296), (195, 152)]
可以看到,其结果是删除了所有凹进去的点,最后形成一个完全包裹原来凹多边形的凸多边形。
其他的一些尝试:
获取多边形的矩形尺寸
对于下图所示的多边形,我们使用make_atlas
:
var p1 = polygon1.polygon
print(JSON.stringify(Geometry2D.make_atlas(p1),"\t"))
打印内容:
{
"points": "[(251, 940), (693, 595), (0, 294), (0, 187), (0, 0), (543, 0), (448, 272), (374, 595), (0, 319), (446, 940)]",
"size": "(1004, 1167)"
}
可以看到结果是一个字典,包含两个键:
points
返回的是图形顶点数据size
返回一个尺寸,但是我暂时没有搞懂它的含义,它的尺寸是一个远大于多边形包围盒尺寸的
任意多边形包围盒position、end和size求取函数
- 通过求所有顶点中最小的
x
和y
组成的Vector2
就可以得到多边形包围盒Rect2
的左上角顶点position
。 - 通过求所有顶点中最大的
x
和y
组成的Vector2
就可以得到多边形包围盒Rect2
的右下角顶点end
。 Rect2
的尺寸size
=end - position
Rect2
的中心点center
=position + size/2.0
# 求任意多边形包围盒Rect2的position
func get_polygon_Rect2_position(polygon:PackedVector2Array) -> Vector2:
var xs = [] # 所有的x
var ys = [] # 所有的y
for i in range(polygon.size()):
xs.append(polygon[i].x)
ys.append(polygon[i].y)
# 升序排列
xs.sort()
ys.sort()
return Vector2(xs[0],ys[0]) # 返回由最小的x和y组成的点坐标
# 求任意多边形包围盒Rect2的end
func get_polygon_Rect2_end(polygon:PackedVector2Array) -> Vector2:
var xs = [] # 所有的x
var ys = [] # 所有的y
for i in range(polygon.size()):
xs.append(polygon[i].x)
ys.append(polygon[i].y)
# 降序排列
xs.sort()
xs.reverse()
ys.sort()
ys.reverse()
return Vector2(xs[0],ys[0]) # 返回由最大的x和y组成的点坐标
# 求任意多边形包围盒Rect2的size
func get_polygon_Rect2_size(polygon:PackedVector2Array) -> Vector2:
var size:Vector2
var position = get_polygon_Rect2_position(polygon)
var end = get_polygon_Rect2_end(polygon)
if end > position:
size = end - position
else:
size = position - end
return size
# 求任意多边形包围盒Rect2的center
func get_polygon_Rect2_center(polygon:PackedVector2Array) -> Vector2:
var size = get_polygon_Rect2_size(polygon)
var position = get_polygon_Rect2_position(polygon)
return position + size/2.0
通过设定一个空的控件尺寸,可以验证结果的正确性:
当然,上面的代码只是展示了求取的思路,实际上,我们只需要一个函数,直接返回Rect2就行。
# 求任意多边形包围盒Rect2
func get_polygon_Rect2(polygon:PackedVector2Array) -> Rect2:
var xs = [];var ys = [] # 所有的x和y
# 遍历所有顶点,抽离出所有的x和y坐标到单独的数组
for i in range(polygon.size()):
xs.append(polygon[i].x);ys.append(polygon[i].y)
# 升序排列
xs.sort();ys.sort()
# 获取 position
var pos = Vector2(xs[0],ys[0]) # Rect2.position
# 降序排列
xs.reverse();ys.reverse()
# 获取 end
var end = Vector2(xs[0],ys[0]) # Rect2.end
# 获取 size
var size = end - pos if end > pos else pos -end # 计算 Rect2.size
return Rect2(pos,size)
测试:
var p1 = polygon1.polygon
var rect = get_polygon_Rect2(p1)
print(rect.position)
print(rect.size)
print(rect.end)
print(rect.get_center())
对多边形进行三角化
extends Node2D
@onready var polygon1 = $Polygon1
func _ready():
var p1 = polygon1.polygon
print(Geometry2D.triangulate_delaunay(p1))
打印内容:
[1, 2, 3, 2, 3, 4, 3, 4, 5, 3, 5, 6, 6, 7, 8, 1, 3, 8, 3, 6, 8, 0, 1, 8, 0, 8, 9]
其中的数字代表的是多边形的顶点索引,每三个顶点构成一个三角形。所以返回的元素数目肯定是3的整数倍。
我们编写一个函数来获得所有三角形的数据。
# 返回多边形三角化后的所有三角形数据
func polygon_triangles(polygon:PackedVector2Array) -> Array[PackedVector2Array]:
var arr:Array[PackedVector2Array] = []
var indexs = Geometry2D.triangulate_delaunay(polygon)
print(indexs)
for i in range(indexs.size()/3):
var tag_index = indexs.slice(3 * i,3 * i + 3)
var tag:PackedVector2Array = []
tag.append(polygon[tag_index[0]])
tag.append(polygon[tag_index[1]])
tag.append(polygon[tag_index[2]])
arr.append(tag)
return arr
通过遍历和绘制所有三角形:
extends Node2D
@onready var polygon1 = $Polygon1
var tags:Array[PackedVector2Array]
func _ready():
var p1 = polygon1.polygon
tags = polygon_triangles(p1)
func _draw():
for tag in tags:
var color = Color(randf(),randf(),randf())
draw_colored_polygon(tag,color)
绘制出的结果:
我们使用一个更明显的凹多边形:
可以看到,凹多边形三角化后,会填补为一个凸多边形。
如果想要凹多边形得到完全一致的三角化效果,则需要用Geometry2D
的triangulate_polygon
替代triangulate_delaunay
。
之前的自定义函数也就变为了:
# 返回多边形三角化后的所有三角形数据
func polygon_triangles(polygon:PackedVector2Array) -> Array[PackedVector2Array]:
var arr:Array[PackedVector2Array] = []
var indexs = Geometry2D.triangulate_polygon(polygon)
for i in range(indexs.size()/3):
var tag_index = indexs.slice(3 * i,3 * i + 3)
var tag:PackedVector2Array = []
tag.append(polygon[tag_index[0]])
tag.append(polygon[tag_index[1]])
tag.append(polygon[tag_index[2]])
arr.append(tag)
return arr
凹多边形返回的三角化效果: