Skip to main content

认识材质

材质(Material)定义了物体的表面外观,包括它的颜色、光影、纹理等。渲染引擎通过读取材质属性,计算光线与物体的交互,最终生成逼真的图像,比如各种不同颜色的表面、映射出周围环境的镜面材质、凹凸不平的砖砌材质、带有小凹点的木板材质或者带有铁锈的金属材质。

material pbr_material

在这里,我们将讨论以下话题:

  • 定义材质:了解在Illustrator Core中如何定义材质
  • 着色器:您可以深层次了解着色器程序如何利用您定义的材质属性
  • Uniform参数:通过Uniform变量将材质属性传递到着色器
  • 状态绑定:绘制材质时可以绑定状态参数,这些参数会影响OpenGL的图元处理流程,从而影响最终渲染结果

下面我们从一个简单例子,了解在Illustrator core中是如何定义材质的。

定义材质

材质包含了三大部分参数:

  • programs: 着色器程序,定义了如何将3D模型转换为屏幕上的像素分布。我们会用到两种着色器:顶点着色器和面元着色器。
  • parameters:着色器中需要的统一的材质属性,比如材质颜色、材质透明度等。
  • states:状态参数,比如深度测试、混合测试、纹理贴图等,这些内容会在后面的章节中陆续为您介绍。

下面给出具体的例子,帮助您理解它的参数是如何定义的。

    // 定义了顶点着色器程序
std::string vertexSource = R"(
#version 330 core
// 读入物体的几何顶点信息,在顶点着色器中将顶点坐标转换到裁剪空间
in vec3 positions;
uniform mat4 modelViewProjectionMatrix;

void main()
{
gl_Position = modelViewProjectionMatrix * vec4(positions, 1.0);
}
)";
// 定义了面元着色器程序
std::string fragmentSource = R"(
#version 330 core

out vec4 fragColor;
// 着色器对面元进行着色时,需要使用到材质的基础颜色“color”
uniform vec3 color;

void main()
{
fragColor = vec4(color, 1.0);
// 您也可以直接在函数中定义材质的颜色
// fragColor = vec4(0.4, 0.6, 0.2, 1.0);
}
)";
...

// 定义一个材质
auto material = _material(
_program(
// 指定了该材质的顶点着色器和面元着色器
vertexSource, fragmentSource
),
_parameters({
// Uniform变量,指定了该材质的颜色
{"color", _clr3(0.4, 0.6, 0.2)}
}),
_states({
// 打开了深度测试
_depth(true)}
)
);

着色器

OpenGL中提供了多种与渲染相关的着色器,它们本质上是把输入转换为输出的独立程序,使用着色器语言(glsl)可以编写着色器。多个着色器之间彼此独立,唯一的沟通是它们的输入与输出。

我们在材质渲染中会使用到其中两种着色器:顶点着色器(vertex shader)和面元着色器(fragment shader),在上面的例子中,我们已经看到了非常基础的着色器的源代码。

下面是一个典型的着色器结构:

    #version version_number
in type in_variable_name;
in type in_variable_name;

out type out_variable_name;

uniform type uniform_name;

int main()
{
// 处理输入并进行一些图形操作
...
// 输出处理过的结果到输出变量
out_variable_name = weird_stuff_we_processed;
}

在顶点着色器中,声明了计算所需的输入顶点属性,比如positions,noramls,texture coords,有些情况下甚至还有colors。顶点着色器会输出预定义变量gl_Position,也可以声明其他输出变量,比如v_Normal, v_TexCoord等。在顶点着色器中,我们会编写main运算函数,完成顶点属性从3D空间坐标到屏幕坐标的变换。

着色器之间可以传递同名的输入输出变量。在后续的面元着色器中,必须声明一个颜色输出变量,因此片段着色器所做的是计算像素最后的颜色输出。

Uniform参数

我们既可以直接在着色器程序中对变量赋值,也可以通过我们的应用程序从CPU上传递到GPU上,后者一般是通过声明一个uniform类型的参数来实现。uniform是全局的,意味着uniform参数必须在每个着色器程序对象中都是独一无二的,而且它可以被着色器程序的任意着色器在任意阶段访问。在下面的例子中,我们使用接口_parameters为material定义了一个uniform参数color

    auto material = _material(
_program(
// 指定了该材质的顶点着色器和面元着色器
vertexSource, fragmentSource
),
_parameters({
// Uniform变量,指定了该材质的颜色
{"color", _clr3(0.4, 0.6, 0.2)}
}),
_states({
// 打开了深度测试
_depth(true)}
)
);

状态参数

在材质中,还可以绑定一系列状态变量,用来设置状态的开启或禁用和状态使用方式,从而控制OpenGL去执行一些我们想要的操作。下面是各种状态的简单介绍。

混合

混合(Blending)是实现物体透明度的一种技术。透明的物体,它的颜色是其本身颜色和它背后其他物体颜色的不同强度结合。

深度测试

当深度测试(Depth Testing)被启用的时候,OpenGL会将一个片段的深度值与深度缓冲的内容进行对比,如果这个测试通过了的话,深度缓冲将会更新为新的深度值。如果深度测试失败了,片段将会被丢弃。

模板测试

模板测试是根据模板缓冲来进行的,我们可以在渲染的时候更新它来获得一些很有意思的效果。当片段着色器处理完一个片段之后,模板测试(Stencil Test)会开始执行,和深度测试一样,它会根据场景中已绘制的其它物体的片段,来决定是否丢弃特定的片段。

纹理贴图

纹理贴图看作是一个2D图片(在某些特殊场景中,也可以是1D或者3D的),2D图片中包含的像素值,既可以是某种真实材质的颜色,也可以是其他表面特性,比如光照反射率、凹凸系数,从而为模型增加丰富的表面细节,使其看起来更加逼真。

面剔除

面剔除是将我们观察不到的片段在进入着色器之前就丢弃掉,这样可以节省很大一部分资源调用。我们需要告诉OpenGL哪些是正向面,哪些是背向面,我们可以通过顶点数据的环绕顺序来判断正背面。

polygon offset

在3D渲染中会遇到深度冲突的问题,从而导致某些片段的可见性之间互相打架,忽隐忽现。采用polygon offsetting技术,稍微调整片段的深度值,从而在它们之间创造出微小的间隙,这样就可以保证它们在深度缓冲(depth buffer)中可以被区别开来。