着色器模块

不像是之前的API,Vulkan着色器代码一定要用字节码格式,而不是人类可读的语法如GLSL和HLSL。这个字节码就是SPIR-V,设计用于Vulkan和OpenCL。这是一个可以用于编写图形和计算着色器的格式,但是我们主要关注的是Vulkan的图形管线。使用字节码格式的优点之一是GPU厂商写的编译器将着色器代码转化为原生代码会非常简单。过去的经验表明,人类易读的语法如GLSL,某些GPU厂商是能很便捷地解读这些标准的。但是如果你碰巧写了不一般的着色器,那可能会导致厂家的着色器因为你的语法错误而拒绝执行,甚至更糟,就是能执行,却因为编译器bug得到的是错误的结果。直接用字节码格式就能避免这些问题。

但是,这不表示我们要自己动手写字节码,Khronos已经发行了他们自己的厂商无关的编译器,能够将GLSL编译到SPIR-V格式。这个编译器就是用来验证你的着色器都是和标准兼容的,它会产生一个SPIR-V的二进制输出,可以和你的程序一同发行。该编译器包含在LunarG SDK中了,也就是glslangValidator.exe,所以不用额外下载任何内容。

GLSL是C语法风格的着色器语言,用它写的程序有一个main方法来让每个对象调用。没有用参数作为输入,返回一个值作为输出这种做法,GLSL使用了全局变量来处理输入和输出。该语言包含了许多特性以便于图形编程,比如内建的向量和矩阵原型。叉乘,矩阵-向量相乘,向量反射之类操作用的函数都包括在内。

向量类型叫做vec,后面跟着一个数字表示元素个数。比如一个3D位置应该存储为vec3。可以用类似.x的方式获取其单独的组件,但是也可能会创建一个新的变量,比如vec3(1.0, 2.0, 3.0).xy就会得到一个vec2。向量的构造器也可以接受向量对象的组合以及标量值,比如vec3可以用vec3(vec2(1.0, 2.0), 3.0)构造。

像之前章节提到的,我们要写一个顶点着色器和片段着色器,以便将三角形显示到屏幕上。下面两部分会介绍每一部分的GLSL代码,之后我会介绍如何产生两份SPIR-V二进制文件并加载到程序中。

第一部分,顶点着色器。顶点着色器处理每个到来的顶点,用其属性如世界坐标,颜色,法线和材质坐标等作为输入。输出是最终在裁剪坐标的位置和需要传递给片段着色器的属性,比如颜色和材质坐标。这些值会被片段着色器根据光栅器插值,产生平滑的梯度。

裁剪坐标是来自顶点着色器的四维向量,随后被通过最后一个元素除以整个向量转变成一个归一化设备坐标。这些归一化设备坐标是齐次坐标,将帧缓冲映射到纵横都是[-1, 1]的坐标系,如下:

我们第一个三角形不用任何变换,我们就直接明确三个点的位置作为归一化设备坐标,来创建如下的三角形:

我们可以直接输出归一化设备坐标,做法就是将他们作为裁剪坐标从顶点着色器输出,最后一个部分置为1。这样变换裁剪坐标到归一化设备坐标时候的除法操作就什么都保持不变。

通常这些坐标会存储在顶点缓冲中,但是创建顶点缓冲并填充数据在Vulkan中并非微不足道。因此我们决定先将其推迟,直到我们满足地看到三角形绘制到了屏幕上。同时我们还要做一些不太正统的东西:直接将坐标包含在顶点着色器中。代码如下:

#version 450vec2 positions[3] = vec2[](vec2(0.0, -0.5),vec2(0.5, 0.5),vec2(-0.5, 0.5)
);void main() {gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0);
}

main方法是每个顶点都涉及的,内置的gl_VertexIndex变量包含了当前顶点的索引。这通常是顶点缓冲的索引,但是我们这儿它就是硬编码数组的顶点数据的索引。每个顶点的位置通过着色器的连续数组获取,且和虚拟z和w部分一起组成一个裁剪坐标的位置。内置变量gl_Position作为输出。

第二部分,片段着色器。由来自顶点着色器的位置形成的三角形用片段来填充屏幕上的区域。片段着色器就在这些片段上执行来为帧缓冲产生一个颜色和深度。一个简单的为整个三角形输出红色的片段着色器如下:

#version 450
#extension GL_ARB_separate_shader_objects : enablelayout(location = 0) out vec4 outColor;void main() {outColor = vec4(1.0, 0.0, 0.0, 1.0);
}

main方法被每个片段调用,就和顶点着色器的main方法被每个顶点调用一样。GLSL的颜色是由4部分组成的向量,就是RGB和alpha通道,范围都是[0, 1]。不像是顶点着色器的gl_Position,没有内置变量为当前片段输出一个颜色。你必须为每个帧缓冲明确自己的输出变量,布局(location = 0)修改器明确了帧缓冲的索引。这里outColor写成红色,和索引为0的第一个帧缓冲连接起来。

整个三角形都设置红色不太好玩,下面这种看起来会好很多:

我们必须对两个着色器做一些改变来实现该效果。首先我们要为每个顶点明确一个特定颜色,顶点着色器应该包含一个颜色数组,就像坐标一样:

vec3 colors[3] = vec3[](vec3(1.0, 0.0, 0.0),vec3(0.0, 1.0, 0.0),vec3(0.0, 0.0, 1.0)
);

现在我们要将这每个顶点的颜色传递给片段着色器,以便它输出插值给帧缓冲。给顶点着色器添加一个颜色输出,在main方法中写入:

layout(location = 0) out vec3 fragColor;void main(){gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0);fragColor = colors[gl_VertexIndex];
}

接下来,我们需要在片段着色器添加一个匹配输入:

layout(location = 0) in vec3 fragColor;void main() {outColor = vec4(fragColor, 1.0);
}

输入变量不一定要用相同的名称,它们会使用location中的索引来连接。main方法已经被修改了,以输出颜色和透明度。fragColor的值会被自动插值,得到平滑梯度效果。

接着就是编译着色器了,在项目根目录创建一个shaders目录,存储我们的着色器代码。两份着色器分别是shader.vert和shader.frag,GLSL没有官方扩展名,但是这两个通常用于区分他们。

shader.vert如下所示:

#version 450
#extension GL_ARB_separate_shader_objects : enablelayout(location = 0) out vec3 fragColor;vec2 positions[3] = vec2[](vec2(0.0, -0.5),vec2(0.5, 0.5),vec2(-0.5, 0.5)
);vec3 colors[3] = vec3[](vec3(1.0, 0.0, 0.0),vec3(0.0, 1.0, 0.0),vec3(0.0, 0.0, 1.0)
);void main(){gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0);fragColor = colors[gl_VertexIndex];
}

shader.frag如下:

#version 450
#extension GL_ARB_separate_shader_objects : enablelayout(location = 0) in vec3 fragColor;layout(location = 0) out vec4 outColor;void main() {outColor = vec4(fragColor, 1.0);
}

现在我们准备用glslangValidator将其编译成SPIR-V字节码:

glslc shader.vert -o vert.spv
glslc shader.frag -o frag.spv

这两条命令用-V标志调用了编译器,表明要求编译器将GLSL源文件编译成SPIR-V字节码。当你运行编译脚本的时候,就会发现两个SPIR-V二进制文件产生了,即vert.spv和frag.spv。这些名字直接来自shader类型,但是你可以进行重命名。Vulkan SDK包含了libshaderc,也就是将你的GLSL代码编译成SPIR-V的东西。

接着是加载着色器部分。

当前我们可以产生SPIR-V着色器了,是时候将其加载到我们的程序中了,然后在某个时刻将其插入到图形管线中。我们先要写一个简单的助手方法来从文件加载二进制数据:

#include <fstream>
#include <vector>static std::vector<char> readFile(const std::string& filename) {std::ifstream file(filename, std::ios::ate | std::ios::binary);if (!file.is_open()) {throw std::runtime_error("failed to open file!");}
}

readFile方法会从指定文件读取所有字节,返回std::vector管理的byte数组。我们用两个标记打开该文件:

ate:开始读的时候在文件末尾,就是说打开文件的时候定位到文件尾;

binary:以二进制文件读取文件(避免text转换)。

开始读的时候定位到文件尾的好处是我们能利用读取位置来确定文件大小,然后分配一个缓冲:

size_t fileSize = (size_t)file.tellg();
std::vector<char> buffer(fileSize);

之后,我们可以查找到文件开头处来一次性读取所有字节:

file.seekg(0);
file.read(buffer.data(), fileSize);

最后关闭文件并返回字节:

file.close();return buffer;

现在我们从createGraphicsPipeline调用该方法:

void createGraphicsPipeline() {auto vertShaderCode = readFile("shaders/vert.spv");auto fragShaderCode = readFile("shaders/frag.spv");
}

下面准备创建着色器模块,在开始将代码传递到管线之前,我们需要将其包装到VkShaderModule对象中,创建一个createShaderModule方法:

VkShaderModule createShaderModule(const std::vector<char>& code) {}

该方法会接收一个字节码缓冲作为参数,创建一个VkShaderModule出来。创建着色器模块是很容易的,只需要指定一个指向到缓冲的指针,以及它的长度。这些信息都在VkShaderModuleCreateInfo结构体中,有一点要注意的是字节码的大小是用字节指定的,但是字节码指针是uint32_t类型的指针而不是char类型的指针。因此我们将需要用reinterpret_cast转换,转换的时候要保证数据满足uint32_t的对齐要求。幸好该数据就是存储在vector中的,其默认分配器就保证了最差情况的对齐要求。

VkShaderModuleCreateInfo createInfo = {};
createInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
createInfo.codeSize = code.size();
createInfo.pCode = reinterpret_cast<const uint32_t*>(code.data());

VkShaderModule就能通过调用vkCreateShaderModule调用了:

VkShaderModule shaderModule;
if (vkCreateShaderModule(device, &createInfo, nullptr, &shaderModule) != VK_SUCCESS) {throw std::runtime_error("failed to create shader module!");
}

参数和之前创建对象的方法中的类似:逻辑设备,指向创建信息结构体的指针,可选的自定义分配器指针以及处理输出的变量。缓冲可以在创建着色器模块后立即释放,别忘记返回着色器模块:

return shaderModule;

着色器模块就是着色器字节码的一个简单的包装。编译和链接SPIR-V字节码到机器码以便GPU执行是要等到渲染管线创建后才会做的,因此我们要在创建管线后销毁着色器模块,所以要在createGraphicsPipeline中将其设置为局部变量,而不是作为类成员:

VkShaderModule vertShaderModule = createShaderModule(vertShaderCode);
VkShaderModule fragShaderModule = createShaderModule(fragShaderCode);

清理就在方法结束的时候添加两行:

vkDestroyShaderModule(device, fragShaderModule, nullptr);
vkDestroyShaderModule(device, vertShaderModule, nullptr);

之后本章还有一些代码,就放在这两行之前。

为了真的用起来这些着色器,我们需要用VkPipelineShaderStageCreateInfo将它们分配到指定管线阶段,作为真正的管线创建过程的一部分。我们需要填充顶点着色器所需的结构体,还是在createGraphicsPipeline方法中:

VkPipelineShaderStageCreateInfo vertShaderStageInfo = {};
vertShaderStageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
vertShaderStageInfo.stage = VK_SHADER_STAGE_VERTEX_BIT;

第一步,除了必要的sType成员外,都向Vulkan提供了该着色器会在管线哪个阶段使用的信息。每个可编程阶段都有一个枚举值。接下来的两个成员表明了着色器模块以及用触发的方法,也就是入口点。这意味着能组合多个片段着色器为单个着色器模块,用不同的入口点来差异化其表现。但是这里我们还是用标准的main方法:

vertShaderStageInfo.module = vertShaderModule;
vertShaderStageInfo.pName = "main";

还有一个可选成员pSpecializationInfo,这里不用,但是值得讨论一下。它能让你指定着色器常量的值,你可以用单个着色器模块,它的行为会在管线创建阶段通过不同常数值来进行配置。这比渲染的时候用变量配置着色器要高效,因为编译器能够做优化,比如依赖这些值的if语句。你不需要任何那样的常量就置为空指针,也就是结构体初始化自动执行的操作。

修改该结构体以适配片段着色器:

VkPipelineShaderStageCreateInfo fragShaderStageInfo = {};
fragShaderStageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
fragShaderStageInfo.stage = VK_SHADER_STAGE_FRAGMENT_BIT;
fragShaderStageInfo.module = fragShaderModule;
fragShaderStageInfo.pName = "main";

定义一个数组,包含这两个结构体,以后会用到:

VkPipelineShaderStageCreateInfo shaderStages[] = {vertShaderStageInfo, fragShaderStageInfo
};

以上就是所有渲染管线可编程阶段的介绍,下一章看固定管线阶段。

Vulkan教程 - 08 着色器及编译SPIR-V相关推荐

  1. [译]Vulkan教程(08)逻辑设备和队列

    [译]Vulkan教程(08)逻辑设备和队列 Introduction 入门 After selecting a physical device to use we need to set up a  ...

  2. Unity3D教程:着色器

    如下图所示,在Project面板中新建一个New Material.这样子就准备好了一个空白的着色器.它能支持的类型也就在Shader中全部显示出来.不得不说,很好很强大. Unity3D教程:着色器 ...

  3. OpenGL4.0教程 计算着色器简介

    reference:https://antongerdelan.net/opengl/compute.html 本篇文章给出了OpenGL计算着色器的实用介绍,并且我们将要开始制作一个玩具光线追踪渲染 ...

  4. 【译】【PyOpenGL教程-介绍着色器】 漫反射、环境光、平行光

    漫反射.环境光.方向光 原文地址:http://pyopengl.sourceforge.net/context/tutorials/shader_5.html 本教程在以往教程的基础上添加了: 环境 ...

  5. Threejs教程之着色器

    Three.js着色器 Three.js视频教程 很多时候如果想写一些特效,往往需要编写着色器代码GLSL,如果你不熟悉着色器语言,自然需要学习着色器语言语法,如果你有着色器语言基础,直接使用Thre ...

  6. unity烘培单个物体_Unity可编程渲染管线(SRP)教程:二、自定义着色器

    本文翻译自Catlike Coding,原作者:Jasper Flick. 本文经原作者授权,转载请说明出处. 原文链接在下: https://catlikecoding.com/unity/tuto ...

  7. HLSL CG 与glsl着色器编译及其原理

    导言:公司旧的渲染引擎用的结构无语了,要写一个渲染特效现在Unity 用shaderlab实现调试好,Unity插件导出GLTF格式(shader等已经包含在材质信息中),然后再导到自研引擎(用的OP ...

  8. DirectX11--HLSL编译着色器的三种方法

    前言 本教程不考虑Effects11(FX11),而是基于原始的HLSL. 目前编译与加载着色器的方法如下: 使用Visual Studio中的HLSL编译器,随项目编译期间一同编译,并生成.cso( ...

  9. unity Shader Lab(cg hlsl glsl)着色器入门教程 以及 vs2019 支持unity shader语法(更新中2019.9.5)

    前言: 如果你对cg glsl hlsl 顶点着色器 片段着色器 表面着色器 固定渲染管线 等等有所疑惑,或是想学会unity的渲染,看这一篇就足够了.另外我博客的shader分类中还有很多shade ...

  10. Unity ShaderLab特效教程 适用于贴图、sprite和ugui的2d着色器实例 代码+详解注释 【将贴图转为马赛克效果】

    如果代码中有什么不清楚请查看以下基础知识 Shader基础知识 unity3d 中 七种坐标知识详解 一个将贴图转为马赛克效果的shader: 万恶的马赛克其实也是一种艺术风格,如果你在开发2d游戏不 ...

最新文章

  1. 关于Transformer,那些的你不知道的事
  2. QIIME 2教程. 23图形界面q2studio(2020.11)
  3. avro和java原生序列化的区别,java原生序列化和Kryo序列化性能比较
  4. 转:高效代码审查的八条准则和十个经验
  5. msconfig深解
  6. ASP.NET中下载文件的几种方法
  7. Lcs客户端配置和测试
  8. 14.使用 CSS 显示 XML
  9. 一旦辞职,应该立即批准。留一段时间没有好处
  10. Movielens/IMDB电影数据分析(一)
  11. 文献 | 柳叶刀发文:虚拟现实的新用途之治疗场所恐惧症
  12. 功能性需求和非功能性需求
  13. SpringBoot物流管理项目,拿去学习吧(源码)
  14. h5 禁止微信内置浏览器调整字体大小方法
  15. 循环,100遍“好好学习,天天向上”两种方法。
  16. Vista不是黄金甲
  17. APP在电脑模拟器上完美运行,真机运行时闪退现象记录
  18. Python采集二手房源数据信息 基础版, 多线程版
  19. ORA-64203: 目标缓冲区太小, 无法容纳字符集转换之后的 CLOB 数据。
  20. ElementUI 树型组件 el-tree 后台数据结构构建

热门文章

  1. 关于pycharm中代码为灰色以及如何调整代码检查级别的问题
  2. 浏览器安全检查5秒解决方案
  3. 2014年中国行地产排行
  4. Android关于Activity屏蔽/拦截Home键
  5. Using the Hardware Scaler for Performance and Efficiency
  6. 定时器应用—选项卡自动切换
  7. “最新”手机号码归属地库制作
  8. 线上bug快速定位小技巧 - chrome实时调试线上js代码
  9. Sentinel简单使用
  10. JavaScript——onblur事件失效问题解决方案