# How to draw a specific color with gpu shader

This is a bulid-in shader of gpu module to display a 2D rectangle in viewport,
if you check the color of the rectangle via photoshop/MSpaint or use gpu.state.active_framebuffer_get().read_color(, you will see the color is #060000. The input for the red channel in the script below is 0.005.
However, when I test for values between 0 and 0.005, It never shows the color #050000.
So how can I draw that rectangle with the color #050000?

import bpy
import gpu

vertices = (
(100, 100), (300, 100),
(100, 200), (300, 200))

indices = (
(0, 1, 2), (2, 1, 3))

def draw():


• I think you can't because floating-point decimal values generally do not have an exact binary representation so it gets capped. so basically you can't. Commented Jul 29, 2023 at 15:06
• @HarryMcKenzie They have an exact binary representation, it just has a limited precision and uses a binary rather than decimal numeric system. Some aliens using a ternary numeric system on their planet (because they have 3 fingers), could be very annoyed human decimal system can't express $1 \over 3$ exactly (they would write it as $0.1$). Commented Jul 29, 2023 at 20:19

Actually you can, see edit below.

As @Harry McKenzie suggested, you can't. Let me make this plausible by some observations. I changed your script a little to produce all grey tones from 0-1 with a step of 0.001.

import bpy
import gpu

indices = (
(0, 1, 2), (2, 1, 3))

batch = []
for i in range(1000):
vertices = (
(10 + i, 100), (10 + i+1, 100),
(10 + i, 200), (10 + i+1, 200))

pixels = []
for i in range(1000):
pixels.append(None)

def draw():
for i in range(1000):
c = i * 0.001

pixels[i] = gpu.state.active_framebuffer_get().read_color(10 + i, 100, 1,1, 4, 0, 'FLOAT')

# warning: prints a lot
print("p", i, ":", pixels[i])



Inspecting the prints, you will notice there are "blocks of 4 equal floats":

p 0 : Buffer(FLOAT, [[[0.0, 0.0, 0.0, 1.0]]])
p 1 : Buffer(FLOAT, [[[0.0, 0.0, 0.0, 1.0]]])
p 2 : Buffer(FLOAT, [[[0.003921568859368563, 0.003921568859368563, 0.003921568859368563, 1.0]]])
p 3 : Buffer(FLOAT, [[[0.003921568859368563, 0.003921568859368563, 0.003921568859368563, 1.0]]])
p 4 : Buffer(FLOAT, [[[0.003921568859368563, 0.003921568859368563, 0.003921568859368563, 1.0]]])
p 5 : Buffer(FLOAT, [[[0.003921568859368563, 0.003921568859368563, 0.003921568859368563, 1.0]]])
p 6 : Buffer(FLOAT, [[[0.007843137718737125, 0.007843137718737125, 0.007843137718737125, 1.0]]])
p 7 : Buffer(FLOAT, [[[0.007843137718737125, 0.007843137718737125, 0.007843137718737125, 1.0]]])
p 8 : Buffer(FLOAT, [[[0.007843137718737125, 0.007843137718737125, 0.007843137718737125, 1.0]]])
p 9 : Buffer(FLOAT, [[[0.007843137718737125, 0.007843137718737125, 0.007843137718737125, 1.0]]])
p 10 : Buffer(FLOAT, [[[0.011764707043766975, 0.011764707043766975, 0.011764707043766975, 1.0]]])
p 11 : Buffer(FLOAT, [[[0.011764707043766975, 0.011764707043766975, 0.011764707043766975, 1.0]]])
p 12 : Buffer(FLOAT, [[[0.011764707043766975, 0.011764707043766975, 0.011764707043766975, 1.0]]])
p 13 : Buffer(FLOAT, [[[0.011764707043766975, 0.011764707043766975, 0.011764707043766975, 1.0]]])
...
p 987 : Buffer(FLOAT, [[[0.988235354423523, 0.988235354423523, 0.988235354423523, 1.0]]])
p 988 : Buffer(FLOAT, [[[0.988235354423523, 0.988235354423523, 0.988235354423523, 1.0]]])
p 989 : Buffer(FLOAT, [[[0.988235354423523, 0.988235354423523, 0.988235354423523, 1.0]]])
p 990 : Buffer(FLOAT, [[[0.988235354423523, 0.988235354423523, 0.988235354423523, 1.0]]])
p 991 : Buffer(FLOAT, [[[0.9921569228172302, 0.9921569228172302, 0.9921569228172302, 1.0]]])
p 992 : Buffer(FLOAT, [[[0.9921569228172302, 0.9921569228172302, 0.9921569228172302, 1.0]]])
p 993 : Buffer(FLOAT, [[[0.9921569228172302, 0.9921569228172302, 0.9921569228172302, 1.0]]])
p 994 : Buffer(FLOAT, [[[0.9960784912109375, 0.9960784912109375, 0.9960784912109375, 1.0]]])
p 995 : Buffer(FLOAT, [[[0.9960784912109375, 0.9960784912109375, 0.9960784912109375, 1.0]]])
p 996 : Buffer(FLOAT, [[[0.9960784912109375, 0.9960784912109375, 0.9960784912109375, 1.0]]])
p 997 : Buffer(FLOAT, [[[0.9960784912109375, 0.9960784912109375, 0.9960784912109375, 1.0]]])
p 998 : Buffer(FLOAT, [[[0.9960784912109375, 0.9960784912109375, 0.9960784912109375, 1.0]]])
p 999 : Buffer(FLOAT, [[[1.0, 1.0, 1.0, 1.0]]])


or as an image:

This is because you have to squeeze 1000 values into the R-channel which goes from #00 to #FF and offers place for only 256 values. Thus roughly 4 adjacent float values will fall into one hexadecimal value, because 1000 / 256 = 3.90625. (The two values at the beginning for #00 and the end value for #FF belong symmetrically together, p 1000 would also be 1.0). Thats why sometimes you can see a block of just 3 (missing a 4th) or a 5/3 split instead of 4/4 (e.g. at p 993/994). This is because of rounding errors (for the exact conversion formula one would need to know all data types participating when the color values go back and forth, but i did not look into the engine).

(Edit: choosing 1024 would have made the explanantion easier :))

But now you know for sure that:

{0.000, ... ,0.001} is mapped to 0.0 which is evaluated as #00

{0.002, ... ,0.005} is mapped to 0.003921568859368563 which is evaluated as #06

and so on...

So, no chance for these guys #01, #02, #03, #04, #05 :(

Edit:

I dug deeper and there actually is a way to get all hex values on screen, but not with the approach above so that observation still holds. But the observation only shows that the values get capped, but why?

Simplifying the above script to this:

import bpy
import gpu

batches = []
indices = ((0, 1, 2), (2, 1, 3))
for i in range(256):
vertices = ((10 + i, 100), (10 + i+1, 100), (10 + i, 200), (10 + i+1, 200))

def draw():
for i in range(256):
c = i / 255.0  # come on, all hex values plz



and running it gives a linear looking gradient ((127,127,127) in the middle) but with capped values, so there may be a problem within the shader. The shader's implementation is interesting (source):

#pragma BLENDER_REQUIRE(gpu_shader_colorspace_lib.glsl)

void main()
{
fragColor = blender_srgb_to_framebuffer_space(color); // huh?
}


Looking further one finds the implementation of this function (source):

#ifndef USE_GPU_SHADER_CREATE_INFO
uniform bool srgbTarget = false;
#endif

vec4 blender_srgb_to_framebuffer_space(vec4 in_color)
{
if (srgbTarget) {
# this is the very precise version of the common gamma correction
vec3 c = max(in_color.rgb, vec3(0.0));
vec3 c1 = c * (1.0 / 12.92);
vec3 c2 = pow((c + 0.055) * (1.0 / 1.055), vec3(2.4));
in_color.rgb = mix(c1, c2, step(vec3(0.04045), c));
}
return in_color;
}


Note the uniform srgbTarget which is defaulted to false. Setting this uniform explicitly to false with shader.uniform_bool("srgbTarget", False) should not change anything. But surprise, now the gradient is nonlinear ((187,187,187) in the middle)!

This can't be. Turns out srgbTarget for the UNIFORM_COLOR shader is set to true (source), but this means the gamma correction in blender_srgb_to_framebuffer_space takes place before the color value is fed into FragColor. And this means the values must be somehow corrected back after that! There is a newer feature in OpenGL where the driver can gamma correct all framebuffers after fragment shaders have been run and there are traces of that in Blender (source), thus i assume its enabled for the default framebuffer. Thus gamma correcting and reverting this correction seems to be the root cause for the numerical issues.

However, one can do offscreen rendering (source) and hope that the driver's gamma correction is not enabled (i have no idea if this can be set). Starting from there and with the other examples, i created this script:

import bpy
import gpu
import math
import random
from mathutils import Matrix

# create the 'UNIFORM COLOR' shader ourself

"void main()"
"{"
"  gl_Position = viewProjectionMatrix * vec4(position, 1.0f);"
"}"
)

"void main()"
"{"
"  float c = color / 255.0;"
"  FragColor = vec4(c, c, c, 1.0);"
"}"
)

# create the batches to render each column in the image

batches = []
step = 2 / 255.0
for i in range(256):
k = i * step - 1.0 - step * 0.5
coords = [
(k, -1, 0), (k+step, -1, 0), (k+step, 1, 0),
(k+step, 1, 0), (k, 1, 0), (k, -1, 0)]

# start offscreen rendering

IMAGE_NAME = "all_hex_values"
WIDTH = 256
HEIGHT = 256

offscreen = gpu.types.GPUOffScreen(WIDTH, HEIGHT)

with offscreen.bind():
fb = gpu.state.active_framebuffer_get()
fb.clear(color=(0.0, 0.0, 0.0, 0.0))
with gpu.matrix.push_pop():
# reset matrices -> use normalized device coordinates [-1, 1]

for i in range(256):

buffer = fb.read_color(0, 0, WIDTH, HEIGHT, 4, 0, 'UBYTE')

offscreen.free()

# copy the render result into an blender image

if IMAGE_NAME not in bpy.data.images:
bpy.data.images.new(IMAGE_NAME, WIDTH, HEIGHT)

image = bpy.data.images[IMAGE_NAME]
image.scale(WIDTH, HEIGHT)

buffer.dimensions = WIDTH * HEIGHT * 4
image.pixels = [v / 255 for v in buffer]


Save the image and you got all these precious hex values in its full glory:

• yes exactly +1 :) Commented Jul 29, 2023 at 16:27
• I made some tests and apparently the implementation underneath uses an 8-bit minifloat. Here's a calculator, if you set 4 as both precision and exponent bits, and then test consecutive numbers 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6… You will get 0, 0.001953125, 0.00390625, 0.005859375, 0.0078125, 0.009765625, 0.01171875, suspiciously similar ;) Commented Jul 29, 2023 at 20:16
• Apparently it uses 3 precision bits (1 sign, 2 mantissa; if there's no sign, then it's just 2 precision bits) and 4 exponent bits, totaling 7 bits (or 6 without a sign), then it skips every other value listed above. Commented Jul 29, 2023 at 20:35
• As user43967 points out in the comment section to his answer, the values listed 0.003921568859368563, 0.007843137718737125, 0.011764707043766975, when multiplied by 255 give 1.000000059138983565, 2.000000118277966875, 3.000000296160578625, so the precision is enough to get those low values. However, it starts failing at 0x9, which gives one too much: 0.0390625 ×255 = 9.9609375 instead of 9. This is because the exponent increases and from now on some colors will be skipped. Understandable: need 8 bits to describe 0..255 range, and above I just figured it's 6/7 bits… Commented Jul 30, 2023 at 8:42

There is a lower bound values of the color (00-FF) except ['0x1', '0x2', '0x3', '0x4', '0x5', '0x7', '0x8', '0xa', '0xe', '0x92', '0x96', '0x99', '0x9b', '0xa1', '0xa3', '0xa5', '0xa8', '0xab', '0xb1', '0xb6', '0xb8', '0xba', '0xbc', '0xbe', '0xc1', '0xc3', '0xe8', '0xf3', '0xfb']

This means that there are 29 color of a channel that cannot be displayed in this shader. (24389 of 16777216)

I'd appreciate it if someone could solve this problem through use the alpha value or a custom shader to display those missing colors.

Hex    Value
0x00 : 0.0
0x01 : -
0x02 : -
0x03 : -
0x04 : -
0x05 : -
0x06 : 0.0015771483886055648
0x07 : -
0x08 : -
0x09 : 0.005520019447430968
0x0a : -
0x0b : 0.009462890680879354
0x0c : 0.013405761215835811
0x0d : 0.017348631285130978
0x0e : -
0x0f : 0.02129150275141001
0x10 : 0.025234374217689037
0x11 : 0.029177243821322918
0x12 : 0.0370629858225584
0x13 : 0.04099433310329915
0x14 : 0.04546292312443257
0x15 : 0.04898468963801861
0x16 : 0.05300186388194561
0x17 : 0.061055721715092666
0x18 : 0.06452127173542978
0x19 : 0.06893400475382805
0x1a : 0.07262223586440086
0x1b : 0.07616695389151573
0x1c : 0.08006047829985619
0x1d : 0.08829088881611825
0x1e : 0.09216826036572458
0x1f : 0.0959075875580311
0x20 : 0.0999157913029194
0x21 : 0.10378383472561838
0x22 : 0.10752426460385324
0x23 : 0.11604240164160728
0x24 : 0.11942188814282419
0x25 : 0.12335999682545663
0x26 : 0.12780576199293137
0x27 : 0.13149937242269516
0x28 : 0.1350933536887169
0x29 : 0.1391696259379387
0x2a : 0.14312943071126938
0x2b : 0.15073328465223312
0x2c : 0.15490704029798508
0x2d : 0.15896772593259811
0x2e : 0.16292332857847214
0x2f : 0.16678082197904587
0x30 : 0.17054661363363266
0x31 : 0.17422639578580856
0x32 : 0.17871292680501938
0x33 : 0.18221714347600937
0x34 : 0.18984784930944443
0x35 : 0.19394680112600327
0x36 : 0.19795342534780502
0x37 : 0.20187317579984665
0x38 : 0.20571088045835495
0x39 : 0.20947109907865524
0x3a : 0.21388676017522812
0x3b : 0.21749045699834824
0x3c : 0.2217286452651024
0x3d : 0.22519230097532272
0x3e : 0.23326962441205978
0x3f : 0.23719196766614914
0x40 : 0.2410418912768364
0x41 : 0.24482300132513046
0x42 : 0.2491515800356865
0x43 : 0.2527943104505539
0x44 : 0.25696931779384613
0x45 : 0.2604866474866867
0x46 : 0.26509262621402746
0x47 : 0.26848720014095306
0x48 : 0.2729371637105942
0x49 : 0.27621997892856603
0x4a : 0.28052739799022675
0x4b : 0.2878845483064652
0x4c : 0.2919911295175553
0x4d : 0.2960308641195297
0x4e : 0.3000065237283707
0x4f : 0.3039207607507706
0x50 : 0.3077761381864548
0x51 : 0.31157501041889196
0x52 : 0.3153195530176163
0x53 : 0.31992693245410925
0x54 : 0.32355655729770666
0x55 : 0.32713808119297033
0x56 : 0.33155001699924475
0x57 : 0.33502940833568573
0x58 : 0.33931849896907806
0x59 : 0.3431235700845719
0x5a : 0.3468784242868424
0x5b : 0.3550512939691544
0x5c : 0.3590535074472428
0x5d : 0.3630023151636124
0x5e : 0.36689950525760656
0x5f : 0.37074701488018036
0x60 : 0.37454645335674286
0x61 : 0.37829931080341345
0x62 : 0.38274364173412323
0x63 : 0.38639952242374426
0x64 : 0.3900134414434433
0x65 : 0.3942967206239701
0x66 : 0.39782290160655975
0x67 : 0.4020042270421982
0x68 : 0.40613268315792084
0x69 : 0.40953396260738373
0x6a : 0.4135699421167374
0x6b : 0.41755788028240204
0x6c : 0.4214992970228195
0x6d : 0.4253955036401749
0x6e : 0.42924802005290985
0x6f : 0.43305812776088715
0x70 : 0.43745122849941254
0x71 : 0.4454674571752548
0x72 : 0.4491072744131088
0x73 : 0.45271067321300507
0x74 : 0.4568699449300766
0x75 : 0.4609823673963547
0x76 : 0.46447114646434784
0x77 : 0.46850042045116425
0x78 : 0.4724867194890976
0x79 : 0.47643129527568817
0x7a : 0.48033522069454193
0x7b : 0.48419974744319916
0x7c : 0.4880258589982987
0x7d : 0.49235282838344574
0x7e : 0.49610008299350744
0x7f : 0.49981193244457245
0x80 : 0.504012018442154
0x81 : 0.5081683099269867
0x82 : 0.511770099401474
0x83 : 0.5153402388095857
0x84 : 0.5193823873996736
0x85 : 0.5233851373195648
0x86 : 0.5273495018482209
0x87 : 0.5312764942646028
0x88 : 0.5351668894290925
0x89 : 0.5390217006206512
0x8a : 0.5428418219089509
0x8b : 0.5470990836620331
0x8c : 0.5513145625591278
0x8d : 0.5550274550914764
0x8e : 0.5587090551853181
0x8f : 0.5623598992824554
0x90 : 0.5668815076351166
0x91 : 0.5704658329486848
0x92 : -
0x93 : 0.5784274041652679
0x94 : 0.5823560655117035
0x95 : 0.586251050233841
0x96 : -
0x97 : 0.5939435064792634
0x98 : 0.5981624424457551
0x99 : -
0x9a : 0.6056618392467499
0x9b : -
0x9c : 0.6138570606708528
0x9d : 0.6179024875164032
0x9e : 0.6219141185283661
0x9f : 0.6250996291637421
0xa0 : 0.6329734027385712
0xa1 : -
0xa2 : 0.6368633806705476
0xa3 : -
0xa4 : 0.6453152596950531
0xa5 : -
0xa6 : 0.652876764535904
0xa7 : 0.6610659658908845
0xa8 : -
0xa9 : 0.6647453010082245
0xaa : 0.6683984696865082
0xab : -
0xac : 0.676345854997635
0xae : 0.6883957087993622
0xaf : 0.6918881237506868
0xb0 : 0.6960491836071014
0xb1 : -
0xb2 : 0.7001781165599823
0xb3 : 0.7035949528217317
0xb4 : 0.7076669037342073
0xb5 : 0.7117086350917816
0xb6 : -
0xb7 : 0.719703882932663
0xb8 : -
0xb9 : 0.7275852262973787
0xba : -
0xbb : 0.7353568971157074
0xbc : -
0xbd : 0.7428644001483918
0xbe : -
0xbf : 0.7510570585727693
0xc0 : 0.7547992765903474
0xc1 : -
0xc2 : 0.7628252804279329
0xc3 : -
0xc4 : 0.770742565393448
0xc5 : 0.7743611037731171
0xc6 : 0.7785550057888031
0xc7 : 0.782126396894455
0xc8 : 0.7862661778926849
0xc9 : 0.7903777062892914
0xca : 0.7938797175884247
0xcb : 0.7979399859905243
0xcc : 0.801973432302475
0xcd : 0.8059804737567903
0xce : 0.8099614083766937
0xcf : 0.8133535087108612
0xd0 : 0.8178474605083466
0xd1 : 0.8211969435214997
0xd2 : 0.82563516497612
0xd3 : 0.8294930160045624
0xd4 : 0.8333274424076081
0xd5 : 0.8371388614177704
0xd6 : 0.8409275710582733
0xd7 : 0.8452303111553192
0xd8 : 0.8489717543125154
0xd9 : 0.852691560983658
0xda : 0.8569168746471405
0xdb : 0.8611148893833162
0xdc : 0.8647662699222566
0xdd : 0.868397444486618
0xde : 0.8725230395793915
0xdf : 0.8766230642795564
0xe0 : 0.8806980550289154
0xe1 : 0.884243279695511
0xe2 : 0.8882721960544587
0xe3 : 0.8917779624462128
0xe4 : 0.8957622945308685
0xe5 : 0.8997233808040619
0xe6 : 0.9036616384983064
0xe7 : 0.9075773656368256
0xe8 : -
0xe9 : 0.9114709198474885
0xea : 0.9153425991535188
0xeb : 0.9196726381778718
0xec : 0.9234990179538728
0xed : 0.9277788698673249
0xee : 0.9315613806247712
0xef : 0.9353236258029939
0xf0 : 0.9427888095378876
0xf1 : 0.9474151432514192
0xf2 : 0.9510947763919831
0xf3 : -
0xf4 : 0.9547555744647981
0xf5 : 0.9588519632816316
0xf6 : 0.9629253447055818
0xf7 : 0.9665270149707795
0xf8 : 0.9705578386783601
0xf9 : 0.9745666086673738
0xfa : 0.9825190603733064
0xfb : -
0xfc : 0.9860263168811799
0xfd : 0.9903871715068817
0xfe : 0.9938577711582185
0xff : 0.9981732666492463

• In fact, I used 0.000000...001 step to test more than trillion of values, and that table is the result, there is no way to display some colors in that shader :(
– X Y
Commented Jul 30, 2023 at 16:17
• @X Y, i updated my answer, hope it helps! Commented Jul 31, 2023 at 0:51
• Thank you, this helps a lot!
– X Y
Commented Jul 31, 2023 at 11:49

Although you can't get 0.001 steps, the steps should be 1/255 = 0.0039... Try 1/255*5=0.0196..., or just use 5.0/255.0 to get 5 in the red channel.

This provides a simple test. Replace pixels in bpy image with X/255.0, then you can just hover over the images and see the RGB values, which are all int8 and not float.

show_image(bpy_image):
pixels = np.multiply(bpy_image.pixels, 255)
array = np.array(pixels, dtype=np.uint8)
array = np.reshape(array, (bpy_image.size[1], bpy_image.size[0], 4))
print(np.shape(array))
img = Image.fromarray(array)
imgplot = plt.imshow(img)
plt.show()

• Unfortunately this is not true, as stated by both OP and taiyo Commented Jul 29, 2023 at 23:37
• That isn't what Taiyo showed. He actually showed that 0.001 was smaller than one step. As expected, you see 0 for the first 2 steps. Then you see: 1 (0.0039...) for the next 4 steps, 2 for the next 4 steps (0.0078...) and so on. If he is using the shader and it continues to work that way, you should see the value for 5 (0.0195...) in some of the other steps. If the values are really skipped like X Y indicates, that is a different issue. Based on Taiyo's test, all 256 values should be possible. Commented Jul 30, 2023 at 5:57
• You're correct, I was so focused on very low precision that I missed the first value 0.003921568859368563 multiplied by 255 gives 1.000000059138983565. However it still doesn't solve the question, and setting the value to 1/255 (1/256 would be incorrect) doesn't allow to set the color to 0x01 Commented Jul 30, 2023 at 8:31
• Little but important typo: it should be 0.0039... not 0.039... Commented Jul 30, 2023 at 11:51
• 255 is actually correct -- see docs.blender.org/api/current/mathutils.html#mathutils.Color Commented Jul 30, 2023 at 15:12