zoukankan      html  css  js  c++  java
  • OpenGL核心技术之混合技术

    笔者介绍:姜雪伟IT公司技术合伙人,IT高级讲师,CSDN社区专家,特邀编辑,畅销书作者。国家专利发明人;已出版书籍:《手把手教你架构3D游戏引擎》电子工业出版社和《Unity3D实战核心技术具体解释》电子工业出版社等。

    CSDN视频网址:http://edu.csdn.net/lecturer/144

    本章给读者介绍关于混合技术的实现。混合在游戏中常常使用,它在引擎中的实现主要是分为三种:透明,半透明,次序无关透明度。本篇博文主要是环绕它们进行。

    在OpenGL中。物体透明技术通常被叫做混合(Blending)。一个物体的透明度。被定义为它的颜色的alpha值。alpha颜色值是一个颜色
    向量的第四个元素。

    美术在制作的游戏图片颜色,主要是由rgba四位组成的。颜色的最后一位就是我们说的alpha通道,它主要是决定材质
    的透明度的。
    先说透明的材质处理,做过3D游戏的开发人员都比較熟悉。在3D场景编辑器中常常须要在地面上刷一些草,这些草的图片制作是带有
    alpha通道的,效果例如以下所看到的:


    程序的处理方式就是把背景去掉,把草显示出来,所以。当向场景中加入像这样的纹理时,我们不希望看到一个方块图像,
    而是仅仅显示实际的纹理像素。剩下的部分能够被看穿。我们要忽略(丢弃)纹理透明部分的像素,不必将这些片段储存到颜色
    缓冲中。

    接下来要做的事情就是载入带有Alpha通道的纹理图片,在这里我们使用了SOIL库, SOIL 是一个用于向OpenGL中
    载入纹理的小型C语言库。下载地址:http://www.lonesock.net/soil.html。SOIL库提供的载入函数例如以下:

    unsigned char * image = SOIL_load_image(path, &width, &height, 0, SOIL_LOAD_RGBA);
    不要忘记还要改变OpenGL生成的纹理:

    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, image);

    保证你在片段着色器中获取了纹理的全部4个颜色元素,而不仅仅是RGB元素:

    void main()
    {
        // color = vec4(vec3(texture(texture1, TexCoords)), 1.0);
        color = texture(texture1, TexCoords);
    }

    透明材质就载入完毕了,接下来就是对草进行摆放了。代码段例如以下所看到的:

    vector<glm::vec3> vegetation;
    vegetation.push_back(glm::vec3(-1.5f,  0.0f, -0.48f));
    vegetation.push_back(glm::vec3( 1.5f,  0.0f,  0.51f));
    vegetation.push_back(glm::vec3( 0.0f,  0.0f,  0.7f));
    vegetation.push_back(glm::vec3(-0.3f,  0.0f, -2.3f));
    vegetation.push_back(glm::vec3( 0.5f,  0.0f, -0.6f));

    一个单独的四边形被贴上草的纹理。这并不能完美的表现出真实的草,可是比起载入复杂的模型还是要高效非常多。利用一些小技巧,


    比方在同一个地方加入多个不同朝向的草,还是能获得比較好的效果的。


    由于草纹理被加入到四边形物体上。我们须要再次创建还有一个VAO。向里面填充VBO,以及设置合理的顶点属性指针。


    在我们绘制完地面和两个立方体后,我们就来绘制草叶:

    glBindVertexArray(vegetationVAO);
    glBindTexture(GL_TEXTURE_2D, grassTexture);  
    for(GLuint i = 0; i < vegetation.size(); i++)
    {
        model = glm::mat4();
        model = glm::translate(model, vegetation[i]);
        glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
        glDrawArrays(GL_TRIANGLES, 0, 6);
    }  
    glBindVertexArray(0);

    执行程序得到的效果例如以下所看到的:


    出现这样的情况是由于OpenGL默认是不知道怎样处理alpha值的。不知道何时忽略(丢弃)它们。我们不得不手动做这件事。
    幸运的是这非常easy,感谢着色器。GLSL为我们提供了discard命令,它保证了片段不会被进一步处理,这样就不会进入颜色缓冲。
    有了这个命令我们就能够在片段着色器中检查一个片段是否有在一定的阈限下的alpha值,假设有,那么丢弃这个片段,就好像
    它不存在一样:

    #version 330 core
    in vec2 TexCoords;
    
    out vec4 color;
    
    uniform sampler2D texture1;
    
    void main()
    {
        vec4 texColor = texture(texture1, TexCoords);
        if(texColor.a < 0.1)
            discard;
        color = texColor;
    }

    在这儿我们检查被採样纹理颜色包括着一个低于0.1这个阈限的alpha值,假设有,就丢弃这个片段。

    这个片段着色器能够保证我们仅仅渲染哪些不是全然透明的片段。

    如今我们来看看效果:



    以下把Shader脚本的顶点着色器给读者展演示样例如以下:

    #version 330 core
    layout (location = 0) in vec3 position;
    layout (location = 1) in vec2 texCoords;
    
    out vec2 TexCoords;
    
    uniform mat4 model;
    uniform mat4 view;
    uniform mat4 projection;
    
    void main()
    {
        gl_Position = projection * view * model * vec4(position, 1.0f);
        TexCoords = texCoords;
    }

    片段着色器代码例如以下所看到的:

    #version 330 core
    in vec2 TexCoords;
    
    out vec4 color;
    
    uniform sampler2D texture1;
    
    void main()
    {             
        vec4 texColor = texture(texture1, TexCoords);
        if(texColor.a < 0.1)
            discard;
        color = texColor;
    }

    其次介绍半透明处理。以上Shader完毕了透明材质的渲染,事实上这样的方法在材质渲染中常常使用。能够把不须要的颜色放弃掉,这样的方式不适合渲染半透明的图片。也没实用到Blend混合模式。为了渲染出不同的透明度级别,须要开启混合(Blending),开启混合功能函数例如以下:

    glEnable(GL_BLEND);
    开启混合后,我们还须要告诉OpenGL它该怎样混合。

    OpenGL以以下的方程进行混合:


    • :源颜色向量。这是来自纹理的本来的颜色向量。

    • :目标颜色向量。这是储存在颜色缓冲中当前位置的颜色向量。
    • :源因子。设置了对源颜色的alpha值影响。

    • :目标因子。设置了对目标颜色的alpha影响。
    片段着色器执行完毕而且全部的測试都通过以后,混合方程才干自由执行片段的颜色输出,当前它在颜色缓冲中(前面片段的颜色在当前片段之前储存)。源和目标颜色会自己主动被OpenGL设置,而源和目标因子能够让我们自由设置。

    我们来看一个简单的样例:


    我们有两个方块,我们希望在红色方块上绘制绿色方块。红色方块会成为目标颜色(它会先进入颜色缓冲),我们将在红色方块上绘制绿色方块。




    那么问题来了:我们怎样来设置因子呢?我们起码要把绿色方块乘以它的alpha值,所以我们打算把FsrcFsrc设置为源颜色向量的alpha值:0.6。接着。让目标方块的浓度等于剩下的alpha值。

    假设终于的颜色中绿色方块的浓度为60%,我们就把红色的浓度设为40%(1.0 – 0.6)。所以我们把Fdestination 设置为1减去源颜色向量的alpha值。方程将变成:


    终于方块结合部分包括了60%的绿色和40%的红色,得到一种脏兮兮的颜色:


    最后的颜色被储存到颜色缓冲中,代替先前的颜色。

    这个方法不错,但我们怎样告诉OpenGL来使用这样的因子呢?恰好有一个叫做glBlendFunc的函数。

    void glBlendFunc(GLenum sfactor, GLenum dfactor)接收两个參数,来设置源(source)和目标(destination)因子。OpenGL为

    我们定义了非常多选项。我们把最常常使用的列在以下。注意,颜色常数向量能够用glBlendColor函数分开来设置。


    为从两个方块获得混合结果。我们打算把源颜色的alpha给源因子。1alpha给目标因子,调整到glBlendFunc

    后就像这样:

    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    也能够为RGB和alpha通道各自设置不同的选项,使用glBlendFuncSeperate

    glBlendFuncSeperate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA,GL_ONE, GL_ZERO);

    这个方程就像我们之前设置的那样,设置了RGB元素,可是仅仅让终于的alpha元素被源alpha值影响到。

    OpenGL给了我们很多其它的自由,我们能够改变方程源和目标部分的操作符。

    如今,源和目标元素已经相加了。假设我们愿意的话。

    我们还能够把它们相减。

    void glBlendEquation(GLenum mode)同意我们设置这个操作,有3种可行的选项:

    • GL_FUNC_ADD:默认的,彼此元素相加:
    • GL_FUNC_SUBTRACT:彼此元素相减: 
    • GL_FUNC_REVERSE_SUBTRACT:彼此元素相减。但顺序相反:

    通常我们能够简单地省略glBlendEquation由于GL_FUNC_ADD在大多数时候就是我们想要的,可是假设你假设你真想尝试

    努力打破主流常规,其它的方程也许符合你的要求。如今我们知道OpenGL怎样处理混合,是时候把我们的知识运用起来了,

    我们来加入几个半透明窗子。首先,初始化时我们须要开启混合,设置合适和混合方程:

    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

    由于我们开启了混合,就不须要丢弃片段了,所以我们把片段着色器设置为原来的那个版本号:

    #version 330 core
    in vec2 TexCoords;
    
    out vec4 color;
    
    uniform sampler2D texture1;
    
    void main()
    {
        color = texture(texture1, TexCoords);
    }

    它依据alpha值,把当前片段的颜色和颜色缓冲中的颜色进行混合。

    由于窗子的玻璃部分的纹理是半透明的,我们应该能够

    透过玻璃看到整个场景。


    假设你细致看看,就会注意到有些不正确劲。前面的窗子透明部分堵塞了后面的。为什么会这样?

    原因是深度測试在与混合的一同工作时出现了点状况。

    当写入深度缓冲的时候,深度測试不关心片段是否有透明度,

    所以透明部分被写入深度缓冲。就和其它值没什么差别。结果是整个四边形的窗子被检查时都忽视了透明度。即便透明部

    分应该显示出后面的窗子,深度缓冲还是丢弃了它们。

    所以我们不能简简单单地去渲染窗子,我们期待着深度缓冲为我们解决这全部问题;这也正是混合之处代码不怎么好

    看的原因。

    为保证前面窗子显示了它后面的窗子,我们必须首先绘制后面的窗子。

    这意味着我们必须手工调整窗子的顺序。

    从远到近地逐个渲染。

    这里要注意:对于全透明物体,比方草叶,我们选择简单的丢弃透明像素而不是混合,这样就降低了令我们头疼的问题(没有深度測试题)。

    以下介绍怎样依照顺序渲染物体。要让混合在多物体上有效。我们必须先绘制最远的物体。最后绘制近期的物体。

    普通的无混合物体仍然能够使用深度缓冲正常绘制,所以不必给它们排序。

    我们一定要保证它们在透明物体前绘制好。

    当无透明度物体和透明物体一起绘制的时候,通常要遵循以下原则:

    先绘制全部不透明物体。

    为全部透明物体排序。

    按顺序绘制透明物体。 一种排序透明物体的方式是。获取一个物体

    到观察者透视图的距离。这能够通过获取摄像机的位置向量和物体的位置向量来得到。接着我们就能够把它和对应的位置

    向量一起储存到一个map数据结构(STL库)中。

    map会自己主动基于它的键排序它的值,所以当我们把它们的距离作为键添

    加到全部位置中后,它们就自己主动依照距离值排序了:

    std::map<float, glm::vec3> sorted;
    for (GLuint i = 0; i < windows.size(); i++) // windows contains all window positions
    {
        GLfloat distance = glm::length(camera.Position - windows[i]);
        sorted[distance] = windows[i];
    }

    最后产生了一个容器对象,基于它们距离从低到高储存了每一个窗子的位置。

    随后当渲染的时候,我们逆序获取到每一个map的值(从远到近),然后以正确的绘制对应的窗子:

    for(std::map<float,glm::vec3>::reverse_iterator it = sorted.rbegin(); it != sorted.rend(); ++it)
    {
        model = glm::mat4();
        model = glm::translate(model, it->second);
        glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
        glDrawArrays(GL_TRIANGLES, 0, 6);
    }

    我们从map得来一个逆序的迭代器,迭代出每一个逆序的条目,然后把每一个窗子的四边形平移到对应的位置。这个相对

    简单的方法对透明物体进行了排序,修正了前面的问题。如今场景看起来像这样:


    尽管这个依照它们的距离对物体进行排序的方法在这个特定的场景中能够良好工作,但它不能进行旋转、缩放或者

    进行其它的变换,奇怪形状的物体须要一种不同的方式。而不能简单的使用位置向量。

    在场景中排序物体是个有难度的技术,它非常大程度上取决于你场景的类型,更不必说会耗费额外的处理能力了。

    完美地渲染带有透明和不透明的物体的场景并不那么easy。

    接下介绍技术次序无关透明度。为了解决上述提到的问题,我们使用更高级的更高级的技术更高级的技术次序无关

    透明度。

    以下介绍该技术的实现,我们的渲染主要分两步:

    第一步:渲染填充链表;

    第二步:渲染排序+blend;

      先看第一步。我们使用片段着色器来填充链表:

    #version 420 core
    
    layout (early_fragment_tests) in;
    
    layout (binding = 0, r32ui) uniform uimage2D head_pointer_image;
    layout (binding = 1, rgba32ui) uniform writeonly uimageBuffer list_buffer;
    
    layout (binding = 0, offset = 0) uniform atomic_uint list_counter;
    
    layout (location = 0) out vec4 color;
    
    in vec4 surface_color;
    
    uniform vec3 light_position = vec3(40.0, 20.0, 100.0);
    
    void main(void)
    {
        uint index;
        uint old_head;
        uvec4 item;
    
        index = atomicCounterIncrement(list_counter);
    
        old_head = imageAtomicExchange(head_pointer_image, ivec2(gl_FragCoord.xy), uint(index));
    
        item.x = old_head;
        item.y = packUnorm4x8(surface_color);
        item.z = floatBitsToUint(gl_FragCoord.z);
    	item.w = 255 / 4;
    
        imageStore(list_buffer, int(index), item);
    
        //color = surface_color;
    	discard;
    }

    同一时候把顶点着色器代码也给读者展示一下:

    #version 330
    
    layout (location = 0) in vec3 position;
    layout (location = 1) in vec3 normal;
    
    uniform mat4 model_matrix;
    uniform mat4 view_matrix;
    uniform mat4 projection_matrix;
    
    uniform float minAlpha = 0.5f;
    
    out vec4 surface_color;
    
    void main(void)
    {
        vec3 color = normal;
        if (color.r < 0) { color.r = -color.r; }
        if (color.g < 0) { color.g = -color.g; }
        if (color.b < 0) { color.b = -color.b; }
    	vec3 normalized = normalize(color);
    	float variance = (normalized.r - normalized.g) * (normalized.r - normalized.g);
    	variance += (normalized.g - normalized.b) * (normalized.g - normalized.b);
    	variance += (normalized.b - normalized.r) * (normalized.b - normalized.r);
    	variance = variance / 2.0f;// range from 0.0f - 1.0f
    	float a = (0.75f - minAlpha) * (1.0f - variance) + minAlpha;
        surface_color = vec4(normalized, a);
    
        gl_Position = projection_matrix * view_matrix * model_matrix * vec4(position, 1.0f);
    }

    以下開始第二步骤的操作是渲染排序+blend,片段着色器代码例如以下所看到的:

    #version 420 core
    // The per-pixel image containing the head pointers
    layout (binding = 0, r32ui) uniform uimage2D head_pointer_image;
    // Buffer containing linked lists of fragments
    layout (binding = 1, rgba32ui) uniform uimageBuffer list_buffer;
    
    // This is the output color
    layout (location = 0) out vec4 color;
    
    // This is the maximum number of overlapping fragments allowed
    #define MAX_FRAGMENTS 40
    
    // Temporary array used for sorting fragments
    uvec4 fragment_list[MAX_FRAGMENTS];
    
    void main(void)
    {
        uint current_index;
        uint fragment_count = 0;
    
        current_index = imageLoad(head_pointer_image, ivec2(gl_FragCoord).xy).x;
    
        while (current_index != 0 && fragment_count < MAX_FRAGMENTS)
        {
            uvec4 fragment = imageLoad(list_buffer, int(current_index));
            fragment_list[fragment_count] = fragment;
            current_index = fragment.x;
            fragment_count++;
        }
    
    	if (fragment_count > 1)
    	{
    		for (uint i = 0; i < fragment_count - 1; i++)
    		{
    			uint p = i;
    			uint depth1 = (fragment_list[p].z);
    			for (uint j = i + 1; j < fragment_count; j++)
    			{
    				uint depth2 = (fragment_list[j].z);
    				if (depth1 < depth2)
    				{
    					p = j; depth1 = depth2;
    				}
    			}
    			if (p != i)
    			{
    				uvec4 tmp = fragment_list[p];
    				fragment_list[p] = fragment_list[i];
    				fragment_list[i] = tmp;
    			}
    		}
    	}
    
        vec4 final_color = vec4(0.0);
    
        for (uint i = 0; i < fragment_count; i++)
        {
            vec4 modulator = unpackUnorm4x8(fragment_list[i].y);
            //final_color = mix(final_color, modulator, modulator.a);
    		final_color = final_color * (1.0f - modulator.a) + modulator * modulator.a;
        }
    
        color = final_color;
        // color = vec4(float(fragment_count) / float(MAX_FRAGMENTS));
    }

    附带着顶点着色器代码例如以下所看到的:

    #version 420 core
    
    in vec3 position;
    
    uniform mat4 model_matrix;
    uniform mat4 view_matrix;
    uniform mat4 projection_matrix;
    
    void main(void)
    {
        gl_Position = projection_matrix * view_matrix * model_matrix * vec4(position, 1.0f);
        //gl_Position = vec4(position, 1.0f);
    }

    实现的效果图例如以下所看到的:


    假设没有使用次序无关透明技术实现的效果图例如以下所看到的:



    总结:

    关于在3D游戏中的混合技术主要用于处理透明。半透明以及在解决渲染次序问题使用的次序无关透明度技术,希望对读者有所帮助。。。。

    。。


  • 相关阅读:
    移动MM首届手机软件设计及创意大赛决赛取得圆满成功
    Windows Phone 7 EKB系列文章发布
    EVC3/4项目升级到Visual Studio项目的一些建议
    Windows Phone SDK 7.1 RTM 发布
    Howto: 创建Windows Phone 7自定义控件
    风云再起,7迹由你WP7技术沙龙上海站第二次活动
    Windows Phone Dev Notes如何使用ConnectionSettingsTask 来启动连接设置页面
    【OneNote Mobile】 如何处理便签内容的格式?
    《101 Windows Phone 7 Apps》读书笔记PASSWORDS & SECRETS
    3年MVP路,一颗感恩的心
  • 原文地址:https://www.cnblogs.com/llguanli/p/8384406.html
Copyright © 2011-2022 走看看