UVs, Loading and Sampling Textures (Metal Part 14)

z4gon
z4gon

Defining the UV coordinates in the vertices of the mesh, and also in the GPU structs used in the Vertex and Fragment functions. Initializing MTLTextures and MTLSamplerStates to handle texture sampling. Passing textures and samplers to the GPU from the material. Accessing the texture and sampler in the corresponding memory blocks in the GPU and sampling the texels using the UV coordinates in the fragment shader function.

Source Code

See Project in GitHub 👩‍💻

References

Table of Content


Vertex

We need to update the class that represents the vertices in the mesh by adding a new float2 to represent the uv coordinates.

1struct Vertex: sizeable{ 2 var position: float3 3 var color: float4 4 var uv: float2 5 6 init( 7 position: float3, 8 color: float4 = float4(repeating: 0), 9 uv: float2 = float2(repeating: 0) 10 ) { 11 self.position = position 12 self.color = color 13 self.uv = uv 14 } 15}

Similarly, we also need to update the MTLVertexDescriptor.

1public struct BasicVertexDescriptor : VertexDescriptor{ 2 ... 3 4 init(){ 5 ... 6 7 // uv 8 vertexDescriptor.attributes[2].format = MTLVertexFormat.float2 9 vertexDescriptor.attributes[2].bufferIndex = 0 10 vertexDescriptor.attributes[2].offset = float3.size + float4.size 11 12 ... 13 } 14}

Finally, we also need to update the VertexData and FragmentData structs in the GPU side, and update the basic_vertex_shader to pass along the uvs to the fragments.

Structs

1struct VertexData { 2 float3 position [[ attribute(0) ]]; 3 float4 color [[ attribute(1) ]]; 4 float2 uv [[ attribute(2) ]]; 5}; 6 7struct FragmentData { 8 // use position attribute to prevent interpolation of the value 9 float4 position [[ position ]]; 10 float4 color; 11 float2 uv; 12 float time; 13};

Vertex Shader Function

1vertex FragmentData basic_vertex_shader( 2 // metal can infer the data because we are describing it using the vertex descriptor 3 const VertexData IN [[ stage_in ]], 4 constant ModelConstants &modelConstants [[ buffer(1) ]], 5 constant SceneConstants &sceneConstants [[ buffer(2) ]] 6){ 7 FragmentData OUT; 8 9 ... 10 11 OUT.uv = IN.uv; 12 13 ... 14 15 return OUT; 16}

Mesh

We update the Quad mesh to have uv coordinates.

1class QuadMesh : Mesh{ 2 override func createMesh() { 3 vertices = [ 4 Vertex(position: float3( 0.5, 0.5,0), uv: float2(1,0)), //Top Right 5 Vertex(position: float3(-0.5, 0.5,0), uv: float2(0,0)), //Top Left 6 Vertex(position: float3(-0.5,-0.5,0), uv: float2(0,1)), //Bottom Left 7 Vertex(position: float3( 0.5,-0.5,0), uv: float2(1,1)) //Bottom Right 8 ] 9 10 indices = [ 11 0,1,2, 12 0,2,3 13 ] 14 } 15}

Basic UVs Shader

Right now we can define a simple fragment shader to display the uv coordinates colors, and also animate it using the global game time.

1fragment half4 uvs_fragment_shader( 2 const FragmentData IN [[ stage_in ]] 3){ 4 return half4( 5 sin(IN.uv.x + IN.time), 6 sin(IN.uv.y + IN.time), 7 0, 8 1 9 ); 10}

UVs Gradient

Picture


Texture

We will create a cache to store built in textures, using MTLTexture and MTKTextureLoader.

1class TextureLoader { 2 private var _name: String! 3 private var _fileExtension: String! 4 private var _origin: MTKTextureLoader.Origin 5 6 ... 7 8 public func load()->MTLTexture { 9 var result: MTLTexture! 10 11 if let url = Bundle.main.url(forResource: _name, withExtension: _fileExtension) { 12 13 let loader = MTKTextureLoader(device: Engine.device) 14 let options: [MTKTextureLoader.Option: Any] = [MTKTextureLoader.Option.origin : _origin] 15 16 do { 17 result = try loader.newTexture(URL: url, options: options) 18 result.label = _name 19 ... 20 21 return result 22 } 23}

Sampler

Similarly, we will keep a cache of MTLSamplerState to use when sampling the textures.

Linear means the algorithm that will be use when sampling textures, when the object is minified or magnified on screen.

1class LinearSamplerState : SamplerState { 2 3 override init() { 4 super.init() 5 6 let samplerDescriptor = MTLSamplerDescriptor() 7 8 samplerDescriptor.minFilter = .linear 9 samplerDescriptor.magFilter = .linear 10 11 samplerState = Engine.device.makeSamplerState(descriptor: samplerDescriptor) 12 } 13}

Material

The material will be in charge of passing the Texture and the SamplerState to the GPU before rendering.

1class TextureSampleMaterial: Material { 2 3 private var _textureType: BuiltInTexture! 4 5 init(_ textureType: BuiltInTexture) { 6 super.init() 7 fragmentFunctionName = FragmentFunctionNames.TextureSample 8 setTextureType(textureType) 9 } 10 11 func setTextureType(_ textureType: BuiltInTexture) { 12 _textureType = textureType 13 } 14 15 override func setGpuValues() { 16 Graphics.renderCommandEncoder.setFragmentSamplerState(SamplerStateCache.get(.Linear), index: 0) 17 Graphics.renderCommandEncoder.setFragmentTexture(TextureCache.get(_textureType), index: 0) 18 } 19}

The Game Object initializes the MeshRenderer with the corresponding Material and Texture.

1let mesh = MeshCache.get(.Quad) 2let material = TextureSampleMaterial(.MonaLisa) 3 4self.addComponent(MeshRenderer(mesh: mesh, material: material))

Shader

In the Metal Shader code, we will access the Texture and SamplerState from the corresponding blocks of memory.

sampler(0) and texture(0) mean that we will access the desired elements at the specified indexes.

1fragment half4 texture_sample_fragment_shader( 2 const FragmentData IN [[ stage_in ]], 3 4 // sampler and texture2d coming in their corresponding memory blocks 5 sampler sampler2d [[ sampler(0) ]], 6 texture2d<float> texture [[ texture(0) ]] 7){ 8 float4 color = texture.sample(sampler2d, IN.uv); 9 return half4(color.r, color.g, color.b, color.a); 10}

Result

The Quad now renders the texture using the UV texture coordinates.

Picture