Shader笔记——4.纹理基础

纹理采样的原理

纹理展开技术

美术人员在建模时,会在建模软件中使用纹理展开技术纹理映射坐标 Texture Mapping Coordinates存储到每个顶点上,储存的纹理映射坐标定义了该顶点在纹理中对应的二维坐标(u,v),所以纹理映射坐标又称UV坐标。

纹理映射技术

纹理映射技术Texture Mapping,用于逐纹素Texel的控制模型的颜色

纹理的大小多种多样,但是UV坐标都会被归一化到[0,1]的范围内,而在采样时遇到不在[0,1]范围内坐标的时候,纹理属性中的Wrap Mode会决定纹理的在平铺时的具体行为。

OpenGL和DirectX在二维纹理空间坐标系并不相同,OpenGL里纹理空间的原点位于左下角,而在DirectX中原点位于左上角,我们在Unity引擎使用的和OpenGL的一样位于左下角,而跨平台问题Unity引擎会进行处理

纹理的形状可以是非正方形的,但是长宽的大小应该是2的幂,因为非2的幂大小的纹理Non Power of Two(NPOT)会占用更多的内存空间且GPU读取NPOT纹理的速度也会下降,甚至某些平台并不支持NPOT纹理,在Unity引擎中,Unity要对NPOT纹理进行内部处理缩,将大小放为最近的2的幂

Unity中纹理的属性

Unity中的纹理,就是在游戏物体上或者包裹在游戏物体周围的图片或视频文件,用来给游戏物体一个视觉上的效果。

  • Unity会识别3D工程Assets文件夹下的所有图片或视频文件并保存为Texture,在2D工程中会被保存成Sprite。
  • 只要图片或视频文件满足Unity指定的大小要求,它就会被导入并优化以供游戏使用。
  • 对于多层psd或tiff文件,这些文件在导入Unity时会自动合并,并且原psd或tiff文件的大小不会对游戏造成大小损失。因为合并图层的过程发生在Unity内部,而不是原psd或tiff文件本身。

导入素材窗口分为两部分,上方的纹理导入器Texture Importer,下方的预览窗口

纹理导入器中的各个属性确定了图片如何从工程的Assets文件夹导入到Unity Editor

Capture

纹理类型Texture Type

Texture Type 通过选择的纹理类型Texture type,来确定导入纹理的用途
Default 对大部分纹理最常见的纹理类型设置,在导入纹理时它提供大部分的属性设置
Normal map 将纹理类型设置为法线贴图,贴图会转换颜色到一个适合的实时法线映射的格式
Sprite(2D and UI) 如果你要将此纹理用于2D游戏的UI则选择此选项为你所需要的纹理类型
Cursor 如果你要将此纹理用作一个自定义的游标则选择此选项为纹理类型
Lighting Map 如果你要将此纹理用作一个光照图则选择此选项为纹理类型,这个选项会允许对一些特殊格式进行编码,(根据平台的不同,如RGBM或者dLDR)

为导入的纹理选择合适的类型,只有正确设置类型后才能向Shader传递正确的纹理,某些情况Unity还可以对纹理进行优化

单张纹理

使用单张纹理时,要在Properties属性代码块中添加一个_MainTex属性,同时也要在CG代码块中添加两个与_MainTex属性相配的变量,sampler2D类型的_MainTex和float4类型的_MainTex_ST来和材质面板中的属性建立联系,_MainTex_ST后缀ST分别指缩放Scale和平移Transform的缩写,_MainTex_ST.xy存储的是缩放值,_MianTex_ST.zw存储的是位移值

Shader "Custom/Texture"{
    Properties{
        _MainTex("Main Tex", 2D) = "white"{}
        //code elide
    }
    SubShader{
        pass{
            Tages{"LightMode" = "ForwardBase"}
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "Lighting.cginc"
                sampler2D _MainTex;
                float4 _MainTex_ST;
            //code elide
            v2f vert(a2v v){
                v2f o;
                o.pos = mul(UNITY_MATRIX_MVP,v.vertex);
                o.worldNormal = UnityObjectWorldNormal(v.normal);
                //o.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
                o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
                return o;
            }
            //code elide
            ENDCG
        }
    }
    Fallback "Specular"
}

法线纹理

凹凸映射Bunp Mapping,是使用一张纹理来修改模型表面的法线来为模型提供更为多的细节(这种方法并不会真的改变顶点位置,仅是看起来像是凹凸的)
进行凹凸映射的方法主要有两种,一种是使用一张高度纹理Height Map来模拟表面位移,得到修改后的法线值,这种方法称为高度映射Height Map;另一种方法是使用一张法线纹理Normal Map来直接存储表面法线,这种方法称之为法线映射Normal Map

高度纹理Height Map

使用高度纹理来实现凹凸映射,高度纹理中存储的是强度值Intensity,用于表示模型表面局部的海拔高度,这种方式的优点是非常直观,颜色越浅表示越往外突起,颜色越深表示越向内凹陷;缺点是计算比较复杂,实时计算时不能直接得到表面法线,需要计算像素的灰度值而消耗更多的性能

法线纹理Normal Map

使用法线纹理来实现凹凸映射,法线纹理中直接存储的就是表面法线的方向,法线方向的分量范围是[-1,1],而像素的分量范围是[0,1],所以我们需要进行pixel = (normal + 1)/2的映射,或者其逆函数normal = pixel * 2 - 1

  • 模型空间存储法线
    由于方向是相对于坐标空间的,所以对于模型顶点的法线,是定义在模型空间中的,可以将其映射修改后储存在一张纹理中;因为多有的法线都是在同一个坐标空间,即模型空间,而每个点存储的法线方向都是不同的,例如(0,1,0)映射修改后出储存位RGB(0.5,1,0.5)浅绿色,(0,-1,0)映射修改后出储存位RGB(0.5,0,0.5)紫色,所以模型空间下的法线纹理看起来是五颜六色的

  • 切线空间存储法线
    也可以使用模型顶点的切线空间Tangent Space,模型上的每一个顶点都有一个属于自己的切线空间,这个切线空间的原点是该顶点本身,Z轴是该顶点的法线方向n,X轴是顶点的切线方向t,而Y轴则是有Z轴和X轴的叉积得到,被称为副切线或者副法线,因为每个法线方向所在的坐标系都是不一样的,都是各自顶点的切线空间,如果一个顶点的法线方向不需要改变,则其法线方向就是切线空间的Z轴方向(0, 0, 1),映射存储位RGB(0.5, 0.5, 1)浅蓝色,所以切线空间下的法线纹理看起来几乎全部都是浅蓝色的

模型空间下的法线记录的都是绝对法线信息,仅能用于创建该法线纹理的模型使用而不能用于其他模型,切线空间下的法线纹理记录的是相对法线信息,可以被应用到不同的模型上;可以通过移动纹理的UV坐标来实现凹凸移动的效果;可以仅存储XY轴的方向,Z轴的方向可推导出来,进行压缩

使用法线纹理时,在Properties属性代码块中添加法线纹理以及控制凹凸程度的属性,并在CG代码块中添加与之相应的变量

Shader "Custoom/NormalMap"{
    Properties{
        _BumpMap("Normal Map", 2D) = "bump"{}
        _BumpScale("Bump Scale", Float) = 1.0
        //code elide
    }
    SubShader{
        Pass{
            Tags{"LightMode" = "ForwardBase"}
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "Lighting.cginc"

            sampler2D _BumpMap;
            float4 _BumpMap_ST;
            float _BumpScale;

            //在a2v结构体中通过TANGENT语义描述float4类型的tangent变量,让Unity把顶点的切线方向填充至tangent变量中,因为后续要通过tangent的w变量来获取副切线的方向,所以声明为float4
            struct a2v{
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                float4 tangent : TANGENT;
                float4 texcoord : TEXCOORD0;
            };
            //在顶点着色器中计算了切线空间下的光照和视角方向,所以在v2f结构体中使用lightDir和viewDir来接收变换后的光照和视角方向
            struct v2f{
                float4 pos : SV_POSITION;
                float4 uv : TEXCOORD0;
                float3 lightDir : TEXCOORD1;
                float3 viewDir : TEXCORD2;
            };

            v2f vert(a2v v){
                v2f o;
                o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
                //使用了一张普通贴图和一张法线贴图,所以需要存储两个纹理坐标,分别存储在float4 uv下的xy分量和zw分量
                o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
                o.uv.zw = v.texcoord.xy * _BumpTex_ST.xy + _BumpMap_ST.zw;
                //计算切线空间的副切线,对法线方向以及切向方向进行叉乘结果乘以v.tangent.w来决定副切线方向
                float3 binormal = cross(normalize(v.normal), normalize(v.tangent.xyz)) * v.tangent.w;
                //构建一个矩阵用来吧模型空间的向量变换到切线空间
                float3x3 rotation = float3x3(v.tangent.xyx, binormal, v.normal);
                ////或者直接使用内置宏来计算
                //TANGENT_SPACE_ROTATION;

                o.lightDir = mul(rotation, ObjSpaceLightDir(v.vertex)).xyz;
                o.viewDir = mul(rotation, ObjSpaceViewDir(v.vertex)).xyz;
                return o;
            }
            //在切线空间下及逆行光照计算
            fixed4 frag(v2f i) : SV_Target{
                fixed3 tangentLightDir = normalize(i.lightDir);
                fixed3 tangentViewDir = normalize(i.viewDir);

                fixed4 packedNormal = tex2D(_BumpMap, i.uv.zw);
                fixed3 tangentNormal;
                //tangentNormal.xy = (packedNormal.xy * 2 -1) * _BumpScale;
                //tangentNormal.z = sqrt(1.0 - salurate(dot(tangentNormal.xy, tangentNormal.xy)));
                ////如果在导入纹理时在纹理类型Texture Type中设置了类型位Normal Map
                tangentNormal = UnpackNormal(packedNormal);
                tangentNormal.xy *= _bumpScale;
                tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));
                fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
                fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(tangentNormal, tangentLightDir));
                fixed3 halfDir = normalize(tangentLightDir + tangentViewDir);
                fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(tangentNormal, halfDir)), _Gloss);
                return fixed4(ambient + diffuse + specular, 1.0);
            }

            ENDCG
        }
    }
    Fallback "Specular"
}

纹理形状Texture Shape

Texture Shape 用这个选项来确定纹理的形状,默认设置为2D
2D 对大部分纹理最常见的纹理形状设置,这个选项确定该图片文件为一个2D的纹理。这样的纹理经常被用来做3D网格的贴图或者GUI
Cube Cube选项定义该纹理为一个cubemap,这样的纹理被用来做天空盒或者反射环境

循环模式Wrap Mode

循环模式Wrap Mode决定了当纹理坐标超过了[0,1]范围后纹理会如何被平铺

Wrap Mode有两种模式,一种是Rrepeat,一种是Clamp

  • Repeat模式下,如果纹理坐标超过了1,则舍弃整数部分,用小数部分进行采样从而使纹理不断被重复
  • Clamp模式下,如果纹理坐标大于1,则截取到1,如果小于0,则截取到0

Wrap Mode模式结合纹理平铺Tiling属性和偏移Offset属性,决定纹理铺设的结果

滤波模式Fliter Mode

决定纹理由于变换而产生拉伸时会采用哪种滤波模式,滤波模式Filter Mode支持三种模式,Point、Bilinear、trilinear,它们得到的纹理滤波效果依次提升,但是消耗的性能也依次增大,纹理滤波会影响纹理在放大或者缩小时的图片质量

  • 放大纹理,三种滤波模式得到的效果依次提升
  • 缩小纹理,要比放大纹理复杂一些,因为缩小纹理时多个像素会对应一个目标像素并且往往还需要处理抗锯齿的问题,一个常用的方法就是多级渐远纹理技术Mipmapping

多级渐远纹理技术的原理是提前使用滤波处理来得到更小的图像,从而在距离摄像机较远时直接使用较小的纹理,缺点是需要消耗额外的空间来存储这些多级渐远纹理(通常多占用33%)

高级设置

Property Description
Non Power of 2 如果你要导入纹理的尺寸不是2的幂,那么这个选项会在导入时进行一个缩放,默认选项设置为 None ,即纹理大小是2的幂,纹理尺寸导入时不会发生缩放;ToNearest选项会将尺寸缩放到距离导入尺寸最近的2的幂的尺寸,例如导入时 257x511 px Texture,会被缩放为256x512 px (注意,如果最终平台的是IOS,则纹理格式会被转化为PVRTC,纹理需要是正方形的,经过ToNearest缩放会被缩放为512x512 px);ToLarger选项会将该贴图缩放为512x512 px;ToSmaller选项会将该纹理缩放为256x256 px。
Read/Write Enabled 开启这个选项将会允许从脚本方法中获取纹理数据,例如Texture2D.SetPixels, Texture2D.GetPixels 和其他的 Texture2D方法。需要注意的是,这种操作是拷贝一份纹理数据,将会消耗双倍的纹理内存资源,所以必须在必要的时候再启用这个属性。这个操作也只适用于未被压缩的和DXT压缩的纹理,其他类型的纹理不能被读取;默认选项设置为关闭
Streaming Mip Maps 开启这个选项来在该纹理上使用纹理流,这个属性适用于在3D环境下的Unity通过MeshRender渲染的所有纹理。漫反射贴图、法线贴图和光照图都用于纹理流
Mip Map Priority 使用这个选项设置mipmap的优先级。Unity使用这一点来确定在分配资源时要优先考虑哪些mipmap。值越大表示优先级越高(例如,3的优先级高于1)。此设置仅在启用流式MIP映射时可用。mip map priority number也是内存预算的mipmap偏移量(启用纹理流时在质量设置中设置)。例如,优先级为2时,纹理流系统尝试使用比优先级为0的纹理高两个mip级别的mipmap。每个轴上高一个mip级别是2x,每个轴上高两个级别是4x,因此高两个mip级别会导致纹理更大16x。如果它不能做到这一点,它将使用较低的MIP级别来满足内存预算。负值也有效。有关详细信息,请参见纹理流API。
Generate Mip Maps 开启这个选项来进行minmap的生成,
Border Mip Maps 开启这个选项来避免色彩渗出到mip较低层次的边缘。用于光源cookies
Mip Map Filtering 有两种优化图片质量的minmap过滤方法,默认的是Box,是最简单的方式淡出mipmap,随着尺寸的减小mip级别变得更平滑。Kaiser,是随着尺寸的减小,通过在纹理上的锐化算法来进行过滤mip maps。如果你的纹理在远距离变模糊,试试这个选项。
Mip Maps Preserve Coverage 开启这个选项来使生成的mipmap的alpha通道在alpha测试期间保留覆盖范围
Fadeout Mip Maps 开启这个选项可使mipmap随着mip级别的进展逐渐变灰。常用于地图详情。最左边的滚动是第一个开始淡出的mip级别。最右边的滚动定义了纹理完全变灰的mip级别。
Aniso Level 从一个高角度看纹理时提高纹理质量。适用于地板与地面纹理

特定平台的纹理压缩格式

大量使用未经压缩的纹理或者使用较大的分辨率都会造成由带宽引起的性能问题

  • 使用的纹理最好是正方形,长宽最好是2的整数幂
  • 尽可能使用多级渐远纹理技术Mipmapping和纹理压缩(除非我们确定该纹理不会发生缩放如UI和2D游戏中的纹理)

普通图片

图片类型分为两种:矢量图和位图,矢量图,又称向量图,使用点和线来描述物体。位图又称点阵图、光栅图,其存储单位是图像上每一个点的像素值,每个像素都有自己的颜色信息。每个像素点都用RGB或RGBA表示。一般情况下R,G,B(A)每个分量是用一个字节(8位)来表示,所以RGB的图片中每个像素大小就是3*8=24位(用RGB24表示) ARGB的图片中每个像素大小是4*8=32位(用ARGB32表示)。如果直接将图像原始数据存储到硬盘中,存储ARGB32的图片,一个像素占4字节,储存一张1080P的图片则需要1920*1080*4字节= 8294400B 8294400/1024/1024 = 7.9101MB。为了更加高效和便捷要使用压缩算法也针对图像数据结构进行特殊处理,于是便有了jpeg、png等图片格式。

不同图形库中每个像素点中RGBA的排序顺序可能不一样。上面说过像素一般会有RGB,或RGBA四个分量,那么在内存中RGB的排列就有6种情况RGB、RBG、GRB、GBR、BGR、BRG,同理RGBA的排列有24种情况。不过一般只会有RGB,BGR, RGBA, RGBA, BGRA这几种排列。 绝大多数图形库或环境是BGR/BGRA排列。

RGB565 每个像素用2个字节表示,RGB分量分别使用5位、6位、5位,共16位

RGB555 每个像素用2个字节表示,RGB分量都使用5位(剩下1位不用),共16位

RGB24 每个像素用3个字节表示,RGB分量各使用8位,共24位

RGB32 每个像素用4个字节表示,RGB分量各使用8位(剩下8位不用),共32位

ARGB32 每个像素用4个字节表示,ARGB分量各使用8位,共32位

ETC格式

  • ETC1

  • ETC2

PVRTC格式

  • PVRTC 2-bpp

  • PVRTC 4-bpp

DXT

REF

文档:

https://docs.unity3d.com/Manual/class-TextureImporter.html
https://docs.unity3d.com/Manual/class-TextureImporterOverride.html

书籍:

OpenGL宝典、Unity Shader入门

原文地址:https://www.cnblogs.com/sylvan/p/10798010.html