/* -*- tab-width: 4; -*- */ /* vi: set sw=2 ts=4 expandtab: */ /* * Copyright 2017-2020 Mark Callow. * SPDX-License-Identifier: Apache-2.0 */ /** * @internal * @class TextureCubemap * @~English * * @brief Test loading of cubemap textures. * * @author Mark Callow, www.edgewise-consulting.com. * * @par Acknowledgement * Thanks to Sascha Willems' - www.saschawillems.de - for the concept, * the VulkanTextOverlay VulkanMeshLoader classes and the shaders used * by this test. */ #include #include #include #include #include #include "TextureCubemap.h" #include "VulkanTextureTranscoder.hpp" #include "argparser.h" #include "ltexceptions.h" #define VERTEX_BUFFER_BIND_ID 0 #define ENABLE_VALIDATION false #define USE_GL_RH_NDC 0 // Vertex layout for this example std::vector vertexLayout = { vkMeshLoader::VERTEX_LAYOUT_POSITION, vkMeshLoader::VERTEX_LAYOUT_NORMAL, vkMeshLoader::VERTEX_LAYOUT_UV }; VulkanLoadTestSample* TextureCubemap::create(VulkanContext& vkctx, uint32_t width, uint32_t height, const char* const szArgs, const std::string sBasePath) { return new TextureCubemap(vkctx, width, height, szArgs, sBasePath, USE_GL_RH_NDC ? 1 : -1); } TextureCubemap::TextureCubemap(VulkanContext& vkctx, uint32_t width, uint32_t height, const char* const szArgs, const std::string sBasePath, int32_t yflip) : VulkanLoadTestSample(vkctx, width, height, sBasePath, yflip) { zoom = -4.0f; rotationSpeed = 0.25f; rotation = { -7.25f, 120.0f, 0.0f }; transcoded = false; ktxVulkanDeviceInfo vdi; ktxVulkanDeviceInfo_Construct(&vdi, vkctx.gpu, vkctx.device, vkctx.queue, vkctx.commandPool, nullptr); processArgs(szArgs); KTX_error_code ktxresult; ktxTexture* kTexture; std::string ktxfilepath = externalFile ? ktxfilename : getAssetPath() + ktxfilename; ktxresult = ktxTexture_CreateFromNamedFile(ktxfilepath.c_str(), preloadImages ? KTX_TEXTURE_CREATE_LOAD_IMAGE_DATA_BIT : KTX_TEXTURE_CREATE_NO_FLAGS, &kTexture); if (KTX_SUCCESS != ktxresult) { std::stringstream message; message << "Creation of ktxTexture from \"" << ktxfilepath << "\" failed: " << ktxErrorString(ktxresult); throw std::runtime_error(message.str()); } if (ktxTexture_NeedsTranscoding(kTexture)) { TextureTranscoder tc(vkctx); tc.transcode((ktxTexture2*)kTexture); transcoded = true; } vk::Format vkFormat = static_cast(ktxTexture_GetVkFormat(kTexture)); transcodedFormat = vkFormat; vk::FormatProperties properties; vkctx.gpu.getFormatProperties(vkFormat, &properties); vk::FormatFeatureFlags wantedFeatures = vk::FormatFeatureFlagBits::eSampledImage | vk::FormatFeatureFlagBits::eSampledImageFilterLinear; if (!(properties.optimalTilingFeatures & wantedFeatures)) { ktxTexture_Destroy(kTexture); throw unsupported_ttype(); } ktxresult = ktxTexture_VkUpload(kTexture, &vdi, &cubeMap); if (KTX_SUCCESS != ktxresult) { std::stringstream message; message << "ktxTexture_VkUpload failed: " << ktxErrorString(ktxresult); throw std::runtime_error(message.str()); } if (kTexture->orientation.y == KTX_ORIENT_Y_DOWN) { // Assume a KTX-compliant cubemap. That means the faces are in a // LH coord system with +y up. Multiply the cube's y and z by -1 to // put the +z face in front of the view and keep +y up. Alternatively // we could multiply the y and x coords by -1 to kepp the +y up while // placing the +z face in the +z direction. #if !USE_GL_RH_NDC ubo.uvwTransform = glm::scale(glm::mat4(1.0f), glm::vec3(1, -1, -1)); #else // Scale the skybox cube's z by -1 to convert it to LH coords // with the +z face in front of the view. ubo.uvwTransform = glm::scale(glm::mat4(1.0f), glm::vec3(1, 1, -1)); #endif } else { std::stringstream message; message << "Cubemap faces have unsupported KTXorientation value."; throw std::runtime_error(message.str()); } ktxTexture_Destroy(kTexture); ktxVulkanDeviceInfo_Destruct(&vdi); try { prepare(); } catch (std::exception& e) { (void)e; // To quiet unused variable warnings from some compilers. cleanup(); throw; } } TextureCubemap::~TextureCubemap() { cleanup(); } void TextureCubemap::resize(uint32_t width, uint32_t height) { this->w_width = width; this->w_height = height; rebuildCommandBuffers(); updateUniformBuffers(); } void TextureCubemap::run(uint32_t /*msTicks*/) { // Nothing to do since the scene is not animated. // VulkanLoadTests base class redraws from the command buffer we built. } //=================================================================== void TextureCubemap::processArgs(std::string sArgs) { // Options descriptor struct argparser::option longopts[] = { {"external", argparser::option::no_argument, &externalFile, 1}, {"preload", argparser::option::no_argument, &preloadImages, 1}, {NULL, argparser::option::no_argument, NULL, 0} }; argvector argv(sArgs); argparser ap(argv); int ch; while ((ch = ap.getopt(nullptr, longopts, nullptr)) != -1) { switch (ch) { case 0: break; default: assert(false); // Error in args in sample table. } } assert(ap.optind < argv.size()); ktxfilename = argv[ap.optind]; } /* ------------------------------------------------------------------------- */ void TextureCubemap::cleanup() { // Clean up used Vulkan resources // Clean up texture resources if (sampler) vkctx.device.destroySampler(sampler); if (imageView) vkctx.device.destroyImageView(imageView); ktxVulkanTexture_Destruct(&cubeMap, vkctx.device, nullptr); if (pipelines.reflect) vkctx.device.destroyPipeline(pipelines.reflect); if (pipelines.skybox) vkctx.device.destroyPipeline(pipelines.skybox); if (pipelineLayout) vkctx.device.destroyPipelineLayout(pipelineLayout); if (descriptorSetLayout) vkctx.device.destroyDescriptorSetLayout(descriptorSetLayout); vkctx.destroyDrawCommandBuffers(); for (size_t i = 0; i < meshes.objects.size(); i++) { vkMeshLoader::freeMeshBufferResources(vkctx.device, &meshes.objects[i]); } vkMeshLoader::freeMeshBufferResources(vkctx.device, &meshes.skybox); uniformData.object.freeResources(vkctx.device); uniformData.skybox.freeResources(vkctx.device); } void TextureCubemap::rebuildCommandBuffers() { if (!vkctx.checkDrawCommandBuffers()) { vkctx.destroyDrawCommandBuffers(); vkctx.createDrawCommandBuffers(); } buildCommandBuffers(); } void TextureCubemap::buildCommandBuffers() { vk::CommandBufferBeginInfo cmdBufInfo({}, nullptr); vk::ClearValue clearValues[2]; clearValues[0].color = defaultClearColor; clearValues[1].depthStencil = vk::ClearDepthStencilValue(1.0f, 0); vk::RenderPassBeginInfo renderPassBeginInfo(vkctx.renderPass, nullptr, {{0, 0}, {w_width, w_height}}, 2, clearValues); for (uint32_t i = 0; i < vkctx.drawCmdBuffers.size(); ++i) { // Set target frame buffer renderPassBeginInfo.framebuffer = vkctx.framebuffers[i]; VK_CHECK_RESULT(vkBeginCommandBuffer(vkctx.drawCmdBuffers[i], &static_cast(cmdBufInfo))); vkCmdBeginRenderPass(vkctx.drawCmdBuffers[i], &static_cast(renderPassBeginInfo), VK_SUBPASS_CONTENTS_INLINE); #if !USE_GL_RH_NDC vk::Viewport viewport(0, 0, (float)w_width, (float)w_height, 0.0f, 1.0f); #else // Make OpenGL style viewport vk::Viewport viewport(0, (float)w_height, (float)w_width, -(float)w_height, 0.0f, 1.0f); #endif vkCmdSetViewport(vkctx.drawCmdBuffers[i], 0, 1, &static_cast(viewport)); vk::Rect2D scissor({0, 0}, {w_width, w_height}); vkCmdSetScissor(vkctx.drawCmdBuffers[i], 0, 1, &static_cast(scissor)); VkDeviceSize offsets[1] = { 0 }; // Skybox if (displaySkybox) { vkCmdBindDescriptorSets(vkctx.drawCmdBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, 1, &static_cast(descriptorSets.skybox), 0, NULL); vkCmdBindVertexBuffers(vkctx.drawCmdBuffers[i], VERTEX_BUFFER_BIND_ID, 1, &meshes.skybox.vertices.buf, offsets); vkCmdBindIndexBuffer(vkctx.drawCmdBuffers[i], meshes.skybox.indices.buf, 0, VK_INDEX_TYPE_UINT32); vkCmdBindPipeline(vkctx.drawCmdBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, pipelines.skybox); vkCmdDrawIndexed(vkctx.drawCmdBuffers[i], meshes.skybox.indexCount, 1, 0, 0, 0); } // 3D object vkCmdBindDescriptorSets(vkctx.drawCmdBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, 1, &static_cast(descriptorSets.object), 0, NULL); vkCmdBindVertexBuffers(vkctx.drawCmdBuffers[i], VERTEX_BUFFER_BIND_ID, 1, &meshes.objects[meshes.objectIndex].vertices.buf, offsets); vkCmdBindIndexBuffer(vkctx.drawCmdBuffers[i], meshes.objects[meshes.objectIndex].indices.buf, 0, VK_INDEX_TYPE_UINT32); vkCmdBindPipeline(vkctx.drawCmdBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, pipelines.reflect); vkCmdDrawIndexed(vkctx.drawCmdBuffers[i], meshes.objects[meshes.objectIndex].indexCount, 1, 0, 0, 0); vkCmdEndRenderPass(vkctx.drawCmdBuffers[i]); VK_CHECK_RESULT(vkEndCommandBuffer(vkctx.drawCmdBuffers[i])); } } void TextureCubemap::loadMeshes() { std::string filepath = getAssetPath() + "models/"; // Skybox loadMesh(filepath + "cube.obj", &meshes.skybox, vertexLayout, 0.05f); // Objects meshes.objects.resize(3); loadMesh(filepath + "sphere.obj", &meshes.objects[0], vertexLayout, 0.05f); loadMesh(filepath + "teapot.dae", &meshes.objects[1], vertexLayout, 0.05f); loadMesh(filepath + "torusknot.obj", &meshes.objects[2], vertexLayout, 0.05f); } void TextureCubemap::setupVertexDescriptions() { // Binding description vertices.bindingDescriptions.resize(1); vertices.bindingDescriptions[0] = vk::VertexInputBindingDescription( VERTEX_BUFFER_BIND_ID, vkMeshLoader::vertexSize(vertexLayout), vk::VertexInputRate::eVertex); // Attribute descriptions // Describes memory layout and shader positions vertices.attributeDescriptions.resize(3); // Location 0 : Position vertices.attributeDescriptions[0] = vk::VertexInputAttributeDescription( 0, VERTEX_BUFFER_BIND_ID, vk::Format::eR32G32B32Sfloat, 0); // Location 1 : Vertex normal vertices.attributeDescriptions[1] = vk::VertexInputAttributeDescription( 1, VERTEX_BUFFER_BIND_ID, vk::Format::eR32G32B32Sfloat, sizeof(float) * 3); // Location 2 : Texture coordinates vertices.attributeDescriptions[2] = vk::VertexInputAttributeDescription( 2, VERTEX_BUFFER_BIND_ID, vk::Format::eR32G32Sfloat, sizeof(float) * 6); vertices.inputState = vk::PipelineVertexInputStateCreateInfo(); vertices.inputState.vertexBindingDescriptionCount = static_cast(vertices.bindingDescriptions.size()); vertices.inputState.pVertexBindingDescriptions = vertices.bindingDescriptions.data(); vertices.inputState.vertexAttributeDescriptionCount = static_cast(vertices.attributeDescriptions.size()); vertices.inputState.pVertexAttributeDescriptions = vertices.attributeDescriptions.data(); } void TextureCubemap::setupDescriptorPool() { // Example uses one ubo and one image sampler std::vector poolSizes = { {vk::DescriptorType::eUniformBuffer, 2}, {vk::DescriptorType::eCombinedImageSampler, 2} }; vk::DescriptorPoolCreateInfo descriptorPoolInfo( {}, 2, static_cast(poolSizes.size()), poolSizes.data()); vkctx.device.createDescriptorPool(&descriptorPoolInfo, nullptr, &descriptorPool); } void TextureCubemap::setupDescriptorSetLayout() { std::vector setLayoutBindings = { // Binding 0 : Vertex shader uniform buffer { 0, vk::DescriptorType::eUniformBuffer, 1, vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment, }, // Binding 1 : Fragment shader image sampler { 1, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment }, }; vk::DescriptorSetLayoutCreateInfo descriptorLayout( {}, static_cast(setLayoutBindings.size()), setLayoutBindings.data()); vkctx.device.createDescriptorSetLayout(&descriptorLayout, nullptr, &descriptorSetLayout); vk::PipelineLayoutCreateInfo pipelineLayoutCreateInfo( {}, 1, &descriptorSetLayout); vkctx.device.createPipelineLayout(&pipelineLayoutCreateInfo, nullptr, &pipelineLayout); } void TextureCubemap::setupDescriptorSets() { vk::DescriptorSetAllocateInfo allocInfo( descriptorPool, 1, &descriptorSetLayout); vkctx.device.allocateDescriptorSets(&allocInfo, &descriptorSets.object); // Image descriptor for the cubemap texture vk::DescriptorImageInfo cubeMapDescriptor( sampler, imageView, vk::ImageLayout::eShaderReadOnlyOptimal); std::vector writeDescriptorSets = { // Binding 0 : Vertex shader uniform buffer vk::WriteDescriptorSet( descriptorSets.object, 0, 0, 1, vk::DescriptorType::eUniformBuffer, nullptr, &uniformData.object.descriptor), // Binding 1 : Fragment shader cubemap sampler vk::WriteDescriptorSet( descriptorSets.object, 1, 0, 1, vk::DescriptorType::eCombinedImageSampler, &cubeMapDescriptor) }; vkctx.device.updateDescriptorSets( static_cast(writeDescriptorSets.size()), writeDescriptorSets.data(), 0, nullptr); // Sky box descriptor set vkctx.device.allocateDescriptorSets(&allocInfo, &descriptorSets.skybox); writeDescriptorSets = { // Binding 0 : Vertex shader uniform buffer vk::WriteDescriptorSet( descriptorSets.skybox, 0, 0, 1, vk::DescriptorType::eUniformBuffer, nullptr, &uniformData.skybox.descriptor), // Binding 1 : Fragment shader texture sampler vk::WriteDescriptorSet( descriptorSets.skybox, 1, 0, 1, vk::DescriptorType::eCombinedImageSampler, &cubeMapDescriptor) }; vkctx.device.updateDescriptorSets( static_cast(writeDescriptorSets.size()), writeDescriptorSets.data(), 0, nullptr); } void TextureCubemap::preparePipelines() { vk::PipelineInputAssemblyStateCreateInfo inputAssemblyState( {}, vk::PrimitiveTopology::eTriangleList); vk::PipelineRasterizationStateCreateInfo rasterizationState; // Must be false because we haven't enabled the depthClamp device feature. rasterizationState.depthClampEnable = false; rasterizationState.rasterizerDiscardEnable = VK_FALSE; rasterizationState.polygonMode = vk::PolygonMode::eFill; rasterizationState.cullMode = vk::CullModeFlagBits::eBack; // Make the faces on the inside of the cube the front faces. The mesh // was designed with the exterior faces as the front faces for OpenGL's // default of GL_CCW. #if !USE_GL_RH_NDC rasterizationState.frontFace = vk::FrontFace::eCounterClockwise; #else rasterizationState.frontFace = vk::FrontFace::eClockwise; #endif rasterizationState.lineWidth = 1.0f; vk::PipelineColorBlendAttachmentState blendAttachmentState; blendAttachmentState.blendEnable = VK_FALSE; //blendAttachmentState.colorWriteMask = 0xf; blendAttachmentState.colorWriteMask = vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG | vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA; vk::PipelineColorBlendStateCreateInfo colorBlendState; colorBlendState.attachmentCount = 1; colorBlendState.pAttachments = &blendAttachmentState; vk::PipelineDepthStencilStateCreateInfo depthStencilState; depthStencilState.depthTestEnable = VK_FALSE; depthStencilState.depthWriteEnable = VK_FALSE; depthStencilState.depthCompareOp = vk::CompareOp::eLessOrEqual; vk::PipelineViewportStateCreateInfo viewportState; viewportState.viewportCount = 1; viewportState.scissorCount = 1; vk::PipelineMultisampleStateCreateInfo multisampleState; multisampleState.rasterizationSamples = vk::SampleCountFlagBits::e1; std::vector dynamicStateEnables = { vk::DynamicState::eViewport, vk::DynamicState::eScissor }; vk::PipelineDynamicStateCreateInfo dynamicState( {}, static_cast(dynamicStateEnables.size()), dynamicStateEnables.data()); // Load shaders std::array shaderStages; std::string filepath = getAssetPath() + "shaders/"; shaderStages[0] = loadShader(filepath + "skybox.vert.spv", vk::ShaderStageFlagBits::eVertex); shaderStages[1] = loadShader(filepath + "skybox.frag.spv", vk::ShaderStageFlagBits::eFragment); vk::GraphicsPipelineCreateInfo pipelineCreateInfo; pipelineCreateInfo.layout = pipelineLayout; pipelineCreateInfo.renderPass = vkctx.renderPass; pipelineCreateInfo.pVertexInputState = &vertices.inputState; pipelineCreateInfo.pInputAssemblyState = &inputAssemblyState; pipelineCreateInfo.pRasterizationState = &rasterizationState; pipelineCreateInfo.pColorBlendState = &colorBlendState; pipelineCreateInfo.pMultisampleState = &multisampleState; pipelineCreateInfo.pViewportState = &viewportState; pipelineCreateInfo.pDepthStencilState = &depthStencilState; pipelineCreateInfo.pDynamicState = &dynamicState; pipelineCreateInfo.stageCount = (uint32_t)shaderStages.size(); pipelineCreateInfo.pStages = shaderStages.data(); vkctx.device.createGraphicsPipelines(vkctx.pipelineCache, 1, &pipelineCreateInfo, nullptr, &pipelines.skybox); // Cube map reflect pipeline shaderStages[0] = loadShader(filepath + "reflect.vert.spv", vk::ShaderStageFlagBits::eVertex); shaderStages[1] = loadShader(filepath + "reflect.frag.spv", vk::ShaderStageFlagBits::eFragment); // Enable depth test and write depthStencilState.depthWriteEnable = VK_TRUE; depthStencilState.depthTestEnable = VK_TRUE; // Flip cull mode rasterizationState.cullMode = vk::CullModeFlagBits::eFront; vkctx.device.createGraphicsPipelines(vkctx.pipelineCache, 1, &pipelineCreateInfo, nullptr, &pipelines.reflect); } // Prepare and initialize uniform buffer containing shader uniforms void TextureCubemap::prepareUniformBuffers() { // 3D objact vkctx.createBuffer( vk::BufferUsageFlagBits::eUniformBuffer, vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent, sizeof(ubo), nullptr, &uniformData.object.buffer, &uniformData.object.memory, &uniformData.object.descriptor); // Skybox vkctx.createBuffer( vk::BufferUsageFlagBits::eUniformBuffer, vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent, sizeof(ubo), nullptr, &uniformData.skybox.buffer, &uniformData.skybox.memory, &uniformData.skybox.descriptor); updateUniformBuffers(); } void TextureCubemap::updateUniformBuffers() { // 3D object glm::mat4 viewMatrix = glm::mat4(); ubo.projection = glm::perspective(glm::radians(60.0f), (float)w_width / (float)w_height, 0.001f, 256.0f); viewMatrix = glm::translate(viewMatrix, glm::vec3(0.0f, 0.0f, zoom)); ubo.modelView = glm::mat4(); ubo.modelView = viewMatrix * glm::translate(ubo.modelView, cameraPos); ubo.modelView = glm::rotate(ubo.modelView, glm::radians(rotation.x), glm::vec3(1.0f, 0.0f, 0.0f)); ubo.modelView = glm::rotate(ubo.modelView, glm::radians(rotation.y), glm::vec3(0.0f, 1.0f, 0.0f)); ubo.modelView = glm::rotate(ubo.modelView, glm::radians(rotation.z), glm::vec3(0.0f, 0.0f, 1.0f)); // Do the inverse here because doing it in every fragment is a bit much. // Also MetalSL does not have inverse() and does not support passing // transforms between stages. ubo.invModelView = glm::inverse(ubo.modelView); uint8_t *pData; VK_CHECK_RESULT(vkMapMemory(vkctx.device, uniformData.object.memory, 0, sizeof(ubo), 0, (void **)&pData)); memcpy(pData, &ubo, sizeof(ubo)); vkUnmapMemory(vkctx.device, uniformData.object.memory); // Skybox // Remove translation from modelView so the skybox doesn't move. ubo.modelView = glm::mat4(glm::mat3(ubo.modelView)); // Inverse not needed by skybox. VK_CHECK_RESULT(vkMapMemory(vkctx.device, uniformData.skybox.memory, 0, sizeof(ubo), 0, (void **)&pData)); memcpy(pData, &ubo, sizeof(ubo)); vkUnmapMemory(vkctx.device, uniformData.skybox.memory); } void TextureCubemap::prepareSamplerAndView() { // Create sampler. vk::SamplerCreateInfo samplerInfo; // Set the non-default values samplerInfo.magFilter = vk::Filter::eLinear; samplerInfo.minFilter = vk::Filter::eLinear; samplerInfo.mipmapMode = vk::SamplerMipmapMode::eLinear; samplerInfo.maxLod = (float)cubeMap.levelCount; if (vkctx.gpuFeatures.samplerAnisotropy == VK_TRUE) { samplerInfo.anisotropyEnable = VK_TRUE; samplerInfo.maxAnisotropy = 8; } else { // vulkan.hpp needs fixing samplerInfo.maxAnisotropy = 1.0; } samplerInfo.borderColor = vk::BorderColor::eFloatOpaqueWhite; sampler = vkctx.device.createSampler(samplerInfo); // Create image view. // Textures are not directly accessed by the shaders and are abstracted // by image views containing additional information and sub resource // ranges. vk::ImageViewCreateInfo viewInfo; // Set the non-default values. viewInfo.image = cubeMap.image; viewInfo.format = static_cast(cubeMap.imageFormat); viewInfo.viewType = static_cast(cubeMap.viewType); viewInfo.subresourceRange.aspectMask = vk::ImageAspectFlagBits::eColor; viewInfo.subresourceRange.layerCount = cubeMap.layerCount; viewInfo.subresourceRange.levelCount = cubeMap.levelCount; imageView = vkctx.device.createImageView(viewInfo); } void TextureCubemap::prepare() { prepareSamplerAndView(); loadMeshes(); setupVertexDescriptions(); prepareUniformBuffers(); setupDescriptorSetLayout(); preparePipelines(); setupDescriptorPool(); setupDescriptorSets(); vkctx.createDrawCommandBuffers(); buildCommandBuffers(); } void TextureCubemap::toggleSkyBox() { displaySkybox = !displaySkybox; rebuildCommandBuffers(); } void TextureCubemap::toggleObject() { meshes.objectIndex++; if (meshes.objectIndex >= static_cast(meshes.objects.size())) { meshes.objectIndex = 0; } rebuildCommandBuffers(); } void TextureCubemap::changeLodBias(float delta) { ubo.lodBias += delta; if (ubo.lodBias < 0.0f) { ubo.lodBias = 0.0f; } if (ubo.lodBias > cubeMap.levelCount) { ubo.lodBias = (float)cubeMap.levelCount; } updateUniformBuffers(); //updateTextOverlay(); } void TextureCubemap::keyPressed(uint32_t keyCode) { switch (keyCode) { case 's': toggleSkyBox(); break; case ' ': toggleObject(); break; case SDLK_KP_PLUS: changeLodBias(0.1f); break; case SDLK_KP_MINUS: changeLodBias(-0.1f); break; } } void TextureCubemap::getOverlayText(VulkanTextOverlay *textOverlay, float yOffset) { std::stringstream ss; ss << std::setprecision(2) << std::fixed << ubo.lodBias; textOverlay->addText("Press \"s\" to toggle skybox", 5.0f, yOffset, VulkanTextOverlay::alignLeft); textOverlay->addText("Press \"space\" to change object", 5.0f, yOffset+20.0f, VulkanTextOverlay::alignLeft); textOverlay->addText("LOD bias: " + ss.str() + " (numpad +/- to change)", 5.0f, yOffset+40.0f, VulkanTextOverlay::alignLeft); } const char* TextureCubemap::customizeTitle(const char* const baseTitle) { if (transcoded) { this->title = baseTitle; this->title += " Transcoded to "; this->title += vkFormatString((VkFormat)transcodedFormat); return this->title.c_str(); } return baseTitle; }