i used this plan to create a three-patch repeating texture setup especially for grass:

plan page 1
plan page 2

when i look at the debug_print output (with segments set to 2, which is what the plan shows), i see that the points and UVs are placed exactly like in my plan:

polygon[52] =	[(0, 0), (8, 0), (16, 0), (24, 0), (24, 0), (32, 0), (40, 0), (40, 0), (48, 0), (56, 0), (56, 0), (64, 0), (72, 0), (72, 0), (80, 0), (88, 0), (88, 0), (96, 0), (104, 0), (104, 0), (112, 0), (120, 0), (120, 0), (128, 0), (136, 0), (144, 0), (144, 16), (136, 16), (128, 16), (120, 16), (120, 16), (112, 16), (104, 16), (104, 16), (96, 16), (88, 16), (88, 16), (80, 16), (72, 16), (72, 16), (64, 16), (56, 16), (56, 16), (48, 16), (40, 16), (40, 16), (32, 16), (24, 16), (24, 16), (16, 16), (8, 16), (0, 16)]
uv[52] =	[(0, 0), (0.25, 0), (0.5, 0), (0.75, 0), (0.25, 0), (0.5, 0), (0.75, 0), (0.25, 0), (0.5, 0), (0.75, 0), (0.25, 0), (0.5, 0), (0.75, 0), (0.25, 0), (0.5, 0), (0.75, 0), (0.25, 0), (0.5, 0), (0.75, 0), (0.25, 0), (0.5, 0), (0.75, 0), (0.25, 0), (0.5, 0), (0.75, 0), (1, 0), (1, 1), (0.75, 1), (0.5, 1), (0.25, 1), (0.75, 1), (0.5, 1), (0.25, 1), (0.75, 1), (0.5, 1), (0.25, 1), (0.75, 1), (0.5, 1), (0.25, 1), (0.75, 1), (0.5, 1), (0.25, 1), (0.75, 1), (0.5, 1), (0.25, 1), (0.75, 1), (0.5, 1), (0.25, 1), (0.75, 1), (0.5, 1), (0.25, 1), (0, 1)]
   At: res://main.tscn::GDScript_th3jb:89:_draw()

this should produce UVs that repeat the middle half portion of the texture, but i get this uncomfortable mess instead:

mangled grass

this is my script for this texture setup on an empty Polygon2D that gets generated when playing:

#@tool
extends Polygon2D


@export_range(1,pow(2,16)-1,0.001,'or_less','or_greater','hide_slider') var segment_height : float = 16
@export_range(1,pow(2,16)-1,0.001,'or_less','or_greater','hide_slider') var segment_width : float = 16
@export_range(1,pow(2,16)-1,1,'or_greater') var segments = 1

var cursor: Vector2 = Vector2.ZERO

var debug_counter: bool = true


func _on_property_list_changed() -> void:
	queue_redraw()


#func _ready() -> void:
func _process(_delta: float) -> void:
	queue_redraw()


func _draw() -> void:
	var set_points: Callable = func() -> void:
		var points: PackedVector2Array = []
		
		var cursor_move_right: Callable = func() -> void:
			cursor += Vector2.RIGHT * segment_width / 2.0
		var cursor_move_down: Callable = func() -> void:
			cursor += Vector2.DOWN * segment_width
		var cursor_move_left: Callable = func() -> void:
			cursor += Vector2.LEFT * segment_width / 2.0
		
		var add_top_row: Callable = func() -> void:
			cursor = Vector2.ZERO
			points.append(cursor)
			cursor_move_right.call()
			for i in segments:
				points.append(cursor)
				cursor_move_right.call()
				points.append(cursor)
				cursor_move_right.call()
				points.append(cursor)
			cursor_move_right.call()
			points.append(cursor)
		var add_bottom_row: Callable = func() -> void:
			cursor_move_down.call()
			points.append(cursor)
			cursor_move_left.call()
			for i in segments:
				points.append(cursor)
				cursor_move_left.call()
				points.append(cursor)
				cursor_move_left.call()
				points.append(cursor)
			cursor_move_left.call()
			points.append(cursor)
		
		add_top_row.call()
		add_bottom_row.call()
		set_polygon(points)
	var set_uvs: Callable = func() -> void:
		var uvs: PackedVector2Array = []
		
		var add_top_row: Callable = func() -> void:
			uvs.append(Vector2.ZERO)
			for i in segments:
				uvs.append(Vector2(1/4.0, 0))
				uvs.append(Vector2(2/4.0, 0))
				uvs.append(Vector2(3/4.0, 0))
			uvs.append(Vector2.RIGHT)
		var add_bottom_row: Callable = func() -> void:
			uvs.append(Vector2.ONE)
			for i in segments:
				uvs.append(Vector2(3/4.0, 1))
				uvs.append(Vector2(2/4.0, 1))
				uvs.append(Vector2(1/4.0, 1))
			uvs.append(Vector2.DOWN)
		
		add_top_row.call()
		add_bottom_row.call()
		set_uv(uvs)
	
	set_points.call()
	set_uvs.call()
	if debug_counter:
		debug_counter = false
		print('polygon[', polygon.size() ,'] =\t', polygon)
		print_debug('uv[', uv.size(), '] =\t', uv)

download my project (made in Godot v4.0.3.stable.official [5222a99f5]):

mangled-grass.zip
16kB

(16.1 KiByte download, 18.2 KiByte unzipped)

  • i don't know why my original plan does not work. i can only speculate that Godot gets confused from the doubled-up vertices.
    so i changed my plan to avoid doubling up vertices — by adding a new polygon everywhere the UV coordinates change abruptly.
    this looks like i want, but it slows my computer way too much and it doesn't update properly until reloading the scene or the game — so i can't use this for anything.
    so i changed my plan again, to avoid slowing the computer — by switching from Polygon2D to Node2D and drawing the polygons from code.

    because i want to displace the geometry in a shader, i first coded a rectangle to see if custom-drawn vertices would be displaced by a vertex shader. i was very happy when i first saw they get displaced in the same way as Polygon2D vertices. ☺️
    then i coded the grass ledge generator in Node2D — and it is very fast, as proven by feel and by the profiler and visual profiler showing ≈16.67ms frame time.

    grass ledge

    this is my new script:

    @tool
    extends Node2D
    
    
    @export_group('Size')
    @export_range( \
    	1,pow(2,16)-1,0.001,'or_greater','hide_slider','suffix:px' \
    ) var segment_width: float = 16
    @export_range( \
    	1,pow(2,16)-1,0.001,'or_greater','hide_slider','suffix:px' \
    ) var segment_height: float = 16
    @export_range(0,pow(2,16)-1,1,'or_greater') var segments: int = 1
    @export_group('')
    @export var texture: Texture2D
    
    var pivot: Vector2 = Vector2.RIGHT * segment_width / 2
    
    
    func _process(_delta: float) -> void:
    	if Engine.is_editor_hint():
    		queue_redraw()
    
    
    func _draw() -> void:
    	var reset_pivot: Callable = func() -> void:
    		pivot = Vector2.RIGHT * segment_width / 2
    	var create_begin_piece: Callable = func() -> void:
    		draw_polygon(
    			[
    				Vector2.ZERO,
    				Vector2.RIGHT * segment_width / 2.0,
    				Vector2(segment_width / 2.0, segment_height),
    				Vector2.DOWN * segment_height,
    			],
    			[Color.WHITE],
    			[
    				Vector2.ZERO,
    				Vector2.RIGHT / 2.0,
    				Vector2.ONE / 2.0,
    				Vector2.DOWN / 2.0,
    			],
    			texture
    		)
    	var create_middle_piece: Callable = func() -> void:
    		draw_polygon(
    			[
    				pivot,
    				pivot + Vector2.RIGHT * segment_width / 2.0,
    				pivot + Vector2.RIGHT * segment_width,
    				pivot + Vector2(segment_width, segment_height),
    				pivot + Vector2(segment_width / 2.0, segment_height),
    				pivot + Vector2.DOWN * segment_height,
    			],
    			[Color.WHITE],
    			[
    				Vector2.DOWN / 2.0,
    				Vector2.ONE / 2.0,
    				Vector2(1, 1/2.0),
    				Vector2.ONE,
    				Vector2(1/2.0, 1),
    				Vector2.DOWN,
    			],
    			texture
    		)
    		pivot.x += segment_width
    	var create_end_piece: Callable = func() -> void:
    		draw_polygon(
    			[
    				pivot,
    				pivot + Vector2.RIGHT * segment_width / 2.0,
    				pivot + Vector2(segment_width / 2.0, segment_height),
    				pivot + Vector2.DOWN * segment_height,
    			],
    			[Color.WHITE],
    			[
    				Vector2.RIGHT / 2.0,
    				Vector2.RIGHT,
    				Vector2(1, 1/2.0),
    				Vector2.ONE / 2.0,
    			],
    			texture
    		)
    	
    	reset_pivot.call()
    	create_begin_piece.call()
    	for i in segments:
    		create_middle_piece.call()
    	create_end_piece.call()

    my new project (made in Godot v4.0.3.stable.official [5222a99f5]):

    grass-ledge.zip
    16kB

    (16.3 KiByte download, 19.8 KiByte unzipped)

i don't know why my original plan does not work. i can only speculate that Godot gets confused from the doubled-up vertices.
so i changed my plan to avoid doubling up vertices — by adding a new polygon everywhere the UV coordinates change abruptly.
this looks like i want, but it slows my computer way too much and it doesn't update properly until reloading the scene or the game — so i can't use this for anything.
so i changed my plan again, to avoid slowing the computer — by switching from Polygon2D to Node2D and drawing the polygons from code.

because i want to displace the geometry in a shader, i first coded a rectangle to see if custom-drawn vertices would be displaced by a vertex shader. i was very happy when i first saw they get displaced in the same way as Polygon2D vertices. ☺️
then i coded the grass ledge generator in Node2D — and it is very fast, as proven by feel and by the profiler and visual profiler showing ≈16.67ms frame time.

grass ledge

this is my new script:

@tool
extends Node2D


@export_group('Size')
@export_range( \
	1,pow(2,16)-1,0.001,'or_greater','hide_slider','suffix:px' \
) var segment_width: float = 16
@export_range( \
	1,pow(2,16)-1,0.001,'or_greater','hide_slider','suffix:px' \
) var segment_height: float = 16
@export_range(0,pow(2,16)-1,1,'or_greater') var segments: int = 1
@export_group('')
@export var texture: Texture2D

var pivot: Vector2 = Vector2.RIGHT * segment_width / 2


func _process(_delta: float) -> void:
	if Engine.is_editor_hint():
		queue_redraw()


func _draw() -> void:
	var reset_pivot: Callable = func() -> void:
		pivot = Vector2.RIGHT * segment_width / 2
	var create_begin_piece: Callable = func() -> void:
		draw_polygon(
			[
				Vector2.ZERO,
				Vector2.RIGHT * segment_width / 2.0,
				Vector2(segment_width / 2.0, segment_height),
				Vector2.DOWN * segment_height,
			],
			[Color.WHITE],
			[
				Vector2.ZERO,
				Vector2.RIGHT / 2.0,
				Vector2.ONE / 2.0,
				Vector2.DOWN / 2.0,
			],
			texture
		)
	var create_middle_piece: Callable = func() -> void:
		draw_polygon(
			[
				pivot,
				pivot + Vector2.RIGHT * segment_width / 2.0,
				pivot + Vector2.RIGHT * segment_width,
				pivot + Vector2(segment_width, segment_height),
				pivot + Vector2(segment_width / 2.0, segment_height),
				pivot + Vector2.DOWN * segment_height,
			],
			[Color.WHITE],
			[
				Vector2.DOWN / 2.0,
				Vector2.ONE / 2.0,
				Vector2(1, 1/2.0),
				Vector2.ONE,
				Vector2(1/2.0, 1),
				Vector2.DOWN,
			],
			texture
		)
		pivot.x += segment_width
	var create_end_piece: Callable = func() -> void:
		draw_polygon(
			[
				pivot,
				pivot + Vector2.RIGHT * segment_width / 2.0,
				pivot + Vector2(segment_width / 2.0, segment_height),
				pivot + Vector2.DOWN * segment_height,
			],
			[Color.WHITE],
			[
				Vector2.RIGHT / 2.0,
				Vector2.RIGHT,
				Vector2(1, 1/2.0),
				Vector2.ONE / 2.0,
			],
			texture
		)
	
	reset_pivot.call()
	create_begin_piece.call()
	for i in segments:
		create_middle_piece.call()
	create_end_piece.call()

my new project (made in Godot v4.0.3.stable.official [5222a99f5]):

grass-ledge.zip
16kB

(16.3 KiByte download, 19.8 KiByte unzipped)