Unity3d Shader基础学习(二):漫反色

发表于2017-06-11
评论0 1.9k浏览

上一篇我们了解了Unity中Shader相关介绍以及实现了一个最简单的Shader。

这一篇我们会学习中更为立体的Shader,即通过光照计算物体表面的漫反色。这里,我们通过兰伯特光照模型原理实现物体漫反色原理。关于标准光照模型,我们留在下次再介绍。好,先来看看我们这次所实现效果:


可能录制时采样问题,我这里再提供一张静态图。


这里我实现了关于逐顶点和逐像素的漫反射,虽然看起来感觉差不多,但是还是有细微的区别,毕竟处理方式不同,相对逐顶点而言,逐像素看起来会更柔化一些。

好,我们再来看看代码实现,先看逐顶点:

//Shader模块定义  
Shader "xiaolezi/Lambert Lighting Model Per-Vertex Shader"  
{  
    //属性设置  
    Properties  
    {  
        //定义一个物体表面颜色,格式:[属性名]([Inspector面板显示名字],属性类型)=[初始值]  
        _DiffuseColor("Diffuse Color", Color) = (1, 1, 1, 1)  
    }  
    //第一个SubShader块  
    SubShader  
        {  
            //第一个Pass块  
            Pass  
            {  
                //指定灯光渲染模式  
                Tags{ "LightMode" = "ForwardBase" }  
                //开启CG着色器编辑模块  
                CGPROGRAM  
                //定义顶点着手器函数名  
                #pragma vertex vert  
                //定义片段着色器函数名  
                #pragma fragment frag  
                //包含相关头文件  
                #include "UnityCG.cginc"      
                #include "Lighting.cginc"  
                //定义一个从应用程序到顶点数据的结构体  
                struct appdata  
                {  
                    float4 vertex : POSITION;//POSITION语义:表示从该模型中获取到顶点数据  
                    float3 normal : NORMAL;  //NORMAL语义:获取该模型法线  
                };  
                //定义一个从顶点数据到片段数据的结构体  
                struct v2f  
                {  
                    float4 pos : SV_POSITION;//SV_POSITION语义:从顶点输出数据中获取到顶点数据  
                    fixed3 diffuse : COLOR0;//COLOR0语义:定义颜色信息  
                };  
                //从属性模块中取得该变量  
                fixed4 _DiffuseColor;  
                //顶点着色器函数实现  
                v2f vert(appdata v)  
                {  
                    v2f o;  
                    o.pos = UnityObjectToClipPos(v.vertex);//让模型顶点数据坐标从本地坐标转化为屏幕剪裁坐标  
                    fixed3 normalDir = normalize(UnityObjectToWorldNormal(v.normal));   //计算世界法线方向  
                    fixed3 lightDir = normalize(ObjSpaceLightDir(v.vertex));            //计算灯光方向  
                    float Lambert = max(dot(normalDir, lightDir), 0);//兰伯特值  
                    o.diffuse = _LightColor0.rgb * _DiffuseColor.rgb * Lambert; //计算漫反色  
                    return o;  
                }  
                //片段着色器函数实现  
                fixed4 frag(v2f f) : SV_Target//SV_Target语义:输出片元着色器值,可直接认为是输出到屏幕颜色  
                {  
                    return fixed4(f.diffuse, 1.0);  
                }  
                    //结束CG着色器编辑模块  
                    ENDCG  
            }  
        }  
        Fallback "Diffuse"//默认着色器  
}  

接着是逐像素:
//Shader模块定义  
Shader "xiaolezi/Lambert Lighting Model Per-Fragment Shader"  
{  
    //属性设置  
    Properties  
    {  
        //定义一个物体表面颜色,格式:[属性名]([Inspector面板显示名字],属性类型)=[初始值]  
        _DiffuseColor("Diffuse Color", Color) = (1, 1, 1, 1)  
    }  
    //第一个SubShader块  
    SubShader  
        {  
            //第一个Pass块  
            Pass  
            {  
                //指定灯光渲染模式  
                Tags{ "LightMode" = "ForwardBase" }  
                //开启CG着色器编辑模块  
                CGPROGRAM  
                //定义顶点着手器函数名  
                #pragma vertex vert  
                //定义片段着色器函数名  
                #pragma fragment frag  
                //包含相关头文件  
                #include "UnityCG.cginc"      
                #include "Lighting.cginc"  
                //定义一个从应用程序到顶点数据的结构体  
                struct appdata  
                {  
                    float4 vertex : POSITION;//POSITION语义:表示从该模型中获取到顶点数据  
                    float3 normal : NORMAL;  //NORMAL语义:获取该模型法线  
                };  
                //定义一个从顶点数据到片段数据的结构体  
                struct v2f  
                {  
                    float4 pos : SV_POSITION;//SV_POSITION语义:从顶点输出数据中获取到顶点数据  
                    float3 normal : COLOR0;//COLOR0语义:定义法线变量  
                    float4 vertex : COLOR1;//COLOR1语义:定义顶点变量  
                };  
                //从属性模块中取得该变量  
                fixed4 _DiffuseColor;  
                //顶点着色器函数实现  
                v2f vert(appdata v)  
                {  
                    v2f o;  
                    o.pos = UnityObjectToClipPos(v.vertex);//让模型顶点数据坐标从本地坐标转化为屏幕剪裁坐标  
                    o.normal = v.normal;  
                    o.vertex = v.vertex;  
                    return o;  
                }  
                //片段着色器函数实现  
                fixed4 frag(v2f f) : SV_Target//SV_Target语义:输出片元着色器值,可直接认为是输出到屏幕颜色  
                {  
                    fixed3 normalDir = normalize(UnityObjectToWorldNormal(f.normal));   //计算世界法线方向  
                    fixed3 lightDir = normalize(ObjSpaceLightDir(f.vertex));            //计算灯光方向  
                    float Lambert = max(dot(normalDir, lightDir), 0);//兰伯特值  
                    fixed3 diffuse = _LightColor0.rgb * _DiffuseColor.rgb * Lambert;    //计算漫反色  
                    return fixed4(diffuse, 1.0);  
                }  
                    //结束CG着色器编辑模块  
                    ENDCG  
            }  
        }  
        Fallback "Diffuse"//默认着色器  
}  

好,现在我们先不解释上述代码,我们先来了解一下兰伯特光照原理。先来看一张图:


其实我们兰伯特计算原理就是法线方向向量与灯光方向向量的点积,即兰伯特值Lambert = NormalDir * LightDir *Cos(夹角a)。由此我们可以得知,夹角越大,兰伯特值越大,物体表面越亮。

知道兰伯特原理之后,我们就可以来直接看代码了,基本的那些代码就不多说,注释也有说明,我们来看看不一样的地方。

先来看逐顶点着色器部分,逐顶点的是指把所有颜色相关计算都在顶点着色器中计算,而最终通过一个颜色变量传递给片元着色器直接输出。我们直接来看Pass语句块实现原理。

第一行代码:

//指定灯光渲染模式  
Tags{ "LightMode" = "ForwardBase" }  


首先,因为考虑到灯光颜色对物体影响,我们需要得到灯光的变量_LightColor0。而这个变量虽然可以通过包含灯光文件"Lighting.cginc"来获取得到,但是如果没有指定灯光的渲染模式,该变量并不能正确被赋值,那么在计算的时候,就会导致实现的效果与预期效果有所偏差,这也不是我们所想要的结果。

接着,引入两个头文件,可以通过所引入头文件获取当中相关变量和方法。

//包含相关头文件  
#include "UnityCG.cginc"      
#include "Lighting.cginc"  


这里建议读者有空可以多去看看Unity中相关的CG头文件里的内容,具体目录在:

"安装目录/Unity/Editor/Data/CGIncludes/"

如上面的"UnityCG.cginc",里面有相关坐标的转化,例如:我们上述代码中所实现的ObjSpaceLightDir方法,UnityObjectToWorldNormal方法,都是在当中定义实现的,是相当值得一看的,这里就不多介绍了。

然后,我们来看看我们的顶点函数中所实现的兰伯特值:

//顶点着色器函数实现  
v2f vert(appdata v)  
    {  
        v2f o;  
        o.pos = UnityObjectToClipPos(v.vertex);//让模型顶点数据坐标从本地坐标转化为屏幕剪裁坐标  
        fixed3 normalDir = normalize(UnityObjectToWorldNormal(v.normal));   //计算世界法线方向  
        fixed3 lightDir = normalize(ObjSpaceLightDir(v.vertex));            //计算灯光方向  
        float Lambert = max(dot(normalDir, lightDir), 0);//兰伯特值  
        o.diffuse = _LightColor0.rgb * _DiffuseColor.rgb * Lambert; //计算漫反色  
        return o;  
    } 


因为计算兰伯特值需要得到法线和光照两个方向向量,而法线可以通过模型法线转换得到,光照向量可以由模型顶点得到,所以我们需要在顶点结构体中定义了一个法线语义来获取模型法线:
//定义一个从应用程序到顶点数据的结构体  
struct appdata  
    {  
        float4 vertex : POSITION;//POSITION语义:表示从该模型中获取到顶点数据  
        float3 normal : NORMAL;  //NORMAL语义:获取该模型法线  
    }; 

在计算完兰伯特值之后,就可以计算物体的漫反色颜色值了,计算完成之后,我们需要在片元结构体中定义一个漫反色值来存储该颜色值:
//定义一个从顶点数据到片段数据的结构体  
struct v2f  
    {  
        float4 pos : SV_POSITION;//SV_POSITION语义:从顶点输出数据中获取到顶点数据  
        fixed3 diffuse : COLOR0;//COLOR0语义:定义颜色信息  
    };  

最后,我们在片元函数中直接输出该漫反色值即可:
//片段着色器函数实现  
fixed4 frag(v2f f) : SV_Target//SV_Target语义:输出片元着色器值,可直接认为是输出到屏幕颜色  
    {  
        return fixed4(f.diffuse, 1.0);  
    }  

和上一篇不同的是,我最后还加上一句使用默认的着色器:
Fallback "Diffuse"//默认着色器  

这其实就是在硬件条件不支持SubShader块时,会尽可能使用该当前硬件所支持的漫反色渲染。

以上就是使用兰伯特原理实现的逐顶点着色器代码。


相信对于逐像素实现原理,如果理解了逐顶点的代码,这个自然也就好理解很多了。

逐像素实现的漫反色其实就是将所有颜色相关计算的都放在片元着色器中。
对于类似代码就不多说了,我们直接来看片元着色器函数中的实现:

//片段着色器函数实现  
fixed4 frag(v2f f) : SV_Target//SV_Target语义:输出片元着色器值,可直接认为是输出到屏幕颜色  
    {  
        fixed3 normalDir = normalize(UnityObjectToWorldNormal(f.normal));   //计算世界法线方向  
        fixed3 lightDir = normalize(ObjSpaceLightDir(f.vertex));            //计算灯光方向  
        float Lambert = max(dot(normalDir, lightDir), 0);//兰伯特值  
        fixed3 diffuse = _LightColor0.rgb * _DiffuseColor.rgb * Lambert;    //计算漫反色  
        return fixed4(diffuse, 1.0);  
    }  


同理,求兰伯特值需要传递法线和灯光方向向量,所以在片元结构体数据中需要存储模型法线以及模型坐标:
//定义一个从顶点数据到片段数据的结构体  
struct v2f  
    {  
        float4 pos : SV_POSITION;//SV_POSITION语义:从顶点输出数据中获取到顶点数据  
        float3 normal : COLOR0;//COLOR0语义:定义法线变量  
        float4 vertex : COLOR1;//COLOR1语义:定义顶点变量  
    };  

而在顶点函数中,我们只是把片元结构数据进行填充:
//顶点着色器函数实现  
v2f vert(appdata v)  
    {  
        v2f o;  
        o.pos = UnityObjectToClipPos(v.vertex);//让模型顶点数据坐标从本地坐标转化为屏幕剪裁坐标  
        o.normal = v.normal;  
        o.vertex = v.vertex;  
        return o;  
    }  

当然了,填充的所需的也就是顶点模型位置和顶点法线,便从顶点结构数据中得到:
//定义一个从应用程序到顶点数据的结构体  
struct appdata  
    {  
        float4 vertex : POSITION;//POSITION语义:表示从该模型中获取到顶点数据  
        float3 normal : NORMAL;  //NORMAL语义:获取该模型法线  
    };  


以上便是对逐顶点及逐像素代码进行分析。

可能对读者而言,感觉Unity中Shader的学习有些时候学起来还是很迷,而且有很多不知所以然。个人建议,读者先把效果实现出来,然后再一点点分析当中不明所以的地方,查查资料,百百度都是可以的,只有通过不断地摸索学习,才能进步哈。


本章源码可以在GitHub仓库找到:GitHub仓库地址


希望以上文章能够帮助到你,Happy Coding...


Unity3dShader基础学习:

Unity3DShader基础学习(一):Shader基础知识

Unity3dShader基础学习(三):光照模型

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引

0个评论