第八课:材质与光源——让物体真正“活”起来
说实话,前几节课我们一直在跟顶点、纹理、矩阵打交道。你可能会觉得,嗯,画面是有了,但总感觉少了点什么?
少了“质感”。
一个纯色的立方体,和一个看起来像金属、像塑料、像陶瓷的立方体,差别在哪?就在材质和光照。今天我们就来啃这块硬骨头。
8.1 光照模型:为什么我们需要它?
现实世界里,我们能看到物体,是因为光线照到物体表面,然后反射到我们眼睛里。计算机里要模拟这个过程,就得靠光照模型。
最经典、最常用的就是冯氏光照模型(Phong Lighting Model)。它把光照拆成三个独立的部分:
- 环境光(Ambient):就算在阴影里,物体也不是全黑的。环境光模拟了这种“无处不在”的微弱光线。
- 漫反射(Diffuse):光线照到粗糙表面,向四面八方均匀反射。这是物体颜色的主要来源。
- 镜面高光(Specular):光线照到光滑表面,产生集中的反射。比如金属上的高光点。
我个人习惯把这三部分加起来,就是最终的颜色:
最终颜色 = 环境光 + 漫反射 + 镜面高光
你想想看,这个模型虽然简单,但组合起来效果已经非常好了。
8.2 材质属性:定义物体“长什么样”
材质,说白了就是告诉OpenGL:这个物体对光线的反应是什么样的。
在冯氏模型中,材质通常由以下几个属性定义:
| 属性 | 含义 | 示例值(金属) | 示例值(塑料) |
|---|---|---|---|
| ambient | 环境光反射系数 | (0.2, 0.2, 0.2) | (0.1, 0.1, 0.1) |
| diffuse | 漫反射反射系数 | (0.8, 0.6, 0.2) | (0.8, 0.8, 0.8) |
| specular | 镜面反射系数 | (1.0, 1.0, 1.0) | (0.5, 0.5, 0.5) |
| shininess | 高光锐利度 | 32.0 | 8.0 |
我在项目中遇到过一个问题:把金属的shininess设得太高,结果高光点小到几乎看不见。后来才意识到,金属的高光虽然亮,但范围其实比塑料要集中得多。
8.3 点光源 vs 方向光
光源类型不同,计算方式也不同。我们重点讲两种:
8.3.1 方向光
方向光模拟的是太阳光。所有光线都是平行的,方向固定。计算时只需要一个方向向量。
它的好处是简单,性能好。坏处是——没有位置,没有衰减。
// 方向光计算(片段着色器)
vec3 lightDir = normalize(-u_LightDirection);
float diff = max(dot(normal, lightDir), 0.0);
vec3 diffuse = diff * lightColor * material.diffuse;
8.3.2 点光源
点光源模拟的是灯泡、蜡烛。它有位置,光线从一点向四周发射。
点光源需要计算衰减:距离越远,光越弱。衰减公式一般是:
float attenuation = 1.0 / (constant + linear * distance + quadratic * distance * distance);
嗯,这里要注意:constant、linear、quadratic三个系数需要调参。我刚开始做的时候直接抄网上的值,结果画面要么太亮要么太暗。后来我习惯先固定constant=1.0,然后根据场景大小调整linear和quadratic。
8.4 多光源组合:让场景丰富起来
实际项目中,很少只用一种光源。比如一个室内场景,可能有:
- 一个方向光模拟窗外阳光
- 几个点光源模拟台灯、吊灯
- 甚至再加一个聚光灯模拟手电筒
多光源组合的核心思路很简单:每个光源单独计算,然后加起来。
// 伪代码:多光源组合
vec3 result = vec3(0.0);
// 方向光
result += calcDirectionalLight(normal, viewDir, material);
// 点光源1
result += calcPointLight(lightPos1, normal, fragPos, viewDir, material);
// 点光源2
result += calcPointLight(lightPos2, normal, fragPos, viewDir, material);
// 别忘了环境光
result += ambient * material.ambient;
FragColor = vec4(result, 1.0);
我曾经在一个AR项目中同时用了4个点光源+1个方向光。性能上其实还好,因为每个光源的计算量并不大。但要注意:光源数量越多,着色器越复杂,GPU压力越大。移动端建议控制在3-5个以内。
8.5 实战:在Android中实现材质与多光源
好了,理论说完了,我们动手写代码。这里我给出一个完整的片段着色器示例,它支持材质、一个方向光、两个点光源。
// fragment_shader.glsl
precision mediump float;
struct Material {
vec3 ambient;
vec3 diffuse;
vec3 specular;
float shininess;
};
struct DirectionalLight {
vec3 direction;
vec3 color;
};
struct PointLight {
vec3 position;
vec3 color;
float constant;
float linear;
float quadratic;
};
uniform Material u_Material;
uniform DirectionalLight u_DirLight;
uniform PointLight u_PointLights[2];
uniform vec3 u_ViewPos;
varying vec3 v_Normal;
varying vec3 v_FragPos;
vec3 calcDirectionalLight(vec3 normal, vec3 viewDir) {
vec3 lightDir = normalize(-u_DirLight.direction);
float diff = max(dot(normal, lightDir), 0.0);
vec3 reflectDir = reflect(-lightDir, normal);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), u_Material.shininess);
vec3 ambient = u_DirLight.color * u_Material.ambient;
vec3 diffuse = u_DirLight.color * diff * u_Material.diffuse;
vec3 specular = u_DirLight.color * spec * u_Material.specular;
return ambient + diffuse + specular;
}
vec3 calcPointLight(PointLight light, vec3 normal, vec3 fragPos, vec3 viewDir) {
vec3 lightDir = normalize(light.position - fragPos);
float diff = max(dot(normal, lightDir), 0.0);
vec3 reflectDir = reflect(-lightDir, normal);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), u_Material.shininess);
float distance = length(light.position - fragPos);
float attenuation = 1.0 / (light.constant + light.linear * distance + light.quadratic * distance * distance);
vec3 ambient = light.color * u_Material.ambient;
vec3 diffuse = light.color * diff * u_Material.diffuse;
vec3 specular = light.color * spec * u_Material.specular;
return (ambient + diffuse + specular) * attenuation;
}
void main() {
vec3 normal = normalize(v_Normal);
vec3 viewDir = normalize(u_ViewPos - v_FragPos);
vec3 result = calcDirectionalLight(normal, viewDir);
result += calcPointLight(u_PointLights[0], normal, v_FragPos, viewDir);
result += calcPointLight(u_PointLights[1], normal, v_FragPos, viewDir);
gl_FragColor = vec4(result, 1.0);
}
这段代码里,我把材质和光源都定义成了struct。这样代码清晰,也方便扩展。如果你要加第三个点光源,只需要在uniform数组里加一个元素就行。
核心要点:多光源组合的本质就是“累加”。每个光源独立计算,最后把结果加起来。但要注意,环境光不要重复计算——通常只加一次。
8.6 知识体系总览
下面这张图,是我自己整理的光照与材质知识结构。你可以把它当作一个“地图”,学完这节课后对照着检查自己掌握了哪些。
我的小技巧:调试光照时,可以先只开漫反射,确认法线方向正确。然后再加环境光和镜面高光。这样出问题了容易定位。
注意:法线一定要在片段着色器里重新归一化(normalize)。因为顶点着色器插值出来的法线长度会变化,不归一化的话光照效果会出错。我曾经在这个坑里浪费了半天时间。
好了,这节课的内容就到这里。材质和光源是OpenGL里最有趣的部分之一。你想想看,同样的模型,换一套材质参数、换几个光源位置,出来的效果天差地别。这就是图形学的魅力所在。