认识材质
材质(Material)定义了物体的表面外观,包括它的颜色、光影、纹理等。渲染引擎通过读取材质属性,计算光线与物体的交互,最终生成逼真的图像,比如各种不同颜色的表面、映射出周围环境的镜面材质、凹凸不平的砖砌材质、带有小凹点的木板材质或者带有铁锈的金属材质。
在这里,我们将讨论以下话题:
- 定义材质:了解在ORE Core中如何定义材质
- 材质属性:通过参数将材质属性传递到着色器
- 着色器:您可以深层次了解着色器程序如何利用材质属性
- 状态绑定:绘制材质时可以绑定状态参数,这些参数会影响WebGL的图元处理流程,从而影响最终渲染结果
下面我们从一个简单例子,了解在ORE core中是如何定义材质的。
定义材质
材质包含了三大部分参数:
- program: 着色器程序,定义了如何将3D模型转换为屏幕上的像素分布。我们会用到两种着色器:顶点着色器和面元着色器。
- parameters:着色器中需要的统一的材质属性,比如材质颜色、材质透明度等。
- states:状态参数,比如深度测试、混合测试、纹理贴图等,这些内容会在后面的章节中陆续为您介绍。
下面给出具体的例子,帮助您理解它的参数是如何定义的。
// 定义顶点着色器
const vs = `#version 300 es
// 输入几何的顶点数据,将顶点坐标转换到相机空间
in vec3 positions;
in vec3 normals;
out vec3 v_normal;
uniform mat4 modelViewProjectionMatrix;
uniform mat3 normalMatrix;
void main() {
gl_Position = modelViewProjectionMatrix * vec4(positions, 1.0);
v_normal = vec3(normalMatrix * normals);
}`;
// 定义片元着色器
const fs = `#version 300 es
precision mediump float;
// 当给片元着色时,需要用到材质中的color属性
uniform vec3 color;
in vec3 v_normal;
out vec4 fragColor;
float headLight(vec3 normal) {
float nDotV = abs(normalize(normal).z);
return float(0.4) + nDotV * float(0.6) + float(0.15) * pow(nDotV, float(50.0));
}
void main() {
float lightIntensity = headLight(v_normal);
fragColor = vec4(color * lightIntensity, 1.0);
}`;
// 定义材质
const material: JMaterial = {
program: {
// 指定材质的顶点着色器和片元着色器
vertex: vert(vs),
fragment: frag(fs)
},
parameters: {
// 定义材质的颜色
color: clr3(0.8, 0.35, 0.1)
},
states: {
// 开启深度测试
depth: Depth({ enabled: true })
}
};
着色器
WebGL中提供了多种与渲染相关的着色器,它们本质上是把输入转换为输出的独立程序,使用着色器语言(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空间坐标到屏幕坐标的变换。
着色器之间可以传递同名的输入输出变量。在后续的面元着色器中,必须声明一个颜色输出变量,因此片段着色器所做的是计算像素最后的颜色输出。
参数
我们既可以直接在着色器程序中对变量赋值,也可以通过我们的应用程序从CPU上传递到GPU上,后者一般是通过声明一个uniform类型的参数来实现。uniform是全局的,意味着uniform参数必须在每个着色器程序对象中都是独一无二的,而且它可以被着色器程序的任意着色器在任意阶段访问。在下面的例子中,我们为material定义了一个uniform参数color。
const material: JMaterial = {
program: {
// 指定了该材质的顶点着色器和面元着色器
vertex: vert(vs),
fragment: frag(fs)
},
parameters: {
// 指定了该材质的颜色
color: clr3(0.8, 0.35, 0.1)
},
states: {
// 打开了深度测试
depth: Depth({ enabled: true })
}
};
状态参数
在材质中,还可以绑定一系列状态变量,用来设置状态的开启或禁用和状态使用方式,从而控制WebGL去执行一些我们想要的操作。下面是各种状态的简单介绍。
混合
混合(Blending)是实现物体透明度的一种技术。透明的物体,它的颜色是其本身颜色和它背后其他物体颜色的不同强度结合。
深度测试
当深度测试(Depth Testing)被启用的时候,WebGL会将一个片段的深度值与深度缓冲的内容进行对比,如果这个测试通过了的话,深度缓冲将会更新为新的深度值。如果深度测试失败了,片段将会被丢弃。
模板测试
模 板测试是根据模板缓冲来进行的,我们可以在渲染的时候更新它来获得一些很有意思的效果。当片段着色器处理完一个片段之后,模板测试(Stencil Test)会开始执行,和深度测试一样,它会根据场景中已绘制的其它物体的片段,来决定是否丢弃特定的片段。
纹理贴图
纹理贴图看作是一个2D图片(在某些特殊场景中,也可以是1D或者3D的),2D图片中包含的像素值,既可以是某种真实材质的颜色,也可以是其他表面特性,比如光照反射率、凹凸系数,从而为模型增加丰富的表面细节,使其看起来更加逼真。
面剔除
面剔除是将我们观察不到的片段在进入着色器之前就丢弃掉,这样可以节省很大一部分资源调用。我们需要告诉WebGL哪些是正向面,哪些是背向面,我们可以通过顶点数据的环绕顺序来判断正背面。
polygon offset
在3D渲染中会遇到深度冲突的问题,从而导致某些片段的可见性之间互相打架,忽隐忽现。采用polygon offsetting技术,稍微调整片段的深度值,从而在它们之间创造出微小的间隙,这样就可以保证它们在深度缓冲(depth buffer)中可以被区别开来。