Unity Shader 入门到改行4——最简纹理采样

运用你的想象力II

0.本文示例代码地址

GitHub

1. 纹理和纹素

  • 什么是纹理?
    先直接把纹理理解成一张图片吧。

  • 纹理在渲染过程中有什么作用?
    作用很多,也很重要。这里先只关注它最简单和最基本的作用:用来表现表面的漫反射颜色。可以这么理解:想象一个地球仪,把贴在地球仪表面的图片撕下来展开,就得到一个纹理,这个纹理在某一个位置的颜色就是地球仪表面对应位置的颜色。

  • 什么是纹素(texel)
    纹理是一张图片,图片有大小,例如一张4x4的纹理,只有16个纹素,256x256的纹理,包含256x256个纹素,每个纹素具有单一的颜色

2. uv 坐标

  • 什么是 uv 坐标?
    uv 坐标首先是一个二维坐标,而坐标用来确定一个位置,uv 就是用来确定一个纹素在某张纹理上的位置。

  • uv 坐标的范围是多少?
    纹理的大小各异,希望能用同一个范围内的坐标来表示所有大小的纹理的 uv 坐标,所以 uv 坐标通常是归一化后的坐标,区间在[0,1]。

  • 如何获得 uv 坐标,或者说 uv 坐标存放在哪里?
    你需要渲染一个物体时,首先需要有这个物体的模型(顶点数据),以及某一个顶点对应图片的哪一个坐标,所以 uv 坐标是存放在顶点数据中的。在模型中,每一个顶点有一个属性,表示uv坐标。在3d游戏中,3d建模人员在建模软件中完成顶点绑定纹理坐标的操作。

  • 纹理采样是什么意思?
    给定一个 uv 坐标和一张纹理,获得这张纹理在 uv 坐标处的纹素的颜色值。这个过程就是纹理采样。GPU 中有专门的纹理采样硬件单元。

  • uv 坐标会超过取值范围吗?
    通常存储在模型顶点中的 uv 坐标都在 [0,1] 区间内。但是在进行纹理采样时,传入的 uv 却不一定,这是因为我们可以在 着色器内对 uv 进行各种计算。
    在进行纹理采样时,如果接受到一个 不在 [0, 1] 区间的纹理坐标时,如何确定该返回哪个纹素,由纹理的“平铺模式”确定。在 Unity 中,纹理的“平铺模式”通常由引擎开发选项进行设置。

  • Unity 中的纹理坐标系
    OpenGL 中纹理坐标原点在左下角,DirectX 中纹理坐标原点在左上角。Unity中采用和 OpenGL 一直的纹理坐标系。


    Unity中的纹理坐标系

3. 采样单张纹理

在之前的 Blinn-Phong 中,我们使用如下的方式来计算物体表面的漫反射部分颜色:

// 半兰伯特计算漫反射
fixed3 diffuse = _LightColor0.rgb * _DiffuseColor.rgb * (dot(lightDir, worldNormal) * 0.5 + 0.5);

这样我们只能表示一个纯色物体,当我们需要表示物体表面各个位置不同颜色时,可以将不同位置处的颜色记录到纹理中,然后在渲染的时候读取这个纹理,把纹理中存储的颜色取出来,应用到漫反射计算公式中的 _DiffuseColor 即可。

现在我们先不考虑高光、环境光和自发光的情况,仅仅考虑如何将一个图片“贴”到物体的表面。
先上整体 Shader 代码 04_Texture.shader:

Shader "Shader_Examples/04_Texture"
{
    Properties
    {
        _MainTex ("MainTex", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque"}

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag                       

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {                           
                float4 vertex : SV_POSITION;
                float2 uv : TEXCOORD0;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            
            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);// 顶点世界坐标
                o.uv = v.uv;
                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                // 纹理采样
                fixed3 diffuse = tex2D(_MainTex, i.uv);
                return fixed4(diffuse, 1);
            }
            ENDCG
        }
    }
}

下面逐步分析这段代码的各个部分

3.1 纹理属性的声明

    Properties
    {
        _MainTex ("MainTex", 2D) = "white" {}
    }
  • 2D 是纹理类型属性的声明方式,"white" {} 是纹理的默认值,没有设置时使用内置的白色纹理作为参数。

3.2 顶点着色器输入和片段着色器输入

struct appdata
{
    float4 vertex : POSITION;
    float2 uv : TEXCOORD0;
};

struct v2f
{                           
    float4 vertex : SV_POSITION;
    float2 uv : TEXCOORD0;
};
  • app_data 中使用语义TEXCOORD0 将模型第一组纹理坐标指定给 uv 字段,传递给顶点着色器
  • v2f 中添加变量 uv,用来接受顶点着色器中的 uv 字段,以便片段着色器中使用

3.3 为属性声明相应的变量

sampler2D _MainTex;
float4 _MainTex_ST;
  • _MainTex 是对应纹理属性的变量
  • 针对每一个纹理属性,都必须声明一个 float4 类型的变量:纹理名_ST,可以通过这个变量获得纹理的缩放平移值。也就是在 Unity 材质面板中如下的属性:
    ST变量

    现在只需要知道声明的 _ST 变量与图中这两个值有关就行了。

3.4 顶点着色器

v2f vert (appdata v)
{
    v2f o;
    o.vertex = UnityObjectToClipPos(v.vertex);// 顶点世界坐标
    o.uv = v.uv;
    return o;
}
  • 顶点着色器的基本作用:1. 顶点坐标变换
  • 顶点着色器的基本作用:2. 将顶点的 uv 坐标传递给片段着色器

3.5 片段着色器

fixed4 frag (v2f i) : SV_Target
{
    // 纹理采样
    fixed3 diffuse = tex2D(_MainTex, i.uv);
    return fixed4(diffuse, 1);
}
  • tex2D 是一个内置操作,作用是根据纹理坐标,取出纹理对应的颜色

使用上面的 04_Texture.shader ,创建材质并指定给物体,然后照一张贴图指定给材质,可以得到如下的效果:


简单纹理采样

4. 采样贴图后加入环境光和高光

在将贴图“贴”到物体表面后,我们再给物体添加环境光和高光,关于高光和环境光,请看我之前的文章简单光照模型,添加环境光和高光后的 shader 代码如下:

Shader "Shader_Examples/04_BlinnPhong_Texture"
{
    Properties
    {
        _MainTex ("MainTex", 2D) = "white" {}
        _SpecularColor ("SpecularColor", Color) = (1,1,1,1)
        _Gloss ("Gloss", Range(8, 256)) = 20
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" "LightMode"="ForwardBase"}

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag       

            #include "Lighting.cginc"               

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
                float3 normal : NORMAL;
            };

            struct v2f
            {                           
                float4 vertex : SV_POSITION;
                float2 uv : TEXCOORD0;
                float3 worldNormal : TEXCOORD1;
                float3 worldPos : TEXCOORD2;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            float4 _SpecularColor;
            float _Gloss;
            
            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);// 顶点世界坐标

                o.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);// 法线变换
                o.worldPos = mul(unity_ObjectToWorld, o.vertex);
                o.uv = v.uv;
                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                float3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
                float3 worldNormal = normalize(i.worldNormal);
                float3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
                float3 halfDir = normalize(lightDir + viewDir);

                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;      
                
                // 纹理采样
                fixed3 diffuse = tex2D(_MainTex, i.uv).rgb * _LightColor0.rgb * (dot(lightDir, worldNormal) * 0.5 + 0.5);
                
                // 高光
                fixed3 specular = _LightColor0.rgb * _SpecularColor.rgb * pow(saturate(dot(worldNormal, halfDir)), _Gloss);
                return fixed4(diffuse + ambient + specular, 1);
            }
            ENDCG
        }
    }
}

渲染出的效果如下图:


纹理+环境光+高光

4. 纹理的其它应用

4.1 法线纹理(法线贴图)

法线贴图用来表现物体表面的凹凸效果,在贴图中存储物体在 "凹凸" 效果下的法线信息,渲染时将存储的法线信息采样出来,替代物体表面法线来进行光照计算,“还原”凹凸效果。具体法线贴图的应用,可以参考Unity Shader 入门到改行5——法线贴图

4.2 渐变纹理(渐变贴图)
渐变贴图是一种更灵活控制表面漫反射的方法,提供一张渐变贴图(通常是一维贴图),与传统的漫反射光照计算结合,可以提供各种有趣的渐变效果。


三种不同的渐变贴图控制漫反射效果

上图来自 UnityShader入门精要 中使用的3种渐变贴图和应用效果,对渐变贴图使用的核心代码是:

// Use the texture to sample the diffuse color
fixed halfLambert  = 0.5 * dot(worldNormal, worldLightDir) + 0.5;
fixed3 diffuseColor = tex2D(_RampTex, fixed2(halfLambert, halfLambert)).rgb * _Color.rgb;
                
fixed3 diffuse = _LightColor0.rgb * diffuseColor;

渐变贴图通常为 一维 贴图,所以这里采样时用的 u 和 v 坐标一样,从代码可以看出,表面的法线与光线越夹角越小,漫反射颜色越接近渐变贴图的右边颜色,表面法线与光线夹角越大,漫反射颜色越接近渐变贴图的左边颜色。

4.3 遮罩纹理(遮罩贴图)

遮罩纹理是一种可以更加精准控制模型表面性质的方法,例如,我希望物体的某一部分不会产生高光,可以生成一张遮罩纹理,把物体在不产生高光部分的颜色值分量r设置为0并写到遮罩纹理中。渲染时采样遮罩纹理,使用 r 分量与计算出的 specular 相乘得到最终的 高光分量。使用这种方法可以灵活和精准地控制表面渲染效果。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容