diff --git a/base/VulkanUIOverlay.cpp b/base/VulkanUIOverlay.cpp index 60c9226c..b18e2a5a 100644 --- a/base/VulkanUIOverlay.cpp +++ b/base/VulkanUIOverlay.cpp @@ -434,6 +434,13 @@ namespace vks return res; } + bool UIOverlay::radioButton(const char* caption, bool value) + { + bool res = ImGui::RadioButton(caption, value); + if (res) { updated = true; }; + return res; + } + bool UIOverlay::inputFloat(const char *caption, float *value, float step, uint32_t precision) { bool res = ImGui::InputFloat(caption, value, step, step * 10.0f, precision); diff --git a/base/VulkanUIOverlay.h b/base/VulkanUIOverlay.h index afb46ed6..7d45179b 100644 --- a/base/VulkanUIOverlay.h +++ b/base/VulkanUIOverlay.h @@ -81,6 +81,7 @@ namespace vks bool header(const char* caption); bool checkBox(const char* caption, bool* value); bool checkBox(const char* caption, int32_t* value); + bool radioButton(const char* caption, bool value); bool inputFloat(const char* caption, float* value, float step, uint32_t precision); bool sliderFloat(const char* caption, float* value, float min, float max); bool sliderInt(const char* caption, int32_t* value, int32_t min, int32_t max); diff --git a/data/shaders/glsl/vertexattributes/scene.frag b/data/shaders/glsl/vertexattributes/scene.frag new file mode 100644 index 00000000..c736fd31 --- /dev/null +++ b/data/shaders/glsl/vertexattributes/scene.frag @@ -0,0 +1,43 @@ +#version 450 + +layout (set = 1, binding = 0) uniform sampler2D samplerColorMap; +layout (set = 1, binding = 1) uniform sampler2D samplerNormalMap; + +layout (location = 0) in vec3 inNormal; +layout (location = 1) in vec2 inUV; +layout (location = 2) in vec3 inViewVec; +layout (location = 3) in vec3 inLightVec; +layout (location = 4) in vec4 inTangent; + +layout (location = 0) out vec4 outFragColor; + +layout(push_constant) uniform PushConsts { + mat4 model; + uint alphaMask; + float alphaMaskCuttoff; +} pushConsts; + +void main() +{ + vec4 color = texture(samplerColorMap, inUV); + + if (pushConsts.alphaMask == 1) { + if (color.a < pushConsts.alphaMaskCuttoff) { + discard; + } + } + + vec3 N = normalize(inNormal); + vec3 T = normalize(inTangent.xyz); + vec3 B = cross(inNormal, inTangent.xyz) * inTangent.w; + mat3 TBN = mat3(T, B, N); + N = TBN * normalize(texture(samplerNormalMap, inUV).xyz * 2.0 - vec3(1.0)); + + const float ambient = 0.1; + vec3 L = normalize(inLightVec); + vec3 V = normalize(inViewVec); + vec3 R = reflect(-L, N); + vec3 diffuse = max(dot(N, L), ambient).rrr; + float specular = pow(max(dot(R, V), 0.0), 32.0); + outFragColor = vec4(diffuse * color.rgb + specular, color.a); +} \ No newline at end of file diff --git a/data/shaders/glsl/vertexattributes/scene.frag.spv b/data/shaders/glsl/vertexattributes/scene.frag.spv new file mode 100644 index 00000000..300acdbb Binary files /dev/null and b/data/shaders/glsl/vertexattributes/scene.frag.spv differ diff --git a/data/shaders/glsl/vertexattributes/scene.vert b/data/shaders/glsl/vertexattributes/scene.vert new file mode 100644 index 00000000..2cd8c1a6 --- /dev/null +++ b/data/shaders/glsl/vertexattributes/scene.vert @@ -0,0 +1,39 @@ +#version 450 + +layout (location = 0) in vec3 inPos; +layout (location = 1) in vec3 inNormal; +layout (location = 2) in vec2 inUV; +layout (location = 3) in vec4 inTangent; + +layout (set = 0, binding = 0) uniform UBOScene +{ + mat4 projection; + mat4 view; + vec4 lightPos; + vec4 viewPos; +} uboScene; + +layout(push_constant) uniform PushConsts { + mat4 model; + uint alphaMask; + float alphaMaskCuttoff; +} pushConsts; + +layout (location = 0) out vec3 outNormal; +layout (location = 1) out vec2 outUV; +layout (location = 2) out vec3 outViewVec; +layout (location = 3) out vec3 outLightVec; +layout (location = 4) out vec4 outTangent; + +void main() +{ + outNormal = inNormal; + outUV = inUV; + outTangent = inTangent; + gl_Position = uboScene.projection * uboScene.view * pushConsts.model * vec4(inPos.xyz, 1.0); + + outNormal = mat3(pushConsts.model) * inNormal; + vec4 pos = pushConsts.model * vec4(inPos, 1.0); + outLightVec = uboScene.lightPos.xyz - pos.xyz; + outViewVec = uboScene.viewPos.xyz - pos.xyz; +} \ No newline at end of file diff --git a/data/shaders/glsl/vertexattributes/scene.vert.spv b/data/shaders/glsl/vertexattributes/scene.vert.spv new file mode 100644 index 00000000..83c99138 Binary files /dev/null and b/data/shaders/glsl/vertexattributes/scene.vert.spv differ diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 1a4e49c5..7f20749d 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -136,6 +136,7 @@ set(EXAMPLES texturesparseresidency triangle variablerateshading + vertexattributes viewportarray vulkanscene ) diff --git a/examples/vertexattributes/README.md b/examples/vertexattributes/README.md new file mode 100644 index 00000000..2900bf26 --- /dev/null +++ b/examples/vertexattributes/README.md @@ -0,0 +1,5 @@ +# Vertex attributes + +## Synopsis + +This sample demonstrates how to pass vertex attributes using interleaved or separate buffers. \ No newline at end of file diff --git a/examples/vertexattributes/vertexattributes.cpp b/examples/vertexattributes/vertexattributes.cpp new file mode 100644 index 00000000..be405b67 --- /dev/null +++ b/examples/vertexattributes/vertexattributes.cpp @@ -0,0 +1,657 @@ +/* + * Vulkan Example - Passing vertex attributes using interleaved and separate buffers + * + * Copyright (C) 2021 by Sascha Willems - www.saschawillems.de + * + * This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT) + */ + +#include "vertexattributes.h" + +/* + Vulkan glTF scene class +*/ + +VulkanglTFScene::~VulkanglTFScene() +{ + // Release all Vulkan resources allocated for the model + vertices.destroy(); + indices.destroy(); + for (Image image : images) { + vkDestroyImageView(vulkanDevice->logicalDevice, image.texture.view, nullptr); + vkDestroyImage(vulkanDevice->logicalDevice, image.texture.image, nullptr); + vkDestroySampler(vulkanDevice->logicalDevice, image.texture.sampler, nullptr); + vkFreeMemory(vulkanDevice->logicalDevice, image.texture.deviceMemory, nullptr); + } +} + +/* + glTF loading functions + + The following functions take a glTF input model loaded via tinyglTF and convert all required data into our own structure +*/ + +void VulkanglTFScene::loadImages(tinygltf::Model& input) +{ + // POI: The textures for the glTF file used in this sample are stored as external ktx files, so we can directly load them from disk without the need for conversion + images.resize(input.images.size()); + for (size_t i = 0; i < input.images.size(); i++) { + tinygltf::Image& glTFImage = input.images[i]; + images[i].texture.loadFromFile(path + "/" + glTFImage.uri, VK_FORMAT_R8G8B8A8_UNORM, vulkanDevice, copyQueue); + } +} + +void VulkanglTFScene::loadTextures(tinygltf::Model& input) +{ + textures.resize(input.textures.size()); + for (size_t i = 0; i < input.textures.size(); i++) { + textures[i].imageIndex = input.textures[i].source; + } +} + +void VulkanglTFScene::loadMaterials(tinygltf::Model& input) +{ + materials.resize(input.materials.size()); + for (size_t i = 0; i < input.materials.size(); i++) { + // We only read the most basic properties required for our sample + tinygltf::Material glTFMaterial = input.materials[i]; + // Get the base color factor + if (glTFMaterial.values.find("baseColorFactor") != glTFMaterial.values.end()) { + materials[i].baseColorFactor = glm::make_vec4(glTFMaterial.values["baseColorFactor"].ColorFactor().data()); + } + // Get base color texture index + if (glTFMaterial.values.find("baseColorTexture") != glTFMaterial.values.end()) { + materials[i].baseColorTextureIndex = glTFMaterial.values["baseColorTexture"].TextureIndex(); + } + // Get the normal map texture index + if (glTFMaterial.additionalValues.find("normalTexture") != glTFMaterial.additionalValues.end()) { + materials[i].normalTextureIndex = glTFMaterial.additionalValues["normalTexture"].TextureIndex(); + } + // Get some additional material parameters that are used in this sample + materials[i].alphaMode = glTFMaterial.alphaMode; + materials[i].alphaCutOff = (float)glTFMaterial.alphaCutoff; + materials[i].doubleSided = glTFMaterial.doubleSided; + } +} + +void VulkanglTFScene::loadNode(const tinygltf::Node& inputNode, const tinygltf::Model& input, VulkanglTFScene::Node* parent, std::vector& indexBuffer, std::vector& vertexBuffer) +{ + VulkanglTFScene::Node node{}; + node.name = inputNode.name; + + // Get the local node matrix + // It's either made up from translation, rotation, scale or a 4x4 matrix + node.matrix = glm::mat4(1.0f); + if (inputNode.translation.size() == 3) { + node.matrix = glm::translate(node.matrix, glm::vec3(glm::make_vec3(inputNode.translation.data()))); + } + if (inputNode.rotation.size() == 4) { + glm::quat q = glm::make_quat(inputNode.rotation.data()); + node.matrix *= glm::mat4(q); + } + if (inputNode.scale.size() == 3) { + node.matrix = glm::scale(node.matrix, glm::vec3(glm::make_vec3(inputNode.scale.data()))); + } + if (inputNode.matrix.size() == 16) { + node.matrix = glm::make_mat4x4(inputNode.matrix.data()); + }; + + // Load node's children + if (inputNode.children.size() > 0) { + for (size_t i = 0; i < inputNode.children.size(); i++) { + loadNode(input.nodes[inputNode.children[i]], input, &node, indexBuffer, vertexBuffer); + } + } + + // If the node contains mesh data, we load vertices and indices from the buffers + // In glTF this is done via accessors and buffer views + if (inputNode.mesh > -1) { + const tinygltf::Mesh mesh = input.meshes[inputNode.mesh]; + // Iterate through all primitives of this node's mesh + for (size_t i = 0; i < mesh.primitives.size(); i++) { + const tinygltf::Primitive& glTFPrimitive = mesh.primitives[i]; + uint32_t firstIndex = static_cast(indexBuffer.size()); + uint32_t vertexStart = static_cast(vertexBuffer.size()); + uint32_t indexCount = 0; + + // Vertex attributes + const float* positionBuffer = nullptr; + const float* normalsBuffer = nullptr; + const float* texCoordsBuffer = nullptr; + const float* tangentsBuffer = nullptr; + size_t vertexCount = 0; + + // Get buffer data for vertex positions + if (glTFPrimitive.attributes.find("POSITION") != glTFPrimitive.attributes.end()) { + const tinygltf::Accessor& accessor = input.accessors[glTFPrimitive.attributes.find("POSITION")->second]; + const tinygltf::BufferView& view = input.bufferViews[accessor.bufferView]; + positionBuffer = reinterpret_cast(&(input.buffers[view.buffer].data[accessor.byteOffset + view.byteOffset])); + vertexCount = accessor.count; + } + // Get buffer data for vertex normals + if (glTFPrimitive.attributes.find("NORMAL") != glTFPrimitive.attributes.end()) { + const tinygltf::Accessor& accessor = input.accessors[glTFPrimitive.attributes.find("NORMAL")->second]; + const tinygltf::BufferView& view = input.bufferViews[accessor.bufferView]; + normalsBuffer = reinterpret_cast(&(input.buffers[view.buffer].data[accessor.byteOffset + view.byteOffset])); + } + // Get buffer data for vertex texture coordinates + // glTF supports multiple sets, we only load the first one + if (glTFPrimitive.attributes.find("TEXCOORD_0") != glTFPrimitive.attributes.end()) { + const tinygltf::Accessor& accessor = input.accessors[glTFPrimitive.attributes.find("TEXCOORD_0")->second]; + const tinygltf::BufferView& view = input.bufferViews[accessor.bufferView]; + texCoordsBuffer = reinterpret_cast(&(input.buffers[view.buffer].data[accessor.byteOffset + view.byteOffset])); + } + // POI: This sample uses normal mapping, so we also need to load the tangents from the glTF file + if (glTFPrimitive.attributes.find("TANGENT") != glTFPrimitive.attributes.end()) { + const tinygltf::Accessor& accessor = input.accessors[glTFPrimitive.attributes.find("TANGENT")->second]; + const tinygltf::BufferView& view = input.bufferViews[accessor.bufferView]; + tangentsBuffer = reinterpret_cast(&(input.buffers[view.buffer].data[accessor.byteOffset + view.byteOffset])); + } + + // Append data to model's vertex buffer + for (size_t v = 0; v < vertexCount; v++) { + + // Append interleaved attributes + Vertex vert{}; + vert.pos = glm::vec4(glm::make_vec3(&positionBuffer[v * 3]), 1.0f); + vert.normal = glm::normalize(glm::vec3(normalsBuffer ? glm::make_vec3(&normalsBuffer[v * 3]) : glm::vec3(0.0f))); + vert.uv = texCoordsBuffer ? glm::make_vec2(&texCoordsBuffer[v * 2]) : glm::vec3(0.0f); + vert.tangent = tangentsBuffer ? glm::make_vec4(&tangentsBuffer[v * 4]) : glm::vec4(0.0f); + vertexBuffer.push_back(vert); + + // Append separate attributes + vertexAttributes.pos.push_back(glm::make_vec3(&positionBuffer[v * 3])); + vertexAttributes.normal.push_back(glm::normalize(glm::vec3(normalsBuffer ? glm::make_vec3(&normalsBuffer[v * 3]) : glm::vec3(0.0f)))); + vertexAttributes.tangent.push_back(tangentsBuffer ? glm::make_vec4(&tangentsBuffer[v * 4]) : glm::vec4(0.0f)); + vertexAttributes.uv.push_back(texCoordsBuffer ? glm::make_vec2(&texCoordsBuffer[v * 2]) : glm::vec3(0.0f)); + + } + + // Indices + const tinygltf::Accessor& accessor = input.accessors[glTFPrimitive.indices]; + const tinygltf::BufferView& bufferView = input.bufferViews[accessor.bufferView]; + const tinygltf::Buffer& buffer = input.buffers[bufferView.buffer]; + + indexCount += static_cast(accessor.count); + + // glTF supports different component types of indices + switch (accessor.componentType) { + case TINYGLTF_PARAMETER_TYPE_UNSIGNED_INT: { + const uint32_t* buf = reinterpret_cast(&buffer.data[accessor.byteOffset + bufferView.byteOffset]); + for (size_t index = 0; index < accessor.count; index++) { + indexBuffer.push_back(buf[index] + vertexStart); + } + break; + } + case TINYGLTF_PARAMETER_TYPE_UNSIGNED_SHORT: { + const uint16_t* buf = reinterpret_cast(&buffer.data[accessor.byteOffset + bufferView.byteOffset]); + for (size_t index = 0; index < accessor.count; index++) { + indexBuffer.push_back(buf[index] + vertexStart); + } + break; + } + case TINYGLTF_PARAMETER_TYPE_UNSIGNED_BYTE: { + const uint8_t* buf = reinterpret_cast(&buffer.data[accessor.byteOffset + bufferView.byteOffset]); + for (size_t index = 0; index < accessor.count; index++) { + indexBuffer.push_back(buf[index] + vertexStart); + } + break; + } + default: + std::cerr << "Index component type " << accessor.componentType << " not supported!" << std::endl; + return; + } + + Primitive primitive{}; + primitive.firstIndex = firstIndex; + primitive.indexCount = indexCount; + primitive.materialIndex = glTFPrimitive.material; + node.mesh.primitives.push_back(primitive); + } + } + + if (parent) { + parent->children.push_back(node); + } + else { + nodes.push_back(node); + } +} + +VkDescriptorImageInfo VulkanglTFScene::getTextureDescriptor(const size_t index) +{ + return images[index].texture.descriptor; +} + +/* + glTF rendering functions +*/ + +// Draw a single node including child nodes (if present) +void VulkanglTFScene::drawNode(VkCommandBuffer commandBuffer, VkPipelineLayout pipelineLayout, VulkanglTFScene::Node node, bool separate) +{ + if (!node.visible) { + return; + } + if (node.mesh.primitives.size() > 0) { + // Pass the node's matrix via push constants + // Traverse the node hierarchy to the top-most parent to get the final matrix of the current node + + PushConstBlock pushConstBlock; + + glm::mat4 nodeMatrix = node.matrix; + VulkanglTFScene::Node* currentParent = node.parent; + while (currentParent) { + nodeMatrix = currentParent->matrix * nodeMatrix; + currentParent = currentParent->parent; + } + for (VulkanglTFScene::Primitive& primitive : node.mesh.primitives) { + if (primitive.indexCount > 0) { + VulkanglTFScene::Material& material = materials[primitive.materialIndex]; + pushConstBlock.nodeMatrix = nodeMatrix; + pushConstBlock.alphaMask = (material.alphaMode == "MASK"); + pushConstBlock.alphaMaskCutoff = material.alphaCutOff; + vkCmdPushConstants(commandBuffer, pipelineLayout, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(PushConstBlock), &pushConstBlock); + vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 1, 1, &material.descriptorSet, 0, nullptr); + vkCmdDrawIndexed(commandBuffer, primitive.indexCount, 1, primitive.firstIndex, 0, 0); + } + } + } + for (auto& child : node.children) { + drawNode(commandBuffer, pipelineLayout, child, separate); + } +} + +/* + Vulkan Example class +*/ + +VulkanExample::VulkanExample() : VulkanExampleBase(ENABLE_VALIDATION) +{ + title = "Separate vertex attribute buffers"; + camera.type = Camera::CameraType::firstperson; + camera.flipY = true; + camera.setPosition(glm::vec3(0.0f, 1.0f, 0.0f)); + camera.setRotation(glm::vec3(0.0f, -90.0f, 0.0f)); + camera.setPerspective(60.0f, (float)width / (float)height, 0.1f, 256.0f); +} + +VulkanExample::~VulkanExample() +{ + vkDestroyPipelineLayout(device, pipelineLayout, nullptr); + vkDestroyDescriptorSetLayout(device, descriptorSetLayouts.matrices, nullptr); + vkDestroyDescriptorSetLayout(device, descriptorSetLayouts.textures, nullptr); + shaderData.buffer.destroy(); +} + +void VulkanExample::getEnabledFeatures() +{ + enabledFeatures.samplerAnisotropy = deviceFeatures.samplerAnisotropy; +} + +void VulkanExample::buildCommandBuffers() +{ + VkCommandBufferBeginInfo cmdBufInfo = vks::initializers::commandBufferBeginInfo(); + + VkClearValue clearValues[2]; + clearValues[0].color = defaultClearColor; + clearValues[0].color = { { 0.25f, 0.25f, 0.25f, 1.0f } };; + clearValues[1].depthStencil = { 1.0f, 0 }; + + VkRenderPassBeginInfo renderPassBeginInfo = vks::initializers::renderPassBeginInfo(); + renderPassBeginInfo.renderPass = renderPass; + renderPassBeginInfo.renderArea.offset.x = 0; + renderPassBeginInfo.renderArea.offset.y = 0; + renderPassBeginInfo.renderArea.extent.width = width; + renderPassBeginInfo.renderArea.extent.height = height; + renderPassBeginInfo.clearValueCount = 2; + renderPassBeginInfo.pClearValues = clearValues; + + const VkViewport viewport = vks::initializers::viewport((float)width, (float)height, 0.0f, 1.0f); + const VkRect2D scissor = vks::initializers::rect2D(width, height, 0, 0); + + for (int32_t i = 0; i < drawCmdBuffers.size(); ++i) + { + renderPassBeginInfo.framebuffer = frameBuffers[i]; + VK_CHECK_RESULT(vkBeginCommandBuffer(drawCmdBuffers[i], &cmdBufInfo)); + vkCmdBeginRenderPass(drawCmdBuffers[i], &renderPassBeginInfo, VK_SUBPASS_CONTENTS_INLINE); + vkCmdSetViewport(drawCmdBuffers[i], 0, 1, &viewport); + vkCmdSetScissor(drawCmdBuffers[i], 0, 1, &scissor); + + // Select the separate or interleaved vertex binding pipeline + vkCmdBindPipeline(drawCmdBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, vertexAttributeSettings == VertexAttributeSettings::separate ? pipelines.vertexAttributesSeparate : pipelines.vertexAttributesInterleaved); + + // Bind scene matrices descriptor to set 0 + vkCmdBindDescriptorSets(drawCmdBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, 1, &descriptorSet, 0, nullptr); + + // Use the same index buffer, no matter how vertex attributes are passed + vkCmdBindIndexBuffer(drawCmdBuffers[i], glTFScene.indices.buffer, 0, VK_INDEX_TYPE_UINT32); + + if (vertexAttributeSettings == VertexAttributeSettings::separate) { + // Using separate vertex attribute bindings requires binding all attribute buffers + VkDeviceSize offsets[4] = { 0, 0, 0, 0 }; + std::array buffers = { vertexAttibuteBuffers.pos.buffer, vertexAttibuteBuffers.normal.buffer, vertexAttibuteBuffers.uv.buffer, vertexAttibuteBuffers.tangent.buffer }; + vkCmdBindVertexBuffers(drawCmdBuffers[i], 0, static_cast(buffers.size()), buffers.data(), offsets); + } else { + // Using interleaved attribute bindings only requires one buffer bind + VkDeviceSize offsets[1] = { 0 }; + vkCmdBindVertexBuffers(drawCmdBuffers[i], 0, 1, &glTFScene.vertices.buffer, offsets); + } + // Render all nodes starting at top-level + for (auto& node : glTFScene.nodes) { + glTFScene.drawNode(drawCmdBuffers[i], pipelineLayout, node, vertexAttributeSettings == VertexAttributeSettings::separate); + } + + drawUI(drawCmdBuffers[i]); + vkCmdEndRenderPass(drawCmdBuffers[i]); + VK_CHECK_RESULT(vkEndCommandBuffer(drawCmdBuffers[i])); + } +} + +void VulkanExample::loadglTFFile(std::string filename) +{ + tinygltf::Model glTFInput; + tinygltf::TinyGLTF gltfContext; + std::string error, warning; + + this->device = device; + +#if defined(__ANDROID__) + // On Android all assets are packed with the apk in a compressed form, so we need to open them using the asset manager + // We let tinygltf handle this, by passing the asset manager of our app + tinygltf::asset_manager = androidApp->activity->assetManager; +#endif + bool fileLoaded = gltfContext.LoadASCIIFromFile(&glTFInput, &error, &warning, filename); + + // Pass some Vulkan resources required for setup and rendering to the glTF model loading class + glTFScene.vulkanDevice = vulkanDevice; + glTFScene.copyQueue = queue; + + size_t pos = filename.find_last_of('/'); + glTFScene.path = filename.substr(0, pos); + + std::vector indexBuffer; + std::vector vertexBuffer; + + if (!fileLoaded) { + vks::tools::exitFatal("Could not open the glTF file.\n\nThe file is part of the additional asset pack.\n\nRun \"download_assets.py\" in the repository root to download the latest version.", -1); + return; + } + glTFScene.loadImages(glTFInput); + glTFScene.loadMaterials(glTFInput); + glTFScene.loadTextures(glTFInput); + const tinygltf::Scene& scene = glTFInput.scenes[0]; + for (size_t i = 0; i < scene.nodes.size(); i++) { + const tinygltf::Node node = glTFInput.nodes[scene.nodes[i]]; + glTFScene.loadNode(node, glTFInput, nullptr, indexBuffer, vertexBuffer); + } + + /* Upload vertex and index buffers */ + + /* Anonymous functions to simplify buffer creation */ + /* Create a staging buffer used as a source for copies */ + auto createStagingBuffer = [this](vks::Buffer& buffer, void* data, VkDeviceSize size) { + VK_CHECK_RESULT(vulkanDevice->createBuffer(VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, &buffer, size, data)); + }; + /* Create a device local buffer used as a target for copies*/ + auto createDeviceBuffer = [this](vks::Buffer& buffer, VkDeviceSize size, VkBufferUsageFlags usageFlags = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT) { + VK_CHECK_RESULT(vulkanDevice->createBuffer(usageFlags | VK_BUFFER_USAGE_TRANSFER_DST_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, &buffer, size)); + }; + + size_t vertexBufferSize = vertexBuffer.size() * sizeof(VulkanglTFScene::Vertex); + size_t indexBufferSize = indexBuffer.size() * sizeof(uint32_t); + + vks::Buffer vertexStaging, indexStaging; + + createStagingBuffer(indexStaging, indexBuffer.data(), indexBufferSize); + createStagingBuffer(vertexStaging, vertexBuffer.data(), vertexBufferSize); + + createDeviceBuffer(glTFScene.indices, indexStaging.size, VK_BUFFER_USAGE_INDEX_BUFFER_BIT); + createDeviceBuffer(glTFScene.vertices, vertexStaging.size); + + // Copy data from staging buffers (host) do device local buffer (gpu) + VkCommandBuffer copyCmd = vulkanDevice->createCommandBuffer(VK_COMMAND_BUFFER_LEVEL_PRIMARY, true); + VkBufferCopy copyRegion = {}; + + copyRegion.size = vertexBufferSize; + vkCmdCopyBuffer(copyCmd, vertexStaging.buffer, glTFScene.vertices.buffer, 1, ©Region); + + copyRegion.size = indexBufferSize; + vkCmdCopyBuffer(copyCmd, indexStaging.buffer, glTFScene.indices.buffer, 1, ©Region); + + vulkanDevice->flushCommandBuffer(copyCmd, queue, true); + + // Free staging resources + vkDestroyBuffer(device, vertexStaging.buffer, nullptr); + vkFreeMemory(device, vertexStaging.memory, nullptr); + vkDestroyBuffer(device, indexStaging.buffer, nullptr); + vkFreeMemory(device, indexStaging.memory, nullptr); + + /* + Interleaved vertex attributes + We create one single buffer containing the interleaved vertex attributes + */ + + /* + Separate vertex attributes + We create a separate buffer for each of the vertex attributes (position, normals, etc.) + */ + + std::array stagingBuffers; + createStagingBuffer(stagingBuffers[0], glTFScene.vertexAttributes.pos.data(), glTFScene.vertexAttributes.pos.size() * sizeof(glTFScene.vertexAttributes.pos[0])); + createStagingBuffer(stagingBuffers[1], glTFScene.vertexAttributes.normal.data(), glTFScene.vertexAttributes.normal.size() * sizeof(glTFScene.vertexAttributes.normal[0])); + createStagingBuffer(stagingBuffers[2], glTFScene.vertexAttributes.uv.data(), glTFScene.vertexAttributes.uv.size() * sizeof(glTFScene.vertexAttributes.uv[0])); + createStagingBuffer(stagingBuffers[3], glTFScene.vertexAttributes.tangent.data(), glTFScene.vertexAttributes.tangent.size() * sizeof(glTFScene.vertexAttributes.tangent[0])); + + createDeviceBuffer(vertexAttibuteBuffers.pos, stagingBuffers[0].size); + createDeviceBuffer(vertexAttibuteBuffers.normal, stagingBuffers[1].size); + createDeviceBuffer(vertexAttibuteBuffers.uv, stagingBuffers[2].size); + createDeviceBuffer(vertexAttibuteBuffers.tangent, stagingBuffers[3].size); + + // Stage + std::vector attributeBuffers = { + vertexAttibuteBuffers.pos, + vertexAttibuteBuffers.normal, + vertexAttibuteBuffers.uv, + vertexAttibuteBuffers.tangent, + }; + + // Copy data from staging buffers (host) do device local buffer (gpu) + copyCmd = vulkanDevice->createCommandBuffer(VK_COMMAND_BUFFER_LEVEL_PRIMARY, true); + copyRegion = {}; + for (size_t i = 0; i < attributeBuffers.size(); i++) { + copyRegion.size = attributeBuffers[i].size; + vkCmdCopyBuffer(copyCmd, stagingBuffers[i].buffer, attributeBuffers[i].buffer, 1, ©Region); + } + vulkanDevice->flushCommandBuffer(copyCmd, queue, true); + + /* + Index buffer + The index buffer is always the same, no matter how we pass the vertex attributes + */ + + + // @todo: clear +} + +void VulkanExample::loadAssets() +{ + loadglTFFile(getAssetPath() + "models/sponza/sponza.gltf"); +} + +void VulkanExample::setupDescriptors() +{ + // One ubo to pass dynamic data to the shader + // Two combined image samplers per material as each material uses color and normal maps + std::vector poolSizes = { + vks::initializers::descriptorPoolSize(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1), + vks::initializers::descriptorPoolSize(VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, static_cast(glTFScene.materials.size()) * 2), + }; + // One set for matrices and one per model image/texture + const uint32_t maxSetCount = static_cast(glTFScene.images.size()) + 1; + VkDescriptorPoolCreateInfo descriptorPoolInfo = vks::initializers::descriptorPoolCreateInfo(poolSizes, maxSetCount); + VK_CHECK_RESULT(vkCreateDescriptorPool(device, &descriptorPoolInfo, nullptr, &descriptorPool)); + // Descriptor set layout for passing matrices + std::vector setLayoutBindings = { + vks::initializers::descriptorSetLayoutBinding(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VK_SHADER_STAGE_VERTEX_BIT, 0) + }; + VkDescriptorSetLayoutCreateInfo descriptorSetLayoutCI = vks::initializers::descriptorSetLayoutCreateInfo(setLayoutBindings.data(), static_cast(setLayoutBindings.size())); + VK_CHECK_RESULT(vkCreateDescriptorSetLayout(device, &descriptorSetLayoutCI, nullptr, &descriptorSetLayouts.matrices)); + // Descriptor set layout for passing material textures + setLayoutBindings = { + // Color map + vks::initializers::descriptorSetLayoutBinding(VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, VK_SHADER_STAGE_FRAGMENT_BIT, 0), + // Normal map + vks::initializers::descriptorSetLayoutBinding(VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, VK_SHADER_STAGE_FRAGMENT_BIT, 1), + }; + descriptorSetLayoutCI.pBindings = setLayoutBindings.data(); + descriptorSetLayoutCI.bindingCount = 2; + VK_CHECK_RESULT(vkCreateDescriptorSetLayout(device, &descriptorSetLayoutCI, nullptr, &descriptorSetLayouts.textures)); + + // Pipeline layout using both descriptor sets (set 0 = matrices, set 1 = material) + std::array setLayouts = { descriptorSetLayouts.matrices, descriptorSetLayouts.textures }; + VkPipelineLayoutCreateInfo pipelineLayoutCI = vks::initializers::pipelineLayoutCreateInfo(setLayouts.data(), static_cast(setLayouts.size())); + // We will use push constants to push the local matrices of a primitive to the vertex shader + VkPushConstantRange pushConstantRange = vks::initializers::pushConstantRange(VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, sizeof(PushConstBlock), 0); + // Push constant ranges are part of the pipeline layout + pipelineLayoutCI.pushConstantRangeCount = 1; + pipelineLayoutCI.pPushConstantRanges = &pushConstantRange; + VK_CHECK_RESULT(vkCreatePipelineLayout(device, &pipelineLayoutCI, nullptr, &pipelineLayout)); + + // Descriptor set for scene matrices + VkDescriptorSetAllocateInfo allocInfo = vks::initializers::descriptorSetAllocateInfo(descriptorPool, &descriptorSetLayouts.matrices, 1); + VK_CHECK_RESULT(vkAllocateDescriptorSets(device, &allocInfo, &descriptorSet)); + VkWriteDescriptorSet writeDescriptorSet = vks::initializers::writeDescriptorSet(descriptorSet, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 0, &shaderData.buffer.descriptor); + vkUpdateDescriptorSets(device, 1, &writeDescriptorSet, 0, nullptr); + + // Descriptor sets for the materials + for (auto& material : glTFScene.materials) { + const VkDescriptorSetAllocateInfo allocInfo = vks::initializers::descriptorSetAllocateInfo(descriptorPool, &descriptorSetLayouts.textures, 1); + VK_CHECK_RESULT(vkAllocateDescriptorSets(device, &allocInfo, &material.descriptorSet)); + VkDescriptorImageInfo colorMap = glTFScene.getTextureDescriptor(material.baseColorTextureIndex); + VkDescriptorImageInfo normalMap = glTFScene.getTextureDescriptor(material.normalTextureIndex); + std::vector writeDescriptorSets = { + vks::initializers::writeDescriptorSet(material.descriptorSet, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 0, &colorMap), + vks::initializers::writeDescriptorSet(material.descriptorSet, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, &normalMap), + }; + vkUpdateDescriptorSets(device, static_cast(writeDescriptorSets.size()), writeDescriptorSets.data(), 0, nullptr); + } +} + +void VulkanExample::preparePipelines() +{ + VkPipelineInputAssemblyStateCreateInfo inputAssemblyStateCI = vks::initializers::pipelineInputAssemblyStateCreateInfo(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, 0, VK_FALSE); + VkPipelineRasterizationStateCreateInfo rasterizationStateCI = vks::initializers::pipelineRasterizationStateCreateInfo(VK_POLYGON_MODE_FILL, VK_CULL_MODE_BACK_BIT, VK_FRONT_FACE_COUNTER_CLOCKWISE, 0); + VkPipelineColorBlendAttachmentState blendAttachmentStateCI = vks::initializers::pipelineColorBlendAttachmentState(0xf, VK_FALSE); + VkPipelineColorBlendStateCreateInfo colorBlendStateCI = vks::initializers::pipelineColorBlendStateCreateInfo(1, &blendAttachmentStateCI); + VkPipelineDepthStencilStateCreateInfo depthStencilStateCI = vks::initializers::pipelineDepthStencilStateCreateInfo(VK_TRUE, VK_TRUE, VK_COMPARE_OP_LESS_OR_EQUAL); + VkPipelineViewportStateCreateInfo viewportStateCI = vks::initializers::pipelineViewportStateCreateInfo(1, 1, 0); + VkPipelineMultisampleStateCreateInfo multisampleStateCI = vks::initializers::pipelineMultisampleStateCreateInfo(VK_SAMPLE_COUNT_1_BIT, 0); + const std::vector dynamicStateEnables = { VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }; + VkPipelineDynamicStateCreateInfo dynamicStateCI = vks::initializers::pipelineDynamicStateCreateInfo(dynamicStateEnables.data(), static_cast(dynamicStateEnables.size()), 0); + VkPipelineVertexInputStateCreateInfo vertexInputStateCI = vks::initializers::pipelineVertexInputStateCreateInfo(); + std::array shaderStages; + + // @todo: comment + const std::vector vertexInputBindingsInterleaved = { + vks::initializers::vertexInputBindingDescription(0, sizeof(VulkanglTFScene::Vertex), VK_VERTEX_INPUT_RATE_VERTEX), + }; + const std::vector vertexInputAttributesInterleaved = { + vks::initializers::vertexInputAttributeDescription(0, 0, VK_FORMAT_R32G32B32_SFLOAT, offsetof(VulkanglTFScene::Vertex, pos)), + vks::initializers::vertexInputAttributeDescription(0, 1, VK_FORMAT_R32G32B32_SFLOAT, offsetof(VulkanglTFScene::Vertex, normal)), + vks::initializers::vertexInputAttributeDescription(0, 2, VK_FORMAT_R32G32B32_SFLOAT, offsetof(VulkanglTFScene::Vertex, uv)), + vks::initializers::vertexInputAttributeDescription(0, 3, VK_FORMAT_R32G32B32_SFLOAT, offsetof(VulkanglTFScene::Vertex, tangent)), + }; + + // @todo: comment + const std::vector vertexInputBindingsSeparate = { + vks::initializers::vertexInputBindingDescription(0, sizeof(glm::vec3), VK_VERTEX_INPUT_RATE_VERTEX), + vks::initializers::vertexInputBindingDescription(1, sizeof(glm::vec3), VK_VERTEX_INPUT_RATE_VERTEX), + vks::initializers::vertexInputBindingDescription(2, sizeof(glm::vec2), VK_VERTEX_INPUT_RATE_VERTEX), + vks::initializers::vertexInputBindingDescription(3, sizeof(glm::vec4), VK_VERTEX_INPUT_RATE_VERTEX), + }; + const std::vector vertexInputAttributesSeparate = { + vks::initializers::vertexInputAttributeDescription(0, 0, VK_FORMAT_R32G32B32_SFLOAT, 0), + vks::initializers::vertexInputAttributeDescription(1, 1, VK_FORMAT_R32G32B32_SFLOAT, 0), + vks::initializers::vertexInputAttributeDescription(2, 2, VK_FORMAT_R32G32B32_SFLOAT, 0), + vks::initializers::vertexInputAttributeDescription(3, 3, VK_FORMAT_R32G32B32_SFLOAT, 0), + }; + + VkGraphicsPipelineCreateInfo pipelineCI = vks::initializers::pipelineCreateInfo(pipelineLayout, renderPass, 0); + pipelineCI.pVertexInputState = &vertexInputStateCI; + pipelineCI.pInputAssemblyState = &inputAssemblyStateCI; + pipelineCI.pRasterizationState = &rasterizationStateCI; + pipelineCI.pColorBlendState = &colorBlendStateCI; + pipelineCI.pMultisampleState = &multisampleStateCI; + pipelineCI.pViewportState = &viewportStateCI; + pipelineCI.pDepthStencilState = &depthStencilStateCI; + pipelineCI.pDynamicState = &dynamicStateCI; + pipelineCI.stageCount = static_cast(shaderStages.size()); + pipelineCI.pStages = shaderStages.data(); + + shaderStages[0] = loadShader(getShadersPath() + "vertexattributes/scene.vert.spv", VK_SHADER_STAGE_VERTEX_BIT); + shaderStages[1] = loadShader(getShadersPath() + "vertexattributes/scene.frag.spv", VK_SHADER_STAGE_FRAGMENT_BIT); + + //rasterizationStateCI.cullMode = material.doubleSided ? VK_CULL_MODE_NONE : VK_CULL_MODE_BACK_BIT; + vertexInputStateCI = vks::initializers::pipelineVertexInputStateCreateInfo(vertexInputBindingsInterleaved, vertexInputAttributesInterleaved); + VK_CHECK_RESULT(vkCreateGraphicsPipelines(device, pipelineCache, 1, &pipelineCI, nullptr, &pipelines.vertexAttributesInterleaved)); + + vertexInputStateCI = vks::initializers::pipelineVertexInputStateCreateInfo(vertexInputBindingsSeparate, vertexInputAttributesSeparate); + VK_CHECK_RESULT(vkCreateGraphicsPipelines(device, pipelineCache, 1, &pipelineCI, nullptr, &pipelines.vertexAttributesSeparate)); +} + +void VulkanExample::prepareUniformBuffers() +{ + VK_CHECK_RESULT(vulkanDevice->createBuffer( + VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, + VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, + &shaderData.buffer, + sizeof(shaderData.values))); + VK_CHECK_RESULT(shaderData.buffer.map()); + updateUniformBuffers(); +} + +void VulkanExample::updateUniformBuffers() +{ + shaderData.values.projection = camera.matrices.perspective; + shaderData.values.view = camera.matrices.view; + shaderData.values.viewPos = camera.viewPos; + memcpy(shaderData.buffer.mapped, &shaderData.values, sizeof(shaderData.values)); +} + +void VulkanExample::prepare() +{ + VulkanExampleBase::prepare(); + loadAssets(); + prepareUniformBuffers(); + setupDescriptors(); + preparePipelines(); + buildCommandBuffers(); + prepared = true; +} + +void VulkanExample::render() +{ + renderFrame(); + if (camera.updated) { + updateUniformBuffers(); + } +} + +void VulkanExample::OnUpdateUIOverlay(vks::UIOverlay* overlay) +{ + if (overlay->header("Vertex buffer attributes")) { + bool interleaved = (vertexAttributeSettings == VertexAttributeSettings::interleaved); + bool separate = (vertexAttributeSettings == VertexAttributeSettings::separate); + if (overlay->radioButton("Interleaved", interleaved)) { + vertexAttributeSettings = VertexAttributeSettings::interleaved; + buildCommandBuffers(); + } + if (overlay->radioButton("Separate", separate)) { + vertexAttributeSettings = VertexAttributeSettings::separate; + buildCommandBuffers(); + } + } +} + +VULKAN_EXAMPLE_MAIN() \ No newline at end of file diff --git a/examples/vertexattributes/vertexattributes.h b/examples/vertexattributes/vertexattributes.h new file mode 100644 index 00000000..8d96ac17 --- /dev/null +++ b/examples/vertexattributes/vertexattributes.h @@ -0,0 +1,176 @@ +/* + * Vulkan Example - Passing vertex attributes using interleaved and separate buffers + * + * Copyright (C) 2021 by Sascha Willems - www.saschawillems.de + * + * This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT) + */ + +#define TINYGLTF_IMPLEMENTATION +#define STB_IMAGE_IMPLEMENTATION +#define TINYGLTF_NO_STB_IMAGE_WRITE +#define TINYGLTF_NO_STB_IMAGE +#define TINYGLTF_NO_EXTERNAL_IMAGE +#ifdef VK_USE_PLATFORM_ANDROID_KHR +#define TINYGLTF_ANDROID_LOAD_FROM_ASSETS +#endif +#include "tiny_gltf.h" + +#include "vulkanexamplebase.h" + +#define ENABLE_VALIDATION false + +struct PushConstBlock { + glm::mat4 nodeMatrix; + uint32_t alphaMask; + float alphaMaskCutoff; +}; + + // Contains everything required to render a basic glTF scene in Vulkan + // This class is heavily simplified (compared to glTF's feature set) but retains the basic glTF structure +class VulkanglTFScene +{ +public: + // The class requires some Vulkan objects so it can create it's own resources + vks::VulkanDevice* vulkanDevice; + VkQueue copyQueue; + + // The vertex layout for the samples' model + struct Vertex { + glm::vec3 pos; + glm::vec3 normal; + glm::vec2 uv; + glm::vec4 tangent; + }; + + // Single vertex buffer for all primitives + vks::Buffer vertices; + + // Used at loading time + struct VertexAttributes { + std::vector uv; + std::vector pos, normal; + std::vector tangent; + } vertexAttributes; + + // Single index buffer for all primitives + vks::Buffer indices; + + // The following structures roughly represent the glTF scene structure + // To keep things simple, they only contain those properties that are required for this sample + struct Node; + + // A primitive contains the data for a single draw call + struct Primitive { + uint32_t firstIndex; + uint32_t indexCount; + int32_t materialIndex; + }; + + // Contains the node's (optional) geometry and can be made up of an arbitrary number of primitives + struct Mesh { + std::vector primitives; + }; + + // A node represents an object in the glTF scene graph + struct Node { + Node* parent; + std::vector children; + Mesh mesh; + glm::mat4 matrix; + std::string name; + bool visible = true; + }; + + // A glTF material stores information in e.g. the texture that is attached to it and colors + struct Material { + glm::vec4 baseColorFactor = glm::vec4(1.0f); + uint32_t baseColorTextureIndex; + uint32_t normalTextureIndex; + std::string alphaMode = "OPAQUE"; + float alphaCutOff; + bool doubleSided = false; + VkDescriptorSet descriptorSet; + }; + + // Contains the texture for a single glTF image + // Images may be reused by texture objects and are as such separated + struct Image { + vks::Texture2D texture; + }; + + // A glTF texture stores a reference to the image and a sampler + // In this sample, we are only interested in the image + struct Texture { + int32_t imageIndex; + }; + + /* + Model data + */ + std::vector images; + std::vector textures; + std::vector materials; + std::vector nodes; + + std::string path; + + ~VulkanglTFScene(); + VkDescriptorImageInfo getTextureDescriptor(const size_t index); + void loadImages(tinygltf::Model& input); + void loadTextures(tinygltf::Model& input); + void loadMaterials(tinygltf::Model& input); + void loadNode(const tinygltf::Node& inputNode, const tinygltf::Model& input, VulkanglTFScene::Node* parent, std::vector& indexBuffer, std::vector& vertexBuffer); + void drawNode(VkCommandBuffer commandBuffer, VkPipelineLayout pipelineLayout, VulkanglTFScene::Node node, bool separate); +}; + +class VulkanExample : public VulkanExampleBase +{ +public: + VulkanglTFScene glTFScene; + + enum VertexAttributeSettings { interleaved, separate }; + VertexAttributeSettings vertexAttributeSettings = separate; + + // Buffers for the separate vertex attributes + struct VertexAttributeBuffers { + vks::Buffer pos, normal, uv, tangent; + } vertexAttibuteBuffers; + + struct ShaderData { + vks::Buffer buffer; + struct Values { + glm::mat4 projection; + glm::mat4 view; + glm::vec4 lightPos = glm::vec4(0.0f, 2.5f, 0.0f, 1.0f); + glm::vec4 viewPos; + } values; + } shaderData; + + struct Pipelines { + VkPipeline vertexAttributesInterleaved; + VkPipeline vertexAttributesSeparate; + } pipelines; + + VkPipelineLayout pipelineLayout; + VkDescriptorSet descriptorSet; + + struct DescriptorSetLayouts { + VkDescriptorSetLayout matrices; + VkDescriptorSetLayout textures; + } descriptorSetLayouts; + + VulkanExample(); + ~VulkanExample(); + virtual void getEnabledFeatures(); + void buildCommandBuffers(); + void loadglTFFile(std::string filename); + void loadAssets(); + void setupDescriptors(); + void preparePipelines(); + void prepareUniformBuffers(); + void updateUniformBuffers(); + void prepare(); + virtual void render(); + virtual void OnUpdateUIOverlay(vks::UIOverlay* overlay); +}; \ No newline at end of file