GLSL 编程/Unity/漫反射
本教程涵盖每个顶点的漫反射。
它是 Unity 中基本光照教程系列的第一篇。在本教程中,我们从单个方向光源的漫反射开始,然后包括点光源和多个光源(使用多遍)。后续教程涵盖了其扩展,特别是镜面反射、每个像素的照明和双面照明。
月球几乎完全表现出漫反射(也称为朗伯反射),即光线向各个方向反射,没有镜面高光。其他此类材料的例子包括粉笔和哑光纸;事实上,任何看起来暗淡和哑光的表面。
在完美漫反射的情况下,观察到的反射光强取决于表面法线向量和入射光线之间的角度的余弦。如左图所示,通常考虑从表面一点开始的归一化向量,在那里应该计算照明:归一化表面法线向量N与表面正交,归一化光线方向L指向光源。
对于观察到的漫反射光,我们需要归一化表面法线向量N和指向光源的归一化方向L之间的角度的余弦,它是点积N·L,因为任何两个向量a和b的点积a·b是
.
对于归一化向量,长度 |a| 和 |b| 都是 1。
如果点积N·L为负,则光源位于表面的“错误”一侧,我们应该将反射设置为 0。这可以通过使用 max(0, N·L) 来实现,这确保了点积的值对于负点积被钳制为 0。此外,反射光取决于入射光强和一个材料常数用于漫反射:对于黑色表面,材料常数为 0,对于白色表面,它为 1。漫反射强度的方程为
对于彩色光,此方程适用于每个颜色分量(例如红色、绿色和蓝色)。因此,如果变量,,和表示颜色向量,乘法是逐分量执行的(它们是 GLSL 中的向量),此方程也适用于彩色光。这是我们在着色器代码中实际使用的。
如果只有一个方向光源,实现 公式的着色器代码相对较小。为了实现该公式,我们遵循在“轮廓增强”部分中讨论的有关实现公式的问题。
- 应该在顶点着色器还是片段着色器中实现该公式?我们这里尝试顶点着色器。在“平滑镜面高光”部分中,我们将探讨在片段着色器中的实现。
- 应该在哪个坐标系中实现该公式?在Unity中,我们默认尝试世界空间。(事实证明这是一个不错的选择,因为Unity在世界空间中提供了光线方向。)
- 我们从哪里获取参数?这个问题的答案有点长。
我们使用着色器属性(参见“在世界空间中着色”)让用户指定漫射材质颜色。我们可以从Unity特定的统一变量_WorldSpaceLightPos0
获取世界空间中的光源方向,以及从Unity特定的统一变量_LightColor0
获取光线颜色。如“在世界空间中着色”部分所述,我们必须用Tags {"LightMode" = "ForwardBase"}
标记着色器通道,以确保这些统一变量具有正确的值。(下面我们将讨论这个标签的实际含义。)我们从属性gl_Normal
获取物体坐标中的表面法线向量。由于我们在世界空间中实现该公式,因此我们必须将表面法线向量从物体空间转换为世界空间,如“轮廓增强”部分所述。
然后着色器代码如下所示。
Shader "GLSL per-vertex diffuse lighting" {
Properties {
_Color ("Diffuse Material Color", Color) = (1,1,1,1)
}
SubShader {
Pass {
Tags { "LightMode" = "ForwardBase" }
// make sure that all uniforms are correctly set
GLSLPROGRAM
uniform vec4 _Color; // shader property specified by users
// The following built-in uniforms (except _LightColor0)
// are also defined in "UnityCG.glslinc",
// i.e. one could #include "UnityCG.glslinc"
uniform mat4 _Object2World; // model matrix
uniform mat4 _World2Object; // inverse model matrix
uniform vec4 _WorldSpaceLightPos0;
// direction to or position of light source
uniform vec4 _LightColor0;
// color of light source (from "Lighting.cginc")
varying vec4 color;
// the diffuse lighting computed in the vertex shader
#ifdef VERTEX
void main()
{
mat4 modelMatrix = _Object2World;
mat4 modelMatrixInverse = _World2Object; // unity_Scale.w
// is unnecessary because we normalize vectors
vec3 normalDirection = normalize(
vec3(vec4(gl_Normal, 0.0) * modelMatrixInverse));
vec3 lightDirection = normalize(
vec3(_WorldSpaceLightPos0));
vec3 diffuseReflection = vec3(_LightColor0) * vec3(_Color)
* max(0.0, dot(normalDirection, lightDirection));
color = vec4(diffuseReflection, 1.0);
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}
#endif
#ifdef FRAGMENT
void main()
{
gl_FragColor = color;
}
#endif
ENDGLSL
}
}
// The definition of a fallback shader should be commented out
// during development:
// Fallback "Diffuse"
}
使用此着色器时,请确保场景中只有一个光源,该光源必须是方向光源。如果没有光源,您可以通过从主菜单中选择游戏对象 > 创建其他 > 方向光来创建方向光源。此外,请确保“前向渲染路径”处于活动状态,方法是选择编辑 > 项目设置 > 播放器,然后在检查器视图中将每平台设置 > 其他设置 > 渲染 > 渲染路径设置为前向。(有关“前向渲染路径”的更多详细信息,请参见下文。)
备用着色器
[edit | edit source]着色器代码中的Fallback "Diffuse"
行定义了一个内置的备用着色器,以防Unity找不到合适的子着色器。对于我们的示例,如果Unity不使用“前向渲染路径”(见下文)或无法编译着色器代码,则它将使用备用着色器。通过为我们的着色器属性选择特定的名称“_Color”,我们确保了这个内置的备用着色器也能访问它。内置着色器的源代码可以在Unity的网站上找到。检查此源代码似乎是确定合适的备用着色器及其使用的属性名称的唯一方法。
如前所述,如果着色器代码中存在编译错误,Unity也将使用备用着色器。在这种情况下,错误只会报告在着色器的检查器视图中;因此,可能难以理解正在使用备用着色器。因此,在开发着色器时,通常最好注释掉备用指令,但在最终版本中将其包含在内以提高兼容性。
用于多个方向(像素)光源的着色器代码
[edit | edit source]到目前为止,我们只考虑了一个光源。为了处理多个光源,Unity会根据渲染和质量设置选择不同的技术。在本文的教程中,我们只介绍“前向渲染路径”。为了选择它,请选择编辑 > 项目设置 > 播放器,然后在检查器视图中将每平台设置 > 其他设置 > 渲染 > 渲染路径设置为前向。(此外,所有摄像机都应配置为使用播放器设置,默认情况下它们就是这样。)
在本教程中,我们只考虑Unity所谓的像素光源。对于第一个像素光源(它始终是方向光源),Unity调用用Tags { "LightMode" = "ForwardBase" }
标记的着色器通道(如我们上面的代码所示)。对于每个额外的像素光源,Unity调用用Tags { "LightMode" = "ForwardAdd" }
标记的着色器通道。为了确保所有光源都被渲染为像素光源,您必须确保质量设置允许足够的像素光源:选择编辑 > 项目设置 > 质量,然后在您使用的任何质量设置中增加标为像素光源数量的数字。如果场景中的光源数量超过了像素光源数量允许的数量,Unity只会将最重要的光源渲染为像素光源。或者,您可以将所有光源的渲染模式设置为重要,以便将它们渲染为像素光源。(有关不太重要的顶点光源的讨论,请参见“多个光源”部分。)
到目前为止,我们的着色器代码对于ForwardBase
通道来说是可以的。对于ForwardAdd
通道,我们需要将反射光添加到已存储在帧缓冲区中的光线。为此,我们只需将混合配置为将新的片段颜色(gl_FragColor
)添加到帧缓冲区中的颜色即可。如“透明度”部分所述,这是通过加性混合方程实现的,该方程由以下行指定
混合 一 一
混合会自动将所有结果钳制在0到1之间;因此,我们不必担心大于1的颜色或alpha值。
总而言之,我们用于多个方向光源的新着色器如下所示。
Shader "GLSL per-vertex diffuse lighting" {
Properties {
_Color ("Diffuse Material Color", Color) = (1,1,1,1)
}
SubShader {
Pass {
Tags { "LightMode" = "ForwardBase" }
// pass for first light source
GLSLPROGRAM
uniform vec4 _Color; // shader property specified by users
// The following built-in uniforms (except _LightColor0)
// are also defined in "UnityCG.glslinc",
// i.e. one could #include "UnityCG.glslinc"
uniform mat4 _Object2World; // model matrix
uniform mat4 _World2Object; // inverse model matrix
uniform vec4 _WorldSpaceLightPos0;
// direction to or position of light source
uniform vec4 _LightColor0;
// color of light source (from "Lighting.cginc")
varying vec4 color;
// the diffuse lighting computed in the vertex shader
#ifdef VERTEX
void main()
{
mat4 modelMatrix = _Object2World;
mat4 modelMatrixInverse = _World2Object; // unity_Scale.w
// is unnecessary because we normalize vectors
vec3 normalDirection = normalize(
vec3(vec4(gl_Normal, 0.0) * modelMatrixInverse));
vec3 lightDirection = normalize(
vec3(_WorldSpaceLightPos0));
vec3 diffuseReflection = vec3(_LightColor0) * vec3(_Color)
* max(0.0, dot(normalDirection, lightDirection));
color = vec4(diffuseReflection, 1.0);
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}
#endif
#ifdef FRAGMENT
void main()
{
gl_FragColor = color;
}
#endif
ENDGLSL
}
Pass {
Tags { "LightMode" = "ForwardAdd" }
// pass for additional light sources
Blend One One // additive blending
GLSLPROGRAM
uniform vec4 _Color; // shader property specified by users
// The following built-in uniforms (except _LightColor0)
// are also defined in "UnityCG.glslinc",
// i.e. one could #include "UnityCG.glslinc"
uniform mat4 _Object2World; // model matrix
uniform mat4 _World2Object; // inverse model matrix
uniform vec4 _WorldSpaceLightPos0;
// direction to or position of light source
uniform vec4 _LightColor0;
// color of light source (from "Lighting.cginc")
varying vec4 color;
// the diffuse lighting computed in the vertex shader
#ifdef VERTEX
void main()
{
mat4 modelMatrix = _Object2World;
mat4 modelMatrixInverse = _World2Object; // unity_Scale.w
// is unnecessary because we normalize vectors
vec3 normalDirection = normalize(
vec3(vec4(gl_Normal, 0.0) * modelMatrixInverse));
vec3 lightDirection = normalize(
vec3(_WorldSpaceLightPos0));
vec3 diffuseReflection = vec3(_LightColor0) * vec3(_Color)
* max(0.0, dot(normalDirection, lightDirection));
color = vec4(diffuseReflection, 1.0);
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}
#endif
#ifdef FRAGMENT
void main()
{
gl_FragColor = color;
}
#endif
ENDGLSL
}
}
// The definition of a fallback shader should be commented out
// during development:
// Fallback "Diffuse"
}
这似乎是一个相当长的着色器;但是,除了ForwardAdd
通道中的标签和Blend
设置之外,两个通道是相同的。
点光源的更改
[edit | edit source]在方向光源的情况下,_WorldSpaceLightPos0
指定光线来自的方向。然而,在点光源(或聚光灯源)的情况下,_WorldSpaceLightPos0
指定光源在世界空间中的位置,我们必须将指向光源的方向计算为从世界空间中顶点的位置到光源位置的差向量。由于点的第4个坐标是1,方向的第4个坐标是0,因此我们可以轻松地区分这两种情况。
vec3 lightDirection;
if (0.0 == _WorldSpaceLightPos0.w) // directional light?
{
lightDirection = normalize(vec3(_WorldSpaceLightPos0));
}
else // point or spot light
{
lightDirection = normalize(
vec3(_WorldSpaceLightPos0 - modelMatrix * gl_Vertex));
}
虽然方向光源没有光线衰减,但我们应该为点光源和聚光灯源添加一些距离衰减。当光线从三维空间中的一个点向外传播时,它在更大的距离处覆盖着越来越大的虚拟球体。由于这些球体的表面积随着半径的增大而二次方增大,而每个球体的总光量相同,因此每单位面积的光量随着距离点光源距离的增大而二次方减小。因此,我们应该将光源的强度除以到顶点的距离的平方。
由于二次衰减速度相当快,因此我们使用线性距离衰减,即我们用距离而不是距离的平方来除以强度。代码可以是
vec3 lightDirection;
float attenuation;
if (0.0 == _WorldSpaceLightPos0.w) // directional light?
{
attenuation = 1.0; // no attenuation
lightDirection = normalize(vec3(_WorldSpaceLightPos0));
}
else // point or spot light
{
vec3 vertexToLightSource = vec3(_WorldSpaceLightPos0
- modelMatrix * gl_Vertex);
float distance = length(vertexToLightSource);
attenuation = 1.0 / distance; // linear attenuation
lightDirection = normalize(vertexToLightSource);
}
然后应该将因子attenuation
乘以_LightColor0
来计算入射光;请参见下面的着色器代码。请注意,聚光灯源具有额外的功能,这些功能超出了本教程的范围。
还要注意,这段代码不太可能为您提供最佳性能,因为任何if
通常都很昂贵。由于_WorldSpaceLightPos0.w
是0或1,实际上重写代码以避免使用if
并进一步优化并不太难。
vec3 vertexToLightSource = vec3(_WorldSpaceLightPos0
- modelMatrix * gl_Vertex * _WorldSpaceLightPos0.w);
float one_over_distance =
1.0 / length(vertexToLightSource);
float attenuation =
mix(1.0, one_over_distance, _WorldSpaceLightPos0.w);
vec3 lightDirection =
vertexToLightSource * one_over_distance;
但是,为了清晰起见,我们将使用包含if
的版本。(“保持简单,愚蠢!”)
多方向光和点光源的完整着色器代码如下:
Shader "GLSL per-vertex diffuse lighting" {
Properties {
_Color ("Diffuse Material Color", Color) = (1,1,1,1)
}
SubShader {
Pass {
Tags { "LightMode" = "ForwardBase" }
// pass for first light source
GLSLPROGRAM
uniform vec4 _Color; // shader property specified by users
// The following built-in uniforms (except _LightColor0)
// are also defined in "UnityCG.glslinc",
// i.e. one could #include "UnityCG.glslinc"
uniform mat4 _Object2World; // model matrix
uniform mat4 _World2Object; // inverse model matrix
uniform vec4 _WorldSpaceLightPos0;
// direction to or position of light source
uniform vec4 _LightColor0;
// color of light source (from "Lighting.cginc")
varying vec4 color;
// the diffuse lighting computed in the vertex shader
#ifdef VERTEX
void main()
{
mat4 modelMatrix = _Object2World;
mat4 modelMatrixInverse = _World2Object; // unity_Scale.w
// is unnecessary because we normalize vectors
vec3 normalDirection = normalize(
vec3(vec4(gl_Normal, 0.0) * modelMatrixInverse));
vec3 lightDirection;
float attenuation;
if (0.0 == _WorldSpaceLightPos0.w) // directional light?
{
attenuation = 1.0; // no attenuation
lightDirection = normalize(vec3(_WorldSpaceLightPos0));
}
else // point or spot light
{
vec3 vertexToLightSource = vec3(_WorldSpaceLightPos0
- modelMatrix * gl_Vertex);
float distance = length(vertexToLightSource);
attenuation = 1.0 / distance; // linear attenuation
lightDirection = normalize(vertexToLightSource);
}
vec3 diffuseReflection =
attenuation * vec3(_LightColor0) * vec3(_Color)
* max(0.0, dot(normalDirection, lightDirection));
color = vec4(diffuseReflection, 1.0);
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}
#endif
#ifdef FRAGMENT
void main()
{
gl_FragColor = color;
}
#endif
ENDGLSL
}
Pass {
Tags { "LightMode" = "ForwardAdd" }
// pass for additional light sources
Blend One One // additive blending
GLSLPROGRAM
uniform vec4 _Color; // shader property specified by users
// The following built-in uniforms (except _LightColor0)
// are also defined in "UnityCG.glslinc",
// i.e. one could #include "UnityCG.glslinc"
uniform mat4 _Object2World; // model matrix
uniform mat4 _World2Object; // inverse model matrix
uniform vec4 _WorldSpaceLightPos0;
// direction to or position of light source
uniform vec4 _LightColor0;
// color of light source (from "Lighting.cginc")
varying vec4 color;
// the diffuse lighting computed in the vertex shader
#ifdef VERTEX
void main()
{
mat4 modelMatrix = _Object2World;
mat4 modelMatrixInverse = _World2Object; // unity_Scale.w
// is unnecessary because we normalize vectors
vec3 normalDirection = normalize(
vec3(vec4(gl_Normal, 0.0) * modelMatrixInverse));
vec3 lightDirection;
float attenuation;
if (0.0 == _WorldSpaceLightPos0.w) // directional light?
{
attenuation = 1.0; // no attenuation
lightDirection = normalize(vec3(_WorldSpaceLightPos0));
}
else // point or spot light
{
vec3 vertexToLightSource = vec3(_WorldSpaceLightPos0
- modelMatrix * gl_Vertex);
float distance = length(vertexToLightSource);
attenuation = 1.0 / distance; // linear attenuation
lightDirection = normalize(vertexToLightSource);
}
vec3 diffuseReflection =
attenuation * vec3(_LightColor0) * vec3(_Color)
* max(0.0, dot(normalDirection, lightDirection));
color = vec4(diffuseReflection, 1.0);
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}
#endif
#ifdef FRAGMENT
void main()
{
gl_FragColor = color;
}
#endif
ENDGLSL
}
}
// The definition of a fallback shader should be commented out
// during development:
// Fallback "Diffuse"
}
请注意,ForwardBase
通道中的光源始终是方向光;因此,第一个通道的代码实际上可以简化。另一方面,对两个通道使用相同的 GLSL 代码,可以更容易地在需要编辑着色器代码的情况下将代码从一个通道复制粘贴到另一个通道。
如果着色器存在问题,请记住通过选择**编辑 > 项目设置 > 播放器**,然后在**检视器视图**中将**每平台设置 > 其他设置 > 渲染 > 渲染路径**设置为**前向**来激活“前向渲染路径”。
聚光灯的更改
[edit | edit source]Unity 使用饼干纹理来实现聚光灯,如“饼干”部分所述;但是,这有点复杂。这里,我们将聚光灯视为点光源。
总结
[edit | edit source]恭喜!您刚刚学习了 Unity 的每像素灯光的工作原理。这对于以下有关更高级照明的教程至关重要。我们还看到了
- 漫反射是什么以及如何用数学方法描述它。
- 如何在着色器中为单个方向光源实现漫反射。
- 如何扩展着色器以支持具有线性衰减的点光源。
- 如何进一步扩展着色器以处理多个每像素灯光。
进一步阅读
[edit | edit source]如果您想了解更多
- 关于将法线向量变换到世界空间,您应该阅读“世界空间中的着色”部分。
- 关于 Unity 提供的统一变量和着色器属性,您应该阅读“世界空间中的着色”部分。
- 关于(叠加)混合,您应该阅读“透明度”部分。
- 关于 Unity 中的通道标签(例如
ForwardBase
或ForwardAdd
),您应该阅读Unity 的 ShaderLab 关于通道标签的参考。 - 关于 Unity 如何一般性地处理光源,您应该阅读Unity 的关于渲染路径的手册。