【OpenGL ES】着色器Shader与程序Program
来源:互联网 发布:淘宝什么时候优惠最大 编辑:程序博客网 时间:2024/06/07 06:09
在OpenGL ES 3程序中,Shader和Program是两个重要的概念,至少需要创建一个顶点Shader对象、一个片段Shader对象和一个Program对象,才能用着色器进行渲染,理解Shader对象和Program对象的最佳方式是将它们比作C语言的编译器和链接程序,从Shader的创建到Program的链接共六个基本步骤,创建Shader、加载Shader源码、编译Shader、创建Program、绑定Program与Shader、链接Program,下面介绍Shader和Program的创建方法及相关概念。
1、创建Shader
GLuint glCreateShader(GLenum shaderType);
创建着色器使用glCreateShader,成功时返回一个空的着色器对象句柄,一个正整数,失败时返回0,shaderType有误时产生错误GL_INVALID_ENUM,shaderType可以是:
GL_VERTEX_SHADER // vertexGL_FRAGMENT_SHADER // fragmentGL_COMPUTE_SHADER // from GL ES 3.1GL_TESS_CONTROL_SHADER // from GL ES 3.2GL_TESS_EVALUATION_SHADER // from GL ES 3.2GL_GEOMETRY_SHADER // from GL ES 3.2
与glCreateShader对应的有个glDeleteShader,用于删除着色器对象,包括内存释放和shader句柄释放,如果这个着色器对象attach到了一个程序对象,那么这个着色器对象将作个删除标记而不会立即删除,等到不再attach任何程序对象时再删除,参数shader无效时产生错误GL_INVALID_VALUE。
void glDeleteShader(GLuint shader);
对一个着色器对象来说,判断其是否有删除标记,可以调用如下函数:
glGetShaderiv(shader, GL_DELETE_STATUS, params);
2、加载Shader源码
void glShaderSource(GLuint shader, GLsizei count, const GLchar **string, const GLint *length);
创建了着色器对象之后,通过glShaderSource加载着色器源码,替换原有的着色器源码,shader为着色器对象句柄,count为string数组和length数组的大小,string为指针数组,保存了count个着色器源码,length为整型数组,指定了每个着色器源码的长度,如果length为NULL,则认为着色器源码字符串以null结尾,length为正数时只加载指定长度的着色器源码字符串,如果lengh不合适,可能导致错误产生,length为负数时也认为着色器源码字符串以null结尾。加载失败时可能的错误为GL_INVALID_VALUE、GL_INVALID_OPERATION。glShaderSource拷贝了对应的着色器源码,所以之后就可以立即释放相关的内存了。
3、编译Shader
void glCompileShader(GLuint shader);
成功加载着色器源码之后,通过glCompileShader编译着色器对象,处理上一步加载的着色器源码字符串,shader为前面创建的着色器对象句柄,失败时可能的错误为GL_INVALID_VALUE、GL_INVALID_OPERATION。
当OpenGL ES编译和绑定着色器时,着色器代码通常解析为某种中间表现形式,这和大部分编译语言相同,如抽象语法树,编译器必须将抽象形式转换为硬件的机器指令,理想状态下,这个编译器还应该进行大量的优化,如无用代码删除、常量传播等,进行这些工作需要付出代价,主要是CPU时间和内存。OpenGL ES 3.0实现必须支持在线着色器编译,用glGetBooleanv检索的GL_SHADER_COMPILER值必须是GL_TRUE,可以指定着色器使用glShaderSource,还可以尝试缓解着色器编译对资源的影响,也就是说,一旦完成了应用程序中着色器的编译,就可以调用如下glReleaseShaderCompiler,这个函数提示OpenGL ES实现已经完成了着色器编译的工作,可以释放它的资源了。需要注意的是,这个函数只是一个提示,如果决定用glCompileShader编译更多的着色器,那么OpenGL ES实现需要重新为编译器分配资源。
void glReleaseShaderCompiler(void);
4、检查Shader状态
void glGetShaderiv(GLuint shader, GLenum pname, GLint *params);void glGetShaderInfoLog(GLuint shader, GLsizei maxLength, GLsizei *length, GLchar *infoLog);
glGetShaderiv用于检查Shader状态,shader为着色器对象句柄,pname为待检查的Shader状态,params返回检查的Shader状态,失败时可能的错误为GL_INVALID_VALUE、GL_INVALID_OPERATION、GL_INVALID_ENUM,Shader状态即函数参数pname取值如下:
GL_SHADER_TYPE:着色器类型,如GL_VERTEX_SHADER等。GL_DELETE_STATUS:着色器是否有删除标记。GL_COMPILE_STATUS:着色器是否编译成功。GL_INFO_LOG_LENGTH:着色器通知日志长度,包括null结尾的字符。GL_SHADER_SOURCE_LENGTH:着色器源码字符串长度,包括null结尾的字符。
glGetShaderInfoLog用于获取Shader通知日志的字符串表示,shader为着色器对象句柄,maxLength为获取通知日志的buffer的最大长度,length返回获取的通知日志的长度,不包括null结尾的字符,可以为NULL,infoLog返回通知日志,失败时可能的错误为GL_INVALID_VALUE、GL_INVALID_OPERATION。
另外,还有几个有用的函数,glIsShader判断是否为着色器对象,glGetShaderSource获取着色器源码的字符串表示。
GLboolean glIsShader(GLuint shader);void glGetShaderSource(GLuint shader, GLsizei bufSize, GLsizei *length, GLchar *source);
5、创建Program
GLuint glCreateProgram(void);
创建程序对象使用glCreateProgram,glCreateProgram创建一个空的程序对象,一个非零值,失败时返回0。与glCreateProgram对应的有个glDeleteProgram,用于删除程序对象,包括内存释放和program句柄释放,如果这个程序对象在使用中,那么将作个删除标记而不会立即删除,等到不再使用时再删除,其中的着色器对象将自动detach,着色器对象有删除标记就删除,否则不删除。参数program无效时产生错误GL_INVALID_VALUE。
void glDeleteProgram(GLuint program);
对一个程序对象来说,判断其是否有删除标记,可以调用如下函数:
void glGetProgramiv(program, GL_DELETE_STATUS, params);
6、绑定Program与Shader
void glAttachShader(GLuint program, GLuint shader);
创建了程序对象之后,通过glAttachShader将其绑定到一个着色器对象,program和shader为对应的程序对象和着色器对象,同一种类型的着色器对象只能绑定一次,失败时可能的错误为GL_INVALID_VALUE、GL_INVALID_OPERATION。与glAttachShader对应的有个glDetachShader,用于解除绑定,program和shader为对应的程序对象和着色器对象,如果着色器对象有删除标记且没有被其它地方使用将删除,失败时可能的错误为GL_INVALID_VALUE、GL_INVALID_OPERATION。
void glDetachShader(GLuint program, GLuint shader);
7、链接Program
void glLinkProgram(GLuint program);
程序对象与着色器对象attach之后,使用glLinkProgram链接程序对象,这是使用程序对象前的最后一步,参数program为需要链接的程序对象句柄,失败时可能的错误为GL_INVALID_VALUE、GL_INVALID_OPERATION。程序对象链接成功之后,所有活动的用户定义的uniform变量都被初始化为0而且被设置一个位置,这个位置使用glGetUniformLocation获取,所有活动的命名uniform块中的uniform变量都被设置一个偏移量,包括数组和矩阵,所有活动的用户定义的attribute变量没有绑定到一般的顶点属性索引时将在此时绑定一个。链接操作负责生成最终的可执行程序,链接阶段是生成在硬件上运行的最终指令的时候,链接程序将检查各种对象的数量,确保成功链接,例如,链接程序将确保顶点着色器写入片段着色器使用的所有顶点着色器输出变量并用相同的类型声明,确保任何在顶点和片段着色器中都声明的统一变量和统一变量缓冲区的类型相符,确保最终的程序符合具体实现的限制,如属性、统一变量、输入输出着色器变量的数量等。链接程序对象可能会失败,常见原因可参照https://www.khronos.org/registry/OpenGL-Refpages/es3/html/glLinkProgram.xhtml。
8、检查Program状态
void glGetProgramiv(GLuint program, GLenum pname, GLint *params);void glGetShaderInfoLog(GLuint shader, GLsizei maxLength, GLsizei *length, GLchar *infoLog);
检查Program状态的方法类似于上面介绍的检查Shader状态的方法,只是使用的函数不同,分别为glGetProgramiv和glGetShaderInfoLog,glGetProgramiv的参数pname支持的状态包括(详细用法可参照https://www.khronos.org/registry/OpenGL-Refpages/es3/html/glGetProgramiv.xhtml):
GL_ACTIVE_ATOMIC_COUNTER_BUFFERSGL_ACTIVE_ATTRIBUTESGL_ACTIVE_ATTRIBUTE_MAX_LENGTHGL_ACTIVE_UNIFORM_BLOCKSGL_ACTIVE_UNIFORM_BLOCK_MAX_NAME_LENGTHGL_ACTIVE_UNIFORMSGL_ACTIVE_UNIFORM_MAX_LENGTHGL_ATTACHED_SHADERSGL_COMPUTE_WORK_GROUP_SIZEGL_DELETE_STATUSGL_GEOMETRY_LINKED_INPUT_TYPEGL_GEOMETRY_LINKED_OUTPUT_TYPEGL_GEOMETRY_LINKED_VERTICES_OUTGL_GEOMETRY_SHADER_INVOCATIONSGL_INFO_LOG_LENGTHGL_LINK_STATUSGL_PROGRAM_BINARY_RETRIEVABLE_HINTGL_PROGRAM_SEPARABLEGL_TESS_CONTROL_OUTPUT_VERTICESGL_TESS_GEN_MODEGL_TESS_GEN_POINT_MODEGL_TESS_GEN_SPACINGGL_TESS_GEN_VERTEX_ORDERGL_TRANSFORM_FEEDBACK_BUFFER_MODEGL_TRANSFORM_FEEDBACK_VARYINGSGL_TRANSFORM_FEEDBACK_VARYING_MAX_LENGTHGL_VALIDATE_STATUS
下面是一个创建Shader和Program的代码例子:
// esShader.cGLuint esLoadShader ( GLenum type, const char *shaderSrc ){ GLuint shader; GLint compiled; // Create the shader object shader = glCreateShader ( type ); if ( shader == 0 ) { return 0; } // Load the shader source glShaderSource ( shader, 1, &shaderSrc, NULL ); // Compile the shader glCompileShader ( shader ); // Check the compile status glGetShaderiv ( shader, GL_COMPILE_STATUS, &compiled ); if ( !compiled ) { GLint infoLen = 0; glGetShaderiv ( shader, GL_INFO_LOG_LENGTH, &infoLen ); if ( infoLen > 1 ) { char *infoLog = malloc ( sizeof ( char ) * infoLen ); glGetShaderInfoLog ( shader, infoLen, NULL, infoLog ); esLogMessage ( "Error compiling shader:\n%s\n", infoLog ); free ( infoLog ); } glDeleteShader ( shader ); return 0; } return shader;}GLuint ESUTIL_API esLoadProgram ( const char *vertShaderSrc, const char *fragShaderSrc ){ GLuint vertexShader; GLuint fragmentShader; GLuint programObject; GLint linked; // Load the vertex/fragment shaders vertexShader = esLoadShader ( GL_VERTEX_SHADER, vertShaderSrc ); if ( vertexShader == 0 ) { return 0; } fragmentShader = esLoadShader ( GL_FRAGMENT_SHADER, fragShaderSrc ); if ( fragmentShader == 0 ) { glDeleteShader ( vertexShader ); return 0; } // Create the program object programObject = glCreateProgram ( ); if ( programObject == 0 ) { return 0; } glAttachShader ( programObject, vertexShader ); glAttachShader ( programObject, fragmentShader ); // Link the program glLinkProgram ( programObject ); // Check the link status glGetProgramiv ( programObject, GL_LINK_STATUS, &linked ); if ( !linked ) { GLint infoLen = 0; glGetProgramiv ( programObject, GL_INFO_LOG_LENGTH, &infoLen ); if ( infoLen > 1 ) { char *infoLog = malloc ( sizeof ( char ) * infoLen ); glGetProgramInfoLog ( programObject, infoLen, NULL, infoLog ); esLogMessage ( "Error linking program:\n%s\n", infoLog ); free ( infoLog ); } glDeleteProgram ( programObject ); return 0; } // Free up no longer needed shader resources glDeleteShader ( vertexShader ); glDeleteShader ( fragmentShader ); return programObject;}
9、使用着色器程序
void glUseProgram(GLuint program);
最后,成功链接程序对象之后,就可以使用这个着色器程序了,后面在添加一些顶点数据就可以进行图元绘制了。glUseProgram把这个program程序对象安装到当前渲染状态的一部分,失败时可能的错误为GL_INVALID_VALUE、GL_INVALID_OPERATION。
另外,还有几个有用的函数,glIsProgram判断是否为程序对象,glGetIntegerv(GL_CURRENT_PAROGRAM, data)获取当前活动的程序对象,glGetAttachedShaders获取attach到一个程序对象上的着色器对象,glValidateProgram用于验证程序。
GLboolean glIsProgram( GLuint program);void glGetIntegerv(GLenum pname, GLint * data); // glGetXxxvoid glGetAttachedShaders(GLuint program,GLsizei maxCount,GLsizei *count,GLuint *shaders);void glValidateProgram(GLuint program);
10、uniform变量查询与设置
程序对象链接成功之后,就可以在对象上进行许多查询,如活动的uniform变量,是着色语言中的只读变量,uniform变量分为两种类型,默认uniform变量和uniform变量块,如下所示:
// defaultuniform mat4 matViewProj;uniform mat3 matNormal;uniform mat3 matTexGen;// blockuniform TransformBlock{ mat4 matViewProj; mat3 matNormal; mat3 matTexGen;};
如果uniform变量在顶点着色器和片段着色器中均有声明,则声明的类型必须相同,且在两个着色器中的值也需相同。查找活动uniform变量之前,可以使用GL_ACTIVE_UNIFORMS参数调用glGetProgramiv函数,获得活动uniform变量的个数,包括前面提到的两种类型的uniform变量和内建uniform变量,还可以使用GL_ACTIVE_UNIFORM_MAX_LENGTH获得uniform变量名的最大长度,然后就可以使用如下几个函数查询活动uniform变量了。
void glGetActiveUniform(GLuint program, GLuint index, GLsizei bufSize, GLsizei *length, GLint *size, GLenum *type, GLchar *name);
glGetActiveUniform获取程序对象program中位置index的活动uniform变量的信息,bufSize指定name的最大长度,length返回name的实际长度,可以为NULL,uniform变量非数组时size为1,uniform变量是数组时size为使用的最大数组元素,type返回uniform变量的类型,name返回uniform变量名,失败时可能的错误为GL_INVALID_VALUE、GL_INVALID_OPERATION。type返回的uniform变量类型与着色语言中的变量类型有个对应关系,如GL_FLOAT对应于float,详细对应关系可参照https://www.khronos.org/registry/OpenGL-Refpages/es3/html/glGetActiveUniform.xhtml。
void glGetActiveUniformsiv(GLuint program, GLsizei uniformCount, const GLuint *uniformIndices, GLenum pname, GLint *params);
glGetActiveUniformsiv获取程序对象program中uniformCount个uniform活动变量的类型为pname的信息,uniformIndices是个长度为uniformCount的保存了uniform变量位置的数组,params返回对应的结果,也是个长度为uniformCount的数组,失败时可能的错误为GL_INVALID_VALUE、GL_INVALID_OPERATION、GL_INVALID_ENUM。pname类型如下:
GL_UNIFORM_TYPEGL_UNIFORM_SIZEGL_UNIFORM_NAME_LENGTHGL_UNIFORM_BLOCK_INDEXGL_UNIFORM_OFFSETGL_UNIFORM_ARRAY_STRIDEGL_UNIFORM_MATRIX_STRIDEGL_UNIFORM_IS_ROW_MAJOR
知道了活动uniform变量名之后,通过glGetUniformLocation可以获取其位置,失败时返回-1,可能的错误为GL_INVALID_VALUE、GL_INVALID_OPERATION。
GLint glGetUniformLocation(GLuint program, const GLchar *name);
然后,通过uniform变量的位置就可以使用glGetUniformXxx查询其值或者使用glUniformXxx改变其值了,失败时可能的错误为GL_INVALID_VALUE、GL_INVALID_OPERATION,下面是这两个函数的某一个版本。
void glGetUniformfv(GLuint program, GLint location, GLfloat *params);void glUniform1f(GLint location, GLfloat v0);
下面是示例代码:
GLint maxUniformLen;GLint numUniforms;char *uniformName;GLint index;glGetProgramiv(progObj, GL_ACTIVE_UNIFORMS, &numUniforms);glGetProgramiv(progObj, GL_ACTIVE_UNIFORM_MAX_LENGTH, &maxUniformLen);uniformName = malloc(sizeof(char) * maxUniformLen);for (index = 0; index < numUniforms; ++i) { Glint len; GLint size; GLenum type; GLint location; // get the uniform info glGetAcitveUniform(progObj, index, maxUniformLen, &len, &size, &type, uniformName); // get the uniform location location = glGetUniformLocation(progObj, uniformName);}
11、uniform变量缓冲区对象
可以使用缓冲区对象存储uniform变量数据,从而在程序中的着色器之间甚至程序之间共享uniform变量,这种缓冲区对象称作uniform变量缓冲区对象,使用uniform变量缓冲区对象,可以在更新大的uniform变量块时降低API开销,而且增加了uniform变量的可用存储,可以不受默认uniform变量块大小的限制,更新uniform变量缓冲区对象时可以使用glBufferData创建和初始化一个缓冲区对象、glBufferSubData更新缓冲区对象、glMapBufferRange映射缓冲区对象的一个区域、glUnmapBuffer使用缓冲区对象前解除映射的数据等。
与统一变量位置值用于引用统一变量类似,统一变量块索引用于引用统一变量块,可以用glGetUniformBlockIndex获取命名统一变量块的索引。从统一变量块索引,可以用glGetActiveBlockName和glGetActiveUniformBlockiv确定活动统一变量块的细节,获取统一变量块的许多属性,命名统一变量块的索引必须小于GL_ACTIVE_UNIFORM_BLOCKS的值。
GLuint glGetUniformBlockIndex(GLuint program, const GLchar *uniformBlockName);void glGetActiveUniformBlockName(GLuint program, GLuint uniformBlockIndex, GLsizei bufSize, GLsizei *length, GLchar *uniformBlockName);void glGetActiveUniformBlockiv(GLuint program, GLuint uniformBlockIndex, GLenum pname, GLint *params);
glGetActiveUniformBlockiv的pname可以是:
GL_UNIFORM_BLOCK_BINDINGGL_UNIFORM_BLOCK_DATA_SIZEGL_UNIFORM_BLOCK_NAME_LENGTHGL_UNIFORM_BLOCK_ACTIVE_UNIFORMSGL_UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICESGL_UNIFORM_BLOCK_REFERENCED_BY_VERTEX_SHADERGL_UNIFORM_BLOCK_REFERENCED_BY_FRAGMENT_SHADER
一旦有了统一变量块索引,就可以调用glUniformBlockBinding,将该索引和程序中的统一变量块绑定点关联,索引不能超过GL_MAX_UNIFORM_BUFFER_BINDINGS的值。最后,可以用glBindBufferRange或者glBindBufferBase将统一变量缓冲区对象绑定到GL_UNIFORM_BUFFER目标和程序中的统一变量块绑定点。
void glUniformBlockBinding(GLuint program, GLuint uniformBlockIndex, GLuint uniformBlockBinding);void glBindBufferRange(GLenum target, GLuint index, GLuint buffer, GLintptr offset, GLsizeipt rsize);void glBindBufferBase(GLenum target, GLuint index, GLuint buffer);
上面函数的target如下,需要注意的是不同类型的target其offset是不同的,可以用glGetXxx查询其限制。
GL_ATOMIC_COUNTER_BUFFER // from GL ES 3.1 (4)GL_SHADER_STORAGE_BUFFER // from GL ES 3.1 (GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT)GL_TRANSFORM_FEEDBACK_BUFFER // (4)GL_UNIFORM_BUFFER // (GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT)
下面是示例代码:
GLuint blockId, bufferId;GLint blockSize;GLuint bindingPoint = 1;GLfloat lightData[] ={ // lightDirection (padded to vec4 based on std140 rule) 1.0f, 0.0f, 0.0f, 0.0f, // lightPosition 0.0f, 0.0f, 0.0f, 1.0f}// retrive the uniform block indexblockId = glGetUniformBlockIndex(program, "LightBlock");// associate the uniform block index with a binding pointglUniformBlockBinding(program, blockId, bindingPoint);// get the size of lightData// alternatively we can calculate it using sizeof(lightData) in this exampleglGetAcitveUniformBlockiv(program, blockId, GL_UNIFORM_BLOCK_DATA_SIZE, &blockSize);// create and fill a buffer objectglGenBuffers(1, &bufferId);glBindBuffer(GL_UNIFORM_BUFFER, bufferId);glBufferData(GL_UNIFORM_BUFFER, blockSize, lightData, GL_DYNAMIC_DRAW);// bind the buffer object to the uniform block bindint pointglBindBufferBase(GL_UNIFORM_BUFFER, bindingPoint, buffer);
12、属性查询
上面介绍了uniform变量的查询和设置方法,我们还需要使用程序对象设置顶点属性,对顶点属性的查询和统一变量查询非常相似,可以用GL_ACTIVE_ATTRIBUTES查询找到活动属性列表,GL_ACTIVE_ATTRIBUTE_MAX_LENGTH查询活动属性名字的最大长度,可以用如下glGetActiveAttrib等函数找到某个属性的特性、设置顶点数组以加载顶点属性值等。glGetXxx(GL_MAX_VERTEX_ATTRIBS)还可以查询最大的顶点属性。
void glGetActiveAttrib(GLuint program, GLuint index, GLsizei bufSize, GLsizei *length, GLint *size, GLenum *type, GLchar *name);
13、着色程序二进制码
着色程序二进制码是完全编译和链接的着色程序的二进制表示,可以保存到文件供以后使用,避免了在线编译的代价,也不用在实现中分发着色器源代码,涉及如下两个函数:
void glGetProgramBinary(GLuint program, GLsizei bufsize, GLsizei *length, GLenum *binaryFormat, void *binary);void glProgramBinary(GLuint program, GLenum binaryFormat, const void *binary, GLsizei length);
glGetProgramBinary用于获取程序二进制码,program为程序对象句柄,bufsize为可以写入binary的最大字节数,不能小于GL_PROGRAM_BINARY_LENGTH(用glGetProgramiv获取),否则产生错误GL_INVALID_OPERATION,length为二进制数据的字节数,可以为NULL,binaryFormat为供应商专用二进制格式标志,binary为着色器编译器生成的二进制数据指针。glProgramBinary用于将程序二进码读回OpenGL ES实现,program为程序对象句柄,binaryFormat为供应商专用二进制格式标志,binary为着色器编译器生成的二进制数据指针,length为二进制数据的字节数,失败时可能的错误为GL_INVALID_OPERATION、GL_INVALID_ENUM,相关函数涉及glGetXxx的参数GL_NUM_PROGRAM_BINARY_FORMATS和GL_PROGRAM_BINARY_FORMATS。
- 【OpenGL ES】着色器Shader与程序Program
- [OpenGL]OpenGL ES 着色器(shader)介绍
- OpenGL ES 着色器(shader)介绍
- OpenGL ES 着色器(shader)介绍
- OpenGL ES 着色器(shader)介绍
- OpenGL ES 着色器(shader)介绍
- OpenGL ES 2.0 新增Shader着色器
- OpenGL ES 着色器(shader)介绍
- OpenGL ES 着色器(shader)介绍(转)
- OpenGL ES 着色器(shader)介绍
- opengl es 3:程序使用着色器
- OpenGL ES 2.0——顶点着色器Vertex Shader
- OpenGL ES 2.0——片段着色器Fragment Shader
- OpenGL ES 2.0——顶点着色器Vertex Shader
- [OpenGL ES 02]OpenGL ES渲染管线与着色器
- [OpenGL ES 02]OpenGL ES渲染管线与着色器
- [OpenGL ES 02]OpenGL ES渲染管线与着色器
- [OpenGL ES 02]OpenGL ES渲染管线与着色器
- 网络编程
- Kerberos认证协议
- SonicOperator之Abstract
- cdn是什么
- 程序员要拥抱变化,聊聊Android即将支持的Java 8
- 【OpenGL ES】着色器Shader与程序Program
- A. Find Amir
- Python列表
- Solr6.5.1搭建
- 3.第一章难点:假设空间 版本空间
- C#:运算符重载
- android 设置toolbar透明度所有的页面都会变透明
- 0507 Flex弹性布局
- poj2287Tian Ji -- The Horse Racing