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
from gpu_extras.batch import batch_for_shader
indices = (
(0, 1, 2), (2, 1, 3))
shader = gpu.shader.from_builtin('UNIFORM_COLOR')
batch = []
for i in range(1000):
vertices = (
(10 + i, 100), (10 + i+1, 100),
(10 + i, 200), (10 + i+1, 200))
batch.append(batch_for_shader(shader, 'TRIS', {"pos": vertices}, indices=indices))
pixels = []
for i in range(1000):
pixels.append(None)
def draw():
for i in range(1000):
c = i * 0.001
shader.uniform_float("color", (c,c,c, 1.0))
batch[i].draw(shader)
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])
bpy.types.SpaceView3D.draw_handler_add(draw, (), 'WINDOW', 'POST_PIXEL')
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
from gpu_extras.batch import batch_for_shader
shader = gpu.shader.from_builtin('UNIFORM_COLOR')
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))
batches.append(batch_for_shader(shader, 'TRIS', {"pos": vertices}, indices=indices))
def draw():
for i in range(256):
c = i / 255.0 # come on, all hex values plz
#shader.uniform_bool("srgbTarget", False)
shader.uniform_float("color", (c,c,c, 1.0))
batches[i].draw(shader)
bpy.types.SpaceView3D.draw_handler_add(draw, (), 'WINDOW', 'POST_PIXEL')
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
from gpu_extras.batch import batch_for_shader
# create the 'UNIFORM COLOR' shader ourself
shader_info = gpu.types.GPUShaderCreateInfo()
shader_info.push_constant('MAT4', "viewProjectionMatrix")
shader_info.push_constant('FLOAT', "color")
shader_info.vertex_in(0, 'VEC3', "position")
shader_info.fragment_out(0, 'VEC4', "FragColor")
shader_info.vertex_source(
"void main()"
"{"
" gl_Position = viewProjectionMatrix * vec4(position, 1.0f);"
"}"
)
shader_info.fragment_source(
"void main()"
"{"
" float c = color / 255.0;"
" FragColor = vec4(c, c, c, 1.0);"
"}"
)
shader = gpu.shader.create_from_info(shader_info)
del shader_info
# 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)]
batches.append(batch_for_shader(shader, 'TRIS', {"position": coords}))
# 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]
shader.uniform_float("viewProjectionMatrix", Matrix.Identity(4))
for i in range(256):
shader.uniform_float("color", i)
batches[i].draw(shader)
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: