Materials, Multiple Render Pipeline Descriptors and States (Metal Part 13)

z4gon
z4gon

Defining materials with different vertex, fragment functions and attributes. Maintaining multiple render pipeline descriptors and states to match the different materials being used. Passing values to the GPU from the materials. Setting the corresponding render pipeline state to the render command encoder, based on the current material attached to the mesh renderer.

Cover Image for Materials, Multiple Render Pipeline Descriptors and States (Metal Part 13)

Source Code

See Project in GitHub 👩‍💻

References

Table of Content


Material

The materials will have references to the names of the functions used for the Vertex Shader and Fragment Shader stages.

1class Material { 2 public var vertexFunctionName: String = VertexFunctionNames.Basic 3 public var fragmentFunctionName: String = FragmentFunctionNames.VertexColor 4 5 public var renderPipelineStateId: String { 6 return "\(vertexFunctionName)/\(fragmentFunctionName)" 7 } 8 9 func setGpuValues() {} 10}

Specific instances of Materials, will also optionally have other attributes.

And also will set values to the GPU via the Render Command Encoder, using .setVertexBytes and .setFragmentBytes.

1class FillColorMaterial: Material { 2 3 private var _color: float4 = float4(repeating: 0) 4 5 init(_ color: float4) { 6 super.init() 7 fragmentFunctionName = FragmentFunctionNames.FillColor 8 _color = color 9 } 10 11 func setColor(_ color: float4) { 12 _color = color 13 } 14 15 override func setGpuValues() { 16 Graphics.renderCommandEncoder.setFragmentBytes(&_color, length: float4.stride, index: 1) 17 } 18}

MTLRenderPipelineDescriptor

The render pipeline descriptor will use the functions defined in the material, which will be reused for any instances of Materials that use the same combination of Vertex/Fragment shaders.

1public struct RenderPipelineDescriptor{ 2 ... 3 4 init(material: Material){ 5 6 // create the descriptor for the render pipeline 7 _renderPipelineDescriptor = MTLRenderPipelineDescriptor() 8 9 ... 10 11 _renderPipelineDescriptor.vertexFunction = VertexShaderCache.get(material.vertexFunctionName) 12 _renderPipelineDescriptor.fragmentFunction = FragmentShaderCache.get(material.fragmentFunctionName) 13 14 ... 15 } 16}

There will also be a render pipeline state associated to the combination of vertex and fragment functions used by the material.

1public struct RenderPipelineState { 2 var renderPipelineState: MTLRenderPipelineState! 3 4 init(material: Material){ 5 do{ 6 renderPipelineState = try Engine.device.makeRenderPipelineState(descriptor: RenderPipelineDescriptorCache.get(material)) 7 }catch let error as NSError { 8 print("ERROR::CREATE::RENDER_PIPELINE_STATE::__\(material.renderPipelineStateId)__::\(error)") 9 } 10 } 11}

Mesh Renderer

The mesh renderer will now configure the render command encoder to use the appropriate render pipeline state, according to the associated material.

Also, before rendering, it will execute the material function to setGpuValues()

1class MeshRenderer : Component, Renderable, LateUpdatable { 2 3 ... 4 private var _material: Material! 5 6 init(mesh: Mesh, material: Material) { 7 ... 8 9 _material = material 10 } 11 12 ... 13 14 func doRender() { 15 16 Graphics.renderCommandEncoder.setRenderPipelineState(RenderPipelineStateCache.get(_material)) 17 18 ... 19 20 _material.setGpuValues() 21 22 ... 23 } 24}

Shader

The metal shader can now pickup the passed in values directly off of the buffers.

1fragment half4 fill_color_fragment_shader( 2 const FragmentData IN [[ stage_in ]], 3 constant float4 &color [[ buffer(1) ]] 4){ 5 return half4(color.r, color.g, color.b, color.a); 6}

These values were set by the material earlier:

1override func setGpuValues() { 2 Graphics.renderCommandEncoder.setFragmentBytes(&_color, length: float4.stride, index: 1) 3}

Result

Now the quad is using a different fragment function than the cube, because the render command encoder is using different render pipeline states.

Picture