GLSL 编程/Blender/漫反射
本教程涵盖每个顶点的漫反射。
这是关于 Blender 中基本光照的一系列教程中的第一个。在本教程中,我们从单个方向光源的漫反射开始,然后包括点光源和聚光灯。后面的教程将涵盖其扩展,特别是镜面反射、每个像素的光照、双面光照和多个光源。
月球几乎完全表现出漫反射(也称为朗伯反射),即光线被反射到所有方向而没有镜面高光。其他此类材料的例子包括粉笔和哑光纸;实际上,任何看起来暗淡和哑光的表面都是如此。
在完美的漫反射情况下,观察到的反射光的强度取决于表面法向量和入射光线之间的角度的余弦值。如左侧图所示,通常考虑从表面某一点开始的归一化向量,在那里应该计算光照:归一化表面法向量N与表面正交,归一化光方向L指向光源。
对于观察到的漫反射光,我们需要归一化表面法向量N和指向光源的归一化方向L之间的角度的余弦值,即点积N·L,因为任何两个向量a和b的点积a·b是
.
对于归一化向量,长度 |a| 和 |b| 都是 1。
如果点积N·L为负,光源位于表面的“错误”一侧,我们应该将反射设置为 0。这可以通过使用 max(0, N·L) 来实现,它确保点积的值对于负点积被钳制到 0。此外,反射光取决于入射光线的强度和一个材料常数 用于漫反射:对于黑色表面,材料常数 为 0,对于白色表面,它是 1。那么漫反射强度的方程为
对于彩色光,此方程适用于每个颜色分量(例如红色、绿色和蓝色)。因此,如果变量、 和 表示颜色向量,乘法是按分量执行的(它们在 GLSL 中是针对向量的),此方程也适用于彩色光。这实际上是我们将在着色器代码中使用的。
如果我们只有一个方向光源(即 Blender 中的“太阳”光),那么实现方程的着色器代码相对较小。为了实现该方程,我们遵循关于实现方程的问题,这些问题在轮廓增强教程中进行了讨论。
- 应该在顶点着色器还是片段着色器中实现该方程?我们在这里尝试顶点着色器。在平滑镜面高光教程中,我们将查看片段着色器中的实现。
- 应该在哪个坐标系中实现该方程?我们默认尝试 Blender 中的视图空间。(事实证明这是一个很好的选择,因为 Blender 在视图空间中提供了光方向。)
- 我们从哪里获取参数?这个问题的答案比较长。
正如视空间着色教程中所述,gl_FrontMaterial.diffuse
通常为黑色;因此,我们使用 gl_FrontMaterial.emission
来表示漫反射材质颜色 。(请记住在材质选项卡中将着色 > 发射设置为 1。)视空间中光源L的方向可以在 gl_LightSource[0].position
中获取,光线颜色 可以从 gl_LightSource[0].diffuse
中获取。(如果只有一个光源,它始终是数组 gl_LightSource[]
中的第 0 个。)我们从属性 gl_Normal
获取物体坐标系中的表面法向量。由于我们在视空间中实现方程,因此我们必须将表面法向量从物体空间转换为视空间,如轮廓增强教程中所述。
然后,顶点着色器如下所示
varying vec4 color;
void main()
{
vec3 normalDirection =
normalize(gl_NormalMatrix * gl_Normal);
vec3 lightDirection =
normalize(vec3(gl_LightSource[0].position));
vec3 diffuseReflection =
vec3(gl_LightSource[0].diffuse)
* vec3(gl_FrontMaterial.emission)
* max(0.0, dot(normalDirection, lightDirection));
color = vec4(diffuseReflection, 1.0);
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}
片段着色器为
varying vec4 color;
void main()
{
gl_FragColor = color;
}
在尝试此着色器时,请确保场景中只有一个光源,该光源必须是方向光,即类型为“太阳”。如果没有光源,可以通过在信息窗口菜单中选择添加 > 灯光 > 太阳来创建方向光源。
对于方向光源,gl_LightSource[0].position
指定光线照射的方向。然而,对于点光源(或聚光灯),gl_LightSource[0].position
指定视空间中光源的位置,我们必须计算光源方向,即视空间中顶点位置到光源位置的差向量。由于点的第 4 个坐标为 1,而方向的第 4 个坐标为 0,因此我们可以轻松区分这两种情况
vec3 lightDirection;
if (0.0 == gl_LightSource[0].position.w)
// directional light?
{
lightDirection =
normalize(vec3(gl_LightSource[0].position));
}
else // point or spot light
{
lightDirection =
normalize(vec3(gl_LightSource[0].position
- gl_ModelViewMatrix * gl_Vertex));
}
虽然方向光源没有光线衰减,但我们应该为点光源和聚光灯添加一些随距离变化的衰减。由于光线从三维空间中的一个点向外扩散,因此它在更远的距离处覆盖着越来越大的虚拟球体。由于这些球体的表面积随着半径的增大而二次增长,而每个球体上的总光量相同,因此每单位面积的光量随着距离点光源的距离的增大而二次减小。因此,我们应该将光源强度除以顶点到光源的距离的平方。
由于二次衰减速度很快,因此我们使用线性衰减,即用距离而不是距离的平方来除以强度。代码可能是
vec3 lightDirection;
float attenuation;
if (0.0 == gl_LightSource[0].position.w)
// directional light?
{
attenuation = 1.0; // no attenuation
lightDirection =
normalize(vec3(gl_LightSource[0].position));
}
else // point or spot light
{
vec3 vertexToLightSource =
vec3(gl_LightSource[0].position
- gl_ModelViewMatrix * gl_Vertex);
float distance = length(vertexToLightSource);
attenuation = 1.0 / distance; // linear attenuation
lightDirection = normalize(vertexToLightSource);
}
通常,我们会将线性衰减函数 1.0 / distance
乘以统一变量 gl_LightSource[0].linearAttenuation
;但是,Blender 似乎没有正确设置此统一变量。事实上,如果 Blender 正确设置了统一变量,我们应该用这种方式计算衰减
attenuation =
1.0 / (gl_LightSource[0].constantAttenuation
+ gl_LightSource[0].linearAttenuation * distance
+ gl_LightSource[0].quadraticAttenuation
* distance * distance);
无论如何,因子 attenuation
应该乘以 gl_LightSource[0].diffuse
来计算入射光;请参见下面的完整着色器代码。请注意,聚光灯具有其他功能,将在下一节中讨论。
还要注意,此代码可能无法提供最佳性能,因为任何 if
通常都很昂贵。由于 gl_LightSource[0].position.w
为 0 或 1,因此实际上并不难重写代码以避免使用 if
并进一步优化
vec3 vertexToLightSource = vec3(gl_LightSource[0].position
- gl_ModelViewMatrix * gl_Vertex
* gl_LightSource[0].position.w);
float one_over_distance =
1.0 / length(vertexToLightSource);
float attenuation = mix(1.0, one_over_distance,
gl_LightSource[0].position.w);
vec3 lightDirection =
vertexToLightSource * one_over_distance;
但是,为了清晰起见,我们将使用包含 if
的版本。(“保持简单,笨蛋!”)
如果 gl_LightSource[0].spotCutoff
小于或等于 90.0,则第 0 个光源是聚光灯(否则应为 180.0)。因此,测试可以是
if (gl_LightSource[0].spotCutoff <= 90.0) // spotlight?
聚光灯的形状由内置统一变量 gl_LightSource[0].spotDirection
、gl_LightSource[0].spotExponent
和 gl_LightSource[0].spotCutoff
描述。具体来说,如果 -lightDirection
和 gl_LightSource[0].spotDirection
之间的角度的余弦小于 gl_LightSource[0].spotCutoff
的余弦,即如果阴影点位于聚光灯方向周围的光锥之外,则衰减因子设置为 。我们可以通过两个归一化向量的点积来计算两个向量之间角度的余弦。gl_LightSource[0].spotCutoff
的余弦实际上是在 gl_LightSource[0].spotCosCutoff
中提供的。如果我们将余弦钳位在 0 以忽略聚光灯后面的点,则测试为
float clampedCosine = max(0.0, dot(-lightDirection,
gl_LightSource[0].spotDirection));
if (clampedCosine < gl_LightSource[0].spotCosCutoff)
// outside of spotlight cone?
{
attenuation = 0.0;
}
否则,如果一个点位于光锥内,则 OpenGL 中的衰减应该用这种方式计算
attenuation = attenuation * pow(clampedCosine,
gl_LightSource[0].spotExponent);
这允许更宽(较小的 spotExponent
)和更窄(较大的 spotExponent
)的聚光灯。
但是,Blender 的内置着色器似乎更类似于
attenuation = attenuation * pow(clampedCosine
- gl_LightSource[0].spotCosCutoff,
gl_LightSource[0].spotExponent / 128.0);
我们将坚持使用 OpenGL 版本。
总而言之,我们新的顶点着色器,用于单个方向光、点光或具有线性衰减的聚光灯,变为
varying vec4 color;
void main()
{
vec3 normalDirection =
normalize(gl_NormalMatrix * gl_Normal);
vec3 lightDirection;
float attenuation;
if (0.0 == gl_LightSource[0].position.w)
// directional light?
{
attenuation = 1.0; // no attenuation
lightDirection =
normalize(vec3(gl_LightSource[0].position));
}
else // point light or spotlight (or other kind of light)
{
vec3 vertexToLightSource =
vec3(gl_LightSource[0].position
- gl_ModelViewMatrix * gl_Vertex);
float distance = length(vertexToLightSource);
attenuation = 1.0 / distance; // linear attenuation
lightDirection = normalize(vertexToLightSource);
if (gl_LightSource[0].spotCutoff <= 90.0) // spotlight?
{
float clampedCosine = max(0.0, dot(-lightDirection,
gl_LightSource[0].spotDirection));
if (clampedCosine < gl_LightSource[0].spotCosCutoff)
// outside of spotlight cone?
{
attenuation = 0.0;
}
else
{
attenuation = attenuation * pow(clampedCosine,
gl_LightSource[0].spotExponent);
}
}
}
vec3 diffuseReflection = attenuation
* vec3(gl_LightSource[0].diffuse)
* vec3(gl_FrontMaterial.emission)
* max(0.0, dot(normalDirection, lightDirection));
color = vec4(diffuseReflection, 1.0);
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}
片段着色器仍然是
varying vec4 color;
void main()
{
gl_FragColor = color;
}
恭喜!您已经了解了很多关于 OpenGL 光源的知识。这对以下关于更高级照明的教程至关重要。具体来说,我们已经看到了
- 什么是漫反射以及如何用数学方法描述它。
- 如何在着色器中为单个方向光源实现漫反射。
- 如何扩展着色器以处理具有线性衰减的点光源。
- 如何进一步扩展着色器以处理聚光灯。
如果您还想了解更多