• Hey! Guest! The 39th GMC Jam will take place between November 26th, 12:00 UTC and November 30th, 12:00 UTC. Why not join in! Click here to find out more!

Shaders Does shader_set_uniform cause texture swaps? - Writing a recolour shader

Hello!
The game I'm working on is going to require lots of recoloring. I am wondering:
Does shader_set_uniform cause texture swaps or lag?
I am hoping to set the shader at the draw begin event, then when everything that needs recoloring draws itself it will pass its pallate id into the shader.
Thanks!
 
It will break the vertex batch.

What you could do is put all your pallets into one texture, and then encode the palette to use into the image_blend or image_alpha variables, which will be put into the in_Colour vertex attribute. Note: RGBA will be 1 byte each.
 
It will break the vertex batch.

What you could do is put all your pallets into one texture, and then encode the palette to use into the image_blend or image_alpha variables, which will be put into the in_Colour vertex attribute. Note: RGBA will be 1 byte each.
Ahh ok. Yeah i thought uniforms might be slow. Good ideas to use image_alpha and blend.
You see, I need to recolor i items in my game. They can be made any combination of 3 materials and each material has a 3x1 sprite of its colours. So I need to put the 3 coordinate/sprite index of those maps into the shader to replace the 9 colours of the base sprite with. But this might need to be done with potentially every item on screen with different materials on each!
Is there any more variables that are passed into a shader i can use as material colour map indexes?
 

Sybok

Member
Yeah, setting shader uniforms is generally a relativery slow process, in the scheme of things.
 
Hmm ok then. So what are my options as far as recolouring most objects on screen with different palletes? Dynamically preferably
 

vdweller

Member
You can encode additional info in the vertex position or even the draw alpha/draw tint color. You can extract that info in the vertex/fragment shader.

How many bits of info do you need?
 
You can encode additional info in the vertex position or even the draw alpha/draw tint color. You can extract that info in the vertex/fragment shader.

How many bits of info do you need?
Hi vdweller. I'm basically using your real-time recolour shader but modified a little bit. (Thanks for that great tutorial btw:))
Each sprite I recolour will need the id (y position of colour map from a sprite containing a colour map on each line) of 3 materials. So i will need to input 3 values.
My shader knowledge ends will your tutorial basically. I know a tiny bit about frag shaders but nothing about vertex shaders sorry.
At the moment, rather than run the shader every draw event I'm saving the recolured sprites to surfaces (which cause texture swaps)
 
I wondering. If I saved all the surfaces using sprite_create_from_surface during runtime when a new combination of materials is made, is there a way to avoid batch breaks using these dynamic sprites?
 

Mert

Member
If "texture swaps" is the matter of issue for you, you can throw uniform arrays and matrices as well(Which you can then use for different purposes in your shaders)
 
These 3 values, how many bits (or bytes) are they? Like, what is their numerical range?
They could be any number really but always integers. I'd say way less than 100 max. They represent the id of the material so depends how many materials I add to the game.
 
If "texture swaps" is the matter of issue for you, you can throw uniform arrays and matrices as well(Which you can then use for different purposes in your shaders)
The reason I'm wondering if theres a better way to recolour many sprites is because at the moment I'm using surfaces which, when i could have hundred, each cause a batch break/texture swap. So it will probably be really bad on performance.
Never heard of uniform "arrays" or matrices. I'm a real begginer at shaders though 😬😀
 

GMWolf

aka fel666
Assuming you want to use the existing vertex batch system (rather than writing your own), you could use image_blend to pass in each of your 3 values as the r, g and b components of the colour.

Then in the shader you can get the vertex colour and use it however you like.

A common pattern is to have a uniform array of values, and have one of the vertex attributes (image blend for example) index into that array. This is useful when multiple objects reuse the same values.
 
Assuming you want to use the existing vertex batch system (rather than writing your own), you could use image_blend to pass in each of your 3 values as the r, g and b components of the colour.

Then in the shader you can get the vertex colour and use it however you like.

A common pattern is to have a uniform array of values, and have one of the vertex attributes (image blend for example) index into that array. This is useful when multiple objects reuse the same values.
Hmm ok. I understand that first bit. So I can set image_blend rgb to the 3 ids of my materials which the shader can use as the y position on the colour map.
I don't really know how to write my own shaders but i understand the logic.
The next bit about vertex arrays... sorry i don't quite understand that🙂
 

GMWolf

aka fel666
The next bit about vertex arrays... sorry i don't quite understa
The next bit is kinda an extension of the first bit.
A uniform can hold a single value, but it can also hold an array of values.

So if what you want to store doesn't fit in an existing vertex attribute (colour, UV, etc) then you can store your values in the uniform arrays. Then you use the colour attribute to index into that array.
 
The next bit is kinda an extension of the first bit.
A uniform can hold a single value, but it can also hold an array of values.

So if what you want to store doesn't fit in an existing vertex attribute (colour, UV, etc) then you can store your values in the uniform arrays. Then you use the colour attribute to index into that array.
Ok kinda get it a bit now. I don't think that will be necessary because my 3 inputs will never go over 255 (i think thats the colour limit?).
How would I access the rgb of the current image_blend inside a shader?
Would that method mean after setting the shader, drawing with draw_sprite_ext or draw_set_colour? Do they cause batch breaks anyway?
Thanks for everyones ideas so far! Really appreciate it😁
 

GMWolf

aka fel666
Would that method mean after setting the shader, drawing with draw_sprite_ext or draw_set_colour? Do they cause batch breaks anyway?
Yes, you would need one of these functions.
Afaik it doesn't break the batch (shouldn't need to).

How would I access the rgb of the current image_blend inside a shader?
An extra vertex attribute input in the vertex shader. The default shader already has the code.for position and texcoords, you should be able to copy it from the colour too. You can then pass it to fragment shader like any other varying.
You need to put it in the right position and give it the right name for GM to bind it correctly. Honestly I don't remember what name it should be. Hopefully someone here knows!
 

vdweller

Member
If you use draw_sprite_ext for instance, you can use the color (tint) parameter to pass that info. You want 3 values, so any value from 0-255 can be combined to make a color. Example: make_color_rgb(val1,val2,val3);

Use that parameter (v_vColour in the shader) to extract your values from it.

Example. make_color_rgb(4,96,17);

v_vColour.r will be 4/255. Multiply this with 255 to go back to 4. Or don't. Just convert it back to something meaningful for your code, like a sampling coordinate. Precision shouldn't be an issue for so small numbers.

If you're clever you can even hide extra info on the vertex position, as long your sprite isn't drawn in very high coords > 2048.

Remember, a 24-bit color value is just a number. Vertex coords are 24-bit floats IIRC. If you only need, say, 12 bits for the position, you can break the 24-bit color value in 2 sets of 12 bits and store the first 12 bits in the X position and the other 12 bits in the Y position. I have actually done this with a button shader and it works as expected. This really goes into bit handling and I strongly suggest you take a look at this stuff at some point.

EDIT: draw_set_color breaks the batch afaik. draw_sprite_ext doesn't. But double-check this!
 
Last edited:

GMWolf

aka fel666
If you're clever you can even hide extra info on the vertex position, as long your sprite isn't drawn in very high coords > 2048.
I would avoid this since you don't know what type is used for positions. And it could change per platform. It could be 32bit just as easily as it could be 16bit floats. (Although, it's probably 32 bits).
draw_set_color
Really? I was under the impression that draw_set_color + draw_* was the same as draw_"_ext. Is it instead used as a uniform?
 
If you use draw_sprite_ext for instance, you can use the color (tint) parameter to pass that info. You want 3 values, so any value from 0-255 can be combined to make a color. Example: make_color_rgb(val1,val2,val3);

Use that parameter (v_vColour in the shader) to extract your values from it.

Example. make_color_rgb(4,96,17);

v_vColour.r will be 4/255. Multiply this with 255 to go back to 4. Or don't. Just convert it back to something meaningful for your code, like a sampling coordinate. Precision shouldn't be an issue for so small numbers.

If you're clever you can even hide extra info on the vertex position, as long your sprite isn't drawn in very high coords > 2048.

Remember, a 24-bit color value is just a number. Vertex coords are 24-bit floats IIRC. If you only need, say, 12 bits for the position, you can break the 24-bit color value in 2 sets of 12 bits and store the first 12 bits in the X position and the other 12 bits in the Y position. I have actually done this with a button shader and it works as expected. This really goes into bit handling and I strongly suggest you take a look at this stuff at some point.

EDIT: draw_set_color breaks the batch afaik. draw_sprite_ext doesn't. But double-check this!
Wow thanks guys for all these tips. Will definately check them out once I get back to my pc.
Yes I've heard that draw_sprite_ext doesn't batch break but draw_set_colour does.
Thanks I'll let you know how I go!😁
 
So I've given it a go and attempted to write my first completely custom shader. It doesn't compile. I have no idea why. :)
Anyway here is what I'm trying to to based on your suggestions:
12cd3900-e798-4ad1-8fff-34600df97a80.png
^ this little guys is a 3x3 sprite which I want to use as my colour map, similar to vdwellers tutorial but with height. Each y position (0-2) represents one of the 3 materials that an item can be crafted from.
c00f0b08-50bf-46fb-bf9e-3ff9a32344bb.png
^ This is my pre-recoloured item sprite. It has the same colours as the map but doesn't necessarily have all of them.
0eb3e3e6-2206-4f32-bfce-b18c993e9d35.png
^ This is my material colour maps sprite. Each y position is the colour map for a certain material.
I've never written my own shader before so this is purely based on other shaders I've seen and what I think each of the functions do. That's why it doesn't work (obviously;))
Here is my shader code (please excuse the noob code:p):
Code:
//
// Recolour shader
//
varying vec2 v_vTexcoord;
varying vec4 v_vColour;

uniform sampler2D texColmap; //the base colour map
uniform sampler2D materialMap; //the color maps for materials

void main()
{
    //variables
    vec4 output;
    vec4 sprite;
    sprite = texture2D(gm_BaseTexture,v_vTexcoord); //the sprite currently being drawn
    //finding match
    for (int yy=0;yy<3;yy++) //loop thru materials
        {
        for (int xx=0;xx<3;xx++) //loop thru material shades
            {
            if (vec3(sprite)==vec3(texture2D(texColmap,vec2(xx,yy))) //if map matches current pixel
                {
                if (yy==0) //matched first material
                    {
                    output=vec4(texture2D(materialMap,vec2(xx,v_vColour.r)));
                    break;
                    }
                else if (yy==1) //matched second material
                    {
                    output=vec4(texture2D(materialMap,vec2(xx,v_vColour.g)));
                    break;
                    }
                else if (yy==2) //matched third material
                    {
                    output=vec4(texture2D(materialMap,vec2(xx,v_vColour.b)));
                    break;
                    }
                }
            }
        }   
    //output
    gl_FragColor = output;
}
And heres my draw event in an otherwise empty obj_item:
GML:
shader_set(shader0);
texture_set_stage(shader_get_sampler_index(shader0,"texColmap"),sprite_get_texture(spr_colmap,0));
texture_set_stage(shader_get_sampler_index(shader0,"materialMap"),sprite_get_texture(spr_matmap,0));
draw_sprite_ext(sprite_index,image_index,x,y,image_xscale,image_yscale,image_angle,make_color_rgb(2,4,1),image_alpha);
shader_reset();
My logic behind it is: I try to find the pixel on the colourmap that matches the pixel being looked at/drawn. If it matches, set the output colour to the current x position and v_vColour's r,g or b value,(using this to insert my material map y positions), depending on what yy is in the loop, on the material map sprite.

I would GREATLY appreciate if anyone with a knowledge of shaders is able to help me out on this one.
Thanks in advance!:)
 

basementApe

Member
If you rename 'output' to 'outpt' or something then you get rid of the 'Fragment Shader: shader13 at line 14 : 'output'' error at least. Maybe 'output' is reserved for something else internally? In any case, renaming it works for me.

The next error that comes up is for your nested if-statement, inside the second for-loop. I've never seen if-statements nested like that inside a shader before so I have a feeling that's something you can't do. I've also read that if-statements in shaders are costly so they're something you want to avoid whenever you can.

I'm a beginner myself when it comes to shaders so kinda stabbing in the dark here.
 

HalRiyami

Member
I see a few issues with your shader code:
1. Like @basementApe said, change output into something else. I wouldn't use "input" and "output" since they are generally used internally.

2. You're passing integers in the uv coordinates of the samplers when they should be fractions of 1. "xx" and "yy" go from 0 to 2 but uv coordinates go from 0 to 1.
A 3 pixel texture has its first pixel occupying the 0 to 0.33 space so if you want to get the color of that first pixel I'd suggest getting if from the center of the pixel (0.33/2 in uv space). This way you can avoid blending issues the happen at the edges of pixels.

3. Texture2D returns 4 values but you're assigning it to a vec3 so that could also be causing an issue with the compiler.

4. You don't seem to be breaking the outer loop because I can only see 1 break. You need another check outside the inner loop and if true break again.

Below is another method you can use but this might be complicated if you're rotating and scaling sprites. I only suggest this because I don't like to use multiple samplers unless absolutely necessary.
Make your sprite twice as big (assuming you want to keep it a power of 2) and encode the color pallet into the first 4 pixel columns. When you draw, tell the shader to discard the first 4 columns (in uv space that would be 4pixels/sprite_width) and after that you can tell the shader to match the current pixel with the first column and swap it with the one from the material column. You only need to pass the material id in one color channel through the image_blend variable then retrieve it within the shader. The pic below should help illustrate what I'm trying to suggest.
Suggestion.png
 
If you rename 'output' to 'outpt' or something then you get rid of the 'Fragment Shader: shader13 at line 14 : 'output'' error at least. Maybe 'output' is reserved for something else internally? In any case, renaming it works for me.

The next error that comes up is for your nested if-statement, inside the second for-loop. I've never seen if-statements nested like that inside a shader before so I have a feeling that's something you can't do. I've also read that if-statements in shaders are costly so they're something you want to avoid whenever you can.

I'm a beginner myself when it comes to shaders so kinda stabbing in the dark here.
Ok thanks. I didn't realise there were other internal variable names I couldn't use.
Yeah I know if statements are slow in shaders so I'd prefer to do it more mathmatically but I don't think I can refer to a vector with a number as a position can I? Eg.
v_vcolour.1?
 
I see a few issues with your shader code:
1. Like @basementApe said, change output into something else. I wouldn't use "input" and "output" since they are generally used internally.

2. You're passing integers in the uv coordinates of the samplers when they should be fractions of 1. "xx" and "yy" go from 0 to 2 but uv coordinates go from 0 to 1.
A 3 pixel texture has its first pixel occupying the 0 to 0.33 space so if you want to get the color of that first pixel I'd suggest getting if from the center of the pixel (0.33/2 in uv space). This way you can avoid blending issues the happen at the edges of pixels.

3. Texture2D returns 4 values but you're assigning it to a vec3 so that could also be causing an issue with the compiler.

4. You don't seem to be breaking the outer loop because I can only see 1 break. You need another check outside the inner loop and if true break again.

Below is another method you can use but this might be complicated if you're rotating and scaling sprites. I only suggest this because I don't like to use multiple samplers unless absolutely necessary.
Make your sprite twice as big (assuming you want to keep it a power of 2) and encode the color pallet into the first 4 pixel columns. When you draw, tell the shader to discard the first 4 columns (in uv space that would be 4pixels/sprite_width) and after that you can tell the shader to match the current pixel with the first column and swap it with the one from the material column. You only need to pass the material id in one color channel through the image_blend variable then retrieve it within the shader. The pic below should help illustrate what I'm trying to suggest.
View attachment 34522
Wow thanks for the help man! Appreciate the effort of making a diagram.
Ok. so:
1. Yep done that and it gets past that line when compiling
2. Ok didn't realise that texture coodinates were more of a percentage thing. Is that what UV means? (Noob question haha)
3. Ok I'll fix that and change those to vec4s. Don't know why I didn't do that in the first place:D
4. Yeah I did think that. I'll add another check to break the other loop.
You method is interesting. However I'm not sure it would work in my case as the reason I am using all 3 colour channels is that my items can be recoloured with any combination of 3 materials. I am inputting the 3 material map y positions as the rgb of the draw colour. Thanks though.
I will try this out tomorrow and report back :D
 

Joe Ellis

Member
Hi, I just wanted to share some knowledge about this subject.

As soon as you set a different shader, or even set a new uniform, all draw_sprite calls get submitted into the current batch that's building up. So the aim is to group all the sprites\instances that use certain shaders and "materials", basically different uniform settings, so that they all get drawn in one go while that shader and uniform setup is in place.

If you want an example for effectively breaking them into batches, this is the kind of thing I do in my engine. It's 3d so it uses vertex buffer submits rather than draw_sprite, but the same thing applies to both.

The standard render script\function that's used for drawing a batch of instances that share the same material is this:

GML:
function render_material(shader, materials, instances) {

    var
    i = 0, mtl, ins_list, j, ins,
    global_uniform_ids = shader.global_uniform_ids,
    global_values = shader.global_values,
    material_uniform_ids = shader.material_uniform_ids,
    num_material_uniforms = shader.num_material_uniforms;

    shader_set(shader.id)

    repeat shader.num_global_uniforms
    {shader_set_uniform_f_array(global_uniform_ids[i], global_values[++i])}
    i = 0
    repeat materials[0]
    {
    mtl = materials[++i]
    ins_list = instances[i]
    j = 0
    repeat num_material_uniforms
    {shader_set_uniform_f_array(material_uniform_ids[j], mtl[++j])}
    j = 0
    repeat ins_list[0]
    {
    ins = ins_list[++j]
    matrix_set(matrix_world, ins[0].matrix)
    vertex_submit(ins[1], pr_trianglelist, ins[2])
    }
    }
}

So this way, everything is done the minimal amount. Global uniforms like the sun direction and color are only set once per shader, and potentially hundreds of instances that all use the same material get rendered just after the material's uniforms have been set.

The actual way that it builds these batches is pretty complicated, so this kind of thing will take a while to set up and learn\work out how it works. But I think this is one of the most efficient ways of doing this kind of thing.

Here is the script for building the batches:

GML:
function update_render() {

    global.flag_render = false

    var instances = global.instances, i = 0, ins, materials, textures, j, mtl, shd,
    list_shaders, num_shaders = 0, list_materials, list_instances,
    shd_materials, material_instances,
    num_skeletal = 0, instances_skeletal,
    num_static = 0, instances_static, batch,
    num_instances = global.instances[0],
    editor = global.editor_id != 0;

    i = num_instances + 1
    repeat num_instances
    {
    ins = instances[--i]

    if instance_exists(ins)
    && (ins.visible3d || editor)
    && ins.model != 0
    {
    materials = ins.materials
    j = 0
    repeat materials[0]
    {
    mtl = materials[++j]
    shd = mtl.shader
    if !mtl.material_index
    {
    if !shd.shader_index
    {
    list_shaders[++num_shaders] = shd
    shd.shader_index = num_shaders
    list_materials[num_shaders] = [0]
    list_instances[num_shaders] = [0]
    }
    shd_materials = list_materials[shd.shader_index]
    shd_materials[@ ++shd_materials[@ 0]] = mtl
    mtl.material_index = shd_materials[0]
    material_instances = list_instances[shd.shader_index]
    material_instances[@ mtl.material_index] = [0]
    }
    else
    {material_instances = list_instances[shd.shader_index]}
    material_instances = material_instances[mtl.material_index]
    material_instances[@ ++material_instances[@ 0]] = [ins, ins.vbuffers[j], ins.textures[j].pointer]
    }

    if !ins.no_deferred
    {
    if ins.model.skeletal
    {instances_skeletal[++num_skeletal] = ins}
    else
    {instances_static[++num_static] = ins}
    }

    }
    }

    i = 0
    repeat num_shaders
    {
    list_shaders[++i].shader_index = 0
    materials = list_materials[i]
    j = 0
    repeat materials[0]
    {materials[++j].material_index = 0}
    }

    instances_static[0] = num_static
    list_instances[0] = num_shaders
    list_materials[0] = num_shaders
    list_shaders[0] = num_shaders

    global.batch_static_instances = instances_static
    global.batch_instances = list_instances
    global.batch_materials = list_materials
    global.batch_shaders = list_shaders
    global.batch_num_shaders = num_shaders
    global.batch_num_materials = num_shaders
    global.batch_num_instances = num_shaders
    global.batch_num_static = num_static

    if num_skeletal
    {
    instances_skeletal[0] = num_skeletal
    global.batch_skeletal_instances = instances_skeletal
    }
    else
    {global.batch_skeletal_instances = 0}

}

So you can see it's pretty complicated and all of the materials and shaders are set up previously with nodes\structs. But this script will not usually have to do much, it basically loops through all active instances and they will probably only be using 3 or 4 shaders between them, plus like 8 materials, so even though it looks pretty heavy, the loop actually just has to do this line once the shaders and materials have been registered: material_instances[@ ++material_instances[@ 0]] = [ins, ins.vbuffers[j], ins.textures[j].pointer]. Basically adding that instance\vertex buffer to that material in a 3 part array, then the render script just goes through these 1 at a time. This is the fastest method I've made so far after about 4 years of development and often trying to re think the procedure.
Anyway I thought I would share this cus if you are interested in going this far, I can provide a fully working system for it. I'm also trying to demonstrate how building a whole batching system is very complicated, and if you don't actually need it, as in the game can run at 60fps without it, I wouldn't go through all the trouble of doing it.
 
Last edited:
Hi, I just wanted to share some knowledge about this subject.

As soon as you set a different shader, or even set a new uniform, all draw_sprite calls get submitted into the current batch that's building up. So the aim is to group all the sprites\instances that use certain shaders and "materials", basically different uniform settings, so that they all get drawn in one go while that shader and uniform setup is in place.

If you want an example for effectively breaking them into batches, this is the kind of thing I do in my engine. It's 3d so it uses vertex buffer submits rather than draw_sprite, but the same thing applies to both.

The standard render script\function that's used for drawing a batch of instances that share the same material is this:

GML:
function render_material(shader, materials, instances) {

    var
    i = 0, mtl, ins_list, j, ins,
    global_uniform_ids = shader.global_uniform_ids,
    global_values = shader.global_values,
    material_uniform_ids = shader.material_uniform_ids,
    num_material_uniforms = shader.num_material_uniforms;

    shader_set(shader.id)

    repeat shader.num_global_uniforms
    {shader_set_uniform_f_array(global_uniform_ids[i], global_values[++i])}
    i = 0
    repeat materials[0]
    {
    mtl = materials[++i]
    ins_list = instances[i]
    j = 0
    repeat num_material_uniforms
    {shader_set_uniform_f_array(material_uniform_ids[j], mtl[++j])}
    j = 0
    repeat ins_list[0]
    {
    ins = ins_list[++j]
    matrix_set(matrix_world, ins[0].matrix)
    vertex_submit(ins[1], pr_trianglelist, ins[2])
    }
    }
}

So this way, everything is done the minimal amount. Global uniforms like the sun direction and color are only set once per shader, and potentially hundreds of instances that all use the same material get rendered just after the material's uniforms have been set.

The actual way that it builds these batches is pretty complicated, so this kind of thing will take a while to set up and learn\work out how it works. But I think this is one of the most efficient ways of doing this kind of thing.

Here is the script for building the batches:

GML:
function update_render() {

    global.flag_render = false

    var instances = global.instances, i = 0, ins, materials, textures, j, mtl, shd,
    list_shaders, num_shaders = 0, list_materials, list_instances,
    shd_materials, material_instances,
    num_skeletal = 0, instances_skeletal,
    num_static = 0, instances_static, batch,
    num_instances = global.instances[0],
    editor = global.editor_id != 0;

    i = num_instances + 1
    repeat num_instances
    {
    ins = instances[--i]

    if instance_exists(ins)
    && (ins.visible3d || editor)
    && ins.model != 0
    {
    materials = ins.materials
    j = 0
    repeat materials[0]
    {
    mtl = materials[++j]
    shd = mtl.shader
    if !mtl.material_index
    {
    if !shd.shader_index
    {
    list_shaders[++num_shaders] = shd
    shd.shader_index = num_shaders
    list_materials[num_shaders] = [0]
    list_instances[num_shaders] = [0]
    }
    shd_materials = list_materials[shd.shader_index]
    shd_materials[@ ++shd_materials[@ 0]] = mtl
    mtl.material_index = shd_materials[0]
    material_instances = list_instances[shd.shader_index]
    material_instances[@ mtl.material_index] = [0]
    }
    else
    {material_instances = list_instances[shd.shader_index]}
    material_instances = material_instances[mtl.material_index]
    material_instances[@ ++material_instances[@ 0]] = [ins, ins.vbuffers[j], ins.textures[j].pointer]
    }

    if !ins.no_deferred
    {
    if ins.model.skeletal
    {instances_skeletal[++num_skeletal] = ins}
    else
    {instances_static[++num_static] = ins}
    }

    }
    }

    i = 0
    repeat num_shaders
    {
    list_shaders[++i].shader_index = 0
    materials = list_materials[i]
    j = 0
    repeat materials[0]
    {materials[++j].material_index = 0}
    }

    instances_static[0] = num_static
    list_instances[0] = num_shaders
    list_materials[0] = num_shaders
    list_shaders[0] = num_shaders

    global.batch_static_instances = instances_static
    global.batch_instances = list_instances
    global.batch_materials = list_materials
    global.batch_shaders = list_shaders
    global.batch_num_shaders = num_shaders
    global.batch_num_materials = num_shaders
    global.batch_num_instances = num_shaders
    global.batch_num_static = num_static

    if num_skeletal
    {
    instances_skeletal[0] = num_skeletal
    global.batch_skeletal_instances = instances_skeletal
    }
    else
    {global.batch_skeletal_instances = 0}

}

So you can see it's pretty complicated and all of the materials and shaders are set up previously with nodes\structs. But this script will not usually have to do much, it basically loops through all active instances and they will probably only be using 3 or 4 shaders between them, plus like 8 materials, so even though it looks pretty heavy, the loop actually just has to do this line once the shaders and materials have been registered: material_instances[@ ++material_instances[@ 0]] = [ins, ins.vbuffers[j], ins.textures[j].pointer]. Basically adding that instance\vertex buffer to that material in a 3 part array, then the render script just goes through these 1 at a time. This is the fastest method I've made so far after about 4 years of development and often trying to re think the procedure.
Anyway I thought I would share this cus if you are interested in going this far, I can provide a fully working system for it. I'm also trying to demonstrate how building a whole batching system is very complicated, and if you don't actually need it, as in the game can run at 60fps without it, I wouldn't go through all the trouble of doing it.
Thanks for your insights. It does seem a bit complicated for what I'm trying to do but I appreciate your offer.
In my project there is no way of knowing what combination of 3 materials any given sprite will be until it is about to be drawn or even if any sprites need to be drawn with the same material combination in the first place. It could be (gold,silver,wood), (wood,wood,silver), (wood,iron,gold) etc... So I think the best method I can come up with is using the rgb channels of draw_sprite_ext to input the 3 materials into the shader.
Thanks for your ideas though I'll keep them in mind! Appreciate it
 
Ok so I've had a go at fixing my shader:
Code:
//
// Recolour shader
//
varying vec2 v_vTexcoord;
varying vec4 v_vColour;

uniform sampler2D texColmap; //the base colour map
uniform sampler2D materialMap; //the color maps for materials

void main()
{
    //variables
    vec4 outp;
    vec4 spr;
    bool done;
    spr = texture2D(gm_BaseTexture,v_vTexcoord); //the sprite currently being drawn
    done = false;
    //finding match
    for (int yy=0;yy<3;yy++) //loop thru materials
        {
        for (int xx=0;xx<3;xx++) //loop thru material shades
            {
            if (vec4(spr) == vec4(texture2D(texColmap,vec2(xx/3,yy/3)))) //if map matches current pixel
                {
                outp=vec4(texture2D(materialMap,vec2(xx/3,v_vColour.yy)));
                done == true;
                break;
                }
            }
        if (done == true)
            {
            break;   
            }
        }
    //output
    gl_FragColor = outp;
}
I have tried to fix those things you mentioned in your post and it compiles and runs now! :D
Unfortunately though it does not give me the result I want. When I run it, instead of drawing the sprite remapped to the new materials colours it just draw 1 colour in blue no matter what rgb I put in make_colour_rgb. Like this: (before and after)
Screenshot (8).png
The other colours seem to turn black.
Draw event code:
GML:
shader_set(shader1);
texture_set_stage(shader_get_sampler_index(shader1,"texColmap"),sprite_get_texture(spr_colmap,0));
texture_set_stage(shader_get_sampler_index(shader1,"materialMap"),sprite_get_texture(spr_matmap,0));
draw_sprite_ext(sprite_index,image_index,x,y,image_xscale,image_yscale,image_angle,make_color_rgb(2,1,4),image_alpha);
shader_reset();
I've set both my colour map sprites to separate texture pages. Also I optimised it by replacing all those slow if statements to directly accessing v_vColours.rgb with the yy loop. This works because it does the exact same result as the if statements.
 
Ok so I've had a go at fixing my shader:
Code:
//
// Recolour shader
//
varying vec2 v_vTexcoord;
varying vec4 v_vColour;

uniform sampler2D texColmap; //the base colour map
uniform sampler2D materialMap; //the color maps for materials

void main()
{
    //variables
    vec4 outp;
    vec4 spr;
    bool done;
    spr = texture2D(gm_BaseTexture,v_vTexcoord); //the sprite currently being drawn
    done = false;
    //finding match
    for (int yy=0;yy<3;yy++) //loop thru materials
        {
        for (int xx=0;xx<3;xx++) //loop thru material shades
            {
            if (vec4(spr) == vec4(texture2D(texColmap,vec2(xx/3,yy/3)))) //if map matches current pixel
                {
                outp=vec4(texture2D(materialMap,vec2(xx/3,v_vColour.yy)));
                done == true;
                break;
                }
            }
        if (done == true)
            {
            break;   
            }
        }
    //output
    gl_FragColor = outp;
}
I have tried to fix those things you mentioned in your post and it compiles and runs now! :D
Unfortunately though it does not give me the result I want. When I run it, instead of drawing the sprite remapped to the new materials colours it just draw 1 colour in blue no matter what rgb I put in make_colour_rgb. Like this: (before and after)
View attachment 34600
The other colours seem to turn black.
Draw event code:
GML:
shader_set(shader1);
texture_set_stage(shader_get_sampler_index(shader1,"texColmap"),sprite_get_texture(spr_colmap,0));
texture_set_stage(shader_get_sampler_index(shader1,"materialMap"),sprite_get_texture(spr_matmap,0));
draw_sprite_ext(sprite_index,image_index,x,y,image_xscale,image_yscale,image_angle,make_color_rgb(2,1,4),image_alpha);
shader_reset();
I've set both my colour map sprites to separate texture pages. Also I optimised it by replacing all those slow if statements to directly accessing v_vColours.rgb with the yy loop. This works because it does the exact same result as the if statements.
*bump*
Anyone with shader knowledge able to help me out with this one?😁
 

HalRiyami

Member
I'm not on my PC right now so I can't help much but a quick look at your code shows that you might be using v_vColour incorrectly.
You input rgb values of (2, 1, 4) which I assume you want to use as is, but these are interpreted as a single color in the shader. What you need to do is again convert that single color into its components multiplied by 256 to retrieve your input (v_vColour.yy * 256 = 2 @yy = 0).
My other advice is to take some time to learn how to use shadertoy.com,this in turn will enormously help you speed up any shader related testing/creating. It saved me days worth of time because of how fast you can see your changes.
 
I'm not on my PC right now so I can't help much but a quick look at your code shows that you might be using v_vColour incorrectly.
You input rgb values of (2, 1, 4) which I assume you want to use as is, but these are interpreted as a single color in the shader. What you need to do is again convert that single color into its components multiplied by 256 to retrieve your input (v_vColour.yy * 256 = 2 @yy = 0).
My other advice is to take some time to learn how to use shadertoy.com,this in turn will enormously help you speed up any shader related testing/creating. It saved me days worth of time because of how fast you can see your changes.
Ahh ok. Yeah completely forgot about that. I think someone mentioned that eariler^
I'm afk to so I'll try that out later.
Also thanks for the tip with shadertoy.com I'll check it out!
😁
 
I'm not on my PC right now so I can't help much but a quick look at your code shows that you might be using v_vColour incorrectly.
You input rgb values of (2, 1, 4) which I assume you want to use as is, but these are interpreted as a single color in the shader. What you need to do is again convert that single color into its components multiplied by 256 to retrieve your input (v_vColour.yy * 256 = 2 @yy = 0).
My other advice is to take some time to learn how to use shadertoy.com,this in turn will enormously help you speed up any shader related testing/creating. It saved me days worth of time because of how fast you can see your changes.
Ok one last error and then hopefully this will work! :p
Code:
outp=vec4(texture2D(materialMap,vec2(xx/3,v_vColour.yy*256)));
I'm getting an error with the * symbol. Is there some reason I can't use it here? I've tried to pre-calculate this value in a separate float variable but still the same error.
Edit: I realised that the 256 needs to be 256.0 and 3.0 because its a float.
Still getting the same result as before. Looks like this:
Screenshot (8).png
 
Last edited:

HalRiyami

Member
All floats need to be written as such. 256 is an integer so you'd need to write 256.0 for it to be recognized as a float. It's giving you an error because you're multiplying an integer and float which doesn't work.
 
All floats need to be written as such. 256 is an integer so you'd need to write 256.0 for it to be recognized as a float. It's giving you an error because you're multiplying an integer and float which doesn't work.
thanks. Yes I fixed this and it compiles but still gives me that result(pic in above post)^
 

HalRiyami

Member
If you're using GMS 2.2, could you upload the project here? It would be easier for me to see what's wrong. I don't mind you stripping the project and just keeping the three sprites.
 

HalRiyami

Member
So here is what I came up with

Draw Event Code:
GML:
shader_set(shader11);
texture_set_stage(shader_get_sampler_index(shader11,"colormap"),sprite_get_texture(spr_colmap,0));
texture_set_stage(shader_get_sampler_index(shader11,"materialmap"),sprite_get_texture(spr_matmap,0));
draw_sprite_ext(sprite_index,image_index,x,y,image_xscale,image_yscale,image_angle,make_color_rgb(1,2,3),1.0);
shader_reset();
Nothing really changed here, just make sure you're using the correct references to the samplers.

Shader Code:
GML:
varying vec2 v_vTexcoord;
varying vec4 v_vColour;

uniform sampler2D colormap; //the base colour map
uniform sampler2D materialmap; //the color maps for materials

void main() {
    // Get the current pixel color
    vec4 currentcol = vec4(texture2D(gm_BaseTexture,v_vTexcoord));
    bool found = false;
    
    // If transparent pixel then skip
    if (currentcol.a == 0.0) {
        discard;
    } else {
        for (float yy=0.0;yy<3.0;yy++) {//loop thru materials
            // Cast float yy to integer
            int iyy = int(yy);
            
            // If the input value is 0 then return the original color and skip this iteration
            if (v_vColour[iyy]*255.0 == 0.0) {
                gl_FragColor = currentcol;
                continue;
            }
            
            for (float xx=0.0;xx<3.0;xx++) {//loop thru material shades
                // Get the uv coords of the color we want to match from the4 color map
                // We add 0.5 to make sure we are in the center of the pixel
                vec2 coltomatch = vec2((xx +0.5)/4.0,(yy +0.5)/4.0);
                vec4 matchcol = vec4(texture2D(colormap,coltomatch));
                
                // Get the uv coords of the color we want to use from the material map
                // We subtract 0.5 in the y axis because we want to reserve input value 0
                // for when we don't want to change the color
                vec2 mattouse = vec2((xx +0.5)/16.0,(v_vColour[iyy]*255.0 -0.5)/16.0);
                vec4 matcolor = vec4(texture2D(materialmap,mattouse));
                
                // If a match is found we can now substitute the color
                if (currentcol == matchcol) {
                    gl_FragColor = matcolor;
                    found = true;
                    break;
                }
            if (found) {break;}
            }
        }
    }
}
As you can see, I commented as much as I could to explain why I did what I did.
Now one thing to keep in mind is that the color and material maps use a different canvas size. 4x4 for the color map and 16x16 for the material map to keep them in powers of 2 so that the math can work out neatly.
If you input image_blend(0,0,0) the shader will return the original palette.

Hope this helps.
 
Wow tha
So here is what I came up with

Draw Event Code:
GML:
shader_set(shader11);
texture_set_stage(shader_get_sampler_index(shader11,"colormap"),sprite_get_texture(spr_colmap,0));
texture_set_stage(shader_get_sampler_index(shader11,"materialmap"),sprite_get_texture(spr_matmap,0));
draw_sprite_ext(sprite_index,image_index,x,y,image_xscale,image_yscale,image_angle,make_color_rgb(1,2,3),1.0);
shader_reset();
Nothing really changed here, just make sure you're using the correct references to the samplers.

Shader Code:
GML:
varying vec2 v_vTexcoord;
varying vec4 v_vColour;

uniform sampler2D colormap; //the base colour map
uniform sampler2D materialmap; //the color maps for materials

void main() {
    // Get the current pixel color
    vec4 currentcol = vec4(texture2D(gm_BaseTexture,v_vTexcoord));
    bool found = false;
    
    // If transparent pixel then skip
    if (currentcol.a == 0.0) {
        discard;
    } else {
        for (float yy=0.0;yy<3.0;yy++) {//loop thru materials
            // Cast float yy to integer
            int iyy = int(yy);
            
            // If the input value is 0 then return the original color and skip this iteration
            if (v_vColour[iyy]*255.0 == 0.0) {
                gl_FragColor = currentcol;
                continue;
            }
            
            for (float xx=0.0;xx<3.0;xx++) {//loop thru material shades
                // Get the uv coords of the color we want to match from the4 color map
                // We add 0.5 to make sure we are in the center of the pixel
                vec2 coltomatch = vec2((xx +0.5)/4.0,(yy +0.5)/4.0);
                vec4 matchcol = vec4(texture2D(colormap,coltomatch));
                
                // Get the uv coords of the color we want to use from the material map
                // We subtract 0.5 in the y axis because we want to reserve input value 0
                // for when we don't want to change the color
                vec2 mattouse = vec2((xx +0.5)/16.0,(v_vColour[iyy]*255.0 -0.5)/16.0);
                vec4 matcolor = vec4(texture2D(materialmap,mattouse));
                
                // If a match is found we can now substitute the color
                if (currentcol == matchcol) {
                    gl_FragColor = matcolor;
                    found = true;
                    break;
                }
            if (found) {break;}
            }
        }
    }
}
As you can see, I commented as much as I could to explain why I did what I did.
Now one thing to keep in mind is that the color and material maps use a different canvas size. 4x4 for the color map and 16x16 for the material map to keep them in powers of 2 so that the math can work out neatly.
If you input image_blend(0,0,0) the shader will return the original palette.

Hope this helps.
Wow thanks mate! Appreciate the effort that went into this. I'm AFK so I'll let you know how I go tommorow.
Again, thanks!
 
So here is what I came up with

Draw Event Code:
GML:
shader_set(shader11);
texture_set_stage(shader_get_sampler_index(shader11,"colormap"),sprite_get_texture(spr_colmap,0));
texture_set_stage(shader_get_sampler_index(shader11,"materialmap"),sprite_get_texture(spr_matmap,0));
draw_sprite_ext(sprite_index,image_index,x,y,image_xscale,image_yscale,image_angle,make_color_rgb(1,2,3),1.0);
shader_reset();
Nothing really changed here, just make sure you're using the correct references to the samplers.

Shader Code:
GML:
varying vec2 v_vTexcoord;
varying vec4 v_vColour;

uniform sampler2D colormap; //the base colour map
uniform sampler2D materialmap; //the color maps for materials

void main() {
    // Get the current pixel color
    vec4 currentcol = vec4(texture2D(gm_BaseTexture,v_vTexcoord));
    bool found = false;
   
    // If transparent pixel then skip
    if (currentcol.a == 0.0) {
        discard;
    } else {
        for (float yy=0.0;yy<3.0;yy++) {//loop thru materials
            // Cast float yy to integer
            int iyy = int(yy);
           
            // If the input value is 0 then return the original color and skip this iteration
            if (v_vColour[iyy]*255.0 == 0.0) {
                gl_FragColor = currentcol;
                continue;
            }
           
            for (float xx=0.0;xx<3.0;xx++) {//loop thru material shades
                // Get the uv coords of the color we want to match from the4 color map
                // We add 0.5 to make sure we are in the center of the pixel
                vec2 coltomatch = vec2((xx +0.5)/4.0,(yy +0.5)/4.0);
                vec4 matchcol = vec4(texture2D(colormap,coltomatch));
               
                // Get the uv coords of the color we want to use from the material map
                // We subtract 0.5 in the y axis because we want to reserve input value 0
                // for when we don't want to change the color
                vec2 mattouse = vec2((xx +0.5)/16.0,(v_vColour[iyy]*255.0 -0.5)/16.0);
                vec4 matcolor = vec4(texture2D(materialmap,mattouse));
               
                // If a match is found we can now substitute the color
                if (currentcol == matchcol) {
                    gl_FragColor = matcolor;
                    found = true;
                    break;
                }
            if (found) {break;}
            }
        }
    }
}
As you can see, I commented as much as I could to explain why I did what I did.
Now one thing to keep in mind is that the color and material maps use a different canvas size. 4x4 for the color map and 16x16 for the material map to keep them in powers of 2 so that the math can work out neatly.
If you input image_blend(0,0,0) the shader will return the original palette.

Hope this helps.
Thankyou so much! It works! Thankyou to everyone who helped out with this one, I really appreciate it. I've learned so much about shaders.
Here is some screenshots of the results in my test project (not implemented in my game yet:)):
Recolour_shader.PNG
 
Hello! I'm back with another question: :D How can I keep image_alpha working with the recolour shader?
I tried setting outp.a = spr.a but it did nothing. I want it to still recolour or passthru but keep image_alpha set in draw_sprite_ext.
Code:
//
// Improved Recolour shader
//
varying vec2 v_vTexcoord;
varying vec4 v_vColour;

uniform sampler2D colormap; //the base colour map
uniform sampler2D materialmap; //the color maps for materials

void main()
{
    //variables
    vec4 outp;
    vec4 spr;
    bool done;
    spr = texture2D(gm_BaseTexture,v_vTexcoord); //the sprite currently being drawn
    outp = spr; //original pixel colour (should passthru unless a colour match is found and the colour is changed)
    done = false;
    //if pixel is transparent then skip
    if (spr.a == 0.0)
        {
        discard;
        }
    //finding match
    for (float yy=0.0;yy<3.0;yy++) //loop thru materials
        {
        //float to integer
        int iyy = int(yy);
        //if input value is 0 then skip this iteration
        if (v_vColour[iyy]*255.0 == 0.0)
            {
            outp = spr;
            continue;   
            }
        for (float xx=0.0;xx<3.0;xx++) //loop thru material shades
            {
            if (vec4(spr) == vec4(texture2D(colormap,vec2((xx +0.5)/4.0,(yy +0.5)/4.0)))) //if map matches current pixel // We add 0.5 to make sure we are in the center of the pixel
                {
                outp = vec4(texture2D(materialmap,vec2((xx+0.5)/4.0,(v_vColour[iyy]*255.0 -0.5)/16.0))); // We subtract 0.5 in the y axis because we want to reserve input value 0
                outp.a = spr.a;
                done = true;
                break;
                }
            }
        if (done == true)
            {
            break;   
            }
        }
    //output
    gl_FragColor = outp;
}
Thanks!
 
Top