Arzie100 发表于 2022-8-6 12:54

技术美术成长之路——UnityShader篇(四)、开启Shader之旅

本文参考:
相关文章汇总:
第五章、开启Unity Shader之旅

一、环境

关于环境,我是高版本应该没啥问题,学起来再说吧~
二、最简单的顶点/片元着色器

window-rendering-lighting-environment-天空盒给他换掉就行了
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'

Shader "unityShaderBook/chapter5/simpleShader"{
    SubShader
    {
      Pass
      {
            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag

            float4 vert(float4 v : POSITION) : SV_POSITION
            {
                return UnityObjectToClipPos(v);
            }
            fixed4 frag() : SV_Target
            {
                return fixed4(1.0, 1.0, 1.0, 1.0);
            }
            ENDCG
      }   
    }
}这里代码copy过来还给了一个更新提示,MVP矩阵可以写成上边的代码了。
这里主要的新内容:#pragma vertex vert及下一行。这俩是告诉unity哪个函数包含了顶点着色器的代码,哪个是片元着色器。因为这俩函数是我们自定义函数名字的。
指定的形式就是:#pragma vertex name,一般习惯用vert和frag。
其中的语义SV_POSITION就表示裁剪空间的坐标。POSITION就是模型空间坐标。SV_Target就是指的输出的颜色值。
语义的意思就是我这里的有参数,你应该传进来什么?也就是unity会根据我们的语义来对参数进行对应的填充。那unity的这些数据从哪里来的,是使用该材质的mesh render组件提供的。在每一帧drawcall调用的时候他会把这些需要的纹理等给到unity shader。


对于顶点着色器从应用程序层得到的法线等数据,这个一般都是原地不动往后传递给片元着色器使用的,那么对于两者之间的通信如何进行的?
也就是通过返回值和参数,因为这里东西又不只是一个坐标,所以应该搞一个结构体出来。另外注意,从顶点着色器虽说是传给了片元着色器,但是并不是直接传递的,他俩根本不是一个层级的。对于顶点shader操作的是每一个顶点,返回的数据是每一个顶点返回一个。而片元shader是针对片元的。
这里是顶点的结果经过插值,然后得到片元对应的值,然后再作为片元shader的输入。
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'

Shader "unityShaderBook/chapter5/simpleShader"{
    SubShader
    {
      Pass
      {
            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag

            struct a2v
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                float4 texcoord : TEXCOORD0;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                float3 color : COLOR0;
            };

            v2f vert(a2v v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.color = v.normal * 0.5 * fixed3(0.5, 0.5, 0.5);
                return o;
            }
            fixed4 frag(v2f i) : SV_Target
            {
                return fixed4(i.color, 1.0);
            }
            ENDCG
      }   
    }
}这里我们的球就出现效果了:


可能因为版本的问题,他书上的代码vert多写了一个返回值的语义,因为我们在结构体里面写过了,这里应去掉,和输入参数类似。
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'

Shader "unityShaderBook/chapter5/simpleShader"{

    Properties
    {
      _Color("Color Tint", Color) = (1.0, 1.0, 1.0, 1.0)
    }


    SubShader
    {
      Pass
      {
            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag
            
            fixed4 _Color;

            struct a2v
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                float4 texcoord : TEXCOORD0;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                float3 color : COLOR0;
            };

            v2f vert(a2v v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.color = v.normal * 0.5 * fixed3(0.5, 0.5, 0.5);
                return o;
            }
            fixed4 frag(v2f i) : SV_Target
            {
                fixed3 c = i.color;
                c *= _Color.rgb;
                return fixed4(c, 1.0);
            }
            ENDCG
      }   
    }
}现在就可以根据颜色拾取器来控制球的颜色了。注意要把变量在pass里面再进行定义一次。
再进行定义的时候需要注意名字和类型都要匹配,名字必须一样,但是注意,但是对于类型,shaderlab中有的类型在HLSL这种语言中不一定有,所以这里他们有一个分量个数的对应就够了:


你是4维向量,我是4个float,那咱俩就能对应上。


三、unity中的内置文件和变量

内置文件也就是类似c++中的库,头文件,include之后就能用。


相关的常用结构体:


struct appdata_base {
    float4 vertex : POSITION;
    float3 normal : NORMAL;
    float4 texcoord : TEXCOORD0;
    UNITY_VERTEX_INPUT_INSTANCE_ID
};

struct appdata_tan {
    float4 vertex : POSITION;
    float4 tangent : TANGENT;
    float3 normal : NORMAL;
    float4 texcoord : TEXCOORD0;
    UNITY_VERTEX_INPUT_INSTANCE_ID
};

struct appdata_full {
    float4 vertex : POSITION;
    float4 tangent : TANGENT;
    float3 normal : NORMAL;
    float4 texcoord : TEXCOORD0;
    float4 texcoord1 : TEXCOORD1;
    float4 texcoord2 : TEXCOORD2;
    float4 texcoord3 : TEXCOORD3;
    fixed4 color : COLOR;
    UNITY_VERTEX_INPUT_INSTANCE_ID
};

struct appdata_img
{
    float4 vertex : POSITION;
    half2 texcoord : TEXCOORD0;
    UNITY_VERTEX_INPUT_INSTANCE_ID
};

struct v2f_img
{
    float4 pos : SV_POSITION;
    half2 uv : TEXCOORD0;
    UNITY_VERTEX_INPUT_INSTANCE_ID
    UNITY_VERTEX_OUTPUT_STEREO
};还有相关的函数:


针对这些函数的实现可以看一下,看一下别人的实现思路。当然他实现的时候套娃,用VSCode来看有点费劲没法转到定义。
四、Unity提供的CG/HLSL语义

1、语义what

语义其实就是对于参数和返回值的一个补充解释。
其实这个东西对于a2f变量来说,是很重要的东西,因为给顶点着色器赋值的是应用程序,是机器操作,他只会比对,发现这个是TEXCOOD0,那好他就会把他那里TEXCOOD0的纹理给赋值过去。
而对于v2f的语义,其实并不重要,因为两头都是由我们进行控制的,对于顶点着色器输出的COLOR0,我们其实并不一定要装颜色,装其他也可以,比如装顶点坐标,到了片元着色器时,我们把他当作顶点坐标使用就好了。这是完全没问题的。
对于语义有的带有SV,这种表示system-value,也就是他是系统相关的语义,这样的都具有特定的意义,比如SV_POSITION表示的时裁剪空间的坐标,SV_Target表示的时fragment shader的输出颜色。
因为GPU会拿他们做一些特定的事情,所以这些不能随便改动,你改了GPU找不着了就。当然这里的俩SV语义,是可以等价于POSITION和COLOR的,但是有一些平台等价,你可以用这俩替换系统值,但是有些不可以!
所以最好还是在这些关键的地方使用SV,保证跨平台的时候没问题。
所以总的来说,就是和机器交互的部分,你得正儿八经写语义,包括顶点着色器的所有输入参数和输出的坐标值,还有像素着色器的返回值。且该用SV系统值的时候就用。其他的语义实际上意义就不大。
            struct v2f
            {
                float4 pos : SV_POSITION;
                float3 color : CO;
            };对于v2f也就是顶点shader的输出结构体,我这里改成了CO,也是完全ok的,效果不变,因为就是一个完全由我们控制的,我们知道他是咋对应的,所以语义写成啥都行。
2、unity中支持的语义





Shader Model是啥?好像是和一些硬件啥啊的有关。。


注意这里都写了几个字,但不是必需的。通常的一些自定义的变量就是用TEXCOOD0~7来进行传递的。


这里还说了:


意思好像是说,关于一个语义能够使用的寄存器是有上限的!!!
五、Shader的调试

1、第一种方法

一种方式是把我们需要调试的变量当作颜色输出到屏幕上,因为这个是唯一的输出,只能通过这个输出,通过输出颜色来查验结果。
在unity shader中,颜色的范围是0到1,所以我们需要把调试的变量需要映射一下。如果不知道范围,针对超出范围的还是1,小于0的就是0,可以进行相关的尝试。
如果调试的一个一维变量,那么输出的时候就作为一个颜色分量输出,其他都0。当然多维的可以一个一个分析,也可以同时多个正好对应到颜色的三个分量上。




frac函数是返回小数部分 x - (int)x。decimal fraction小数
saturate函数是把结果限制在0到1范围内,小于0的取0,大于1的取1。
any(x) 参数中只要有一个不为0就返回true 否则返回false。Test if any component of x is nonzero.
其实都是把范围进行缩放一下,基本没有别的啥操作。


额,脚本不会用,试了一下,拖拽之后,component中也有了,但是依旧提示下面的:


但是利用QQ可以截图。
2、第二种方法:利用VS的图形调试器

等我出bug了,我再去学学。
3、第三种方式:帧调试器

回头再学(114页)
六、平台差异



针对一般情况的渲染输出没有问题,而我们以后可以把结果输出到渲染目标上,然后用渲染纹理来进行存储,这时候可能出一些问题,但是unity也帮我们处理了。
但是有一种例外情况,就是我们开启了抗锯齿并渲染到纹理时,就会有问题了。




至于为啥,看的糊里糊涂的。


这里的第二个也没看懂,表面着色器,不太了解,先放着吧。


关于语义已经说过了,尽可能在该使用SV的时候使用带SV的。
更多的官方去看吧:
七、Shader 整洁之道

1、关于float half fixed三种变量类型的选择:



这个精度和具体平台有关系,这里不一定。




主要就是移动平台需要注意!
2、语法规范

3、避免不必要的计算



因为对于片元着色器本身是一个需要大量执行的,一个三角形很多像素,实际程序中三角形数量又很多,所以他的高效执行就非常重要。
而且从内存上看,如果它使用太多的寄存器或者指令会出问题,因为它本身是高度并行的一个程序,那么对于寄存器这些资源大家都需要用,所以是有上限的。


这里我们可以调整我们的Shader Model,也就是和硬件相关的一些限制。


4、慎用循环和分支



因为GPU就是一帮小孩,你给他一个初中题,可能硬憋一会也能做出来,但是他比正常要慢很多。CPU就像高中生,大学生,对这些小操作很快就能搞定。小孩的优点在于,我人多啊。


5、不要除以0

这一章,还是一些相关的东西的介绍,对于shader现在差不多的东西都介绍了,下面应该就是算法相关了。

DomDomm 发表于 2022-8-6 12:56

可以多交流哦
页: [1]
查看完整版本: 技术美术成长之路——UnityShader篇(四)、开启Shader之旅