美文网首页Unity Shader分享Shader
Unity 渲染教程(三):使用多张纹理贴图

Unity 渲染教程(三):使用多张纹理贴图

作者: 码农老K | 来源:发表于2017-10-21 20:38 被阅读294次

    对多个纹理进行采样

    应用一张细节贴图

    在线性空间中处理颜色

    使用一张splat纹理

    这是关于渲染的教程系列的第三部分。 前面的部分介绍了着色器和纹理。 我们已经看到如何使用单个纹理来使平坦表面看起来更加复杂。 现在我们超越了它,同时使用多个纹理。

    本教程是使用Unity 5.4.0进行的,目前该版本是开放测试版。 我使用build 5.4.0b15。

    混合的多张贴图

    texel的解释

    texel(纹理元素的简写)是纹理图形的基本单位,用于定义三维对象的曲面。3D 对象曲面的基本单位是纹理,而 2D 对象由像素组成。

    纹理,纹理元素或纹理像素是纹理图的基本单位,[1]用于计算机图形学。 纹理由表示纹理空间的纹素数组表示,就像其他图像由像素数组表示一样。

    Texels也可以通过通过简单的过程获得的图像区域进行描述,例如阈值。 Voronoi细分可以用来定义它们的空间关系。 这意味着在每个纹理像素的质心和每个周围纹理的质心之间的中点进行整个纹理的划分。 结果是每个纹素重心都会围绕着一个Voronoi多边形。 这个多边形区域由所有比其他质心更贴近纹素质心的点组成

    当纹理化3D表面或表面(称为纹理映射的过程)时,渲染器将纹素映射到输出图像中的适当像素。在现代计算机上,该操作在图形处理单元上完成。

    纹理过程从空间中的位置开始。该位置可以在世界空间中,但通常它在模型空间中,使得纹理与模型一起移动。将投影机功能应用于位置,以将位置从三元素向量改变为值为零到一(uv)的两元素向量。[3]这些值乘以纹理的分辨率以获得纹素的位置。当请求的纹素不在整数位置时,应用纹理过滤。

    当请求纹理外部的纹理,使用两种技术之一:夹紧或缠绕。夹紧将纹理限制到纹理大小,如果超过纹理大小,则将其移动到最接近的边。包装以纹理大小的增量移动纹理文件,以将其重新导入纹理。包装导致重复纹理;夹紧使其仅在一个位置。

    纹理很好,但是有局限性。 它们具有固定数量的纹素,无论它们显示什么大小。 如果它们变小,我们可以使用mipmap来保持它们的良好状态。 但是当它们变大时,它们变得模糊。 我们不能发明任何细节,所以没有办法。 还是在那里?

    当然,我们可以使用更大的纹理。 更多的纹素意味着更多的细节。 但纹理的大小有限制。 存储很多额外的数据是很浪费的,这些数据只是很接近。

    增加纹理密度的另一种方法是平铺纹理。 那么你可以得到你想要的那么小,但你显然会得到一个重复的模式。 尽管如此,这可能并不明显。 毕竟,当你站在你的鼻子上碰壁,你只会看到整个墙壁的一小部分。

    因此我们最好是能够通过合并未平铺的贴图和一张平铺的贴图来实现添加纹理。为了达到这个目的,让我们使用一张有明显样式的贴图。下面是一张方格网格。默认设置方式导入工程。我稍微扭曲了一下网格,使它更有趣,并使它可以感知到它的平铺。

    轻微扭曲的网格贴图

    复制一份My First Shader并重新命名为Textured With Detail。我们从现在开始使用新的 shader。

    Shader"Custom/Textured With Detail"{    Properties {        _Tint ("Tint",Color) = (1,1,1,1)        _MainTex ("Texture",2D) ="white"{}    }    SubShader {        …    }}

    创建一个新的材质球,并把这张贴图赋值给它。

    带网格的细节材质

    把材质球附加给四边形并查看一下。从远处看,它还挺好的。但是越靠近,它会变得越来越模糊。除了缺少细节外,由于压缩而导致的粗糙的艺术品也特别明显。

    多次贴图采样

    目前为止我们只是使用单张贴图来进行的采样,并使用采样的数据作为我们的片段程序的结果。接下来,我们将尝试去改变这种取样的方式,我们可以把采用的数据存储在一个临时的变量当中。

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

    我们推断,我们可以通过引入一张平铺纹理来增加纹理密度。 我们简单地表现第二张纹理,这张纹理的采样是原始纹理采样的10倍的平铺,它是原始样本的十倍。 实际上替换原来的颜色,而不是叠加上去的。

    float4color= tex2D(_MainTex, i.uv) * _Tint;color= tex2D(_MainTex, i.uv *10);returncolor;

    这样就产生了更细更小的网格。在网格看起来模糊之前,你现在要比前面能够靠得更近一些,也就是说没那么容易看得出来模糊了,也就是图像的分辨率变得更高了。

    注意,到目前为止,我们表现了2张贴图,而最终只使用了他们中的一张。这看起来很浪费。是这样吗?让我们来看看编译后的代码,分别在 OpenGLCore 和Direct3D 11平台下。

    uniformsampler2D_MainTex;invec2vs_TEXCOORD0;layout(location =0)outvec4SV_TARGET0;vec2t0;voidmain(){    t0.xy = vs_TEXCOORD0.xy *vec2(10.0,10.0);    SV_TARGET0 =texture(_MainTex, t0.xy);return;}

    SetTexture0[_MainTex]2D0ps_4_0      dcl_sampler s0, mode_default      dcl_resource_texture2d (float,float,float,float) t0      dcl_input_ps linear v0.xydcl_output o0.xyzwdcl_temps10:mulr0.xy, v0.xyxx, l(10.000000,10.000000,0.000000,0.000000)1: sample o0.xyzw,r0.xyxx, t0.xyzw, s02:ret

    硬编码的平铺

    你发现没,在编译过后的代码中只有一张贴图在采样?对的。编译器替我们移除掉了不需要的代码。本质上来讲,编译器会以最后的结果为准,而舍弃掉任何对于结果无用的的事情。

    当然话又说转来,我们并不是想替换掉原来的取样。我们想要的是合并两次取样。让我们把这两次取样的结果相乘起来,来实现我们想要的合并效果。

    float4color= tex2D(_MainTex, i.uv) * _Tint;color*= tex2D(_MainTex, i.uv);returncolor;

    那 shader 编译器是如何做的呢?

    uniformsampler2D_MainTex;invec2vs_TEXCOORD0;layout(location =0)outvec4SV_TARGET0;mediumpvec4t16_0;lowpvec4t10_0;voidmain(){    t10_0 =texture(_MainTex, vs_TEXCOORD0.xy);    t16_0 = t10_0 * t10_0;    SV_TARGET0 = t16_0 * _Tint;return;}

    SetTexture0[_MainTex]2D0ConstBuffer"$Globals"144Vector96[_Tint]BindCB"$Globals"0ps_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.xydcl_output o0.xyzwdcl_temps10: sampler0.xyzw, v0.xyxx, t0.xyzw, s01:mulr0.xyzw,r0.xyzw,r0.xyzw2:mulo0.xyzw,r0.xyzw, cb0[6].xyzw3:ret

    再一次,我们的结果里面只有单次纹理采样。编译器检测到了重复的代码然后做了优化,因此贴图只被采样了一次。采样的结果被存储到寄存器中被重复使用。编译器足够聪明,能够检测到这些代码是重复的,即使我们使用的是中间变量。它会追溯所有东西的原始输入,然后它尽可能高香的识别到任何东西。

    现在我们把第二次取样中的UV 坐标与10的相乘结合在一块来写。我们最终看到了大小网格合并在一起了(既有大的网格,又有小的网格)

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

    两次不同的平铺相乘的结果

    由于两次取样不再相同,编译器就不得不对这两次采样都视为有用而使用进去。(也就是可以总结说,前面我们讲到的采用一次的现象,是因为编译器对一模一样的采样会智能的进行优化舍弃,取其一来进行取样,而不会取样两次)

    uniformsampler2D_MainTex;invec2vs_TEXCOORD0;layout(location =0)outvec4SV_TARGET0;vec4t0;lowpvec4t10_0;vec2t1;lowpvec4t10_1;voidmain(){    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;}

    SetTexture0[_MainTex]2D0ConstBuffer"$Globals"144Vector96[_Tint]BindCB"$Globals"0ps_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.xydcl_output o0.xyzwdcl_temps20: sampler0.xyzw, v0.xyxx, t0.xyzw, s01:mulr0.xyzw,r0.xyzw, cb0[6].xyzw2:mulr1.xy, v0.xyxx, l(10.000000,10.000000,0.000000,0.000000)3: sampler1.xyzw,r1.xyxx, t0.xyzw, s04:mulo0.xyzw,r0.xyzw,r1.xyzw5:ret

    分离细节贴图

    当你将两张贴图相乘,相乘后的渲染结果会是比较黑的。除非其中一张图是白色的。那是因为每个纹理元素的颜色通道的值都介于0到1之间。当添加了一张细节贴图到一张贴图上,你可能想通过变暗来实现,但是也可以通过变亮来实现。

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

    我的扩展理解

    tex2D采样有两种模式,Clamp和Repeat。Clamp只会在UV指定的区域进行取样,而Repeat会重复取样。比如我们uv*10,那么UV坐标的区间就变成了(0,10),超过1后,tex2D函数又开始重复的取,比如UV为(1.5,1.5),那么取样点就同(1.5-1,1.5-1)=(0.5,0.5)这个点。

    另外,我们取样后本来是颜色值,介于(0,1),如果乘以一个倍率,将不是一个线性的颜色增强,而是分区间段的。比如乘以2,那么0.5以上的UV区间比0.5以上的UV区间增强的效果要明显一些。

    乘以2倍的细节

    这种方法要求我们重新解释用于细节的纹理。 乘以1不改变任何东西。 但是,当我们加倍的细节样本,这是现在的½。 这意味着坚实的灰色 - 不是白色的纹理将不会产生变化。 低于1/2的所有值都会使结果变暗,而½以上的任何值会使其变亮。

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

    图片的下方注解

    细节贴图必须是灰度级的么?

    它们不必是灰度级的,但它们通常是灰度级的。 灰度细节纹理将通过增亮和变暗来严格调整原色。 这是比较直接的工作。 与非灰色的乘法产生较不直观的结果。 但没有什么是阻止你做这件事。 彩色细节纹理用于产生微妙的色彩偏移。

    要使用一张单独的细节贴图,我们就得添加第二个贴图属性到我们的shader中。默认使用它的灰度,这样就可以不改变我们主贴图的外观。

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

    把这张贴图添加到材质球上并把tiling设置为10.

    两张贴图

    当然我们还需要在代码里面添加细节贴图和它的平铺和偏移数据的变量。

    sampler2D_MainTex, _DetailTex;            float4 _MainTex_ST, _DetailTex_ST;

    使用两对UV

    取代硬编码的直接乘以10,我们现在应该使用细节贴图的平铺和偏移数据了。我们可以最终计算出细节贴图的UV,就像我们在顶点程序中计算主贴图那样,这意味着我们需要插入一对新的UV

    struct Interpolators {                float4position: 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;}

    uniformvec4_Tint;uniformvec4_MainTex_ST;uniformvec4_DetailTex_ST;invec4in_POSITION0;invec2in_TEXCOORD0;outvec2vs_TEXCOORD0;outvec2vs_TEXCOORD1;vec4t0;voidmain(){    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;}

    Vector112[_MainTex_ST]Vector128[_DetailTex_ST]ConstBuffer"UnityPerDraw"352Matrix0[glstate_matrix_mvp]BindCB"$Globals"0BindCB"UnityPerDraw"1vs_4_0      dcl_constantbuffer cb0[9], immediateIndexed      dcl_constantbuffer cb1[4], immediateIndexed      dcl_input v0.xyzwdcl_input v1.xydcl_output_siv o0.xyzw, position      dcl_output o1.xydcl_output o1.zwdcl_temps10:mulr0.xyzw, v0.yyyy, cb1[1].xyzw1: madr0.xyzw, cb1[0].xyzw, v0.xxxx,r0.xyzw2: madr0.xyzw, cb1[2].xyzw, v0.zzzz,r0.xyzw3: mad o0.xyzw, cb1[3].xyzw, v0.wwww,r0.xyzw4: mad o1.xy, v1.xyxx, cb0[7].xyxx, cb0[7].zwzz5: mad o1.zw, v1.xxxy, cb0[8].xxxy, cb0[8].zzzw6:ret

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

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

    uniformvec4_Tint;uniformvec4_MainTex_ST;uniformvec4_DetailTex_ST;uniformsampler2D_MainTex;uniformsampler2D_DetailTex;invec2vs_TEXCOORD0;invec2vs_TEXCOORD1;layout(location =0)outvec4SV_TARGET0;vec4t0;lowpvec4t10_0;lowpvec4t10_1;voidmain(){    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;}

    SetTexture0[_MainTex]2D0SetTexture1[_DetailTex]2D1ConstBuffer"$Globals"144Vector96[_Tint]BindCB"$Globals"0ps_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.xydcl_input_ps linear v0.zwdcl_output o0.xyzwdcl_temps20: sampler0.xyzw, v0.xyxx, t0.xyzw, s01:mulr0.xyzw,r0.xyzw, cb0[6].xyzw2: sampler1.xyzw, v0.zwzz, t1.xyzw, s13:mulr0.xyzw,r0.xyzw,r1.xyzw4:addo0.xyzw,r0.xyzw,r0.xyzw5:ret

    现在我们的shader见效了。主贴图变得既明亮又暗,基于细节贴图。

    亮部和暗部

    far away

    close-up

    让细节消隐

    添加细节的想法是,它们可以使材料的外观更加贴近或缩小,不应该看得很远或者缩小,因为这样使得平铺很明显。 所以当纹理的显示尺寸减小时,我们需要一种淡化细节的方法。 我们可以通过将细节纹理淡化为灰色来实现,因为没有改变颜色。

    我们已经做到了! 我们需要做的就是在详细纹理的导入设置中启用Fadeout Mip Maps。 请注意,这也自动将过滤器模式切换为三线性,以使渐变为灰色渐变。

    图片的下方注解

    很明显能看出来,网格从细节详细到几乎没什么细节,但通常我们不会察觉到。比如,这里有一张大理石材质的主贴图和一张细节贴图。拖拽到工程中,使用默认设置。

    大理石贴图

    一旦我们的材质球使用了这些贴图,贴图细节的消失就不在那么引起注意了。

    大理石材质

    然而,多亏了有这张细节贴图,我们近距离观看这张大理石贴图感觉好多了。

    大理石贴图

    线性颜色空间

    我们的 shder 在 gamma 空间中渲染是OK的,但一旦我们切换到线性空间就出错了。

    选择颜色空间

    什么是伽马颜色空间?

    伽玛空间是指伽马校正的颜色。伽马校正是光强度的调整。最简单的方法是将原始价值提高到一定的强度,即originValue 的 gamma 次方。 伽马值为1意味着没有变化。 伽马值为2意味着是原始值是平方的。

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

    gamma空间与线性空间的对比

    最广泛使用的图像颜色格式是sRGB。它使用比简单求幂更复杂的公式,但它存储平均伽玛为1 / 2.2的颜色。这在很多情况下是一个合理的近似。要将此数据转换回其原始颜色,请应用2.2的伽马校正。

    贴图混合技术(Texture Splatting)

    细节贴图的一个限制是相同的细节被使用在了整个表面上。在均衡的统一的表面上这个当然表现得还好,像大理石平面。然而,当表面没有一个统一的外观,你就不想使用相同的细节来表现每一个地方。

    想象一下一个巨大的地形。它有可能有草,有沙子,有石头,有积雪等等。你想要这些地形类型的细节能够搭配得很贴合。但是一张贴图来覆盖整个地形,永远不能拥有足够的纹理元素。你可以针对每一个表面类型使用一张单独的贴图来解决这个问题,然后平铺它们。但是你怎么知道哪张贴图应该使用在哪个地方呢?

    假使我们一个地形上只有两种表面类型。每一个顶点我们都要决定使用哪种类型表面的贴图,要么是第一种,要么是第二种。我们可以用一个布尔值来代表这个。设置为true,代表使用第一张贴图,设置为false代表使用第二张贴图。我们可以用一张灰度图来存储这个信息。值为1代表true也就是使用第一张贴图,值为0代表fakse也就是使用第二张贴图。实际上,我们可以使用这些值来在这两张贴图见进行线性插值。这样介于0到1之间的值就代表是这两张贴图的混合。这样就能做到过渡平滑。

    Binary splat map

    把这张贴图导入工程后设置它的贴图类型为Advanced。开启Bypass sRGB Sampling选项,这预示着它的mipmap应该在线性空间中生成。因为这张贴图并不呈现sRGB颜色(因为它是一张灰度图嘛),所以这是必须的而不是可选项。因此这张灰度图在线性空间渲染的时候应该不被转化。因此,设置这张贴图的包裹模式为clamp,因为我们不会去平铺它。

    导入设置

    拷贝My First Shader,创建一个新的名为Texture Splatting的shader。因为地形是典型的不统一进行着色的(它要用很多贴图和细节来表现),因此去掉Tint。

    Shader"Custom/Texture Splatting"{    Properties {//        _Tint ("Tint", Color) = (1, 1, 1, 1)_MainTex ("Splat Map",2D) ="white"{}    }    SubShader {        Pass {            CGPROGRAM#pragmavertex MyVertexProgram#pragmafragment MyFragmentProgram#include "UnityCG.cginc"//            float4 _Tint;sampler2D _MainTex;            float4 _MainTex_ST;structVertexData {                float4 position : POSITION;                float2 uv : TEXCOORD0;            };structInterpolators {                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);returni;            }            float4 MyFragmentProgram (Interpolators i) : SV_TARGET {returntex2D(_MainTex, i.uv);// * _Tint;}            ENDCG        }    }}

    创建一个使用这个shader的材质球,并把这张splat贴图附加上去作为主贴图。因为我们还没有更改shader的逻辑,所以现在显示的就是这张灰度图。

    显示中的splat贴图

    添加贴图

    为了能够选择2张贴图,我们需要在我们的shader中添加2个属性,姑且就叫Texture1Texture2吧。

    Properties{

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

    你可以使用任意你想使用的贴图。我就简单的使用已经存在的大理石这张贴图。

    2张新增的贴图

    当然,我们会对添加到着色器的每个纹理进行平铺和偏移控制。我们确实可以单独地为每一个纹理分别提供单独的tiling和偏移量。但这需要我们将更多的数据从顶点传递到片段着色器,或者计算像素着色器中的UV调整。这很好,但是典型的地形纹理都是一样的。而一张splat的贴图根本就没有进行平铺。因此,我们只需要一个tiling和偏移控制的贴图。

    你可以像c#代码那样添加一个属性到shader的属性中去。NoScaleOffset属性会暗示这一点。这样贴图就不再拥有tiling和offset的shader属性了。

    Properties{

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

    添加sampler变量到shader代码中。我们没有必要添加_ST变量。

    去掉tiling和offset控制

    sampler2D_MainTex;            float4 _MainTex_ST;sampler2D_Texture1, _Texture2;

    为了检查我们是否能够以这种方式对纹理进行采样,更改片段着色器,以便将它们添加到一起。

    使用Splat贴图

    为了对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);

    两张贴图,叠加在一起的效果

    RGB Splat Map

    我们现在让材质球已经起效果了,但是我们只支持了2张贴图。我们能支持更多的贴图么?我们现在只使用了R通道,那么我们同时使用R和G通道呢?于是(1,0,0)就代表使用第1张贴图,(0,1,0)代表第2张贴图,(0,0,1)就可以代表第3张贴图。为了在这三张贴图之间能够得到一个正确的插值,我们只需要保证splat贴图中的RGB值加起来不超过1。

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

    这就形成了一张有三种颜色和黑色的splat地图。只要三个通道加在一起不超过1,它就是一个有效的映射。下面就是这样一张贴图,导入进工程,使用和以前一样的导入设置。

    带RGB的splat贴图

    center>当R+G+B大于1时会发生什么?

    如果R+G+B加起来大于1,前三种纹理的混合将会过于强烈。同时,第四种纹理的效果会被减去,而不是增加。如果错误很小,那么您就不会注意到,结果已经足够好了。示例RGB映射实际上并不完美,但您不会注意到。纹理压缩会带来更多的错误,但这很难被注意到。

    我们可以也使用alpha通道吗?

    的确可以。这意味着一张RGBA的splat贴图可以支持5种不同的地形类型。但是对于我们这个教程,已经足够了。

    如果你想要使用超过5张贴图,你应该使用多张splat贴图了。尽管这是可行的,但最终会有大量的贴图采样。这个时候最好的技术就是使用纹理数组。

    为了支持RGB的splat贴图,现在你需要继续新增两张贴图到我们的shader中。我把大理石贴图和测试贴图添加进去了。

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

    4张贴图

    添加需要的变量。再一次,没有_ST变量。

    sampler2D_Texture1, _Texture2, _Texture3, _Texture4;

    1

    在片元程序中,添加额外的采样。第2张贴图现在使用G通道,第3张使用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);

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

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

    在伽玛空间渲染的情况下,这些样本被混合在了伽马空间中,就是这样。但是当在线性空间中渲染时,它们首先被转换成线性空间,然后混合,然后再转换回伽玛空间。结果略有不同。在线性空间中,混合也是线性的。但在伽玛空间中,这种混合倾向于深颜色。

    4张贴图混合

    现在,你已经知道如何去应用细节贴图和如何使用splat映射混合多张贴图。也可以将这些方法组合起来。

    你可以在splat着色器中添加四个细节纹理,并使用映射来混合它们。当然,这需要四个额外的sampler,所以它们并不是没有消耗的。

    你还可以使用映射来控制应用的细节纹理,以及省略的位置。在这种情况下,你需要一个单色贴图,它作为一个蒙版。当一个纹理包含代表不同材质的区域时,这是很有用的,但它的大小不像地形那么大。例如,如果我们的大理石纹理也包含了金属片,你不希望大理石的细节被应用到那里。

    相关文章

      网友评论

        本文标题:Unity 渲染教程(三):使用多张纹理贴图

        本文链接:https://www.haomeiwen.com/subject/qoqwuxtx.html