Unity Shader知识点(四) 基础纹理Shader

前言

此文及专栏系以《Shader入门精要》为基础整理的Unity Shader学习笔记,尽量以初学者视角还原(其实也就是半年前),错误还需指正。

本文是实操部分的第四个Shader,即基础纹理Shader,涉及的Shader相关基础概念可能不再赘述。本专栏仍在持续更新中,可作为相关Shader类型的查阅,也应可作为新手学习之用。

关于纹理类型Shader

其实,贴一张简单的图到3D物体上——这是计算机图形渲染最早的需求之一,也是众多进阶Shader的基础。在游戏制作过程中,纹理多数情况下是美术所关心的,其往往是通过uv坐标系来定义的。啥是uv呢?你可以理解为一种特殊的坐标系,是图像坐标和模型顶点的对应关系。其中,u是横向,v是纵向坐标,一般被归一化为[0,1]区间。

从网络上找张图帮助我们理解,这是被展开后的UV,因为负责贴图的美术不可能总是对着一个3d的模型进行绘制、对应之类的工作,3D模型一般都会做展UV、调整UV这样的工作,创造一个能够展示为平面的UV坐标系方便操作。在这个平面UV内,坐标就是我们说的UV坐标啦。

作为一个美术背景的读者,个人以为了解UV对写Shader还是非常重要的。话不多说,将一张简单的贴图贴到模型上(好像一张打印着贴图的纸包裹上去),就是我们今天要做的事。关于贴图,网络上其实有很多资源,自行搜索木纹、墙纸、建筑贴图,直接用png格式导入unity即可,或者从这个网站去找找专业的CG资源texture贴图网站

单张纹理Shader

 我们在unity里新建一个unlit shader,其实默认创建的就是一个贴图纹理Shader,不过,默认的不够完整,并且使用了过多封装好的用法,这里我们还是使用如下代码:

Shader "Unlit/TextureShader"
{
    Properties
    {
        _Color ("Color Tine",Color) = (1, 1, 1, 1)  //这是基础色彩
        _MainTex ("Texture", 2D) = "white" {}       //这是传入的纹理图片
        _Specular ("Specular", Color) = (1, 1, 1, 1)//这是高光色彩
        _Gloss ("Gloss",Range(8.0, 256)) = 20         //这是光泽度
    }
    SubShader
    {
        Pass
        {
            Tags{"LightMode" = "ForwardBase"}
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "Lighting.cginc"

            fixed4 _Color;
            sampler2D _MainTex;
            float4 _MainTex_ST;     //这是纹理的UV偏移属性
            fixed4 _Specular;
            float _Gloss;

            struct a2v
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                float2 texcoord : TEXCOORD0;
            };
            struct v2f
            {
                float3 worldNormal : TEXCOORD0;
                float3 worldPos : TEXCOORD1;
                float2 uv : TEXCOORD2;
                float4 pos : SV_POSITION;
            };


            v2f vert (a2v v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.worldNormal = UnityObjectToWorldNormal(v.normal);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
                o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);       //对UV按照输入值进行变换
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed3 worldNormal = normalize(i.worldNormal);
                fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
                
                fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
                fixed3 diffuse = _LightColor0.rgb * albedo * max(0,dot(worldNormal, worldLightDir));
                fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
                fixed3 halfDir = normalize(worldLightDir + viewDir);
                fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(worldNormal,halfDir)),_Gloss);
                return fixed4(ambient + diffuse + specular, 1.0);
            }
            ENDCG
        }
    }
    Fallback "Specular"
}

前几次文章是把代码按结构切成了块分析,单独的代码块有很多不规范的地方,这次按照Shader 的结构分解,一些符号什么的就跳过了。

Properties块

Properties
    {
        _Color ("Color Tine",Color) = (1, 1, 1, 1)  //这是基础色彩
        _MainTex ("Texture", 2D) = "white" {}       //这是传入的纹理图片
        _Specular ("Specular", Color) = (1, 1, 1, 1)//这是高光色彩
        _Gloss ("Gloss",Range(8.0, 256)) = 20         //这是光泽度
    }

properties块是用来声明传入的外部变量的,用户可以在Material的inspector面板进行修改,如本Shader就可以在该面板指定Texture,用一张2D平面图即可。

本Shader仍然需要漫反射基础色彩(可以控制物体的基础色彩,相当于底)和高光色彩,另外还有高光反射中使用的光泽度也一并传入,比较重要的是Texture格式的变量_MainTex,它的格式是2D也就是2D图片,默认值是white,全白色贴图。

Tag标签和相关声明

            Tags{"LightMode" = "ForwardBase"}
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "Lighting.cginc"

            fixed4 _Color;
            sampler2D _MainTex;
            float4 _MainTex_ST;     //这是纹理的UV偏移属性
            fixed4 _Specular;
            float _Gloss;

接下来我们声明Shader的Tags、引用变量和使用库。首先我们声明Tags,LightMode标签这里用于定义整个Pass块在Unity渲染流水线中的角色,值是ForwardBase(具体作用是一种光照模式,如果我们不声明这个Tag会导致一些内置变量无法使用)

我们指定顶点着色器和片元着色器函数,vert和frag,这里不再进行赘述。我们包含unity的内置文件Lighting.cginc,该文件包括一些光照渲染中的常用参数,如果缺失会导致接下来内置变量无法使用。

接下来一段将我们在properties块中传入的变量重新声明,注意这里使用了_MainTex_ST变量,它的名字是纹理贴图_ST,代表纹理的UV偏移属性,通俗解释就是这张贴图被包裹在你的模型上时,是否进行了缩放和移动,其4个分类分别是uv方向的缩放和偏移。看下面这张图,tilling是缩放,offset是偏移,可以在材质的inspector面板中手动调节,如果你尝试改变会发现,这张贴图“移动”或者“变大变小”了。事实上,这个变量是自动包含在我们的贴图2D变量下的,为了使用我们专门进行声明。

 输入输出结构体

            struct a2v
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                float2 texcoord : TEXCOORD0;
            };
            struct v2f
            {
                float4 pos : SV_POSITION;
                float3 worldNormal : TEXCOORD0;
                float3 worldPos : TEXCOORD1;
                float2 uv : TEXCOORD2;
            };

还是a2v和v2f,我们熟悉的顶点着色器输入和输出结构体。

在输入结构体也就是a2v中,我们定义了以下变量:顶点坐标vertex,用POSITION语义传入;法线normal;texcoord用于存储模型的纹理坐标,用TEXCOORD0语义传入相似语义还有TEXCOORD1、2、3等等,我对这一点非常疑惑,查了一下才明白,这其实是同种模型的不同UV,是美术定义的【美术:我没有别瞎说,其实美术也不一定知道这事】,用于贴图的映射和改变,例如左右脸贴图就可以用同种贴图不同UV

输出结构体这里,首先是要传入的坐标pos,其语义固定为SV_POSITION;

顶点着色器

            v2f vert (a2v v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.worldNormal = UnityObjectToWorldNormal(v.normal);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
                o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);       //对UV按照输入值进行变换
                return o;
            }

接下来我们看顶点着色器的代码,其函数结构不再解释。

首先用UnityObjectToClipPos将输入的顶点坐标,裁剪到裁剪空间中(没看过前面文章的可以理解为将不显示的坐标排除);用内置函数UnityObjectToWorldNormal将法线转换为世界坐标系;将顶点坐标vetex与内置的转换矩阵unity_ObjectToWorld相乘,转换成世界坐标系;最后,对uv进行处理,使用输入的uv也就是我们之前定义的_MainTex.ST来进行偏移和缩放,用的函数是内置的TRANSFORM_TEX,它的两个参数是顶点纹理坐标和纹理名,能够自动将uv按照输入值进行变换(其实也可以手动四则运算,不过既然unity提供了,何必呢)

片元着色器

            fixed4 frag (v2f i) : SV_Target
            {
                fixed3 worldNormal = normalize(i.worldNormal);
                fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
                
                fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
                fixed3 diffuse = _LightColor0.rgb * albedo * max(0,dot(worldNormal, worldLightDir));

                fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
                fixed3 halfDir = normalize(worldLightDir + viewDir);
                fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(worldNormal,halfDir)),_Gloss);
                return fixed4(ambient + diffuse + specular, 1.0);
            }

片元着色器结构和参数还是不解释,首先拿到顶点着色器输出的v2f,计算世界坐标系下的法线和光照方向并用normalize归一化,其中UnityWorldSpaceLightDir(i.worldPos)是计算worldPos这一点到光源的方向向量。

然后,我们用tex2D对纹理进行采样。什么是采样呢?我们这里毕竟是片元着色器,是显示中的片元,并不对应纹理的每个像素点,那么就有必要对这个片元应该显示的像素点采样得出一个唯一结果。tex2D就是干这个事,它的头一个参数是采样纹理,第二个则是uv坐标,返回值是计算得到的色彩信息,与基础色彩_Color相乘,就是片元着色器计算出的纹素(可以理解为最终色彩的一部分——表面颜色)。

将纹素与环境光也就是UNITY_LIGHTMODEL_AMBIENT的xyz值相乘,得到环境光部分;回过头,我们用纹素albedo计算出diffuse也就是漫反射结果,这里其实是把漫反射Shader里_LightColor0.rgb * _Diffuse.rgb * halfLambert中的diffuse信息换成了我们的纹素,也就是默认的漫反射纹理换成了我们计算后的结果。

接下来的活儿首先是用UnityWorldSpaceViewDir(类似于UnityWorldSpaceLightDir)得到观察方向向量,然后仍然是狸猫换太子,我们高光反射的计算式是_LightColor0.rgb * _Specular.rgb * pow(saturate(dot(reflectDir, viewDir)),_Gloss),那么我们这里需要变换的是viewDir,这里用一个非常粗暴的方式,即halfDir(光照入射向量和观察向量的均值)替代viewDir,计算出高光反射结果。

最后,纹素(环境光)、漫反射、高光三个结果相加,加上第四个分量1.0,我们的计算就大功告成了!

总结

看看结果?左边的就是我们刚刚写的Shader的渲染结果,纹理我用的网上下载的墙壁纹理,可见效果还是不错的,因为本次没有使用半兰伯特修正,后面是全黑的有点难看,实际运用可以改正。

很多人学到这里可能还是没有弄清楚我们的着色器为什么这样写、为什么又这样返回,事实上,我也是如此。不过写Shader是一个在高度封装好的平台上的工作,我觉得我们应该理解:顶点着色器和片元着色器是我们编辑的重点,它们分别对应模型顶点的计算和屏幕显示结果片元(或理解为像素)的计算,这些乱七八糟的计算只是渲染很少的一部分,但却是我们对图形渲染过程作改变非常好用的部分。那么我们只需要提出我们需要修改的变量,再指定他们的运算,再一股脑返回给unity就好——这样想或许会让你省心些,当然,更深入的学习必然是要看看图形学的。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值