Page MenuHome

PyAPI: GPU Shader: Humanize uniform_sampler parameters
Needs ReviewPublic

Authored by Germano Cavalcante (mano-wii) on Sep 2 2021, 12:59 AM.

Details

Summary

rB2aad8fc7bc2a introduced sampler enums in a crude and poorly documented way.

This patch proposes to expose these parameters in a safer and more documented way.

Description as seen in console:

>>> gpu.types.GPUShader.uniform_sampler(
uniform_sampler(name, texture, filter='NEAREST', repeat=[False, False, False], use_mipmap=False, clamp_to_border_color=False, compare_enabled=False)
.. method:: uniform_sampler(name, texture, filter='NEAREST', repeat=[False, False, False], use_mipmap=False, clamp_to_border_color=False, compare_enabled=False)
Specify the texture and state for an uniform sampler in the current GPUShader.
:param name: name of the uniform variable whose texture is to be specified.
:type name: str
:param texture: texture to attach.
:type texture: :class:`gpu.types.GPUTexture`
:param filter: filtering option which can be 'NEAREST', 'LINEAR' or 'ANISO'.
:type filter: str
:param repeat: Three-dimensional array specifying the addressing mode for outside [0..1] range for U, V and W coordinates.
:type repeat: tuple
:param use_mipmap: specify the mipmap filter to apply to lookups.
:type use_mipmap: bool
:param clamp_to_border_color: clamp to border color instead of border texel.
:type clamp_to_border_color: bool
:param compare_enabled: compare mode for depth formats.
:type compare_enabled: bool

Diff Detail

Repository
rB Blender
Branch
arcpatch-D12375 (branched from master)
Build Status
Buildable 16887
Build 16887: arc lint + arc unit

Event Timeline

Germano Cavalcante (mano-wii) requested review of this revision.Sep 2 2021, 12:59 AM
Germano Cavalcante (mano-wii) created this revision.
  • Remove unused define

This has the side effect that if a previously written code uses uniform_sampler it will now always override the texture internal sampler parameters which are set by default.
But since there is currently no possibility to modify the internal texture state using PyGPU, I would not consider this a major issue.

However, if we go this way, this means every texture bind must have all parameters explicitly set up otherwise you might have GL errors (linear filter on integer texture is Undefined Behavior if I recall correctly).

One way to avoid this would be to pass an opaque object constructed with the parameters you added. The object can be simply an int value containing the final enum. This object could be saved and passed when needed (much less verbose) and could be omitted using None to use the internal texture parameters (which we could expose at some point).

source/blender/python/gpu/gpu_py_shader.c
82

I think it wouldn't hurt to have the full name here.

This has the side effect that if a previously written code uses uniform_sampler it will now always override the texture internal sampler parameters which are set by default.
But since there is currently no possibility to modify the internal texture state using PyGPU, I would not consider this a major issue.

However, if we go this way, this means every texture bind must have all parameters explicitly set up otherwise you might have GL errors (linear filter on integer texture is Undefined Behavior if I recall correctly).

One way to avoid this would be to pass an opaque object constructed with the parameters you added. The object can be simply an int value containing the final enum. This object could be saved and passed when needed (much less verbose) and could be omitted using None to use the internal texture parameters (which we could expose at some point).

One thing that complicates this opaque object is that part of the texture state enums apparently cannot be flags.
What happens if the user sets "NEAREST", "LINEAR" and "ANISOTROPIC" in the same flag?
Maybe the state should be split into 2 parts:

  • texture.state_filter: Enum containing the "NEAREST", "LINEAR" and "ANISOTROPIC";
  • texture.state: Flag with the rest of the items are not mutually exclusive (repeat u, v and w; use_mipmap; clamp_to_border_color; compare_enabled).

It wouldn't be as clean as a single state parameter, but it would also make it a little less verbose:

uniform_sampler(name, texture, filter=texture.state_filter, state=texture.state)

My point is that it should not be a flag but an object with different properties. This way it's much cleaner to setup.

## Create a default state object ##
state_obj = bpy.gpu.sampler_state()

## Modify needed parameters ##
state_obj.filter = 'ANISOTROPIC'
state_obj.repeat = {True, False, False}
state_obj.compare_enabled = True

## Create a default state object ##
uniform_sampler(name, texture, state=state_obj)

I give you that this might be a bit more tricky to code.

It's a good idea, but I'm a little concerned about how to create this object.
The proposal was:

>>> state_obj = gpu.sampler_state()

But, by the convention of the gpu module, all object types are listed in gpu.types. So the new object would conventionally be gpu.types.GPUTextureState.
and initialized with:

>>> state_obj = gpu.types.GPUTextureState()

The problem with this solution is that the list of types is starting to get long:

>>> gpu.types.
              Buffer(
              GPUBatch(
              GPUFrameBuffer(
              GPUIndexBuf(
              GPUOffScreen(
              GPUShader(
              GPUTexture(
              GPUUniformBuf(
              GPUVertBuf(
              GPUVertFormat(

And we already have a type that is a little confusing: bpy.types.GPUVertFormat (this is for another topic).
So I would prefer to avoid new types :\


Maybe, if we really want to decrease verbose, we can just modify the default texture state and not add the new parameters to uniform_sampler:

## Create or get the GPUTexture ##
texture = gpu.texture.from_image(bpy_Image)

## Store previous state needed ##
previous_filter = texture.filter
previous_repeat = texture.repeat
previous_compare_enabled = texture.compare_enabled

## Modify needed parameters ##
texture.filter = 'ANISOTROPIC'
texture.repeat = (True, False, False)
texture.compare_enabled = True

## Set uniform sampler ##
shader.uniform_sampler(name, texture)

## Restore previous state ##
texture.filter = previous_filter
texture.repeat = previous_repeat
texture.compare_enabled = previous_compare_enabled

(I'm just not sure what would happen to the drawing if we change the state after setting the unform_sampler).

The issue with changing the texture parameters is that if the texture comes from Blender (i.e: image Texture) the state changes will propagate to the internals of blender. I would like to avoid this case.

Also the idiom of save and restore is quite the opposite of what we want to acheive with the GPU module. We aim for a stateless and explicit programing to avoid side effects with the internals of blender.

I don't really think adding a new type is really that bothersome. The user have to get familiar with it only if they needs to.

About the naming conventions you are right. I did not look them up when I was typing my example.

Also the constructor of gpu.types.GPUTextureState could very well contain the parameters.

I'm implementing this idea but there's something concerning...

The issue with changing the texture parameters is that if the texture comes from Blender (i.e: image Texture) the state changes will propagate to the internals of blender...

In that case, it doesn't seem like a good idea to expose the state of a GPUTexture at all. What would be the use of knowing if the default value of "clamp_to_border_color" for example if we can't change that value?
Maybe in python it's better to let uniform_sampler always overwrite default values. It's like the texture doesn't have default values.
Therefore, gpu.types.GPUTextureState would only be useful for the uniform_sampler(...) method. Which makes me wonder if creating a new object for a single method wouldn't be too much.

About the excess of parameters making the thing too verbose, maybe that's not too much of a problem. These parameters are optional anyway. And we even have other functions in the blender python API that have many parameters. (See bpy.types.UILayout.prop).

Therefore, gpu.types.GPUTextureState would only be useful for the uniform_sampler(...) method. Which makes me wonder if creating a new object for a single method wouldn't be too much.

This is a valid concern. The benefit of using an object is that it's easy to setup once and reuse, have error checking on assignment, and it replaces all states at once where distinct optional parameters will only override some (which is the issue I have with this patch current behavior).

Not allowing using the texture without de internal sampler will break compatibility of a lot of script and will require more boilerplate code for any texture usage since the default is no repeat and no filter.
I do agree that exposing the internal sampler parameter is not something I think is necessary.

I'm all for keeping a long list of args but it needs to have a way to use the internal sampler parameters and override all sampling param at once in a nice way. Maybe we could just make another version of the function but that's not very pythonic.

Germano Cavalcante (mano-wii) marked an inline comment as done.
  • Rename ANISO -> ANISOTROPIC
  • draw_texture_2d: pass the NEAREST filter in the uniform_sampler parameter

(...) The benefit of using an object is that it's easy to setup once and reuse, have error checking on assignment, and it replaces all states at once where distinct optional parameters will only override some (which is the issue I have with this patch current behavior).
(...)

It's not common, but in python we can pass the keyword parameters through a dict:

def draw():
    shader.bind()

    kwds = {"filter":'LINEAR', "repeat":(True, True, True)}
    shader.uniform_sampler("image", texture, **kwds)
    batch.draw(shader)

So we can use the same parameters in different uniform_sampler calls.

Error checking in the method itself may be enough to avoid errors. In the patch, this error checking was implemented:
"GPUShader.uniform_sampler: Only depth formats does support compare mode."

More error checking can be added if needed. (Due to limitations in the C API, I was unable to make an integer texture to test the LINEAR filter).

Not allowing using the texture without de internal sampler will break compatibility of a lot of script and will require more boilerplate code for any texture usage since the default is no repeat and no filter.

I looked at the addons that come with Blender and noticed that they all use the draw_texture_2d utility to draw textures. So I just edited this utility to avoid breakage.

  • draw_texture_2d: use LINEAR instead of NEAREST

I looked at the addons that come with Blender and noticed that they all use the draw_texture_2d utility to draw textures. So I just edited this utility to avoid breakage.

Avoiding breakage only for our addons is not enough to me. This also complicates the usage of the API for simple scripts. Also depth texture do not have filter by default.

What if the texture given to draw_texture_2d is an integer texture (not currently doable but could totally be in the future)?

The suggestion of using a dictionary seems okayish. It's just not very pretty.

Maybe @Campbell Barton (campbellbarton) will have something to say about this.