Hello Vulkan(三)| 图形渲染新技术Vulkan 着色器及SPIR-V


回顾
上一期技术分享中,我们讲述了如何使用Vulkan进行绘制一个正方体,其实只是对Vulkan使用的一个简单例子,通过这个例子大家会对Vulkan图形渲染有了大概的认识,接下来会对一些重点技术进行拆解和更深入的讲解,更有助于我们的开发与实践。
本期分享内容「着色器及SPIR-V」,我们会介绍如何在Vulkan中使用shader。其实早在Vulkan还在开发中的时候,作为开发者,我很担心他们会开发出一个新的shader语言,然后我所有的OpenGL shader都需要重写来适配Vulkan,但幸运的是,他们没有,仍然运用GLSL作为shader语言,尽管有一些细微的变化,所以,在本期也会介绍这些细微的不同。
Shader管线
熟悉图形渲染的开发者一定对这个并不陌生,Vulkan中shader渲染管线和OpenGL是相同的,至少需要顶点着色器(Vertex Shader)和片元着色器(Fragment Shader),其他的着色器stage的缺失是没有问题的。每一层的输出结果将会是下一层的输入,在最后一层片元着色器前,会进行光栅化,所谓光栅化就是将几何数据经过一系列变换后最终转化为像素的过程,光栅化后的结果会被送入片元着色器。

这是Vulkan中Shader stage的类型定义,加粗的部分就是Shader stage:
typedef enum VkPipelineStageFlagBits{VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT = 0x00000001, VK_PIPELINE_STAGE_DRAW_INDIRECT_BIT = 0x00000002, VK_PIPELINE_STAGE_VERTEX_INPUT_BIT = 0x00000004, VK_PIPELINE_STAGE_VERTEX_SHADER_BIT = 0x00000008, VK_PIPELINE_STAGE_TESSELLATION_CONTROL_SHADER_BIT= 0x00000010, VK_PIPELINE_STAGE_TESSELLATION_EVALUATION_SHADER_BIT= 0x00000020,VK_PIPELINE_STAGE_GEOMETRY_SHADER_BIT = 0x00000040, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT = 0x00000080, VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT = 0x00000100, VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT = 0x00000200, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT = 0x00000400, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT = 0x00000800, VK_PIPELINE_STAGE_TRANSFER_BIT = 0x00001000, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT = 0x00002000,VK_PIPELINE_STAGE_HOST_BIT = 0x00004000,VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT = 0x00008000, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT = 0x00010000,} VkPipelineStageFlagBits;
Vulkan GLSL vs. OpenGL GLSL
1.编译器不同
Vulkan使用外部编译器编译,在外部编译器中需要指定版本,类似于这样:#define VULKAN 130。当然,如果要同时兼容OpenGL GLSL和Vulkan GLSL,可以这样定义:
#ifdef VULKAN
. . .
#endif
在else里定义OpenGL版本,就可以解决
2.Vertex&Index indices
Vulkan Vertex indices:
gl_VertexIndex
gl_InstanceIndex
OpenGL:
gl_VertexID
gl_InstanceID
这这点上,其实OpenGL中,对VertexID的称呼并不正确,其实并不是ID,而是Index才对,这一点上Vulkan已经修复了这个错误的叫法,当然这些ID或者Index都是从0开始计数。
3.纹理和Samplers
Vulkan中支持纹理数据和sampler2D组合,比如这样:
uniform sampler s;uniform texture2D t;vec4 rgba = texture( sampler2D( t, s ), vST );
Vulkan编译 – SPIR-V
因为上门介绍了Vulkan编译是需要外部GLSL编译器做前半段预编译,然后shader会以中间形式存在,这种中间形态就是SPIR-V(Standard Portable Intermediate Representation),在程序运行的时候,SPIR-V会完全编译。

SPIR-V有几个明显的优势,开发者再也不会暴露shader源文件,语法错误也会在SPIR-V步骤就会显示,而不是运行时才会报错,因为有预编译过程,程序运行也会更快。
在编译SPIR-V二进制码时,我们需要用到glslangValidator进行编译。glslangValidator是Khronos Group定制的GLSL参考编译器,命令行编译模式方便了用户直接测试GLSL语法而绕过C/C++的相关依赖库编译,也不需要在主文件编写大量初始化代码。
glslangValidator编译对不同功能的GLSL文件后缀名有要求:
.vert Vertex.tesc Tessellation Control.tese Tessellation Evaluation .geom Geometry.frag Fragment.comp Compute
-V  Compile for Vulkan 
-G  Compile for OpenGL
-I   Directory(ies) to look in for #includes
-S  Specify stage rather than get it from shaderfile extension 
-c  Print out the maximum sizes of various properties 
编译命令,比如:glslangValidator shaderFile -V [-H] [-I<dir>] [-S <stage>] -o shaderBinaryFile.spv,Windows下则是使用glslangValidator.exe,Linux/Mac使用glslangValidator,最方便的就是写Makefile文件进行编译:

运行“make allshaders”命令,进行编译。
命令里的-V代表的就是Compile for Vulkan,如果改为-G,就是Compile for OpenGL,而.spv文件就是SPIR-V的二进制码文件。
到这里,已经能够完成Vulkan着色器的SPIR-V编译了,那么如何在在Vulkan Shader Module中读取SPIR-V文件呢?需要将其加载到我们的程序中了,然后在某个时刻将其插入到图形管线中。我们先要写一个简单的助手方法来从文件加载二进制数据:
#ifndef _WIN32typedef int errno_t;int fopen_s( FILE**, const char *, const char * );#endif#define SPIRV_MAGIC 0x07230203…VkResultInit12SpirvShader( std::string filename, VkShaderModule * pShaderModule ){FILE *fp;#ifdef WIN32errno_t err = fopen_s( &fp, filename.c_str( ), "rb" );if( err != 0 )#elsefp = fopen( filename.c_str( ), "rb" );if( fp == NULL )#endif{fprintf( FpDebug, "Cannot open shader file '%s'", filename.c_str( ) );return VK_SHOULD_EXIT;}uint32_t magic;fread( &magic, 4, 1, fp );if( magic != SPIRV_MAGIC ){fprintf( FpDebug, "Magic number for spir-v file '%s is 0x%08x -- should be 0x%08x", filename.c_str( ), magic, SPIRV_MAGIC );return VK_SHOULD_EXIT;}fseek( fp, 0L, SEEK_END );int size = ftell( fp );rewind( fp );unsigned char *code = new unsigned char [size];fread( code, size, 1, fp );fclose( fp );
下面准备创建着色器模块,在开始将代码传递到管线之前,我们需要将其包装到VkShaderModule对象中,创建一个createShaderModule方法。该方法会接收一个字节码缓冲作为参数,创建一个VkShaderModule出来。创建着色器模块是很容易的,只需要指定一个指向到缓冲的指针,以及它的长度。这些信息都在VkShaderModuleCreateInfo结构体中,有一点要注意的是字节码的大小是用字节指定的,但是字节码指针是uint32_t类型的指针而不是char类型的指针,类似以下代码:
…VkShaderModule ShaderModuleVertex;...VkShaderModuleCreateInfo vsmci;vsmci.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;vsmci.pNext = nullptr;vsmci.flags = 0;vsmci.codeSize = size;vsmci.pCode = (uint32_t *)code;VkResult result = vkCreateShaderModule( LogicalDevice, &vsmci, PALLOCATOR, OUT & ShaderModuleVertex );fprintf( FpDebug, "Shader Module '%s' successfully loaded", filename.c_str() );delete [ ] code;return result;
到这儿所有的Vulkan着色器编译内容就结束了。欢迎大家关注虹图AI开放平台公众号,后台留言交流,也欢迎大家移步至我们刚上线的开发者社区中交流和分享。
关于Vulkan以及shader相关的实践内容、以及其他的如缓冲区等内容,我们会在本系列后续内容中继续与大家分享。(虹图人像人体中美颜SDK部分是基于Vulkan进行开发封装的,性能极致,对开发者更加友好,十行之内完成一个简单的demo,点击【阅读原文】可查看详情。)
敬请期待~
到顶部