ComputeShader手机兼容性

官方文档对设备的要求:

Compute shaders in Unity closely match DirectX 11 DirectCompute technology. Platforms where compute shaders work:

  • Windows and Windows Store, with a DirectX 11 or DirectX 12 graphics API and Shader Model 5.0 GPU
  • macOS and iOS
     using Metal graphics API
  • Android, Linux and Windows platforms with Vulkan API
  • Modern OpenGL platforms (OpenGL 4.3 on Linux or Windows; OpenGL ES 3.1 on Android). Note that Mac OS X does not support OpenGL 4.3
  • Modern consoles (Sony PS4 and Microsoft Xbox One)

Compute shader support can be queried runtime using SystemInfo.supportsComputeShaders.

对比GPU Instancing技术:

GPU Instancing is available on the following platforms and APIs:

  • DirectX 11 and DirectX 12 on Windows
  • OpenGL Core 4.1+/ES3.0+ on Windows, macOS, Linux, iOS
     and Android
  • Metal on macOS and iOS
  • Vulkan on Windows, Linux and Android
  • PlayStation 4
     and Xbox One
  • WebGL
     (requires WebGL 2.0 API)

网友给出的兼容性测试报告:

简介

  Compute Shader是微软DirectX 11 API新加入的特性,在Compute Shader的帮助下,程序员可直接将GPU作为并行处理器加以利用,GPU将不仅具有3D渲染能力,也具有其他的运算能力,也就是我们说的GPGPU的概念和物理加速运算。多线程处理技术使游戏更好地利用系统的多个核心。故对其在手机上的支持情况做了如下测试与分析。

Android

  利用WeTest测试213台手机,返回201台手机信息。具体测试信息如下:

OpenGL ES 3.2    ShaderModel 5.0 112台
OpenGL ES 3.1    ShaderModel 5.0 50台
OpenGL ES 3.1    ShaderModel 4.5 16台
OpenGL ES 3.0    ShaderModel 3.5 22台
OpenGL ES 2.0    ShaderModel 3.0 1台

  利用WeTest测试Top100常用手机,返回99台手机信息。具体测试信息如下:

OpenGL ES 3.2    ShaderModel 5.0 58台
OpenGL ES 3.1    ShaderModel 5.0 22台
OpenGL ES 3.1    ShaderModel 4.5 10台
OpenGL ES 3.0    ShaderModel 3.5 9台

  Android5.0开始支持Compute Shaders,来源如下: https://developer.android.google.cn/about/versions/android-5.0

  在所有测试的Android手机中,OpenGL ES 3.1以上的手机均使用的是Shader Model 4.5或Shader Model 5.0,在使用unity提供的API SystemInfo.supportsComputeShaders在上述手机显示为true,但通过运行一段ComputeShader程序,在Shader Model4.5上运行结果却不符合预期。

  综上所述,Shader Model 5.0开始支持ComputeShader。但文档说OpenGL ES 3.1就已经支持,unity API返回结果也是如此,为什么Shader Model4.5不支持还有待探讨。

iOS

  根据苹果开发文档,从iOS 9开始支持Compute Shaders:https://developer.apple.com/library/archive/releasenotes/General/WhatsNewIniOS/Articles/iOS9.html#//apple_ref/doc/uid/TP40016198-SW1

  iPhone所使用的图形API为Metal,ComputeShader在iphone6 ios10 系统已经支持(再早的iphone手机由于没有测试设备,没有分析)。 以上测试方法是通过两方面进行,一是使用unity提供的API SystemInfo.supportsComputeShaders在上述设备显示为true,二是通过运行一段ComputeShader程序,运行结果也符合预期。

总结

1.ComputeShader技术在Android上运行要求:Shader Model 5.0的设备(OpenGL ES 3.1及以上设备大部分满足此条件,少数3.1的设备不满足)。

2.ComputeShader技术在iOS上运行要求:系统软件最低为iOS 9(iPhone6之前的因缺乏设备未能测试,有需要的看官请自行测试)。

原文链接:https://blog.csdn.net/weixin_39630182/article/details/112293859

RenderDoc调试Shader(转)

原文地址:https://zhuanlan.zhihu.com/p/74622572

工欲善其事必先利其器,作为一个逻辑开发者,刚开始学习unity shader的时候,最头痛的我就觉得就是shader的调试吧,总是想单步调试下看看具体的数值是怎么变化的,尤其是显示效果不对的时候。虽然现在我认为当你习惯用假色彩图像调试之后,确实单步debug没有太大的必要,但是今天还是总结一下吧,我觉得利用工具是非常有利于理解渲染流程的。

先说下最常用的工具就是visual studio了,unity 官方的教程在这里,这里我就不多说了。

https://link.zhihu.com/?target=https%3A//docs.unity3d.com/Manual/SL-DebuggingD3D11ShadersWithVS.html

这里只说他的缺点,就是每次都要用vs 启动unity 编辑器,比较繁琐和耗时,而且装的东西也比较多。那么接下来就说说我们的主角RenderDoc (https://link.zhihu.com/?target=https%3A//renderdoc.org/docs/index.html) ,当我第一次发现这个工具的时候,非常兴奋,觉得终于有一个符合我的编程习惯的工具了。接下来就简单介绍一下吧。RenderDoc是一个开源的可以调试shader的工具,我主要讲一下和unityshader 结合使用的部分。

  1. 首先在官网下载他的release版本,我现在用的是V1.4, 下载地址是https://link.zhihu.com/?target=https%3A//renderdoc.org/
  2. 也可以去github 下载他的源码编译最新的版本,github地址是 (https://link.zhihu.com/?target=https%3A//github.com/baldurk/renderdoc/tree/v1.4
  3. 下载安装RenderDoc后,打开unity ,可以在Scene或者Game视图右键,弹出LoadRenderDoc菜单,然后点击加载就可以了

3.加载之后,会在视图上出现一个renderdoc的Logo,如下图

4.点击这个图标,会调用起RenderDoc 并且自动截图

6.如果你的窗口现在没有打开TextureViewer MeshViewer PipeLineState的话,你可以通过window 菜单项打开他们,方便我们先运行效果。至于这些窗口的说明和介绍,大家可以去看官方文档。

7.接下来就是调试我们的shader了 ,这时候要注意的一个点就是我们要在我们的shader里面加上一句宏

#pragma enable_d3d11_debug_symbols

来保证unity 不会优化掉我们的调试信息,把他放到我们的CGPROGRAM 代码块里面,记得renderdoc截图的时候,保证这句代码已经在shader里面的,之前没有加的话,记得重新走一次renderdoc截图啊,还有截图的时候最好保证unity的其他窗口是关闭的,保证只有game视图一个窗口,不然截图的内容可能是错误的


8.OK,当我们看到事件列表的时候,我们选择我们要调试的一个事件

大家一定注意选择的事件,不要选择错了,不然选择的是其他事件,调用的不是你想要调试的shader,那么你看到调试代码就是dxbc格式的,不是我们想要的HLSL的格式,我们选中事件之后,在mesh Viewer面板可以调试vertex shader,在texture viewer可以调试frag shader .

在mesh viewer 面板就是 选中一个点,然后右键,选择debug this vertex

就会打开调试面板

然后我们选择Debug in HlSl 就可以切换到Hlsl语法代码,进行愉快的调试了

点击debug in assembly 可以来回切换面板

9.如何调试frag shader呢,跟上面差不多,打开textureview 面板

右键选中你要调试的片元,然后点击右下角的debug按钮即可。

下面的watch 面板就是监视面板,你可以监察你的变量。也可以打断点一步一步调试,用法和vs差不多。

10.你也可以在pipleline state面板来看各个流程,可以实时编辑你的shader,来调整效果

点击edit

修改代码后,点击F5键刷新就可以在textureviewer看效果

好了,简单的介绍就到这里,总而言之,用renderdoc还是很方便的,解决了心头大患,具体更详细的介绍和使用,大家可以去官网在去了解下。

Unity Shader GrabPass使用注意的问题(转)

最近项目中碰到GrabPass多次抓图的问题,经测试发现可以每帧只抓取一次图,然后共用。后来看到网上这篇文章总结得很好,所以收藏下。

文档

关于Unity Shader中的GrabPass说明文档:

官方的ShaderLab: GrabPass
CSDN其他博主翻译的ShaderLab: GrabPass


GrabPass有两种写法

GrapPass { }
GrabPass { “TheGrabTextureName” }
两种写法的去写在哪呢。
文档中有说明,但是可能说的还是不够清楚。

我用自己总结的:

GrabPass { } 是每次Drawcall中的Shader的GrabPass使用时都会中屏幕内容抓取一次绘制的内容,并保存在默认的命为_GrabTexture的纹理中
GrabPass { “TheGrabTextureName” } 是每一帧Drawcall中的Shader的GrabPass中,第一次调用该GrabPass抓取的内容,保存在TheGrabTextureName的纹理中,后面Drawcall或是pass调用的GrabPass { “TheGrabTextureName” }只要TheGrabTextureName纹理名字与之前的GrabPass { “TheGrabTextureName” }中的TheGrabTextureName相同,都不会再执行GrabPass的过程,而直接使用之前Grab好的纹理对象内容。
下面我在实际Unity测试项目中测试结果写下来。


区别

我写了个测试用的,类似毛玻璃的模糊效果的Shader,如下:

使用GrabPass { } 的方式

// jave.lin 2020.03.04
Shader "Custom/GrabTexBlur" {
    Properties {
        _Blur ("_Blur", Range(0, 1)) = 0
    }
    SubShader {
        Tags { "Queue"="Transparent" "RenderType"="Transparent" }
        GrabPass { }
        Pass {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            struct appdata {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };
            struct v2f {
                float4 vertex : SV_POSITION;
                float4 grabPos : TEXCOORD1;
                float4 worldPos : TEXCOORD2;
            };
            sampler2D _GrabTexture;
            float4 _GrabTexture_TexelSize;
            fixed _Blur;
            fixed3 blur(float2 uv) {
                fixed3 sum = 0;
                const int blurSize = 4;
                const int initV = blurSize / 2;
                const int maxV = initV + 1;
                for (int i = -initV; i < maxV; i++) {
                    for (int j = -initV; j < maxV; j++) {
                        sum += tex2D(_GrabTexture, uv + float2(i * _GrabTexture_TexelSize.x, j * _GrabTexture_TexelSize.y));
                    }
                }
                // sum /= (blurSize + 1) * (blurSize + 1); // 这句时正确的效果
                sum /= blurSize * blurSize; // 这句时为了测试两个Sphere交集部分更亮的效果
                return sum;
            }
            v2f vert (appdata v) {
                v2f o;
                o.worldPos = mul(unity_ObjectToWorld, v.vertex);
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.grabPos = ComputeGrabScreenPos(o.vertex);
                return o;
            }
            fixed4 frag (v2f i) : SV_Target {
                i.grabPos.xy /= i.grabPos.w;
                fixed4 col = tex2D(_GrabTexture, i.grabPos.xy);
                col.rgb = lerp(col.rgb, blur(i.grabPos.xy), _Blur);
                return col;
            }
            ENDCG
        }
    }
}


应用

  • 场景中新建一个Sphere球体。
  • 创建Material材质,设置使用的Shader。
  • 给球体设置材质。

效果如下图:

Profiler > Frame Debugger

打开Frame Debugger查看绘制过程,因为shader指定在transparent的queue绘制队列,所以直接看TransparentGeometry下绘制的内容就好

可以看到有一个Grab RenderTexture的绘制

这时再复制一个毛玻璃的Sphere,然后再看看绘制过程。

这次看到有两次Grab RenderTexture的绘制
这是GrabPass { } 的绘制过程。
看看运行效果,记住这个效果,与下面的其他方式是不一样的

使用GrabPass { “TheGrabTextureName” }方式

shader还是和上面的一样,都是毛玻璃的测试shader。
只不过,这次我们给GrabPass出来的屏幕内容的纹理定义了一个名字。

GrabPass { "_GrabTexture" }

注意,如果你起的名字与默认的_GrabTexture相同也是没问题的,只要用了这种给GrabPass出来的纹理起个名字,就和没使用名字的处理流程是不一样的

然后再看看绘制过程

可以看到,这次虽然绘制了两个Sphere(黄色框那),但是Grab RenderTexture只执行了一次,因为都使用了GrabPass {“Name”}的方式,并且Name是一样的。那么后续的GrabPass就直接使用之前的纹理。

效果上会也和没起名字的不一样

两图对比一下

首先,shader中有这么一句故意使用的:

               // sum /= (blurSize + 1) * (blurSize + 1); // 这句时正确的效果
                sum /= blurSize * blurSize; // 这句时为了测试两个Sphere交集部分更亮的效果

因为少除了一些采样数据均值权重,所以整体权重比之前大,就会更亮。

GrabPass{ }的方式,在两球体交集部分会比较亮,原因是:GrabPass { } 每次Drawcall时的都重新先取拿一次当前绘制屏幕内容的内容到一个纹理中,所以第一个球体对Grab出来的屏幕像素内容处理逻辑了,导致某些像素比较亮了。但是第二个球体再次Grab出来的屏幕像素时,有些与第一个球体的像素集合有交集的本身有处理过,所以亮度本身比较高了,那么再次处理亮度,就会亮上加亮。
而GrabPass { “Name” } 的方式,区别在于,第二个球再次GrabPass { “Name” }时发现这个”Name”的纹理之前有了,就不再重新抓取当前屏幕内容了。所以使用的还是第一个球体执行毛玻璃+亮度之前的原始屏幕内容,所以交集出的部分不会更亮。第一次球体绘制的与第二次球体绘制有交集的那部分内容,都给第二次球体绘制的覆盖了。

不同Shader中使用了一样的GrabPass {“Name”}

再添加另一个shader,此shader作用就是添加亮度的,如下

Shader "Custom/GrabTexBrightness" {
    Properties {
        _Brightness ("_Brightness", Range(1, 2)) = 1
    }
    SubShader {
        Tags { "Queue"="Transparent" "RenderType"="Transparent" }
        GrabPass { "_GrabTexture" }
        Pass {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            struct appdata {
                float4 vertex : POSITION;
            };
            struct v2f {
                float4 vertex : SV_POSITION;
                float4 grabPos : TEXCOORD1;
            };
            sampler2D _GrabTexture;
            half _Brightness;
            v2f vert (appdata v) {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.grabPos = ComputeGrabScreenPos(o.vertex);
                return o;
            }
            fixed4 frag (v2f i) : SV_Target {
                return tex2Dproj(_GrabTexture, i.grabPos) * _Brightness;
            }
            ENDCG
        }
    }
}

可以看到使用了GrabPass { “_GrabTexture” },名字与之前的毛玻璃+亮度方式的shader的”_GrabTexture”是一直的名字。

那么看看Frame Debugger的绘制过程

三次Draw Mesh,但只有一次Grab RenderTexxture,而且Draw Mesh BrightnessSphere与后面两次BlurSphere的Shader是不一样的,只不过他们使用的GrabPass {“Name”}中的Name是一样的。

注意性能

从Frame Debugger中的GrabPass的处理过程,显示的是:Grab RenderTexture,注意后面是RenderTexture

注意都是猜测,具体调用底层的渲染API这个需要使用一个分析工具才能确定

而这个过程是很费(耗费性能)的,所以我们尽量能用GrabPass {“Name”} 就不用GrabPass{ }

刚开始我以为是RT的方式,后来发现一篇文章,虽然不是将GrabPass的,是将一般后效使用的MonoBehaviour类的回调OnRenderImage的方法下的Graphics.Blit,也是挺卡的,在一般的手机上。

这篇文章中有讲解:在使用Mali系列的Android手机。他用Mali Graphics Debugger看到底层渲染API的调用。

图中可以看到Unity的Profiler中,有显示调用的是RenderTexture.GrabPixels

中MGD(Mali Graphics Debugger)中查看底层API

在glReadPixels的最后个参数不为空,则表示数据从显存传输到系统内存,从CPU到GPU的逆向传输,这是非常缓慢的过程,并且是阻塞模式。

2020.03.15 更新,在看到unity 的SIGGRAPH2011的某个文档有说明:
GrabPass {“name”}
• new in 3.4: only copies the color buffer once per frame (_Grab is shared)

所以确定,调用的就是glReadPixel来读取ColorBuffer的像素的。

在FORCE FIELD EFFECT那部分有说明,文档:SIGGRAPH2011 Special Effect with Depth.pdf
如果多年后,下载不了,链接无效了,可以点击这里(Passworld:cmte)下载(我收藏到网盘了)

原文地址

UnityShaderVariant的一些探究心得[转]

最近项目中Global Keyword超标(>256),导致频繁报错,所以特效了解了下这方面内容,发现这篇文章解释得挺清楚。

ShaderVariant

举个例子,对于一个支持法线贴图的Shader来说,用户肯定希望无论是否为材质提供法线贴图它的Shader都能正确的进行渲染处理。一般有两种方法来保证这种需求:

  1.在底层shader(GLSL,HLSL等)定义一个由外部传进来的变量(如int),有没有提供法线贴图由外部来判断并给这个shader传参,若是有则传0,否则传1,在Shader用if对这个变量进行判断,然后在两个分支中进行对应的处理。

  2.对底层shader封装,如Unity的ShaderLab就是这种,然后在上层为用户提供定义宏的功能,并决定宏在被定义和未被定义下如何处理。最终编译时,根据上层的宏定义,根据不同的组合编译出多套底层shader.

  上述两种方法,各有利弊,对于前者由于引入了条件判断,会影响最终shader在GPU上的执行效率。而后者则会导致生成的shader源码(或二进制文件)变大。Unity中内置的Shader往往采取的是后者,所以这里只讨论这种情况。   

  Unity的Shader中通过multi_compile和shader_feature来定义宏(keyword)。最终编译的时候也是根据这些宏来编译成多种组合形式的Shader源码。其中每一种组合就是这个Uniy Shader的一个Variant。

MaterialShaderVariant的关系

一个Material同一时刻只能对应它所使用的Shader的一个variant。进行切换的要使用Material.EnableKeyword()和Material.DisableKeyword()来开关对应的宏,然后Unity会根据你设定的组合来匹配响应的shader variant进行渲染。如果你是在编辑器非运行模式下进行的修改那么这些keyword的设置会被保存到材质的.mat文件中,尝试用NotePad++打开.mat文件,你应该会看到类似于下面的一段内容(需要在编辑器设置里把AssetSerializationMode设置为Force Text):

%YAML 1.1

%TAG !u! tag:unity3d.com,2011:

--- !u!21 &2100000

Material:

  serializedVersion: 6

  m_ObjectHideFlags: 0

  m_PrefabParentObject: {fileID: 0}

  m_PrefabInternal: {fileID: 0}

  m_Name: New Material

  m_Shader: {fileID: 4800000, guid: 3e0be7fac8c0b7c4599935fa92c842a4, type: 3}

  m_ShaderKeywords: _B

  m_LightmapFlags: 1

  m_CustomRenderQueue: -1

  …

其中的m_ShaderKeywords就保存了这个材质球使用了哪些宏(keyword).

  如果你手头有built-in Shader的源码可以打开里面的StandardShaderGUI.cs看一下Unity自己事怎么处理对于StandardShader的keyword设置的。

  另外Shader.EnableKeyword,和Shader.DisableKeyword是对Shader进行全局宏设置的,这里不提了。

multi_compileshader_feature的区别

完全没接触过它们的同学可以先看官方文档的介绍,multi_compile是一直都有的,shader_feature是后来的unity版本中加入的关键字。

举例介绍一下multi_compile和shader_feature:

1.如果你在shader中添加了

#pragma multi_compile  _A _B 
#pragma multi_compile _C _D

那么无论这些宏是否真的被用到,你的shader都会被Unity编译成四个variant,分别包含了_A _C,_A _D, _B _C,_B _D四种keyword组合的代码

2.如果是

#pragma shader_feature _A _B 
#pragma shader_feature _C _D

那么你的shader只会保留生成被用到的keyword组合的variant,至于如何判定哪些组合被用到了,等后面提到Assetbundle时候再说。

ShaderVariant与Assetbundle的关系

我所遇到的问题正是和Assetbundle(简称AB)有关,原因是打成AB包之后shader_feature所定义的宏没有被正确包含进去。

  上面说了multi_compile定义的keyword是一定能正确的生成对应的多种组合的shaderVariant,但shader_feature不尽然,Unity引入shader_feature就是为了避免multi_compile那种完整编译所导致组合爆炸,很多根本不会被使用的shader_variant也会被生成。Unity在处理shader_feature时会判断相应的keyword组合是否被使用。需要区分一下几种情况:

1.如果shader没有与使用它的材质打在一个AB中,那么shader_feature的所有宏相关的代码都不会被包含进AB包中(有一种例外,就是当shader_feature _A这种形式的时候是可以的),这个shader最终被程序从AB包中拿出来使用也会是错误的(粉红色).

  2.把shader和使用它的材质放到一个AB包中,但是材质中没有保存任何的keyword信息(你在编辑器中也是这种情况),shader_feature会默认的把第一个keyword也就是上面的_A和_C(即每个shader_feature的第一个)作为你的选择。而不会把_A _D,_B _C,_B _D这三种组合的代码编译到AB包中。

  3.把shader和使用它的材质放到一个AB包中,并且材质保存了keyword信息(_A _C)为例,那么这个AB包就只包含_A _C的shaderVariant.

  可以看到shader_feature所定义的keyword产生的ShaderVariant并不是全部被打包到AB中,特别是你想在游戏运行时动态的通过EnableKeyWorld函数来进行动态修改材质使用的shaderVariant,如果一开始就没有把对于variant放进AB包,自然也就找不到。

ShaderVariantCollection

要正确的让各种variant正确的在游戏运行时正确处理,

最直接暴力的两种方法:

1.把Shader放到在ProjectSetting->Graphics->Always Include Shaders列表里,Unity就会编译所有的组合变种。

2.把Shader放到Resources文件夹下,也会正确处理,我猜也应该是全部keyword组合都编译,有知道的同学,麻烦留言告诉我。

  但是这两种情况最大的问题就是组合爆炸的问题,如果keyword比较少还好,要是多了那真是不得了,比如你把standardShader放进去,由于它有大量的keyword,全部变种都生成的话大概有几百兆。另外一个问题就是这种办法没法热更新。自然不如放到AB包里的好控制。

  放到AB包就又涉及到shader_feature的处理,为了在运行时动态切换材质的shadervariant,可以在工程里新建一堆材质,然后把每个材质设置成一种想要的keyword组合,把他们和shader放到一起打到一个AB中去,这样虽然能让shadervariant正确生成,但是这些Material是完全多余的。

  为了解决这种问题,Unity5.0以后引入了ShaderVariantCollection(下面简称SVC),这里不讲用法,只说问题,这个SVC文件可以让我指定某个shader要编译都要编译带有哪些keyword的变种。并且在ProjectSetting->Graphics界面新加了一个Preloaded Shaders列表,可以让你把SVC文件放进去,编译时指定的Shader就会按照SVC中的设置进行正确的variant生成,而不会像Always Include Shaders列表中的那样全部变种都生成。

  但是它在AB中的表现可就不尽如人意了,要让它起作用,就必须把它和对应的shader放在一个AB中,而且除了5.6以外版本,我试了几个都不能正确使用,不是一个variant都没生成,就是只生成一个shadervariant(和放一个没有设置keyword的材质效果一样).你可以自己用UnityStudio打开查看一下生成的AB内容。

写在最后

应该正确的理解Unity提供multi_compile和shader_feature以及ShaderVariantCollection的意图,根据自己的情况来选择合理的解决方案。

  在查这个问题的过程中也google了一些,发现国外在这方面的讨论远没国内多,应该是因为老外很少使用热更新这种东西,也自然很少用AB。

作者esfog,原文地址http://www.cnblogs.com/Esfog/p/Shader_Variant.html

Unity Shader中的multi_complie和ShaderFeature(转)

一,multi_complie 还是 shader_feature

shader_feature 和 multi_complie 是两个很相似的预编译指令,在Editor模式下,他们是几乎没有区别的。

共同点是:

  • 声明Keyword,用来产生Shader的变体(Variant)
#pragma multi_compile A B
//OR #pragma shader_feature A B

//-----------------------A模块----------------------
#if A
  return fixed4(1,1,1,1); 
#endif 
//---------------------------------------------------

//-----------------------B模块-----------------------
#if B
  return fixed4(0,0,0,1); 
#endif
//---------------------------------------------------
  • 这个Shader会被编译成两个变体:一是只包含A模块代码的变体A;二是只包含B模块代码的变体B;
  • 指定的第一个关键字是默认生效的,即默认使用变体A;
  • 在脚本里用Material.EnableKeyword或Shader.EnableKeyword来控制运行时具体使用变体A还是变体B;
  • 它们声明的Keyword是全局的,可以对全局的包含该Keyword的不同Shader起作用;
  • 全局最多只能声明256个这样的Keyword;
  • 请注意Keyword的数量和变体的数量之间的关系,并可能由此导致的性能开销,比如声明#pragma multi_compile A B和#pragma multi_compile D E 这样的两组Keyword会产生 2×2=4 个Shader变体,但若声明10组这样的keyword,则该Shader会产生1024个变体;

区别在于:

如果使用shader_feature,build时没有用到的变体会被删除,不会打出来。也就是说,在build以后环境里,运行代码Material.EnableKeyword(“B”)可能不起作用,因为没有Material在使用变体B,所以变体B没有被build出来,运行时也找不到变体B。

如果想解决这个问题,可以采取以下办法中的其中一种:

  1. 使用multi_complie 代替 shader_feature,multi_complie 会把所有变体build出来;
  2. 把这个Shader加入“always included shaders”中 (Project Settings -> Graphic);
  3. 创造一个使用变体B的Material,强行说明变体B有用;

二,__

上文已经提到了,最多声明256个全局Keyword,因此我们要尽量节省Keyword的使用数量。其中一个技巧是使用 __(两条下划线),如:

#pragma multi_compile __ A
//OR #pragma shader_feature __ A

//-----------------------A模块----------------------
#if A
  return fixed4(1,1,1,1); 
#endif 
//---------------------------------------------------

return fixed4(0,0,0,1);
  • 此方式相比#pragma multi_compile A B 的方式,我们可以减少使用一个Keyword。
  • 此方式仍会编译成两个变体:一是不包含A模块代码的变体非A;二是包含A模块代码的变体A;
  • 默认为 __ ,即变体非A生效。

三,multi_complie_local

全局的Keyword只能有256个,这或许会最终对我们造成限制,而且大部分Keyword并不需要进行全局声明。

因此,我们可以使用multi_complie_local来声明局部的、只在该Shader内部起作用的Keyword,用法相似:

#pragma multi_compile_local __ A
//OR #pragma shader_feature_local __ A

//-----------------------A模块----------------------
#if A
  return fixed4(1,1,1,1); 
#endif 
//---------------------------------------------------

return fixed4(0,0,0,1);

但需要注意:

  • local Keyword仍有数量限制,每个Shader最多可以包含64个local Keyword
  • 因为这种Keyword是局部的,Material.EnableKeyword仍是有效的,但对Shader.EnableKeyword或CommandBuffer.EnableShaderKeyword这种全局开关说拜拜
  • 当你既声明了一个全局的Keyword A ,同时又声明了一个同名的、局部的Keyword A,那么优先认为Keyword A是局部的。

原文地址:https://zhuanlan.zhihu.com/p/77043332