阅读 21

OpenGL ES 2.0 Shader

获取绘制信息

EGL的API作用分类:

用于与手机关联并获取手机支持的配置信息
用于根据需要生成手机支持的surface和context,并对surface和context进行关联
用于指定使用哪个版本的OpenGL ES,并与OpenGL ES建立关联
用于操作EGL上纹理,以及与多线程相关的高级功能
其他用处
复制代码
EGLint eglGetError(void);
int eglGetError ();

功能:用于返回当前thread,如果EGL的API出错的话,最近一个错误所对应的错误代码
输出:错误代码

private void checkEglError(String msg) {
    int error;
    if ((error = EGL14.eglGetError()) != EGL14.EGL_SUCCESS) {
        throw new RuntimeException(msg + ": EGL error: 0x" + Integer.toHexString(error));
    }
}
复制代码
EGLDsiplay eglGetDisplay(EGLNativeDisplayType display_id);
EGLDisplay eglGetDisplay (int display_id);

功能:从EGL运行的操作系统中获取一个Display(获取手机屏幕)
输入:从操作系统中,得知的Display的ID
输出:用于显示图片绘制的Display

EGLDisplay mEGLDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
if (mEGLDisplay == null) {
    throw new RuntimeException("unable to get EGL14 display");
}
复制代码
EGLBoolean eglInitalize(EGLDisplay dpy, EGLint *major, EGLint *minor);
boolean eglInitialize (EGLDisplay dpy, 
                int[] major, 
                int majorOffset, 
                int[] minor, 
                int minorOffset);

功能:针对某display初始化一个某版本的EGL
输入:使用Display的handle特指某个Display,major和minor共同用于指定EGL的版本
输出:EGL初始化成功或者失败

int[] version = new int[2];
if (!EGL14.eglInitialize(mEGLDisplay, version, 0, version, 1)) {
    throw new RuntimeException("unable to initialize EGL14");
}
复制代码
EGLBoolean eglGetConfigs(EGLDisplay dpy, EGLConfig *configs, EGLint config_size, EGLint *num_config)
boolean eglGetConfigs (EGLDisplay dpy, 
                EGLConfig[] configs, 
                int configsOffset, 
                int config_size, 
                int[] num_config, 
                int num_configOffset)

功能:获取某display支持的配置信息
输入:display的handle,一个用于保存配置信息的指针,指针中存放的配置信息的数量,某display支持的配置信息数量
输出:配置信息获取成功或者失败
复制代码
EGLBoolean eglChooseConfig(EGLDisplay dpy, const EGLint * attrib_list, EGLConfig* configs, ELGint config_size, EGLint * num_config)
boolean eglChooseConfig (EGLDisplay dpy, 
                int[] attrib_list, 
                int attrib_listOffset, 
                EGLConfig[] configs, 
                int configsOffset, 
                int config_size, 
                int[] num_config, 
                int num_configOffset)

功能:获取与需求匹配,且某display支持的配置信息(根据开发者的需要,获取一种手机支持的buffer格式)
输入:display的handle,用于匹配使用的需求信息,一个用于保存匹配信息的指针,指针中存放的配置信息的数量,匹配配置信息的数量
输出:匹配的配置信息获取成功或者失败

int[] attribList = {
        EGL14.EGL_RED_SIZE, 8,
        EGL14.EGL_GREEN_SIZE, 8,
        EGL14.EGL_BLUE_SIZE, 8,
        EGL14.EGL_ALPHA_SIZE, 8,
        EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT,
        EGL_RECORDABLE_ANDROID, 1,
        EGL14.EGL_NONE
};
EGLConfig[] configs = new EGLConfig[1];
int[] numConfigs = new int[1];
EGL14.eglChooseConfig(mEGLDisplay, attribList, 0, configs, 0, configs.length, numConfigs, 0);
复制代码
EGLSurface eglCreateWindowSurface(EGLDisplay dpy, EGLConfig config, EGLNativeWindowType win, const EGLint *attrib_list);
EGLSurface eglCreateWindowSurface (EGLDisplay dpy, 
                EGLConfig config, 
                Object win, 
                int[] attrib_list, 
                int offset)

功能:创建一个可以显示在屏幕上的rendering surface(根据获取的格式,创建handle,用于包含手机系统提供的绘制所需的buffer)
输入:display的handle,用于创建surface的配置信息,窗口信息的handle,额外的需求信息
输出:创建的rendering surface的handle.

int[] surfaceAttribs = {
        EGL14.EGL_NONE
};
mEGLSurface = EGL14.eglCreateWindowSurface(mEGLDisplay, configs[0], mSurface, surfaceAttribs, 0);
复制代码
EGLBoolean eglBindAPI(EGLenum api)
boolean eglBindAPI (int api)

功能:设置当前thread的绘制API(用于指定使用的绘制语言:EGL_OPENGL_ES_API)
输入:支持的绘制API
输出:API设置成功或者失败
复制代码
EGLContext eglCreateContext(EGLDisplay dpy, EGLConfig config, EGLContext share_context, const EGLint* attrib_list)
EGLContext eglCreateContext (EGLDisplay dpy, 
                EGLConfig config, 
                EGLContext share_context, 
                int[] attrib_list, 
                int offset)
                
功能:针对当前的绘制API创建一个rendering context(创建调色板,用于保存状态集)
输入:display的handle,用于创建context的配置信息,指定一个context使得创建的context与其share,额外的需求信息
输出:创建的rendering context的handle

int[] attrib_list = {
        EGL14.EGL_CONTEXT_CLIENT_VERSION, 2,
        EGL14.EGL_NONE
};
mEGLContext = EGL14.eglCreateContext(mEGLDisplay, configs[0], EGL14.EGL_NO_CONTEXT, attrib_list, 0);
复制代码
EGLBoolean eglMakeCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx)
boolean eglMakeCurrent (EGLDisplay dpy, 
                EGLSurface draw, 
                EGLSurface read, 
                EGLContext ctx)

功能:将指定的context绑定到当前thread以及读和写的surface上(将创建的surface和context启动)
输入:display的handle,用于写入的surface,用于读取的surface,指定的context
输出:创建的rendering context的handle

EGL14.eglMakeCurrent(mEGLDisplay, mEGLSurface, mEGLSurface, mEGLContext);
复制代码
EGLBoolean eglSwapBuffers(EGLDisplay dpy, EGLSurface surface)
boolean eglSwapBuffers (EGLDisplay dpy, 
                EGLSurface surface)
                
功能:将surface中的color buffer显示到屏幕上(用于将绘制完毕的buffer与手机屏幕上显示的buffer进行交换)
输入:display的handle,将会被展示的surface
输出:显示成功或者失败

boolean result = EGL14.eglSwapBuffers(mEGLDisplay, mEGLSurface);
复制代码
EGLBoolean eglTerminate(EGLDisplay dpy)
boolean eglTerminate (EGLDisplay dpy)
boolean eglDestroySurface (EGLDisplay dpy, 
                EGLSurface surface)
boolean eglDestroyContext (EGLDisplay dpy, 
                EGLContext ctx)
boolean eglReleaseThread ()

功能:将某display对应的EGL相关的资源释放
输入:使用Display的handle特指某个display
输出:释放EGL相关资源成功或者失败

EGL14.eglMakeCurrent(mEGLDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE,
                        EGL14.EGL_NO_CONTEXT);
EGL14.eglDestroySurface(mEGLDisplay, mEGLSurface);
EGL14.eglDestroyContext(mEGLDisplay, mEGLContext);
EGL14.eglReleaseThread();
EGL14.eglTerminate(mEGLDisplay);
复制代码

传入绘制信息

OpenGL ES 2.0API的作用分类:

用于从手机获取绘制buffer
用于沟通GPU可编程模块
用于传入绘制信息
用于设置绘制状态
用于执行绘制命令
用于查询环境、清理状态
复制代码
void glGenBuffers(GLsizei n, GLuint *buffers);
void glGenBuffers (int n, IntBuffer buffers)
void glGenBuffers (int n, int[] buffers, int offset)

功能:创建若干个buffer object name
输入:数字n用于指定创建buffer的数量,指针用于保存这组buffer的变量名

final int[] buffers = new int[1];
GLES20.glGenBuffers(buffers.length, buffers, 0);
复制代码
void glBindBuffer(GLenum target, GLuint buffer);
void glBindBuffer (int target, int buffer)

功能:将某个特定的buffer用于保存某个特定target的信息
输入:某个特定的target,特定buffer的ID

GLES30.glBindBuffer(GLES30.GL_PIXEL_PACK_BUFFER, buffers[i]);
复制代码
void glBufferData(GLenum target, GLsizeptr size, const GLvoid* data, GLenum usage);
void glBufferData (int target, int size, Buffer data, int usage)

功能:通过指定一个特定的target,往target对应的buffer传入一定长度的数据,并且指定该buffer未来被使用的频率
输入:某个特定target,一定长度的数据,该buffer的使用方式

final FloatBuffer data;
GLES20.glBufferData(GL_ARRAY_BUFFER, data.capacity() * FLOAT_SIZE_BYTES, data, GL_STATIC_DRAW);
复制代码
void glBufferSubData(GLenum target, GLintptr offset, GLsizeptr size, const GLvoid* data);
void glBufferSubData (int target, int offset, int size, Buffer data)

功能:通过指定一个特定的target, 往target对应buffer的第offset位开始,传入一定长度的数据
输入:某个特定的target,buffer的第offset位,一定长度的数据
复制代码
void glDeleteBuffers(GLsizei n, const GLuint* buffers);
void glDeleteBuffers (int n, int[] buffers, int offset)
void glDeleteBuffers (int n, IntBuffer buffers)

功能:删除指定的buffer object
输入:数字n用于指定要删除的buffer的数量,指针用于保存这组buffer的变量名

GLES20.glDeleteBuffers(1, new int[]{vertexBufferName}, 0);
复制代码
void glDeleteShader (int shader)
void glDeleteProgram (int program)
复制代码
void glBindAttribLocation(GLuint program, GLuint index, const GLchar* name)
void glBindAttribLocation (int program, int index, String name)

功能:用于将指定program中的某个attribute,与某个固定的index进行绑定,在OpenGL ES中就可以通过该index对此attribute进行访问
输入:指定的program的ID,一个index常量,program绑定的VS中某个attribute的变量名字符串

String[] attributes = new String[]{"texture", "vPosition", "vTexCoordinate", "textureTransform"}
if (attributes != null)
{
    final int size = attributes.length;
    for (int i = 0; i < size; i++)
    {
        GLES20.glBindAttribLocation(programHandle, i, attributes[i]);
    }
}
复制代码
GLint glGetAttribLocation(GLuint program, const GLchar* name)
int glGetAttribLocation (int program, String name)

功能:获取指定program中的某个attribute的index,在OpenGL ES中就可以通过该index对此attribute进行访问
输入:指定program的ID,program绑定的VS中某个attribute的变量名字符串
输出:一个index常量

vPosition = GLES30.glGetAttribLocation(program, "vPosition")
vCoordinate = GLES30.glGetAttribLocation(program, "vCoordinate")
复制代码
void glEnableVertexAttribArray(GLuint index)
void glEnableVertexAttribArray (int index)

功能:将指定program中的某个attribute的开关打开,打开后,在OpenGL ES中就可以通过该index对此attribute进行访问,且在绘制的时候,Shader就可以访问到attribute对应的值
输入:指定program中的某个attribute的index

GLES30.glEnableVertexAttribArray(vPosition)
GLES30.glEnableVertexAttribArray(vCoordinate)
复制代码
void glDisableVertexAttribArray(GLuint index)
void glDisableVertexAttribArray (int index)

功能:将指定program中的某个attribute的开关关闭,关闭后,在绘制的时候,Shader将无法访问到attribute对应的值
输入:指定program中的某个attribute的index

GLES20.glDisableVertexAttribArray(getHandle("vPosition"));
GLES20.glDisableVertexAttribArray(getHandle("vCoordinate"));
复制代码
void glVertexAttribPointer(GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const GLvoid* pointer)
void glVertexAttribPointer (int indx, 
                int size, 
                int type, 
                boolean normalized, 
                int stride, 
                int offset)
                
功能:往attribute传入一定数量、某种格式、特定间隔的数据,并指明传入之后,是否需要对这些数据进行归一化
输入:attribute的index,数据的尺寸、类型、是否归一化、间隔和数据本身,如果使用BO的话,那么不需要传入数据的本身,而传入数据在BO中的偏移量

GLES20.glEnableVertexAttribArray(getHandle("aPosition"));
GLES20.glVertexAttribPointer(getHandle("aPosition"), VERTICES_DATA_POS_SIZE, GL_FLOAT, false, VERTICES_DATA_STRIDE_BYTES, VERTICES_DATA_POS_OFFSET);
GLES20.glEnableVertexAttribArray(getHandle("aTextureCoord"));
GLES20.glVertexAttribPointer(getHandle("aTextureCoord"), VERTICES_DATA_UV_SIZE, GL_FLOAT, false, VERTICES_DATA_STRIDE_BYTES, VERTICES_DATA_UV_OFFSET);
复制代码
void glVertexAttrib*f(GLuint index, GLfloat v0, GLfloat v1, GLfloat v2, GLfloat v3)
public static void glVertexAttrib4f (int indx, 
                float x, 
                float y, 
                float z, 
                float w)

功能:给指定program中的某个attribute进行赋值,可以在该函数中传入一到四个成员的值
输入:指定program中的某个attribute的index,用于赋值的值
复制代码

执行绘制命令

GLint glGetUniformLocation(GLuint program, const GLchar* name);
int glGetUniformLocation (int program, String name)

功能:获取指定program中的某个uniform的index,在OpenGL ES中就可以通过index对uniform进行访问;
输入:指定program的ID, program绑定的某个uniform的变量名字符串;
输出:一个Index常量。

vPosition = GLES30.glGetAttribLocation(program, "vPosition")
vCoordinate = GLES30.glGetAttribLocation(program, "vCoordinate")
vTexture = GLES30.glGetUniformLocation(program, "vTexture")
复制代码
void glUniform*iv(GLint location, GLsizei count, const GLint *value);
void glUniform4f (int location, 
                float x, 
                float y, 
                float z, 
                float w)

功能:给指定program中的某个uniform进行赋值,可以在该函数中传入一到四个成员的值或者一个数组;
输入:指定program中的某个uniform的index,如果uniform为数组时为数组的index,以及要赋的值。
复制代码
void glViewport(GLint x, GLint y, GLsizei width, GLsizei height);
void glViewport (int x, 
                int y, 
                int width, 
                int height)

功能:设定绘制的视窗大小;
输入:视窗左下角的坐标,视窗的宽和高。

GLES30.glViewport(0, 0, width, height)
复制代码
void glClearColor(GLclampf red, GLclampf green, GLclampf  blue, GLclampf alpha);
void glClearColor (float red, 
                float green, 
                float blue, 
                float alpha)

功能:设置一种用于清理绘制buffer的默认颜色;
输入:rgba四个颜色分量。

GLES30.glClearColor(1.0f, 1.0f, 1.0f, 1.0f)
复制代码
void glClear(GLbitfield mask);
void glClear (int mask)

功能:将绘制buffer设置成统一的值;
输入:颜色buffer或深度buffer或者stencilbuffer。

GLES30.glClear(GLES30.GL_DEPTH_BUFFER_BIT | GLES30.GL_COLOR_BUFFER_BIT)
复制代码
void glDrawArrays(GLenum mode, GLint first, GLsizei count);
void glDrawArrays (int mode, 
                int first, 
                int count)
                
功能:按照一定的模式绘制图片;
输入:绘制模式,绘制顶点的起点和数量。

GLES30.glDrawArrays(GLES30.GL_TRIANGLE_STRIP, 0, 4)
复制代码
void glDrawElements(GLenum mode, GLsizei count, GLenum type, const GLvoid* indices);
void glDrawElements (int mode, 
                int count, 
                int type, 
                int offset)

功能:按照一定的模式绘制图片;
输入:绘制模式,绘制使用顶点的索引、数量,以及索引的类型。

private static short drawOrder[] = {0, 1, 2, 0, 2, 3};
ByteBuffer dlb = ByteBuffer.allocateDirect(drawOrder.length * 2);
dlb.order(ByteOrder.nativeOrder());
drawListBuffer = dlb.asShortBuffer();
drawListBuffer.put(drawOrder);
drawListBuffer.position(0);

GLES20.glDrawElements(GLES20.GL_TRIANGLE_STRIP, drawOrder.length, GLES20.GL_UNSIGNED_SHORT, drawListBuffer);
复制代码

绑定Shader

GLuint glCreateShader(GLenum shaderType)
int glCreateShader (int type)

功能:创建一个指定类型的shader对象
输入:shader类型
输出:一个该类型的shader对象

var shader = GLES30.glCreateShader(shaderType)
复制代码
void glShaderSource(GLuint shader, GLsizei, count, const GLchar* const* string, const GLint* length)
void glShaderSource (int shader, String string)

功能:更新指定shader对象的源代码
输入:指定shader对象的ID,使用几组字符串对该shader对象进行赋值,字符串数组以及用于存放每个字符串包含字节长度的数组

GLES30.glShaderSource(shader, source)
复制代码
void glCompileShader(GLuint shader)
void glCompileShader (int shader)

功能:对指定shader对象进行编译
输入:指定shader对象的ID

GLES30.glCompileShader(shader)
复制代码
void glShaderBinary(GLsizei n, const GLuint *shaders, GLenum binaryformat, const void *binary, GLsizei length);
void glShaderBinary (int n, 
                int[] shaders, 
                int offset, 
                int binaryformat, 
                Buffer binary, 
                int length)

功能;将预先编译好的shader存入一组shader对象中
输入:一组shader的数量以及shader的指针,预先编译好的shader格式,编译好的shader二进制的位置和长度
复制代码
GLuint glCreateProgram(void)
int glCreateProgram ()

功能:创建一个program,用于把一组shader绑定在一起
输出:一个program对象

var program = GLES30.glCreateProgram()
复制代码
void glAttachShader(GLuint program, GLuint shader)
void glAttachShader (int program, int shader)

功能:将一个指定的shader,绑定到一个指定的program上
输入:指定program的ID,以及指定shader的ID

GLES30.glAttachShader(program, vertexShader)
复制代码
void glDetachShader(GLuint program, GLuint shader)
void glDetachShader (int program, int shader)

功能:将一个指定的shader,从指定的program上解绑
输入:指定program的ID,以及指定shader的ID
复制代码
void glLinkProgram(GLuint program)
void glLinkProgram (int program)

功能:将指定的program做链接操作,也就是将Program上绑定的shader互相链接起来
输入:指定Programe的ID

GLES30.glLinkProgram(program)
复制代码
void glUseProgram(GLuint program);
void glUseProgram (int program)

功能:将指定program将被GPU使用,也就是说:如果OpenGL ES的其他API以及GPU对Program进行操作,那么操作就是这个program
输入:指定program的ID

GLES30.glUseProgram(program)
复制代码
void glDeleteProgram(GLuint program);
void glDeleteProgram (int program)

功能:删除一个指定的Program
输入:指定program的ID

GLES30.glDeleteProgram(program)
复制代码
void glDeleteShader(GLuint shader)
void glDeleteShader (int shader)

功能:删除一个指定的shader
输入:指定shader的ID

GLES30.glDeleteShader(shader)
复制代码
void glReleaseShaderCompiler(void)
void glReleaseShaderCompiler ()

功能:删除Shader编译器相关的资源
复制代码

GLSL 语法

基本类型:

1.数值类型
2.布尔类型
注意:不能自动转型;矢量也不能自动补齐
复制代码

变量:

1.a-z, A-Z, 0-9, _
2.首字母不能为数字
3.不能用关键字和保留字作变量名
4.不能以gl_,_webgl_开头
复制代码

复杂变量:

结构体(和C类似)
数组
1.只支持一维数组
2.长度必须大于0的整型常量表达式
3.不可用const修饰
注:ES 2.0有以下限制:
4.Vertex Shader中的Sampler和Fragment Shader中的uniform数组只能被常整数表达式索引。(常整数表达式:常整数或常整数的运算)
采样器
1.通过采样器访问纹理
2.sampler2D
3.只能是uniform变量
4.纹理单元编号(至少支持8个)
复制代码

流程控制

1.if / else if / else
注:ES 2.0有以下限制:
①条件只能是bool类型;
②条件最好是uniform类型。
2.for(和C类似)
注:ES 2.0有以下限制:
①循环条件中的loop_index必须是和常量表达式比较;
②循环条件中的loop_index只能和常量表达式进行增减;
③循环主体中的loop_index相当于常量,不能被修改;
④不支持whiledo-while循环。
复制代码

内置函数

三角函数
指数函数
通用函数
几何函数
矩阵函数
矢量函数
纹理查询函数
复制代码

限定字

存储限定字:
const
编译时的常量,或者对函数来说为只读的参数,必须在声明时初始化。
attribute
修饰的变量只能用于顶点着色器,片元着色器中无法使用,数量限制至少是8,不能修饰array和struct。
uniform
客户端传递进来的变量,修饰的变量相对于所有顶点之间(或所有片元之间)在某一次渲染中是不变(相同)的,在着色器中为只读。
顶点着色器中uniform的个数有限制,ES 2.0是128个vec4条目,ES 3.0是256个vec4条目
片元着色器中uniform的个数限制,ES 2.0是16个vec4条目。
一共有四种类型的变量会占用uniform存储空间:
①uniform修饰符声明的变量
②const常量
③字面量(直接使用数字):相同值的实例会重复占用空间,所以最好使用const变量进行声明
④特定实现的常数
varying(ES 2.0才有这个类型,ES 3.0直接in/out)
修饰的数据从顶点着色器传输(插值)到片元着色器中
in
上一个阶段传递过来的变量,只读
in centroid
上一个阶段传递过来的变量,使用质心插值(针对多重采样缓冲区才有效)
out
传递到下一个阶段或者在函数中指定的返回值
out centroid
传递到下一个阶段,使用质心插值(针对多重采样缓冲区才有效)
inout
一个读/写变量,仅用于声明一个函数的一个参数;因为GLSL不支持指针和引用,所以这是一个值传递到一个函数并且允许这个函数修改并返回同一个变量值的唯一方法。
ES 3.0 支持smooth/flat插值修饰符,默认是smooth。
ES 3.0 支持uniform blocks。
复制代码

精度限定字

highp:
通常顶点着色器中的float是highp
适用于通过矩阵进行顶点位置变换、法线变换、纹理坐标的生成以及变换等操作。
mediump:
通常顶点着色器中的int是mediump
适用于颜色与光照的计算。
lowp:
通常片元着色器中的float是lowp
手动定义精度的方式:
precision lowp float
为变量指定合适的精度能够显著提高shader的性能
复制代码

精度限定字表

在顶点着色器中,如果没有指定默认精度,则int和float的默认精度都是highp。对于片元着色器,float类型的默认精度没有默认值,必须显示的声明。并且在fs中,不一定支持highp,可查询得知是否支持(GL_FRAGMENT_PRECISION_HIGH定义,或者查询OES_fragment_precision_high扩展)。
复制代码

不变量限定字

nvariance:
invariant关键字可以作用于vs输出的任意varying变量上。
shader在编译时,编译器可能进行优化,导致指令被重排。这意味着两个shader间相同的计算,不一定产生精确相等的结果。这对于multipass渲染来说是个问题,一个物体被渲染多次,如果计算出来的位置有差别,就会有瑕疵。比如产生z-fighting。
使用invariant可以在写shader时指定如果使用了相同的计算,输出的结果必须精确一致。
1.invariant关键字可以使用在varying声明上或者已经声明的varying上。
invariant gl_Position; //内置的已经声明的varying,使用invariant
invariant varying texCoord; //声明时使用invariant
2.可以使用#pragma指令器然所有的变量invariant:
#pragmaSTDGL invariant(all)
注意,为了实现invariant,编译器限制了优化,所有仅当需要时才使用invariant。
复制代码

VBO与VAO的作用与关系

早期的OpenGL为了将模型的顶点数据传送到显卡,需要逐个顶点进行(冗余处理的问题),如果还需要额外的信息(纹理坐标和法线)的话,当模型比较复杂时,将导致大量函数的调用,传输开销是相当大的!为了解决这个问题引入了VBO(Vertex Buffer Object),VBO可以将顶点数据保存在显存中,绘制时直接从显存中取数据,减少了数据传输的开销。

1.“客户端状态”和“服务端状态”

VBO出现的背景是人们发现VA(Vertex Array)还有不让人满足的地方。一般,在OpenGL里,提高顶点绘制速度的手法,一是把常规的glBegin() - glEnd()类代码段放入一个显示列表中(通常在初始化阶段完成),然后每遍渲染都调用这个显示列表;二是使用顶点数组,把顶点以及顶点属性数据作为数组,渲染的时候直接用一个或几个函数调动这些数组里的数据进行绘制,形式上是减少函数调用的次数(glVertex再见),提高绘制效率。但是这两种方法都有缺点。在说这个之前,应该对“客户端状态”和“服务端状态”两个名词有点了解。

OpenGL是个状态机,我们通常见到的glEnable - glDisable函数就是通知OpenGL开启/关闭某种状态的,譬如光照、深度检测等等。但是也有glEnableClientState - glDisableClientState这对。它们的区别是通知的具体对象在概念上不一样——分别是服务端和客户端。事实上我也无法很清楚地告诉你区别之处,反正你把你电脑上的具体程序,包括它用到的内存等等看作客户端,把你电脑里面的——显卡里的OpenGL“模块”,乃至整张拥有OpenGL流水线、硬件实现OpenGL功能的显卡,作为服务端。它们各自维护一些“状态”,glEnable 等是直接维护流水线处理相关的状态的,glEnableClientState 维护的则是进入流水线前的状态。流水线早期的T&L阶段,程序的顶点数据就被获知而接受处理了。至于顶点是怎么来的——是glVertex来的,还是glDrawArray来的,流水线没必要知道——这就是客户端的任务,所以是否使用顶点数组(作为一种状态是否需要被启动)都是由客户端决定。显示列表的glCallList比较特殊,它绕过客户端,直接通知服务端把之前初始化时设定的代码段所映射的硬件设置“启亮”,这是相当于直接把显存的某一段占有而随时呼唤了,硬件对此命令没有丝毫犹豫地接受,对该呼唤的答应变成一种“神经反射”行为——这是最理想最高级的“绘制”。

2.VBO,存在的理由

VBO出现的背景是人们发现VA还有不让人满足的地方,同样,显示列表也是。VA(顶点数组)是在客户端设置的,所以执行这类函数(glDrawArray,glDrawElement等等)后,客户端还得把得到的顶点数据向服务端传输一次(所谓的“二次处理”),这样一来就有了不必要的动作了,降低了效率——如果我们写的函数能直接把顶点数据发给服务端就好了——这正是VBO的特性之一。你可能会说,既然上面说到显示列表这么强大,用显示列表不就好了?显示列表的缺点正在于它的古板——一旦设定,就不容许更改,所以它只适合对一些“固定”的东西的绘制进行包装。(我们无办法直接在硬件层改顶点数据,因为这是脱离了流水线的事物。)而VBO直接把顶点数据交到流水线的第一步,与显示列表的效率还是有差距,但它这样就得到了操作数据的弹性——渲染阶段,我们的VBO绘制函数持续把顶点数据交给流水线,在某一刻我们可以把该帧到达了流水线的顶点数据捏回客户端修改(Vertex mapping),再提交回流水线(Vertex unmapping),(或者用glBufferData或glBufferSubData重新全部/部分提交更改了的顶点数据,)这是VBO的另一特性。

或者说,VBO结合了VA和显示列表的这个说法不太妥当,应该说它结合了两者的一些的特性,绘制效率在两者之间,且拥有良好的数据更改弹性。这种折衷造就了它一直为目前“最高”的地位。

3.VBO的使用

VBO的使用一方面跟FBO相似(应该调转一下主谓),另一方面与VA相似,绘制部分函数的样子根本就是同一个(毕竟叫作VA的升级版嘛)。

初始化部分:

//mVertexBufferObject为VBO对象ID
GLsizeiptr VertDataSize = XSCALE * ZSCALE * sizeof(CVector3);
glGenBuffers(1, &mVertexBufferObject);
glBindBuffer(GL_ARRAY_BUFFER, mVertexBufferObject);
glBufferData(GL_ARRAY_BUFFER, VertDataSize, Waterpos, GL_STREAM_DRAW);

GLsizeiptr TexCoordDataSize = XSCALE * ZSCALE * sizeof(TexCoord);
glGenBuffers(1, &mTexCoordBufferObject);
glBindBuffer(GL_ARRAY_BUFFER, mTexCoordBufferObject);
glBufferData(GL_ARRAY_BUFFER, TexCoordDataSize, Watertex, GL_STREAM_DRAW);

GLsizeiptr NormalDataSize = XSCALE * ZSCALE * sizeof(CVector3);
glGenBuffers(1, &mNormalBufferObject);
glBindBuffer(GL_ARRAY_BUFFER, mNormalBufferObject);
glBufferData(GL_ARRAY_BUFFER, NormalDataSize, Waternorm, GL_STREAM_DRAW);
复制代码

其他顶点属性(包括顶点位置glVertex,顶点法线glNormal,顶点颜色glColor,顶点纹理坐标glTexCoord,顶点雾坐标等等,或者shader的attribute属性变量)也是类似的。其中,Waterpos等储存了具体的数据(注意数据存放次序呀,一般栽会栽在这里)。GL_STATIC_DRAW,GL_STREAM_DRAW,GL_DYNAMIC_DRAW用于给OpenGL系统提醒:预期数据是一直不变、数据每帧变一次或几帧变一次、数据每帧变两三次以上,方便硬件内部优化吧。

渲染部分:

glBindBuffer(GL_ARRAY_BUFFER, mVertexBufferObject);
glEnableClientState(GL_VERTEX_ARRAY);
glVertexPointer(3, GL_FLOAT, 0, 0);

glBindBuffer(GL_ARRAY_BUFFER, mNormalBufferObject);
glEnableClientState(GL_NORMAL_ARRAY);
glNormalPointer(GL_FLOAT, 0, 0);
      ........

   glDrawArrays(GL_QUADS, 0, VertexCount);

      .........
glDisableClientState(GL_NORMAL_ARRAY);
glDisableClientState(GL_VERTEX_ARRAY);
glBindBuffer(GL_ARRAY_BUFFER, 0);
复制代码

4.数据更改的方式

就是三种:1.重新对VBO对象进行绑定,用glBufferData把新数据交给VBO。它在当前帧执行,会把当前VBO内容清空,放入新数据后继续;2.glBufferSubData只更新VBO中部分数据,但是如果当前数据正在进行中,它会等待数据全部发送完毕,然后待把某部分改好了再全部发送;3.glMapBuffer - glUnMapBuffer,如上所述,数据会全传回来再传过去。这里是参考资料的一断代码,可参考:

glBindBuffer(GL_ARRAY_BUFFER, BufferName[COLOR_OBJECT]);
glBufferData(GL_ARRAY_BUFFER, ColorSize, NULL, GL_STREAM_DRAW);
GLvoid* ColorBuffer = glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY);
 
glBindBuffer(GL_ARRAY_BUFFER, BufferName[POSITION_OBJECT]);
glBufferData(GL_ARRAY_BUFFER, PositionSize, NULL, GL_STREAM_DRAW);
GLvoid* PositionBuffer = glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY);
 
memcpy(ColorBuffer, ColorData, ColorSize);
memcpy(PositionBuffer, PositionData, PositionSize);
 
glBindBuffer(GL_ARRAY_BUFFER, BufferName[COLOR_OBJECT]);
glUnmapBuffer(GL_ARRAY_BUFFER);
glColorPointer(3, GL_UNSIGNED_BYTE, 0, 0);
 
glBindBuffer(GL_ARRAY_BUFFER, BufferName[POSITION_OBJECT]);
glUnmapBuffer(GL_ARRAY_BUFFER);
glVertexPointer(2, GL_FLOAT, 0, 0);
复制代码

顶点属性(Vertex Attribute),是关于顶点坐标和顶点纹理、顶点法线以及其他信息的统称。

顶点的格式:

typedef struct
{
    float x;
    float y;
    float z;
}Vec3;
 
typedef struct
{
    float x;
    float y;
}
typedef struct
{
    Vec v;
    Vec2 vt;
    Vec3 vn;
}Vertex;
复制代码

从以上可知,通过VBO我们可以将顶点属性数据保存在显存中,当绘制时问题又来了,需要调用好几个函数,过程挺复杂的。为了解决这个问题,OpenGL又引入了VAO(Vertex Array Object)来关联VBO中的数据,有了VAO,任何数组形式的GL函数调用都会添加到VAO的绘制列表当中(直到解除VAO绑定),当需要绘制的时候,我们仅需要重新绑定VAO,那么之前创建的绘制列表将会重新激活,使得绘制代码更加简洁。

VBO归根到底是显卡存储空间里的一块缓存区(Buffer)而已,这个Buffer有它的名字(VBO的ID),OpenGL在GPU的某处记录着这个ID和对应的显存地址(或者地址偏移,类似内存)。

//生成一个Buffer的ID,不管是什么类型的  
glGenBuffers(1, &m_nQuadVBO);   
//绑定ID,同时也指定该ID对应的buffer的信息类型是GL_ARRAY_BUFFER  
glBindBuffer(GL_ARRAY_BUFFER, m_nQuadVBO);  
//为该ID指定一块指定大小的存储区域(区域的位置大抵由末参数影响),  传输数据      
glBufferData(GL_ARRAY_BUFFER, sizeof(fQuadData), fQuadData, GL_STREAM_DRAW);  
复制代码

这里是VBO的初始化阶段。在这里我们看到了这是对位置,还是颜色,还是纹理坐标,还是法线,还是其他顶点属性进行设置的吗?是的,这个信息是:起码在初始化阶段,一个VBO对于交给它存储的数据到底是什么,完全不知道。

glBindBuffer(GL_ARRAY_BUFFER, m_nPositionVBO);  
glEnableClientState(GL_VERTEX_ARRAY);  
glVertexPointer(2, GL_FLOAT, 0, NULL);  
  
glBindBuffer(GL_ARRAY_BUFFER, m_nTexcoordVBO);  
glEnableClientState(GL_TEXTURE_COORD_ARRAY);  
glTexCoordPointer(2, GL_FLOAT, 0, NULL);  
  
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_nIndexVBO);  
  
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, NULL);  
  
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, NULL);  
  
glDisableClientState(GL_TEXTURE_COORD_ARRAY);  
glDisableClientState(GL_VERTEX_ARRAY);  
glBindBuffer(GL_ARRAY_BUFFER, NULL);  
复制代码

对于第一段渲染代码,glVertexPointer(2, GL_FLOAT, 0, NULL)这个函数指定了VBO里的是什么数据——顶点位置,float类型,2个float指涉一个顶点位置,在区域里无偏移地采集数据,等等。之后的glDrawElements只不过根据组织模式(GL_TRIANGLES,这个是直接交给vertex处理后的Geometry处理的)和索引数据去采集VBO里的这些数据罢了——它从某个地方获取了glBindBuffer指定的位置,还有glVertexPointer设定的信息(由glEnableClientState启用),它进行绘制所需要的一切——这个地方,就是所谓的GL-Context吧,那个保存了所有运行时流水线状态的东西。

glBindBuffer(GL_ARRAY_BUFFER, m_nQuadPositionVBO);  
glEnableVertexAttribArray(VAT_POSITION);  
glVertexAttribPointer(VAT_POSITION, 2, GL_INT, GL_FALSE, 0, NULL);  
  
glBindBuffer(GL_ARRAY_BUFFER, m_nQuadTexcoordVBO);  
glEnableVertexAttribArray(VAT_TEXCOORD);  
glVertexAttribPointer(VAT_TEXCOORD, 2, GL_INT, GL_FALSE, 0, NULL);  
  
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_nQuadIndexVBO);  
  
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, NULL);  
  
glDisableVertexAttribArray(VAT_POSITION);  
glDisableVertexAttribArray(VAT_TEXCOORD);  
  
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, NULL);  
glBindBuffer(GL_ARRAY_BUFFER, NULL);  
复制代码

对于第二段渲染代码,大体是一样的,只是glVertexAttribPointer使用第一个参数(location)指涉对应vertex-shader里哪个in attribute。VBO在渲染阶段才指定数据位置和“顶点信息”(Vertex Specification),然后根据此信息去解析缓存区里的数据,联系这两者中间的桥梁是GL-Contenxt。GL-context整个程序一般只有一个,所以如果一个渲染流程里有两份不同的绘制代码,GL-context就负责在它们之间进行状态切换。这也是为什么要在渲染过程中,在每份绘制代码之中有glBindBuffer/glEnableVertexAttribArray/glVertexAttribPointer。那么优化方法就来了——把这些都放到初始化时候完成吧!——这样做的限制条件是“负责记录状态的GL-context整个程序一般只有一个”,那么就不直接用GL-context记录,用别的东西做状态记录吧——这个东西针对"每份绘制代码“有一个,记录该次绘制所需要的所有VBO所需信息,把它保存到GPU特定位置,绘制的时候直接在这个位置取信息绘制。

VAO的全名是Vertex Array Object,首先,它不是Buffer-Object,所以不用作存储数据;其次,它针对”顶点“而言,也就是说它跟”顶点的绘制“息息相关,在GL3.0的世界观里,这相当于”与VBO息息相关“。

VAO记录的是一次绘制中做需要的信息,这包括”数据在哪里-glBindBuffer(GL_ARRAY_BUFFER)“、”数据的格式是怎样的-glVertexAttribPointer“(顶点位置的数据在哪里,顶点位置的数据的格式是怎样的/纹理坐标的数据在哪里,纹理坐标的数据的格式是怎样的....视乎你让它关联多少个VBO、VBO里有多少种数据),顺带一提的是,这里的状态还包括这些属性关联的shader-attribute的location的启用(glEnableVertexAttribArray)、这些顶点属性对应的顶点索引数据的位置(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER),如果你指定了的话)。

struct VertexAttribute  
{  
    bool bIsEnabled = GL_FALSE;  
    int iSize = 4; //This is the number of elements in this attribute, 1-4.  
    unsigned int iStride = 0;  
    VertexAttribType eType = GL_FLOAT;  
    bool bIsNormalized = GL_FALSE;  
    bool bIsIntegral = GL_FALSE;  
    void * pBufferObjectOffset = 0;  
    BufferObject * pBufferObj = 0;  
};  
   
struct VertexArrayObject  
{  
    BufferObject *pElementArrayBufferObject = NULL;  
    VertexAttribute attributes[GL_MAX_VERTEX_ATTRIB];  
}  
复制代码

初始化部分

glGenVertexArrays(1, &m_nQuadVAO);  
glBindVertexArray(m_nQuadVAO);  
  
  
glGenBuffers(1, &m_nQuadPositionVBO);  
glBindBuffer(GL_ARRAY_BUFFER, m_nQuadPositionVBO);  
glBufferData(GL_ARRAY_BUFFER, sizeof(fQuadPos), fQuadPos, GL_STREAM_DRAW);  
  
glEnableVertexAttribArray(VAT_POSITION);  
glVertexAttribPointer(VAT_POSITION, 2, GL_INT, GL_FALSE, 0, NULL);
  
glGenBuffers(1, &m_nQuadTexcoordVBO);  
glBindBuffer(GL_ARRAY_BUFFER, m_nQuadTexcoordVBO);  
glBufferData(GL_ARRAY_BUFFER, sizeof(fQuadTexcoord), fQuadTexcoord, GL_STREAM_DRAW);  
  
glEnableVertexAttribArray(VAT_TEXCOORD);  
glVertexAttribPointer(VAT_TEXCOORD, 2, GL_INT, GL_FALSE, 0, NULL);  
  
glGenBuffers(1, &m_nQuadIndexVBO);  
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_nQuadIndexVBO);  
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(nQuadIndex), nQuadIndex, GL_STREAM_DRAW);  
  
  
glBindVertexArray(NULL);  
  
glBindBuffer(GL_ARRAY_BUFFER, NULL);  
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, NULL);  
复制代码

渲染部分

glBindVertexArray(m_nQuadVAO);  
  
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, NULL);  
  
glBindVertexArray(NULL); 
复制代码

你甚至可以认为VAO就是一个状态容器,其中粗体字的那几行就是它以及它所”包含“的东西——填充了”VertexArrayObject结构体“的东西。注意:1.没有一个合适的地方给glDisableVertexAttribArray了,事实上调用glBindVertexArray(NULL)的时候里面所有状态都”关掉“了,也就没所谓针对顶点属性的location做其他什么;2.glBindBuffer(GL_ARRAY_BUFFER, NULL)/glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, NULL)一定要在glBindVertexArray(NULL)后面(不然VAO就把它们也包含了,最后就渲染不出东西了);3.glDrawElements里面的东西(顶点索引的属性状态)VAO可没记录保存哦;4.glVertexPointer那类函数理论上也可以,但是建议还是不要混用deprecated的函数进去了。

举个例子:假设顶点着色器中定义了以下三个变量,用于保存顶点属性

attribute vec3 vertex_position;
attribute vec2 vertex_texture_coord;
attribute vec3 vertex_normal;
复制代码

在C++代码中定义一个init函数,用于加载shader、模型、纹理,以及最重要的创建VBO和VAO和关联它们

// 初始化
void init()
{
	pid = load_shader_program("vertex.glsl", "fragment.glsl");
	
	// 获取vertex_position、vertex_texture_coord和vertex_nromal的位置
	GLuint vertex_position_loc = glGetAttribLocation(pid, "vertex_position");
	GLuint vertex_texture_coord_loc = glGetAttribLocation(pid, "vertex_texture_coord");
	GLuint vertex_normal_loc = glGetAttribLocation(pid, "vertex_normal");
	
	// 加载纹理
	texture_id = create_texture_2d("texture.bmp");
	// 加载模型
	loadModel("cube.obj", vertices);
	// 设置相机初始位置
	camera.set_position(0.0f, 0.0f, 2.0f);
	
	// 创建VBO
	GLuint vbo_id;
	glGenBuffers(1, &vbo_id);
	glBindBuffer(GL_ARRAY_BUFFER, vbo_id);
	
	// 传输数据
	glBufferData(GL_ARRAY_BUFFER, sizeof(Vertex) * vertices.size(), &vertices[0], GL_STATIC_DRAW);
	
	// 创建VAO
	GLuint vao_id;
	glGenVertexArrays(1, &vao_id);
	glBindVertexArray(vao_id);
	
	// 启用顶点属性
	glEnableVertexAttribArray(vertex_position_loc);
	glEnableVertexAttribArray(vertex_texture_coord_loc);
	glEnableVertexAttribArray(vertex_normal_loc);
	
	glBindBuffer(GL_ARRAY_BUFFER, vbo_id);
	
	// vertex_position_loc与顶点数据映射
	glVertexAttribPointer(vertex_position_loc, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), 0);
	
	// vertex_texture_coord_loc与纹理数据映射
	glVertexAttribPointer(vertex_texture_coord_loc, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (0+sizeof(Vec3)));
	
	// vertex_normal_loc与法线数据映射
	glVertexAttribPointer(vertex_normal_loc, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (GLvoid*)(0+sizeof(Vec3) + sizeof(Vec2)));
}
复制代码

glBufferData用于向VBO传递数据:第二个参数表示数据的大小,第三个参数表示指向数据的指针; glVertexAttribPointer用于将顶点着色器中的attribute变量与VBO中的数据进行绑定:第一个参数表示属性的位置,第二个参数表示分量的个数,第三个参数表示数据类型,第四个参数表示是否归一化,第五个参数表示连续顶点属性之间的偏移量,第六个参数表示组件的第一个分量在对应的数组顶点属性中的偏移量

例子中VBO存储的数据格式如下:

VBO = { v1.x, v1.y, v1.z, vt1.x, vt1.y, vt1.z, vn1.x, vn1.y, vn1.z,
        v2.x, v2.y, v2.z, vt2.x, vt2.y, vt2.z, vn2.x, vn2.y, vn2.z,
        ... ...
        vn.x, vn.y, vn.z, vtn.x, vtn.y, vtn.z, vnn.x, vnn.y, vnn.z,}
复制代码

以取到法线数据为例,法线数据是vn1.xyz到vnn.xyz,从VBO的格式可知,vn1.x在VBO的 0+sizeof(Vec3)+sizeof(Vec2)位置,vn1.x到vn2.x之间相差了sizeof(Vertex)大小,这就是glVertexAttribPointer第五和第六个参数的由来。

最后绘制的时候,我们只需要调用以下代码即可

glBindVertexArray(vao_id);
glDrawArrays(GL_QUADS, 0, (GLsizei)vertices.size());
复制代码

OpenGL/GLSL数据传递

shader代码可以用文本文件保存,可以在程序中用字符串保存,但最终还是必须在程序中以字符串的形式传入Shader对象中;

创建Shader对象(glCreateShader——成功时返回非0的无符号Handle值,指涉Shader对象);

把shader代码传入shader对象(glShaderSource——注意此函数的参数,字符流地址参量是GLchar*,不支持宽字符。执行后可删除内存上保存的shader代码字符串副本);

编译Shader对象——正如我们编译任何代码一样(glCompileShader——应该以GL_COMPILE_STATUS为参调用glGetShaderiv检查编译是否成功。如果代码出现问题会在这个阶段报错,debug时可用glGetError查看更具体的错误类型);

按上步骤把一个“过程”中需要的各类型shader编译好(通常每种类型最多建一个shader对象——因为对于一个渲染管道(流程)来说,顶点、单几何体、像素都只处理一遍,不可能返回。shaders只是插入这个流程中取代对应的固定管道处理的“外挂”[在抹除固定管道处理、全shader时代来临之前可以这么说],在一个渲染流程中起作用的shaders姑且统称为一个shader过程);

OpenGL通过一个名为shaderProgram的对象来与shader交互。也可以说shaders通过这个对象连接到我们的OpenGL应用程序中,它具体地指涉shader过程。宏观概念上类似于我们平时写的“程序”,不过它是基于GPU的;

创建这么一个shaderProgram对象(glCreateProgram——成功时返回非0的无符号Handle值,指涉Shader程序对象);

把之前创建的shaders,一个一个地Attach到这个shaderProgram对象上(glAttachShader——当然理论上可以attach多于一个的同类型完整的shader,譬如vertexShader。但是在一个特定的渲染流程中只允许其中一个起作用,你明白的);

链接shaderProgram——正如我们链接任何程序一样(glLinkProgram——应该以GL_LINK_STATUS为参调用glGetProgramiv检查链接是否成功。)

至此一个shader程式装载完毕。在进入一批渲染流程前(即渲染一组物件前)启用这个shaderProgram对象(glUseProgram),它就会在这批渲染流程中起作用了。渲染完后可以(也应该)调用glUseProgram(NULL)来关闭这种介入,或者以其他shaderProgram渲染别的物件。

1.attribute变量

一般是指vertex attribute(顶点属性)——每个顶点都有一份。在vertexShader中,我们处理的是每个顶点,而我们希望传入的变量时每个顶点各异的时候,就使用这种变量(在shader中以attribute为限定符),它不必是传统意义上的“顶点属性”(顶点位置、法线之类),但它确实又是一种顶点的“属性”。OpenGL3.0之前(或者说,固定管道被严重BS之前),我们可以很舒心地使用gl_Vertex, gl_MultiTexcoord[], gl_Normal这类内置的attribute变量来指涉传入shader里的传统的顶点属性,但如今其实我们最好习惯于“没有你们的日子”(因为被BS了)。

这种变量需要在GPU里的Shader的存储空间中有固定的位置(地址)。在链接shaderProgram之前,这个位置是未确定的,因此我们可以在这个shaderProgram调用glLinkProgram之前,为这个attribute变量指定一个位置(用无符号值表示):glBindAttribLocation:

//为shader中的attribute变量attribName绑定到一个位置
GLuint nHandle = glCreateProgram()
 
glAttachShader(nHandle, nShaderHandle1);
....
 
glBindAttribLocation(nHandle, 2,  "attribName");
 
glLinkProgram(nHandle);
复制代码

在我的ZWGLSL类中,封装后提供给上层的shader装载API是setXXShader(建立各类型shader对象)和Load(类似上面的代码,建立shaderProgram,attach,bind和link)。上层怎么指定一个attribute的位置呢?当然可以把glLinkProgram独立起来——不过这样就拆开了上面的代码,上层需要多个调用,个人不喜欢。于是便在类内提供一个map容器:

//Load函数大致:
GLuint nHandle = glCreateProgram()
glAttachShader(nHandle, nShaderHandle1);
....
for(std::map<GLuint, const GLchar *>::iterator p = m_LocAttribMap.begin();p != m_LocAttribMap.end(); ++p)
{
    glBindAttribLocation(m_nShaderProgram, p->first, p->second);
}
glLinkProgram(nHandle);
 
 
//BindAttributeLocation函数:
void ZWGLSL::BindAttributeLocation(const GLchar *AttributeName, GLuint nLocation)
{
    m_LocAttribMap.insert(std::pair<GLuint, const GLchar *>(nLocation, AttributeName));
}
 
 
//外部调用一个shader的装载代码(需要绑定一个attribute变量的位置的情形):
m_ZWShader.SetVertexShader("XXX.vert");
m_ZWShader.SetFragmentShader("XXX.frag");
m_ZWShader.BindAttributeLocation("attribName",  2);
m_ZWShader.Load();
复制代码

我们利用这个“位置”(上面为2)来指定需要传给shader里的attribue变量的数据。

另一种获得这个“位置”的方法:我们不要去显式设定这个位置,而是去获取它。通常,如果shader里有attribute变量,且我们没有为它绑定一个位置(见1上文),那么shaderProgram在链接后,会自动为它分配一个位置。我们可以在任何需要的时候获取(查询)这个位置:glGetAttribLocation,就不必局限于在shaderProgram的创建和链接之间去绑定了:

//GetAttributeLocation函数:
GLint ZWGLSL::GetAttributeLocation(const GLchar *AttributeName)
{
    return glGetAttribLocation(m_nShaderProgram, AttributeName);
}
 
//外部调用一个shader的装载代码:
m_ZWShader.SetVertexShader("XXX.vert");
m_ZWShader.SetFragmentShader("XXX.frag");
m_ZWShader.Load();

....
//获取一个attribute变量的位置
GLint  nAttribLoc = m_ZWShader.GetAttributeLocation("attribName");

if(-1 != nAttribLoc )
{ 
     //使用
}
复制代码

这里返回一个有符号的int值,因为当要查询的这个变量在shader中不存在,或者它没有作用(非活动的:non-active),就会返回-1,否则才是它的位置。(当我们在shader里定义了一个变量,但是代码里却没见它有什么作为,就说它是非活动的。glGetActiveAttrib可返回那些活动的attributes。)

使用atribute变量的“位置”为它传递数据:

a)传统的glVertex3f类逐点绘制下(使用glVertexAttrib3f函数,以“位置”为首参):

//nAttribLoc是获得的一个vec3的attribute变量的位置
glBegin(GL_QUADS);
glNormal3f(vNormal.x, vNormal.y, vNormal.z);
glVertexAttrib3f(nAttribLoc, vAttribData.x, vAttribData.y, vAttribData.z);
glTexCoord2d(0.0, 0.0);
glVertex3d(pt1.x, pt1.y, pt1.z);

glNormal3f(vNormal.x, vNormal.y, vNormal.z);
glVertexAttrib3f(nAttribLoc, vAttribData.x, vAttribData.y, vAttribData.z);
glTexCoord2d(0.0,  1.0);  
glVertex3d(pt2.x, pt2.y, pt2.z);

.......
glEnd();
复制代码

b)VBO:

//nAttribLoc是获得的一个vec3的attribute变量的位置
 
glBindBuffer(GL_ARRAY_BUFFER,  nPosVBO);
glEnableClientState(GL_VERTEX_ARRAY);
glVertexPointer(3, GL_FLOAT, 0, NULL);

glBindBuffer(GL_ARRAY_BUFFER, nNormVBO);
glEnableClientState(GL_NORMAL_ARRAY);
glNormalPointer(GL_FLOAT, 0, NULL);

glBindBuffer(GL_ARRAY_BUFFER, nTexcoordVBO);
glEnableClientState(GL_TEXTURE_COORD_ARRAY);
glTexCoordPointer(2, GL_FLOAT, 0, NULL);

glBindBuffer(GL_ARRAY_BUFFER, nAttrbDataVBO);
glEnableVertexAttribArray(nAttribLoc);
glVertexAttribPointer(nAttribLoc, 3, GL_FLOAT, GL_FALSE, 0, NULL);

glDrawElements/glDrawArrays

glDisableVertexAttribArray(nAttribLoc);
glDisableClientState(GL_TEXTURE_COORD_ARRAY);
glDisableClientState(GL_NORMAL_ARRAY);
glDisableClientState(GL_VERTEX_ARRAY);
glBindBuffer(GL_ARRAY_BUFFER, NULL);
复制代码

有必要说明一下,在VBO中,glEnableClientState/glVertexPointer(glNormalPointer,glTexCoordPointer等)可以正确地为shader中内置的gl_Vertex, gl_Normal, gl_MultiTexcoord[]这些attribute变量指定VBO中的数据,但是这些传统顶点属性之外的就需要glEnableVertexAttribArray/glVertexAttribPointer了(事实上在日后不提倡用内置attribute变量后,这些传统的顶点属性也得被一视同仁地用这两个函数指定启用和数据格式)。参数除了特别的首参需要一个“位置”外,基本是一样的(中间多了个不常用的是否normalize数据的参)。

注意,每个顶点属性的数据都依托在一个VBO中了。要想给一个attrbute变量传递数据,请把数据交给一个VBO对象。

glsl3

在前文中提及到GLSL中每一个attribute变量都有一个“位置”值(Location),在ShaderProgram链接(link)前,可以Bind之,链接之后,可以Get之。通过这两种方式都可以建立attribute变量与顶点属性的联系。如今引入第三种方式——直接在GLSL代码中指定这些位置值:

#version 330  
  
layout(location = 0) in vec3 attrib_position;  
layout(location = 1) in vec2 attrib_texcoord;  
layout(location = 2) in vec3 attrib_normal;  
layout(location = 3) in int  attrib_clustercount;
复制代码

在上面的Vertex Program代码中,第一行(#version 330),表明我们现在使用的GLSL版本是GLSL3.3,以区别于以前的版本并允许我们使用基于GLSL3.3的功能。在过去,OpenGL的版本和GLSL版本是不统一的(前文中的GL2.2所对应的是GLSL1.2,而后来的对应关系是GL3.0-GLSL1.3,GL3.1-GLSL1.4,GL3.2-GLSL1.5),直到2010年OpenGL3.3/4.0规范的提出,khronos委员会决定让两者版本统一,所以就有了现在本博客所使用的OpenGL3.3-GLSL3.3的对应关系(注,ShaderModel4.0的显卡可达到的最高版本)。

接下来的几行声明了4个attribute变量。在GL2.x中一个attribute变量通常是“attribute vec3 attrib_position;”这样来表示,在GL3.x中,废弃了attribute关键字(以及varying关键字),属性变量统一用in/out作为前置关键字,对每一个Shader stage来说,in表示该属性是作为输入的属性,out表示该属性是用于输出的属性。这里,attribute变量作为Vertex Shader的顶点输入属性,所以都用in标记。另外,这里使用了layout关键字(通常是layout(layoutAttrib1=XXX, layoutAttrib2=XXX, ...)这样的形式)。这个关键字用于一个具体变量前,用于显式标明该变量的一些布局属性,这里就是显式设定了该attribute变量的位置值(location),其作用跟ShaderProgram(着色程序)链接前调用glBindAttribLocation来设定atribute变量的位置值是等效的。

为什么采用这种方式更好呢?其一当然是编码量减少了,二来也避免了去Get某个attribute的location带来的开销,三来,最重要的是,它重定义了OpenGL和GLSL之间attribute变量属性的依赖。过去我们的OpenGL端必须首先要知道GLSL端某个attribute的名字,才能设置/获得其位置值,如今两者只需要location对应起来就可以完成绘制时顶点属性流的传递了。不再需要在ShaderProgram的compile和link之间插入代码也更方便于其模块化。

2.uniform变量

uniform变量的特点是,对于一个vertexShader内处理的每个顶点(或者fragmentShader内处理的每个像素,诸此类推),它都是不变的。事实上它相当于一个全局量,并非每个顶点/几何/像素所雍有的变量(只是uniform对它们每一个都public而已)。当然了,你想在一个渲染流程中改变它的值也是8可能的。Shader中有gl_ModelViewMatrix等一大批内置的uniform变量(当然在OpenGL3.0/GLSL1.3后它们也被BS了)。

我们通常在shaderProgram链接后获取一个uniform变量的位置,然后向这个位置传送数据(glGetUniformLocation/glUniform)。但是要特别注意的一点是:我们只有在启用了一个shaderProgram后,才能做这样的事情:

//SendUniform函数,其中一个重载版本
void ZWGLSL::SendUniform(const GLchar *UniformName, GLfloat x)                            
{
   GLint location = glGetUniformLocation(m_nShaderProgram,UniformName);
   glUniform1f(location, x);
}
 
inline void Enable() { glUseProgram(m_nShaderProgram); };
inline void Disable(){ glUseProgram(0); };
 
//传送数据, m_UniformData为一个float数据:
ZWShader.Enable();
ZWShader.SendUniform("uniformName",  m_UniformData);
//Render Something
ZWShader.Disable();
复制代码

可能是不同的shaderProgram都有一套用于分配的“位置”吧,为了不混淆就so了(擦,怎么想起了相对内存地址来了- -)

有些时候,我们身不由己——我们给一个渲染对象类关联一个shader相关类的指针,shaderProgram启用与否完全交给这个渲染对象类——我们还是得在上层为shader指定uniform数据。这时候,可以在shader类指针之外,再关联一个map<uniform位置,uniform数据>(当然了这个“数据”还得根据uniform变量类型来细分),我们只把数据传给这个map。当shader在渲染对象类里头被启用之后,立即就把这些数据都通过glUniform传送。——这其实是面向对象设计要关心的内容了。

//not that good!
 
//somewhere:
glUseProgram(nHandle);
...//send uniform
glUniform(data used in rendering)...
glUseProgram(nHandle);
 
//somewhere else
glUseProgram(nHandle);
...//Render Something
glUseProgram(nHandle);
坐等可能出现的意想不到的悲剧吧。
复制代码

glsl3

对于uniform变量的声明方式,跟GL2.x的一致,使用uniform关键字就可以了。

#version 330  
  
uniform sampler2D   basetex1;  
uniform float fAlphaRestrictVal;  
uniform mat4 matModel;  
uniform mat4 matView;  
uniform mat4 matProj;  
复制代码

每一个uniform变量也都有其一个“位置值”(Location),在OpenGL中,我们可以通过glGetUniformLocation来获得。那么我们可以不可以像attribute变量那样,在Shader代码中显式指定这个Location呢?(其好处也是跟上述差不多的,但就是如果uniform变量太多的话这样做也麻烦,因为得在代码中一个一个指定不重复的location。)嘛,attribute变量location的显式指定,是经由GL扩展GL_ARB_explicit_attrib_location实现的,而事实上,现在也有GL_ARB_explicit_uniform_location这样一个GL扩展,能实现这样的功能,只不过它是OpenGL4.3标准的一部分,隶属于GLSL4.3,所以即使GL3.x支持这个扩展,我们还是暂时不要用的好。

那我们就像往常一样,在glUseProgram启用了某个ShaderProgram之后,一个一个地给每个unifom变量关联数据咯(通过其location)——等等,这是在运行期间设置数据值吧,那如果我这个关联数据并不是每帧都变化的,甚至它是一个固定值,这样做岂不太无聊太浪费了?事实上我们还是可以在glUseProgram之外绑定数据的——乃至直接在初始化时。这得益于glProgramUniform系列函数的引入,它比起往常的glUniform要多一个参数用来接收一个ShaderProgram的ID。在建立ShaderProgram后,我们也不需要glUseProgram来预先绑定它就可以直接取得某个uniform变量的location值并用glProgramUniform系列函数关联数据,而且这个数据在其后运行期间的每次glUseProgram后都不会失效。从理论上将,这族函数完全可以替代glUniform系列函数(是它们功能的一个超集),但是就不知道会不会有性能上的损失了(这个暂时目前找不到说法),所以我暂时建议是只对那些非动态变化的uniform变量使用了。

再来看看uniform变量的问题。通常一个稍微复杂点点、更多控制参数的Shader,都会有大量的Uniform变量需要设置,所以导致了我们很多时候在glUseProgram之后要调用一长串的glUniform函数来传递该Pass的数据。有没有方法尽量把这些操作合并呢?另外,我们知道一个Shader的可用Uniform数据大小是有一个上限值的(例如我目前显卡的一个vertex shader的GL_MAX_VERTEX_UNIFORM_COMPONENTS值是4096,意味着我在一个VertexShader里使用的active uniforms,大概就是最多4096个float/int值了,或者说最多1024个vec4、最多256个mat16),那么有没办法提高这个上限呢?优先选择TBO(Texture Buffer Object)作为传入数据的媒介,把数据装入一个一维纹理的Buffer中以提供给Shader。那么除了使用纹理数据外,还有没有更直接的方式呢?

Uniform Buffer Object(UBO)顾名思义,就是一个装载Uniform变量数据的Buffer Object。就概念而言,它跟VBO之类Buffer Object差不多,反正就是显存中一块用于储存特定数据的区域了。在OpenGL端,它的创建、更新、销毁的方式都与其他Buffer Object没什么区别,我们只不过把一个或多个uniform数据交给它,以替代glUniform的方式传递数据而已。这里必须明确一点,这些数据是给到这个UBO,存储于这个UBO上,而不再是交给ShaderProgram,所以它们不会占用这个ShaderProgram自身的uniform存储空间,所以UBO是一种全新的传递数据的方式,从路径到目的地,都跟传统uniform变量的方式不一样。自然,对于这样的数据,在Shader中不能再使用上面代码中的方式来指涉了。随着UBO的引入,GLSL也引入了uniform block这种指涉工具。

#version 330  
  
layout(std140) uniform matVP  
{  
   mat4 matProj;  
   mat4 matView;  
};  
复制代码

uniform block是Interface block的一种,(layout意义容后再述)在unifom关键字后直接跟随一个block name和大括号,里面是一个或多个uniform变量。一个uniform block可以指涉一个UBO的数据——我们要把block里的uniform变量与OpenGL里的数据建立关联。 因为这些uniform变量不是存储在Shader的“uniform区域”里的,所以也就没有那一套“位置值”(location),那么我们通过什么建立关联呢?

对于每一个uniform block,都有一个“索引值”(index),这个索引值我们可以在OpenGL中获得,并把它与一个具体的UBO关联起来。这样block内的数据声明就会与UBO中的实质数据联系起来了:

GLint nMatVPBlockIndex = glGetUniformBlockIndex(nProgramHandler, "matVP");  
  
//GLint nMatVPBlockIndex = glGetProgramResourceIndex(nProgramHandler, GL_UNIFORM_BLOCK, "matVP");  
  
if (GL_INVALID_INDEX != nMatVPBlockIndex)  
{  
    GLint nBlockDataSize = 0;  
  
    glGetActiveUniformBlockiv(nProgramHandler, nMatVPBlockIndex, GL_UNIFORM_BLOCK_DATA_SIZE, &nBlockDataSize);  
  
    glGenBuffers(1, &m_nUBO);  
  
    glBindBuffer(GL_UNIFORM_BUFFER, m_nUBO);  
  
    glBufferData(GL_UNIFORM_BUFFER, nBlockDataSize, NULL, GL_DYNAMIC_DRAW);  
  
    glBindBufferRange(GL_UNIFORM_BUFFER, 0, m_nUBO, 0, nBlockDataSize);  
  
    glUniformBlockBinding(nProgramHandler, nMatVPBlockIndex, 0);  
  
    glBindBuffer(GL_UNIFORM_BUFFER, NULL);  
} 
复制代码

一般我们可以使用glGetUniformBlockIndex来获取这个Index,但扩展GL_ARB_program_interface_query引入了比较统一的获取ShaderProgram内资源的相关属性的API(详见此扩展的spec),所以也可以以GL_UNIFORM_BLOCK调用glGetProgramResourceIndex来获取资源的Index。得到名为matVP的uniform block的Index后,我们可以查询这个block的相关信息(glGetActiveUniformBlockiv)。为了建立合适大小的UBO,这里查询了这个block所需的字节大小(GL_UNIFORM_BLOCK_DATA_SIZE)的值(注意这个值代表此block所占的大小,它可能会比block内数据实际相加后的值要大,下面会再述)。

建立一个UBO的过程跟建立其他类型的Buffer Object相似,不过Target是GL_UNIFORM_BUFFER,数据为空。接下来是把一个UBO(ID为m_nUBO)和Shader内的uniform block(Index为nMatVPBlockIndex)相关联:把它们都关联到同一个uniform buffer binding-point。其中前者通过glBindBufferBase或glBindBufferRange来完成,其中第二个参数就是binding-point,这里选择的是binding-point_0(参数值为0,当然你可以输入1、2、3...以选择binding-point_1、binding-point_2、binding-point_3…);同样,对于后者uniform block,也通过glUniformBlockBinding来完成,其中第三个参数是binding-point,这里同样选择了第0个binding-point——这样OpenGL端的UBO和GLSL端的uniform block就联系在一起了。Shader中需要使用block中的uniform变量时,就会索引到对应的UBO中对应的位置的数据。

所谓binding-point(或者说binding-location),我理解为是OpenGL的Context上的一个个状态位。通常来说,我们可以建立非常多的UBO,它们的数据区在显存中,以ID标识,一般通过Context绑定一个UBO的ID的方式让OpenGL去寻找对应的显存位置——这是一种非常耗时的操作(应该说,所有bind类的操作都是)。数据需要更新就算了,但如果Shader执行时也必须为每个uniform block去绑定、寻觅数据区……为避免这样的情况所以就需要一个足以减少消耗的桥梁物,这个中间物件保存着能够直达具体某个UBO数据区的“方式”(不妨暂假想为该数据区的起始显存地址、长度等),然后我们把这个中间物件的位置告诉Shader,让Shader在需要时直接“来到”这个中间件中获取某个显存区的实质数据。这里与前者最大的区别应该就是Shader到中间件的用时——这应该足够快。所以首先这个中间物件应该存储在OpenGL的Context上(于是它名义上就是一个OpenGL状态),OpenGL内的对象的交流是比较便捷的,至少比Bind方式去存取“遥远的”显存数据要快不少,其次这个中间物件自身也应该容易表示,让Shader能“直接认门牌”——这些中间物件就是单纯Zero-Base数字序列形式的uniform binding-point,OpenGL通过它一步定位到实质数据处。

OpenGL Context本身也应该是一个尽量小体积的东西,所以不便在它身上放太多这种binding-point。在我的显卡上,GL_MAX_UNIFORM_BUFFER_BINDINGS的个数为36,这表示同一时间能映射的UBO-uniform block关系最多只有36对(间接也限制了一个ShaderProgram中uniform block的个数),哪怕你有大量的UBO,为了以上机制的实行,也只能接受这个限制。我们就是通过glBindBufferBase/glBindBufferRange来我UBO或UBO中的某分区的信息存储至某个binding-point上,然后通过glUniformBlockBinding来“通知”ShaderProgram某个uniform block的数据信息存储在哪个binding-point上。如果把glUniformBlockBinding当成glUniform族函数,这个操作会更亲切一点:只不过如今对于目标block使用的是Index而不是Location(事实上它的行为更类似上面提到的glProgramUniform族函数,因为不需要事先glUseProgram启用某个ShaderProgram而是作为首参罢)。

除了UBO,前面某篇博文[乱弹纪录IV:Transform Feedback]中提到的Transform Feedback Buffer也是使用binding-point(参见文中代码段)的“好手”。因为Shader同样需要快速找出需要feedback的那个Buffer的所在地,尤其是通过GL_SEPARATE_ATTRIBS的方式为每一个输出数据独立指定buffer时,就需要用到多个transform-feedback binding-point来储存各个buffer的信息了。其限制个数其实就是GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS了(本显卡此数字为4)。

这里引出一个问题:我们能不能像TransformFeedback那样,为一个UBO对象非配数个binding-point呢?可以的。这样做的目的也很明确——单个UBO多个uniform-block。准确地说,是每个uniform block对应该UBO存储区域中不同的分区域(sub-region)——glBindBufferRange,就是你了!

GLint nMatVPBlockIndex = glGetUniformBlockIndex(nProgramHandler, "matVP");  
  
GLint nCloudScaleIndex = glGetUniformBlockIndex(nProgramHandler, "Scale");  
  
if (GL_INVALID_INDEX != nMatVPBlockIndex && GL_INVALID_INDEX != nCloudScaleIndex)  
{  
    int nUniformBufferAlignSize = 0;   
  
    glGetIntegerv(GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT, &nUniformBufferAlignSize);   
  
    GLint nBlockDataSize1 = 0, nBlockDataSize2 = 0;  
  
    glGetActiveUniformBlockiv(nProgramHandler, nMatVPBlockIndex, GL_UNIFORM_BLOCK_DATA_SIZE, &nBlockDataSize1);  
  
    glGetActiveUniformBlockiv(nProgramHandler, nCloudScaleIndex, GL_UNIFORM_BLOCK_DATA_SIZE, &nBlockDataSize2);  
  
    glGenBuffers(1, &m_nUBO);  
  
    glBindBuffer(GL_UNIFORM_BUFFER, m_nUBO);  
  
    glBufferData(GL_UNIFORM_BUFFER, nUniformBufferAlignSize + nBlockDataSize2, NULL, GL_DYNAMIC_DRAW);  
  
    glBindBufferRange(GL_UNIFORM_BUFFER, 0, m_nUBO, 0, nBlockDataSize1);  
  
    glUniformBlockBinding(nProgramHandler, nMatVPBlockIndex, 0);  
  
    glBindBufferRange(GL_UNIFORM_BUFFER, 1, m_nUBO, nUniformBufferAlignSize, nBlockDataSize2);  
  
    glUniformBlockBinding(nProgramHandler, nCloudScaleIndex, 1);  
}  
复制代码

上面代码段中,我们把两个uniform-block关联到同一个UBO的两个区域:[0 ~ nBlockDataSize1]、[nUniformBufferAlignSize ~ nUniformBufferAlignSize+nBlockDataSize2]。为什么第二个block不是映射到[nBlockDataSize1 ~ nBlockDataSize1+nBlockDataSize2]呢?这里有个比较重要的概念:数据对齐。对于uniform-block,可以通过GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT找出这个对齐值(本显卡上此数值是256字节,所以每个uniform block都是256字节对齐,相邻uniform block的间隔必须满足256字节的整数倍,否则会发现数据会对不上)。记住了,block与block的数据不是紧紧pack在一起的。很容易想象,跟CPU上存储结构体一样,这是为了数据存取效率考虑,至于为什么是这个值,就要更深入研究了。

麻烦可不止这一个——单个uniform-block里面的数据,也有字节对齐的机制——这给uniorm变量的数据更新带来更大的麻烦。

先来大致了解一下上面的GLSL代码的uniform block前面的layout内容。一般uniform block按数据组织类型可分为三种(目前):packed、shared、std140,我们可以在它们前面用layout去指定该block属于哪种类型(也可以全局设置,也就是把layout单独作为一语句,此时它影响随后的各个没前接layout的uniform block)。

UBO的一个最显眼的好处就是实现数据共享。譬如我上面的matPV这个uniform block就是最好的例子:通常渲染场景时,只会有一个视图矩阵和一个投影矩阵,而且它们相对每一帧都是固定数据。而我们可能场景里物件用到的Shader不一样,但它们都得通过这两个矩阵计算最终的顶点输出啊?以前的话,我得每个Shader都传一次这些相同的矩阵数据,不仅时间上glUniform族函数会比较多而且空间上也分别占每个ShaderProgram本身的同等的存储资源。如今把它们统一在一个UBO中,每帧更新就只要更新UBO一次就可以了,而且也只占一份的资源空间(在显存上)。

为什么突然插播以上“广告”呢?因为这对数据组织形式影响甚大。为了实现数据共享,必须保证各个shader里的指涉该UBO的unifom block“一模一样”。GLSL编译器会检查并自动删掉那些非active(在shader中没有实质用途)的uniform变量。那么,假如我们的多个Shader里都有相同的uniform block,而里面某个变量x被ShaderA用到而没背ShaderB用到,那么前者就会把它默默删掉,这样数据结构不统一,自然映射到同一个UBO也无法预计得到各个子数据的具体位置(必须得针对每个Shader的uniform block内每个变量查询它的Offset)——block内的这种“检查”机制由packed这种layout掌控,为了要关闭这种机制,就需要选择其他三种layout。而shared(顺带一提,这个是默认layout)与std140不同之处在于,它虽然不会“删掉”block内的non-active变量,而且保证这些uniform block内的数据在存储分布上的一致性(所以各个shader能共享同一个block结构),但它不会去固定统一存储分布,所以还是有必要去查询各个变量的offset(因为可能在显卡A上这个offset是16在显卡B上就变32了)。至于std140等,其实就是排除这些因素而有着严格限制的一个数据组织结构“无优化”的版本,所以一般的场合下我们应该首选这类std(OpenGL-Standard)的layout。顺带说一下因为最初UBO/uniform block是跟随OpenGL3.1/GLSL1.4引入的所以有此std140之名(其实现存的类似layout还有个std430,但它是专门留给OpenGL/GLSL4.3的storage buffer block产生更小的offset而用的,按此不表)。

说了那么多,既然一般应用首选std140,那么它那个固定的offset是多少呢?根据我的不严格查验(没验证多个显卡),其值为16字节,也就是说数据按16字节对齐。而数据中还再分为vector、数组、矩阵这些,也是按类似规则限制(不一一举出,查spec去吧)。举例一下吧:

layout(std140) uniform matVP  
{  
   float elapsedTime;  
   mat4 matProj;  
   mat4 matView;  
};  
layout(std140) uniform Scale
{
  uniform float cloudScale;
};
复制代码

要更新这两个block对应的那个UBO,应该这样:

{  
//Render  
    glBindBuffer(GL_UNIFORM_BUFFER, m_nUBO);  
    glBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof(GLfloat), &fElapsed);  
  
    //current std140 alignment(for base sclaer): 16  
    glBufferSubData(GL_UNIFORM_BUFFER, 16, sizeof(Matrix16), m_mtProj.mt);  
    glBufferSubData(GL_UNIFORM_BUFFER, 16 + sizeof(Matrix16), sizeof(Matrix16), m_mtView.mt);  
  
    //GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT: 256  
    glBufferSubData(GL_UNIFORM_BUFFER, 256, sizeof(GLfloat), &fCScale);  
    glBindBuffer(GL_UNIFORM_BUFFER, NULL);  
}  
复制代码

其实UBO除了能够共享unifom变量数据外,上面的叙述还隐含有它的两个重要优点:一点是索引、切换binding-point的速度比较快,比起多个glUniform的调用传递数据也更快;另一点是对于显存中的uniform数据,可用的存储空间也大幅增加(而且对比TBO,UBO更适合需要线性存取的数据)——这回应了介绍UBO前的那两个问题。

uniform数据应尽量使用UBO来存放,尤其是那些需要Shader共享的数据,当然了零碎细小的数据还是glUniform/glProgramUniform类函数会更方便点吧~

3.varying变量

这个就是shader之间传递的变量类型了。不是本文关心的。当然还是要提醒一下,在这边的shader里的varying输出,在那边的varying输入就可能是被栅格化了(说通俗点,被插值了)。

varying变量主要用于在Shader Stage间进行传递,注意的是在光栅化(Rasterization)的时候,这些变量也会跟着一起被光栅插值。那如果我们不想某个顶点属性被光栅化,该怎么办呢?

glShadeModel,它在固定管道渲染流水线上能起到控制图元属性是否被插值的功效(需要光栅化时传入参数GL_SMOOTH,不需要时传入GL_FLAT),那么当选择不插值时(GL_FLAT),流水线上发生了什么呢?

假设现在流水线上,经过裁剪、归一化等,生成了一个屏幕上的三角图元(三个顶点上的颜色属性分别是c1、c2、c3),进入光栅化阶段。假如进行插值,三角图元里各像(假设共n个)素会根据其各自位置对三个顶点的颜色值进行线性插值,生成对应的n个颜色值(cList[n]);假如不插值,则该三角形里所有像素都会是同一个值(cConst),这个值可能等于c1、c2或c3其中一个。到底是哪一个呢?这取决于哪个顶点是provoking-vertex。你可以在OpenGL端通过glProvokingVertex函数改变这个设置(参数GL_FIRST_VERTEX_CONVENTION/GL_LAST_VERTEX_CONVENTION决定取图元绘制顺序的第一个顶点还是最后一个顶点作为provoking-vertex)。

其实要让GLSL中某个作为顶点属性的varying变量不被光栅化,只要在它前面加一个flat关键字就可以了。这样它就像上述的那样,到达Fragmen Shader的图元上所有像素的该varying值都是相同的值(provoking vertex上的值):

//vertex shader or geometry shadr  
flat out float visibility;  
  
//fragment shader  
flat in float visibility;  
复制代码

一个ShaderProgram中不能有两个同为输入的同名varing变量,也不能有两个同为输出的同名varing变量存在。所以即使表示的是同一个变量,也得使其名字不一样:

//Vertex Shader  
out vec2 varying_vg_texcoord;    
  
//Geometry Shader  
in  vec2 varying_vg_texcoord[];    
out vec2 varying_gf_texcoord;    
  
//Fragment Shader  
in vec2 varying_gf_texcoord;    
复制代码

这样的话,在有些场合需要实现不同shader的组合——譬如实现一个可加入也可不加入的Geometry Shader,就难办了(何况当代流水线上的Shader可不止这三个呢)。为解决这个麻烦,也为了把变量声明组织得更“好看”一些,我们再次用到interface block。上面的uniform block是其中一种,但它还包括in block和out block这两种可用于varing变量的:

//Vertex Shader  
out Varying  
{  
     vec2 texcoord;  
}VaryingOut;  
  
//Geometry Shader  
in Varying  
{  
     vec2 texcoord;  
}VaryingIn[];  
  
out Varying  
{  
    vec2 texcoord;  
}VaryingOut;  
  
//Fragment Shader  
in Varying  
{  
    vec2  texcoord;     
}VaryingIn;  
复制代码

注意这里使用了block insatnce name(紧随大括号后的那个名字),这个名字对各Shader Stage来说都是独特的,所以改成上面这样的话,block之间也不会发生名字冲突,block内的varying变量也就可以用同一个名字了。使用时需要按"blockInstanceName.varyingVariable"的类似结构体内变量的样式来表示:

//Geometry Shader Example  
void main(void)  
{  
   for(int i = 0; i < gl_in.length(); ++i)  
   {  
        gl_Position = gl_in[i].gl_Position;  
  
        VaryingOut.texcoord = VaryingIn[i].texcoord;  
  
    EmitVertex();  
   }  
   EndPrimitive();  
}  
复制代码

block自带组织多个变量声明的功效:

//Geometry Shader Example  
out Varying  
{  
    vec3 position;  
    int  cloudcount;  
    vec3 dimension;  
}VaryingOut;  
复制代码

另外,对于Transform Feedback,指定输出Varing属性时,也要按上述的结构体内变量表示法:

//OpenGL Code Example  
const GLchar *varyingOutCloudFeed[] = {"Varying.position", "Varying.cloudcount", "Varying.dimension"};  
glTransformFeedbackVaryings(m_CloudFeedShader.GetProgramHandler(), 3, varyingOutCloudFeed, GL_SEPARATE_ATTRIBS);  
m_CloudFeedShader.Link();  
复制代码

4.fragmentShader输出

其实主要是想带出另一个函数:glBindFragDataLocation。其实目前来说,它在数据传递中的作用,其实只是指定了fragmentShader最终的像素颜色信息所要输出的BUFFER(GL_DRAW_BUFFER0 /GL_DRAW_BUFFER1)。这个是OpenGL3.0/GLSL1.3要鄙视我们熟悉的(gl_FragColor/gl_FragData[])用的——把shader里定义的一个输出的out型变量(用以输出最终像素颜色)绑定给一个输出Buffer。

一般来说,输出的是颜色值,输出目标是Frame Buffer。这又包括常规的输出到屏幕Buffer、输出到FBO,另外还可以通过MRT(Multi Render Target)输出到两个以上的FBO中。但是,这些对于Fragment Shader来说并没太多不一样:通过ShaderProgram链接前的glBindFragDataLocation指定输出到第几个Buffer(默认是0)。类似于上述的attribute变量,我们也可以直接通过layout来指定这个location值:

//单输出  
layout(location = 0) out vec4 fragColor;  
  
//MRT  
layout(location = 0) out vec4 fragColor0;  
layout(location = 1) out vec4 fragColor1;  
...  
复制代码

然后只要在FragmentShader中把结果对应地赋给这些输出型变量就可以了。但是,这些layout里的关键字其实还有个index——只是默认为0而已:

layout(location = 0, index = 0) out vec4 fragColor;  
layout(location = 0, index = 1) out vec4 src1Color;  
复制代码

它们同样是输出到第0个缓冲区,但是其中有一个的index为1——这个src1Color是所谓的Second Output。它同样储存在一块缓冲区域中,但我们在OpenGL中怎么获得这个区域的颜色值呢?答案就是由GL_ARB_blend_func_extended扩展引入的,新的混合参数(GL_SRC1_COLOR/GL_SRC1_ALPHA等等这类新旧的enum)。它们作为混合因子而存在——这里输出的src1Color,就只能作为各个对应像素混合因子来用。简单举例:

glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_SRC1_COLOR);  
复制代码

该代码启用混合,当前绘制的内容(混合源src,即fragColor)的混合因子是自己的alpha值,而背景(混合目标dst,即绘制前此FrameBuffer的内容)处对应的被覆盖像素的混合因子则是该对应像素输出的src1Color值,其中RGBA分量分别用于混合RGBA四个通道:

finalColor = sourceColor * sourceAlpha + destinationColor * src1Color
复制代码
关注下面的标签,发现更多相似文章
评论