Shaders

Shaders

[TOC]

说明

内容

  • Converting GLSL shaders to SPIR-V assemblies
  • Writing vertex shaders
  • Writing tessellation control shaders
  • Writing tessellation evaluation shaders
  • Writing geometry shaders
  • Writing fragment shaders
  • Writing compute shaders
  • Writing a vertex shader that multiplies a vertex position by a projection matrix
  • Using push constants in shaders
  • Writing a texturing vertex and fragment shaders
  • Displaying polygon normals with a geometry shader

介绍

3D graphics data(vertices和fragments/pixels)运行在叫做stages的一些列步骤里.某些stages只执行固定操作(只能配置某些扩展操作).单有其他stages需要programmed.控制这些stages的行为的小programs称为shaders.

vulkan里最主要的5个可编程graphic pipeline stages为vertex,tessellation control,evaluation,geometry,fragment.(1.1.85发布了Raytracing, Mesh Shaders & Other New NVIDIA Extensions,https://www.phoronix.com/scan.php?page=news_item&px=Vulkan-1.1.85-Released).compute pipeline有compute shader programs.core Vulkan API 通过SPIR-V的programs控制这些stages.他是一门中间语言,允许我们process graphics data和进行vectors,matrices,images,buffers,samplers的数学计算.这种语言的low-level特性提高了编译时间(compilation times).也使得书写shader困难.所以vulkan sdk提供一个工具链glslangValidator.

glslangValidator可以将GLSL转换为SPIR-V assemblies.

本文介绍GLSL,所有programmable stages的shaders实现,如何实现tessellation或texturing,如何使用geometry shaders来debuging.以及将GLSL转换为SPIR-V assemblies(使用glslangValidator).

Converting GLSL shaders to SPIR-V assemblies

shaders与stage

  • vert for the vertex shader stage
  • tesc for the tessellation control shader stage
  • tese for the tessellation evaluation shader stage
  • geom for the geometry shader stage
  • frag for the fragment shader stage
  • comp for the compute shader

Writing vertex shaders

顶点.

如果想绘制nosolid geometry,需要在创建logical device是激活fillModeNonSolid属性.

vertex processing是graphics pipeline第一个stage,主要目的是对顶点进行坐标转换,从局部坐标转换到coordinate system(clip space).这个clip coordinate system用于图形硬件后续步骤.其一为clipping.

但这个工作也可以延后到tessellation or geeometry shader.

存储到gl_Position.

一个例子

1
2
3
4
5
#version 450
layout( location = 0 ) in vec4 app_position;
void main() {
gl_Position = app_position;
}

Writing tessellation control shaders

细分.

需要在创建logical device时激活tessellationShader特性.

可选的.分为三步,其中两步是可编程的.The first programmable tessellation stage is used to set up parameters that control how the tessellation is performed.通过编写tessellation control shaders指明tessellation factors的值.

tessellation control and tessellation evaluation shaders是一起用的.

tessellation stage对patches进行操作.Patches由vertices组成,但与传统polygons不同,$\color {red}{每个patch可能任意个顶点,1~(至少)32个}​$.

细分是通过在shader代码里指明的inner和outer tessellation factors实现的.inner fctor由内置gl_TessLevelInner[]数组指明patch内部是如何细分的.outer factor 对应的是gl_TessLevelOuter[],定义了如何细分patches的外边缘(outer edges).每个数组元素对应于patches的给定边.

tessellation control shader对patch的每个vertex执行一次.当前vertex的index为gl_InvocationID.只有当前的可写,但可以通过gl_in[].gl_Position读取input patch的所有vertices.

一个例子

1
2
3
4
5
6
7
8
9
10
11
#version 450
layout( vertices = 3 ) out;
void main() {
if( 0 == gl_InvocationID ) {
gl_TessLevelInner[0] = 3.0;
gl_TessLevelOuter[0] = 3.0;
gl_TessLevelOuter[1] = 4.0;
gl_TessLevelOuter[2] = 5.0;
}
gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;
}

Writing tessellation evaluation shaders

Tessellation evaluations是tessellation process的第二个可编程stage.当geometry被细分后(subdivided),手机细分的结果形成新的vertices并重新修改它们.我们需要编写tessellation evaluation shaders来获取生成的顶点的位置,并将它们提供给连续的consecutive pipeline stages.

在这两个着色器之间是根据control stage 提供的参数进行细分的真实过程.tessellation的结果在evaluation stage中用到,生成新的geometry.

在tessellation evaluation中能控制新的primitives如何对其并形成.我们指定了它们的缠绕顺序(winding order)和生成顶点之间的间距.同时能选择是否在tessellation stage创建isolines, triangles,or quads来改变图元类型.

新顶点㐊直接创建的,细分器只为新vertices生成重心细分坐标(barycentric tessellation coordinates)(weights),内置变量为gl_TessCoord.我们可以使用这些坐标在形成面片的顶点的原始位置之间进行插值(interpolate),并将新的顶点放置在正确的位置.这也是evaluation shader可以访问所有顶点的原因.(gl_in[].gl_Position)

三角形通常不用做任何事

1
2
3
4
5
6
7
#version 450
layout( triangles, equal_spacing, cw ) in;
void main() {
gl_Position = gl_in[0].gl_Position * gl_TessCoord.x +
gl_in[1].gl_Position * gl_TessCoord.y +
gl_in[2].gl_Position * gl_TessCoord.z;
}

Writing geometry shaders

当绘制Object时,我们提供顶点并指明图元(primitives)类型(points,lines,triangles).在vertex和可选的tessellation stages后,形成了指定的图元类型.可选的可以激活geometry stage并写geometry shaders控制或改变从vertices形成primitives的过程.在geometry shaders甚至可以创建新的primities或销毁已经存在的.

geometry shaders允许创建额外的vertices混合primitives,删除已经存在的或改变vertices形成的primitive类型.

它能访问primitvie的所有vertices,且能调整它们.根据这些数据能原文传递或者创建新的vertices和primitives.但不能在一个gs中创建太多vetices.tessellation shaders更适合做这种操作.应尽量减少gs发出的顶点数.

gs总生成strip primitives.如果想创建独立图元–不形成strip,只需要在合适的时机end a primitive–vertices emitted

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#version 450
layout( triangles ) in;
layout( triangle_strip, max_vertices = 9 ) out;
void main() {
for( int vertex = 0; vertex < 3; ++vertex ) {
gl_Position = gl_in[vertex].gl_Position + vec4( 0.0, -0.2, 0.0, 0.0 );
EmitVertex();
gl_Position = gl_in[vertex].gl_Position + vec4( -0.2, 0.2, 0.0, 0.0 );
EmitVertex();
gl_Position = gl_in[vertex].gl_Position + vec4( 0.2, 0.2, 0.0, 0.0 );
EmitVertex();
EndPrimitive();//介绍一个图元,不形成strip
}
}

Writing fragment shaders

fragment对geometry shader后的操作(rasterization,光栅化)的数据进行计算,一般使用屏幕坐标系(screen space coordinate(x,y,depth)).能选择哪个attachment写入颜色.

geometry 形成的primitives在rasterization过程中转换为fragments(pixels).fragment sahder对每个fragment执行一遍.shader中或framebuffer tests(depth,stencil,scissor tests)中Fragments可能discard,不会成为pixels–这是它们称为fragments而不是pixels的原因.

fragment sahder最重要的目的是设置写入attachement的颜色.通常进行光照计算和texturing.和compute shaders配合,fragment shader还常用来进行后处理(bloom等)或者defered shading/lighting.同时,只有fragment shader能访问render pass定义的input attachments.

一个简单例子

1
2
3
4
5
#version 450
layout( location = 0 ) out vec4 frag_color;
void main() {
frag_color = vec4( 0.8, 0.4, 0.0, 1.0 );
}

Writing compute shaders

compute shader常用来数学计算.它们以3D形式的组执行并可能访问a common set of data.同时,很多local groups能一起执行能更快得到结果.

只能在compute pipeline里使用.

compute shadeers没有任何早于或晚于pipeline stages的输入或输出.只有compute pipeline stage的.uniform varables必须作为其数据来源.相似,计算结果存储在Uniform variables.

有些内置变量.用来索引.

local workgroup中给定compute shader调用的索引

1
uvec3 gl_LocalInvocationID

同时分发的workgroups的数量

1
uvec3 gl_NumWorkGroups

当前workgroup的number

1
uvec3 gl_WorkGroupID

还有个给当前shader在所有workgroups里所有调用唯一编号的

1
2
uvec3 gl_GlobalInvocationID;
= gl_WorkGroupID * gl_WorkGroupSize + gl_LocalInvocationID

local workgroup的size由input layout qualifier定义.在shader里,uvec3 gl_WorkGroupSize也可以获得这个size.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#version 450
layout( local_size_x = 32, local_size_y = 32 ) in;
layout( set = 0, binding = 0, rgba8 ) uniform image2D StorageImage;
void main() {
vec2 z = gl_GlobalInvocationID.xy * 0.001 - vec2( 0.0, 0.4 );
vec2 c = z;
vec4 color = vec4( 0.0 );
for( int i=0; i<50; ++I ) {
z.x = z.x * z.x-- z.y * z.y + c.x;
z.y = 2.0 * z.x * z.y + c.y;
if( dot( z, z ) > 10.0 ) {
color = i * vec4( 0.1, 0.15, 0.2, 0.0 );
break;
}
}
imageStore( StorageImage, ivec2( gl_GlobalInvocationID.xy ), color );
}

Writing a vertex shader that multiplies vertex position by a projection matrix

将geometry从local 变换到clip space通常在vertex shader进行,尽管其他vertex processing stage(tessellation or geometry)也能完成这个工作.这个变换通过指明model,view,projection matrices和从app提供三者的值给shader或者合为一个MVP矩阵.最普通和简单的方式是使用uniform buffer.

1
2
3
4
5
6
7
8
#version 450
layout(location = 0) in vec4 app_position;
layout(set=0, binding=0) uniform UniformBuffer {
mat4 ModelViewProjectionMatrix;
};
void main() {
gl_Position = ModelViewProjectionMatrix * app_position;
}

uniform buffer $0^{th}$ set,$0^{th}$ binding.

using push constants in shaders

当给shaders提供数据通常使用uniform buffers,storage buffers,或其他descriptor resources.不幸的是更新这样一个 resources可能不太合适,尤其是经常改变数据的时候.

为了这个目的,需要引入push constants.通过它们我们能用更简单和快速的方法提供数据.单需要fit更小的可用空间.

使用GLSL shaders里的push constants类似uniform buffers.

不同之处

  • 1.使用layout(push_constant)
  • 必须指明instance name
  • 每个shader只能定义一个
  • 通过访问instance name of the block 访问push constant

constants在更新频繁的小数据上非常游泳,比如变换矩阵或物理量(time,dt等).需要记住数据的size比descriptor resources小得多.$\color{red}{规范要求push constants至少存储128 bytes数据}$.每个硬件平台可能允许更多的存储,但可能不会大得多.

在提供颜色的片段明暗器中定义和使用push常量的示例如下:

1
2
3
4
5
6
7
8
#version 450
layout( location = 0 ) out vec4 frag_color;
layout( push_constant ) uniform ColorBlock {
vec4 Color;
} PushConstant;
void main() {
frag_color = PushConstant.Color;
}

Writing texturing vertex and fragment shaders

vertex shader

1
2
3
4
5
6
7
8
#version 450
layout( location = 0 ) in vec4 app_position;
layout( location = 1 ) in vec2 app_tex_coordinates;
layout( location = 0 ) out vec2 vert_tex_coordinates;
void main() {
gl_Position = app_position;
vert_tex_coordinates = app_tex_coordinates;
}

fragment shader

1
2
3
4
5
6
7
#version 450
layout( location = 0 ) in vec2 vert_tex_coordinates;
layout( set=0, binding=0 ) uniform sampler2D TextureImage;
layout( location = 0 ) out vec4 frag_color;
void main() {
frag_color = texture( TextureImage, vert_tex_coordinates );
}

Displaying polygon normals with a geometry shader

介绍一种简单的检查发现的方法.

vertex shader

1
2
3
4
5
6
7
8
9
10
11
12
#version 450
layout( location = 0 ) in vec4 app_position;
layout( location = 1 ) in vec3 app_normal;
layout( set = 0, binding = 0 ) uniform UniformBuffer {
mat4 ModelViewMatrix;
mat4 ProjectionMatrix;
};
layout( location = 0 ) out vec4 vert_normal;
void main() {
gl_Position = ModelViewMatrix * app_position;
vert_normal = vec4( mat3( ModelViewMatrix ) * app_normal * 0.2, 0.0 );
}

view space

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#version 450
layout( triangles ) in;
layout( location = 0 ) in vec4 vert_normal[];
layout( set = 0, binding = 0 ) uniform UniformBuffer {
mat4 ModelViewMatrix;
mat4 ProjectionMatrix;
};
layout( line_strip, max_vertices = 6 ) out;//line
layout( location = 0 ) out vec4 geom_color;
void main() {
for( int vertex = 0; vertex < 3; ++vertex ) {
gl_Position = ProjectionMatrix * gl_in[vertex].gl_Position;
geom_color = vec4( 0.2 );
EmitVertex();
gl_Position = ProjectionMatrix * (gl_in[vertex].gl_Position +
vert_normal[vertex]);//normal 方向延长,并变换到clip space
geom_color = vec4( 0.6 );
EmitVertex();
EndPrimitive();
}
}

为什么使用一个uniform buffer呢?一般来说cb里步骤越少效率越高

fragment shader

1
2
3
4
5
6
#version 450
layout( location = 0 ) in vec4 geom_color;
layout( location = 0 ) out vec4 frag_color;
void main() {
frag_color = geom_color;
}