其他分享
首页 > 其他分享> > 【跟着catlikecoding学渲染#2】纹理混合

【跟着catlikecoding学渲染#2】纹理混合

作者:互联网

一, Detail Texture

纹理很好,但它们有局限性。它们具有固定数量的纹素,无论它们以何种大小显示。如果它们被渲染得很小,我们可以使用mipmap来保持它们看起来不错。但是当它们被渲染得很大时,它们会变得模糊。

  我们不能凭空发明额外的细节,所以没有办法解决这个问题。还是有? 当然,我们可以使用更大的纹理。更多的纹素意味着更多的细节。但是纹理的大小是有限制的。而且,存储大量仅在近距离可见的额外数据是一种浪费。

  增加纹理密度的另一种方法是平铺纹理。然后你可以随心所欲地变小,但你显然会得到一个重复的模式。但是,这在近距离内可能并不明显。毕竟,当你站着鼻子碰到一堵墙时,你只会看到整面墙的一小部分。

  因此,我们应该能够通过将未切片的纹理与平铺纹理相结合来添加细节。为了尝试这一点,让我们使用具有明显图案的纹理。这是一个方格网格。

Shader "Custom/Textured With Detail" {

	Properties {
		_Tint ("Tint", Color) = (1, 1, 1, 1)
		_MainTex ("Texture", 2D) = "white" {}
	}

	SubShader {
		…
	}
}

  将材质指定给四边形并查看它。从远处看,它看起来会很好。但是靠得太近,它会变得模糊和模糊。除了缺乏细节外,纹理压缩引起的伪像也会变得明显。

1.1 Multiple Texture Samples

我们获取采样一下纹理的颜色作为片元着色器的结果

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

因此,我们可以尝试通过引入平铺纹理来增加纹素密度

float4 color = tex2D(_MainTex, i.uv) * _Tint;
				color = tex2D(_MainTex, i.uv * 10);//好家伙直接乘了个十倍
				return color;

我们可以尝试将这两个示例组合在一起。让我们通过将它们相乘来做到这一点。但再一次,让我们添加一个转折点。使用完全相同的 UV 坐标对纹理进行两次采样,合成巨大喷流。

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

1.2 Separate Detail Texture

将两个纹理相乘时,结果会变暗。除非至少有一个纹理是白色的。这是因为纹素的每个颜色通道都有一个介于 0 和 1 之间的值。向纹理添加细节时,您可能希望通过变暗和增亮来执行此操作。

   要使原始纹理变亮,您需要大于 1 的值。假设最多 2 个,这将使原始颜色加倍。这可以通过将细节样本加倍,然后再将其与原始颜色相乘来支持。

color *= tex2D(_MainTex, i.uv * 10) * 2;

  乘以 1 不会改变任何东西。但是,当我们将详细样本加倍时,现在1/2也是如此。这意味着纯灰色(而不是白色)纹理不会产生任何变化。所有低于 1/2 的值都会使结果变暗,而高于 1/2 的任何值都会使结果变亮。

因此,我们需要一个以灰色为中心的特殊细节纹理。这是网格的纹理。

Must detail textures be grayscale?

细节纹理不一定是灰度的,但通常是灰度的,灰度细节纹理可以通过增亮和变暗来严格调整原始颜色,使用非灰色颜色进行乘法会产生不太直观的结果

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

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

sampler2D _MainTex, _DetailTex;
float4 _MainTex_ST, _DetailTex_ST;

 1.3 Using Two UV Pairs

我们k有直接再顶点着色器中计算最终的UV

struct Interpolators {
				float4 position : SV_POSITION;
				float2 uv : TEXCOORD0;
				float2 uvDetail : TEXCOORD1;
			};

新的细节 UV 是通过使用细节纹理的平铺和偏移来变换原始 UV 来创建的。

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;
			}

现在我们可以输出给片元着色器了

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

1.4 Fading Details

  添加细节的想法是,它们可以近距离或放大来改善材料的外观。它们不应该在远处可见或缩小,因为这会使平铺变得明显。因此,我们需要一种方法来随着纹理显示尺寸的减小而淡化细节。

我们可以通过将细节纹理淡化为灰色来做到这一点,因为这不会改变颜色。 我们以前做过这个!我们需要做的就是在细节纹理的导入设置中启用淡出 Mip maps。请注意,这还会自动将滤镜模式切换到三线性,以便渐进到灰色是渐进的。

然后我们通过一个大理石的材质来演示这个效果

1.5 Linear Color Space

在 Gamma 色彩空间中渲染场景时,着色器工作正常,但如果切换到线性色彩空间,着色器就会出错,Unity默认Gamma空间,而选择线性空间时,引擎的渲染流程会再线性空间计算,理想状况下项目使用线性空间的贴图颜色,不需要勾选sRGB,如果勾选了,Unity会通过硬件特性采样时进行线性转换

详细见语雀笔记https://www.yuque.com/docs/share/6806031f-351e-419b-b2fa-40506cbebc27?#

What is gamma space

伽玛空间是指伽玛校正颜色。伽玛校正是对光强度的调整。最简单的方法是将原始价值提高到一定程度,因此价值游戏。γ 值为 1 表示没有变化。灰度系数为 2 表示原始值是平方的。

   引入此转换最初是为了适应CRT显示监视器的非线性特性。另一个好处是,它也大致对应于我们的眼睛对不同光强度的敏感程度。我们注意到深色之间的差异多于明亮颜色之间的差异。因此,将数字数字的更多位专用于较暗的值而不是较亮的值是有意义的。幂允许我们通过在较大的范围内拉伸较低的值,同时压扁较高的值来做到这一点。

  Unity 假定纹理和颜色存储为 sRGB。在 Gamma 空间中渲染时,着色器直接访问原始颜色和纹理数据。这就是我们到目前为止的假设。 在线性空间中渲染时,情况不再如此。GPU 会将纹理样本转换为线性空间。

  此外,Unity 还会将材质颜色属性转换为线性空间。然后,着色器使用这些线性颜色进行操作。之后,片段程序的输出将被转换回伽玛空间。 使用线性颜色的优点之一是它可以实现更真实的照明计算。这是因为光的相互作用在现实生活中是线性的,而不是指数级的。

由于我们将细节纹理样本加倍,因此值为 1/2 会导致主纹理不会发生更改。然而,转换为线性空间会将其更改为接近1/22.2≈0.22。翻倍大约是0.44,远小于1。这解释了变暗的原因。

  我们可以通过在细节纹理的导入设置中启用绕过sRGB采样来解决此错误。这可以防止从 gamma 转换为线性空间,因此着色器将始终访问原始图像数据。但是,细节纹理是 sRGB 图像,因此结果仍然是错误的。

   最好的解决方案是重新对齐细节颜色,使它们再次以1为中心。我们可以通过乘以1 / 1 / 22.2≈4.59而不是2来做到这一点。但是,只有当我们在线性空间中渲染时,我们才能这样做。

  幸运的是,UnityCG定义了一个统一变量,该变量将包含要乘以的正确数字。它是一个 float4,其 rgb 分量中包含 2 个或大约 4.59 个(视情况而定)。由于伽玛校正未应用于 Alpha 通道,因此它始终为 2

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

二.Texture Splatting

细节纹理的局限性在于,整个表面使用相同的细节。这适用于均匀的表面,如大理石板。但是,如果您的材质没有统一的外观,则不希望在任何地方都使用相同的细节。 考虑一个大地形。它可以有草,沙子,岩石,雪等。

  您希望这些地形类型在近距离内相当详细。但是,覆盖整个地形的纹理永远不会有足够的纹素。您可以通过对每种表面类型使用单独的纹理并平铺这些纹理来解决此问题。但是你怎么知道在哪里使用哪种纹理呢?

  假设我们有一个具有两种不同表面类型的地形。在每一点上,我们都必须决定使用哪种表面纹理。要么是第一个,要么是第二个。我们可以用布尔值来表示它。如果设置为 true,我们将使用第一个纹理,否则使用第二个纹理

  。我们可以使用灰度纹理来存储此选择。值 1 表示第一个纹理,而值 0 表示第二个纹理。实际上,我们可以使用这些值在两个纹理之间进行线性插值。然后,介于 0 和 1 之间的值表示两种纹理之间的混合。这使得平滑过渡成为可能。 这样的纹理被称为splat贴图。这就像您将多个地形特征溅到画布上一样。由于插值,此map甚至不需要高分辨率。

  将其添加到项目后,将其导入类型切换为高级。启用旁路 sRGB 采样并指示其 mipmap 应在线性空间中生成。这是必需的,因为纹理不代表 sRGB 颜色,而是选择。因此,在线性空间中渲染时不应对其进行转换。另外,将其包装模式设置为夹紧,因为我们不打算平铺此map。

Shader "Custom/Texture Splatting" {

	Properties {
//		_Tint ("Tint", Color) = (1, 1, 1, 1)
		_MainTex ("Splat Map", 2D) = "white" {}
	}

	SubShader {

		Pass {
			CGPROGRAM

			#pragma vertex MyVertexProgram
			#pragma fragment MyFragmentProgram

			#include "UnityCG.cginc"

//			float4 _Tint;
			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); // * _Tint;
			}

			ENDCG
		}
	}
}

创建使用此着色器的新材质,并将 splat 贴图指定为其主要纹理。由于我们尚未更改着色器,因此它只会显示地图。

2.1Adding Textures

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

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

当然,我们为添加到着色器中的每个纹理提供了平铺和偏移控件。我们确实可以单独支持对每个纹理进行单独的平铺和偏移。但这需要我们将更多数据从顶点传递到片段着色器,或者计算像素着色器中的 UV 调整。

  这很好,但通常地形的所有纹理都是相同的平铺。而且 splat map根本没有平铺。因此,我们只需要一个平铺和偏移控件的实例。 可以向着色器属性添加特性,就像在 C# 代码中一样。NoScaleOffset 属性将按照其名称建议执行。是的,它确实将平铺和偏移称为比例和偏移。它不是非常一致的命名。

  让我们将此属性添加到额外的纹理中,并保留主纹理的平铺和偏移输入。

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

  然后我们再处理一下

sampler2D _MainTex;
			float4 _MainTex_ST;

			sampler2D _Texture1, _Texture2;
float4 MyFragmentProgram (Interpolators i) : SV_TARGET {
				return
					tex2D(_Texture1, i.uv) +
					tex2D(_Texture2, i.uv);
			}

2.2 Using the Splat Map

为了对splat贴图进行采样,我们还必须将未修改的UV从顶点程序传递到片段程序。

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 贴图进行采样,然后再对其他纹理进行采样。

float4 MyFragmentProgram (Interpolators i) : SV_TARGET {
				float4 splat = tex2D(_MainTex, i.uvSplat);
				return
					tex2D(_Texture1, i.uv) +
					tex2D(_Texture2, i.uv);
			}

我们决定值 1 表示第一个纹理。由于我们的 splat 映射是单色的,因此我们可以使用任何 RGB 通道来检索此值。让我们使用 R 通道并将其与纹理相乘。

return
    tex2D(_Texture1, i.uv) * splat.r +
    tex2D(_Texture2, i.uv);

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

return
		tex2D(_Texture1, i.uv) * splat.r +
		tex2D(_Texture2, i.uv) * (1 - splat.r);

 

2.3 RGB Splat Map

  我们有一个功能 splat material,但它只支持两种纹理。我们能支持更多吗?我们只使用R通道,那么我们是否也添加G和B通道呢?然后 (1,0,0) 表示第一个纹理,(0,1,0) 表示第二个纹理,(0,0,1) 表示第三个纹理。为了在这三者之间获得正确的插值,我们只需要确保RGB通道的总和等于1。

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

  这将导致具有三种颜色和黑色的splat地图。只要三个通道加在一起不超过1,它就是一个有效的地图。这是这样一张地图,抓住它并使用与以前相同的导入设置。

What happens when R + G + B exceeds 1?

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

Can we use the alpha channel as well?

确实可以!这意味着单个 RGBA 板图最多可以支持五种不同的地形类型。但对于本教程,四个就足够了。 如果要使用五个以上的纹理,则必须使用多个 splat 贴图。虽然这是可能的,但你最终会得到很多纹理样本。此时可以使用更好的技术,例如纹理数组。

要支持 RGB 板块贴图,我们必须向着色器添加两个额外的纹理。我为他们分配了大理石细节和测试纹理。

	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" {}
	}

将所需的变量添加到着色器。不需要额外的_ST变量。

sampler2D _Texture1, _Texture2, _Texture3, _Texture4;

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

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);

 

Why do the blended regions look different in linear color space?

我们的 splat 贴图绕过了 sRGB 采样,因此混合不应该取决于我们使用的色彩空间,对吧?splat地图确实不受影响。但是发生混合的色彩空间确实会发生变化。 在伽玛空间渲染的情况下,样本在伽玛空间中混合,仅此而已。

  但是在线性空间中渲染时,它们首先被转换为线性空间,然后混合,然后转换回伽马空间。结果略有不同。在线性空间中,混合也是线性的。但在伽马空间中,混合倾向于较深的颜色。

现在,您知道了如何应用细节纹理以及如何将多个纹理与 splat 贴图混合。也可以将这些方法结合起来。 您可以向 splat 着色器添加四个细节纹理,并使用贴图在它们之间进行混合。

  当然,这需要四个额外的纹理样。 您还可以使用贴图来控制细节纹理的应用位置以及省略细节纹理的位置。在这种情况下,您需要一个单色贴图,它充当蒙版。当单个纹理包含表示不同材质的区域,但其比例不如地形大时,这很有用。例如,如果我们的大理石纹理也包含金属片,那么您不希望在那里应用大理石细节。

标签:tex2D,MainTex,catlikecoding,渲染,uv,纹理,着色器,splat
来源: https://www.cnblogs.com/Naxts1021/p/16321071.html