UnityShader(五)纹理采样

纹理最初的目的就是使用一张图片控制模型的外观。使用纹理映射技术,我们可以将一张图片黏在模型表面,逐纹素地控制模型的颜色。
在美术人员建模时,通常会在建模软件中利用纹理展开技术把纹理映射坐标存储在每个顶点上。纹理映射坐标定义了该顶点在纹理中对应的2D坐标。通常,这些坐标使用一个二维向量(u,v)表示,其中u是横向坐标,v是纵向坐标。因此纹理映射坐标也被称为uv坐标

一、单张纹理

我们还是在之前的Shader基础上进行修改。首先复制一份之前的逐像素高光反射Shader

Shader "Unlit/TextureSampling"
{
   
   
    Properties
    {
   
   
        _Diffuse("Diffuse",Color) = (1,1,1,1)
        _Specular("Specular",Color) = (1,1,1,1)
        _Gloss("Gloss",Range(1,256)) = 20
    }
    SubShader
    {
   
   
        Tags {
   
    "LightMode"="ForwardBase" }
        LOD 100

        Pass
        {
   
   
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            #include "Lighting.cginc"

            fixed4 _Diffuse;
            fixed4 _Specular;
            float _Gloss;

            struct v2f
            {
   
   
                float4 vertex: SV_POSITION;
                fixed3 worldNormal: TEXCOORD0;
                fixed3 worldVert: TEXCOORD1;
            };

            v2f vert(appdata_base v)
            {
   
   
                v2f o;
                // 将顶点坐标从物体空间转换到裁剪空间
                o.vertex = UnityObjectToClipPos(v.vertex);
                // 将法线从物体空间转换到世界空间
                const fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
                o.worldNormal = worldNormal;
                // 缓存顶点的世界坐标
                o.worldVert = mul(unity_ObjectToWorld, v.vertex);
                return o;
            }

            fixed4 frag(v2f i):SV_Target
            {
   
   
                // 获取归一化的光源方向
                const fixed3 worldLight = normalize(_WorldSpaceLightPos0);
                // 环境光
                const fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
                // 计算漫反射
                const fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(i.worldNormal,worldLight));

                // 反射光向量
                const fixed3 reflectDir = normalize(reflect(-worldLight, i.worldNormal));
                // 视角方向向量 摄像机位置-顶点位置
                const fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldVert);
                // 计算高光反射
                const fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(reflectDir, viewDir)), _Gloss);
                
                fixed3 color = ambient + diffuse+specular;
                return fixed4(color,1);
            }
            ENDCG
        }
    }
}

在属性中定义一个纹理属性。其中“white”指的是内置纹理的名字,即一个全白纹理

_MainTex("MainTex",2D) = "white" {
   
   }

然后在CG代码中声明对应的变量。这里要注意,除了需要声明_MainTex变量外,还需要声明一个_MainTex_ST变量用来得到缩放和平移值,也就是面板中的「Tiling」和「Offset」。这个名字并不是随便取的,而是Unity规定的使用纹理名_ST的方式声明某个纹理的属性。

sampler2D _MainTex;  
float4 _MainTex_ST;

接下来我们需要在输出结构体中声明一个新的变量uv用来存储纹理坐标

float2 uv:TEXCOORD2;

然后在顶点着色器中计算纹理坐标。在appdata_base中就可以直接获取到,但为了让我们定义的属性可以对其进行控制,我们需要乘上缩放并加上偏移

// 计算uv坐标  
o.uv = v.texcoord.xy*_MainTex_ST.xy+_MainTex_ST.zw;

最后在片元着色器中对其进行纹理采样,可以直接使用tex2D函数,它的第一个参数是需要被采样的纹理,第二个参数是一个float类型的纹理坐标。它将返回计算得到的纹素值。我们直接将返回值乘到漫反射计算公式中

const fixed3 albedo = tex2D(_MainTex,i.uv).rgb;
const fixed3 diffuse = _LightColor0.rgb * albedo * _Diffuse.rgb * saturate(dot(i.worldNormal, worldLight));

最后在面板中选择一张图片,效果如下

二、凹凸映射

所谓的凹凸映射就是使用一张纹理来修改模型表面的法线(法线贴图),来让模型看起来像是凹凸不平的效果。在之前计算漫反射时,光的入射方向是不变的,唯一能确定计算结果的是法线方向。对于一个凹凸不平的物体表面,其法线方向也是不同的。那么要想让一个光滑的物体表面看起来凹凸不平,也只需要改变其法线方向即可

实现凹凸映射主要有两种方式,一种是使用高度纹理来模拟表面位移,然后得到一个修改后的法线值,这种方法也叫高度映射。另一种方法是使用一张法线纹理来直接存储表面法线,这种方法也叫法线映射。

2.1 高度纹理

高度图中存储的是强度值,用来表示模型表面局部的海拔高度,图中颜色越浅表明该位置的表面越凸出,反之越内凹。我们可以通过PS中的「滤镜->3D->生成凹凸图」来生成一张图片的高度图。

高度图的优点是非常直观,我们可以很明确的知道一个模型表面的凹凸情况。但缺点是在实时计算时不能直接得到表面法线,而是需要根据像素的灰度值计算而来,因此更耗费性能。

2.2 法线纹理

法线纹理中存储的是表面的法线方向。由于法线方向的各个分量范围在[−1,1][-1,1][1,1]之间,而像素的分量范围在[0,1][0,1][0,1]之间,所以需要进行映射
pixel=normal∗0.5+0.5 pixel = normal*0.5 + 0.5 pixel=normal0.5+0.5
也就是说,我们在Shader中对法线纹理进行纹理采样后,还要对结果进行一次逆映射,即
normal=pixel∗2−1 normal = pixel*2 - 1 normal=pixel21
根据法线纹理中存储的法线所在坐标空间的不同,法线纹理还分为模型空间法线纹理切线空间法线纹理。下图中左侧的是模型空间法线纹理,右侧的是切线空间法线纹理。可以发现,前者是五颜六色的,而后者确是比较单一的蓝紫色。这是因为前者所有法线所在的坐标空间是同一个空间,即模型空间。而每个点存储的法线方向是各异的,比如法线(0,1,0)经过映射后存储到纹理中就对应了RGB(0.5,1,0.5),也就是浅绿色;(0,-1,0)经过映射后就变成了(0.5,0,0.5)也就是紫色。后者每个法线方向所在的坐标空间都是不一样的,即每个点各自的切线空间。也就是说,如果一个点的法线方向与模型本身的法线方向一样,则在切线空间中,新的法线方向就是z轴方向,也就是(0,0,1),映射后为RGB(0.5,0.5,1)浅蓝色。显示为紫色则代表这个顶点的法线与模型的法线偏移较大。

同样的,我们也可以直接使用PS获取到图片的法线纹理,操作是「滤镜->3D->生成法线图」

使用模型空间存储法线的优点如下:

  • 实现简单,看起来直观。
  • 纹理坐标缝合处和尖锐的边角部分,可见的突变较少。
    使用切线空间存储法线的有点更多:
  • 自由度高,可以应用到不同的模型。
  • 可以实现UV动画。
  • 可以重用法线纹理。
  • 可压缩。
    因而在很多情况下,人们都选择使用切线空间存储法线。

2.3 在Unity中实现凹凸映射

这里我们采用切线空法线纹理进行计算。有如下两种方案:

  • 在顶点着色器中,将视角、光照方向等变换到切线空间,与法线进行计算,然后在片元着色器中进行采样、光照模型计算。
  • 直接在片元着色器中,将采样得到的法线方向变换到世界空间下,再进行其他计算。
    在性能上来说第一种方案由于第二种。但第二种方法更具通用性,因为有时我们需要在世界空间下进行一些计算。

切线空间纹理映射

首先我们来实现第一种方案。

依然是复制一份之前的Shader,并在其基础上进行修改。

先定义两个属性,一个用于添加法线纹理,另一个用于控制凹凸程度

_BumpMap("Normal Map",2D) = "bump" {
   
   }  
_BumpScale("Bump Scale",
Unity 中实现凹凸纹理效果(Bump Mapping)通常通过法线贴图(Normal Mapping)来完成。法线贴图是一种逐像素修改法线方向的技术,它可以让低多边形模型在光照下呈现出高多边形模型的细节效果。以下是关于如何在 Unity Shader 中实现这一效果的详细说明。 ### 法线贴图原理 在传统的光照计算中,法线是从模型顶点数据中获取的,它决定了表面的朝向和光照响应。然而,对于低面数模型来说,这种法线信息无法表现出复杂的表面细节。法线贴图通过将每个像素的法线方向存储在一张纹理中,从而在不增加模型复杂度的前提下模拟出凹凸不平的表面效果。 在像素着色器中,使用法线贴图时,需要将法线信息从纹理采样出来,并将其转换到世界空间或视图空间,以参与光照计算。这种方法可以让表面在不同光照角度下呈现出更加丰富的细节[^4]。 ### Unity Shader 实现步骤 #### 1. 准备法线贴图 在 Unity 中,首先需要一张法线贴图(Normal Map)。这张贴图通常是以切线空间(Tangent Space)的形式存储的。在导入贴图时,需要将其设置为“Normal Map”类型,Unity 会自动将其转换为适合着色器使用的格式。 #### 2. 使用 `UnpackNormal` 函数 由于法线贴图在纹理中是以压缩格式存储的(通常是 [0, 1] 范围内的 RGB 值),因此在着色器中需要使用 `UnpackNormal` 函数将其还原为 [-1, 1] 范围的法线向量: ```hlsl fixed4 normalTex = tex2D(_BumpTex, i.uv); float3 normal = UnpackNormal(normalTex); ``` 该函数会将纹理中的颜色值转换为正确的法线向量,并考虑 `_BumpScale` 参数来控制凹凸程度[^2]。 #### 3. 构建 TBN 矩阵 为了将法线从切线空间转换到世界空间,需要构建 TBN(Tangent-Binormal-Normal)矩阵。在 Unity 中,顶点结构通常包含 `tangent` 和 `normal` 成员,可以通过它们计算出副法线(Binormal)并构造出 TBN 矩阵: ```hlsl float3 binormal = cross(v.normal, v.tangent.xyz) * v.tangent.w; float3x3 TBN = float3x3(v.tangent.xyz, binormal, v.normal); ``` 这样构造出的矩阵可以将法线从切线空间变换到世界空间: ```hlsl float3 worldNormal = normalize(mul(TBN, normal)); ``` #### 4. 应用光照计算 最后,使用变换后的法线向量进行光照计算,例如 Phong 或 Blinn-Phong 模型: ```hlsl float3 lightDir = normalize(_WorldSpaceLightPos0.xyz); float diffuse = max(0, dot(worldNormal, lightDir)); ``` ### 完整示例代码 以下是一个简化的 Unity Shader 示例,展示了如何实现法线贴图效果: ```hlsl Shader "Custom/NormalMapShader" { Properties { _MainTex ("Base (RGB)", 2D) = "white" {} _BumpTex ("Normal Map", 2D) = "bump" {} _BumpScale ("Bump Scale", Float) = 1.0 } SubShader { Tags { "RenderType"="Opaque" } LOD 200 CGPROGRAM #pragma surface surf Lambert sampler2D _MainTex; sampler2D _BumpTex; float _BumpScale; struct Input { float2 uv_MainTex; float2 uv_BumpTex; }; void surf (Input IN, inout SurfaceOutput o) { fixed4 c = tex2D(_MainTex, IN.uv_MainTex); o.Albedo = c.rgb; fixed4 bump = tex2D(_BumpTex, IN.uv_BumpTex); o.Normal = UnpackNormal(bump); o.Normal.z /= _BumpScale; } ENDCG } FallBack "Diffuse" } ``` ### 优化与扩展 - **Mipmap 与 Detail Map**:在处理法线贴图时,也可以结合 Mipmap 技术来优化远处的渲染效果。通过设置合适的 Mipmap 层级和使用三线性过滤,可以避免法线贴图在远处出现模糊或闪烁现象[^3]。 - **Detail Map 混合**:可以将细节贴图(Detail Map)叠加到主贴图上,以增强近距离观察时的表面细节。这通常通过简单的颜色叠加或乘法操作实现。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值