Applying textures to polygon2d via script

Godot Version

4.2

Question

  1. Hi all. I’m stuck in a point where I’m trying to procedurally generate a texture via gdscript (godot 4.2), but I keep getting pink textures. Here are the main parts of the code:

@export var texture_dimensions: Vector2 = Vector2(100, 100)

…code that generates hexagons (works)
…apply textures:

func generate_voronoi_texture(polygon: Polygon2D):
var voronoi_texture = Image.new()
voronoi_texture.create(int(texture_dimensions.x), int(texture_dimensions.y), false, Image.FORMAT_RGBA8)
print ("texture dimensions: ", texture_dimensions)

var points: Array[Vector2] = []
var new_voronoi_regions = randomize_in_range(voronoi_regions, region_min_bias, region_max_bias)
for i in range(new_voronoi_regions):
    points.append(Vector2(randi_range(0, int(texture_dimensions.x)), randi_range(0, int(texture_dimensions.y))))

for i in range(int(texture_dimensions.x)):
    for j in range(int(texture_dimensions.y)):
        var nearest_point_index = 0
        var nearest_distance = INF

        for k in range(points.size()):
            var distance = points[k].distance_to(Vector2(i, j))
            if distance < nearest_distance:
                nearest_distance = distance
                nearest_point_index = k

        var color = Color.from_hsv(nearest_point_index / float(new_voronoi_regions), 1, 1)
        voronoi_texture.set_pixel(i, j, color)

voronoi_texture.generate_mipmaps()

var texture = ImageTexture.new()
texture.create_from_image(voronoi_texture)
polygon.texture = texture

==> result, textures are pink (edited)

You’re calling both create and create_from_image on an instance of Image, but those methods are static (i.e. should be directly called from the type). You should get a warning about that as well. And a ton of errors, directly resulting from it.

So turn this:

# ...
var voronoi_texture = Image.new()
voronoi_texture.create(int(texture_dimensions.x), int(texture_dimensions.y), false, 
# ...
var texture = ImageTexture.new()
texture.create_from_image(voronoi_texture)
# ...

into this:

# ...
var voronoi_texture = Image.create(int(texture_dimensions.x), int(texture_dimensions.y), false, 
# ...
var texture = ImageTexture.create_from_image(voronoi_texture)
# ...
1 Like

@njamster Thank you very much for your solution.
It worked and now I am getting actual colors on the hexagon.
The next part which is still missing is the creation of voronoi “islands” within each hexagon, so that you get multiple colors per hexagon, based on the voronoi regions.
I’m attaching a comparison of the results from my original script done in Unity, and the current results in Godot.
Any help would be greatly appreciated.

Here is the updated code:

func generate_voronoi_texture(polygon: Polygon2D):

var voronoi_texture = Image.create(int(texture_dimensions.x), int(texture_dimensions.y), false, Image.FORMAT_RGBA8)
print ("texture dimensions: ", texture_dimensions)

var points: Array[Vector2] =
var new_voronoi_regions = randomize_in_range(voronoi_regions, region_min_bias, region_max_bias)
for i in range(new_voronoi_regions):
points.append(Vector2(randi_range(0, int(texture_dimensions.x)), randi_range(0, int(texture_dimensions.y))))

for i in range(int(texture_dimensions.x)):
for j in range(int(texture_dimensions.y)):
var nearest_point_index = 0
var nearest_distance = INF

  	for k in range(points.size()):
  		var distance = points[k].distance_to(Vector2(i, j))
  		if distance < nearest_distance:
  			nearest_distance = distance
  			nearest_point_index = k

  	var color = Color.from_hsv(nearest_point_index / float(new_voronoi_regions), 1, 1)
  	voronoi_texture.set_pixel(i, j, color)

voronoi_texture.generate_mipmaps()

var texture = ImageTexture.create_from_image(voronoi_texture)
texture.create_from_image(voronoi_texture)
polygon.texture = texture

Can you also share the values you use for voronoi_regions, region_min_bias and region_max_bias?

Hi @njamster

Here are the var definitions:>

extends Node2D

@export var hex_radius: float = 50.0
@export var grid_width: int = 3
@export var grid_height: int = 3
@export var voronoi_regions: int = 5
@export var region_min_bias: int = 2
@export var region_max_bias: int = 5
@export var texture_dimensions: Vector2 = Vector2(100, 100)
@export var outline_color: Color = Color.BLACK
@export var show_outline: bool = true

var hexagons: Array[Node2D] =
var hex_mesh: Array[Vector2] =
var hex_uvs: Array[Vector2] =

The voronoi_regions divides each hexagon into that amount. The min_bias and max_bias apply a range for that amount.
For example, if the Voronoi regions are 5, with a min_bias of 2 and a max_bias of 3,
The number of regions can range between 3 and 8 (5-2 and 5+3).

I tried changing the randomization to floats instead of ints:>

for i in range(new_voronoi_regions):
points.append(Vector2(randf_range(0, texture_dimensions.x), randf_range(0, texture_dimensions.y)))

But it’s not impacting it.
I’m not sure how Godot splits a polygon2D into texture surfaces. I’m still new to it and not overly familiar with the nuances.

Below is the Unity script for reference:

private void GenerateVoronoiTexture(MeshRenderer renderer)
{

  // Create a new texture object
  Texture2D voronoiTexture = new Texture2D(textureDimensions.x, textureDimensions.y);
  voronoiTexture.filterMode = FilterMode.Bilinear;

  // Create a list of randomly placed points
  List<Vector2> points = new List<Vector2>();

  //! create randomization of Voronoi Regions
  int newVoronoiRegions = RandomizeInRange(voronoiRegions, regionMinBias, regionMaxBias);

  for (int i = 0; i < newVoronoiRegions; i++)
  {
  	//! set range based on textureDimensions
  	points.Add(new Vector2(Random.Range(0, textureDimensions.x), Random.Range(0, textureDimensions.y)));
  }

  for (int i = 0; i < voronoiTexture.width; i++)
  {
  	for (int j = 0; j < voronoiTexture.height; j++)
  	{
  		int nearestPointIndex = 0;
  		float nearestDistance = float.MaxValue;

  		for (int k = 0; k < points.Count; k++)
  		{
  			float distance = Vector2.Distance(points[k], new Vector2(i, j));

  			if (distance < nearestDistance)
  			{
  				nearestDistance = distance;
  				nearestPointIndex = k;
  			}
  		}

  		Color color = Color.HSVToRGB(nearestPointIndex / (float)newVoronoiRegions, 1, 1);
  		voronoiTexture.SetPixel(i, j, color);
  	}
  }

  voronoiTexture.Apply();

  renderer.material.mainTexture = voronoiTexture;

}

So each of those hexagons from your screenshots is a polygon and gets passed to your generate_voronoi_texture function? If so, the only reason I can see why yours only would have one color, is that all the points of your polygon are outside of the texture_dimensions (i.e. in the same voronoi region). Here’s what I did:

extends Node

@export var texture_dimensions: Vector2i = Vector2(100, 100)

@export var voronoi_regions := 5
@export var region_min_bias := 2
@export var region_max_bias := 3


func _ready() -> void:
	var hexagon := Polygon2D.new()
	hexagon.polygon = [
		Vector2(0, 50),
		Vector2(25, 0),
		Vector2(75, 0),
		Vector2(100, 50),
		Vector2(75, 100),
		Vector2(25, 100)
	]
	add_child(hexagon)

	generate_voronoi_texture(hexagon)


func generate_voronoi_texture(polygon: Polygon2D) -> void:
	var voronoi_texture = Image.create(texture_dimensions.x, texture_dimensions.y, false, Image.FORMAT_RGBA8)

	var points: Array[Vector2] = []
	var new_voronoi_regions := randomize_in_range(voronoi_regions, region_min_bias, region_max_bias)

	for i in range(new_voronoi_regions):
		points.append(Vector2(randi_range(0, texture_dimensions.x), randi_range(0, texture_dimensions.y)))

	for i in range(texture_dimensions.x):
		for j in range(texture_dimensions.y):
			var nearest_point_index: int = 0
			var nearest_distance: float = INF

			for k in range(points.size()):
				var distance: float = points[k].distance_to(Vector2(i, j))
				if distance < nearest_distance:
					nearest_distance = distance
					nearest_point_index = k

			var color := Color.from_hsv(nearest_point_index / float(new_voronoi_regions), 1, 1)
			voronoi_texture.set_pixel(i, j, color)

	voronoi_texture.generate_mipmaps()

	polygon.texture = ImageTexture.create_from_image(voronoi_texture)


func randomize_in_range(a: int, b: int, c: int) -> int:
	var r = range(a-b, a+c)
	return r[randi() % r.size()]

If you want to move the polygon away from the top left corner, change its position, not its polygon! Also, note that the center points for your regions may still land outside your hexagon’s bounds, since your picking them from a rectangular region, not a hexagonal one.

Figured it out!
The textures needed to be normalized, so they would show properly on each hexagon.
Here’s the updated function:

func generate_voronoi_texture(polygon: Polygon2D, hex_position: Vector2) -> void:
	# Generates a Voronoi texture and applies it to the given polygon
	var voronoi_image = Image.create(int(texture_dimensions.x), int(texture_dimensions.y), false, Image.FORMAT_RGBA8)

	var points: Array[Vector2] = []
	var new_voronoi_regions := randomize_in_range(voronoi_regions, region_min_bias, region_max_bias)

	# Randomly generate points for the Voronoi diagram
	for i in range(new_voronoi_regions):
		points.append(Vector2(randf_range(0, texture_dimensions.x), randf_range(0, texture_dimensions.y)))

	# Fill the image with colors based on the nearest Voronoi point
	for i in range(texture_dimensions.x):
		for j in range(texture_dimensions.y):
			var nearest_point_index: int = 0
			var nearest_distance: float = INF

			# Find the nearest Voronoi point for each pixel
			for k in range(points.size()):
				var distance: float = points[k].distance_to(Vector2(i, j))
				if distance < nearest_distance:
					nearest_distance = distance
					nearest_point_index = k

			# Set the pixel color based on the index of the nearest point
			var color := Color.from_hsv(nearest_point_index / float(new_voronoi_regions), 1, 1)
			voronoi_image.set_pixel(i, j, color)

	voronoi_image.generate_mipmaps()  # Generate mipmaps for the texture
	var texture = ImageTexture.create_from_image(voronoi_image)  # Create a texture from the image
	polygon.texture = texture  # Apply the texture to the polygon

	# Calculate UVs based on the hexagon's position and mesh coordinates
	var uvs = []
	for vertex in hex_mesh:
		# Normalize the UVs to be in the range [0, 1]
		var uv = (vertex + Vector2(hex_radius, hex_radius)) / (2 * hex_radius)
		uvs.append(uv * texture_dimensions)
	polygon.uv = uvs
1 Like