现代OpenGL的介绍 第4章:用Phong着色渲染动态3D场景

原文地址   现代OpenGL的介绍。第4章:用Phong着色渲染动态3D场景

更新2010年7月15日08:06:09 PDT
«第3章 | 目录

在这一点上,我们已经看到了OpenGL API最重要的核心部分,并获得了GLSL语言的体面品味。现在是开始锻炼OpenGL并实现一些图形效果的好时机,在我们走过时,引入了新的细微差别和OpenGL和GLSL的特殊功能。在接下来的几章中,我准备了一个新的演示程序,您可以从我的Github ch4-flag存储库获得。该标志的演示上呈现对一个简单的背景旗杆飘扬的旗帜:

平板的,看起来很酷的草和砖纹理以及不自然的缺少由旗帜投下的阴影,它看起来像任天堂64将呈现的东西,但这是一个开始。我们将在接下来的几章中改进演示的图形保真度。对于本章,我们将通过实施Phong着色模型来呈现上述图像,这将作为更高级效果的基础,我们将在后面讨论。

标志程序概述

我组织了四个C文件和四个标题的标志。您已经在hello-gl中看到了很多:file-util.cfile-util.h文件包含read_tgafile_contents函数,gl-util.cgl-util.h包含make_texturemake_shadermake_program函数。vec -util.h头包含一些基本的向量数学函数。flag.c看起来很像hello-gl.c做的:在main中,我们初始化GLUT和GLEW,为GLUT事件设置回调,调用一个make_resources函数来分配一堆GL资源,并调用glutMainLoop开始运行演示。然而,设置和渲染比上次更多地涉及。让我们来看看新功能和变化:

网格施工

meshes.c文件包含生成的顶点和元素阵列,统称为码,为我们将要呈现的标志,旗杆,地面和墙壁的对象。现实世界中的大多数物体,包括真正的旗杆和旗帜,都具有平滑的曲面,但显卡处理三角形。为了渲染这些对象,我们必须将其表面近似为三角形集合。我们通过填充具有沿其表面放置的顶点的顶点数组来完成此操作,将表面的属性存储在每个顶点中,并使用元素数组将样本连接到三角形,以给出原始曲面的近似值。

每个顶点的网格存储的基本属性是其在世界空间中的位置和其正常的垂直于原始表面的向量。正常是阴影计算的基础,我们稍后会看到。法线应为单位向量,即长度为一的向量。每个顶点还具有指示表面如何被遮蔽的材料参数。该材料可以由一组每顶点值组成,纹理坐标从纹理采样材料信息,或两者的某种组合。

对于标示演示,材质由纹理坐标组成,用于从网格纹理,镜面颜色和光泽度因子中采样色。我们将看到如何使用这些参数。因此,我们的顶点缓冲区包含一个如下的flag_vertex结构体:

struct flag_vertex {
    GLfloat位置[4];
    GLfloat正常[4];
    GLfloat texcoord [2];
    GLfloat光泽
    GLubyte镜面[4];
};

虽然位置和法线是三维向量,但是我们将它们填充到四个元素,因为大多数GPU优先从128位对齐的缓冲器(如SIMD指令集,如SSE)加载向量数据。对于每个网格,我们将顶点缓冲区,元素缓冲区,纹理对象和元素计数收集到一个flag_mesh结构中。当我们渲染时,我们设置glVertexAttribPointer来将所有的flag_vertex属性传递给顶点着色器:

 struct flag_mesh {
    GLuint vertex_buffer,element_buffer;
    GLsizei element_count;
    GLuint纹理;
};
static void render_mesh(struct flag_mesh const * mesh)
{
    glBindTexture(GL_TEXTURE_2D,mesh-> texture);

    glBindBuffer(GL_ARRAY_BUFFER,mesh-> vertex_buffer);
    glVertexAttribPointer(
        g_resources.flag_program.attributes.position,
        3,GL_FLOAT,GL_FALSE,sizeof(struct flag_vertex),
        (void *)offsetof(struct flag_vertex,position)
    );
    glVertexAttribPointer(
        g_resources.flag_program.attributes.normal,
        3,GL_FLOAT,GL_FALSE,sizeof(struct flag_vertex),
        (void *)offsetof(struct flag_vertex,normal)
    );
    glVertexAttribPointer(
        g_resources.flag_program.attributes.texcoord,
        2,GL_FLOAT,GL_FALSE,sizeof(struct flag_vertex),
        (void *)offsetof(struct flag_vertex,texcoord)
    );
    glVertexAttribPointer(
        g_resources.flag_program.attributes.shininess,
        1,GL_FLOAT,GL_FALSE,sizeof(struct flag_vertex),
        (void *)offsetof(struct flag_vertex,shininess)
    );
    glVertexAttribPointer(
        g_resources.flag_program.attributes.specular,
        4,GL_UNSIGNED_BYTE,GL_TRUE,sizeof(struct flag_vertex),
        (void *)offsetof(struct flag_vertex,specular)
    );

    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,mesh-> element_buffer);
    glDrawElements(
        GL_TRIANGLES,
        mesh-> element_count,
        GL_UNSIGNED_SHORT,
        (无效*)0
    );
}

需要注意的是glVertexAttribPointer呼吁镜面颜色属性通过GL_TRUE标准化参数。镜面颜色存储为0255之间的四组分字节数组,就像它们位于位图图像中一样,但是随着标准化标志的设置,它们将以0.0的标准化浮点值呈现给着色器和1.0

生成网格的实际代码是相当乏味的,所以我将在高层描述它。我们构建两种不同的网格:背景目,通过创建init_background_mesh,它由静态的旗杆,接地和壁对象; 和由init_flag_mesh设置的标志。背景网格由地面和墙壁的两个大矩形组成,一个带有尖卡车的薄圆柱体制作旗杆。墙壁,地面和旗杆分配纹理坐标以从包含在background.tga中存储的草,砖和金属纹理的单个纹理图集图像中进行采样。这允许整个背景通过相同的活动纹理单次渲染。旗杆另外被赋予黄色镜面颜色,当它遮蔽时,会给它一个金属光泽。该标志通过在st参数轴上以0和1之间的规则间隔评估函数calculate_flag_vertex生成,产生看起来像在微风中飘动的旗帜的东西。标志是一个单独的网格,使得随着标志动画更新网格数据,并让我们用自己的纹理渲染它,并从flag.tga加载。该标志通过在st参数轴上以0和1之间的规则间隔评估函数calculate_flag_vertex生成,产生看起来像在微风中飘动的旗帜的东西。标志是一个单独的网格,使得随着标志动画更新网格数据,并让我们用自己的纹理渲染它,并从flag.tga加载。该标志通过在st参数轴上以0和1之间的规则间隔评估函数calculate_flag_vertex生成,产生看起来像在微风中飘动的旗帜的东西。标志是一个单独的网格,使得随着标志动画更新网格数据,并让我们用自己的纹理渲染它,并从flag.tga加载。

流动动态网格数据

void update_flag_mesh(
    struct flag_mesh const * mesh,
    struct flag_vertex * vertex_data,
    GLfloat时间
){
    GLsizei s,t,i;
    for(t = 0,i = 0; t <FLAG_Y_RES; ++ t)
        for(s = 0; s <FLAG_X_RES; ++ s,++ i){
            GLfloat ss = FLAG_S_STEP * s,tt = FLAG_T_STEP * t;

            calculate_flag_vertex(&vertex_data [i],ss,tt,time);
        }

    glBindBuffer(GL_ARRAY_BUFFER,mesh-> vertex_buffer);
    glBufferData(
        GL_ARRAY_BUFFER,
        FLAG_VERTEX_COUNT * sizeof(struct flag_vertex),
        vertex_data,
        GL_STREAM_DRAW
    );
}

为了使标记动画化,我们使用我们的glutIdleFunc回调来重新计算标志的顶点并更新顶点缓冲区的内容。我们使用与初始化它相同的glBufferData函数更新缓冲区。然而,在初始化和每次更新时,我们给标记顶点数据GL_STREAM_DRAW提示,而不是我们一直在使用的GL_STATIC_DRAW提示。这告诉OpenGL驱动程序进行优化,因为我们将不断用新数据替换缓冲区。由于只有顶点本身的位置和法线本身才会改变,所以标志的元素缓冲区可以保持静态。顶点的连接不会改变。

使用深度缓冲区来排序3D对象

由于我们在3d空间中绘制多个对象,因此我们需要确保靠近查看器的对象在其背后的对象之上呈现。一个简单的方法来做到这一点是只渲染对象后到前,在我们的情况下,使背景网格第一,然后在顶部的标志它,但是这是低效的,因为的透支这种做法会导致:片段由片段着色器生成和处理,用于背景对象,只能被其前面的前景对象立即覆盖。前后渲染也不能自动渲染相互重叠的对象(如两个互锁环),以便先渲染任何一个对象,使其完全重叠。

显卡使用深度缓冲区来提供3d对象的高效可靠的排序。深度缓冲区是位于颜色缓冲区旁边的帧缓冲区的一部分,像彩色缓冲区一样,是像素值的二维数组。代替颜色值,深度缓冲器存储深度值,将投影空间z坐标与每个像素相关联。当三角形与光栅深度测试启用,每个碎片的投影ž值相比Ž当前存储在深度缓冲器值。如果片段比目前的深度缓冲区值更远离观察者,则片段被丢弃。否则,片段被渲染为颜色和深度缓冲区,

除了提供对象的正确排序之外,如果从头到尾呈现对象,深度缓冲也可以最大限度地降低过度绘制的成本。虽然光栅化器仍然会为已渲染对象遮蔽的对象的部分生成碎片,但是现代GPU可以在这些被遮挡的片段穿过片段着色器之前丢弃,从而减少处理器需要执行的片段着色器调用的总数。因为我们的标志网格出现在背景网格的前面,所以我们在背景之前渲染标志,使得背景的遮蔽部分不需要被遮蔽。

要在我们的程序中使用深度测试,我们需要在我们的帧缓冲区中请求一个深度缓冲区,然后在OpenGL状态下启用深度测试。随着GLUT,我们可以询问通过传递窗深度缓冲GLUT_DEPTH标志glutInitDisplayMode

int main(int argc,char * argv [])
{
    glutInit(&argc,argv);
    glutInitDisplayMode(GLUT_RGB | GLUT_DEPTH | GLUT_DOUBLE);
    / * ... * /
}

我们通过使用GL_DEPTH_TEST调用glEnableglDisable来启用和禁用深度测试:

static void init_gl_state(void)
{
    / * ... * /
    glEnable(GL_DEPTH_TEST);
    / * ... * /
}

当我们开始渲染我们的场景时,我们需要清除深度缓冲区以及颜色缓冲区,以确保陈旧的深度值不会影响渲染。我们可以通过将GL_COLOR_BUFFER_BITGL_DEPTH_BUFFER_BIT两者传递给一个glClear调用来清除两个缓冲区:

static void render(void)
{
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    / * ... * /
}

后台淘汰

另一个潜在的超额来源来自一个对象。如果您从任何方向看圆柱形旗杆,您将看到其表面的最多一半。面向前的三角形出现在背面三角形的前面,但它们可以光栅化成屏幕上相同的像素。根据网格中三角形的顺序,前面的三角形将覆盖背面三角形,或者背面三角形的碎片将无法进行深度测试,在任一情况下都需要GPU的额外工作。

然而,我们可以得到的GPU,以便宜和快速丢弃背对着三角形他们得到光栅化和深度测试,甚至之前。如果我们能背面剔除,显卡将每个三角形分类为前置式或背对着运行的顶点着色器和之前立即光栅化,彻底抛弃背对着三角形之后。它通过查看投影空间中每个三角形的绕组来实现。默认情况下,逆时针绕组的三角形被视为正面。这样做是因为将三角形变换成与观众相反的方向扭转了它的缠绕。通过构建我们的网格,以便从正面看的时候,所有的三角形的逆时针风,我们可以使用后台剔除来消除在面对远离观察者时光栅化这些三角形的大部分工作。顶点着色器只需要为其顶点运行。

通过将GL_CULL_FACE传递给glEnable / glDisable来启用和禁用后台淘汰

static void init_gl_state(void)
{
    / * ... * /
    glEnable(GL_CULL_FACE);
    / * ... * /
}

更新投影矩阵和视口

如果你回去一章,并尝试调整的HELLO-GL窗口中,你会发现,图像拉伸以适应窗口的新的大小,破坏我们辛辛苦苦保持纵横比。为了保持准确的纵横比,当窗口大小变化时,我们必须重新计算我们的投影矩阵,并考虑新的宽高比。我们还必须通过调用glViewport通知OpenGL新的视口大小。GLUT允许我们提供一个使用glutReshapeFunc调整窗口大小时调用的回调函数:

static void reshape(int w,int h)
{
    g_resources.window_size [0] = w;
    g_resources.window_size [1] = h;
    update_p_matrix(g_resources.p_matrix,w,h);
    glViewport(0,0,w,h);
}
int main(int argc,char * argv [])
{
    / * ... * /
    glutReshapeFunc(重塑);
    / * ... * /
}

所述update_p_matrix函数实现从最后一章透视矩阵式并存储在新的投影矩阵g_resources.p_matrix阵列,从中我们将我们的饲料着色p_matrix均匀的变量。

使用GLUT处理鼠标和键盘输入

GLUT为鼠标和键盘输入提供了非常原始的支持。在标志中,我已经做到这一点,所以拖动鼠标移动视图,当鼠标按钮被释放时,视图可以恢复到原来的位置。GLUT提供一个glutMotionFunc回调,当鼠标按住按钮时移动,当按下或释放鼠标按钮时调用glutMouseFunc。(还有一个glutPassiveMotionFunc可以在没有按下按钮的情况下处理鼠标移动,我们不使用它)。我们的glutMotionFunc相对于从窗口中心的距离调整模型视图矩阵,而当我们的glutMouseFunc将重置时鼠标按钮放开:

static void drag(int x,int y)
{
    float w =(float)g_resources.window_size [0];
    float h =(float)g_resources.window_size [1];
    g_resources.eye_offset [0] =(float)x / w  -  0.5f;
    g_resources.eye_offset [1] =  - (float)y / h + 0.5f;
    update_mv_matrix(g_resources.mv_matrix,g_resources.eye_offset);
}

static void mouse(int button,int state,int x,int y)
{
    if(button == GLUT_LEFT_BUTTON && state == GLUT_UP){
        g_resources.eye_offset [0] = 0.0f;
        g_resources.eye_offset [1] = 0.0f;
        update_mv_matrix(g_resources.mv_matrix,g_resources.eye_offset);
    }
}
int main(int argc,char * argv [])
{
    / * ... * /
    glutMotionFunc(拖动);
    glutMouseFunc(小鼠);
    / * ... * /
}

update_mv_matrix功能类似于update_p_matrix。它按照上一章的公式生成一个翻译矩阵,并将其存储到g_resources.mv_matrix,从中我们提供着色器的mv_matrix统一变量。

我还绑定了标志,所以您可以通过按R键在演示运行过程中从磁盘重新加载GLSL程序。该glutKeyboardFunc当按下键回调函数被调用。我们的回调检查按下的键是否为R,如果是,则调用update_flag_program

static void keyboard(unsigned char key,int x,int y)
{
    if(key =='r'|| key =='R'){
        update_flag_program();
    }
}
int main(int argc,char * argv [])
{
    / * ... * /
    glutKeyboardFunc(键盘);
    / * ... * /
}

update_flag_program尝试从磁盘加载,编译和链接flag.v.glslflag.f.glsl文件,如果成功,则替换旧的着色器和程序对象。

这涵盖了旗帜演示的C代码。实际的阴影发生在GLSL代码中,我们将在下面看看。

ong阴

物理上精确的光模拟需要昂贵的算法,这些算法最近才能使甚至高端计算机集群实时计算。幸运的是,人眼不需要完美的物理准确性,特别是不能用于快速移动的动画图形,而且实时的计算机图形已经在一般消费者硬件上渲染出令人印象深刻的图形,使用近似光线行为的模拟,而不需要模拟完美的 最根本的这些技巧是Phong着色模型,这是一种价格低廉的近似法,它与20世纪70年代初由计算机图形学先驱Bui Tuong Phong开发的简单材料相互作用。Phong阴影是局部照明模拟 - 它只考虑光源和单个点之间的直接相互作用。因此,单独的Phong阴影不能计算涉及场景中其他对象的影响的影响,例如阴影和镜面反射。这就是为什么旗帜在它后面的地面或墙壁上没有阴影。

Phong模式涉及三种不同的照明术语:

  • 环境反射,一个模拟光的背景水平的常数项;
  • 漫反射,这给我们通常认为的颜色的材料;
  • 镜面反射,抛光或金属表面的光泽。

漫反射和环境反射

如果在黑暗的房间中拿着一张平板纸,直到灯泡正面,当它从灯光旋转时,它会显得更亮,当它垂直于灯泡时最暗光。弯曲表面的行为方式相同; 如果您卷起或折叠纸张,其表面将最亮,最直接面对光线。表面法线和光线方向之间的角度越宽,纸张越暗。如果纸张和光线保持静止,但移动您的头部,纸张的明显颜色和亮度将不会改变。同样地,在标志演示中,如果用鼠标拖动视图,则可以看到标志的阴影保持不变。表面在每个方向均匀地反射光,或“漫射”。

有一种叫做点积的廉价操作,它从与它们之间的角度相关的两个向量产生一个标量值。给定两个单位向量uv,如果它们的点积u·v(发音为“u dot v”)为1,则向量面向完全相同的方向; 如果为零,它们是垂直的; 如果是负面的,他们面对完全相反的方向。正点产品表示锐角,而负点产品表示钝角。GLSL提供一个函数点(u,v)来计算两个相同大小的vec值的点积。

点产品的行为遵循漫反射:表面反射更多的光,它们变得更平行于光源,或者换句话说,它们的正常点和光的方向的点积越接近一个。垂直或背面的表面不会反射光,它们的点积将为零或负。点积和漫反射亮度之间的关系首先被18世纪的物理学家约翰·兰伯特(Johann Lambert)观察到,并被称为朗伯反射率,表现出这种行为的表面称为朗伯表面。Phong阴影使用朗伯反射来模拟漫反射,取表面法线的点积和从表面到光源的方向。如果点积大于零,将其乘以光的漫反射颜色,并将结果与​​表面漫反射颜色相乘以得到阴影结果。(乘以两个颜色的值包括将相应的红色,绿色,蓝色和阿尔法分量相乘,这是GLSL的*运算符在给定两个vec4时执行的)。如果点积为零或负,则漫反射颜色将为零。

然而,在现实世界中,即使表面没有直接点亮,仍然不会出现黑色。在任何封闭区域,会有一定量的环境反射弹起,光源不直接撞击的昏暗照明区域。Phong模型通过为光源赋予恒定的环境色彩来模拟环境效果。这个环境颜色被乘以点积后加入到光的漫反射颜色中。然后将环境和漫反射颜色的总和乘以表面的漫反射颜色以给出阴影结果。

镜面反射

并非所有表面均匀地反射光线; 许多材料,包括金属,玻璃,头发和皮肤,都具有反光光泽。与漫反射不同,如果观察者在光源和闪光物体保持静止时移动,则光线将随着观察者沿表面移动。您可以通过查看旗杆来观看在旗帜演示中模拟的东西:当您上下拖动视图时,金色光泽沿着杆移动。在物理上,当其表面被高反射性的微细覆盖层覆盖时,物体表现出光泽。这些方面面向每一个方向,创造一个明亮的闪光点,光源直接反映给观众。这种效果被称为镜面反射

镜面效应是由光源到观察者的反射引起的,所以Phong阴影通过反射围绕表面法线的光线方向来模拟镜面效果,以产生反射方向。然后,我们可以将反射方向的点积和从表面到观察者的方向。镜面表面上的微绒毛遵循正态分布:多个小平面平行于表面,并且在与表面更陡的角度上的刻面数量呈指数下降。对于更抛光的表面,降落更清晰,提供更小,更紧密的镜面高光。Phong阴影通过将点积提升为称为光泽因子的指数来近似分布,具有更高的光泽度,赋予更加光滑的光泽和更低的因子,从而产生更加漫散的光泽。然后将该最终镜面因子乘以光源和表面的镜面颜色,并将结果添加到漫反射和环境色彩以给出最终颜色。非镜面表面具有透明的镜面颜色,红色,绿色,蓝色和α分量设置为零,这样可以消除阴影方程中的镜面项。

在GLSL中实施Phong阴影

着色计算通常在顶点和片段着色器中执行,在这些着色器中,它们可以利用GPU的并行处理能力。(这是GPU程序的“着色器”一词来自哪里。)让我们回顾一下图形流水线图,了解Phong着色数据流的概述:

为了获得最佳的准确性,我们在每个片段级别执行阴影。(为了更好的性能,在顶点着色器中也可以进行阴影处理,并且在顶点之间插入结果,但这将导致不太准确的阴影,尤其是对于镜面效果)。顶点着色器flag.v.glsl因此仅执行转换和投影,使用p_matrixmv_matrix我们作为制服进入。着色器将大部分材质顶点属性转发给片段着色器使用的不同变量:

#version 110

均匀mat4 p_matrix,mv_matrix;
均匀采样器2D纹理;

属性vec3位置,正常;
属性vec2 texcoord;
属性浮动光泽
属性vec4镜面;

变化的vec3 frag_position,frag_normal;
变化的vec2 frag_texcoord;
变化的浮动frag_shininess;
变化的vec4 frag_specular;

void main()
{
    vec4 eye_position = mv_matrix * vec4(position,1.0);
    gl_Position = p_matrix * eye_position;
    frag_position = eye_position.xyz;
    frag_normal =(mv_matrix * vec4(normal,0.0))。xyz;
    frag_texcoord = texcoord;
    frag_shininess = shininess;
    frag_specular =镜面;
}

除了纹理坐标,光泽度和镜面颜色之外,顶点着色器还会向片段着色器输出模型视图转换的顶点位置。模型视图矩阵转换坐标空间,使得观察者处于原点,因此我们可以从变换位置确定镜面计算所需的表面到观察者的方向。我们同样将法向量转换成与位置相同的参考框架。由于法线是没有位置的方向矢量,所以我们用w分量为零来应用矩阵,这消除了模型视图矩阵的平移,并且只适用于其旋转。使用这组不同的值,片段着色器flag.f.glsl可以执行实际的Phong计算:

#version 110

均匀mat4 p_matrix,mv_matrix;
均匀采样器2D纹理;

变化的vec3 frag_position,frag_normal;
变化的vec2 frag_texcoord;
变化的浮动frag_shininess;
变化的vec4 frag_specular;

const vec3 light_direction = vec3(0.408248,-0.816497,0.408248);
const vec4 light_diffuse = vec4(0.8,0.8,0.8,0.0);
const vec4 light_ambient = vec4(0.2,0.2,0.2,1.0);
const vec4 light_specular = vec4(1.0,1.0,1.0,1.0);

void main()
{
    vec3 mv_light_direction =(mv_matrix * vec4(light_direction,0.0))。xyz,
         normal = normalize(frag_normal),
         eye = normalize(frag_position),
         reflection = reflect(mv_light_direction,normal);

    vec4 frag_diffuse = texture2D(texture,frag_texcoord);
    vec4 diffuse_factor
        = max(-dot(normal,mv_light_direction),0.0)* light_diffuse;
    vec4 ambient_diffuse_factor
        = diffuse_factor + light_ambient;
    vec4 specular_factor
        = max(pow(-dot(反射,眼睛),frag_shininess),0.0)
            * light_specular;
    
    gl_FragColor = specular_factor * frag_specular
        + ambient_diffuse_factor * frag_diffuse;
}

为了保持简单,着色器在着色器源中使用量值定义单个光源。真正的渲染器可能将这些光参数以均匀的值进行馈送,使得可以移动光或者它们的材质属性从主机程序改变。将GLSL中嵌入的光属性作为常量,可以轻松地更改源中的光属性,按R重新加载着色器,并查看结果。我们的光源就像在无限远的地方一样,在场景中的每一个表面照射着相同的光线。光线为白色,基准环境光线为20%。它可以通过替换light_diffuselight_ambient

片段着色器使用我们以前没有看到的几个新的GLSL函数:

  • 归一化(v)返回与v相同方向的单位向量。我们在这里使用它来将片段位置转换为方向向量,并确保正常是单位向量。即使我们的原始顶点法线都是单位矢量,它们的线性内插也不会。
  • pow(x,n)将x提高到第n个幂,我们用于应用镜面光泽因子。
  • max(n,m)返回nm中的较大者。我们使用它来钳制小于零的点数,所以它们的颜色与零一样。
  • 反映(U,V)反映了矢量ü周围v,给人一种载体,使相同的角度与v作为ü,但在相反的方向。有了它,我们得到镜面计算的反射眼睛方向。

我们将恒定的光方向转换成与正常矢量相同的坐标空间。然后,我们从网格纹理中对表面的漫反射颜色进行采样我们将阴影值分配给gl_FragColor以生成最终的阴影片段。

调整Phong模型的风格效果

在我们包装之前,让我们快速看看如何操作Phong框架来提供更多的风格化结果。经典的Phong模型是一个照片写实的模式:它试图模拟现实世界的光线行为。但照片写实并不总是令人满意的。许多游戏通过使用更多风格化的阴影效果使视觉上分开。这些影响通常使用弥漫,环境和镜面照明的基本Phong模型,但是它们在将它们相加在一起之前扭曲各个因素。

作为一个简单的例子,我们可以得到一个更明亮,更柔和的阴影效果,如果不是将背面表面的漫射点积分为零,我们将其缩放使得垂直表面接收半光照,并且背面的表面线性向零。团队要塞2使用这个“半朗伯”反射比例,因为标准朗伯式降落率被减半了,作为其卡通式但是半照片写实的外观(虽然大量修改)的基础。让我们修改flag.f.glsl来扭曲漫射点积:

float warp_diffuse(float d)
{
    返回d * 0.5 + 0.5;
}

void main()
{
    // ...
    vec4 diffuse_factor
        = max(warp_diffuse( -dot(normal,mv_light_direction),0.0)* light_diffuse;
    // ...
}

从这个半朗伯量纲建立的流行效果是细分阴影,其中阶梯函数被应用于半朗伯因子,使得表面在光和暗区域之间具有较高对比度的平坦地阴影,以传统的方式手绘动画片。Jet Set Radio开创了这款外观,它已被用于无数游戏。在GLSL中实现它很容易:

浮法(浮法)
{
    返回平滑步(0.35,0.37,d)* 0.4 +平滑步(0.70,0.72,d)* 0.6;
}

float warp_diffuse(float d)
{
    返回纤维(d * 0.5 + 0.5);
}
GLSL的 smoothstep(lo,hi,x)函数的行为如下:如果 x小于 lo,则返回 0.0 ; 如果大于 ,则返回 1.0 ; 如果在两者之间,它将从零线性转换为1。我们上面的 cel功能使用 平滑步骤创建三个平坦的阴影级别,其间具有短的线性转换。

还有其他的效果可以通过改变warp_diffuse函数来执行。例如,该功能并不需要是漂浮至- 浮动也可以映射到一个色标; 您可以将较大的点产品映射到较暖的红色,而较小的产品则映射到较冷的蓝色,以呈现艺术插图效果。我鼓励您尝试片段着色器代码,以查看可以创建的其他效果。

结论

通过Phong阴影实现,我们可以开始添加附加效果,以进一步改善旗帜场景的外观。最明显的问题是标志没有阴影投射,所以下一章我们将看一下阴影映射,一种将准确的阴影渲染到场景中的技术,并了解该过程中的屏幕外屏幕缓冲对象。同时,如果您有兴趣在没有OpenGL偏见的情况下自行学习实时着色技术,我强烈推荐实时渲染

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值