PA2 Combining Textures

Shader 与渲染算法 - 基础 - 纹理混合

combining texture 所解决的问题:纹理过小。大纹理会产生存储浪费,使用 tile 的方法又很容易看到重复的模式,所以使用纹理混合方案:将一个 untiled texture 和一个 tiled texture 进行混合,得到比较经济实用的结果。

Combining: Use multiple textures at the same time.

grid

1 A × A':自己叠自己

A 叠 A'

1.1 粗糙的想法 Rough Idea

  1. 定义 A 为原图(Untiled Texture),A' 为 A 图的 Tile 版本(Tiled Texture),n 为 Tiled 的密度;

  2. fragment shader:纹理采样赋值给变量 A,A 乘以 A'( ×A=nA×2A'=nA×2

为什么定义 A' (Tiled A)时需要将其 × 2?

对一张纹理进行时,结果纹理的颜色会变暗,因为混合操作是将颜色进行简单叠加的。

为了使画面变亮,选取方法为 A 图叠 (A' 图 × 2),虽然 A' 图部分看起来有点过曝。

1.2 CODING

A&A'.shader
Shader "Code/PA2_Texture/A&A'"
{
    Properties {
        _MainTex("Texture", 2D) = "white"{}
    }
    SubShader {
        Pass {
            CGPROGRAM
            #pragma vertex Vert
            #pragma fragment Frag

            #include "UnityCG.cginc"
            sampler2D _MainTex;
            float4 _MainTex_ST;
            
            struct VertexData {
                float4 position: POSITION;
                float2 uv: TEXCOORD0;
            };

            struct Interpolators {
                float4 position: SV_POSITION;
                float2 uv: TEXCOORD0;
            };
            
            Interpolators Vert(VertexData v) {
                Interpolators i;
                i.uv = TRANSFORM_TEX(v.uv, _MainTex);
                i.position = mul(unity_MatrixMVP, v.position);
                return i;
            }

            float4 Frag(Interpolators i): SV_Target {
                float4 color = tex2D(_MainTex, i.uv);
                color *= tex2D(_MainTex, i.uv * 10) * 2;
                return color;
            }
            ENDCG
        }
    }
}

2 A × B:叠加灰度图

grid with detail

2.1 粗糙的想法 Rough Idea

A × A' (A' = nA × 2) 会使 A' 过曝。解决这个问题,考虑另外使用一张灰度图 B 与 A 图进行叠加。

  1. 为新增的灰度图 B 添加一个 Property 及其控制柄来使用它。分配材质,并为设置其 Tilling;

  2. 在结构体 Interpolators 中增加一个 UV(此时结构体中包含主纹理 uv 、细节纹理 uvDetail);

  3. 在其后的 fragment shader 中,对主纹理坐标和细节纹理坐标分别进行转换(使用两次 TRANSFORM_TEX 宏),连同转换为屏幕空间后的顶点坐标一起打包到 Interpolators 结构体的实例中返回。

什么是灰度图 B?

灰度图 B 是只包含亮度值的图片,所有像素表现为从白到黑的不同灰度。

因为不包含任何颜色信息,所以经常被用于处理亮度或对比度的操作。对亮度进行控制时,以 0.5 作为基准,高于 0.5 时会使图像变亮,反之使图像变暗。此时可以生成细致的纹理混合效果。

如何得到一张灰度图?

灰度图的生成通常是通过将彩色图像的 RGB 值转换为一个单一的亮度值。这个转换的标准方法是根据 RGB 颜色空间中的加权平均值来计算:

Gray=0.299×R+0.587×G+0.114×BGray = 0.299 ×R + 0.587 ×G+0.114×B

这个公式反映了人眼对不同颜色的敏感度。得到的灰度值决定了图像中像素的亮度。通过这种方式,明亮的区域会变深,而较暗的区域会变浅,形成均匀的灰度图像。

MainTex: marble
DetailTex: 灰度图 B

2.2 CODING

A&B.shader
Shader "Code/PA2_Texture/A&B"
{
    // 增加新的 Property
    Properties {
        _MainTex("Texture", 2D) = "white"{}
        _DetailTex("Detail Texture", 2D) = "gray"{}
        // gray:表示如果在材质中没有为 Detail Texture 手动分配纹理, Unity 将使用内置的灰色纹理作为默认值。
    }
    SubShader {
        Pass {
            CGPROGRAM
            #pragma vertex Vert
            #pragma fragment Frag

            #include "UnityCG.cginc"
            sampler2D _MainTex, _DetailTex;
            float4 _MainTex_ST, _DetailTex_ST;
            
            struct VertexData {
                float4 position: POSITION;
                float2 uv: TEXCOORD0;           
            };

            struct Interpolators {
                float4 position: SV_POSITION;
                float2 uv: TEXCOORD0;
                float2 uvDetail: TEXCOORD1; // 增加 detail uv
            };
            
            Interpolators Vert(VertexData v) {
                Interpolators i;
                i.uv = TRANSFORM_TEX(v.uv, _MainTex);
                i.uvDetail = TRANSFORM_TEX(v.uv, _DetailTex); // 分别对顶点纹理坐标进行转换
                i.position = mul(unity_MatrixMVP, v.position);
                return i;
            }

            // 最后,在 fragment shader 里将两个纹理采样并混合
            float4 Frag(Interpolators i): SV_Target {
                float4 color = tex2D(_MainTex, i.uv);
                color *= tex2D(_DetailTex, i.uvDetail) * unity_ColorSpaceDouble;
                return color;
            }
            ENDCG
        }
    }
}

为什么材质混合后需要 × unity_ColorSpaceDouble

色彩空间修正:Gamma & Linear

2.3 渐隐模式:Fading Details

当纹理缩小或放在远处时,过于清晰的纹理会使得 tiling 的模式看起来非常明显。因此需要在纹理中进行 Mipmap 的设置。

细节纹理中设置 Fadeout to Gray,同时可以将 Filter Mode 设置为 Trilinear Mode(默认为 Bilinear)。这样一来,细节纹理将在画面放远时逐渐置灰,且不改变结果颜色。

fadeout to gray & Filter mode (在 texture 的设置里)

3 喷射战士:Splatting Map

细节纹理被重复使用在了全图范围。如果面对更加复杂的场景,想要对不同的表面使用不同的纹理,就需要新的、非全局的混合办法。

modulating-both-textures

3.1 天才的想法 Genius Idea

  1. 需要一个布尔值来判定每个片元使用纹理 A or 纹理 B,Splat map 就是实现这个布尔判定的纹理图;

  2. 添加 Properties 和与之对应的变量。因为一般情况下,对于一个场景内所有的纹理,其 Tilling 和 Offset 设置都相同,而 splat map 不做任何 Tile。因此我们可以只在 splat map 上设置 tile,统一应用于所有纹理,如此可以减少信息传输与计算量;

  3. 在结构体 Interpolators 中加入新的 UV(uvSplat),并在顶点着色器中为它绑定顶点;

  4. fragment shader: 用 splat 的 RGB 通道分量来进行取值并输出。

Binary Splat Map

3.2 准备工作

  1. 开启 Splat map 的 Bypass sRGB Sampling。 因为这张 map 是用来进行操作的,而非使用其中的颜色或亮度信息,因此 splat map 中的颜色值需要直接用于计算,而不经过 sRGB 到线性空间的转换。否则会产生不准确的混合效果

  2. 设置它的 Wrap Mode 为 clamp。即不会进行 tile 效果。

  3. 为纹理 A 和纹理 B 设置一些 Tilling,比如 4 × 4。

no extra tilling and offset controls

3.3 CODING

Splat.shader
// key:理解 tex2D 函数。即接受一个纹理采样器和一个 UV 坐标,输出该位置的颜色值。
Shader "Code/PA2_Texture/Splat"
{
    Properties {
        // _MainTex 为 splat map;_Texture1 和 _Texture2 分别为待混合的两个纹理
        _MainTex("Splat Map", 2D) = "white"{}
        [NoScaleOffset] _Texture1("Texture 1", 2D) = "white" {}
        [NoScaleOffset] _Texture2("Texture 2", 2D) = "white" {}
    }
    SubShader {
        Pass {
            CGPROGRAM
            #pragma vertex Vert
            #pragma fragment Frag

            #include "UnityCG.cginc"
            sampler2D _MainTex;
            sampler2D _Texture1, _Texture2;
            float4 _MainTex_ST;
            
            struct VertexData {
                float4 position: POSITION;
                float2 uv: TEXCOORD0;
            };

            struct Interpolators {
                float4 position: SV_POSITION;
                float2 uv: TEXCOORD0;
                float2 uvSplat: TEXCOORD1;
            };
            
            Interpolators Vert(VertexData v) {
                Interpolators i;
                i.position = mul(unity_MatrixMVP, v.position);
                i.uv = TRANSFORM_TEX(v.uv, _MainTex);
                i.uvSplat = v.uv;
                return i;
            }

            // 最后在片元着色器中完成最终的采样
            float4 Frag(Interpolators i): SV_Target {
                float4 splat = tex2D(_MainTex, i.uvSplat);
                return tex2D(_Texture1, i.uv) * splat.r + tex2D(_Texture2, i.uv) * (1-splat.r);
            }
            ENDCG
        }
    }
}
Properties 中 [NoScaleOffset] 是何用意?

[NoScaleOffset] 是一种 Unity Shader 属性修饰符,用于指定该纹理属性在材质编辑器中不会显示或应用 TilingOffset 设置。

以及因为不需要纹理 A 和纹理 B 的控制柄,因此在变量声明部分不必添加 _ST 后缀的属性变量。

如何理解 vertax shader 中 i.uvi.uvSplat 的赋值?
  1. i.uv 用于两个将要进行混合的纹理。其使用宏 TRANSFORM_TEX,借用 _MainTex (主纹理,Splat map) 的 Tiling 和 Offset 为其设置变化量;

  2. i.uvSplat 用于 Splat map,其直接使用原始 UV 坐标。

如何理解 fragment shader 的逻辑?
  1. 首先为 splat 使用 tex2D 函数绑定纹理对象与纹理坐标;

  1. 在 return 语句中,用 splat 的 RGB 通道来进行取值。

3.4 RGB 喷射战士

four textures splatted

原理同上。只需要增加 Properties 与其对应属性,然后在片元着色器中返回多色通道即可。

RGB Splat Map
// RGB 通道的片元着色器返回值
return
    (_Texture1, i.uv) * splat.r +
    (_Texture2, i.uv) * splat.g +
    (_Texture3, i.uv) * splat.b +
    (_Texture4, i.uv) * (1 - splat.r - splat.g - splat.b);
}

APPENDIX

Anki

Reference

Last updated