16.阴影-不透明物体阴影-让不透明物体接受阴影
16.1 知识点
让物体接收阴影的思路
我们已经能够让物体投射阴影了。所谓的投射阴影,其实就是让物体参与到光源的阴影映射纹理计算中,最终才能影响其他物体在接收阴影时的采样结果。
由此可见,让物体接收阴影的主要思路其实就是要从阴影映射纹理中进行采样,然后将采样结果用于最终的颜色计算中。
总体的流程如下:
- 在顶点着色器中进行顶点坐标转换,将顶点坐标转换为阴影映射纹理坐标。
- 在片元着色器中使用阴影映射纹理坐标在阴影映射纹理中进行采样,通过得到的深度值判断片元(像素)是否在阴影中,以计算出阴影衰减值。
- 将采样结果参与到最终的颜色计算中。
实现物体接收阴影效果
主要步骤
- 创建一个新的Shader,复制之前编写的多种光源综合实现Shader
Lesson11_ForwardLighting.shader
,在此基础上实现接收阴影的效果。 - 接受阴影的三剑客(三个宏)。我们只修改Base Pass中的代码来感受下接受阴影的流程。
在Base Pass当中引用包含内置文件:
#include "AutoLight.cginc"
该内置文件中,有用于计算阴影时需要使用的三剑客。
SHADOW_COORDS (阴影坐标宏):该宏在v2f结构体(顶点着色器返回值)中使用,本质上就是声明了一个用于对阴影纹理进行采样的坐标。需要注意的是,在使用时
SHADOW_COORDS(2)
传入参数2,表示需要使用下一个可用的插值寄存器的索引值。TRANSFER_SHADOW (转移阴影宏):该宏在顶点着色器函数中调用,传入对应的v2f结构体对象。该宏会在内部判断应该使用哪种阴影映射技术(SM、SSSM),最终的目的就是将顶点进行坐标转换并存储到
_ShadowCoord
阴影纹理坐标变量中。需要注意:- 该宏会在内部使用顶点着色器中传入的结构体,该结构体中顶点的命名必须是
vertex
。 - 该宏会在内部使用顶点着色器的返回结构体,其中的顶点位置命名必须是
pos
。
- 该宏会在内部使用顶点着色器中传入的结构体,该结构体中顶点的命名必须是
SHADOW_ATTENUATION (阴影衰减宏):该宏在片元着色器中调用,传入对应的v2f结构体对象。该宏会在内部利用v2f中的阴影纹理坐标变量 (
ShadowCoord
) 对相关纹理进行采样,将采样得到的深度值进行比较,以计算出一个fixed3
的阴影衰减值。我们只需要使用它返回的结果和(漫反射 + 高光反射)的结果相乘即可。
创建一个新的Shader,复制之前编写的多种光源综合实现Shader Lesson11_ForwardLighting.shader
,取名为 Lesson16_OpaqueObjectReceiveShadows.shader
,在此基础上实现接收阴影的效果。添加 Fallback "Specular"
,就不用自己处理投射阴影了
Shader "Unlit/Lesson16_OpaqueObjectReceiveShadows"
{
Properties
{
//...
}
SubShader
{
//...
}
//找不到LightMode为ShaderCaster的Pass 使用Specular的shader
Fallback "Specular"
}
注意:本次只修改 Base Pass 中的代码,感受接收阴影的流程。
在BasePass当中引用包含内置文件#include “AutoLight.cginc”,这个文件有计算阴影时要用到的宏
//引用包含该内置文件 其中有计算阴影会用到的三个宏(阴影三剑客)
#include "AutoLight.cginc"
在v2f结构体声明SHADOW_COORDS宏,本质上就是声明了一个用于对阴影纹理进行采样的坐标
//顶点着色器传递给片元着色器的内容
struct v2f
{
//裁剪空间的位置
float4 pos:SV_POSITION;
//基于世界坐标系下的顶点位置
float3 wPos:TEXCOORD0;
//基于世界坐标系下的法线
float3 wNormal:NORMAL;
// 该宏在v2f结构体(顶点着色器返回值)中使用
// 本质上就是声明了一个用于对阴影纹理进行采样的坐标
// 在内部实际上就是声明了一个名为_ShadowCoord的阴影纹理坐标变量
// 需要注意的是:
// 在使用时 SHADOW_COORDS(2) 传入参数2
// 表示需要时下一个可用的插值寄存器的索引值
//阴影坐标宏 主要用于存储阴影纹理坐标
SHADOW_COORDS(2)
};
在顶点函数中声明TRANSFER_SHADOW宏,传入对应的v2f结构体对象。该宏会判断应该使用哪种阴影映射技术(SM、SSSM),将顶点进行坐标转换并存储到_ShadowCoord阴影纹理坐标变量中。注意顶点函数参数结构体中顶点的命名必须是vertex(appdata_base.vertex),顶点函数结构体返回值中顶点位置命名必须是pos(v2f.pos)。
v2f vert(appdata_base appdata_base)
{
v2f v2f;
//转换模型空间下的顶点到裁剪空间中
v2f.pos = UnityObjectToClipPos(appdata_base.vertex);
//转换模型空间下的法线到世界空间下
v2f.wNormal = UnityObjectToWorldNormal(appdata_base.normal);
//转换模型空间下的顶点到世界空间
v2f.wPos = mul(unity_ObjectToWorld, appdata_base.vertex).xyz;
// 该宏在顶点着色器函数中调用,传入对应的v2f结构体对象
// 该宏会在内部自己判断应该使用哪种阴影映射技术(SM、SSSM)
// 最终的目的就是将顶点进行坐标转换并存储到_ShadowCoord阴影纹理坐标变量中
// 需要注意的是:
// 1.该宏会在内部使用顶点着色器中传入的结构体
// 该结构体中顶点的命名必须是vertex
// 2.该宏会在内部使用顶点着色器的返回结构体
// 其中的顶点位置命名必须是pos
//计算阴影映射纹理坐标 它会在内部去进行计算 然后将其存入 v2f中的SHADOW_COORDS中
TRANSFER_SHADOW(v2f);
return v2f;
}
在片元函数中声明SHADOW_ATTENUATION,传入对应的v2f结构体对象。该宏会在内部利用v2f中的 阴影纹理坐标变量(ShadowCoord)对相关纹理进行采样。将采样得到的深度值进行比较,以计算出一个fixed3的阴影衰减值。我们使用返回的阴影衰减值和 (漫反射+高光反射) 的结果相乘。
fixed4 frag(v2f v2f) : SV_Target
{
//计算兰伯特光照颜色
fixed3 lambertColor = getLambertColor(v2f.wNormal);
//计算BlinnPhong式高光反射颜色
fixed3 specularColor = getSpecularColor(v2f.wPos, v2f.wNormal);
//得到阴影衰减值
// 该宏在片元着色器中调用,传入对应的v2f结构体对象
// 该宏会在内部利用v2f中的 阴影纹理坐标变量(ShadowCoord)对相关纹理进行采样
// 将采样得到的深度值进行比较,以计算出一个fixed3的阴影衰减值
// 我们只需要使用它返回的结果和 (漫反射+高光反射) 的结果相乘即可
fixed3 shadow = SHADOW_ATTENUATION(v2f);
//衰减值
//因为我们这是基础Pass 一般是处理场景中最亮的平行光 所以可以认为不存在衰减值的 衰减值默认是1
//但是之后附加Pass的衰减值就会变化了
fixed atten = 1;
//物体表面光照颜色 = 环境光颜色 + 兰伯特光照模型所得颜色 + 布林Phong式高光反射光照模型所得颜色
//衰减值 会和 漫反射颜色 + 高光反射颜色 后 再进行乘法运算
fixed3 blinnPhongColor = UNITY_LIGHTMODEL_AMBIENT.rgb + (lambertColor + specularColor) * atten*shadow;
return fixed4(blinnPhongColor.rgb, 1);
}
注意:目前处理的方式只是让大家大致了解接受阴影的流程,还没有对 Additional Pass 附加渲染通道进行处理,在下节课中统一处理光照衰减和阴影得到最终效果
创建材质赋值给cube,和Lesson15投放阴影进对比。可以看到Lesson16是可以接受阴影的,但是效果有点怪怪的,因为没有处理AdditionalPass
16.2 知识点代码
Lesson16_阴影_不透明物体阴影_让不透明物体接收阴影.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Lesson16_阴影_不透明物体阴影_让不透明物体接收阴影 : MonoBehaviour
{
void Start()
{
#region 知识点一 让物体接收阴影的思路
//上节课我们已经能够让物体投射阴影了
//所谓的投射阴影,其实就是让物体参与到光源的阴影映射纹理计算中
//最终才能影响其他物体在接收阴影时的采样结果
//由此可见让物体接收阴影的主要思路
//其实就是要从阴影隐射纹理中进行采样
//然后将采样结果用于最终的颜色计算中
//总体的流程就是:
//1.在顶点着色器中进行顶点坐标转换(将顶点坐标 转换为 阴影映射纹理坐标)
//2.在片元着色器中使用阴影映射纹理坐标在阴影映射纹理中进行采样
// 通过得到的深度值判断片元(像素)是否在阴影中,以计算出阴影衰减值
//3.将采样结果参与到最终的颜色计算中
#endregion
#region 知识点二 实现物体接收阴影效果
//1.创建一个新的Shader,复制之前编写的多种光源综合实现Shader Lesson11_ForwardLighting.shader
// 在此基础上实现接受阴影的效果
//2.接受阴影的三剑客(三个宏)
// 我们这节课只修改Bass Pass中的代码
// 来感受下接受阴影的流程
// 首先我们需要在Base Pass当中引用包含内置文件
// #include "AutoLight.cginc"
// 该内置文件中,有用于计算阴影时需要使用的三剑客
// 2-1.SHADOW_COORDS (阴影坐标宏)
// 该宏在v2f结构体(顶点着色器返回值)中使用
// 本质上就是声明了一个用于对阴影纹理进行采样的坐标
// 在内部实际上就是声明了一个名为_ShadowCoord的阴影纹理坐标变量
// 需要注意的是:
// 在使用时 SHADOW_COORDS(2) 传入参数2
// 表示需要时下一个可用的插值寄存器的索引值
// 2-2.TRANSFER_SHADOW (转移阴影宏)
// 该宏在顶点着色器函数中调用,传入对应的v2f结构体对象
// 该宏会在内部自己判断应该使用哪种阴影映射技术(SM、SSSM)
// 最终的目的就是将顶点进行坐标转换并存储到_ShadowCoord阴影纹理坐标变量中
// 需要注意的是:
// 1.该宏会在内部使用顶点着色器中传入的结构体
// 该结构体中顶点的命名必须是vertex
// 2.该宏会在内部使用顶点着色器的返回结构体
// 其中的顶点位置命名必须是pos
// 2-3.SHADOW_ATTENUATION (阴影衰减宏)
// 该宏在片元着色器中调用,传入对应的v2f结构体对象
// 该宏会在内部利用v2f中的 阴影纹理坐标变量(ShadowCoord)对相关纹理进行采样
// 将采样得到的深度值进行比较,以计算出一个fixed3的阴影衰减值
// 我们只需要使用它返回的结果和 (漫反射+高光反射) 的结果相乘即可
//3.注意
// 我们目前处理的方式只是让大家大致了解接受阴影的流程
// 我们还没有对 Additional Pass 附加渲染通道进行处理
// 我们将在下节课中统一处理光照衰减和阴影 得到最终效果
#endregion
}
}
Lesson16_OpaqueObjectReceiveShadows.shader
Shader "Unlit/Lesson16_OpaqueObjectReceiveShadows"
{
Properties
{
//材质的漫反射光照颜色
_MainColor("MainColor", Color) = (1,1,1,1)
//高光反射颜色
_SpecularColor("SpecularColor", Color) = (1,1,1,1)
//光泽度
_SpecularNum("SpecularNum", Range(0, 20)) = 0.5
}
SubShader
{
//Bass Pass 基础渲染通道
Pass
{
//设置我们的光照模式 ForwardBase这种向前渲染模式 主要是用来处理 不透明物体的 光照渲染的
Tags
{
"LightMode"="ForwardBase"
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
//用于帮助我们编译所有变体 并且保证衰减相关光照变量能够正确赋值到对应的内置变量中
#pragma multi_compile_fwdbase
//引用对应的内置文件
//主要是为了之后 的 比如内置结构体使用,内置变量使用
#include "UnityCG.cginc"
#include "Lighting.cginc"
//引用包含该内置文件 其中有计算阴影会用到的三个宏(阴影三剑客)
#include "AutoLight.cginc"
//材质的漫反射颜色
fixed4 _MainColor;
//对应属性当中的颜色和光泽度
fixed4 _SpecularColor;
float _SpecularNum;
//顶点着色器传递给片元着色器的内容
struct v2f
{
//裁剪空间的位置
float4 pos:SV_POSITION;
//基于世界坐标系下的顶点位置
float3 wPos:TEXCOORD0;
//基于世界坐标系下的法线
float3 wNormal:NORMAL;
// 该宏在v2f结构体(顶点着色器返回值)中使用
// 本质上就是声明了一个用于对阴影纹理进行采样的坐标
// 在内部实际上就是声明了一个名为_ShadowCoord的阴影纹理坐标变量
// 需要注意的是:
// 在使用时 SHADOW_COORDS(2) 传入参数2
// 表示需要时下一个可用的插值寄存器的索引值
//阴影坐标宏 主要用于存储阴影纹理坐标
SHADOW_COORDS(2)
};
//得到兰伯特光照模型计算的颜色 (逐片元)
fixed3 getLambertColor(in float3 wNormal)
{
//_WorldSpaceLightPos0.xyz 世界坐标系下光源方向 _WorldSpaceLightPos0是四维向量 只需要xyz即可
//normalize 把向量归一化
//lightDir 得到光源单位向量
float3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
//_LightColor0光 照颜色
//.rgb 代表只使用颜色进行计算 透明度不考虑
//color 计算出了兰伯特光照的漫反射颜色
fixed3 color = _LightColor0.rgb * _MainColor.rgb * max(0, dot(wNormal, lightDir));
return color;
}
//得到Blinn Phong式高光反射模型计算的颜色(逐片元)
fixed3 getSpecularColor(in float3 wPos, in float3 wNormal)
{
//1.视角单位向量
//用观察者的位置(摄像机的位置)减去世界空间下顶点坐标 得到的就是视角方向 并且进行单位化
float3 viewDir = normalize(_WorldSpaceCameraPos.xyz - wPos);
//2.光的反射单位向量
//光的方向
float3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
//半角方向向量
float3 halfA = normalize(viewDir + lightDir);
//color = 光源颜色 * 材质高光反射颜色 * pow( max(0, dot(视角单位向量, 光的反射单位向量)), 光泽度 )
fixed3 color = _LightColor0.rgb * _SpecularColor.rgb * pow(max(0, dot(wNormal, halfA)), _SpecularNum);
return color;
}
v2f vert(appdata_base appdata_base)
{
v2f v2f;
//转换模型空间下的顶点到裁剪空间中
v2f.pos = UnityObjectToClipPos(appdata_base.vertex);
//转换模型空间下的法线到世界空间下
v2f.wNormal = UnityObjectToWorldNormal(appdata_base.normal);
//转换模型空间下的顶点到世界空间
v2f.wPos = mul(unity_ObjectToWorld, appdata_base.vertex).xyz;
// 该宏在顶点着色器函数中调用,传入对应的v2f结构体对象
// 该宏会在内部自己判断应该使用哪种阴影映射技术(SM、SSSM)
// 最终的目的就是将顶点进行坐标转换并存储到_ShadowCoord阴影纹理坐标变量中
// 需要注意的是:
// 1.该宏会在内部使用顶点着色器中传入的结构体
// 该结构体中顶点的命名必须是vertex
// 2.该宏会在内部使用顶点着色器的返回结构体
// 其中的顶点位置命名必须是pos
//计算阴影映射纹理坐标 它会在内部去进行计算 然后将其存入 v2f中的SHADOW_COORDS中
TRANSFER_SHADOW(v2f);
return v2f;
}
fixed4 frag(v2f v2f) : SV_Target
{
//计算兰伯特光照颜色
fixed3 lambertColor = getLambertColor(v2f.wNormal);
//计算BlinnPhong式高光反射颜色
fixed3 specularColor = getSpecularColor(v2f.wPos, v2f.wNormal);
//得到阴影衰减值
// 该宏在片元着色器中调用,传入对应的v2f结构体对象
// 该宏会在内部利用v2f中的 阴影纹理坐标变量(ShadowCoord)对相关纹理进行采样
// 将采样得到的深度值进行比较,以计算出一个fixed3的阴影衰减值
// 我们只需要使用它返回的结果和 (漫反射+高光反射) 的结果相乘即可
fixed3 shadow = SHADOW_ATTENUATION(v2f);
//衰减值
//因为我们这是基础Pass 一般是处理场景中最亮的平行光 所以可以认为不存在衰减值的 衰减值默认是1
//但是之后附加Pass的衰减值就会变化了
fixed atten = 1;
//物体表面光照颜色 = 环境光颜色 + 兰伯特光照模型所得颜色 + 布林Phong式高光反射光照模型所得颜色
//衰减值 会和 漫反射颜色 + 高光反射颜色 后 再进行乘法运算
fixed3 blinnPhongColor = UNITY_LIGHTMODEL_AMBIENT.rgb + (lambertColor + specularColor) * atten*shadow;
return fixed4(blinnPhongColor.rgb, 1);
}
ENDCG
}
//Additional Pass 附加渲染通道
Pass
{
//设置我们的光照模式 ForwardAdd这种向前渲染模式 主要是用来处理 附加光照渲染的
Tags
{
"LightMode"="ForwardAdd"
}
//线性减淡的效果 进行 光照颜色混合
Blend One One
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
//用于帮助我们编译所有变体 并且保证衰减相关光照变量能够正确赋值到对应的内置变量中
#pragma multi_compile_fwdadd
//引用对应的内置文件
//主要是为了之后 的 比如内置结构体使用,内置变量使用
#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"
//材质的漫反射颜色
fixed4 _MainColor;
//对应属性当中的颜色和光泽度
fixed4 _SpecularColor;
float _SpecularNum;
//顶点着色器传递给片元着色器的内容
struct v2f
{
//裁剪空间的位置
float4 pos:SV_POSITION;
//基于世界坐标系下的顶点位置
float3 wPos:TEXCOORD0;
//基于世界坐标系下的法线
float3 wNormal:NORMAL;
};
v2f vert(appdata_base appdata_base)
{
v2f v2f;
//转换模型空间下的顶点到裁剪空间中
v2f.pos = UnityObjectToClipPos(appdata_base.vertex);
//转换模型空间下的法线到世界空间下
v2f.wNormal = UnityObjectToWorldNormal(appdata_base.normal);
//转换模型空间下的顶点到世界空间
v2f.wPos = mul(unity_ObjectToWorld, appdata_base.vertex).xyz;
return v2f;
}
fixed4 frag (v2f v2f) : SV_Target
{
//兰伯特漫反射
//公式:漫反射颜色 = 光颜色 * 属性中颜色 * max(0, dot(世界坐标系下的法线, 世界坐标系下的光方向));
//世界标准法线
fixed3 worldNormal = normalize(v2f.wNormal);
#if defined(_DIRECTIONAL_LIGHT)
//平行光 光的方向 其实就是它的位置
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
#else
//点光源和聚光灯 光的方向 是 光的位置 - 顶点位置
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz - v2f.wPos);
#endif
// 漫反射颜色 = 光颜色 * 属性中颜色 * max(0, dot(世界坐标系下的法线, 世界坐标系下的光方向));
fixed3 diffuse = _LightColor0.rgb * _MainColor.rgb * max(0, dot(worldNormal, worldLightDir));
//BlinnPhong高光反射
//公式:高光颜色 = 光颜色 * 属性中的高光颜色 * pow(max(0, dot(世界坐标系法线, 世界坐标系半角向量)), 光泽度);
//视角方向 摄像机位置 - 顶点位置
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - v2f.wPos.xyz);
//半角方向向量 光方向 + 视角方向 平行四边形法则
fixed3 halfDir = normalize(worldLightDir + viewDir);
// 高光颜色 = 光颜色 * 属性中的高光颜色 * pow(max(0, dot(世界坐标系法线, 世界坐标系半角向量)), 光泽度);
fixed3 specular = _LightColor0.rgb * _SpecularColor.rgb * pow(max(0, dot(worldNormal, halfDir)), _SpecularNum);
//衰减值
#ifdef USING_DIRECTIONAL_LIGHT
//平行光
fixed atten = 1;
#else
#if defined(POINT)
//点光源
//将世界坐标系下顶点转到光源空间下
float3 lightCoord = mul(unity_WorldToLight, float4(v2f.wPos, 1)).xyz;
//利用这个坐标得到距离的平方 然后再再光源纹理中映射得到衰减值
fixed atten = tex2D(_LightTexture0, dot(lightCoord,lightCoord).xx).UNITY_ATTEN_CHANNEL;
#elif defined(SPOT)
//聚光灯
//将世界坐标系下顶点转到光源空间下 聚光灯需要用w参与后续计算
float4 lightCoord = mul(unity_WorldToLight, float4(v2f.wPos, 1));
fixed atten = (lightCoord.z > 0) * //判断在聚光灯前面吗
tex2D(_LightTexture0, lightCoord.xy / lightCoord.w + 0.5).w * //映射到大图中进行采样
tex2D(_LightTextureB0, dot(lightCoord,lightCoord).xx).UNITY_ATTEN_CHANNEL; //距离的平方采样
#else
fixed atten = 1;
#endif
#endif
// 衰减值 不准确的判断
// #if defined(_DIRECTIONAL_LIGHT)
// //平行光
// fixed atten = 1;
// #elif defined(_POINT_LIGHT)
// //点光源
// //将世界坐标系下顶点转到光源空间下
// float3 lightCoord = mul(unity_WorldToLight, float4(v2f.wPos, 1)).xyz;
// //利用这个坐标得到距离的平方 然后再再光源纹理中映射得到衰减值
// fixed atten = tex2D(_LightTexture0, dot(lightCoord,lightCoord).xx).UNITY_ATTEN_CHANNEL;
// #elif defined(_SPOT_LIGHT)
// //聚光灯
// //将世界坐标系下顶点转到光源空间下 聚光灯需要用w参与后续计算
// float4 lightCoord = mul(unity_WorldToLight, float4(v2f.wPos, 1));
// fixed atten = (lightCoord.z > 0) * //判断在聚光灯前面吗
// tex2D(_LightTexture0, lightCoord.xy / lightCoord.w + 0.5).w * //映射到大图中进行采样
// tex2D(_LightTextureB0, dot(lightCoord,lightCoord).xx).UNITY_ATTEN_CHANNEL; //距离的平方采样
// #else
// fixed atten = 1;
// #endif
//在附加渲染通道中不需要在加上环境光UNITY_LIGHTMODEL_AMBIENT.rgb颜色了
//因为它只需要计算一次 在基础渲染通道中已经计算了
return fixed4((diffuse + specular)*atten, 1);
}
ENDCG
}
}
//找不到LightMode为ShaderCaster的Pass 使用Specular的shader
Fallback "Specular"
}
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com