找回密码
 立即注册
查看: 418|回复: 0

Unity Shader正式入门(1)-Shader结构详解

[复制链接]
发表于 2021-11-26 10:25 | 显示全部楼层 |阅读模式
《Unity Shader入门精要》学习笔记(7)-Shader基础(1)

这个系列将会分享我在学习《Unity Shader入门精要》这本书的时候学习到的重点内容和一些心得体会,在书的基础上如果有能力的话会进行一定程度上的创新,以下是本系列的第七篇.这一篇中,我们会对Unity Shader进行更为详细的介绍.
时间:2021.11.22
在阅读这篇文章之前,读者最好已经阅读了前置的关于渲染管线和Shader基本介绍,以及关于数学部分介绍的文章,详细可以点击专栏阅读:
技美学习 - 知乎 (zhihu.com)
这篇文章对应的书的位置:

基础篇 第5章
1.软件版本与相关环境

       在Unity Shader入门精要更新系列中,我使用的Unity版本是Unity2021.1.5f1c1版本,理论上大于等于Unity5.x版本应该暂时都是可以的,使用的编译器是Visual Studio 2017,操作系统为Windows系统.关于OpenGL还是DirectX图形接口带来的差异会在后文出现时进行说明.
2.一个简单的顶点/片元着色器

(1)在Unity中新建Shader

       这里面我们选择新建一个Unlit Shader(关于如何创建Shader,参考前置文章《Unity Shader入门精要》阅读笔记(2)-Shader初探 - 知乎 (zhihu.com)),取名为ShaderLearnChap5_1.双击打开Shader的源代码,可以看到基础结构如下:
Shader "Unlit/ShaderLearnChap5_1"
{
    Properties
    {
        //这里面放置相关的属性
    }
    //针对显卡1的SubShader
    SubShader
    {
        //设置渲染状态与标签,如:
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            //设置渲染状态与标签
            CGPROGRAM
            //该代码片段的编译指令
            #pragma vertex vert
            #pragma fragment frag

            //以下来写CG代码,直至ENDCG部分
                //...
            ENDCG
            //其他设置写在这里
        }
        //其他需要Pass代码写在这里
    }
    //针对显卡2的SubShader
    SubShader
    {
        //...
    }
    // 上述SubShader都失败后用于回调的Unity Shader
    Fallback "VertexLit"
}
       其中,最重要的部分是Pass语义块,因为我们绝大多数的代码是写在这里的.
(2)用实例来体验Shader

       将刚才的Shader中的代码替换为下述的代码:
Shader "ShaderLearnChap5_1"
{
    SubShader{
        Pass {
            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag

            float4 vert(float4 v : POSITION) : SV_POSITION {
                return mul(UNITY_MATRIX_MVP, v);
            }

            fixed4 frag() : SV_Target {
                return fixed4(1.0, 1.0, 1.0, 1.0);
            }

            ENDCG
        }
    }
}
       将这个Shader赋予一个材质TestMaterial1,将Material赋予一个物体,这里用一个Plane来表示,处理结果如下:
关掉SkyBox以更好地看到Shader效果

       有的时候SkyBox会对我们看到的结果造成干扰,可以用如下的方式关掉:
       (a)在Unity的菜单中,选择Window -> Rendering -> Lighting


       (b)将SkyBox这一栏的材质设置为空


       此时关闭了天空盒子,呈现的效果如下:


(3)代码介绍

       重新进入到我们的Shader,可能会发现提示行尾不一致等信息,点击确定之后发现代码被升级了,不用担心,我们将升级后的代码分析一下:
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)' //Unity的提示信息

//这是因为在unity5.6以上版本中,shader中的UNITY_MATRIX_MVP将会被UnityObjectToClipPos替代,参考https://blog.csdn.net/weixin_30631587/article/details/99771586

Shader <span class="s">"ShaderLearnChap5_1"  //这句话定义了Shader的名字
{
    //这里没有定义Properties语句块,事实上这不是必须定义的
    SubShader{
        //SubShader中没有进行任何自定义的渲染设置和标签设置
        Pass {
            //Pass中没有进行任何自定义的渲染设置和标签设置
            CGPROGRAM  
            //由CGPROGRAM和ENDCG包围的是CG代码片段,很重要
            #pragma vertex vert
            #pragma fragment frag //见下方注释1

            float4 vert(float4 v : POSITION) : SV_POSITION {
                return UnityObjectToClipPos(v);
            }//见下方注释2

            fixed4 frag() : SV_Target {
                return fixed4(1.0, 1.0, 1.0, 1.0);
            }//见下方注释3

            ENDCG
        }
    }
}
(a)注释1:pragma

#pragma vertex vert
#pragma fragment frag
//这两句告诉Unity,哪个函数包含了顶点着色器的代码,哪个函数包含了片元着色器的代码。更通用的编译指令表示如下:
#pragma vertex name1
#pragma fragment name2
虽然我们可以自己指定这两个name,但我们通常使用vert和frag,因为他们比较直观.
(b)注释2:vert函数

float4 vert(float4 v : POSITION) : SV_POSITION
{
    return UnityObjectToClipPos(v);
}
       这就是本例中的顶点着色器代码,它是逐顶点进行的.vert函数的输入v包含了这个顶点的位置,这是通过POSITION 语义指定的。它的返回值是一个float4类型的变量,它是该顶点在裁剪空间中的位置(通过函数名可以很明显地看到),POSITIONSV_POSITION 都是Cg/HLSL中的语义 (semantics) ,它们是不可省略的,这些语义将告诉系统用户需要哪些输入值,以及用户的输出是什么。
       在这里,POSITION告诉Unity把模型的顶点坐标填充到输入参数v中;
       SV_POSITION告诉Unity顶点着色器的输出是裁剪空间中的顶点坐标.
       关于各种语义的介绍将在后文中进行总结.
(c)注释3:frag函数

fixed4 frag() : SV_Target
{
    return fixed4(1.0, 1.0, 1.0, 1.0);
}
       类比vert函数,我们可以知道frag函数没有任何输入。它的输出是一个fixed4类型的变量,并且使用了SV_Target 语义进行限定。该语义告诉Unity把用户的输出颜色存储到一个渲染目标(render target)中,这里将输出到默认的帧缓存中。
       片元着色器中的代码很简单,返回了一个表示白色的fixed4类型的变量。片元着色器输出的颜色的每个分量范围在[0, 1],其中(0, 0, 0)表示黑色,而(1, 1, 1)表示白色。
(4)模型数据从哪里来?

       在上述例子中,我们在顶点着色器中通过POSITION语义得到了模型的顶点位置.以下的代码会告诉我们当要获取更多信息的时候要如何做:
举个例子:得到模型上每个顶点的纹理坐标和法线方向.

       在这个例子里,输入的语义不再是一个简单的顶点位置,而应该是一个结构体,新增结构体及修改顶点着色器之后的相关代码如下:
//使用一个结构体来定义顶点着色器的输入
            struct a2v
            {
                //POSITION语义告诉Unity,用模型空间的顶点坐标来填充vertex变量
                float4 vertex:POSITION;
                //MORMAL语义告诉Unity,用模型空间的法线方向填充normal变量
                float3 normal:NORMAL;
                //TEXCOORD0语义告诉Unity,用模型的第一套纹理坐标填充texcoord
                float4 texcoord:TEXCOORD0;
            };

            float4 vert(a2v v) : SV_POSITION //注意,这里输入变量去掉了POSITION语义,而是改成了刚才定义的a2v
            {
                return UnityObjectToClipPos(v.vertex);//注意这里的v对应也要改为v.vertex
            }
Unity支持的顶点着色器语义

       POSITION,TANGENT,NORMAL,TEXCOORD0,TEXCOORD1,TEXCOORD2,TEXCOORD3,COLOR等.
新建结构体的格式要求

struct StructName
{
    Type Name:Semantic;
    Type Name:Semantic;
    ...
};
       其中,语义部分Semantic不能够省略.
       通过上面的程序,可以看到我们将结构体应用于顶点着色器.通过这种自定义结构体的方法,可以在顶点着色器中访问各种模型数据.
a2v的含义

       虽然这个结构体的名字不是固定的,但为了方便理解,取名为a2v会方便记忆(application to vertex shader),也就是说,这个结构体a2v的意思是把数据从应用阶段传递到顶点着色器中.
语义是如何产生的?

       这些语义由材质的Mesh Renderer组件所提供.每帧调Draw Call的时候,Mesh Renderer组件会把它负责渲染的模型数据发送给Unity Shader,而由于模型往往是由三角面片组成的,且面由点组成,点会有很多数据,比如法线,切线,位置,纹理坐标等,所以顶点着色器中自然也可以访问这些模型数据.
(5)顶点着色器与片元着色器之间的通信

       实际应用中,往往会需要顶点着色器输出一些如模型法线,纹理坐标等信息到片元着色器,这就涉及到通信问题.为此.需要定义一个新的结构体.
Shader "ShaderLearnChap5_1"
{
    SubShader{

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-9-23 03:28 , Processed in 0.174097 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表