OGL(教程4)——着色器

原文地址:http://ogldev.atspace.co.uk/www/tutorial04/tutorial04.html

背景知识
从这节教程开始,每一个特效以及我们将要使用的技术都将通过shader来实现。shader是3D图形学现在使用的方法。你也能认为这是一种倒退,因为在固定渲染管线中已经提供了3D的功能,它只需要开发者配置参数(比如灯光的属性、旋转的值等),而使用shader,这些都需要开发者自己实现。但是这种可编程增加拉灵活性以及创新性。

OpenGL可编程渲染管线可以如下图所示:
在这里插入图片描述

顶点处理器主要的任务是对传入流水线的每个顶点都执行顶点着色器。而传入的顶点个数由draw call的参数决定。顶点着色器不知道要渲染的图元的拓扑结构。除此之外,你不能在顶点处理器中丢弃顶点。每个顶点进入顶点处理器只有一次,执行变换,然后进入渲染管线的下一步操作。

下一步是集合处理阶段。在这个阶段就会知道完整的图元,以及知道相邻的顶点。这样就能够在考虑顶点本身之外添加一些额外的信息,使用一些变换技术。几何shader同样能够转换拓扑结构,使其变换成一个不同的样子。比如,你可以提供一些顶点,然后产生两个三角形(比如是四边形)。这个技术叫做公告牌。除此之外,你可以有选择的去除多余的点,然后可以产生更多变换的图元。

下一个阶段是关系的裁剪阶段。这个是固定函数单元,只有一个任务——那就是裁剪图元到一个规格化的盒子内,此盒子我们前一小节见过。同样它也会裁剪在近平面和远平面之外的区域。这里还给用户提供裁剪平面的选择。哪些侥幸幸存的顶点将会被映射到屏幕上,然后根据其拓扑结构进行光栅化处理。比如,对于三角形来说,就是找到所有三角形之内的点。对于每一个顶点,光栅化器都会调用片段处理器。在这里你可决定每个像素的颜色,其颜色可是从纹理上采样或者采用你想要的技术。

三个可编程的阶段:顶点、几何以及片段着色器。这三个是可选择的。如果你没有绑定shader到这三个阶段,那么默认的一些处理将会被执行。

shader的管理很像c/c++程序的创建。首先你写一些shader文本,然后让它在你的程序中可用。这个可以很简单的做到,你只需要在你程序中包含这个文本,后者采用load文件的形式加载进来。然后你开始一个一个编译shader即可。在你链接shader成为一个程序之后,然后把它加载到GPU。链接shader给驱动一个机会就是,这个过程能够精简shader,然后根绝不同shader之间的关系进行优化操作。比如,你在顶点着色器中定义了法线,但是片段着色器中并没有使用到,那么将会被优化掉。在GLSL编译器,能够去除和法线相关的功能,然后加速执行顶点着色器。而如果顶点着色器和一个有用到法线相关函数的片段着色器匹配时,那么GLSL将会产生一个不同的顶点着色器。这个地方可能翻译的不太好,反正一句话,GLSL能够优化我们写shader,把一些多余的去除,至于少了的,估计不能自动补全吧,要不太智能了。

代码注释:

GLuint ShaderProgram = glCreateProgram();

我们从创建一个程序对下开始。我们将会把所有的shader连接到这个对象。

GLuint ShaderObj = glCreateShader(ShaderType);

上面的调用,会产生两个shader对象。一个是类型为GL_VERTEX_SHADER的shader,另外一个是类型为GL_FRAGMENT_SHADER的对象。指定shader源码以及编译shader对于这两个shader对象是相同的。

const GLchar* p[1];
p[0] = pShaderText;
GLint Lengths[1];
Lengths[0]= strlen(pShaderText);
glShaderSource(ShaderObj, 1, p, Lengths);

在编译shader对象之前,我们必须指定它的源码是什么。函数glShaderSource 需要shader对象作为参数,同时还提供了灵活的方式来指定源码。这个源码可是一个字符型。最简单是一个字符数组,然后计算它的长度。第二个参数是表示两个数组中的槽位的个数。什么是槽位????,我们的例子中只有一个槽位,所以传递是1。

glCompileShader(ShaderObj);

编译shader是很简单的。

GLint success;
glGetShaderiv(ShaderObj, GL_COMPILE_STATUS, &success);
if (!success) {
    GLchar InfoLog[1024];
    glGetShaderInfoLog(ShaderObj, sizeof(InfoLog), NULL, InfoLog);
    fprintf(stderr, "Error compiling shader type %d: '%s'\n", ShaderType, InfoLog);
}

但是,你会经常遇到一些编译错误。上面的代码,可以得到编译的状态,然后把这些错误输出出来。

glAttachShader(ShaderProgram, ShaderObj);

最后,我们把编译好的shader对象绑定到程序对象。这个和指定一些对象用于makefile编译一样。由于我们没有makefile,我们只是简单用于程序模拟下。只有被绑定的对象才能参与链接。

glLinkProgram(ShaderProgram);

当编译所有的shader对象,然后把他们绑定到程序,我们最终才能链接他们。注意,在你链接这个程序之后,你可以删除中间的shader对象,其方法是调用glDetachShader 和glDeleteShader ,你要删除哪个就指定删除哪个。OpenGL维护了绝大多数的对象引用。如果一个shader对象被创建了,然后删除了,那么驱动程序就会删除它。但是如果他绑定到程序,那么你调用glDeleteShader 只会标记它为删除,你还需要调用glDetachShader ,使其引用计数为0,只会才能删除它。

glGetProgramiv(ShaderProgram, GL_LINK_STATUS, &Success);
if (Success == 0) {
    glGetProgramInfoLog(ShaderProgram, sizeof(ErrorLog), NULL, ErrorLog);
    fprintf(stderr, "Error linking shader program: '%s'\n", ErrorLog);
}

注意,我们检查程序相关的错误,比如链接错误。这个和检测shader相关的错误不同。不是使用glGetShaderiv ,而是使用glGetProgramiv ,不是使用glGetShaderInfoLog ,而是使用glGetProgramInfoLog。

glValidateProgram(ShaderProgram);

你可能会问,我们为什么要在链接成功之后还要检测它这个程序呢。这个要知道,链接检查是shader联合的基础上,而上面的检测是为了检测程序是否可以在当前的渲染管线阶段执行。在一个拥有很多shader的负责应用中,多个状态会来回切换,所以要在执行每个drawcall之前检测一次。在我们简单的应用中,我们只要检测一次。同样,你可能只要在开发的时候检测,而不是在最后产品的再检测。

glUseProgram(ShaderProgram);

最后,为了使用链接的渲染程序,你需要使用上面的调用来设置管线的状态。这个程序会保持这个状态,直到你用其他的替换,或者禁用它,禁用的函数是glUseProgram 传其参数为NULL即可。如果你创建了一个shader程序,它只包含一种类型的shader,那么其他的阶段操作就会使用默认的固定功能。

我们已经完成了代码的注释。剩余的介绍,就是关于顶点和片段的内容了。

#version 330

这句话告诉编译器我们使用的目标版本是GLSL的3.3版本。如果编译器不支持它,就会报一个错误。

layout (location = 0) in vec3 Position;

这个声明是在顶点着色器中的。它声明了一个顶点属性,它有3维的float,这个属性在shader叫做位置属性Position。顶点明细,意味着每次GPU调用shader,顶点缓冲都会提供一个新的顶点。声明的第一个部分,(location=0),它在属性名和buffer中的属性做了一个绑定。这个在我们的顶点属性包含:positon、normal、texture坐标时有用。我们需要告诉编译器我们的顶点的哪个属性被映射到shader声明的属性。这里有两个方法可以做到。我们可以在这里直接声明为0。在那种情况下,我们可以采用硬编码glVertexAttributePointer。或者声明为in vec3 Position,然后通过glGetAttribLocation查询其位置。前一种种情况,我们需要提供给glVertexAttributePointer 提供返回值。我们选择了很简单的方式,但是复杂的应用最好是让编译器决定属性的索引,然后再运行的时候查询。这个在整合多个shader,并且是从多个源码中整合的时候,会使用这样的方式。

void main()

你可以创建通过连接多个shader,形成你的shader。但是,每个shader阶段都只能有一个主函数(VS/GS/FS)。这个主函数是shader的入口。比如你可以创建光照库,里面包含多个函数,但是其中不能包含一个叫做main的函数,这样你才能使用你的shader链接它。

gl_Position = vec4(0.5 * Position.x, 0.5 * Position.y, Position.z, 1.0);

这儿,我们通过把顶点位置固定死。把x、y除以2,然后z保持不变。gl_Position是一个特殊的内置变量,它保存的是顶点的齐次坐标,包含了xyzw四个分量。光栅化器会寻找这个变量,然后通过一系列的变化把此位置转换为屏幕坐标。把y和y除以2的意思,我们将会看到此时的三角形是上一节的1/4。注意到我们w分量设置为1.0。这个一点很重要,只要这样才能得到正确的三角形展示效果。把透视从3D转换到2D事实上需要两个独立的步骤。首先,你需要把所有的顶点乘以透视矩阵,这个透视矩阵我们后面会介绍到。然后GPU执行的所谓透视除法,之后才会把这些顶点数据送达光栅化器。这就意味着所有gl_Position分量都要除以w分量。在这节中我们在顶点着色器中并没有做任何的透视除法,但是透视除法阶段我们不能控制其不要做。任何从顶点着色器输出的gl_Position都是已经除以了w分量的。这里我们是通过将w分量设置为1.0,就是除以1.0将不会改变原来的任何分量,这样就从另外一个方式避免了透视除法,也就是我们不能控制透视除法不执行,但是可以让透视除法不改变任何分量大小,方法就是将w分量设置为1.0,我们的位置确保了还是在规格化盒子内。

如果所有的都工作正常,三个顶点他们分别是 (-0.5, -0.5), (0.5, -0.5) 和(0.0, 0.5)到达了光栅化器。裁剪器不需要做任何的事情,因为所有的顶点都在规格化盒子内。这些值被映射到屏幕空间坐标,光栅化器对于三角形内的所有的顶点都会跑一遍。对于每个点,像素着色器都会执行。下面的代码是从片段着色器抽出的代码:

out vec4 FragColor;

片段着色器主要是用来决定像素的颜色。除此之外,像素着色器还可以丢弃特定的像素,或者改变z值,这个会影响后续的z测试。输出的颜色是通过上面的变量声明。输出的颜色包含四个分量(RGBA)。你对这个变量设置的值,会被光栅化器接收到,最终被写入到帧缓存。

FragColor = vec4(1.0,0.0,0.0,1.0);

在之前的章节中,没有使用到像素着色器,所有最终的颜色都是默认的白色。这里我么把颜色设置为红色。

// GLUT.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include <stdio.h>
#include <glew.h>
#include <freeglut.h>
#include "math_3d.h"

#pragma comment( lib, "glew32d.lib" )

GLuint VBO;
static const char* pVS = " \n\
#version 330 \n\
\n\
layout (location = 0) in vec3 Position; \n\
\n\
void main() \n\
{ \n\
gl_Position = vec4(0.5 * Position.x, 0.5 * Position.y, Position.z, 1.0); \n\
}";
static const char* pFS = " \n\
#version 330 \n\
\n\
out vec4 FragColor; \n\
\n\
void main() \n\
{ \n\
FragColor = vec4(1.0, 0.0, 0.0, 1.0); \n\
}";
static void RenderSceneCB()
{
	glClear(GL_COLOR_BUFFER_BIT);
	glEnableVertexAttribArray(0);
	glBindBuffer(GL_ARRAY_BUFFER, VBO);
	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);
	glDrawArrays(GL_TRIANGLES, 0, 3);
	glDisableVertexAttribArray(0);
	glutSwapBuffers();
}
static void InitializeGlutCallbacks()
{
	glutDisplayFunc(RenderSceneCB);
}
static void CreateVertexBuffer()
{
	Vector3f Vertices[3];
	Vertices[0] = Vector3f(-1.0f, -1.0f, 0.0f);
	Vertices[1] = Vector3f(1.0f, -1.0f, 0.0f);
	Vertices[2] = Vector3f(0.0f, 1.0f, 0.0f);
	glGenBuffers(1, &VBO);
	glBindBuffer(GL_ARRAY_BUFFER, VBO);
	glBufferData(GL_ARRAY_BUFFER, sizeof(Vertices), Vertices, GL_STATIC_DRAW);
}
static void AddShader(GLuint ShaderProgram, const char* pShaderText, GLenum ShaderType)
{
	GLuint ShaderObj = glCreateShader(ShaderType);
	if (ShaderObj == 0) {
		fprintf(stderr, "Error creating shader type %d\n", ShaderType);
		exit(0);
	}
	const GLchar* p[1];
	p[0] = pShaderText;
	GLint Lengths[1];
	Lengths[0] = strlen(pShaderText);
	glShaderSource(ShaderObj, 1, p, Lengths);
	glCompileShader(ShaderObj);
	GLint success;
	glGetShaderiv(ShaderObj, GL_COMPILE_STATUS, &success);
	if (!success) {
		GLchar InfoLog[1024];
		glGetShaderInfoLog(ShaderObj, 1024, NULL, InfoLog);
		fprintf(stderr, "Error compiling shader type %d: '%s'\n", ShaderType, InfoLog);
		exit(1);
	}
	glAttachShader(ShaderProgram, ShaderObj);
}
static void CompileShaders()
{
	GLuint ShaderProgram = glCreateProgram();
	if (ShaderProgram == 0) {
		fprintf(stderr, "Error creating shader program\n");
		exit(1);
	}
	AddShader(ShaderProgram, pVS, GL_VERTEX_SHADER);
	AddShader(ShaderProgram, pFS, GL_FRAGMENT_SHADER);
	GLint Success = 0;
	GLchar ErrorLog[1024] = { 0 };
	glLinkProgram(ShaderProgram);
	glGetProgramiv(ShaderProgram, GL_LINK_STATUS, &Success);
	if (Success == 0) {
		glGetProgramInfoLog(ShaderProgram, sizeof(ErrorLog), NULL, ErrorLog);
		fprintf(stderr, "Error linking shader program: '%s'\n", ErrorLog);
		exit(1);
	}
	glValidateProgram(ShaderProgram);
	glGetProgramiv(ShaderProgram, GL_VALIDATE_STATUS, &Success);
	if (!Success) {
		glGetProgramInfoLog(ShaderProgram, sizeof(ErrorLog), NULL, ErrorLog);
		fprintf(stderr, "Invalid shader program: '%s'\n", ErrorLog);
		exit(1);
	}
	glUseProgram(ShaderProgram);
}
int main(int argc, char** argv)
{
	glutInit(&argc, argv);
	glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA);
	glutInitWindowSize(1024, 768);
	glutInitWindowPosition(100, 100);
	glutCreateWindow("Tutorial 04");
	InitializeGlutCallbacks();
	// Must be done after glut is initialized!
	GLenum res = glewInit();
	if (res != GLEW_OK) {
		fprintf(stderr, "Error: '%s'\n", glewGetErrorString(res));
		return 1;
	}
	glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
	CreateVertexBuffer();
	CompileShaders();
	glutMainLoop();
	return 0;
}

展示图:
在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值