Shader 学习笔记

Shader "Custom/Diffuse Texture" {                // Shader的开始,双引号内饰该Shader的名字
    Properties {
        _MainTex ("Base (RGB)", 2D) = "white" {}      // 属性块,_属性名字("显示出来的名字", 类型) = 默认值 {选项(可选,但2D,Rect等必须要)}
    }
    SubShader {                                         // 子着色器,至少一个,具体使用哪个,根据目标平台的性能自动决定,如果都不达标,则使用FallBack
        Tags { "RenderType" = "Opaque" }                // 硬件将通过判定这些标签来决定什么时候调用该着色器,这里是当系统在渲染非透明物体时调用该Shader
                                                        // 标签还有诸如"Queue"="xxx"来指定渲染顺序,值越高越靠后渲染

        LOD 200                                         // 品质级别,在Unity中可以设定最大LOD,当设定的LOD小于SubShader所指定的LOD时,这个Sub将不可用,注意,它与Mesh那个自动切换模型的LOD不是一回事,这个是手动设置的

        CGPROGRAM                                       // 此标记到ENDCG表示这中间是一段CG语言写的Shader,如果sub里有这种代码,则说明该sub是可编程渲染
        #pragma surface surf BasicDiffuse vertex:vert   // 表面该sub是表面着色器,并且主函数是surf,使用我们自定的光照模型函数BasicDiffuse,使用顶点函数vert
                                   // Lambert是默认的光照模型函数,如果我们没有自定义的,就用它。顶点函数可以省略

        sampler2D _MainTex;                             // 在上面的属性块里设定的属性,在CG代码块里要再声明一次,将两者连接起来,因为CG代码块里不能直接使用
    
     struct Input {                                  // Input其实是需要我们去定义的结构,这给我们提供了一个机会,可以把所需要参与计算的数据都放到这个Input里,传入surf函数里使用
            float2 uv_MainTex;
        };

     void vert(inout appdata_full v, out Input o)   // 顶点函数,appdata_full是CgInclude里的结构体,存顶点的各种信息
     {
            //在这里修改顶点的各种信息,或者修改Input结构体,给surf函数传递信息
     }

     /* 自定光照模型函数,函数格式必须为:Lighting + 名字, surf主函数计算输出的SurfaceOutput作为参数参传入到此函数里
     * 其它两种格式的光照模型函数
     * half4 LightingName_PrePass (SurfaceOutput s, half4 light)
     * half4 LightingName (SurfaceOutput s, half3 lightDir, half3 viewDir, halfatten)
     */
     inline float4 LightingBasicDiffuse (SurfaceOutput s, fixed3 lightDir, fixed atten) {
       float difLight = max(0, dot(s.Normal, lightDir));                 // 像素点的光照强度与入射光线角度有关,角度越大,像素点越亮
       float4 col;
       col.rgb = s.Albedo * _LightColor0.rgb * (difLight * atten * 2);   // _LightColor.rgb是U3D中的光源,如果不乘这个参数,则无法随光源变化
       col.a = s.Alpha;
       return col; 
     }

        void surf (Input IN, inout SurfaceOutput o) {       // 表面着色器的主函数,参数IN表示我们的输入,o表示输出到屏幕的内容,o一开始是空白的,要我们输内容进去
            half4 c = tex2D (_MainTex, IN.uv_MainTex);      // SurfaceOutput是一个结构体,具体内容参见下面的第七点
            o.Albedo = c.rgb;
            o.Alpha = c.a;                                  // 我们将输入的内容经过变换后,赋予输出,这样就能在屏幕上改变内容了
        }
        ENDCG
    } 
    FallBack "Diffuse"                                      // 如果以上的所有sub着色器都不能使用,则默认使用该着色器
}

 

1.写Shader的三种语言:

HLSL -> DirectX,只有微软平台能用

GLSL -> OpenGL,由硬件编译,所以跨平台性很好

Cg  -> DirectX 与 OpenGl 的上层,类似中间语言,最后会编译成GLSL或者HLSL。由于是微软参与开发,语法很像HLSL,可以无缝转换为HLSL

(Unity Shder一般用CG编写,可以选择CG/HLSL或者GLSL)

 

2.计算机图形渲染分为:

固定管道渲染、可编程管道渲染

 

3.Unity中Shader分为三种:

固定功能着色器(Fixed Function Shader)

表面着色器(Surface Shader)

顶点着色器&片段着色器 (Vertex Shader & Fragment Shader)

前者是固定管道渲染,后两者是可编程管道渲染

 

4.Unity中判断Shader是哪种类型的方法

  • 没有嵌套CG语言,也就是代码段中没有CGPROGARAM和ENDCG关键字的,就是固定功能着色器。而且必须置于Pass块中。
  • 嵌套了CG语言,代码段中有surf函数的,就是表面着色器。不必置于Pass块中
  • 嵌套了CG语言,代码段中有#pragma vertex name和  #pragma fragment frag声明的,就是顶点着色器&片段着色器,置于Pass块中

 

5.Pass通道

执行的时候,从执行的SubShader里开始,从第一个Pass块开始执行,一个一个执行。

表面着色器没有Pass块,但在编译的时候,会被编译成若干Pass块。

 

6.参数类型

ShaderLab的参数类型:

  • Color - 一种颜色,由RGBA(红绿蓝和透明度)四个量来定义;
  • 2D - 一张2的阶数大小(256,512之类)的贴图。这张贴图将在采样后被转为对应基于模型UV的每个像素的颜色,最终被显示出来;
  • Rect - 一个非2阶数大小的贴图;
  • Cube - 即Cube map texture(立方体纹理),简单说就是6张有联系的2D贴图的组合,主要用来做反射效果(比如天空盒和动态反射),也会被转换为对应点的采样;
  • Range(min, max) - 一个介于最小值和最大值之间的浮点数,一般用来当作调整Shader某些特性的参数(比如透明度渲染的截止值可以是从0至1的值等);
  • Float - 任意一个浮点数;
  • Vector - 一个四维数;

  详见U3D官方文档:http://docs.unity3d.com/Manual/SL-Properties.html

CG语言中对应的类型:

  •  
  • sampler2D
  • samplerCube
  •  
  •  
  • float
  • vec
  • half 精度比float还低的浮点数,当然,运算效率更高

float和vec都可以在之后加入一个2到4的数字,来表示被打包在一起的2到4个同类型数

// 定义一个2d vector
vec2 coordinate;  
// 定义一个颜色值(A,R,G,B)
float4 color;  
// Multiply out a color
float3 multipliedColor = color.rgb * coordinate.x; 

 

7.SurfaceOutput的具体内容

这是Unity的CgInclude里定义的默认结构体,有需要的话,可以自己定义一个,可以在光照函数和surf函数里传递信息

struct SurfaceOutput {  
    half3 Albedo;     //像素的颜色
    half3 Normal;     //像素的法向值
    half3 Emission;   //像素的发散颜色
    half Specular;    //像素的镜面高光
    half Gloss;       //像素的发光强度
    half Alpha;       //像素的透明度
};

 

8.Cg语言标准函数库

http://http.developer.nvidia.com/Cg/index_stdlib.html

 

9.Surf 与 光照模型的关系

surf函数只是接受输入(材质,颜色等),将效果设置为对象的输出(即设置对象每一个像素点应该对应的颜色)

设置完像素点的输出颜色后,我们并不能看到,应该在光照模型函数里,将像素点的颜色,在对应的光下反应后的结果输出给color并返回才能在屏幕上看到结果。

 

10.从本质上讲,U3D中只存在顶点/片元着色器,表面着色器,固定函数着色器最终也会被编译成若个个PASS端。

表面着色器只是Unity自己创造的一种着色器,目的在顶多/片元的基础上再封装一层,使用起来更简单,编译时会自动转为PASS,但简单使用的同时也失去一些性能优势。

 

12.Unity中Shader面板扩展的两种方法

http://blog.csdn.net/WPAPA/article/details/51214368

 

13.SurfaceShader里各函数执行顺序

CPU 阶段

  1. 场景管理:加载和管理场景数据,包括模型、材质、光源等。
  2. 变换与动画计算:进行骨骼动画、变换矩阵计算等。
  3. 剔除计算:进行视锥体剔除和遮挡剔除,确定哪些物体需要被渲染。
  4. 批处理与排序:将渲染的物体按材质、渲染状态等进行分组和排序(例如先按材质分,相同材质一组,相同材质里面再按距离相机的位置排序),以减少状态切换。
  5. 传递数据到 GPU:设置渲染状态和将顶点、索引、材质等数据传递给 GPU。

GPU 阶段

  1. 顶点着色器:处理顶点数据,进行变换和光照计算。
  2. 曲面细分控制着色器(可选):用于细分曲面,控制曲面细分的细节层次。
  3. 曲面细分求值着色器(可选):细分后的顶点进行插值和变换。
  4. 几何着色器(可选):可以根据输入图元生成新的图元或修改现有图元。
  5. 光栅化:将图元转换为片段。
  6. 早期深度测试:在片段着色器之前进行深度测试,剔除被遮挡的片段。
  7. 片段着色器:对通过深度测试的片段进行着色计算。
  8. Alpha 测试(可选):基于透明度的条件判断,剔除符合条件的片段。
  9. 模板测试(Stencil Test):对片段进行模板测试,决定是否剔除片段。
  10. 深度测试(非早期深度测试时):根据片段的新深度值进行测试。
  11. Alpha Blending(混合):将片段颜色与帧缓冲区中的颜色进行混合。
  12. G-buffer 写入(延迟渲染,Deferred Rendering,特定阶段):在延迟渲染中,将片段信息写入多个 G-buffers。
  13. 光照累加缓冲(Light Accumulation Buffer)(延迟渲染,Deferred Rendering,特定阶段):对 G-buffer 信息进行光照计算并写入累加缓冲。
  14. Frame Buffer 写入:将最终的片段颜色写入帧缓冲区。
  15. 显示器输出:将渲染结果显示在屏幕上。

参考:Unity3d 图形学之OpenGL渲染流程(一)

vert -> surf -> light

vert先从appdata_full获取顶点信息,并修改(例如顶点位置,法线),然后将某些信息传递给Input结构体(例如顶点颜色)

接着进行插值,顶点间的信息进行插值,进行逐像素渲染

surf从上一步获取Input里存储的信息,开始修改像素信息(颜色,透明,高光啥的),然后将信息传递给SurfaceOutput结构体

最后处理光照信息,light函数利用上一步获取到的SurfaceOutput,当前光照方向,视野方向,光线衰减等,处理每个像素的光照效果

 

14.CgInclude文件

可以理解为C++里的各种头文件,包含了各种常用函数库,常量等,可以减少重复编码。

Unity自带了一个CgInclude.cginc文件,放置在Editor/Data/CGInludes里,可以直接用文本编辑器打开

例如SurfaceShader里不指定光照时,会用默认的Lambert光照函数,该函数就放在那里。

我们也可以自定义一个,但自定义的记得要在SurfaceShader的CGPROGRAM下 “#include "XX.cginc"

 

15.平行光着色跟点光源着色

Forward渲染:

参考:

  Forward Rendering 正向渲染-CSDN博客

  Unity正向渲染解析-CSDN博客

如果要为物体同时支持平行光着色跟点光源着色,一般需要2个Pass。

  第一个用来渲染平行光,支持投射阴影。

  第二个用来渲染各种点光源,不支持阴影。

当场景多有个光源时,对于每一个物体:

  1.平行光应用于所有可见的物体,而点光源有只作用于范围内的物体。

  2.所有RenderMode为Important的灯,都会调用它的Pass来渲染,每次渲染消耗一个DrawCall。

  3.所有Not Important的灯,最多有4个光源(文章里看的未验证过)以逐顶点光照模式进行渲染,其他的会统一编码到​球谐光照函数里,球谐计算是在CPU完成的,基本不消耗GPU全部只占用一个DrawCall来计算它们。(貌似不用在Shader里手动写这部分,Unity会自己加上)

  4.所有RenderMode为Atuo的灯,在Quality里有个PixelLightCount的参数,表示最多允许渲染的像素光,会优选选择最合适的灯(例如最亮,最近)来进行像素Pass渲染,接下来最多有4个光源以逐顶点光照模式进行渲染,其他的统一编码到​球谐光照函数里。

  5.注意不同的光照组间有重叠,如,最后一个逐像素光源也以逐顶点光照模式的方式渲染,这样能减少当物体和灯光移动时可能出现的 "光照跳跃" 现象。

  image

  6.场景里只能有一个主平行光可以投射阴影,当存在多个平行光时,选择最亮的那个光进行投射。

 

延迟渲染:

  TODO

Shader "向前渲染光照" {
    Properties {
    }
    SubShader {
        // --- Base Pass: 处理环境光、主平行光、顶点光照、SH光照等 ---
        Pass {
            Tags { "LightMode"="ForwardBase" } // 基础光照Pass
            ...
        }

        // --- Additive Pass: 处理额外的点光源、聚光灯 ---
        Pass {
            Tags { "LightMode"="ForwardAdd" } // 附加光照Pass,必须添加这个,否则只执行一次,只有第一个点光源成功被着色。加入这个标签,Unity会为每个点光源调用一次这个PASS
            Blend One One // 启用叠加混合模式,将光照效果叠加到Base Pass的结果上,否则最后一个光照会直接覆盖掉前面的光。
            ZWrite Off // 通常附加Pass不需要深度写入
            ...
        }
    }
    FallBack "Diffuse"
}

 

16.着色器变体

着色器变体,有点类似C语言种的宏定义,通过关键字来决定某部分的逻辑是否要执行。

因为GPU运行条件分支的能力很差,所以逻辑的分支一般不放在实际代码运行中,通过关键字来决定某部分逻辑是否编译进去。

但于宏有最明显不同的一点是:

  宏是预编译条件,只会把条件达成的部分编译进代码,其他部分直接抛弃。

  着色器变体,Unity Shader会把条件的所有可能结果都分别编译成一个Shder程序,运行时可以动态指定需要开启哪些关键字来切换哪个版本的Shader程序。

好处是灵活许多,我们可以指定不同的关键字来启用不同版本的着色器,例如同一个着色器,有好几个功能,如 “开高光”,“开纹理”,我们可以分别指定需要什么样的结果来启用相应的变体。

上面的例子中,编译时,Unity会生成4个着色器,分别是:“开高光”,“开纹理” ,“开高光 + 开纹理” 以及 ”关高光 + 关纹理“,根据我们当前选择的关键字,动态切换其中某一个。

缺点也非常明显,当关键字增大,变体的结果会爆炸增加, N 个独立二元关键字的变体数量为 2^N,会严重导致包体变大以及增加编译时间。

 

17.ColorMask

默认情况下,GPU 写入所有通道 (RGBA)。对于某些效果,您可能希望不修改某些通道;例如,您可以禁用颜色渲染来渲染无色阴影。
另一个常见的用例是完全禁用颜色写入,以便您在一个缓冲区中填充数据而无需写入其他缓冲区;例如,您可能需要在不写入渲染目标的情况下填充模板缓冲区。
注1:ColorMask 是对最终颜色进行Mask,在Blend之后进行,不会影响Blend。
注2:CclorMask 是对输出颜色进行屏蔽,而不是修改缓冲区里的颜色,例如只开启R通道的写入,则其他GBA通道,输出被屏蔽,实际显示的颜色,是缓冲区里原来的值
posted @ 2015-12-12 18:53  JeasonBoy  阅读(775)  评论(0)    收藏  举报