一、什么是Shader变体管理
想要回答这个问题,要看看什么是Shader变体。
1. 变体
我们用ShaderLab编写Unity中的Shader,当我们需要让Shader同时满足多个需求,例如说,这个是否支持阴影,此时就需要加keyword(关键字),例如在代码中#pragma multi_compile SHADOW_ON SHADOW_OFF
,对逻辑上有差异的地方用#ifdef SHADOW_ON
或#if defined(SHADOW_ON)
区分(#if defined()
的好处是可以有多个条件,用与、或逻辑运算连接起来):
Light mainLight = GetMainLight();
float shadowAtten = 1;
#ifdef SHADOW_ON
shadowAtten = CalculateShadow(shadowCoord);
#endif
float3 color = albedo * max(0, dot(mainLight.direction, normalWS)) * shadowAtten;
然后对需要的材质进行material.EnableKeyword("SHADOW_ON")
和material.DisableKeyword("SHADOW_ON")
开关关键字,或者用Shader.EnableKeyword("SHADOW_ON")
对全场景包含这一keyword的物体进行设置
上述情况是开关的设置,还有设置配置的情况,例如说我希望高配光照计算用PBR基于物理的光照计算方式,而低配用Blinn-Phong,其他计算例如阴影、雾效完全一致,也可以将光照计算用变体的方式分隔。
如果是shader编写的新手,可能有两个问题:
①我不能直接传递个变量到shader里,用if实时判断吗?
答:不可以,简单来说,由于gpu程序需要高度并行,shader中的分支判断需要将if else两个分支都走一遍,假如你的两个需求都有不短的代码,这样的开销太大且不合理。
②我不可以直接将shader复制一份出来改吗?
答:不是很好,例如你现在复制一份shader出来,还需要对应脚本去找到需要替换的shader然后替换。更重要的是,当你的shader同时包含很多需要切换的效果:阴影、雾效、光照计算、附加光源、溶解、反射等等,总不能有一个需求就shader*2是吧
当你有多组关键字,阴影是否开关,是否有雾效时,你可能会写出下面这样的关键字声明:
#pragma multi_compile SHADOW_OFF SHADOW_ON
#pragma multi_compile FOG_OFF FOG_ON
#pragma multi_compile ADDLIGHT_OFF ADDLIGHT_ON
#pragma multi_compile REFLECT_OFF REFLECT_ON
//something keyword ...
这种写法属于比较死亡的写法,别在意,后面自然会说出各种写法中不好的地方并提出回避建议。
而对于当前材质,就会利用上述的关键字进行排列组合,例如一个“不希望接受阴影,希望有雾,需要附加光源,不带反射”,得到的Keyword组合就是:SHADOW_OFF FOG_ON ADDLIGHT_ON REFLECT_OFF
,这个Keyword组合就是一个变体。对于上面这个例子,可以得到2的4次方16个变体。
我们知道了什么是变体,再来回答为什么要变体管理。
可以发现上述例子中,每多一条都会乘2,实际上一列keyword声明可以不止两个,声明三个、甚至更多也是可能的。
但不管怎么说,随着#pragma multi_compile
的增加,变体数量会指数增长。这样会带来什么问题呢?
这时候需要了解下shader到底是什么。
2. Shader
我就当大家都知道,ShaderLab其实不是很底层的东西,它封装了图形API的Shader,以及一堆渲染命令。对于图形API,Shader是gpu的程序,不同API上传shader略有区别,例如OpenGL:
GLuint vertex_shader;
GLchar * vertex_shader_source[];//glsl源码
//创建并将源码传递给GPU
vertex_shader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertex_shader,