Vulkan Tutorial in C - 010 - Adding Textures Part 2
Right after the app initialization section where we defined the shader stage create infos, we'll create the descriptor system. Locate the part of the code where it says "Define Shader Stage Create Info" and, after that, begin adding the following code.
In this tutorial, I'll be using a simple 2x2 texture with red and semi-transparent black pixels in a checkerboard pattern that I defined myself:
If you encounter any issues, please let me know in the comments. You can also find the full source code for where we’re at here, so you can compare it with your code if it's not working.
In the next tutorials, we’ll move on to adding vertex and index buffers to the app. See you there!
Next
Create the Descriptor Set Layout
VkDescriptorSetLayoutBinding descSetLayoutBinding =
{
0, // binding
VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
1, // descriptorCount
VK_SHADER_STAGE_FRAGMENT_BIT,
NULL // pImmutableSamplers
};
VkDescriptorSetLayoutCreateInfo descSetLayoutInfo =
{
VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO,
NULL,
0,
1, // bindingCount
&descSetLayoutBinding
};
if (vkCreateDescriptorSetLayout(vk.device, &descSetLayoutInfo, NULL,
&descSetLayout) != VK_SUCCESS)
{
assert(!"Failed to create descriptor set layout!");
}
{
0, // binding
VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
1, // descriptorCount
VK_SHADER_STAGE_FRAGMENT_BIT,
NULL // pImmutableSamplers
};
VkDescriptorSetLayoutCreateInfo descSetLayoutInfo =
{
VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO,
NULL,
0,
1, // bindingCount
&descSetLayoutBinding
};
if (vkCreateDescriptorSetLayout(vk.device, &descSetLayoutInfo, NULL,
&descSetLayout) != VK_SUCCESS)
{
assert(!"Failed to create descriptor set layout!");
}
Create the Descriptor Pool
VkDescriptorPoolSize descPoolSize =
{
VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
1 // descriptorCount
};
VkDescriptorPoolSize descPoolSizes[] = { descPoolSize };
VkDescriptorPoolCreateInfo descPoolInfo =
{
VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO,
NULL,
0,
1, // maxSets
array_count(descPoolSizes),
descPoolSizes
};
if (vkCreateDescriptorPool(vk.device, &descPoolInfo, NULL,
&descPool) != VK_SUCCESS)
{
assert(!"Failed to create descriptor pool!");
}
{
VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
1 // descriptorCount
};
VkDescriptorPoolSize descPoolSizes[] = { descPoolSize };
VkDescriptorPoolCreateInfo descPoolInfo =
{
VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO,
NULL,
0,
1, // maxSets
array_count(descPoolSizes),
descPoolSizes
};
if (vkCreateDescriptorPool(vk.device, &descPoolInfo, NULL,
&descPool) != VK_SUCCESS)
{
assert(!"Failed to create descriptor pool!");
}
Allocate the Descriptor Set
VkDescriptorSetLayout descSetLayouts[] = { descSetLayout };
VkDescriptorSetAllocateInfo descSetAllocInfo =
{
VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO,
NULL,
descPool,
array_count(descSetLayouts),
descSetLayouts
};
if (vkAllocateDescriptorSets(vk.device, &descSetAllocInfo,
&descSet) != VK_SUCCESS)
{
assert(!"Failed to allocate descriptor set!");
}
VkDescriptorSetAllocateInfo descSetAllocInfo =
{
VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO,
NULL,
descPool,
array_count(descSetLayouts),
descSetLayouts
};
if (vkAllocateDescriptorSets(vk.device, &descSetAllocInfo,
&descSet) != VK_SUCCESS)
{
assert(!"Failed to allocate descriptor set!");
}
Define Texture Data
Loading texture data into C code is beyond the scope of this tutorial. If you're unsure how to do it, I recommend using the stb_image.h library by Sean Barrett. It's easy to use, well-made, and widely used, so you'll find many tutorials on how to work with it.In this tutorial, I'll be using a simple 2x2 texture with red and semi-transparent black pixels in a checkerboard pattern that I defined myself:
u32 texData[] =
{
0xCC000000, 0xFF0000FF,
0xFF0000FF, 0xCC000000
};
VkDeviceSize texDataSize = sizeof(texData);
{
0xCC000000, 0xFF0000FF,
0xFF0000FF, 0xCC000000
};
VkDeviceSize texDataSize = sizeof(texData);
Create the Staging Buffer
VkBuffer texStagingBuffer;
VkDeviceMemory texStagingBufferMemory;
VkBufferCreateInfo textureStagingBufferInfo =
{
VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO,
NULL,
0,
texDataSize,
VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
VK_SHARING_MODE_EXCLUSIVE,
0, NULL // queue families ignored
};
if (vkCreateBuffer(vk.device, &textureStagingBufferInfo, NULL,
&texStagingBuffer) != VK_SUCCESS)
{
assert(!"Failed to create texture staging buffer!");
}
VkDeviceMemory texStagingBufferMemory;
VkBufferCreateInfo textureStagingBufferInfo =
{
VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO,
NULL,
0,
texDataSize,
VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
VK_SHARING_MODE_EXCLUSIVE,
0, NULL // queue families ignored
};
if (vkCreateBuffer(vk.device, &textureStagingBufferInfo, NULL,
&texStagingBuffer) != VK_SUCCESS)
{
assert(!"Failed to create texture staging buffer!");
}
Allocate Memory for the Staging Buffer
{
VkMemoryRequirements memRequirements = { 0 };
vkGetBufferMemoryRequirements(vk.device, texStagingBuffer,
&memRequirements);
u32 memoryTypeIndex =
vk_find_memory_type(&vk, memRequirements.memoryTypeBits,
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
VkMemoryAllocateInfo memAllocInfo =
{
VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO,
NULL,
memRequirements.size,
memoryTypeIndex
};
if (vkAllocateMemory(vk.device, &memAllocInfo, NULL,
&texStagingBufferMemory) != VK_SUCCESS)
{
assert(!"Failed to allocate staging buffer memory!");
}
}
Note that I wrapped this bit of code in its own scope block to avoid leaking memAllocInfo. I'll reuse this name later, which is the only reason I did this.
VkMemoryRequirements memRequirements = { 0 };
vkGetBufferMemoryRequirements(vk.device, texStagingBuffer,
&memRequirements);
u32 memoryTypeIndex =
vk_find_memory_type(&vk, memRequirements.memoryTypeBits,
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
VkMemoryAllocateInfo memAllocInfo =
{
VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO,
NULL,
memRequirements.size,
memoryTypeIndex
};
if (vkAllocateMemory(vk.device, &memAllocInfo, NULL,
&texStagingBufferMemory) != VK_SUCCESS)
{
assert(!"Failed to allocate staging buffer memory!");
}
}
Bind the buffer to the allocated memory
vkBindBufferMemory(vk.device, texStagingBuffer,
texStagingBufferMemory, 0);
texStagingBufferMemory, 0);
Map the Buffer Memory and Copy Data into it
void *mappedData = 0;
vkMapMemory(vk.device, texStagingBufferMemory, 0, texDataSize, 0,
&mappedData);
memcpy(mappedData, texData, texDataSize);
vkUnmapMemory(vk.device, texStagingBufferMemory);
vkMapMemory(vk.device, texStagingBufferMemory, 0, texDataSize, 0,
&mappedData);
memcpy(mappedData, texData, texDataSize);
vkUnmapMemory(vk.device, texStagingBufferMemory);
Create Texture Image
VkExtent3D imageExtent =
{
2, // width
2, // height
1 // depth
};
VkImageCreateInfo imageInfo =
{
VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO,
NULL,
0,
VK_IMAGE_TYPE_2D,
VK_FORMAT_R8G8B8A8_SRGB,
imageExtent,
1, // mipLevels
1, // arrayLayers
VK_SAMPLE_COUNT_1_BIT,
VK_IMAGE_TILING_OPTIMAL,
VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT,
VK_SHARING_MODE_EXCLUSIVE,
0, NULL, // queue families ignored
VK_IMAGE_LAYOUT_UNDEFINED
};
if (vkCreateImage(vk.device, &imageInfo, NULL,
&texImage) != VK_SUCCESS)
{
assert(!"Failed to create image");
}
{
2, // width
2, // height
1 // depth
};
VkImageCreateInfo imageInfo =
{
VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO,
NULL,
0,
VK_IMAGE_TYPE_2D,
VK_FORMAT_R8G8B8A8_SRGB,
imageExtent,
1, // mipLevels
1, // arrayLayers
VK_SAMPLE_COUNT_1_BIT,
VK_IMAGE_TILING_OPTIMAL,
VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT,
VK_SHARING_MODE_EXCLUSIVE,
0, NULL, // queue families ignored
VK_IMAGE_LAYOUT_UNDEFINED
};
if (vkCreateImage(vk.device, &imageInfo, NULL,
&texImage) != VK_SUCCESS)
{
assert(!"Failed to create image");
}
Allocate Memory for the Texture Image
{
VkMemoryRequirements memRequirements = { 0 };
vkGetImageMemoryRequirements(vk.device, texImage,
&memRequirements);
VkMemoryAllocateInfo memAllocInfo =
{
VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO,
NULL,
memRequirements.size,
vk_find_memory_type(&vk, memRequirements.memoryTypeBits,
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT)
};
if (vkAllocateMemory(vk.device, &memAllocInfo, NULL,
&texImageMemory) != VK_SUCCESS)
{
assert(!"Failed to allocate texture image memory!");
}
// Bind the image to the allocated memory
vkBindImageMemory(vk.device, texImage, texImageMemory, 0);
}
VkMemoryRequirements memRequirements = { 0 };
vkGetImageMemoryRequirements(vk.device, texImage,
&memRequirements);
VkMemoryAllocateInfo memAllocInfo =
{
VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO,
NULL,
memRequirements.size,
vk_find_memory_type(&vk, memRequirements.memoryTypeBits,
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT)
};
if (vkAllocateMemory(vk.device, &memAllocInfo, NULL,
&texImageMemory) != VK_SUCCESS)
{
assert(!"Failed to allocate texture image memory!");
}
// Bind the image to the allocated memory
vkBindImageMemory(vk.device, texImage, texImageMemory, 0);
}
Begin Single Time Command Buffer
VkCommandBuffer texCommandBuffer = vk_begin_single_time_commands(&vk);
Define the Subresource Range
VkImageSubresourceRange subResRange =
{
VK_IMAGE_ASPECT_COLOR_BIT,
0, // baseMipLevel
1, // levelCount
0, // baseArrayLayer
1, // layerCount
};
{
VK_IMAGE_ASPECT_COLOR_BIT,
0, // baseMipLevel
1, // levelCount
0, // baseArrayLayer
1, // layerCount
};
Change Image Layout to Transfer using a barrier
{
VkImageMemoryBarrier barrier =
{
VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
NULL,
0, // srcAccessMask
VK_ACCESS_TRANSFER_WRITE_BIT, // dstAccessMask
VK_IMAGE_LAYOUT_UNDEFINED, // oldLayout
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, // newLayout
VK_QUEUE_FAMILY_IGNORED, // srcQueueFamilyIndex
VK_QUEUE_FAMILY_IGNORED, // dstQueueFamilyIndex
texImage,
subResRange
};
VkImageMemoryBarrier barriers[] = { barrier };
vkCmdPipelineBarrier(texCommandBuffer,
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
VK_PIPELINE_STAGE_TRANSFER_BIT,
0, 0, NULL, 0, NULL,
array_count(barriers),
barriers);
}
VkImageMemoryBarrier barrier =
{
VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
NULL,
0, // srcAccessMask
VK_ACCESS_TRANSFER_WRITE_BIT, // dstAccessMask
VK_IMAGE_LAYOUT_UNDEFINED, // oldLayout
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, // newLayout
VK_QUEUE_FAMILY_IGNORED, // srcQueueFamilyIndex
VK_QUEUE_FAMILY_IGNORED, // dstQueueFamilyIndex
texImage,
subResRange
};
VkImageMemoryBarrier barriers[] = { barrier };
vkCmdPipelineBarrier(texCommandBuffer,
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
VK_PIPELINE_STAGE_TRANSFER_BIT,
0, 0, NULL, 0, NULL,
array_count(barriers),
barriers);
}
Copy texture data from Staging Buffer to Image
VkImageSubresourceLayers subResLayers =
{
VK_IMAGE_ASPECT_COLOR_BIT,
0, // mipLevel
0, // baseArrayLayer
1 // layerCount
};
VkBufferImageCopy imageCopy =
{
0, // bufferOfsset
0, // bufferRowLength
0, // bufferImageHeight
subResLayers,
{0, 0, 0},
imageExtent
};
vkCmdCopyBufferToImage(texCommandBuffer,
texStagingBuffer,
texImage,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
1,
&imageCopy);
{
VK_IMAGE_ASPECT_COLOR_BIT,
0, // mipLevel
0, // baseArrayLayer
1 // layerCount
};
VkBufferImageCopy imageCopy =
{
0, // bufferOfsset
0, // bufferRowLength
0, // bufferImageHeight
subResLayers,
{0, 0, 0},
imageExtent
};
vkCmdCopyBufferToImage(texCommandBuffer,
texStagingBuffer,
texImage,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
1,
&imageCopy);
Change Image Layout to Shader Read using a barrier
{
VkImageMemoryBarrier barrier =
{
VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
NULL,
VK_ACCESS_TRANSFER_WRITE_BIT, // srcAccessMask
VK_ACCESS_SHADER_READ_BIT, // dstAccessMask
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, // oldLayout
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, // newLayout
VK_QUEUE_FAMILY_IGNORED, // srcQueueFamilyIndex
VK_QUEUE_FAMILY_IGNORED, // dstQueueFamilyIndex
texImage,
subResRange
};
vkCmdPipelineBarrier(texCommandBuffer,
VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
0, 0, NULL, 0, NULL, 1, &barrier);
}
VkImageMemoryBarrier barrier =
{
VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
NULL,
VK_ACCESS_TRANSFER_WRITE_BIT, // srcAccessMask
VK_ACCESS_SHADER_READ_BIT, // dstAccessMask
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, // oldLayout
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, // newLayout
VK_QUEUE_FAMILY_IGNORED, // srcQueueFamilyIndex
VK_QUEUE_FAMILY_IGNORED, // dstQueueFamilyIndex
texImage,
subResRange
};
vkCmdPipelineBarrier(texCommandBuffer,
VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
0, 0, NULL, 0, NULL, 1, &barrier);
}
End and Execute Single Time Command Buffer
vk_end_single_time_commands(&vk, texCommandBuffer);
Destroy Staging Buffer and Free its Memory
vkDestroyBuffer(vk.device, texStagingBuffer, NULL);
vkFreeMemory(vk.device, texStagingBufferMemory, NULL);
vkFreeMemory(vk.device, texStagingBufferMemory, NULL);
Create Texture Image View
texImageView = vk_create_image_view(&vk, texImage,
VK_FORMAT_R8G8B8A8_SRGB);
VK_FORMAT_R8G8B8A8_SRGB);
Create Texture Image Sampler
VkSamplerCreateInfo samplerInfo =
{
VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO,
NULL,
0,
VK_FILTER_NEAREST,
VK_FILTER_NEAREST,
VK_SAMPLER_MIPMAP_MODE_LINEAR,
VK_SAMPLER_ADDRESS_MODE_REPEAT,
VK_SAMPLER_ADDRESS_MODE_REPEAT,
VK_SAMPLER_ADDRESS_MODE_REPEAT,
0.0f, // mipLodBias
VK_FALSE, // anisotropyEnable
16.0f, // maxAnisotropy
VK_FALSE, // compareEnabled
VK_COMPARE_OP_ALWAYS,
0.0f, // minLod
VK_LOD_CLAMP_NONE, // maxLod
VK_BORDER_COLOR_INT_OPAQUE_BLACK,
VK_FALSE // unnormalizedCoordinates
};
if (vkCreateSampler(vk.device, &samplerInfo, NULL,
&texSampler) != VK_SUCCESS)
{
assert(!"Failed to create texture sampler!");
}
{
VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO,
NULL,
0,
VK_FILTER_NEAREST,
VK_FILTER_NEAREST,
VK_SAMPLER_MIPMAP_MODE_LINEAR,
VK_SAMPLER_ADDRESS_MODE_REPEAT,
VK_SAMPLER_ADDRESS_MODE_REPEAT,
VK_SAMPLER_ADDRESS_MODE_REPEAT,
0.0f, // mipLodBias
VK_FALSE, // anisotropyEnable
16.0f, // maxAnisotropy
VK_FALSE, // compareEnabled
VK_COMPARE_OP_ALWAYS,
0.0f, // minLod
VK_LOD_CLAMP_NONE, // maxLod
VK_BORDER_COLOR_INT_OPAQUE_BLACK,
VK_FALSE // unnormalizedCoordinates
};
if (vkCreateSampler(vk.device, &samplerInfo, NULL,
&texSampler) != VK_SUCCESS)
{
assert(!"Failed to create texture sampler!");
}
Update Descriptor Set
VkDescriptorImageInfo descImageInfo =
{
texSampler,
texImageView,
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
};
VkWriteDescriptorSet writeDescSet =
{
VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET,
NULL,
descSet,
0, // dstBinding
0, // dstArrayElement
1, // descriptorCount
VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
&descImageInfo,
NULL, // pBufferInfo
NULL // pTexelBufferView
};
vkUpdateDescriptorSets(vk.device, 1, &writeDescSet, 0, NULL);
And that's the end of the initialization. We've set up everything needed for the texture to work. Now, let's update the color blend attachment to use alpha blending:
{
texSampler,
texImageView,
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
};
VkWriteDescriptorSet writeDescSet =
{
VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET,
NULL,
descSet,
0, // dstBinding
0, // dstArrayElement
1, // descriptorCount
VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
&descImageInfo,
NULL, // pBufferInfo
NULL // pTexelBufferView
};
vkUpdateDescriptorSets(vk.device, 1, &writeDescSet, 0, NULL);
VkPipelineColorBlendAttachmentState colorBlendAttachment =
{
VK_TRUE, // blendEnable
VK_BLEND_FACTOR_SRC_ALPHA,
VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA,
VK_BLEND_OP_ADD,
VK_BLEND_FACTOR_SRC_ALPHA,
VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA,
VK_BLEND_OP_ADD,
colorWriteMask,
};
We also need to update the graphics pipeline layout. Where we previously had no descriptor set layouts, we'll now add our descriptor set layout array:
{
VK_TRUE, // blendEnable
VK_BLEND_FACTOR_SRC_ALPHA,
VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA,
VK_BLEND_OP_ADD,
VK_BLEND_FACTOR_SRC_ALPHA,
VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA,
VK_BLEND_OP_ADD,
colorWriteMask,
};
VkPipelineLayoutCreateInfo pipelineLayoutInfo =
{
VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO,
NULL,
0,
array_count(descSetLayouts),
descSetLayouts,
0, NULL // (no push constant ranges)
};
The very last thing we need to do is bind the descriptor set right after binding our graphics pipeline and modify the draw function to draw 6 vertices instead of 3:
{
VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO,
NULL,
0,
array_count(descSetLayouts),
descSetLayouts,
0, NULL // (no push constant ranges)
};
// Bind descriptor set
// Bind descriptor set
VkDescriptorSet descSets[] = { descSet };
vkCmdBindDescriptorSets(graphicsCommandBuffer,
VK_PIPELINE_BIND_POINT_GRAPHICS,
pipelineLayout, 0,
array_count(descSets),
descSets,
0, NULL);
// Draw 6 vertices (2 triangles)
vkCmdDraw(graphicsCommandBuffer, 6, 1, 0, 0);
And there we have it! We're now drawing a quad with a checkerboard texture, including transparency. This involved a lot of steps, and unlike the first tutorial, I had to guide you on where to change or add new code, so there are more opportunities for mistakes, or for me to not be clear enough.// Bind descriptor set
VkDescriptorSet descSets[] = { descSet };
vkCmdBindDescriptorSets(graphicsCommandBuffer,
VK_PIPELINE_BIND_POINT_GRAPHICS,
pipelineLayout, 0,
array_count(descSets),
descSets,
0, NULL);
// Draw 6 vertices (2 triangles)
vkCmdDraw(graphicsCommandBuffer, 6, 1, 0, 0);
If you encounter any issues, please let me know in the comments. You can also find the full source code for where we’re at here, so you can compare it with your code if it's not working.
In the next tutorials, we’ll move on to adding vertex and index buffers to the app. See you there!
Next
Comments
Post a Comment