Vulkan教程 – 25 多重采样(教程结束)

现在我们的程序可以加载贴图LOD了,可以在渲染远处对象的时候修复假象。现在图像看起来更平滑了,但是离近看的时候会发现几何边缘线有凹凸不平锯齿状的图案。

这个并非我们想要的效果就是锯齿,是用于渲染的像素个数受限导致的。由于没有显示器是无限多像素的,所以总会在某些程度上看到该现象。有一些方法可以处理该问题,本章我们着重看一个比较流行的做法,就是多重采样(MSAA)。

一般的渲染中,像素颜色是由单个采样点决定的,这在大多数情况下就是指屏幕上目标像素的中心点。绘制的线穿过某个像素但是却不覆盖采样点的时候,该像素就会是空白,导致锯齿状阶梯效果。

多重采样所做的就是每个像素使用多个采样点,最终确定下来颜色。更多的采样会有更好的结果,但是计算消耗也更高。

我们这里的实现主要关注使用最大可用采样个数。根据你的程序,这可能不是最好的方法。

 我们先看下硬件可用的采样个数。大多数现代GPU支持最少8位采样,但是也不能保证所有都满足。我们添加一个新的类成员:

VkSampleCountFlagBits msaaSamples = VK_SAMPLE_COUNT_1_BIT;

默认情况下,我们对单个像素使用一个采样,这也就是没有多重采样的操作,最终图像也不会改变。最大采样个数可以用VkPhysicalDeviceProperties获取。我们要使用深度缓冲,所以我们要将颜色和深度的采样数都考虑到。我们添加一个方法来获取这些信息:

VkSampleCountFlagBits getMaxUsableSampleCount() {
    VkPhysicalDeviceProperties physicalDeviceProperties;
    vkGetPhysicalDeviceProperties(physicalDevice, &physicalDeviceProperties);

    VkSampleCountFlags counts = std::min(physicalDeviceProperties.limits.framebufferColorSampleCounts,
        physicalDeviceProperties.limits.framebufferDepthSampleCounts);

    if (counts & VK_SAMPLE_COUNT_64_BIT) {
        return VK_SAMPLE_COUNT_64_BIT;
    }

    if (counts & VK_SAMPLE_COUNT_32_BIT) {
        return VK_SAMPLE_COUNT_32_BIT;
    }

    if (counts & VK_SAMPLE_COUNT_16_BIT) {
        return VK_SAMPLE_COUNT_16_BIT;
    }

    if (counts & VK_SAMPLE_COUNT_8_BIT) {
        return VK_SAMPLE_COUNT_8_BIT;
    }

    if (counts & VK_SAMPLE_COUNT_4_BIT) {
        return VK_SAMPLE_COUNT_4_BIT;
    }

    if (counts & VK_SAMPLE_COUNT_2_BIT) {
        return VK_SAMPLE_COUNT_2_BIT;
    }

    return VK_SAMPLE_COUNT_1_BIT;
}

我们现在使用该方法在物理设备选取过程中设置msaaSamples变量,为此,我们要稍微修改下pickPhysicalDevice方法:

void pickPhysicalDevice() {
    ...
    for (const auto& device : devices) {
        if (isDeviceSuitable(device)) {
            physicalDevice = device;
            msaaSamples = getMaxUsableSampleCount();
            break;
        }
    }
    ...
}

MSAA中,每个像素都是在离屏缓冲中采样的,该离屏缓冲之后会渲染到屏幕。这个新缓冲和普通图像有些不同,它们要能为每个像素存储多个采样。一旦创建了多采样缓冲,它就要解析到默认帧缓冲(该缓冲每个像素保存了一个采样)。这就是为什么我们要创建一个附加的渲染对象,因为每次就只有一个渲染操作是活跃的,就和深度缓冲一样。添加下面的类成员:

VkImage colorImage;
VkDeviceMemory colorImageMemory;
VkImageView colorImageView;

该新图像会为每个像素存储所需个数的采样,所以我们要传递该个数到VkImageCreateInfo。修改createImage:

void createImage(uint32_t width, uint32_t height, uint32_t mipLevels,
    VkSampleCountFlagBits numSamples, VkFormat format,
    VkImageTiling tiling, VkImageUsageFlags usage, VkMemoryPropertyFlags properties,
    VkImage& image, VkDeviceMemory& imageMemory) {
    ...
    imageInfo.samples = numSamples;
    ...
}

然后修改该方法的所有调用:

createImage(swapChainExtent.width, swapChainExtent.height, 1, VK_SAMPLE_COUNT_1_BIT, depthFormat,
    VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT,
    VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, depthImage, depthImageMemory);
...
createImage(texWidth, texHeight, mipLevels, VK_SAMPLE_COUNT_1_BIT, VK_FORMAT_R8G8B8A8_UNORM,
    VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_TRANSFER_SRC_BIT |
    VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT,
    VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, textureImage, textureImageMemory);

现在我们创建一个多重采样颜色缓冲。添加一个createColorResources方法并注意我们这里要使用msaaSamples作为一个方法参数来调用createImage。我们还要使用单个Mip贴图等级,这是Vulkan指定的,以防单个像素有多个采样。另外,该颜色缓冲不需要Mip贴图,因为它不是作为一个材质使用的:

void createColorResources() {
    VkFormat colorFormat = swapChainImageFormat;

    createImage(swapChainExtent.width, swapChainExtent.height, 1, msaaSamples, colorFormat,
        VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT |
        VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, colorImage,
        colorImageMemory);

    colorImageView = createImageView(colorImage, colorFormat, VK_IMAGE_ASPECT_COLOR_BIT, 1);

    transitionImageLayout(colorImage, colorFormat, VK_IMAGE_LAYOUT_UNDEFINED,
        VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, 1);
}

为了连续性,在initVulkan的createDepthResources之前调用该方法。

你可能注意到了新创建的颜色图像使用了VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL转移,之前是VK_IMAGE_LAYOUT_UNDEFINED。我们修改下transitionImageLayout来应用该改变:

void transitionImageLayout(VkImage image, VkFormat format, VkImageLayout oldLayout,
    VkImageLayout newLayout, uint32_t mipLevels) {
    ...
    else if(oldLayout == VK_IMAGE_LAYOUT_UNDEFINED &&
        newLayout == VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL) {
        barrier.srcAccessMask = 0;
        barrier.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_READ_BIT |
            VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;

        sourceStage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
        destinationStage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
    } else {
        throw std::invalid_argument("unsupported layout transition!");
    }
    ...
}

现在我们有一个多重采样的颜色缓冲了,下面开始准备深度的。修改createDepthResources并更新深度缓冲使用的采样个数:

createImage(swapChainExtent.width, swapChainExtent.height, 1, msaaSamples, depthFormat,
    VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT,
    VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, depthImage, depthImageMemory);

我们现在创建了一些Vulkan资源,别忘了进行释放:

void cleanupSwapChain() {
    vkDestroyImageView(device, colorImageView, nullptr);
    vkDestroyImage(device, colorImage, nullptr);
    vkFreeMemory(device, colorImageMemory, nullptr);
    ...
}

更新recreateSwapChain以便新颜色图像可以在窗口大小改变的时候用正确的分辨率重建,

createGraphicsPipeline();
createColorResources();
createDepthResources();

现在我们要在管线、帧缓冲和渲染通道中使用这个新的资源了。

先看渲染通道。修改createRenderPass并更新颜色和深度附件结构体:

colorAttachment.samples = msaaSamples;
colorAttachment.finalLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
depthAttachment.samples = msaaSamples;

我们将最终布局从VK_IMAGE_LAYOUT_PRESENT_SRC_KHR换成了VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,因为多重采样的图像不能直接呈现。我们首先要将其解析到一个正常图像,深度缓冲这没这个要求,因为它不会被呈现。因此我们就只需要添加一个新的附件到颜色中,也就是解析附件:

VkAttachmentDescription colorAttachmentResolve = {};
colorAttachmentResolve.format = swapChainImageFormat;
colorAttachmentResolve.samples = VK_SAMPLE_COUNT_1_BIT;
colorAttachmentResolve.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
colorAttachmentResolve.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
colorAttachmentResolve.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
colorAttachmentResolve.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
colorAttachmentResolve.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
colorAttachmentResolve.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;

渲染通道现在要解析多重采样颜色图像到常规附件,创建一个新的附件引用指向颜色缓冲,作为解析目标:

VkAttachmentReference colorAttachmentResolveRef = {};
colorAttachmentResolveRef.attachment = 2;
colorAttachmentResolveRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;

pResolveAttachments设置为指向新创建的附件引用。这就足够让渲染通道定义一个多重采样解析操作了,从而能够让我们将图像渲染到屏幕上:

subpass.pResolveAttachments = &colorAttachmentResolveRef;

现在更新渲染通道信息结构体:

std::array<VkAttachmentDescription, 3> attachments = { colorAttachment, depthAttachment,
    colorAttachmentResolve};

准备好渲染通道了,修改createFrameBuffers并添加新的图像视图到列表中:

std::array<VkImageView, 3> attachments = {
    colorImageView,
    depthImageView,
    swapChainImageViews[i]
};

最终,修改createGraphicsPipeline告诉管线使用多重采样:

multisampling.rasterizationSamples = msaaSamples;

现在运行程序你将看到:

就和Mip贴图一样,差别可能不是那么直接的。仔细看能发现屋顶的边缘性不是那么凹凸不平了,整个图像也更平滑了。

我们当前的MSAA实现还有几点不足,可能导致输出图像质量不好。例如,我们现在没有解决可能由着色器锯齿引起的问题。MSAA只平滑几何图形的边缘,但是不管内部填充。这可能导致你的多边形平滑但是用高对比度颜色的贴图的时候仍然会看起来有锯齿感。有一个办法就是启用采样着色器,从而改善图像质量,虽然有一些性能消耗:

deviceFeatures.sampleRateShading = VK_TRUE;

在逻辑设备中添加这句就可以启用采样着色器特性。

在管线创建部分添加:

multisampling.sampleShadingEnable = VK_TRUE;
multisampling.minSampleShading = .2f;

该程序至此要做大量的工作,但是最终你对Vulkan基础知识有了较好的认识。现在你可以准备探索其他内容了,比如多重子通道,多线程命令缓冲生成。

这也就是本教程最后一章,教程就到此结束。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片