How do function constants work with textureBuffer? (Chapter 15 GPU driven rendering)

In chapter 15, we learned how to use texture buffer to group textures, such as baseColor, normalTexture, ao, etc.
However, in the fragment function, the tutorial didn’t show us how to check whether a particular texture exists. For example, how do we know if the model provided a normal texture? We used to set function constants just for this purpose, but with texture buffer that group all textures, how do we achieve it?

I was not able to get function constants working for chapter 15.

Rather than use conditionals, or create multiple shaders, my choice is to create 1 pixel default textures to use for those models that don’t have those textures.

The color texture depends on what you need to do. For most, it would be white, as that’s multiplying by 1. For normals, you’d need (0.5, 0.5, 1).

Hello Caroline,
thanks for the reply.
How did you sample the 1 pixel texture in the shader function instead of using UV coordinates? e.g. textures.baseTexture.sample(textureSampler, in.uv);

You still use uv coordinates, but the 1 (4?) pixel shader stretches over the surface

These are some of the samplers you can use btw, from Chapter 6, “Textures”

Aha! that’s a good idea! Thanks Caroline!

1 Like

Hi Miraco -

I have also been working on something like this. I’m not sure if any of what I have to say will be helpful, but I managed to get them working. It was not easy to say the least. Here are a few of the things I learned in the process:

Function Constants Create Different Functions
Originally, when I was using function constants, I did not realize that you get back a different function for every function constant configuration. That means you also have a different pipeline state for each function variation you create. When I was trying this at first, I just kept one function under the assumption that the object I was holding held all the specialized variants. But from what I can tell, that’s not the case. If you specialize a fragment shader to read a diffuse texture and another to read a diffuse and a normal texture, but you only create a pipeline state with the first variation, your pipeline state will always expect a diffuse texture and will never expect a normal texture.

Function Constants are Independent of Your Resources
I also thought that FCs were tied to the resources directly in some fashion. In experimenting, what I found is that your functions expect to be specialized if you use an if statement based on a function constant declared in your .metal file. As an experiment, you can declare an FC in your shader, set it to false then try to run your code. It should fail because now you have a FC in your shader and the shader requires you to specialize it.

One trick I used was to create a new function constant based on the others declared in the metal shader. So, my shader might set FCs to indicate that I may or may not have a diffuse texture, normal texture, etc. Then in the .metal file, I can check if I have a texture at all by seeing if all of the FCs are true. When your code calls the specialized function, the FCs will be set based on how you specialized it.

Consider Using a Function to Set Your FCs
One thing I found out, is that, pretty quickly, the combinations of textures that you may or may not have becomes unmanageable to track manually. I took my queue from the code in the Function Specialization Chapter and used a variation of the sample code to check what my FCs should be before I create my shader. That way, I don’t have to figure out every possible combination of textures.

Hope that helps. Good luck have fun!

2 Likes

Hi Iducot2,
thanks a lot for sharing your findings.

In the case where there are 4 FCs, will there be 2^4 = 16 functions created?

Yes, but only if you create all variations. In my code, for example, I have diffuse, normal, emissive, and orm as possible textures in a struct. In my metal file, I have 4 FCs and 1 more FC that is true if at least one of the other 4 are true.

When I start to use the metal library to build my fragment shader, I call a function like the one in the book that checks the model for the texture. If the texture is present, the constant I use in my fragment function is set to true. Otherwise, set to false.

By doing this, I avoid creating all 16 variations of the fragment shader. Most of the models I experiment with only have diffuse and normal, so I usually only build about 3 of the variations (no textures, diffuse only, diffuse and normal only).

Then I create a different pipeline state object for each of my 3 specialized fragment shaders. This is the part that really tripped me up at first. You have to set the correct pipeline state object so the correct variation of your shader is called. In my early attempts, I just kept calling one pipeline that only held the diffuse version of the shader. Consequently, none of my other textures were getting through the pipeline. The “false branch” was the only thing executing for normal/emission/orm etc

In case it helps, for my particular project, I found that an option set really helps track everything. I added a pipeline key object to my model that is a combination of vertex and fragment function options, so I can hold all the pipelines in a dictionary and determine which variation it is by seeing if the dictionary key matches the diffuse/normal/orm option, etc.

1 Like

Thank you for your response. That sounds an excellent way to handle things.
Since pipeline state management is tricky, and holding small default textures is easy, I’ve gone off function constants. I haven’t done any efficiency tests though, and haven’t loaded huge scenes.

Is using function constant efficient?

Setting up function constants would be done at app initialisation. Once they are set up, it would be as efficient as calling any other shader function. And much more efficient than having conditionals in your shader, which you should avoid.

1 Like

Hi Iducot2,
Assume that all model might have a base color texture and a normal texture, then here are the 4 scenarios:

  1. No base Color, no normal
  2. No base color, has normal
  3. Has base color, no normal
  4. Has base color, has normal

For your project, scenario 1 & 4 covers all possibilities, so you create a FC that equals to 1 if the model fits scenario 1, and equals to 0 if the model fits scenario 4. This way, you avoided 2 of the 4 possible combinations. Is this similar to your solution?

What did you have to create different pipeline states if you use FCs? Isn’t the purpose of FC to allow you to specialize in different scenarios in one shader function?

Yes, that is close to what I did. In the .metal file I have something along the lines of:

bool has_diffuse [[ function_constant(DiffuseIndex) ]];
bool has_normal [[ function_constant(NormalIndex) ]];

In my start up phase, when I create the functions, I use MTLFunctionConstantValues to set each one to true or false. So let’s say Model A has a diffuse and normal texture. In that case, I set two values in MTLFunctionConstantValues — one at the DiffuseIndex and another at the NormalIndex.

That gets me one variation of my fragment shader. Let’s call it frag1. I then go on to create a render pipeline state using frag1. When I render Model A, I need to use this pipeline state so that has_diffuse and has_normal are both set to true.

Let’s say Model B doesn’t have a texture at all. In that case, right after creating the pipeline for frag1, I reset the MTLFunctionConstantValues object and set the values at each index to false. This gives me frag2.

In .metal, the shader is exactly the same from our perspective. But behind the scenes, Metal creates a totally separate function for you. So it is doing something along the lines of taking this:

fragment float4 shader(ColorIn in [[stage_in]])
{
if(has_diffuse) { work with diffuse texture }
if(has_normal) { work with normal texture }
. . . do other calculations . . .
}

And turning it into this for Model A:

fragment float4 shader(ColorIn in [[stage_in]])
{
work with diffuse texture
work with normal texture
. . . do other calculations . . .
}

And this for Model B, which has no textures:

fragment float4 shader(ColorIn in [[stage_in]])
{
. . . do other calculations . . .
}

But, in order for Metal to know which of the two variations to use, you need to have two different render pipeline states just like you would if you had written two completely separate functions. You could actually write two completely separate functions, which as I understand it from the book is a common solution along with using macros to create different function variations.

wow, that’s excellent.

And in code, it will be the same as the code from Chapter 7, right?

static func makeFunctionConstants(textures: Textures)
    -> MTLFunctionConstantValues {

      let functionConstants = MTLFunctionConstantValues()

      var property = textures.baseColor != nil
      functionConstants.setConstantValue(&property, type: .bool, index: 0)

      property = textures.normal != nil
      functionConstants.setConstantValue(&property, type: .bool, index: 1)

      return functionConstants
  }

  static func makePipelineState(textures: Textures) -> MTLRenderPipelineState {
    let functionConstants = makeFunctionConstants(textures: textures)
   
    let fragmentFunction: MTLFunction?
    fragmentFunction = try library?.makeFunction(name: "fragment_mainPBR",
                                                   constantValues: functionConstants)
    
    var pipelineState: MTLRenderPipelineState

    /// ... create pipelinState

    return pipelineState
  }