第七章:着色器
高效GPU渲染方案
本章介绍着色器的基本知识以及Geiv下对其提供的支持接口。并以“渐变高斯模糊”为线索进行实例的演示解说。
[背景信息]
[计算机中央处理器的局限性]
在大学的“数字图像处理”课程中,老师解说了高斯模糊的基本算法。并使用C#进行了基本实现。高斯模糊。简单地说,就是使用高斯权重模板对图像的每个像素进行再计算、填充,以达到模糊的效果。
在课程中。对于给定的模板与模糊度系数,对一副800X600的图像进行模糊处理。须要计算48万个像素点,虽然当时机房已经普及了酷睿系列CPU。但这个过程依然耗时2~3秒。
于是有这种一个问题:在游戏中,我们常常看到画面进行模糊到清晰的动态转变。即使是次世代的GBA\SFC等游戏主机依旧有这种效果实现。而论硬件性能,我们的PC应该远远高于这些次世代游戏机,而2、3秒的运算结果已经明白告诉我,想要进行更为高效的模糊计算,显示地挨个计算像素点是不可能达到预期效率的,想要一秒钟完毕60幅不同模糊度的图像计算,还要另寻他路。
为什么计算性能普遍较低的游戏机却能实现高性能CPU都无法流畅做到的运算呢?笔者觉得还是由于CPU的结构差异导致的,PC的任务种类多。过程复杂。须要复杂的指令集系统。但游戏机的CPU主要为图像而生。仅仅需使用针对图像的少数精简指令系统就可以。
总而言之,为了保证CPU在复杂任务下的通用性,计算机的CPU并不擅长进行图形运算。而且在一些场合下,图形运算的效率相当低下。
[图形处理器GPU]
为了弥补CPU处理图形的缺陷,GPU便诞生了。GPU是针对图形运算而生的处理器,现在的主流游戏PC都会有一个高性能的GPU。详细的概念笔者就不在这里提了,相信搞计算机的都会对其了解一二。
[在编程中使用GPU运算资源]
在大多数开发场合下,不管是C还是Java。我们敲的代码都无疑地执行在CPU之上。那我们如何才干显示地调用GPU的资源呢?在OPENGL中,除了经常使用的固定管线式编程外,还提供了更为灵活的功能。它同意开发人员使用着色语言(GLSL,一种类C语言)编写名为“着色器”的程序,经编译执行于GPU之上并接管一部分GPU内置功能。
[Opengl着色器简述]
介于笔者履历有限,这里仅仅做简介,具体概念能够參考《Opengl着色语言》一书,它对着色器和GLSL有着很具体的介绍。
一个Opengl着色器(下简称着色器)由一个顶点着色器与片段着色器构成。它们负责的工作大部分是不同的,仅有少部分交集。
Opengl为着色器的校验、编译、连接和设置參数提供了完整的API,仅仅要显卡支持,在Opengl上下文中就可以由源文件得到着色器程序,无需其它环境。
编写着色器的目的。是为了实现诸如模糊、雾化、发光等固定管线不易叙述的功能,进而丰富图像的表现能力。非常多游戏引擎将Opengl中与着色器相关的API包装到了上层,我在设计时也採用了这样的方案。
[内置着色器]
在GEiv中,内置了5个系统着色器,它们是在设计之初进行着色器測试时所保留下来的。并非因为“经常使用”(笔者感觉着色器的编写往往非常有针对性,一般非常难有经常使用一说)。
依次介绍:
SD_ANTICOLOR:将目标色改为反色
SD_EMBOSS_MODE9:浮雕效果,后面的MODE9表示它使用的是3X3的模板,能够适应一般的需求,若要提高准确度,能够将模板扩充至4X4\5X5等。
SD_GAUSSIAN_MODE9:高斯模糊
SD_LAPLACIAN_MODE9:锐化
SD_MEAN_MODE9:均值化
[为图元设置着色器]
在GEiv中。使用着色器的对象是图元,使用其setShaderProgram方法将着色器绑顶到相应图元上。如:
Obj T =UES.creatObj(UESI.BGIndex); T.addGLImage(0,0, "./mdls.jpg"); T.setShaderProgram(SysShader.SD_GAUSSIAN_MODE9);//将系统着色器绑定到图元T上
[设置Uniform參数]
Uniform修饰词(不是数据类型)指明一个GLSL中的变量是外部传入的。而且在整个着色器运行过程中不会改变(差别于attribute),比如对于实现模糊变化来讲,“模糊度”这个变量属于Uniform型无疑。
在GLSL中。数组有非常多存在形式,float[]能够表示一维线性浮点数组。vect2则表示了一个点对,而vec2[]则表示了一维线性浮点“数对”组,类似的还有vect3\4等等这样在java中没有现成的相应结构的数据。也就是说,设置Uniform參数时必须给定我们的数据源(一维线性数组)和映射到GLSL中变量的方式。
在GEiv的图元类中。setShaderUniform(String uniformName, Object value, intTPFLAG)方法能够设置该图元上绑定的着色器參数,这种方法的參数依次是:
uniformName是着色器中对于的变量名称。
value是数据源,眼下仅仅支持float[]、Float[]或单个的Float型。
TPFLAG 是指定的映射方式。你能够直接从ShaderController的静态值部分直接引用。
它能够是下列值之中的一个:
AFLOAT与FLOATS分别相应单个float值与一维float数组。
VERTXS表示由X个点对组成的数组,即该数组每一个元素包括一个X维度向量。所给数据源必须是一个X整数倍大小的一维线性数组,该数组会依照顺序依次填充这些X维度向量。比如使用TP_VERT2S时,数据源一维数组的前两个元素构成了着色器向量数组的第一个向量元素。
VERTEX表示一个X维度向量。要求给定的一维数据源数组大小必须为X。
[自己定义用户着色器]
除了使用SysShader中现成的着色器外,还能够使用用户自己定义的着色器程序。
在引擎的句柄UESI中,包装并简化的Opengl产生着色器的API:
UESI.createShaderProgram(String spName,String vpPath, String fpPath);
參数介绍:
spName是用户为着色器起的名字。
vpPath是顶点着色器的源文件路径,如”./vp.txt”。
fpPath是片段着色器源文件路径。
之后我们仅仅须要使用设置着色器的API对图元绑定spName即可了。与之前介绍的字库同样。spName也具有全局效应,能够为其他上下文的图元绑定。
[实例-渐变高斯模糊]
您能够在[GitHub]Sample\Sample-Shader下找到这个样例及使用的资源。
Main.java
package com.geiv.test; import geivcore.R; import geivcore.UESI; public class Main{ public static void main(String[] args) { UESI UES = new R(); new Guassion(UES); } }
Guassion.java
package com.geiv.test; import geivcore.SerialTask; import geivcore.UESI; import geivcore.engineSys.shadercontroller.ShaderController; import geivcore.engineSys.shadercontroller.SysShader; import geivcore.enginedata.canonical.CANExPos; import geivcore.enginedata.obj.Obj; import java.awt.event.KeyEvent; import java.util.Arrays; public class Guassion implements SerialTask{ UESI UES; float[] g_aryVerticalOffset; float[] vertstatic; float bur = 600f;//bur作为模糊度因子,使用静态偏移量(vertstatic)除以模糊度因子得到不同的偏移量參数(g_aryVerticalOffset)。因为GLSL上下文是使用的OPENGL坐标式,与GEiv有所不同,因此初始因子经计算转换后定为600f Obj T; public Guassion(UESI UES) { this.UES = UES; T = UES.creatObj(UESI.BGIndex); T.addGLImage(0, 0, "./mdls.jpg"); T.setShaderProgram(SysShader.SD_GAUSSIAN_MODE9);//设置着色器 T.setShaderUniform(SysShader.NA_WEIGHTARGS,SysShader.BR_GAUSSIAN_MODE9, ShaderController.TP_FLOATS);//设置Unfiorm參数,当中规格化后的权重模板及其在GLSL上下文中的名称已经给定。它们能够由SysShader直接引用。 vertstatic = new float[]{-1,-1, 0,-1, 1,-1 -1, 0, 0, 0, 1, 0 -1, 1, 0, 1, 1, 1};//偏移量模板。看不懂的话能够參考以下的图 g_aryVerticalOffset = Arrays.copyOf(vertstatic, vertstatic.length); for(int i = 0;i < g_aryVerticalOffset.length;i++) { g_aryVerticalOffset[i] = vertstatic[i]/bur; } T.setShaderUniform(SysShader.NA_OFFSETARGS, g_aryVerticalOffset,ShaderController.TP_VERT2S);//设置Uniform偏移量 T.setPosition(CANExPos.POS_CENTER); T.show(); UES.addSerialTask(this); } @Override public void Serial(int clock) {//挂载一个扫描式的键盘输入,当Z键按下时bur添加。重置偏移量并重设Uniform。 当X键按下时则相反。 if(UES.getKeyStatus(KeyEvent.VK_Z)) { T.setShaderUniform(SysShader.NA_OFFSETARGS,g_aryVerticalOffset,ShaderController.TP_VERT2S); bur+=4f; for(int i = 0;i < g_aryVerticalOffset.length;i++) { g_aryVerticalOffset[i] = vertstatic[i]/bur; } } else if(UES.getKeyStatus(KeyEvent.VK_X)) { T.setShaderUniform(SysShader.NA_OFFSETARGS,g_aryVerticalOffset,ShaderController.TP_VERT2S); bur-=4f; for(int i = 0;i < g_aryVerticalOffset.length;i++) { g_aryVerticalOffset[i] = vertstatic[i]/bur; } } } }
关于偏移量模板的映射,实际上是这种↓:
除此之外,还有高斯模糊着色器的两部分源码
顶点着色器:
attribute float sys_pIndex; void main(void) { gl_TexCoord[0] = gl_MultiTexCoord0; gl_Position = ftransform(); }
↑这个顶点着色器没有写不论什么实质性的功能,它被非常多片段着色器共用。
高斯-片段着色器:
const int g_iWeightNumber = 9;//权重模板大小 uniform sampler2D g_FilterTexture;//我们的纹理 uniform float g_aryWeight[g_iWeightNumber];//权重数组 uniform vec2 g_aryOffset[g_iWeightNumber];//偏移量-即使使用9个格子,也没有规定必须是相邻的格子;偏移量叙述着权重模板格子间的实际像素距离,取值越高,模糊度越高。 void main(void) { vec4 vec4Sum = vec4(0.0); for(int i = 0; i < g_iWeightNumber; ++i) { vec4Sum += texture2D(g_FilterTexture, gl_TexCoord[0].st + g_aryOffset[i])*g_aryWeight[i];//这里进行实质的计算过程。也就是替换原先我们让CPU挨个计算像素点的部分。 gl_TexCoord[0]是绑定的0号纹理(Opengl中图形是能够绑定多个纹理的)。它的st就是位置,一个vec2类型量;位置与偏移量进行加和,得到偏移位置。之后依据纹理和偏移位置取出相应的rgba四维向量,它是一个vec4类型。进一步,将我们的vec4乘以规格化后的权重。并加和到vec4Sum上。因为是规格化后的权重,因此RGBA都不会越界。 } gl_FragColor = vec4Sum;// 最后,我们将这个计算完成的RGBA结果填入纹理中。 }
最后,运行结果(按住X键时):
↑能够观察到由清晰到模糊的动态过程。
[总结]
本章介绍了着色器的使用场合以及GEiv下相应的API。
使用着色器是为了实现固定管线难以叙述的效果。
着色器执行于GPU,在对图像渲染时效率比直接使用CPU运算高出非常多。
版权声明:本文博客原创文章。博客,未经同意,不得转载。