开启辅助访问
 找回密码
 注册帐号

扫一扫,访问微社区

蛮牛译馆

关注:510

当前位置:游戏蛮牛 技术专区 蛮牛译馆

查看: 861|回复: 1

[外文翻译] Unity渲染 C#教程---Combining Textures(纹理混合)

[复制链接]  [移动端链接]
排名
3816
昨日变化
15

5

主题

60

帖子

468

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
63489
好友
1
蛮牛币
612
威望
0
注册时间
2015-2-27
在线时间
149 小时
最后登录
2017-3-23
发表于 2016-9-19 14:04:55 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有帐号?注册帐号

x
本帖最后由 manew_JR 于 2016-9-19 16:16 编辑

Combining Textures(纹理混合)


内容概要:
简单多重纹理贴图。
应用一个细节纹理贴图。
处理线性空间中的颜色。
使用地形纹理贴图。

这篇文章是我们有关渲染系列的第三篇。在上篇文章中我们介绍了shaders(着色器)和textures(纹理贴图)。我们演示了如何使用单张纹理贴图对地面进行处理。现在我们来讲下更为高级的多纹理贴图。

PS:该教材运行Unity版本为 5.4.0b15。
多纹理贴图混合



1 纹理细节
纹理很好用,但是它也有局限性。纹理的总像素是固定不变的,不论它已何种尺寸显示。当渲染较小尺寸的时候,我们可以使用mipmaps(分级纹理)的方式让渲染看起来好一点。但是如果渲染像素尺寸大于纹理本身,它将变得模糊。因为我们不能凭空制造细节。那应该如何解决呢?

当然,我们可以使用更大的纹理。更多的像素意味着更多的细节。但是纹理的大小总有个限度。与此同时这种方式是以浪费资源为前提而带来很小的效果提升。

另外的一种方法就是增加高像素瓦片贴图。你需要多小就可以制作多小,只不过你得到的将是一张重复的纹理贴图。但是这么做效果也比较一般。当你的鼻子贴着墙看的时候,你就只能看到墙上的小方块。

所以我们要将瓦片纹理和非瓦片纹理进行混合。这里我们试着使用对比度大的纹理。这是我们使用带小波纹的网格。把它抓取到我们的项目中,使用默认导入设置。我将网格之间的分割线调成黑色,这样我们就能更清晰的区分瓦片。

轻微扭曲的网格纹理


复制My First Shaders然后将其重命名为Textured With Detail(带细节纹理贴图),现在我们开始使用者个Shader。

[C#] 纯文本查看 复制代码
Shader "Custom/Textured With Detail" {
        Properties {
                _Tint ("Tint", Color) = (1, 1, 1, 1)
                _MainTex ("Texture", 2D) = "white" {}
        }

        SubShader {
                …
        }
}


使用新的Shader创建一个material(材质),然后把网格材质赋给它。
带网格的细节材质


我们将材质引用到一个方块上看看,从远处看还不错,但是靠近一点你就会发现方块变的模糊不清。缺乏细节和纹理压缩似的它表现的更明显。
靠近格子,低像素纹理和DT1压缩下的效果


1.1 多纹理例子
现在我们来说个简单纹理的例子,并把它的结果用到我们的fragment shader(片段渲染器)。使用这种方式,我们可以很方便的将简单的颜色存储到变量中。

[C#] 纯文本查看 复制代码
float4 MyFragmentProgram (Interpolators i) : SV_TARGET {
    float4 color = tex2D(_MainTex, i.uv) * _Tint;
    return color;
}


我们使用瓦片纹理来增加像素密度。让我来简单处理下第二个纹理例子,将原纹理按照原来的十倍进行平铺。实际上是替换原始的颜色,而不是进行添加。

[C#] 纯文本查看 复制代码
float4 color = tex2D(_MainTex, i.uv) * _Tint;
color = tex2D(_MainTex, i.uv * 10);
return color;



这样我们就得到了一个小的多的网格。在它看起来模糊不清前你可以尽可能的靠近看它。当然因为它是不规则的格子,所以显示出了很明显的重复纹样。
硬编码瓦片纹理

我们实际上制作了两个纹理,但是最后只使用了一个,这看起来有些浪费不是吗?看看下面的vertex programs(顶点着色器),就像我们前一个章节介绍的,我把有关OpenGLCore 和 Direct3D 11有关的代码进行整合。

[C#] 纯文本查看 复制代码
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;
}


[C#] 纯文本查看 复制代码
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




你有没有发现代码里只有一个纹理例子?是的,编译器为我们移除了不必要的代码!编译器在保证最终结果相同的情况下,将所有未使用的内容都丢弃了。

当然,我们并不想替换到原来的例子。我们希望合并两个例子。让我们把它们乘起来。我们需要再一次添加一个扭曲。我们在相同UV坐标的情况下,进行两次采样。

[C#] 纯文本查看 复制代码
float4 color = tex2D(_MainTex, i.uv) * _Tint;
color *= tex2D(_MainTex, i.uv);
return color;



这个shader编译器又做了什么呢?
[C#] 纯文本查看 复制代码
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;
}



[C#] 纯文本查看 复制代码
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



我们再一次得到了一个纹理样本。编译器检查了重复代码并对其进行了优化。所以纹理只采样了一次。其结果被存储到一个寄存器中被复用。哪怕你使用了中间变量,编译器也能足够聪明的将其发现。它追踪到一切输入数据的源头,尽可能高效的组织它们。

现在我们将 x10 UV坐标的图放到第二个采样中,我们就最终的得到了大小两个格子的混合。
[C#] 纯文本查看 复制代码
color *= tex2D(_MainTex, i.uv * 10);


将两张不同的瓦片纹理进行乘法混合

作为纹理样本它们不再相同,编译器不得不使用同时使用他们两个。

[C#] 纯文本查看 复制代码
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;
}



[C#] 纯文本查看 复制代码
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




1.2 分离细节纹理
当两个纹理想乘的时候,纹理会变得更暗。除非至少一个纹理是白色的。因为每一个像素的色值在0到1之间。当添加一个细节纹理的时候,你可以将它变暗或者变亮。

将纹理变量,我们需要一个比1大的值。我们假设它是2,一个比原来颜色大两倍的值。你可以在乘上细节纹理之前将值乘以2.
[C#] 纯文本查看 复制代码
color *= tex2D(_MainTex, i.uv * 10) * 2;


双倍细节

这种方法重新调整了我们使用的纹理细节。乘以1不会带来任何变化。但是两倍的细节采样,现在变成了1/2。这意味着填充变成了灰色而不是白色。纹理不会产生变化。所有值小于1/2都会变暗,所有值大于1/2都会变亮。

所以我们需要一张特殊的细节纹理,这张纹理中间围绕着灰色。这里就有一张这样的纹理。
格子细节纹理


所有的细节纹理都是灰度图吗?

他们不必都是灰度图,但是同来说是这样的。灰度图能够准确调节原始样本的明暗值。操作起来也很简单。乘以一个非灰色的值会让结果变的不直观。但是没有人会阻止你这么做。这么做的结过会带来些颜色的变化和偏移。


使用分离采样,我们必须添加第二个纹理属性值到我们的shader中。使用默认的灰色,这样不会改变主纹理的外观。

[C#] 纯文本查看 复制代码
Properties {
_Tint ("Tint", Color) = (1, 1, 1, 1)
_MainTex ("Texture", 2D) = "white" {}
_DetailTex ("Detail Texture", 2D) = "gray" {}}



将纹理设置到我们的材质中,并把Tilling设置成10.

两个纹理

当然我们还需要一些变量让我们能够访问detail textures(细节纹理),tilling(平铺值)和offset data(偏移数据)。
[C#] 纯文本查看 复制代码
sampler2D _MainTex, _DetailTex;
float4 _MainTex_ST, _DetailTex_ST;



1.3 使用两对UV
为了取代硬编码乘以10的方法,我们需要设置细节纹理的tiling(平铺值)和offset data(偏移值)。我们可以像主UV那样在vertex program(顶点程序)计算最终的细节UV。这就意味着我们需要额外导入一对UV。

[C#] 纯文本查看 复制代码
struct Interpolators {
  float4 position : SV_POSITION;
  float2 uv : TEXCOORD0;
  float2 uvDetail : TEXCOORD1;
};



新的细节UV是由原始的细节UV经过tiling和offset变换后产生的。

[C#] 纯文本查看 复制代码
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;
}


[C#] 纯文本查看 复制代码
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;
}



[C#] 纯文本查看 复制代码
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



要注意的是都定义在了顶点程序代码中。和你的预期一样,OpenGLCore使用了vs_TEXCOORD0和vs_TEXCOORD1。与之形成对比的是,Direct3D 11使用o1这一单一的输出。对于如何解释这些输出提交代码我通常在代码中省略了。
// 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通道中。这实际上是可行的,因为寄存器总是可以存放4个数值。在Direct3D中就是这样实现的。

你可以自己手动打包输出吗?

当然可以,你可以输出任何你想要的结果。包装单独的四个逻辑关系分离的数值到一个输出包中。较少输出能够改善你shader的性能,如果插值确实是这里的性能瓶颈所在的话。

通常情况下打包输出是因为只有少量的插值可以使用。在Shader Model 2的硬件支持8个通用插值器,Shader Model 3的硬件支持10个。复杂的Shader需要考虑如何运行在这些情况下。

现在我们可以在fragment program使用扩展的UV对了。

[C#] 纯文本查看 复制代码
float4 MyFragmentProgram (Interpolators i) : SV_TARGET {
                                float4 color = tex2D(_MainTex, i.uv) * _Tint;
                                color *= tex2D(_DetailTex, i.uvDetail) * 2; 
                                return color;
}


[C#] 纯文本查看 复制代码
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;


[C#] 纯文本查看 复制代码
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;
}




[C#] 纯文本查看 复制代码
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



现在我们的shader具备了完整的功能。主纹理在细节纹理的基础上变得根据亮暗分明。


明暗清晰


1.4 淡化细节
添加细节的方法能够改进当我们靠近和放大时候的问题。但是不支持缩小或者在远处查看,因为这样做会使得瓦片很突出。所以当要显示小尺寸的时候我们需要一种方式来淡化细节纹理。我们可以淡化灰色的细节纹理,这样的结果是颜色并不会改变。

在此之前我们已经这么做过了!我们需要的就是在细节纹理导入设置的时候将Fadeout Mip Maps选项勾上。需要注意的勾选之后会自动将filter mode装换成trilinear模式。这样灰度就会循序渐进变化了。

淡化细节

网格让纹理细节的变换并不是十分的明显,但是我们通常也不会注意到它。例如,这里我们的主要细节纹理只一张大理石的材质。将他们用相同的方式导入我们要用的网格中。
大理石纹理

一旦我们的材质使用了这个纹理,细节纹理的变化就不再明显。

大理石材质

然而我们需要感谢细节纹理,当靠近时大理石材质表现的很好。

靠近时没有细节纹理的情况

1.5 线性颜色空间
在伽马颜色空间我们的shader工作的很好,但是当我们切换到线性颜色空间的时候就出现了问题。使用哪个颜色空间是在项目里设置的。我们可以在Edit/Project Settings/Player设置player settings,在Other Settings面板中对其进行配置。

选择颜色空间


什么是伽马空间?

伽马空间根据伽马值校正颜色。伽马校正是通过修改光线的强度。这是最简单的一种提升原采样值的方式,设置伽马值。伽马值为1表示没有变化。伽马值为2表示为原来值的两倍。

这种转回最初引入是为了使用CRT显示器。另一个好处是它刚好对应了我们眼睛所适合的光线强度。我们眼睛分辨暗的颜色比分辨明亮的颜色更为敏感。所以相对于亮色,我们更倾向于让显示器产生暗色。取幂的方式允许我们这样取值,拉伸低值,挤压高值。

使用最多的颜色模式为sRGB。他使用的公式比取幂更为复杂,它存储的颜色平均值为1/2.2。在多数情况下这是一个合理的平局值。使用伽马2.2可以把颜色转换为原始值。
使用伽马1/2.2编码,使用伽马2.2解码


假设Unity使用sRGB存储纹理和颜色。当在伽马空间渲染时,shaders直接访问原始颜色和纹理数据。这是没有问题的。

当在线性空间渲染时,这就不正确了。GPU会将纹理采样装换到线性空间。同时,Unity将材质颜色变量转换到线性空间。这是shader操作线性空间颜色。在这之后,fragment program将会把输出的值转换回伽马空间。

使用线性颜色的优点是光照计算更加真实。因为光线在现实中是线性的,而不是指数的。不幸的是,这回打乱我们的细节纹理。转换成线性空间之后,颜色变得跟暗了,为什么会产生这种效果?
伽马空间 vs. 线性空间

因为在我们进行两次采样的时候,1/2值的结果在主纹理中没有发生改变。然而,在转换为线性空间的时候这些值变成了½2.2 ≈ 0.22,两次之后为0.44。 值就变得远小于1.这也就解释了为什么变暗。

我们可以使用绕过sRGB采样详细纹理导入设置。这样当伽马空间转变为线性空间时,shader总是访问原始的图片数据。然而,细节纹理还是一个sRGB图片,所以得到的结果还是错误的。

最好的方法是重新调整颜色细节,让他们的值出于1的附近。通过1/½2.2≈4.59,代替2.但是只有当我们渲染线性空间时才这么做。

幸运的是,UnityCG中定义了一个统一的变量用于存储这个需要乘上的值。这是一个floast4类型的rgb分量,本别表示2或者约等于4.59.在伽马空里因为没有alpha通道,所以它总是2.
[C#] 纯文本查看 复制代码
color *= tex2D(_DetailTex, i.uvDetail) * unity_ColorSpaceDouble;

修改这个值之后,我们的细节材质在任何颜色空间渲染的效果就都是一样的了。

2 Texture Splatting

使用细节纹理的显示是整个表面都使用一个纹理。像大理石这种平整的表面它工作的很好。然而如果你的材质不是平整的,你就无法在所有的地方使用相同的细节纹理。

考虑到一个大的地形。里面可能包含了草地,沙土,石头,雪等等。你希望这些类型尽量的接近。但是使用一个纹理覆盖整个地形,像素肯定是不够的。你可以通过使用不同的纹理在不同类型的表面,然后平铺它们。但是你怎么知道什么时候使用什么纹理呢?

我们假设有一个地形里有两种不同的表面类型。每一个点我们都必须选择使用何种表面纹理。第一种或者第二种。我们可以用一个布尔值表示他们。如果是ture的时候,我们使用第一种材质,否则使用第二种。我们可以使用一个灰度纹理来存储这些信息。1代表第一种材质,0代表第二种材质。实际上我们可以使用线性插值表示两种材质之间的情况。使用0和1之间的的值代表混合纹理。用它来平滑过渡。

像这样的纹理我们称之为splat map。如你所见,就像地形投影泼洒在一张画布上一样。因为它是插值的,所以这种地图不需要很高的分辨率。这里有一个样张。
二进制splat map


将其添加入你的工作,修改导入设置。允许Bypass sRGB Sampling,这样它就能在线性空间中生成使用。这是必须的,因为他的纹理并不是sRGB,当需要选上。所以它不需要在线性空间里进行转换。同时设置Wrap Mode为clamp,我们并不会平铺这张地图。
导入设置


复制My First Shader,并将其重命名为Textures Splatting。因为地形并不是统一的,所以我们需要去掉一些功能。


[C#] 纯文本查看 复制代码
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
                }
        }
}

创建一个新的材质使用这个shader,设置splat map为主材质。因为我们还没有改变shader,所以看起来还是原来的样子。

显示splat map


2.1 添加纹理
为了能够选择两个纹理,我们需要在我们的shader中添加两个变量,我们将它们命名为Textures1和Texture2。

[C#] 纯文本查看 复制代码
Properties {
_MainTex ("Splat Map", 2D) = "white" {}
_Texture1 ("Texture 1", 2D) = "white" {}
_Texture2 ("Texture 2", 2D) = "white" {}
}



你可以使用任何你所希望的纹理,这里我使用了我们已经有的格子和大理石纹理。
添加两个纹理

当然我们可以调整到如shader的每个材质的偏移和平铺值。我们确实可以同时设置纹理的属性,但是这会使得有更多的顶点数据通过fragment shader,或者计算UV在shader中调整像素。这样很好,但是一般地形的瓦片纹理都一样。但是splat map并不都是平铺的。所以我们需要一个实例控制偏移和平铺值。

你可以在shader中添加变量,就像在C#中一样。属性NoScaleOffset顾名思义,它是吧tiling和offset当做scale和offset。这并不是统一的命名。

添加属性到我们的临时纹理中,然后保持当前的偏移和平铺值在主纹理的输入值。
[C#] 纯文本查看 复制代码
Properties {
   _MainTex ("Splat Map", 2D) = "white" {}
   [NoScaleOffset] _Texture1 ("Texture 1", 2D) = "white" {}
   [NoScaleOffset] _Texture2 ("Texture 2", 2D) = "white" {}
}


这么做的目的是控制偏移值和平铺值显示在我们shader检视面板的顶部。一会儿我们会使用splat map,把它应用到其他纹理。让我们把平铺值设为4.

没有额外的偏移和平铺控制

现在我们要添加一个采样器变量到我们的shader代码中。但是我们不需要添加他们对应的_ST变量。
[C#] 纯文本查看 复制代码
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _Texture1, _Texture2;


通过检查,我们确实可以这样进行纹理采样,修改fragment shader把它们添加在一起。
[C#] 纯文本查看 复制代码
float4 MyFragmentProgram (Interpolators i) : SV_TARGET {
return tex2D(_Texture1, i.uv) + tex2D(_Texture2, i.uv);
}


两个纹理叠加

2.2 使用splat map
使用splat map采样,我们需要略过从vertex program到fragment program中UV没有修改的值。
[C#] 纯文本查看 复制代码
                      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 map。
[C#] 纯文本查看 复制代码
float4 MyFragmentProgram (Interpolators i) : SV_TARGET {
   float4 splat = tex2D(_MainTex, i.uvSplat);
   return tex2D(_Texture1, i.uv) + tex2D(_Texture2, i.uv);
}


我们使用1代表纹理1.由于我们的splat map是无色的。我们可以使用任意的RGB通道还原它的值。让我们这里使用R通道,并把它乘上纹理。
[C#] 纯文本查看 复制代码
return tex2D(_Texture1, i.uv) * splat.r + tex2D(_Texture2, i.uv);


调整第一个纹理

第一个纹理已经通过splat map调整完成。完成插值,我们需要把其他纹理乘上1-R。
[C#] 纯文本查看 复制代码
return tex2D(_Texture1, i.uv) * splat.r +tex2D(_Texture2, i.uv) * (1 - splat.r);


调整所有的其他纹理


2.3 RGB Splat Map

我们有了一个功能完整的splat 材质,但是它只支持两种纹理。我们能不能支持更多呢?我们只使用了R通道,如果我们继续使用G和B通道呢?(1,0,0)代表第一种材质,(0,1,0)代表第二种材质,(0,0,1)代表第三种材质。为了在这三个之间得到正常的插值,我们只需要保证RGB通道的值总是加上1.

但是等等,当我们使用一个通道的时候,我们能支持两种纹理。这是因为第二个纹理的权值是通过1-R得到的。这种方法适用于任何通道。那么可以通过1 - R - G - B的方式支持其他纹理。

这导致了splat map有三种颜色,和黑色。只要三个通道的值加起来不超过1,就是有效的map。这里又一个这样的map,抓取它,使用和之前一样的导入设置。
RGB splat map


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

当前三个纹理合并的时候过于强烈。与此同时第四个纹理本应该是加上,然而去用减去代替。如果变化很小,你会很难察觉到结果并不是很好。实际上RGB map并不是十分的完美,只是你没有察觉。纹理压缩会导致更多的错误,但是同样的,这些也是很难察觉到的。

我们能够使用alpha通道吗?

当然可以,这也意味着单独的一个RGBA splat map 就可以支持5种不同的地形类型,但是这个系列教程4个已经足够了。
如果你想要使用多余5个的地形,你可以使用多个splat map。这是可行的,到会后你会得到多个的纹理采样。但是我们有更好的解决方案,比如纹理数组。

为了支持RGB splat map,我们需要添加二位的两个问道变量到我们的shader中。这里我使用了大理石纹理和之前的测试纹理。

[C#] 纯文本查看 复制代码
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" {}
}

四个纹理

添加必要的变量到shader中,这里同样不需要额外的_ST变量。
[C#] 纯文本查看 复制代码
sampler2D _Texture1, _Texture2, _Texture3, _Texture4;


在fragment program(片段着色器)里我们需要添加额外的纹理采样。第二个采样现在使用的G通道,第三个采样纹理使用B通道。最后一个纹理使用(1 - R - G - B)。
[C#] 纯文本查看 复制代码
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 map使用 bypasses SRGB的方式进行采样,所以混合方式应该是不依赖于颜色空间,对吗?splat map确实不会受到影响,但是颜色空间在混合的时候就会发生变化。

在伽马空间渲染的时候,样本在伽马空间进行混合。但是在线性空间渲染时,它先要转换到线性空间,然后混合,然后再转换回伽马空间。这样结果就会有一些变化。在线性空间里,线性混合是很好的。但是在伽马空间里,混合后会偏向较暗的颜色。

现在我们知道了如实使用细节纹理以及如何使用splat map混合多种纹理。当然我们也可以把这两种方法混合使用。

你可以添加四个细节纹理到splat shader中,利用它们对地图进行混合。当然你需要额外的四个纹理,它们并不会凭空出现。

你可以使用一张地图去哪些纹理什么时候显示,什么时候不显示。在这个例子中,你需要一张单色的地图作为功能遮罩。这在需要使用单一地形但是包含其他不同纹理的情况下十分有用,但并不适合大地形。例如,我们的大理石纹理中包含金属碎片,你并不希望这要处理大理石纹理。

下一篇教程我们将介绍 The First Light(第一道光)。


原文链接:http://catlikecoding.com/unity/tutorials/rendering/part-3/

扫描下方二维码关注游戏蛮牛官方微信~每日都有精选干货与你分享呦~





更多图片 小图 大图
组图打开中,请稍候......

回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册帐号

本版积分规则

快速回复 返回顶部 返回列表