纹理爆炸-使用伪随机序列避免细节纹理的单调重复
发表于2017-01-05
本篇文章要给大家介绍的是纹理爆炸中使用伪随机序列来避免细节纹理的单调重复,主要是因为一些游戏场景需要在细节纹理的把控上非常严格,这样才能让玩家有一个好的体验。
一、前言
本文旨在与大家一起探讨学习新知识,如有疏漏或者谬误,请大家不吝指出。
本文参考了《GPU精粹1:实时图形编程技术、技巧和技艺》的第二十章。
二、概述
本文的最终实现效果如下:
使用到的素材包括:
下面我们来一步步讲解如何实现。
三、原理与实现
当我们使用一个细节纹理来填充面板时,一般的做法是在面板上正正方方地进行排列,如下图所示:
那么我们来新建个shader实现最简单的这个功能:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | float4 frag (v2f i) : SV_Target { float4 col = tex2D(_MainTex, i.uv)*_Color; // float2 scaledUV = i.uv * _Scale; int2 cell = int2(floor(scaledUV.x), floor(scaledUV.y)); float2 offsetUV = scaledUV - cell; float4 image = tex2D(_TileTex, offsetUV )*_TileColor; if (image.a > 0) { col.xyz = image.xyz; } return col; } |
看上述对uv进行的操作,可以知道,我们对uv进行缩放了_Scale,并且将整数部分去掉,这样uv就相当于从[0, 1]重复了_Scale次。这里有个地方需要注意,要把花纹贴图的mipmap关掉,否则会有artifact。
接下来,我们需要对uv进行随机扰动,这样花纹看起来就不会那么整齐。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | float4 frag (v2f i) : SV_Target { float4 col = tex2D(_MainTex, i.uv)*_Color; // float2 scaledUV = i.uv * _Scale; int2 cell = int2(floor(scaledUV.x), floor(scaledUV.y)); float2 offsetUV = scaledUV - cell; float2 randomUV = cell*float2(0.037, 0.119); float4 random = tex2D(_NoiseTex, randomUV); float4 image = tex2D(_TileTex, offsetUV - random.xy )*_TileColor; if (image.a > 0) { col.xyz = image.xyz; } return col; } |
红色部分是我们新加的代码。我们另外又加入了一张噪声贴图用于进行uv的扰动,它看起来应该是这样的:
当然你可以使用自己的方式对uv进行随机的扰动,但是需要保证随机函数对相同的输入有相同的输出,这是随机函数最大的特征。
然后我们可以得到这样的结果:
上述新增的代码中,取花纹贴图时的uv被偏移了random.xy,而这是从新增的噪声图中得到的值,它的取值范围是[0, 1],永远是正值,所以,花纹贴图应该是朝右上角随机偏移了random.xy的距离。
我们可以看到,花纹贴图在超出[0, 1]的uv范围后被裁剪掉了,所以下一步我们要将被裁减掉的花纹重新画上去。
如上图所示,红色部分是原先被裁剪掉的,我们现在需要将红色部分重新填充。基本的原理是这样的,假如我们当前位于右上角的格子中,那么需要求出其左边的格子以及左边格子对应的随机偏移值random.xy,再重新绘制花纹图案,这样就可以补全被裁剪掉的花纹。对于左下格子以及下方的格子也是做同样的处理(由于花纹只向右上偏移,所以只需要补全左下三个格子即可)。具体代码如下:
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 45 46 | float4 frag (v2f i) : SV_Target { float4 col = tex2D(_MainTex, i.uv)*_Color; // float2 scaledUV = i.uv * _Scale; int2 cell = int2(floor(scaledUV.x), floor(scaledUV.y)); float2 offsetUV = scaledUV - cell; float2 randomUV = cell*float2(0.037, 0.119); float4 random = tex2D(_NoiseTex, randomUV); float4 image = tex2D(_TileTex, offsetUV - random.xy )*_TileColor; if (image.a > 0) { col.xyz = image.xyz; } int2 cell_l = cell + int2(-1, 0); randomUV = cell_l*float2(0.037, 0.119); random = tex2D(_NoiseTex, randomUV); image = tex2D(_TileTex, offsetUV - random.xy - int2(-1, 0))*_TileColor; if (image.a > 0) { col.xyz = image.xyz; } int2 cell_d = cell + int2(0, -1); randomUV = cell_d*float2(0.037, 0.119); random = tex2D(_NoiseTex, randomUV); image = tex2D(_TileTex, offsetUV - random.xy - int2(0, -1))*_TileColor; if (image.a > 0) { col.xyz = image.xyz; } int2 cell_ld = cell + int2(-1, -1); randomUV = cell_ld*float2(0.037, 0.119); random = tex2D(_NoiseTex, randomUV); image = tex2D(_TileTex, offsetUV - random.xy - int2(-1, -1))*_TileColor; if (image.a > 0) { col.xyz = image.xyz; } return col; } |
我们可以把补全左下角三个格子花纹的操作合并成一个for循环:
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 | float4 frag (v2f i) : SV_Target { float4 col = tex2D(_MainTex, i.uv)*_Color; // float2 scaledUV = i.uv * _Scale; int2 cell = int2(floor(scaledUV.x), floor(scaledUV.y)); float2 offsetUV = scaledUV - cell; float2 randomUV = float2(0, 0); float4 random = float4(0,0,0,0); float4 image = float4(0,0,0,0); for ( int i=-1; i<=0; i++) { for ( int j=-1; j<=0; j++) { float2 cell_t = cell + float2(i, j); randomUV = cell_t.xy * float2(0.037, 0.119); random = tex2D(_NoiseTex, randomUV); image = tex2D(_TileTex, offsetUV - random.xy - float2(i, j))*_TileColor; if (image.a > 0) { col.xyz = image.xyz; } } } return col; } |
于是我们得到了一个正确偏移的花纹图案:
接下来考虑对花纹进行随机缩放,当然我们只能缩小花纹,而不能放大,因为我们只处理了左下角三个格子,而放大后花纹被裁剪的区域有可能超过这些区域。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | float2 imageUV = float2(0,0); float scale = 1; for ( int i=-1; i<=0; i++) { for ( int j=-1; j<=0; j++) { float2 cell_t = cell + float2(i, j); randomUV = cell_t.xy * float2(0.037, 0.119); random = tex2D(_NoiseTex, randomUV); imageUV = offsetUV - random.xy - float2(i, j); scale = random.y*2+1; imageUV *= scale; image = tex2D(_TileTex, imageUV)*_TileColor; if (image.a > 0) { col.xyz = image.xyz; } } } |
标红的代码是新增的部分。可以看到,我们用到了噪声贴图的y值,y*2+1后,其值的范围为[1,2],即我们对imageUV缩小了[1,0.5]倍。缩小后的截图如下:
接着是旋转,我们需要将每个花纹随机旋转x角度。2D旋转矩阵如下公式所示:
有几个地方需要注意,上面的旋转矩阵是用于u3d的,而u3d中矩阵是左乘操作。另外,上面的矩阵表示逆时针旋转a弧度,对于旋转来说,规定顺时针为负,逆时针为正,所以如果你想顺时针旋转,需要将上面的a替换成(-a)。最后,我们是对uv坐标进行旋转变换,uv坐标是以左下角为原点,而我们希望进行的操作是让花纹围绕中心点进行旋转,所以我们还需要先平移uv到中心点,接着进行旋转操作,再平移回左下角。具体代码如下:
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 | float rotate = 0; float pi2 = 3.141592*2; float2x2 m2 = float2x2(0,0,0,0); for ( int i=-1; i<=0; i++) { for ( int j=-1; j<=0; j++) { float2 cell_t = cell + float2(i, j); randomUV = cell_t.xy * float2(0.037, 0.119); random = tex2D(_NoiseTex, randomUV); imageUV = offsetUV - random.xy - float2(i, j); //scale scale = random.y*2+1; imageUV *= scale; //rotate rotate = random.x * pi2; m2 = float2x2(cos(rotate), sin(rotate), -sin(rotate), cos(rotate)); imageUV = mul(m2, imageUV - float2(0.5, 0.5)); imageUV += float2(0.5, 0.5); image = tex2D(_TileTex, imageUV)*_TileColor; if (image.a > 0) { col.xyz = image.xyz; } } } |
进行随机旋转后的截图如下:
如果觉得一个花纹太单调,也可以多加入几个花纹,重新制作一个花纹贴图,让四个花纹按行排列,这样在代码中对uv的x值稍微处理一下就行了:
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 | float index = 0; for ( int i=-1; i<=0; i++) { for ( int j=-1; j<=0; j++) { float2 cell_t = cell + float2(i, j); randomUV = cell_t.xy * float2(0.037, 0.119); random = tex2D(_NoiseTex, randomUV); imageUV = offsetUV - random.xy - float2(i, j); //scale scale = random.y*2+1; imageUV *= scale; //rotate rotate = random.x * pi2; m2 = float2x2(cos(rotate), sin(rotate), -sin(rotate), cos(rotate)); imageUV = mul(m2, imageUV - float2(0.5, 0.5)); imageUV += float2(0.5, 0.5); // index = floor(random.z * 3.99f); imageUV.x = (saturate(imageUV.x)+index)/4.0; image = tex2D(_TileTex, imageUV)*_TileColor; if (image.a > 0) { col.xyz = image.xyz; } } } |
添加了多个纹理之后的效果如下:
还可以给不同的花纹添加不同的颜色:
现在看起来舒服多了。但是这还没有结束。
如果希望木板上的花纹数量多一点应该怎么做呢?只需要再加一层for循环就行了。即如果需要再额外画一个花纹,那么就需要多一个重复的操作,多n个,就重复n次,而重复的就是上面所有的操作,所不同的是,我们需要不一样的随机值。我们来看看怎么实现:
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 | for ( int i=-1; i<=0; i++) { for ( int j=-1; j<=0; j++) { float2 cell_t = cell + float2(i, j); randomUV = cell_t.xy * float2(0.037, 0.119); for ( int k=0; k<_Density; k++) { random = tex2D(_NoiseTex, randomUV); imageUV = offsetUV - random.xy - float2(i, j); //scale scale = random.y*2+1; imageUV *= scale; //rotate rotate = random.x * pi2; m2 = float2x2(cos(rotate), sin(rotate), -sin(rotate), cos(rotate)); imageUV = mul(m2, imageUV - float2(0.5, 0.5)); imageUV += float2(0.5, 0.5); // index = floor(random.z* 3.99f); imageUV.x = (saturate(imageUV.x)+index)/4.0; image = tex2D(_TileTex, imageUV)*colors[index]; if (image.a > 0) { col.xyz = image.xyz; } // randomUV += float2(0.03, 0.17); } } } |
下面是重复了5次的绘制效果:
有些时候,美术会希望花纹的分布可以控制,比如说右上角不希望出现花纹图案,或者让花纹图案像螺旋一样分布。要实现这样的效果,我们需要一张额外的mask贴图来控制花纹的分布。而美术则可以通过调整mask贴图从而操控花纹的分布。
我们来通过代码说明如何实现以上功能:
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 | for ( int i=-1; i<=0; i++) { for ( int j=-1; j<=0; j++) { float2 cell_t = cell + float2(i, j); randomUV = cell_t.xy * float2(0.037, 0.119); float probability = tex2D(_DistributionTex, cell_t/_Scale); for ( int k=0; k<_Density; k++) { float noiseW = tex2D(_NoiseTex, randomUV+float2(0.17, 0.57)).z; if (noiseW < probability) { random = tex2D(_NoiseTex, randomUV); imageUV = offsetUV - random.xy - float2(i, j); //scale scale = random.y*2+1; imageUV *= scale; //rotate rotate = random.x * pi2; m2 = float2x2(cos(rotate), sin(rotate), -sin(rotate), cos(rotate)); imageUV = mul(m2, imageUV - float2(0.5, 0.5)); imageUV += float2(0.5, 0.5); // index = floor(random.z* 3.99f); imageUV.x = (saturate(imageUV.x)+index)/4.0; image = tex2D(_TileTex, imageUV)*colors[index]; if (image.a > 0) { col.xyz = image.xyz; } } // randomUV += float2(0.03, 0.17); } } } |
相信代码还是比较容易看明白的。我们从_DistributionTex贴图获得了这个格子的分布概率,然后在每次绘制花纹时都通过噪声值来判定是否绘制。如果分布概率低,那么相应的绘制的花纹数量就少,反之则花纹数量就多。
下面是mask贴图和对应的绘制效果:
到目前为止,效果越来越有趣了不是么?而这些效果仅仅只是基于花纹贴图、noise贴图以及mask贴图。
但是我们想要更加动态的效果,比如说,让花纹动起来。花纹的随机偏移、随机缩放、随机旋转是取决于噪声贴图的,如果改变了噪声贴图,那么花纹的偏移、缩放、旋转都随之改变。现在使用的噪声贴图是由白噪声构成的,如果说我们将噪声贴图进行双线性过滤或者三线性过滤,那么相邻的噪声值就是平滑变化的。
有了这个特性,我们就可以实现花纹的动态效果了。只需要将uv的x值按时间进行线性递增,那么偏移、缩放、旋转就会随着时间平滑变化(如果是无过滤则变化是跳变的)。
具体实现如下:
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 | for ( int i=-1; i<=0; i++) { for ( int j=-1; j<=0; j++) { float2 cell_t = cell + float2(i, j); randomUV = cell_t.xy * float2(0.37, 0.19); float probability = tex2D(_DistributionTex, cell_t/_Scale); for ( int k=0; k<_Density; k++) { float2 noiseZW = tex2D(_NoiseTex, randomUV).zw; if (noiseZW.x+0.01f < probability) { random = tex2D(_NoiseTex, randomUV+_Time.xx*0.01f); imageUV = offsetUV - random.zw - float2(i, j); //scale scale = random.y*2+1; imageUV *= scale; //rotate rotate = random.x * pi2; m2 = float2x2(cos(rotate), sin(rotate), -sin(rotate), cos(rotate)); imageUV = mul(m2, imageUV - float2(0.5, 0.5)); imageUV += float2(0.5, 0.5); // index = floor(noiseZW.y* 3.99f); imageUV.x = (saturate(imageUV.x)+index)/4.0; image = tex2D(_TileTex, imageUV)*colors[index]; if (image.a > 0) { col.xyz = lerp(col.xyz, image.xyz, image.w); } } // randomUV += float2(0.17, 0.031); } } } |
这里提一下需要注意的地方,index是用于分割花纹贴图的,它不应该动态变化,不然花纹就会随着时间突变,比如这一帧是第一个花纹,下一帧就突变为第二张花纹。为了实现这一点我们将另外一个白噪声放入alpha通道,专门用于随机选取花纹(xyz通道都已经被使用了)。
最后是我们的实现效果: