这篇文章是我们有关渲染系列的第三篇。在上篇文章中我们介绍了shaders(着色器)和textures(纹理贴图)。我们演示了如何使用单张纹理贴图对地面进行处理。现在我们来讲下更为高级的多纹理贴图。PS:该教材运行版本为5.4.0b15。 多纹理贴图混合 1纹理细节 纹理很好用,但是它也有局限性。纹理的总像素是固定不变的,不论它已何种尺寸显示。当渲染较小尺寸的时候,我们可以使用mipmaps(分级纹理)的方式让渲染看起来好一点。但是如果渲染像素尺寸大于纹理本身,它将变得模糊。因为我们不能凭空制造细节。那应该如何解决呢?当然,我们可以使用更大的纹理。更多的像素意味着更多的细节。但是纹理的大小总有个限度。与此同时这种方式是以浪费资源为前提而带来很小的效果提升。另外的一种方法就是增加高像素瓦片贴图。你需要多小就可以制作多小,只不过你得到的将是一张重复的纹理贴图。但是这么做效果也比较一般。当你的鼻子贴着墙看的时候,你就只能看到墙上的小方块。所以我们要将瓦片纹理和非瓦片纹理进行混合。这里我们试着使用对比度大的纹理。这是我们使用带小波纹的网格。把它抓取到我们的项目中,使用默认导入设置。我将网格之间的分割线调成黑色,这样我们就能更清晰的区分瓦片。 轻微扭曲的网格纹理 复制MyFirstShaders然后将其重命名为TexturedWithDetail(带细节纹理贴图),现在我们开始使用者个Shader。 使用新的Shader创建一个material(材质),然后把网格材质赋给它。 带网格的细节材质 我们将材质引用到一个方块上看看,从远处看还不错,但是靠近一点你就会发现方块变的模糊不清。缺乏细节和纹理压缩似的它表现的更明显。 靠近格子,低像素纹理和DT1压缩下的效果 1.1多纹理例子 现在我们来说个简单纹理的例子,并把它的结果用到我们的fragmentshader(片段渲染器)。使用这种方式,我们可以很方便的将简单的颜色存储到变量中。 我们使用瓦片纹理来增加像素密度。让我来简单处理下第二个纹理例子,将原纹理按照原来的十倍进行平铺。实际上是替换原始的颜色,而不是进行添加。 这样我们就得到了一个小的多的网格。在它看起来模糊不清前你可以尽可能的靠近看它。当然因为它是不规则的格子,所以显示出了很明显的重复纹样。 硬编码瓦片纹理 我们实际上制作了两个纹理,但是最后只使用了一个,这看起来有些浪费不是吗?看看下面的vertexprograms(顶点着色器),就像我们前一个章节介绍的,我把有关OpenGLCore和Direct3D11有关的代码进行整合。 你有没有发现代码里只有一个纹理例子?是的,编译器为我们移除了不必要的代码!编译器在保证最终结果相同的情况下,将所有未使用的内容都丢弃了。当然,我们并不想替换到原来的例子。我们希望合并两个例子。让我们把它们乘起来。我们需要再一次添加一个扭曲。我们在相同UV坐标的情况下,进行两次采样。 这个shader编译器又做了什么呢? 我们再一次得到了一个纹理样本。编译器检查了重复代码并对其进行了优化。所以纹理只采样了一次。其结果被存储到一个寄存器中被复用。哪怕你使用了中间变量,编译器也能足够聪明的将其发现。它追踪到一切输入数据的源头,尽可能高效的组织它们。 现在我们将x10UV坐标的图放到第二个采样中,我们就最终的得到了大小两个格子的混合。 将两张不同的瓦片纹理进行乘法混合 作为纹理样本它们不再相同,编译器不得不使用同时使用他们两个。 1.2分离细节纹理 当两个纹理想乘的时候,纹理会变得更暗。除非至少一个纹理是白色的。因为每一个像素的色值在0到1之间。当添加一个细节纹理的时候,你可以将它变暗或者变亮。 将纹理变量,我们需要一个比1大的值。我们假设它是2,一个比原来颜色大两倍的值。你可以在乘上细节纹理之前将值乘以2。 双倍细节 这种方法重新调整了我们使用的纹理细节。乘以1不会带来任何变化。但是两倍的细节采样,现在变成了1/2。这意味着填充变成了灰色而不是白色。纹理不会产生变化。所有值小于1/2都会变暗,所有值大于1/2都会变亮。所以我们需要一张特殊的细节纹理,这张纹理中间围绕着灰色。这里就有一张这样的纹理。 格子细节纹理 所有的细节纹理都是灰度图吗? 他们不必都是灰度图,但是同来说是这样的。灰度图能够准确调节原始样本的明暗值。操作起来也很简单。乘以一个非灰色的值会让结果变的不直观。但是没有人会阻止你这么做。这么做的结过会带来些颜色的变化和偏移。使用分离采样,我们必须添加第二个纹理属性值到我们的shader中。使用默认的灰色,这样不会改变主纹理的外观。 将纹理设置到我们的材质中,并把Tilling设置成10. 两个纹理 当然我们还需要一些变量让我们能够访问detailtextures(细节纹理),tilling(平铺值)和offsetdata(偏移数据)。 1.3使用两对UV 为了取代硬编码乘以10的方法,我们需要设置细节纹理的tiling(平铺值)和offsetdata(偏移值)。我们可以像主UV那样在vertexprogram(顶点程序)计算最终的细节UV。这就意味着我们需要额外导入一对UV。 新的细节UV是由原始的细节UV经过tiling和offset变换后产生的。 要注意的是都定义在了顶点程序代码中。和你的预期一样,OpenGLCore使用了vs_TEXCOORD0和vs_TEXCOORD1。与之形成对比的是,Direct3D11使用o1这一单一的输出。对于如何解释这些输出提交代码我通常在代码中省略了。 //Outputsignature:////NameIndexMaskRegisterSysValueFormatUsed//------------------------------------------------------------//SV_POSITION0xyzw0POSfloatxyzw//TEXCOORD0xy1NONEfloatxy//TEXCOORD1zw1NONEfloatzw这就意味着需要把两个UV输出包装到一个寄存器中。第一部分存放在x和y通道中,第二部分存放在z和w通道中。这实际上是可行的,因为寄存器总是可以存放4个数值。在Direct3D中就是这样实现的。 你可以自己手动打包输出吗? 当然可以,你可以输出任何你想要的结果。包装单独的四个逻辑关系分离的数值到一个输出包中。较少输出能够改善你shader的性能,如果插值确实是这里的性能瓶颈所在的话。通常情况下打包输出是因为只有少量的插值可以使用。在ShaderModel2的硬件支持8个通用插值器,ShaderModel3的硬件支持10个。复杂的Shader需要考虑如何运行在这些情况下。现在我们可以在fragmentprogram使用扩展的UV对了。 现在我们的shader具备了完整的功能。主纹理在细节纹理的基础上变得根据亮暗分明。 明暗清晰 1.4淡化细节 添加细节的方法能够改进当我们靠近和放大时候的问题。但是不支持缩小或者在远处查看,因为这样做会使得瓦片很突出。所以当要显示小尺寸的时候我们需要一种方式来淡化细节纹理。我们可以淡化灰色的细节纹理,这样的结果是颜色并不会改变。在此之前我们已经这么做过了!我们需要的就是在细节纹理导入设置的时候将FadeoutMipMaps选项勾上。需要注意的勾选之后会自动将filtermode装换成trilinear模式。这样灰度就会循序渐进变化了。 淡化细节 网格让纹理细节的变换并不是十分的明显,但是我们通常也不会注意到它。例如,这里我们的主要细节纹理只一张大理石的材质。将他们用相同的方式导入我们要用的网格中。 大理石纹理 一旦我们的材质使用了这个纹理,细节纹理的变化就不再明显。 大理石材质 然而我们需要感谢细节纹理,当靠近时大理石材质表现的很好。 靠近时没有细节纹理的情况 1.5线性颜色空间 在伽马颜色空间我们的shader工作的很好,但是当我们切换到线性颜色空间的时候就出现了问题。使用哪个颜色空间是在项目里设置的。我们可以在Edit/ProjectSettings/Player设置playersettings,在OtherSettings面板中对其进行配置。 选择颜色空间 什么是伽马空间? 伽马空间根据伽马值校正颜色。伽马校正是通过修改光线的强度。这是最简单的一种提升原采样值的方式,设置伽马值。伽马值为1表示没有变化。伽马值为2表示为原来值的两倍。这种转回最初引入是为了使用CRT显示器。另一个好处是它刚好对应了我们眼睛所适合的光线强度。我们眼睛分辨暗的颜色比分辨明亮的颜色更为敏感。所以相对于亮色,我们更倾向于让显示器产生暗色。取幂的方式允许我们这样取值,拉伸低值,挤压高值。使用最多的颜色模式为sRGB。他使用的公式比取幂更为复杂,它存储的颜色平均值为1/2.2。在多数情况下这是一个合理的平局值。使用伽马2.2可以把颜色转换为原始值。 使用伽马1/2.2编码,使用伽马2.2解码 假设Unity使用sRGB存储纹理和颜色。当在伽马空间渲染时,shaders直接访问原始颜色和纹理数据。这是没有问题的。当在线性空间渲染时,这就不正确了。GPU会将纹理采样装换到线性空间。同时,Unity将材质颜色变量转换到线性空间。这是shader操作线性空间颜色。在这之后,fragmentprogram将会把输出的值转换回伽马空间。使用线性颜色的优点是光照计算更加真实。因为光线在现实中是线性的,而不是指数的。不幸的是,这回打乱我们的细节纹理。转换成线性空间之后,颜色变得跟暗了,为什么会产生这种效果? 伽马空间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. color*=tex2D(_DetailTex,i.uvDetail)*unity_ColorSpaceDouble; 修改这个值之后,我们的细节材质在任何颜色空间渲染的效果就都是一样的了。2TextureSplatting使用细节纹理的显示是整个表面都使用一个纹理。像大理石这种平整的表面它工作的很好。然而如果你的材质不是平整的,你就无法在所有的地方使用相同的细节纹理。考虑到一个大的地形。里面可能包含了草地,沙土,石头,雪等等。你希望这些类型尽量的接近。但是使用一个纹理覆盖整个地形,像素肯定是不够的。你可以通过使用不同的纹理在不同类型的表面,然后平铺它们。但是你怎么知道什么时候使用什么纹理呢?我们假设有一个地形里有两种不同的表面类型。每一个点我们都必须选择使用何种表面纹理。第一种或者第二种。我们可以用一个布尔值表示他们。如果是ture的时候,我们使用第一种材质,否则使用第二种。我们可以使用一个灰度纹理来存储这些信息。1代表第一种材质,0代表第二种材质。实际上我们可以使用线性插值表示两种材质之间的情况。使用0和1之间的的值代表混合纹理。用它来平滑过渡。像这样的纹理我们称之为splatmap。如你所见,就像地形投影泼洒在一张画布上一样。因为它是插值的,所以这种地图不需要很高的分辨率。这里有一个样张。 二进制splatmap 将其添加入你的工作,修改导入设置。允许BypasssRGBSampling,这样它就能在线性空间中生成使用。这是必须的,因为他的纹理并不是sRGB,当需要选上。所以它不需要在线性空间里进行转换。同时设置WrapMode为clamp,我们并不会平铺这张地图。 导入设置 复制MyFirstShader,并将其重命名为TexturesSplatting。因为地形并不是统一的,所以我们需要去掉一些功能。 创建一个新的材质使用这个shader,设置splatmap为主材质。因为我们还没有改变shader,所以看起来还是原来的样子。 显示splatmap 2.1添加纹理 为了能够选择两个纹理,我们需要在我们的shader中添加两个变量,我们将它们命名为Textures1和Texture2。 你可以使用任何你所希望的纹理,这里我使用了我们已经有的格子和大理石纹理。 添加两个纹理 当然我们可以调整到如shader的每个材质的偏移和平铺值。我们确实可以同时设置纹理的属性,但是这会使得有更多的顶点数据通过fragmentshader,或者计算UV在shader中调整像素。这样很好,但是一般地形的瓦片纹理都一样。但是splatmap并不都是平铺的。所以我们需要一个实例控制偏移和平铺值。你可以在shader中添加变量,就像在C#中一样。属性NoScaleOffset顾名思义,它是吧tiling和offset当做scale和offset。这并不是统一的命名。添加属性到我们的临时纹理中,然后保持当前的偏移和平铺值在主纹理的输入值。 这么做的目的是控制偏移值和平铺值显示在我们shader检视面板的顶部。一会儿我们会使用splatmap,把它应用到其他纹理。让我们把平铺值设为4. 没有额外的偏移和平铺控制 现在我们要添加一个采样器变量到我们的shader代码中。但是我们不需要添加他们对应的_ST变量。 通过检查,我们确实可以这样进行纹理采样,修改fragmentshader把它们添加在一起。 两个纹理叠加 2.2使用splatmap 使用splatmap采样,我们需要略过从vertexprogram到fragmentprogram中UV没有修改的值。 我们可以在其它纹理之前采样splatmap。 我们使用1代表纹理1.由于我们的splatmap是无色的。我们可以使用任意的RGB通道还原它的值。让我们这里使用R通道,并把它乘上纹理。 returntex2D(_Texture1,i.uv)*splat.r+tex2D(_Texture2,i.uv); 调整第一个纹理 第一个纹理已经通过splatmap调整完成。完成插值,我们需要把其他纹理乘上1-R。 returntex2D(_Texture1,i.uv)*splat.r+tex2D(_Texture2,i.uv)*(1-splat.r); 调整所有的其他纹理 2.3RGBSplatMap 我们有了一个功能完整的splat材质,但是它只支持两种纹理。我们能不能支持更多呢?我们只使用了R通道,如果我们继续使用G和B通道呢?(1,0,0)代表第一种材质,(0,1,0)代表第二种材质,(0,0,1)代表第三种材质。为了在这三个之间得到正常的插值,我们只需要保证RGB通道的值总是加上1.但是等等,当我们使用一个通道的时候,我们能支持两种纹理。这是因为第二个纹理的权值是通过1-R得到的。这种方法适用于任何通道。那么可以通过1-R-G-B的方式支持其他纹理。这导致了splatmap有三种颜色,和黑色。只要三个通道的值加起来不超过1,就是有效的map。这里又一个这样的map,抓取它,使用和之前一样的导入设置。 RGBsplatmap 当什么时候回发生R+G+B超过1? 当前三个纹理合并的时候过于强烈。与此同时第四个纹理本应该是加上,然而去用减去代替。如果变化很小,你会很难察觉到结果并不是很好。实际上RGBmap并不是十分的完美,只是你没有察觉。纹理压缩会导致更多的错误,但是同样的,这些也是很难察觉到的。 我们能够使用alpha通道吗? 当然可以,这也意味着单独的一个RGBAsplatmap就可以支持5种不同的地形类型,但是这个系列教程4个已经足够了。如果你想要使用多余5个的地形,你可以使用多个splatmap。这是可行的,到会后你会得到多个的纹理采样。但是我们有更好的解决方案,比如纹理数组。为了支持RGBsplatmap,我们需要添加二位的两个问道变量到我们的shader中。这里我使用了大理石纹理和之前的测试纹理。 四个纹理 添加必要的变量到shader中,这里同样不需要额外的_ST变量。 sampler2D_Texture1,_Texture2,_Texture3,_Texture4; 在fragmentprogram(片段着色器)里我们需要添加额外的纹理采样。第二个采样现在使用的G通道,第三个采样纹理使用B通道。最后一个纹理使用(1-R-G-B)。 returntex2D(_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); 四纹理地形 为什么地形混合后在线性空间里看起来不一样? 我们的splatmap使用bypassesSRGB的方式进行采样,所以混合方式应该是不依赖于颜色空间,对吗?splatmap确实不会受到影响,但是颜色空间在混合的时候就会发生变化。在伽马空间渲染的时候,样本在伽马空间进行混合。但是在线性空间渲染时,它先要转换到线性空间,然后混合,然后再转换回伽马空间。这样结果就会有一些变化。在线性空间里,线性混合是很好的。但是在伽马空间里,混合后会偏向较暗的颜色。现在我们知道了如实使用细节纹理以及如何使用splatmap混合多种纹理。当然我们也可以把这两种方法混合使用。你可以添加四个细节纹理到splatshader中,利用它们对地图进行混合。当然你需要额外的四个纹理,它们并不会凭空出现。你可以使用一张地图去哪些纹理什么时候显示,什么时候不显示。在这个例子中,你需要一张单色的地图作为功能遮罩。这在需要使用单一地形但是包含其他不同纹理的情况下十分有用,但并不适合大地形。例如,我们的大理石纹理中包含金属碎片,你并不希望这要处理大理石纹理。原文链接:北京哪个白癜风医院比较好北京哪个白癜风医院比较好
|