Post

Unity 多纹理融合(翻译三)

Unity 多纹理融合(翻译三)

本篇摘要:

  • 采样多个纹理
  • 应用细节纹理
  • 处理线性空间中的颜色
  • 使用 splat 地图

纹理合并

贴图在游戏应用广泛,但它们有局限性。无论以何种尺寸显示,它们都有固定数量的像素。如果需要被渲染到很小网格,可以使用mipmap来保持它们的部分细节。但是当渲染到很大的网格上,会变得模糊。我们也不能无中生有地渲染更多额外的细节。本文讨论了一些解决办法。

细节纹理

通常可以使用更大的纹理,意味着更多的像素和更多的细节。但是纹理的大小也是有限制的,取决于游戏包体大小和目标平台的内存,以及gpu采样能力。

另一种增加像素密度的方法是平铺纹理。出一张尽可能小的贴图,设置为重复模式。近距离观察下重复感可能不会很明显。毕竟当你站着用鼻子接触墙壁时,你只会看到整面墙壁的一小部分。

因此,我们能够通过拉伸与平铺纹理相结合的方式来尽可能地添加细节。为了尝试这一点,我们使用一张棱角明显的纹理。这是一个方格图,放入的工程内使用默认导入设置。

略微扭曲的网格纹理
略微扭曲的网格纹理

新建一个纹理融合shader

1
2
3
4
5
6
7
8
9
10
Shader "Custom/Textured With Detail" {
    Properties {
        _Tint ("Tint", Color) = (1, 1, 1, 1)
        _MainTex ("Texture", 2D) = "white" {}
    }

    SubShader {

    }
}

使用此着色器创建一个新材质,然后为其指定该shader和网格纹理。

网格纹理
网格纹理
网格纹理

将材质分配给quad并查看它。从远处看效果还行。但是靠得太近看会变得模糊不清。缺失一些细节,同时纹理压缩造成的伪影也会变得很明显。 网格特写,显示低纹素密度和 DXT1 伪影。 多个纹理样本

带有低像素密度和DXT1伪影
带有低像素密度和DXT1伪影

多张纹理贴图采样

现在我们只采样了单个纹理样本并将其用作片段着色器的结果,将采样的颜色存储在一个临时变量中。

1
2
3
4
float4 MyFragmentProgram (Interpolators i) : SV_TARGET {
    float4 color = tex2D(_MainTex, i.uv) * _Tint;
    return color;
}

先假设可以通过引入平铺纹理的方式来增加像素密度。执行一次纹理采样函数,给它一个十倍采样面积,用这个结果替换临时存储的颜色原来的颜色输出到屏幕。

1
2
3
float4 color = tex2D(_MainTex, i.uv) * _Tint;
color = tex2D(_MainTex, i.uv * 10);
return color;

屏幕上会产生很多小网格。靠的很近再观察,结果不那么糟糕了。因为采样纹理用了平铺10次,所以很明显是一个重复的图案。 硬编码平铺。

平铺纹理
平铺纹理

请注意,此时我们正在执行两个纹理采样,但最终却只使用其中一个。这似乎很浪费。是吗?看看编译的顶点程序。

1
2
3
4
5
6
7
8
9
10
uniform  sampler2D _MainTex;
in  vec2 vs_TEXCOORD0;
layout(location = 0) out vec4 SV_TARGET0;
vec2 t0;
void main()
{
    t0.xy = vs_TEXCOORD0.xy * vec2(10.0, 10.0);
    SV_TARGET0 = texture(_MainTex, t0.xy);
    return;
}
1
2
3
4
5
6
7
8
9
10
SetTexture 0 [_MainTex] 2D 0
      ps_4_0
      dcl_sampler s0, mode_default
      dcl_resource_texture2d (float,float,float,float) t0
      dcl_input_ps linear v0.xy
      dcl_output o0.xyzw
      dcl_temps 1
   0: mul r0.xy, v0.xyxx, l(10.000000, 10.000000, 0.000000, 0.000000)
   1: sample o0.xyzw, r0.xyxx, t0.xyzw, s0
   2: ret

是否注意到编译后的代码中只有一个纹理采样?没错,编译器为我们去掉了不必要的代码!编译器基本上会丢弃任何最终未使用的内容。

我们不想丢弃原始采样到颜色,就要合并两次采样结果。让我们通过将它们相乘来做到这一点。再添加一个_Tint属性,叠加一层自定义颜色。

1
2
3
float4 color = tex2D(_MainTex, i.uv) * _Tint;
color *= tex2D(_MainTex, i.uv);
return color;

着色编译器会生成什么样的代码呢,对此有何影响?

1
2
3
4
5
6
7
8
9
10
11
12
uniform  sampler2D _MainTex;
in  vec2 vs_TEXCOORD0;
layout(location = 0) out vec4 SV_TARGET0;
mediump vec4 t16_0;
lowp vec4 t10_0;
void main()
{
    t10_0 = texture(_MainTex, vs_TEXCOORD0.xy);
    t16_0 = t10_0 * t10_0;
    SV_TARGET0 = t16_0 * _Tint;
    return;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
SetTexture 0 [_MainTex] 2D 0
ConstBuffer "Globals" 144
Vector 96 [_Tint]
BindCB  "Globals" 0
      ps_4_0
      dcl_constantbuffer cb0[7], immediateIndexed
      dcl_sampler s0, mode_default
      dcl_resource_texture2d (float,float,float,float) t0
      dcl_input_ps linear v0.xy
      dcl_output o0.xyzw
      dcl_temps 1
   0: sample r0.xyzw, v0.xyxx, t0.xyzw, s0
   1: mul r0.xyzw, r0.xyzw, r0.xyzw
   2: mul o0.xyzw, r0.xyzw, cb0[6].xyzw
   3: ret

这次的纹理采样,编译器检测到重复对_MainTex采样代码。对其进行优化后纹理只采样一次,结果存储在寄存器中并重复使用。即使使用_Tint中间变量等,编译器也足够聪明,可以检测到此类代码重复。最终将所有结果汇总后输出。

现在再对UV坐标平铺×10次,最终看到大网格和小网格的融合在一起

1
color *= tex2D(_MainTex, i.uv * 10);
平铺纹理
平铺纹理

由于纹理采样时参数不再相同,编译器也必须保留两次采样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
uniform  sampler2D _MainTex;
in  vec2 vs_TEXCOORD0;
layout(location = 0) out vec4 SV_TARGET0;
vec4 t0;
lowp vec4 t10_0;
vec2 t1;
lowp vec4 t10_1;
void main()
{
    t10_0 = texture(_MainTex, vs_TEXCOORD0.xy);
    t0 = t10_0 * _Tint;
    t1.xy = vs_TEXCOORD0.xy * vec2(10.0, 10.0);
    t10_1 = texture(_MainTex, t1.xy);
    SV_TARGET0 = t0 * t10_1;
    return;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
SetTexture 0 [_MainTex] 2D 0
ConstBuffer "Globals" 144
Vector 96 [_Tint]
BindCB  "Globals" 0
      ps_4_0
      dcl_constantbuffer cb0[7], immediateIndexed
      dcl_sampler s0, mode_default
      dcl_resource_texture2d (float,float,float,float) t0
      dcl_input_ps linear v0.xy
      dcl_output o0.xyzw
      dcl_temps 2
   0: sample r0.xyzw, v0.xyxx, t0.xyzw, s0
   1: mul r0.xyzw, r0.xyzw, cb0[6].xyzw
   2: mul r1.xy, v0.xyxx, l(10.000000, 10.000000, 0.000000, 0.000000)
   3: sample r1.xyzw, r1.xyxx, t0.xyzw, s0
   4: mul o0.xyzw, r0.xyzw, r1.xyzw
   5: ret

单独的细节纹理

将两个纹理相乘时,结果会更暗。除非至少其中一种纹理是白色的。这是因为像素的每个颜色通道都有一个介于 0 和 1 之间的值。当向纹理添加细节时,可以通过该通道值来实现变暗或变亮。

要使原始纹理变亮,给原始颜色乘2,使得每个颜色值都增大。

1
color *= tex2D(_MainTex, i.uv * 10) * 2;
增亮颜色
增亮颜色

这种直接扩大倍数的做法很粗暴。我们知道任何数乘以1不变,但是对细节纹理色加倍时,但对于1/2这个分界值就有用了。颜色区间是0-1,低于1/2的值将是结果变暗,高于1/2的值将变亮。这里引入一张特殊的灰度细节纹理来处理。

细节灰度图
细节灰度图

灰度细节纹理?

一般都是用灰度细节纹理来增白或加深原始颜色做二次细节调整,不是灰度图跳出 的颜色不是那么直观的结果。

要使用这个单独的细节纹理,我们必须在着色器中添加第二个纹理属性。使用灰色作为默认值,因为这不会改变主纹理的外观。

1
2
3
4
5
Properties {
    _Tint ("Tint", Color) = (1, 1, 1, 1)
    _MainTex ("Texture", 2D) = "white" {}
    _DetailTex ("Detail Texture", 2D) = "gray" {}
}

将细节纹理分配给我们的材质并将其平铺设置为10。

两种纹理
两种纹理

我们必须添加变量来访问细节纹理及其平铺和偏移数据

1
2
sampler2D _MainTex, _DetailTex;
float4 _MainTex_ST, _DetailTex_ST;

使用两个UV

我们应该使用细节纹理的平铺和偏移数据,而不是使用硬编码乘10。

1
2
3
4
5
struct Interpolators {
  float4 position : SV_POSITION;
  float2 uv : TEXCOORD0;
  float2 uvDetail : TEXCOORD1;
}

通过使用主纹理uv对细节纹理进行采样,得到一个新的细节纹理uv。

1
2
3
4
5
6
7
Interpolators MyVertexProgram (VertexData v) {
  Interpolators i;
  i.position = mul(UNITY_MATRIX_MVP, v.position);
  i.uv = TRANSFORM_TEX(v.uv, _MainTex);
  i.uvDetail = TRANSFORM_TEX(v.uv, _DetailTex);
  return i;
}

再一次看看汇编代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
uniform         vec4 _Tint;
uniform         vec4 _MainTex_ST;
uniform         vec4 _DetailTex_ST;
in  vec4 in_POSITION0;
in  vec2 in_TEXCOORD0;
out vec2 vs_TEXCOORD0;
out vec2 vs_TEXCOORD1;
vec4 t0;
void main()
{
    t0 = in_POSITION0.yyyy * glstate_matrix_mvp[1];
    t0 = glstate_matrix_mvp[0] * in_POSITION0.xxxx + t0;
    t0 = glstate_matrix_mvp[2] * in_POSITION0.zzzz + t0;
    gl_Position = glstate_matrix_mvp[3] * in_POSITION0.wwww + t0;
    vs_TEXCOORD0.xy = in_TEXCOORD0.xy * _MainTex_ST.xy + _MainTex_ST.zw;
    vs_TEXCOORD1.xy = in_TEXCOORD0.xy * _DetailTex_ST.xy + _DetailTex_ST.zw;
    return;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Vector 112 [_MainTex_ST]
Vector 128 [_DetailTex_ST]
ConstBuffer "UnityPerDraw" 352
Matrix 0 [glstate_matrix_mvp]
BindCB  "Globals" 0
BindCB  "UnityPerDraw" 1
      vs_4_0
      dcl_constantbuffer cb0[9], immediateIndexed
      dcl_constantbuffer cb1[4], immediateIndexed
      dcl_input v0.xyzw
      dcl_input v1.xy
      dcl_output_siv o0.xyzw, position
      dcl_output o1.xy
      dcl_output o1.zw
      dcl_temps 1
   0: mul r0.xyzw, v0.yyyy, cb1[1].xyzw
   1: mad r0.xyzw, cb1[0].xyzw, v0.xxxx, r0.xyzw
   2: mad r0.xyzw, cb1[2].xyzw, v0.zzzz, r0.xyzw
   3: mad o0.xyzw, cb1[3].xyzw, v0.wwww, r0.xyzw
   4: mad o1.xy, v1.xyxx, cb0[7].xyxx, cb0[7].zwzz
   5: mad o1.zw, v1.xxxy, cb0[8].xxxy, cb0[8].zzzw
   6: ret

注意两个 UV 输出是如何在两个编译器顶点程序中定义的。OpenGLCore使用vs_TEXCOORD0和vs_TEXCOORD1输出,相反Direct3D11只使用一个输出o1.

1
2
3
4
5
6
7
// Output signature:
//
// Name                 Index   Mask Register SysValue  Format   Used
// -- --  -- -- - 
// SV_POSITION              0   xyzw        0      POS   float   xyzw
// TEXCOORD                 0   xy          1     NONE   float   xy  
// TEXCOORD                 1     zw        1     NONE   float     zw

上面代码意味着两个 UV 对都被打包到一个输出寄存器中。第一个在 X 和 Y 通道,第二个在 Z 和 W 通道。因为寄存器总是由四个数字组成的组。Direct3D 11 编译器利用了这一点。

试着手动打包输出? 手动打包输出的常见原因是只有少数几个插值器可用。Shader Model 2硬件支持8个通用插补 器,而Shader Model 3硬件支持10个。复杂着色器可能会遇到这个限制。

现在我们可以在片段程序中使用额外的UV对。

1
2
3
4
5
float4 MyFragmentProgram (Interpolators i) : SV_TARGET {
        float4 color = tex2D(_MainTex, i.uv) * _Tint;
        color *= tex2D(_DetailTex, i.uvDetail) * 2;
        return color;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
uniform         vec4 _Tint;
uniform         vec4 _MainTex_ST;
uniform         vec4 _DetailTex_ST;
uniform  sampler2D _MainTex;
uniform  sampler2D _DetailTex;
in  vec2 vs_TEXCOORD0;
in  vec2 vs_TEXCOORD1;
layout(location = 0) out vec4 SV_TARGET0;
vec4 t0;
lowp vec4 t10_0;
lowp vec4 t10_1;
void main()
{
    t10_0 = texture(_MainTex, vs_TEXCOORD0.xy);
    t0 = t10_0 * _Tint;
    t10_1 = texture(_DetailTex, vs_TEXCOORD1.xy);
    t0 = t0 * t10_1;
    SV_TARGET0 = t0 + t0;
    return;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
SetTexture 0 [_MainTex] 2D 0
SetTexture 1 [_DetailTex] 2D 1
ConstBuffer "Globals" 144
Vector 96 [_Tint]
BindCB  "Globals" 0
      ps_4_0
      dcl_constantbuffer cb0[7], immediateIndexed
      dcl_sampler s0, mode_default
      dcl_sampler s1, mode_default
      dcl_resource_texture2d (float,float,float,float) t0
      dcl_resource_texture2d (float,float,float,float) t1
      dcl_input_ps linear v0.xy
      dcl_input_ps linear v0.zw
      dcl_output o0.xyzw
      dcl_temps 2
   0: sample r0.xyzw, v0.xyxx, t0.xyzw, s0
   1: mul r0.xyzw, r0.xyzw, cb0[6].xyzw
   2: sample r1.xyzw, v0.zwzz, t1.xyzw, s1
   3: mul r0.xyzw, r0.xyzw, r1.xyzw
   4: add o0.xyzw, r0.xyzw, r0.xyzw
   5: ret

基于细节纹理,主纹理变得更亮和更暗。

明暗两张纹理
明暗两张纹理
明暗两张纹理

细节纹理渐变融合

添加细节的想法是它们可以在近距离或放大时改善材质的外观。它们不应该在远处可见或缩小,因为这会使平铺变得明显。所以我们需要一种方法来随着纹理显示尺寸的减小而淡化细节。我们可以通过将细节纹理淡化为灰色来做到这一点,因为这不会导致颜色变化。

需要做的就是在细节纹理的导入设置中启用Fadeout Mip Maps属性。这也会自动将过滤器模式切换为三线性,以便渐变为灰色是渐进的。

纹理过渡
纹理过渡
纹理过渡

网格从详细到不详细的过渡非常明显,但通常不会注意到它。例如,这里是大理石材质的主纹理和细节纹理。

大理石纹理
大理石纹理
大理石纹理

一旦我们的材质使用了这些纹理,细节纹理的过渡痕迹就不再明显了。

大理石材质
大理石材质
大理石材质

然而,由于细节纹理的过渡加持,大理石材质在近距离看起来要好得多。

没有细节和有细节特写
没有细节和有细节特写
没有细节和有细节特写

线性色彩空间

当我们在 gamma 颜色空间中渲染场景时,我们的着色器工作正常,但如果我们切换到线性颜色空间,它就会出错。色彩空间在项目中设置。它配置在Other Settings播放器设置面板,可以通过Edit / Project Settings / Player.

选择颜色空间
选择颜色空间

什么是 Gamma 和 Linear 色彩空间?

在计算机图形学中,Gamma 和 Linear 是两种处理光照和颜色的不同方式,理解它们的区别对于渲染真实感画面至关重要。

1. 为什么会有 Gamma 校正?

这源于早期的 CRT(阴极射线管)显示器。CRT 显示器的物理特性决定了它的输入电压和输出亮度不是线性的。如果你给它 50% 的电压,它并不会产生 50% 的亮度,而是大约 21.8% 的亮度($0.5^{2.2}$)。这种关系呈现出一条向下弯曲的曲线,称为 Gamma 2.2 曲线。

为了让显示器输出正确的亮度,我们需要在把图像存入文件时,预先进行一次反向的增亮处理($0.5^{1/2.2} \approx 0.73$),这个过程叫 Gamma Encoding。这样,当显示器再次以 2.2 的幂次压暗图像时,原本的增亮和显示器的压暗相互抵消,最终人眼就能看到正确的、线性的光照结果。

此外,人眼对暗部细节比对亮部细节更敏感。sRGB 标准利用了这一点,通过 Gamma Encoding 这种非线性存储方式,在有限的 8 位(0-255)数据中,把更多的数据位分配给了暗部。这是一种极其高效的数据压缩方式。

gamma $1\over 2.2$ encoding vs. $2.2$ deconding
gamma $1\over 2.2$ encoding vs. $2.2$ deconding
gamma $1\over 2.2$ encoding vs. $2.2$ deconding

2. Gamma Space vs Linear Space

  • Gamma Space(伽马空间)
    • 工作流:这是旧时代的流程。贴图是 sRGB 的(经过 Gamma 编码),渲染计算(光照、混合)直接使用这些 sRGB 数值,最后直接输出到显示器。
    • 问题:数学上是错误的。光照计算(如 Light * Albedo)假设数值是线性的(1+1=2),但在 Gamma 空间中,0.5 并不代表一半的物理亮度。这会导致光照衰减不自然、混合颜色发黑、高光过曝等问题。
  • Linear Space(线性空间)
    • 工作流:这是现代标准流程。
      1. 输入:读取 sRGB 贴图时,GPU 自动去除 Gamma 编码,将其还原为线性数值(Linear)。
      2. 计算:Shader 中的所有光照、混合计算都在线性空间进行,符合物理规律。
      3. 输出:最终输出到屏幕前,再次进行 Gamma 校正(Encoding),以适应显示器的特性。
    • 优势:光照计算符合物理规律,光影过渡自然,是 PBR(基于物理的渲染)的基础。

3. 游戏开发中的选择与统一

选择哪种空间?

  • Linear Space 是目前游戏开发的绝对主流。PC、主机和中高端移动设备都首选线性空间,因为它能提供更真实的光照效果。
  • Gamma Space 仅在针对极低端老旧设备或某些特定的非写实 2D 游戏中保留。

如何保持统一性? 在 Unity 中,通过 Project Settings -> Player -> Other Settings -> Color Space 选择 Linear。

  • 颜色贴图(Albedo/Diffuse):Unity 默认会将纹理标记为 sRGB。在 Linear 模式下,Unity 会在采样时自动移除 Gamma,将其转为线性值供 Shader 使用。
  • 数值贴图(Normal/Metallic/Roughness):这些纹理存储的是数学数据而非颜色,不需要 Gamma 校正。必须在纹理导入设置中取消勾选 “sRGB (Color Texture)”,确保 Shader 读取到的是未经修改的原始数值。

回到我们的例子,当切换到 Linear 空间后,细节纹理变暗了。这是因为我们原本的算法(* 2)是基于 Gamma 空间的经验公式。在线性空间中,原本的中性灰(Gamma 0.5)被还原为线性值(约为 0.217),乘以 2 后仅为 0.434,远小于 1,所以变暗了。

为了修复这个问题,我们需要在线性空间中使用正确的数学转换,或者使用 Unity 提供的 unity_ColorSpaceDouble 变量,它会自动根据当前色彩空间提供正确的倍增系数(Linear 空间下约为 4.59,Gamma 空间下为 2),从而保证渲染结果的一致性。

1
color *= tex2D(_DetailTex, i.uvDetail) * unity_ColorSpaceDouble;

通过这种更改,无论我们在哪个颜色空间中渲染,我们的细节材质看起来都一样。

纹理splat过渡遮罩

细节纹理的一个限制是对整个表面使用相同的细节。 这适用于均匀的表面,如大理石板。但是,如果材质没有统一的外观,不希望在任何地方都使用相同的细节。

考虑一个大地形。它可以有草、沙、岩石、雪等。希望这些地形类型有一定的细节。但是覆盖整个地形的纹理永远不会有足够的细节。可以通过为每种表面类型使用单独的纹理并平铺这些纹理来解决该问题。但是怎么知道在哪里使用哪种纹理呢?

假设我们有一个具有两种不同表面类型的地形,什么时候决定使用哪种表面纹理呢。不是一就是二。我们可以用一个布尔值来表示这个逻辑。如果设置为true,我们使用第一个纹理,否则使用第二个。我们可以使用灰度纹理来存储这个选择。值1表示第一个纹理,而值0表示第二个纹理。事实上,我们可以使用这些值在两个纹理之间进行线性插值。然后介于 0 和 1 之间的值表示两种纹理之间的混合,这使得平滑过渡成为可能。

这样的纹理被称为splat贴图。就像将多个地形特征喷溅到画布上一样。由于插值,这张地图甚至不需要高分辨率。

splat遮罩贴图
splat遮罩贴图

将其添加到项目后,将其导入类型切换为高级。启用Bypass sRGB Sampling并指示其mipmap应在Linear Space中生成。因为该纹理不需要sRGB颜色。所以在线性空间中渲染时不应该进行转换。另外,将其 Wrap Mode 设置为clamp,因为我们不会平铺这张地图。

splat导入设置
splat导入设置

创建一个新的 Texture Splatting 着色器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
Shader "Custom/Texture Splatting" {
    Properties {
        MainTex ("Splat Map", 2D) = "white" {}
    }

    SubShader {
        Pass {
        CGPROGRAM

        #pragma vertex MyVertexProgram
        #pragma fragment MyFragmentProgram

        #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 MyVertexProgram (VertexData v)
        {
            Interpolators i;
            i.position = mul(UNITY_MATRIX_MVP, v.position);
            i.uv = TRANSFORM_TEX(v.uv, _MainTex);
            return i;
        }

        float4 MyFragmentProgram (Interpolators i) : SV_TARGET
        {
            return tex2D(_MainTex, i.uv);
        }

        ENDCG
        }
    }
}

创建一个新材质并引用该shader,并将splat贴图指定为其主要纹理。

splat图渲染
splat图渲染
splat图渲染

增加融合纹理

为了能够在两个纹理之间进行选择,作为属性添加命名为Texture1和Texture2到我们的着色器中。

1
2
3
4
5
Properties {
    _MainTex ("Splat Map", 2D) = "white" {}
    _Texture1 ("Texture 1", 2D) = "white" {}
    _Texture2 ("Texture 2", 2D) = "white" {}
}

可以为他们使用任何你想要的纹理,这里使用网格纹理和大理石纹理。

增加的额外纹理
增加的额外纹理

为添加到着色器的每个纹理修改平铺和偏移控制值。这需要我们将更多数据从顶点传递到片段着色器,或者在片元着色器中计算UV调整。 这很好,但通常地形的所有纹理都平铺相同。并且 splat 地图根本没有平铺。所以我们只需要一个平铺和偏移控件的实例。

可以将属性控制添加到着色器属性之前,就像在C#代码中一样。NoScaleOffset将禁用纹理平铺和偏移。

1
2
3
4
5
Properties {
    _MainTex ("Splat Map", 2D) = "white" {}
    [NoScaleOffset] _Texture1 ("Texture 1", 2D) = "white" {}
    [NoScaleOffset] _Texture2 ("Texture 2", 2D) = "white" {}
}

同时修改splat贴图tiling为4。

不需要额外的贴图纹理
不需要额外的贴图纹理

将采样器变量添加到我们的着色器代码中,但是不必添加它们对应的 _ST 变量。

1
2
3
4
sampler2D _MainTex;
float4 _MainTex_ST;

sampler2D _Texture1, _Texture2;

对两张纹理采样后叠加,颜色会得到加深,然后输出

1
2
3
float4 MyFragmentProgram (Interpolators i) : SV_TARGET {
    return tex2D(_Texture1, i.uv) + tex2D(_Texture2, i.uv);
}
纹理叠加
纹理叠加

使用splat贴图

采样splat纹理需要顶点程序提供的UV坐标

1
2
3
4
5
6
7
8
9
10
11
12
13
struct Interpolators {
    float4 position : SV_POSITION;
    float2 uv : TEXCOORD0;
    float2 uvSplat : TEXCOORD1;
};

Interpolators MyVertexProgram (VertexData v) {
    Interpolators i;
    i.position = mul(UNITY_MATRIX_MVP, v.position);
    i.uv = TRANSFORM_TEX(v.uv, _MainTex);
    i.uvSplat = v.uv;
    return i;
}

然后,以在对其他纹理进行采样之前对splat贴图进行采样。

1
2
3
4
5
6
float4 MyFragmentProgram (Interpolators i) : SV_TARGET {
    float4 splat = tex2D(_MainTex, i.uvSplat);
    return
    tex2D(_Texture1, i.uv) +
    tex2D(_Texture2, i.uv);
}

因为splat本身是单通道,可以任选一个RGB通道来来存储值,这里先决定使用第一个纹理与splat贴图R通道相乘。

1
2
return tex2D(_Texture1, i.uv) * splat.r +
       tex2D(_Texture2, i.uv);
调制第一个纹理
调制第一个纹理

第一个纹理现在由splat贴图R通道调制。为了完成插值,我们必须将另一个纹理1-R相乘。

1
2
return tex2D(_Texture1, i.uv) * splat.r +
       tex2D(_Texture2, i.uv) * (1 - splat.r);
调制两个纹理
调制两个纹理

RGB Splat贴图

我们有一个功能性的splat材质,但它只支持两种纹理。我们可以支持更多吗?我们现在只使用了R通道,那么我们添加G和B通道怎么样?那么(1,0,0)代表第一个纹理,(0,1,0)代表第二个纹理,(0,0,1)代表第三个纹理。 为了在这三个之间获得正确的插值,我们只需要确保RGB通道的总和为1即可。

但是等等,当我们只使用一个通道时,我们可以支持两个纹理。这是因为第二个纹理的权重是通过1-R得出的。同样的技巧适用于任意数量的通道。因此可以通过1-R-G-B支持另一种纹理。

这导致了一个具有三种颜色和黑色的splat贴图。只要三个通道加起来不超过1,它就是一个有效的贴图。这里给出一张这样的贴图,导入Unity。

RGB splat 贴图
RGB splat 贴图

当 R + G + B 超过1时会发生什么?

那么前三个纹理的组合会太强。 同时,第四个纹理将被减去而不是被添加。 如果错误很小,那么不会注 意到并且结果足够好。 示例 RGB 映射实际上并不完美,但不会注意到。 纹理压缩引入了更多错误,但 同样难以察觉。

可以使用alpha通道吗?

确实可以! 这意味着单个 RGBA splat 贴图最多可以支持五种不同的地形类型。 但是对于本教程,四个 就足够了。

如果要使用超过五个纹理,则必须使用多个splat贴图。虽然这是可能的,但最终会得到很多纹理采样 此时可以使用更好的技术,例如纹理数组。

为了支持 RGB splat贴图,我们必须在着色器中添加两个额外的纹理。为它们分配了大理石细节和测试纹理。

1
2
3
4
5
6
7
Properties {
    _MainTex ("Splat Map", 2D) = "white" {}
    [NoScaleOffset] _Texture1 ("Texture 1", 2D) = "white" {}
    [NoScaleOffset] _Texture2 ("Texture 2", 2D) = "white" {}
    [NoScaleOffset] _Texture3 ("Texture 3", 2D) = "white" {}
    [NoScaleOffset] _Texture4 ("Texture 4", 2D) = "white" {}
}
Four textures
Four textures

将所需的变量添加到着色器。 再一次,没有额外的 _ST需要的变量。

1
sampler2D _Texture1, _Texture2, _Texture3, _Texture4;

在片段程序中,添加额外的纹理样本。第二个样本现在使用G通道,第三个使用B通道。最终样本用 (1 - R - G - B) 调制。

1
2
3
4
return tex2D(_Texture1, i.uv) * splat.r +
        tex2D(_Texture2, i.uv) * splat.g +
        tex2D(_Texture3, i.uv) * splat.b +
        tex2D(_Texture4, i.uv) * (1 - splat.r - splat.g - splat.b);
四个纹理飞溅
四个纹理飞溅

为什么混合区域在线性色彩空间中看起来不同?

我们的 splat 贴图绕过了 sRGB 采样,所以混合不应该取决于我们使用的颜色空间,对吧? splat 地图 确实不受影响。 但是发生混合的色彩空间确实发生了变化。

在伽马空间渲染的情况下,样本在伽马空间中混合,仅此而已。 但是在线性空间中渲染时,它们首先转换为 线性空间,然后混合,然后再转换回伽马空间。 结果略有不同。 在线性空间中,混合也是线性的。 但在伽 马空间中,混合偏向较深的颜色。

现在知道如何应用细节纹理以及如何将多个纹理与splat贴图混合。也可以组合这些方法。

可以将四个细节纹理添加到splat着色器并使用贴图在它们之间进行混合。当然,这需要四个额外的纹理采样,性能有限。

还可以使用贴图来控制应用细节纹理的位置以及省略的位置。在这种情况下,需要一张单色贴图,它可以用作遮罩。当单个纹理同时包含表示多个不同材质的区域但没有地形那么大的面积时,这很有用。例如,如果我们的大理石纹理还包含金属片,则不希望在此处应用大理石细节。

This post is licensed under CC BY 4.0 by the author.