Applying Shaders to Bitmap

Godot Version

4.7

Problem

I am trying to create a map game similar to those made by Paradox as a fun side project. Currently, I am trying to create the map for the game. I have made a png with two countries divided into provinces. Each province has its own unique hex color as a means of identification; however, I want all of the provinces for a country to be of the same color which I am having trouble implementing.

My Work So Far

Here is the map, which I have as a texture, without any shaders:

I initially made a shader material that would highlight whatever province was clicked white. It looks like this:

This all worked fine. here are the two relevant bits of code for applying this shader

//This is the shader file itself, not sure if I selected the right coding language for this part of the post
shader_type canvas_item;



uniform vec4 province_highlight_color : source_color = vec4(1.0,1.0,1.0,1.0);

void fragment() {
	
	vec4 input_color = texture(TEXTURE, UV);
	if (abs(input_color.r- province_highlight_color.r) < 0.0001 
        && abs(input_color.g- province_highlight_color.g) < 0.0001 
        && abs(input_color.b - province_highlight_color.b) < 0.0001){
		COLOR = vec4(1.0,1.0,1.0,input_color.a);
	
	}
}
#and this is the function that handles the clicking of the province and subsequent application of the shader. Essentially what I am doing is taking the color of the pixel that is clicked passing its value to province_highlight_color then comparing it to all pixels in the texture. If the pixels are essentially the same color as the one clicked, the shader applies a white color over every matching pixel

func _unhandled_input(event):
	camera_controller.handle_zoom(event)
		
	if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
		var world_pos = camera.get_global_mouse_position()
		var local_pos = map_sprite.to_local(world_pos)
		
		var tex_size = map_sprite.texture.get_size()
		var pivot_offset = tex_size / 2 if map_sprite.centered else Vector2()
		
		var pixel_pos = local_pos + pivot_offset
		pixel_pos.x = clamp(pixel_pos.x, 0, tex_size.x - 1)
		pixel_pos.y = clamp(pixel_pos.y, 0, tex_size.y - 1)
		pixel_pos = Vector2i(pixel_pos)
		
		var img : Image = map_sprite.texture.get_image()
		var pixel_color : Color = img.get_pixelv(pixel_pos)
		var hex_color = "#" + pixel_color.to_html(false).to_upper()
		
		
		if hex_color in color_to_province: # This if statement is where the selecting of provinces happens
			province_name = color_to_province[hex_color]
			#this line of code below is what does the color comparison and highlighting
			map_sprite.material.set_shader_parameter("province_highlight_color", Color(pixel_color.r,pixel_color.g,pixel_color.b,1.0)) 
			print(Color(pixel_color.r,pixel_color.g,pixel_color.b,1.0))
			#map_coloring()
		else:
				print("unknown color: ", hex_color)

The next step was to make each country appear as a solid color. My first step was to create a class called Nation that has variables for the provinces, name, and color of a nation. There are three functions associated with this class. One to create all the nations, two at the moment, one to change the owner of the nation, haven’t used it yet so not relevant to this post, and one to return the owner of a given province. There is also an array of all the provinces. Here is all the code for all this (it’s in its own script called nation.gd which I preload into my main.gd):

extends Node

class Nation:
	var name : String
	var color : Color
	var provinces : Array = []	


#Nation management data
var nations : Dictionary = {}
var province_to_owner : Dictionary = {}

	
func create_nations():
	var bulgaria = Nation.new()
	bulgaria.name = "BULGARIA"
	bulgaria.color = Color(0,1,0)
	bulgaria.provinces = ["Videin","Lom","Orechow","Pleven","Ruse","Silistria","Dulovo","Dobrich","Cavarna","Tolbuchin","Berkovica","Vrasta","Sopot","Kazanlak","Tarnovo","Shumen","Sliven","Varna","Kyustendil","Pernik","Sofia","Pazardzhik","Stara Zagora","Plovdiv","Dimitrovgrad"]
	nations[bulgaria.name] = bulgaria
	
	
	var serbia = Nation.new()
	serbia.name = "Serbia"
	serbia.color = Color(0,0,1)
	serbia.provinces = ["Salbac","Valjevo","Beograd","Pozega","Kraljevo","Arandelovae","Panavo","Pozarevac","Velico Gradiste","Petrovac","Turnu Severin","Cuprija","Boljevac","Zajecar","Jagodina","Knjazevac","Kursumlija","Medveda","Surdulica","Nis","Vlasotince","Pirot","Krusevac"]	
	nations[serbia.name] = serbia


	


func map_provinces_to_nations():
	for nation in nations.values():
		for province_name in nation.provinces:
			province_to_owner[province_name] = nation

func change_provine_owner(province_name:String, new_owner: Nation):
	var old_owner = province_to_owner.get(province_name,null)
	if old_owner:
		old_owner.provinces.erase(province_name)
	new_owner.provinces.append(province_name)
	province_to_owner[province_name] = new_owner


func get_province_owner(province_name: String) -> Nation:
	return province_to_owner.get(province_name,null)
# Called when the node enters the scene tree for the first time.
func _ready() -> void:
	pass # Replace with function body.


# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta: float) -> void:
	pass



var province_list : Array = [

It is at this point that I am stuck. I am currently trying to use the same shader file to do both the highlighting and the normal shading of the map. I am able change the color of all the provinces and highlight the province I click on. The issue is, I can not color the two countries, Bulgaria and Serbia, properly.

I have tried several different things, though I do not have all the code since I have changed it so much, and none of them work. The two outcomes I seem to get is that either only one province gets colored properly, or every province gets the exact same color as seen here( one province is highlighted which it is supposed to be):

The current shader file looks like this:

//the province array has the color of all of the individual provinces I am using this array in a for loop to compare each province color to the overall texture in the same way I did the highlight
shader_type canvas_item;
const vec4 province_color_array[48] = {
	vec4(0.0157, 0.2667, 0.0627, 1.0),
	vec4(0.0157, 0.2667, 0.1255, 1.0),
	vec4(0.0157, 0.2667, 0.1882, 1.0),
	vec4(0.0157, 0.2667, 0.251, 1.0),
	vec4(0.0157, 0.2667, 0.3137, 1.0),
	vec4(0.0157, 0.2667, 0.3765, 1.0),
	vec4(0.0157, 0.2667, 0.4392, 1.0),
	vec4(0.0157, 0.2667, 0.502, 1.0),
	vec4(0.0157, 0.2667, 0.5647, 1.0),
	vec4(0.0157, 0.2667, 0.5843, 1.0),
	vec4(0.0157, 0.2706, 0.0, 1.0),
	vec4(0.0157, 0.2706, 0.0627, 1.0),
	vec4(0.0157, 0.2706, 0.1255, 1.0),
	vec4(0.0157, 0.2706, 0.1882, 1.0),
	vec4(0.0157, 0.2706, 0.251, 1.0),
	vec4(0.0157, 0.2706, 0.3137, 1.0),
	vec4(0.0157, 0.2706, 0.3765, 1.0),
	vec4(0.0157, 0.2706, 0.4392, 1.0),
	vec4(0.0157, 0.2706, 0.502, 1.0),
	vec4(0.0157, 0.2706, 0.5647, 1.0),
	vec4(0.0157, 0.2745, 0.0, 1.0),
	vec4(0.0157, 0.2745, 0.0627, 1.0),
	vec4(0.0157, 0.2745, 0.1255, 1.0),
	vec4(0.0157, 0.2745, 0.1882, 1.0),
	vec4(0.0157, 0.2745, 0.251, 1.0),
	vec4(0.502, 0.0, 0.0, 1.0),
	vec4(0.0078, 0.5686, 0.4392, 1.0),
	vec4(0.0078, 0.0353, 0.4392, 1.0),
	vec4(0.0078, 0.2078, 0.4392, 1.0),
	vec4(0.251, 0.0078, 0.4392, 1.0),
	vec4(0.4392, 0.0078, 0.4039, 1.0),
	vec4(0.4392, 0.0078, 0.3216, 1.0),
	vec4(0.4392, 0.0078, 0.2118, 1.0),
	vec4(0.149, 0.4392, 0.0078, 1.0),
	vec4(0.0392, 0.4392, 0.0078, 1.0),
	vec4(0.0078, 0.4392, 0.0196, 1.0),
	vec4(0.0078, 0.4392, 0.2, 1.0),
	vec4(0.0078, 0.4392, 0.2902, 1.0),
	vec4(0.4392, 0.0078, 0.1608, 1.0),
	vec4(0.2118, 0.4392, 0.0078, 1.0),
	vec4(0.3647, 0.4392, 0.0078, 1.0),
	vec4(0.0078, 0.4392, 0.1412, 1.0),
	vec4(0.0078, 0.4392, 0.4353, 1.0),
	vec4(0.4392, 0.0078, 0.0588, 1.0),
	vec4(0.4392, 0.1686, 0.0078, 1.0),
	vec4(0.4235, 0.4392, 0.0078, 1.0),
	vec4(0.4392, 0.3922, 0.0078, 1.0),
	vec4(0.4392, 0.2902, 0.0078, 1.0)

};


uniform vec4 province_highlight_color : source_color = vec4(1.0,1.0,1.0,1.0);
uniform vec4 nation_color : source_color = vec4(0.482, 0.157, 0.251, 1.0);
uniform vec4 nation_color_parser : source_color = vec4(0.0, 0.0, 0.0, 1.0);

void fragment() {
	
	vec4 input_color = texture(TEXTURE, UV);
	if (abs(input_color.r- province_highlight_color.r) < 0.0001 && abs(input_color.g- province_highlight_color.g) < 0.0001 && abs(input_color.b - province_highlight_color.b) < 0.0001){
		COLOR = vec4(1.0,1.0,1.0,input_color.a);
	}else {
		for(int i=0; i < 48;i++) {
			if(abs(input_color.r- province_color_array[i].r) < 0.0001 
			&& abs(input_color.g- province_color_array[i].g) < 0.0001 
			&& abs(input_color.b - province_color_array[i].b) < 0.0001
			){
				COLOR = nation_color;
			}
		}
	}
}

I believe the issue comes in that very last line applying nation_color. In my main file I am using a function called map_coloring( ) that looks like this:

func map_coloring() :
	for ins in range(nation_manager.province_list.size()):
		for job in color_to_province:
			if nation_manager.province_list[ins] == color_to_province[job]:
				for key in nation_manager.nations:
					if nation_manager.get_province_owner(job) == nation_manager.nations.get(ins,null) :
						map_sprite.material.set_shader_parameter("nation_color", nation_manager.nations.get(key,null).color) 

This function has three loops. The first loop goes through every province in the list of all provinces.

The second loop goes through each province and its matching color as defined early in this thread. It then compares each entry in the dictionary to the current entry in the province array and only moves on to the final loop when the two match (which they should only do once per iteration of the outermost loop)

The third loop goes through two iterations at the moment and is where the potential solution may lie. This third loop goes through all of the nations in the game, currently just bulgaria and serbia, and sees if the current province we are looking at is owned by one or the other and then assigning the proper nation color to the shader parameter. (bulgaria should be green and serbia should be blue.

After all this is done and the program is run, the result is everything is blue, as seen above, which leads me to believe the loops are going all the way through as serbia is the second country in nations and is the color that shows up.

I am assuming that the issue is that once nation_color changes, all of the provinces that were green are then turned blue.

Is there any way I can prevent this from happening and keep using the shader material I have setup, or will I have to switch up tactics? If the latter is the way, what would you recommend?

Thanks

bump once

That is a lot of loops, but only the last value matters. set_shader_parameter does not immediately start drawing some certain section of the image, it only stores data for the next frame, you can only have one nation_color at a time.

since void fragment goes over every pixel in your texture, and this for loop goes over every nation color, why wouldn’t it color every nation by nation_color every time?

I think you need more conditions as part of this shader, maybe a uniform array to represent which color belongs to which nation. But you do already have a fair amount of hard coded shader data.


Not to throw a huge wrench in it but maybe using a UV-coordinate style system would be easier to manage? Instead of coloring every province and comparing colors directly what if you colored the provide as UV-coordinates to another colored texture. A pure black province would map to 0, 0 on a DrawableTexture2D, a red province to 1, 0 white to 1, 1 and anything in-between.

You could keep pure-black special as a sentinel value so your GDScript can detect clicking out of bounds if hex_color == "#000000":

When claiming a province and updating it’s visuals you can use the DrawableTexture2D to blit the country’s color as a rectangle, by converting a province’s index to a location on the drawable texture that matches the UV-map image.

This is a good video on using UV maps for 2D pixel art, I think it could be a good trick for your project too.

Assuming you want to keep the single image approach - encode nation in the R channel and province in GB channels. You can then hardcode both into the shader and branch accordingly, or use that as a lookup into “palette” texture as @gertkeno already suggested.

Writing data to the map image would be much more difficult though, you’d have to read pixel data comparing the GB channels to a table of provinces, and re-assign the R value for the new nation for every pixel. And all on the CPU which is much slower and much more pixel data to read, the map needs to be high quality while the province-pallet could be a small 16x16 texture to support 255 provinces (though colors may bleed, 64 provinces to be safe).

I meant to just do it in the shader, with a premade bitmap similar to the one that is currently used, only that it also encodes the nation in addition to encoding the province.

This would only require one additional if in the shader and some more hardcoding. It’s minimal intervention in respect to the current setup, which admittedly is not ideal.

And sure, using a LUT texture would be preferable to hardcoding and if-ing.

My understanding is that the provinces change nation over the course of the game, you cannot pre-encode the map with nation data since it will update. If I’m wrong, totally do use color channels for specifics like nation vs province, that would wrap this up nicely.

Right. If provinces can jump nations, then your suggestion would be optimal.

Thank you for the response! I think the idea you provided is interesting, however, it seems like I would need to make a lot of underlying changes. I was able to find a solution that worked for me.

The first thing I did was make a uniform vec4 array: uniform vec4 province_owner_color_array[48] inside of the shader file

I then made another array in my main file called province_owner_color_array_reference

By doing this I no longer have the issue of nation_color only being able to hold one value and thus making everything the same color.

I then changed my map_coloring function to the following:

func map_coloring() :
for ins in range(nation_manager.province_list.size()):
for job in color_to_province:
if nation_manager.province_list[ins] == color_to_province[job]:
for low in range(nation_manager.nations_array.size()):
if nation_manager.get_province_owner(color_to_province[job]) == nation_manager.nations_array[low] :
province_owner_color_array_reference[ins] = nation_manager.nations.get(nation_manager.nations_array[low],null).color
map_sprite.material.set_shader_parameter(“province_owner_color_array”, province_owner_color_array_reference)

This function assigns all of the desired colors to the reference array and then copies all of the values from the reference to the actual shader array which is then passed through essentially the same for loop as before in the shader file, but now it colors everything properly. I can upload my code if that would make more sense than this explanation.