Gridmaps and MeshLibrary optimization

Godot Version

Godot 4.2.2

Question

Hi everyone, I’m Gatto and I’ll explain my problem…
I will use GridMaps to create my lowpoly levels.
I would like to set up a MeshLibrary with simple shapes and change the material to them separately.

Let me explain better with an example:
in the MeshLibrary I have a cube, a cylinder and a pyramid all white. I would like to place a cube and give it the red material, a cube with blue material and a cube with yellow material acting outside the MeshLibrary, so as to have only one cube in the MeshLibrary.

This is because having around 50 shapes and over 40 materials I would have to insert over 2000 objects into MeshLibrary… I hope there is another way, help thanks XD

I know this is an old post, but maybe I can help someone else having the same problem: I’m no expert but I think the best solution really comes down to WHY you’re trying to have multiple identical shapes with different colors.

One possible way to achieve what you want could be to have one of each mesh in the MeshLibrary (like you want), and then have each of those meshes reference a shader that in turn references a Texture3D that is equivalent to the size of your Gridmap. When you or a script places down a cell at a given position in the grid, you could have code that colors the pixel at that same position in the Texture3D to the desired color that you want that cell to be, and then that cell (which is using the shader that references that texture3D) would have its color match the placed Texture3D pixel.

If you want the Gridmap to have each cell placed by you in the editor, then you could have a “tool” script that generates/updates the Texture3D every time you place a cell. That “tool” script could have a “setCellColor” function where you can change the Texture3D pixel (and by extension, the Cell that references that Texture3D for its color) at the clicked position.

If you want the Gridmap to be generated by a script, then the process would generally be the same, except you would just have the script handle the pixel color at each position in the Texture3D by itself.

I have a similar process in place for handling “cell visibility” for each unit for my XCOM Enemy Unknown/XCOM 2-inspired, grid-based, turn-based strategy game project (where every cell in the grid is placed via code, and not placed in the editor), and this is roughly what I did:

To create a Texture3D, you first need to have an array of images (Array[Image]) which comprise each vertical “slice” of the Texture3D “image”. I store my Array of Images as a global variable called “visibilityGrid”. I use the following code to generate my “visibilityGrid” Array[Image], called in my grid script’s “func _ready()”:

func GenerateVisibilityGrid() -> void:
	visibilityGrid.clear() #We only ever call this script once, but we clear it just in case we want to call it again later
    #"gridSize" is a global Vector3i variable I use to set the max size of the grid in all 3 dimensions
    for z in range(gridSize.z):
		#L8 is a single 8-bit depth representing luminance. More performant
		var imgSlice: Image = Image.create_empty(gridSize.x, gridSize.y, false, Image.FORMAT_L8)
		visibilityGrid.append(imgSlice)
    #We create the Texture3D, and then we assign the visibilityGrid "Array[Image]" to it
	var newTex3D: ImageTexture3D = ImageTexture3D.new()
	newTex3D.create(Image.FORMAT_L8, gridSize.x, gridSize.y, gridSize.z, false, visibilityGrid)
	#We assign our newly minted texture3D to our global "texture3D" variable 
    tex3D = newTex3D
	fog.material.density_texture = tex3D

Next, in “func _ready()”, I set the shader parameters for each cell in the Gridmap MeshLibrary. Currently I’m using cubes that have just a single color in their material slot, and when my shader is assigned to the cube mesh I don’t want them to lose that color. So, when I loop over each cell in the MeshLibrary, I grab whatever color I assigned to it (“meshItem.surface_get_material(0).albedo_color”), and then plug that color value into my shader’s “color” shader parameter after I replace the mesh’s material with my shader. I also plug in the global texture3D that was generated, as well as the global “gridSize” so that the shader can scale the texture properly when shading cells:

for cellIdx in gridMap.mesh_library.get_item_list():
	var meshItem: Mesh = gridMap.mesh_library.get_item_mesh(cellIdx)
	var color: Color = meshItem.surface_get_material(0).albedo_color
	meshItem.surface_set_material(0, ShaderMaterial.new())
	var material: Material = meshItem.surface_get_material(0)
	material.shader = load("res://Assets/Shaders/cellShader.gdshader")
	material.set("shader_parameter/tex3_D", tex3D)
	material.set("shader_parameter/dimensions", gridSize)
	material.set("shader_parameter/color", color)

The shader I use looks like what you see below. In my case, the output of the shader at each cell position is just a mix of the color I want the cell to have, and the pixel color at the position in the texture3D that correlates to the position the cell occupies:

shader_type spatial;

uniform vec4 color: source_color;
uniform vec3 dimensions;
uniform sampler3D tex3_D : filter_nearest;
varying vec3 worldPos;

void vertex() {
	worldPos = NODE_POSITION_WORLD+ vec3(0.5);
}

void fragment() {
	vec3 tex_coord = worldPos / dimensions;
	float blend = texture(tex3_D, tex_coord).r; // Grayscale value from Texture3D

	// Convert base color to grayscale using luminance weights
	float luminance = dot(color.rgb, vec3(0.299, 0.587, 0.114));
	vec3 desaturated = mix(color.rgb, vec3(luminance), 0.5);
	// Interpolate between desaturated and saturated. We only use HALF the interpolation value, otherwise the desaturation is too extreme
	vec3 final_color = desaturated * mix(1.0, 0.5, blend);
	ALBEDO = final_color;
}

If you want the cell color at a given position to be EXACTLY the same as the pixel color in the texture3D at the same position, your shader code would be a lot simpler, something like:

uniform vec3 dimensions;
uniform sampler3D tex3_D : filter_nearest;
varying vec3 worldPos;

void vertex() {
	worldPos = NODE_POSITION_WORLD+ vec3(0.5);
}

void fragment() {
	vec3 tex_coord = worldPos / dimensions;
	ALBEDO = texture(tex3_D, tex_coord).rgb
}

The cool thing about this approach in my case is that “FogMaterial” already uses Texture3Ds for determining fog density at different positions in world space. In my case, I simply plug in the same global “tex3D” into my single world “Fog” object in “func _ready()”, and the fog simply matches the desaturation effect that the Texture3D drives for my cells that are “out of view” of each of my units in my tactics game. All I need to do is to make the size of the fog object match my “gridSize” variable and center the fog object over the GridMap in “func _ready()”.

When you want to change the color of a pixel in the Texture3D for a given cell position, just run:

visibilityGrid[cell.position.z].set_pixel(cell.position.x, cell.position.y, Color(<insert color here>))

And then, run:

tex3D.update(visibilityGrid)

And true to the function name, the Texture3D will update to match the changes in the Array[Image] image array.

In my case, the color is just going to be some value between black and white, where black (all zeroes) correlates to low “fog” density (higher visibility on that cell), and white (all ones) correlates to high “fog” density (lower visibility on that cell).