这是一篇学习笔记,原文地址:https://catlikecoding.com/unity/tutorials/custom-srp/directional-lights/
本节开始书写支持光照的 shader。
利用法线计算光照
支持最多4个方向光源
应用BRDF
支持半透明材质
定制shader GUI
1. Lighting
光照是模拟光线与表面的交互的计算过程。
1.1 Lit Shader
从 UnlitPass.hlsl 复制一份,改名为 LitPass.hlsl。修改 include 保护 和 vertex fragment 函数名:
#ifndef CUSTOM_LIT_PASS_INCLUDED
#define CUSTOM_LIT_PASS_INCLUDED
…
Varyings LitPassVertex (Attributes input) { … }
float4 LitPassFragment (Varyings input) : SV_TARGET { … }
#endif
同样的,复制 Unlit.shader 并改名为 Lit.shader,并将文件内定义的顶点/像素着色程序,改为上面定义的函数.
Shader "Custom RP/Lit"
{
Properties { ... }
SubShader
{
Pass
{
...
#pragma vertex LitPassVertex
#pragma fragment LitPassFragment
#include "LitPass.hlsl"
...
}
}
}
我们要定义自己的光照方式,并在 Pass 中定义 light mode 为 CustomLit 来作为标识:
Pass {
Tags { "LightMode" = "CustomLit" }
...
}
在 CameraRenderer 中,定义 shader 标识,并通过 DrawingSettings 进行配置:
...
static ShaderTagId unlitShaderTagId = new ShaderTagId("SRPDefaultUnlit");
static ShaderTagId litShaderTagId = new ShaderTagId("CustomLit");
...
void DrawVisibleGeometry(bool useDynamicBatching, bool useGPUInstancing)
{
// 渲染不透明物体
var sortingSettings = new SortingSettings(camera) { criteria = SortingCriteria.CommonOpaque };
var drawingSettings = new DrawingSettings(unlitShaderTagId, sortingSettings)
{ enableDynamicBatching = useDynamicBatching, enableInstancing = useGPUInstancing };
// 索引是 1,因为索引为 0 的通过构造函数将 unlitShaderTagId 设置了
drawingSettings.SetShaderPassName(1, litShaderTagId);
...
}
现在,可以创建 Lit 材质了
1.2 Normal Vectors
通过法线,定义光的入射方向和表面的相对关系,并基于该相对关系计算光照,因此法线十分重要。
首先为顶点着色器输入定义法线属性,同时光照是在像素着色器中计算的,因此需要传递给像素着色器:
struct Attributes
{
float3 positionOS : POSITION;
float3 normalOS : NORMAL; // 法线
float2 uv : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct Varyings
{
float4 positionCS : SV_POSITION;
float3 normalWS : VAR_NORMAL; // 法线
float2 uv : TEXTCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
因为光照是在世界空间中计算的,因此在顶点着色器中,将顶点法线从模型空间变换到世界空间。
Varyings LitPassVertex(Attributes input)
{
Varyings output;
...
output.normalWS = TransformObjectToWorldNormal(input.normalOS);
return output;
}
float4 LitPassFragment(Varyings input) : SV_TARGET
{
return float4(input.normalWS, 1.0f);
}
在像素着色器中,我们先把法线作为颜色输出,看看是什么效果:
法线的分量有可能是负的,这部分值会被夹取为0.黑色的部分表示全是负的或0.
1.3 法线插值
虽然顶点法线都是归一化的,但是在顶点着色器中变换到世界空间,通过三角形进行线性插值后,它们就不再是归一化的了,通过修改像素着色器,我们可以看到效果:
因此,在像素着色器中使用前,要重新进行归一化:
float3 normalWS = normalize(input.normalWS);
1.4 Surface Properties
光照是模拟光线跟它击中的表面的相互作用,因此表面属性十分重要,常用。同时为了代码整洁,我们把表面属性作为一个结构体定义在 Surface.hlsl 中:
#ifndef UNITY_SURFACE_INCLUDED
#define UNITY_SURFACE_INCLUDED
struct Surface
{
float3 normal;
float3 color;
float alpha;
};
#endif
并在 LitPass.hlsl 中,在 Common.hlsl 之前包含它。
#include "../ShaderLibrary/Surface.hlsl"
#include "../ShaderLibrary/Common.hlsl"
在像素着色器中定义 Surface 类型的变量,并进行填充:
float4 LitPassFragment(Varyings input) : SV_TARGET
{
...
Surface surface;
surface.normal = normalize(input.normalWS);
surface.color = base.rgb;
surface.alpha = base.a;
return float4(surface.color, surface.alpha);
}
1.5 Calculating Lighting
我们创建一个计算光照的函数 GetLighting,并且以 Surface 作为参数.将这个函数放在 Lighting.hlsl 中,后面光照相关的函数,都放在该文件中.
#ifndef CUSTOM_LIGHTING_INCLUDED
#define CUSTOM_LIGHTING_INCLUDED
float3 GetLighting(Surface surface) {
return surface.normal.y;
}
#endif
在 LitPass.hlsl 中,Surface.hlsl 之后,包含该文件.然后在像素着色器中调用该函数
#include "../ShaderLibrary/Surface.hlsl"
#include "../ShaderLibrary/Lighting.hlsl"
...
float4 LitPassFragment(Varyings input) :SV_TARGET
{
...
float3 color = GetLighting(surface);
return float4(color, surface.alpha);
}
效果是这样的:
我们把表面法线的 Y 当作光照颜色返回了,因此在顶部 Y 是 1,然后向下到中间靠下时变成0,到底部最终变成 -1 . 小于 0 的值会被限制为0,因此我们看不到负值的颜色.这恰好匹配法线和 up vector 夹角的余弦.忽略负值的部分,看起来是垂直向下的方向光的效果.
表面的颜色也叫 Albedo ,我们将其应用到光照运算上:
float3 GetLighting(Surface surface) {
return surface.normal.y * surface.color;
}
2. Lights
要执行正确的光照运算,我们需要知道光的属性.当前教程中,我们仅实现方向光.
方向光表示的光源距离足够远,仅需要关心其方向,而忽略其位置.这种简化对于像照射到地球上的太阳/月光,以及那些单向(不发散)光来说足够了.
2.1 Light Structure
我们用一个包含颜色和方向的结构体来存储光源数据,并将其放入 Light.hlsl 中.将来其它类型的光源也会定义在该文件中.
同时定义函数,返回我们临时硬编码的方向光.注意我们的光源方向是指向光源的,而不是光前进的方向.
#ifndef LIGHT_INCLUDED
#define LIGHT_INCLUDED
struct Light{
float3 color;
float3 direction;
}
Light GetDirectionalLight(){
Light light;
light.corlor = 1.0f;
light.direction = float3(0.0, 1.0, 0.0);
return light;
}
#endif
在 Lit.hlsl 中,Lighting.hlsl 之前包含该文件
#include "../ShaderLibrary/Light.hlsl"
#include "../ShaderLibrary/Lighting.hlsl"
2.2 Lighting Function
在 Lighting.hlsl 中定义光照计算函数,根据给定的表面和光源计算光照.我们用表面法线和光源方向的点积,乘以光的颜色来计算光照.
如果点积为负,表示表面方向与光的方向(指向光源)相反,不被照亮,因此通过 satureate 函数钳制为0.
float3 IncomingLight(Surface surface, Light light){
return saturate(dot(surface.normal, light.direction)) * light.color;
}
完善光照函数应用我们刚刚定义的计算函数.同时提供一个仅以表面为参数的光照函数:
float3 GetLighting(Surface surface, Light light){
return IncomingLight(surface, light);
}
float3 GetLighting(Surface surface){
return GetLighting(surface, GetDirectionalLight());
}
2.3 Sending Light Data to the GPU
运行时,光源在 CPU 端维持,并更新.需要在渲染时将光源数据上传到 GPU.
我们的管线最多支持4个方向光,因此需要定义常量.
首先在 Light.hlsl 中定义光源的 constant buffer
#define MAX_DIRECTIONAL_LIGHT_COUNT 4
CBUFFER_START(_CustomLights)
int _DirectionalLightCount;
float4 _DirectionalLightColors[MAX_DIRECTIONAL_LIGHT_COUNT];
float4 _DirectionalLightDirections[MAX_DIRECTIONAL_LIGHT_COUNT];
CBUFFER_END
int GetDirectionalLightCount(){
return _DirectionalLightCount;
}
Light GetDirectionalLight(int index){
Light light;
light.color = _DirectionalLightColors[index].rgb;
light.direction = _DirectionalLightDirections[index].xyz;
return light;
}
然后在光照函数中遍历所有光源,计算光照,并累加他们:
float3 GetLighting(Surface surface){
float3 color = 0;
for(int i = 0; i < GetDirectionalLightCount(); ++i){
color += GetLighting(surface, GetDirectionalLight(i));
}
return color;
}
着色器中的光源数据来源于 CPU,需要我们上传到GPU. 我们把这部分逻辑独立到 Lighting.cs 中.
同样定义最大方向光数量为4
缓存 shader 常量 ID
记录方向光数据
创建 CommandBuffer 用来上传数据
const int MaxDirLightCount = 4;
static int dirLightCountId = Shader.PropertyToID("_DirectionalLightCount");
static int dirLightColorsId = Shader.PropertyToID("_DirectionalLightColors");
static int dirLightDirectionsId = Shader.PropertyToID("_DirectionalLightDirections");
static Vector4[] dirLightColors = new Vector4[MaxDirLightCount];
static Vector4[] dirLightDirections = new Vector4[MaxDirLightCount];
const string bufName = "Lighting";
CommandBuffer cmdBuf = new CommandBuffer(){ name = bufName };
光的颜色由 VisibleLight.finalColor 提供,它其实是由 light.color.linear * light.intensity 计算的来的.不过要使用 finalColor,需要在管线实例的构造函数中启用:
public CustomPipeline(...){
...
GraphicsSettings.lightsUseLinearIntensity = true;
}
定义函数,将方向光数据缓存下来.为了避免参数传递导致的内存开销,我们将参数定义为 ref
// VisibleLight 是摄像机裁剪得到的光源
void SetupDirectionalLight(int index, ref VisibleLight light){
dirLightColors[index] = light.finalColor;
dirLightDirections[index] = -light.localToWorldMatrix.GetColumn(2);
}
定义接口函数,完成光源数据的上传.摄像机可见的光源同样在裁剪结果中,因此需要参数将 CullingResults 传进来.
光源数据混存好后,上传到 GPU
public void Setup(ScriptableRenderContext context, CullingResults cullingResults){
Native<VisibleLight> visibleLights = cullingResults.visibleLights;
int dirLightCount = 0;
for(int i = 0; i < visibleLights.Length; ++i){
VisibleLight light = visibleLights[i];
if(light.lightType == LightType.Directional
&& dirLightCount < MaxDirLightCount) {
SetupDirectionalLight(dirLightCount, ref light);
dirLightCount++;
}
}
cmdBuf.SetGlobalInt(dirLightCountId, dirLightCount);
cmdBuf.SetGlobalVectorArray(dirLightColorsId, dirLightColors);
cmdBuf.SetGlobalVectorArray(dirLightDirectionsId, dirLightDirections);
}
下面的场景中创建了2个方向光,看看效果:
2.4 Shader Target Level
hlsl 代码中的循环,曾经代表低效代码,但是现代 GPU 都能够解决这个问题,尤其是当一个 draw call 的所有 fragment 都是以同样的循环次数计算时更不会成为问题。但是 OpenGL ES 2.0 和 WebGL 1.0 图形 API 不能高效的处理循环,因此我们的管线不对它们支持。
在 Lit.shader 中,加入目标版本定义来实现我们的目的:
HLSLPROGRAM
#pragma target 3.5
...
3. BRDF
前面用到的极简的光照,仅适用于完美的漫反射表面.下面我们引入效果更加真实的BRDF(bidirectional reflectance distribution function 双向反射分布函数).
3.1 incoming light 入射光
当一束光自顶向下照射到像素,这束光的所有能量都会作用到这个像素.简单起见,我们假定光束的宽度等于像素的宽度,那么这种情况下光线方向L和法线方向 N一致,他们的点积 dot(N,L)=1.当他们的方向不一致时,至少有部分光束不会照射到像素,因此作用到像素的能量变少,这部分能量是 dot(N,L).如果点积的结果是负的,意味着光束和表面的方向一定程度上是相反的,因此像素完全不受光束的影响.
3.2 outgoing light 出射光
我们并不能看到到达表面的光,而是看到从表面反射的进入我们眼镜(摄像机)的光.如果表面上一个绝对平面的镜子,那么光束以跟入射角度相同的出射角度,被反射出去,当摄像机方向与反射方向一致时,就能看到这部分光.这就是我们说的高光反射.
当表面不是完美的平面时,光就会散射开,因为表面是由很多微小的表面构成,这些微表面的朝向各不相同,将光线分成方向不同的细小光线.即使观察方向与反射方向不完全一致,也能看到这一散射的光线.
此外,还有部分光线会穿透进物质内部,部分被吸收,部分在内部经过几次反弹后,最终以随机的方向反射出来.如果情况极端一些,所有光线全部进入物质内部并反弹出来,那么就是完全的漫反射.
无论摄像机在哪儿,进入摄像机的漫反射光的量都是一样的.但这意味着我们看到的光远小于照射到表面的光.因此建议通过一个系数对入射光进行缩放.然而由于这个系数是固定的,因此将这个系数直接烘焙(存储到)光的颜色和强度.
3.3 Surface Properties
表面可以是完全漫反射,或完全镜面反射,或介于二者之间.有多种方式来控制这些效果,我们选择其中的金属度模型(metallic),因此需要向 Lit.shader 中加入两个属性:
_Metallic("Metallic", Range(0,1)) = 0
_Smoothness("Smoothness", Range(0,1)) = 0.5
将变量添加的 UnityPerMaterial 中去
UNITY_INSTANCING_BUFFER_START(UnityPerMaterial)
...
float _Metallic;
float _Smoothness;
UNITY_INSTANCING_BUFFER_END(UnityPerMaterial)
添加到 Surface 结构体中
struct Surface
{
float3 normal;
float3 color;
float alpha;
float metallic;
float smoothness;
};
并在 LitPassFragment 函数中,将参数从 instance 数据读取出来,复制到 surface
Surface surface;
surface.normal = normalize(input.normalWS);
surface.color = base.rgb;
surface.alpha = base.a;
surface.metallic = UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _Metallic);
surface.smoothness = UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _Smoothness);
给 PerObjectMaterialProperties.cs 也加入支持这两个属性
[DisallowMultipleComponent]
public class PerObjectMaterialProperties : MonoBehaviour
{
static int baseColorId = Shader.PropertyToID("_BaseColor");
static int metallicId= Shader.PropertyToID("_Metallic");
static int smoothnessId = Shader.PropertyToID("_Smoothness");
[SerializeField]
private Color baseColor = Color.white;
[SerializeField]
private float metallic = 0.0f;
[SerializeField]
private float smoothness = 0.5f;
static MaterialPropertyBlock matPropBlock;
private void Awake()
{
OnValidate();
}
private void OnValidate()
{
if (matPropBlock == null)
matPropBlock = new MaterialPropertyBlock();
matPropBlock.SetColor(baseColorId, baseColor);
matPropBlock.SetFloat(metallicId, metallic);
matPropBlock.SetFloat(smoothnessId, smoothness);
GetComponent<Renderer>().SetPropertyBlock(matPropBlock);
}
}
3.4 BRDF Properties
通过表面属性计算 BRDF 公式,获得从表面反射出来的光,反射光由漫反射和高光反射组成。我们需要把表面颜色分成漫反射和高光反射两部分,同时需要知道表面的粗糙度。将这些数据定义到文件 BRDF.hlsl 中的 BRDF 结构体中
然后定义一个获取 BRDF 的函数。我们从完全漫反射开始,因此漫反射等于颜色值,而高光反射为0,粗糙度为1
#ifndef BRDF_INCLUDED
#define BRDF_INCLUDED
struct BRDF
{
float3 specular;
float3 diffuse;
float roughness;
};
BRDF GetBRDF(Surface surface)
{
BRDF brdf;
brdf.diffuse = surface.color;
brdf.specular = 0;
brdf.roughness = 1;
}
#endif
将 BRDF.hlsl 包含进 LitPass.hlsl
#include "../ShaderLibrary/Light.hlsl"
#include "../ShaderLibrary/BRDF.hlsl"
#include "../ShaderLibrary/Lighting.hlsl"
3.5 Reflectivity 反射率
表面的反射程度都是不同的,但是总的来说,金属通过高光反射反射所有光,所以漫反射为0.因此我们定义反射率等于表面的金属度。反射出去的光当然就不会再参与漫反射了,所以在 GetBRDF 中,漫反射的光用 1-metallic 进行缩放
现实中,绝缘体的表面也会反射一些光,带来一点高光。非金属的反射率各不相同,但平均值大概是 0.04,因此定义最小反射率为 0.04
#define MIN_REFLECTVITY 0.04
float OneMinusReflectivity(float metallic)
{
float range = 1.0 - MIN_REFLECTVITY;
return range - range * metallic;
}
BRDF GetBRDF(Surface surface)
{
BRDF brdf;
brdf.diffuse = surface.color * OneMinusReflectivity(surface.metallic);
brdf.specular = 0;
brdf.roughness = 1;
return brdf;
}

3.6 Specular Color 高光颜色
在高光反射+漫反射中,光通过一种方式进行反射,就不能再参与另一种反射了,这叫能量守恒,也就是说反射光不能超出入射光。所以高光颜色等于 surface color - diffuse color。
同时,由于金属会影响高光反射的颜色,而非金属不会,且绝缘体的高光颜色是白色。通过利用金属度,在 MIN_REFLECTVITY 和surface.color 之间插值来达到这个效果。
BRDF GetBRDF(Surface surface)
{
BRDF brdf;
brdf.diffuse = surface.color * OneMinusReflectivity(surface.metallic);
brdf.specular = lerp(MIN_REFLECTIVITY, surface.color, surface.metallic);
brdf.roughness = 1;
return brdf;
}
3.7 Roughness 粗糙度
粗糙度正好与光滑度相反,因此简单通过1-smoothness 可以得到粗糙度。Core RP Library 中有实现该功能的函数 PerceptualSmoothnessToRoughness,我们直接使用该函数,以表明平滑度以及由此产生的粗糙度都是基于感知的。该函数符合迪士尼的光照模型,且使用该函数进行转换,在编辑材质时更符合直觉。
BRDF GetBRDF(Surface surface)
{
BRDF brdf;
brdf.diffuse = surface.color * OneMinusReflectivity(surface.metallic);
brdf.specular = lerp(MIN_REFLECTIVITY, surface.color, surface.metallic);
float perceptualRoughness = PerceptualSmoothnessToPerceptualRoughness(surface.smoothness);
brdf.roughness = PerceptualRoughnessToRoughness(perceptualRoughness);
return brdf;
}
两个转换函数定义在 CommonMaterial.hlsl 中,因此需要在我们的 Common.hlsl 中包含它
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/CommonMaterial.hlsl"
3.8 View Direction 观察方向
计算观察方向需要知道摄像机位置,在 UnityInput.hlsl 中添加变量 float3 _WorldSpaceCameraPos, unity 渲染时会帮我们将该变量准备好.
float3 _WorldSpaceCameraPos
在着色器中计算观察方向,还需要将顶点的世界坐标传入像素着色器
struct Varyings
{
float4 positionCS : SV_POSITION;
float3 positionWS : VAR_POSITION;
...
};
Varyings LitPassVertex(Attributes input)
{
Varyings output;
...
output.positionWS = TransformObjectToWorld(input.positionOS);
output.positionCS = TransformWorldToHClip(output.positionWS);
...
return output;
}
在 LitPassFragment 中计算观察方向,并将其存储到 Surface 中去
Surface surface;
surface.normal = normalize(input.normalWS);
surface.viewDirection = normalize(_WorldSpaceCameraPos - input.positionWS);
surface.color = base.rgb;
3.9 Specular Strength 高光强度
高光反射到强度,跟观察方向和出射方向的一致性有关. URP 中使用了 Minimalist CookTorrance BRDF 的一种形式,我们也使用该公式.该公式多次用到了平方公式,因此在 common.hlsl 中定义给函数
float Square(float value)
{
return value * value;
}
然后在 BRDF.hlsl 中实现 SpecularStrength 函数
r 是粗糙度
所有点积都要 saturate
N 是表面法线
L 光线方向
H = normalize(N+L), 是 N L 的半角
n = 4r + 2 用来规范化.
// 计算高光强度
float SpecularStrength(Surface surface, BRDF brdf, Light lihgt)
{
float r2 = Square(brdf.roughness);
float3 h = normalize(surface.normal + surface.viewDirection);
float d = Square(dot(surface.normal, h))*(r2-1.0f) + 1.0001f;
float d2 = Square(d);
float lh2 = Square(dot(surface.normal, h));
float n = 4 * brdf.roughness + 2;
float strength = r2 / (d2*max(0.1,lh2) *n);
return strength;
}
// 计算BRDF光照颜色 强度*高管颜色+漫反射颜色
float3 DirectBRDF(Surface surface, BRDF brdf, Light lihgt)
{
return SpecularStrength(surface, brdf, lihgt) * brdf.specular + brdf.diffuse;
}
计算光照的函数: 入射光颜色 * BRDF 颜色
float3 GetLighting(Surface surface, BRDF brdf, Light light)
{
return IncomingLight(surface, light) * DirectBRDF(surface, brdf, light);
}
自左到右,metallic: 0 0.25 0.5 0.75 1 . 自上而下,roughness: 0 0.22 0.5 0.75 0.95
我们现在获得了高光反射让表面看起来有高光.对于绝对粗糙的表面,高光跟漫反射差不多,表面越光滑,高光越集中.绝对光滑的表面,会有一个极小的高光光斑,得需要加一点点散射才能看到.
基于能量守恒,高光在平滑表面会非常亮,这是由于大多数到达表面像素的光都被集中反射了.当看到高光时,我们将看到远亮过漫反射的光.
通过降低颜色值,使其*0.05,可以明显看到,即使周围都完全没有漫反射光(黑色)了,高光的地方仍然很亮:
将材质颜色改为蓝色,则可以看到,高光会影响颜色,而非高光不会.如下图,非高光部分依然是材质的蓝色,而高光部分则是白色.
现在我们有一个还不错的光照,但是由于没有考虑环境光反射,所以看起来有点暗,尤其是金属材质.
修改 MeshBall 脚本,加入金属度和光滑度参数,用 instance 进行渲染:
4. Transparency 透明
现在给材质加入透明的支持.对象依然是基于其 alpha 值淡出,只不过现在淡出的是反射的光.对于漫反射,由于部分光反射,而其余的穿过了表面,这看起来是合理的.
但是现在高光也变淡了,实际对于绝对干净的玻璃来说,光要么穿过,要么反射,高光反射不会变淡,现在的方法是错的.
4.1 Premultiplied Alpha 预乘 alpha
解决方案是仅将 diffuse 变淡,高光强度保持不变.修改混合模式, SrcBlend = One, DstBlend 不变,依然是 OnMinusSrcAlpha
现在高光保持住了,但是漫反射也不会变淡了.我们通过将 surface alpha 乘到 diffuse 上来解决.
4.2 Premultiplication Toggle 预乘开关
预乘 alpha 让对象看起来像是玻璃,普通的 alpha blend 只是让对象部分可见,这两种情况都是需要支持的,所以我们要加入一个 shader feature ,并在材质面板增加开关
[Toggle(_PREMULTIPLY_ALPHA)]_PreMultiplyAlpha("Premultiply Alpha", Float) = 0
...
#pragma shader_feature _CLIPPING
#pragma shader_feature _PREMULTIPLY_ALPHA
然后修改像素着色器
#if defined(_PREMULTIPLY_ALPHA)
BRDF brdf = GetBRDF(surface, true);
#else
BRDF brdf = GetBRDF(surface, false);
#endif
下面是对比效果.左边预乘,右边没有预乘:
5. Shader GUI
我们现在支持多种渲染效果,每种都需要一些设置.为了简化在渲染模式之间的切换,现在向材质面板增加几个按钮,来应用我们预设的参数.
5.1 Custom Shader GUI
在 Lit.shader 中添加 CustomEditor "CustomeShaderGUI" 语句,通知 Unity 使用 CustomShaderGUI.cs 绘制该材质面板:
Shader "Custom RP/Lit"
{
...
CustomEditor "CustomShaderGUI"
...
}
在 Custom RP/Editor 目录下创建 CustomShaderGUI.cs.
public class CustomShaderGUI : ShaderGUI
{
private MaterialEditor materialEditor;
private Object[] materials;
private MaterialProperty[] properties;
public override void OnGUI(MaterialEditor matEditor, MaterialProperty[] props)
{
base.OnGUI(matEditor, props);
// 缓存下需要的对象
materialEditor = matEditor;
materials = matEditor.targets;
properties = props;
// 绘制 peset 按钮
OpaquePreset();
ClipPreset();
FadePreset();
TransparentPreset();
}
// 设置属性的接口.如果没有制定的属性则什么也不做
void SetProperty(string name, float value)
{
MaterialProperty prop = FindProperty(name, properties, false);
if(prop != null)
prop.floatValue = value;
}
// 启用/禁用 keyword
void SetKeyword(string keyword, bool enable)
{
if (enable)
{
foreach(Material m in materials)
m.EnableKeyword(keyword);
}
else
{
foreach (Material m in materials)
m.DisableKeyword(keyword);
}
}
// keyword 和一个 bool 属性关联,因此提供接口统一设置
void SetProperty(string name, string keyword, bool v)
{
SetKeyword(keyword, v);
SetProperty(name, v ? 1 : 0);
}
private bool Clipping
{
set => SetProperty("_Clip", "_CLIPPING", value);
}
bool PremultiplyAlpha
{
set => SetProperty("_PreMultiplyAlpha", "_PREMULTIPLYALPHA", value);
}
private BlendMode SrcBlend
{
set => SetProperty("_SrcBlend", (float)value);
}
private BlendMode DstBlend
{
set => SetProperty("_DstBlend", (float)value);
}
private bool ZWrite
{
set => SetProperty("_ZWrite", value ? 1 : 0);
}
RenderQueue RenderQueue
{
set
{
foreach (Material m in materials)
m.renderQueue = (int)value;
}
}
bool HasProperty(string name)
{
return FindProperty(name, properties, false) != null;
}
// 通用的按钮接口
private bool PresetButton(string name)
{
if (GUILayout.Button(name))
{
// 支持 undo
materialEditor.RegisterPropertyChangeUndo(name);
return true;
}
return false;
}
// 不透明预设
void OpaquePreset()
{
if (PresetButton("Opaque"))
{
Clipping = false;
PremultiplyAlpha = false;
SrcBlend = BlendMode.One;
DstBlend = BlendMode.Zero;
ZWrite = true;
RenderQueue = RenderQueue.Geometry;
}
}
// alpha test 预设
void ClipPreset()
{
if (PresetButton("Clip"))
{
Clipping = true;
PremultiplyAlpha = false;
SrcBlend = BlendMode.One;
DstBlend = BlendMode.Zero;
ZWrite = true;
RenderQueue = RenderQueue.AlphaTest;
}
}
// 普通的半透明
void FadePreset()
{
if (PresetButton("Fade"))
{
Clipping = false;
PremultiplyAlpha = false;
SrcBlend = BlendMode.SrcAlpha;
DstBlend = BlendMode.OneMinusSrcAlpha;
ZWrite = false;
RenderQueue = RenderQueue.Transparent;
}
}
// 带预乘 alpha 的半透明
void TransparentPreset()
{
if (HasProperty("Transparent") && PresetButton("Transparent"))
{
Clipping = false;
PremultiplyAlpha = true;
SrcBlend = BlendMode.One;
DstBlend = BlendMode.OneMinusSrcAlpha;
ZWrite = false;
RenderQueue = RenderQueue.Transparent;
}
}
}
在材质编辑界面,点击这些按钮,可以看到效果
5.2 Presets for Unline
可以用该 shader gui 为 unlit.shader 通过预设按钮.
但是由于 unlit 没有预乘 alpha 的属性,因此效果看起来很怪,因此我们需要在 unlit 中不现实这个按钮,所以需要在显示按钮前,检查是否有该属性
bool HasProperty(string name)
{
return FindProperty(name, properties, false) != null;
}
void TransparentPreset()
{
if (HasProperty("Transparent") && PresetButton("Transparent"))
{
...
}
}