PDA

View Full Version : How can I colorize a texture without losing white?



patrick99e99
11-06-2016, 03:46 PM
If I have the following texture:
black & white arrow example (http://collinatorstudios.com/www/arrow-0.png)

How can I colorize this but not completely tint the texture so that the white in it becomes purple or red?

desired output as purple:
purple arrow example (http://collinatorstudios.com/www/arrow-1.png)
desired output as red:
red arrow example (http://collinatorstudios.com/www/arrow-2.png)

Just a bit of background, this is for an iOS game I am making with Cocos2d which uses OpenGL ES 2.0 under the hood. Originally, I was using 7 different colored texture files, which were identical other than their color, which was very wasteful... So I have been trying to explore ways to programmatically change the colors... My first attempt was to use Apple's CoreImage filters prior to loading the textures into openGL, and it works--- but there is a horrific performance lag when loading my textures now, so I was wondering how easy/difficult of a task this would be with openGL?

The method at line 119 is how the game library loads textures:
https://searchcode.com/codesearch/view/73755093/
(https://searchcode.com/codesearch/view/73755093/)

patrick99e99
11-06-2016, 04:51 PM
I found this question which is exactly what I am wanting:
http://stackoverflow.com/a/5370471/594763
(http://stackoverflow.com/a/5370471/594763)

However, I do not understand the answer at all-- not to mention the code example in the accepted answer involving multi-texturing is for OpenGL ES 1.0.


Anyway, after you premultiply, just bind the byte data as an RGBA OpenGL texture. Using glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_ADD); with a single texture unit should be all you need after that. You should get exactly (or pretty damn close) to what you want.

How do you apply a color to this texture? How would I turn it green, yellow, purple, red, etc?

john_connor
11-07-2016, 02:23 AM
How can I colorize this but not completely tint the texture so that the white in it becomes purple or red?

assuming you have a "fragmentshader" which samples from that texture, you can easily multiply the sampled texel with your desired color

fragmentshader example:


in vec2 sometexcoords;
uniform vec4 desiredcolor;
uniform sampler2D arrow;
out vec4 outcolor;

void main() {
vec4 texel = texture(arrow, sometexcoords);
outcolor = texel * desiredcolor;
}

if the texel at that texture location (described by sometexcoords) is black, then the result wil be black
if the texel at that texture location (described by sometexcoords) is white, then the result wil be desiredcolor

to make black disappear, enable blending and add the result with the color already written in te framebuffer
https://www.opengl.org/wiki/Blending#Blend_Equations

patrick99e99
11-07-2016, 02:12 PM
assuming you have a "fragmentshader" which samples from that texture, you can easily multiply the sampled texel with your desired color


Hmmm, well I just tried what you suggested but that removes all luminance..



#ifdef GL_ES
precision lowp float;
#endif

varying vec4 v_fragmentColor;
varying vec2 v_texCoord;
uniform vec4 u_desiredColor;
uniform sampler2D u_texture;

void main()
{
gl_FragColor = texture2D(u_texture, v_texCoord) * u_desiredColor;
}


This outputs:
My results of fragment shader with red tint (http://collinatorstudios.com/www/bad-arrows.png)

Again what I want is something that looks as close to this as possible:
My desired results (http://collinatorstudios.com/www/arrow-2.png)

To explain further what I want: The darker shades of gray should have the richest color, but as the sampled grayscale pixel approaches white, the colorized pixels lose saturation, where ultimately a sampled pixel of white has 0% saturation-- but a sampled pixel of dark gray would be 100% saturation.

A really simple starting point would just be to have the output pixel color's saturation be something like 1.0 - samplePixelIntensity... So, if the sample pixel's intensity was 1.0, that would mean 0% saturation in the output, if the sample pixel's intensity was 0.5, it would have 50% saturation.. if the sample pixel's intensity was 0.2, then it would have 80% saturation..

Can I achieve this with a fragment shader?

GClements
11-07-2016, 07:13 PM
If I understand you correctly, you want to map the black->grey->white progression in the source texture to a black->(colour)->white progression, for a colour chosen at run time.

One way to do this is with a palette stored e.g. as a 1D texture. Read a pixel from the source texture, use one of the components (all three should have the same value) as the texture coordinate for the 1D texture. I.e.


in vec2 texcoord;
uniform sampler2D tex;
uniform sampler1D palette;

void main()
{
float intensity = texture(tex, texcoord).r;
vec3 color = texture(palette, intensity);
gl_FragColor = vec4(color, 1);
}


For a palette which is a smooth gradient, you can use a function instead of a 1D texture. E.g.



vec3 blend(vec3 color, float t)
{
float a = clamp(2*t,0,1);
float c = clamp(2*(1-t),0,1);
float b = 1-a-c;
return b*color+c; // = a*vec3(0,0,0)+b*color+c*vec3(1,1,1)
}

or


vec3 blend(vec3 color, float t)
{
float a = smoothstep(0.0,0.5,t);
float c = smoothstep(0.5,1.0,t);
float b = 1-a-c;
return b*color+c; // = a*vec3(0,0,0)+b*color+c*vec3(1,1,1)
}

These will vary from black at t=0 to the specified colour at t=0.5 to white at t=1.

But a palette has the advantage of being easier to design in an external program then load from a file. There are some semi-standard formats for palettes, which can be created using paint programs such as GIMP.

patrick99e99
11-07-2016, 09:12 PM
If I understand you correctly, you want to map the black->grey->white progression in the source texture to a black->(colour)->white progression, for a colour chosen at run time.

Thank you so much for your reply! I tried your suggestion involving clamp, and it seems that all of the colors are somehow getting inversed?



#ifdef GL_ES
precision highp float;
#endif

varying vec4 v_fragmentColor;
varying vec2 v_texCoord;
uniform vec3 u_desiredColor;
uniform sampler2D u_texture;

float a;
float b;
float c;
vec4 source;

vec3 blend(vec3 color, float t)
{
a = clamp(2.0 * t, 0.0, 1.0);
c = clamp(2.0 * (1.0 - t), 0.0, 1.0);
b = 1.0 - a - c;
return b * color + c;
}

void main()
{
source = texture2D(u_texture, v_texCoord);
gl_FragColor = vec4(blend(u_desiredColor, source.r), source.a);
}


This gives me:
Output of shader using clamp (http://collinatorstudios.com/www/inversed-arrows.png)

I am not sure if I did something wrong or not? I am passing in vec3(1.0, 0.0, 0.0) for u_desiredColor... Any idea?

I also tried the smoothstep version and it gives me:
Output of shader using smoothstep (http://collinatorstudios.com/www/red-arrows.png)

GClements
11-07-2016, 09:26 PM
Thank you so much for your reply! I tried your suggestion involving clamp, and it seems that all of the colors are somehow getting inversed?
Oops. Try:


a = clamp(1.0 - 2.0 * t, 0.0, 1.0);
c = clamp(2.0 * t - 1.0, 0.0, 1.0);

or:


a = 1.0-clamp(2.0 * t, 0.0, 1.0);
c = 1.0-clamp(2.0 * (1.0 - t), 0.0, 1.0);

The latter approach (1.0-...) can also be applied to the smoothstep() version.

patrick99e99
11-07-2016, 09:27 PM
One way to do this is with a palette stored e.g. as a 1D texture. Read a pixel from the source texture, use one of the components (all three should have the same value) as the texture coordinate for the 1D texture.

So with this approach, I could use the file that looks how I want it to look, and use that as the palette?
My desired results (http://collinatorstudios.com/www/arrow-2.png)

I am a little confused with how that works exactly, if its a 1D texture, that would mean it's a very wide image that is 1px high with all the various colors? They are ordered from lowest intensity to highest?



float intensity = texture(tex, texcoord).r;
vec3 color = texture(palette, intensity);
gl_FragColor = vec4(color, 1);


I'm a bit confused how intensity would map to the correct x coordinate for the correct color + intensity.. How does that work exactly?

patrick99e99
11-07-2016, 09:37 PM
Oops. Try:


a = clamp(1.0 - 2.0 * t, 0.0, 1.0);
c = clamp(2.0 * t - 1.0, 0.0, 1.0);

or:


a = 1.0-clamp(2.0 * t, 0.0, 1.0);
c = 1.0-clamp(2.0 * (1.0 - t), 0.0, 1.0);

The latter approach (1.0-...) can also be applied to the smoothstep() version.

Yeah, that looks a *LOT* better...

Results and comparison (http://collinatorstudios.com/www/closer-arrows.png)

The left is the shader, and the right is my desired output...

The glowy arrow is a little over saturated and blobby, and the non-glowy arrow is a little too far on the white side...

Is this where a palette consisting of the exact colors in my desired result version, would solve all the problems?

patrick99e99
11-07-2016, 09:41 PM
The latter approach (1.0-...) can also be applied to the smoothstep() version.

Hmm, I assume you mean:


float a = 1.0 - smoothstep(0.0,0.5,t);
float c = 1.0 - smoothstep(0.5,1.0,t);


I tried that but the coloring was all wrong-- in any event, even though the colors were wrong, I could still tell the result of the glowy arrow was still a little too blobby and over saturated around the edges...

Is there an easy way I can say add a slight bit more color to the output and smooth out the edges of the glowy version so that it matches more closely my desired results version?

GClements
11-07-2016, 10:01 PM
Is this where a palette consisting of the exact colors in my desired result version, would solve all the problems?
Probably. It's easier to modify a palette by eye than a function.

Also, what neither approach can change is the fact that pixels which have the same colour in the original image will have the same colour in the colourised image.

You can change the rate of transition (e.g. the smoothstep version will tend to have more black, red and white, and less of the in-between colours), but the overall shape is set by the original image.

GClements
11-07-2016, 10:04 PM
Probably. It's easier to modify a palette by eye than a function.

Incidentally, the easiest way to do that is to load the grey-scale image into a paint program, convert it to a palette-based (256-colour) image, then use the paint program's palette editor. That will give you immediate feedback.

Then you can either figure out how to read the paint program's palette files directly, or read the palette from the modified image, or read the colours from the original and modified images and generate the palette from that.

patrick99e99
11-07-2016, 10:34 PM
Well, I was able to get gimp to just output my color palette for one of the original files. It actually will export it out as a text file, so I have a list of hex color codes (not so useful, I know). So, assuming I convert these hex values to 0-1 ranges, how exactly do I do the code for this?

say somewhere I have a 2 dimensional float array consisting of rgb values.



float intensity = texture(tex, texcoord).r;
vec3 color = texture(palette, intensity); // i assume I wouldn't need to do this now? it would be more like:
vec3 color = palleteArray[intensity]; // or something, right?
gl_FragColor = vec4(color, 1);

patrick99e99
11-07-2016, 10:35 PM
Except for the fact that when I look at this palette, gimp did not sort it by intensity-- so I'd need to do that too?

GClements
11-08-2016, 02:07 AM
say somewhere I have a 2 dimensional float array consisting of rgb values.

A texture would probably be preferable to an array, as you can index it using the 0-1 float value you get from the texture. Also, for a smooth gradient, you don't need to use 256 colours; you can use a smaller texture and use GL_LINEAR texture filtering to interpolate it.

If you want to use an array, you'd use something like:


vec3 color = paletteArray[int(round(255*intensity))];



Except for the fact that when I look at this palette, gimp did not sort it by intensity-- so I'd need to do that too?
Hmm; GIMP's palette editing is rather primitive. You could just create the palette as an image which you load as a texture, although then you don't get the live preview. Or you could see if you can find a program with better palette-editing facilities.

patrick99e99
11-08-2016, 08:52 AM
A texture would probably be preferable to an array, as you can index it using the 0-1 float value you get from the texture. Also, for a smooth gradient, you don't need to use 256 colours; you can use a smaller texture and use GL_LINEAR texture filtering to interpolate it.

The thing that I am still hung up on is this code:


float intensity = texture(tex, texcoord).r;
vec3 color = texture(palette, intensity);
gl_FragColor = vec4(color, 1);


This would require the color palette image to have the colors ordered from lowest to highest intensity, right? I don't understand how that can work otherwise...

The palette gimp created from my original image has 875 colors. So I would imagine this means my 1D palette texture is 875x1 ?

patrick99e99
11-08-2016, 09:09 AM
Also, from all my googling, I can't seem to find anything about programs that will export a palette as an image... They all do it as .csv, .txt, etc...

patrick99e99
11-08-2016, 09:17 AM
Well, I just figured out, I can tell gimp to convert the palette to a gradient and then fill a new image (875 X 1px) with that gradient...

patrick99e99
11-08-2016, 02:20 PM
A texture would probably be preferable to an array, as you can index it using the 0-1 float value you get from the texture.

Well I have been trying to get this palette approach going, but it doesn't seem to be working, and I don't know why... I made a .png of the palette, and I have loaded it as a 2D texture and binded it to the variable u_palette:



CCTexture2D *bluePalette = [[CCTextureCache sharedTextureCache] addImage:@"blue_palette.png"];
[bluePalette setAliasTexParameters]; // turn off anti-aliasing

GLint paletteUniformLocation = glGetUniformLocation(shaderProgram->program_, "u_palette");
glUniform1i(paletteUniformLocation, bluePalette.name); // name property is the GLuint id that comes from --> glGenTextures(1, &name_);


and then the fragment shader code is:



ifdef GL_ES
precision highp float;
#endif

varying vec4 v_fragmentColor;
varying vec2 v_texCoord;
uniform sampler2D u_texture;
uniform sampler2D u_palette;
vec3 color;
float intensity;

void main()
{
intensity = texture2D(u_texture, v_texCoord).r;
color = texture2D(u_palette, vec2(intensity, 0.0)).rgb;
gl_FragColor = vec4(color, 1.0);
}


I don't get any OpenGL errors.. but nothing appears on the screen...

Note, I am loading the palette as a 2D texture because I am using the game engine Cocos2D which does not support 1D textures, I figured it should be ok to load it as a 2D texture and just do vec2(intesity, 0.0)... That should be fine right?

Do you see any reason why this code wouldn't be working?

patrick99e99
11-08-2016, 05:03 PM
Do you see any reason why this code wouldn't be working?

Agh.. well now that I have stepped away, it occurred to me that of course that wouldn't work! intensity is 0-1, so I would need to be doing:



color = texture2D(u_palette, vec2(intensity * 875.0, 0.0)).rgb;

GClements
11-08-2016, 06:09 PM
Agh.. well now that I have stepped away, it occurred to me that of course that wouldn't work! intensity is 0-1, so I would need to be doing:



color = texture2D(u_palette, vec2(intensity * 875.0, 0.0)).rgb;

Texture coordinates are in the range 0-1.

Multiplication by the texture dimensions is done internally by the texture hardware.

patrick99e99
11-08-2016, 06:21 PM
Texture coordinates are in the range 0-1.
Multiplication by the texture dimensions is done internally by the texture hardware.

Oh.......................... Well then I am back to not understanding why this isn't working. :(

patrick99e99
11-08-2016, 07:27 PM
Aha.. I figured out why... I didn't fully understand how to properly bind a texture to a uniform variable in the shader world.

I needed to do:



GLint paletteUniformLocation = glGetUniformLocation(shaderProgram->program_, "u_palette");
glUniform1i(paletteUniformLocation, 1);

glActiveTexture(GL_TEXTURE1);
glEnable(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, bluePalette.name);
glActiveTexture(GL_TEXTURE0);


And now it works!

arrows with blue palette (http://collinatorstudios.com/www/crazy-arrow.png)

Except.. now I just need to figure out how to make my palette sorted by intensity-- then everything should be great...

patrick99e99
11-08-2016, 07:45 PM
Well, ok-- I got the palette sorted.. but..... I still am getting quite ugly results:

Purple arrow comparison (http://collinatorstudios.com/www/terrible-compare.png)

You had mentioned doing GL_LINEAR to smooth out intensity sampling? I tried doing this on the palette texture:



glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);


But it didn't seem to make a difference...

Is there anything off the top of your head that I can do to fix this? It's weird to me that the bright glow somehow got an incorrect intensity value-- it mistook many of the bright pixels for dark ones... Why would that happen?

patrick99e99
11-08-2016, 08:51 PM
You know....... I'm sorry.. I was using gimp's palette sorting, which I determined is buggy. I parsed its palette file myself and sorted it, and I get much better results, but still not perfect--- I will explore some other options for generating a more reliable palette.

patrick99e99
11-09-2016, 09:22 PM
So.. I ended up using ImageMagick to do a color histogram and sorted by intensity, which the documentation says intensity is computed as 0.299*R+0.587*G+0.114*B.

The palette I generated for blue looks like this:
My blue palette (http://collinatorstudios.com/www/blue-bright-palette.png)

and the grayscale sprite colored from the fragment shader looks like this:
My blue arrow (http://collinatorstudios.com/www/blue-arrow-damn-it.png)

As you can see there are many rings and weird artifacts around the glow.. I can't tell if that's from the palette still not being right? or not enough colors? or what?

@GlClements, you had mentioned something about using GL_LINEAR? How do you use that exactly? I tried doing:


glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

on both my palette and grayscale textures, but that made no difference...

Am I doing that wrong? Can you or anyone else think of any way to improve this so that the glow looks smooth and as it should?

Again, it's supposed to look like this:
How it's supposed to look (collinatorstudios.com/www/blue-arrow-correct.png)

GClements
11-09-2016, 10:10 PM
So.. I ended up using ImageMagick to do a color histogram and sorted by intensity, which the documentation says intensity is computed as 0.299*R+0.587*G+0.114*B.

The palette I generated for blue looks like this:
My blue palette (http://collinatorstudios.com/www/blue-bright-palette.png)

Well, it appears to have made a mistake in the sorting, because that has darker shades interspersed with lighter shades.

You may be better off just using GIMP's gradient-fill tool to create an image.



@GlClements, you had mentioned something about using GL_LINEAR? How do you use that exactly? I tried doing:


glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

on both my palette and grayscale textures, but that made no difference...

For large enough textures, there won't be much difference, if any. At best it might reduce banding slightly.

The advantage of linear filtering is that it allows you to make the palette texture smaller. You could get away with a 3x1 texture containing just [black, colour, white], although you'd need to tweak the intensity slightly:


intensity = (intensity * 2.0 + 0.5)/3.0

More generally, multiply by width-1, add 0.5, divide by width. For larger textures, this adjustment doesn't matter.

patrick99e99
11-10-2016, 10:51 AM
For large enough textures, there won't be much difference, if any. At best it might reduce banding slightly.

I guess what I am unclear about is which texture am I supposed to put this GL_LINEAR setting on? Both? Or just the palette? and am I supposed to do both MIN and MAG?

GClements
11-10-2016, 06:39 PM
I guess what I am unclear about is which texture am I supposed to put this GL_LINEAR setting on? Both? Or just the palette? and am I supposed to do both MIN and MAG?
Well, I was referring to using it on the palette; in which case, you'd need to use it for both minification and magnification. Which filters you'd on the arrow is independent of whether you're re-colouring it.

patrick99e99
11-12-2016, 12:51 PM
Well, I spent a lot of time manually building palettes as you suggested and got everything looking exactly how I want it, so thank you very much!

I have a new problem now...

So, because I have 14 palettes (7 for the normal sprite, and 7 for the fuzzy-glowy version of the sprite), and I was loading each of these palettes as individual files-- I was getting 0x0050 invalid enum errors when I was trying to bind to the last few color palette textures, which told me that I was using too many textures...

So I combined all my palettes into one texture file that is 14 pixels in height.

But now to get the shader to know what Y coordinate to use in the palette is not turning out to be as simple as I thought it should be?



color = texture2D(u_palette, vec2(intensity, u_paletteRow)).rgb;


My palette texture is setup with the y's ordered like:

0 = blue
1 = red
2 = purple
3 = green
4 = orange
5 = yellow
6 = white
7 = blue-glow
8 = red-glow
9 = purple-glow
10 = green-glow
11 = orange-glow
12 = yellow-glow
13 = white-glow

So, I thought I could just do this (color will equal values 0-6, and brightGlow is boolean to indicate whether to use the palette for the bright glow sprites or not):



GLint paletteRowUniformLocation = glGetUniformLocation(shaderProgram->_program, "u_paletteRow");
glUniform1f(paletteRowUniformLocation, color + (brightGlow ? 7.0f : 0) / 13.0f);


But.. My colors are all messed up when I do this...

patrick99e99
11-12-2016, 01:54 PM
I was able to fix this by just making my texture 256x256 instead of 256x14. I just repeated the gradients in chunks of 16px, and then did:



glUniform1f(paletteRowUniformLocation, color + (brightGlow ? 7.0f : 0) / 16.0f);


Which seems to fix it, but I feel like I shouldn't need to do 256x256... right?

GClements
11-13-2016, 12:53 AM
But now to get the shader to know what Y coordinate to use in the palette is not turning out to be as simple as I thought it should be?



color = texture2D(u_palette, vec2(intensity, u_paletteRow)).rgb;


Bear in mind that texture coordinates are in the range 0..1. If u_paletteRow is an integer between 0 and 13 inclusive, you'd need


color = texture2D(u_palette, vec2(intensity, (u_paletteRow + 0.5)/14.0)).rgb;

patrick99e99
11-13-2016, 01:07 PM
Bear in mind that texture coordinates are in the range 0..1. If u_paletteRow is an integer between 0 and 13 inclusive, you'd need


color = texture2D(u_palette, vec2(intensity, (u_paletteRow + 0.5)/14.0)).rgb;


Oh.. I guess I don't understand the + 0.5 part?

GClements
11-13-2016, 11:17 PM
I guess I don't understand the + 0.5 part?
It ensures that you sample from the centre of each pixel rather than at the edge.

If you're using GL_LINEAR, sampling from the edge will blend between adjacent rows. If you're using GL_NEAREST, there's a chance you'll sample from the wrong row due to rounding error.

King Fly
11-14-2016, 09:02 AM
Hmm, I assume you mean:


float a = 1.0 - smoothstep(0.0,0.5,t);
float c = 1.0 - smoothstep(0.5,1.0,t);


I tried that but the coloring was all wrong-- in any event, even though the colors were wrong, I could still tell the result of the glowy arrow was still a little too blobby and over saturated around the edges...

Is there an easy way I can say add a slight bit more color to the output and smooth out the edges of the glowy version so that it matches more closely my desired results version?

You should try Code :
color = texture2D(u_palette, vec2(intensity, (u_paletteRow + 0.5)/14.0)).rgb;

patrick99e99
11-14-2016, 11:39 PM
It ensures that you sample from the centre of each pixel rather than at the edge.

If you're using GL_LINEAR, sampling from the edge will blend between adjacent rows. If you're using GL_NEAREST, there's a chance you'll sample from the wrong row due to rounding error.

Ah.. ok, got it.. The idea that a pixel has a center is-- so weird to me! I am used to thinking of a pixel as the smallest unit.

Anyway, thank you so much for all your advice and help, everything is working great and looks beautiful!