プロシージャルテクスチャリング:石畳テクスチャの生成





数十個の入力パラメーターを取り、丸石のテクスチャを生成するジェネレーターを作成しています。



はじめに



私の趣味はコンピューターグラフィックスと自転車の発明です。 .kkriegerをプレイしこの記事とこの投稿を読んだ後、テクスチャジェネレーターを作成したいという強い思いがありました。 玉石をテクスチャーのテーマとして選択し、グーグルを始めました。 正直なところ、-minecraftオプションを使用しても、丸石テクスチャジェネレータをGoogleで検索することは困難でした。 この問題に唾を吐き、彼は自分で考え始めました。



どこから始めるか



私の考えは次のとおりでした:

-石が必要です

-より多くの石が必要です! 素晴らしい質感のために。 また、テクスチャが大きいほど、生成時間が長くなります。 これは悪いです。

-石は異なります(サイズと色)

-凸石(エンボス)

-粗い石のテクスチャ

-異なる種類の石には異なる種類の粗さがあります

-テクスチャには色とレリーフが含まれているため、これらのコンポーネントを分離することをお勧めします。 純粋な色のテクスチャと法線マップがあるとします。

ジェネレーターでは、テクスチャは段階的に構築されます。最初に、1つのテクスチャを作成し、フィルタを適用し、別のテクスチャと混合します。 ジェネレーターで使用したすべてのメソッドを簡単にリストし、簡単に説明しながら、最適化について説明します。

QtCreatorのQt 4.XYライブラリを使用して、C ++でジェネレーターを作成することにしました。 ただし、Qtを使用せずにジェネレータークラス自体を記述しようとしました。



ハニカムテクスチャ


これがアルゴリズムの基礎です。 石のフレームを作成するのは彼女です。 ここでは 、セルラーテクスチャとは何か、それを作成する方法について詳しく説明します。

私のコードでは、generateCelluarTexture関数がこれを担当しています。

ネタバレ
unsigned char *ProceduralTexture::generateCelluarTexture(int size){ if (size<2) size = 2; int cellX = w/size+2; int cellY = h/size+2; int pointsX[cellX][cellY]; int pointsY[cellX][cellY]; srand(seed); for (int i=0; i<cellX; i++) for (int j=0; j<cellY; j++){ pointsX[i][j] = i*size+rand()%((int)(size*0.7))+size*0.15-size; pointsY[i][j] = j*size+rand()%((int)(size*0.7))+size*0.15-size; } int distBuff[n]; int maxDist = INT_MIN; for (int i=0; i<n; i++){ int x = i%w; int y = i/w; int min = INT_MAX; int min2 = INT_MAX; int startX = x/size; int finishX = startX+3; for (int cp=-1, point=0; startX<finishX; startX++){ int startY = y/size; int finishY = startY+3; for (; startY<finishY; startY++, point++){ if (startX<0 || startX>=cellX || startY<0 || startY>=cellY) continue; int d = distance(x, y, pointsX[startX][startY], pointsY[startX][startY]); if (d<min){ cp = point; min2 = min; min = d; } if (d<min2 && cp!=point) min2 = d; } } distBuff[i] = min2-min; if (maxDist<distBuff[i]) maxDist = distBuff[i]; } unsigned char *img = new unsigned char[n]; for (int i=0; i<n; i++) img[i] = (distBuff[i]*255)/maxDist; return img; }
      
      









この関数はセルのサイズを取得し、テクスチャの白黒画像を返します。 最適化について少し説明します。

まず、すべてのポイントを並べ替えないために、画像を所定のサイズの長方形の領域に分割しました。 したがって、現在のピクセルに最も近いポイントを見つけるには、現在のセルと隣接セルから9ポイントを表示する必要があります。 これは上記のリンクに書かれています。

第二に、「正直な」距離を探す必要はありません。 2点間の距離について話しています。 距離の2乗は完全に下がります(ルートの抽出を取り除きます)-結果としての画像はよりコントラストが強くなりますが、最終的なテクスチャに悪影響はありません。



明るさとコントラスト


多くの人がこのフィルターをPhotoshopや他のグラフィックエディターで使用したと思います。 次に、その実装を自分で記述します。

コード
 void ProceduralTexture::brightnessContrast(unsigned char *img, float brightness, float contrast, bool onePass){ // onePass -   if (brightness!=0){ if (brightness>0){ if (brightness>1) brightness = 1; brightness = 1.0f-brightness; for (int i=0; i<n; i++){ int r = 255-img[i]; r *= brightness; img[i] = 255-r; } } if (brightness<0){ if (brightness<-1) brightness = -1; brightness = 1.0f+brightness; for (int i=0; i<n; i++) img[i] *= brightness; } } if (contrast!=1){ if (contrast<0) contrast = 0; int avbr = 0; if (!onePass){ for (int i=0; i<n; i++) avbr += img[i]; avbr /= n; } else avbr = 127; for (int i=0; i<n; i++){ int res = contrast*(img[i]-avbr)+avbr; if (res<0) res = 0; if (res>255) res = 255; img[i] = res; } } }
      
      







この関数は、画像への入力ポインターと、輝度およびコントラスト比を取ります。 計算式はここで説明されています



パーリンノイズ


Perlinノイズ(Habréを含む)について多くの記事が書かれています。アルゴリズムについては説明しません。最適化についてのみ説明し、コード例を示します。

コード
 // : interpolateTable = NULL; powTable = new float[maxOctaves]; for (int i=0; i<maxOctaves; i++) powTable[i] = pow(0.5, i); inline int ProceduralTexture::getRnd(int x, int y){ return (0x6C078965*(seed^((x*2971902361)^(y*3572953751))))&0x7FFFFFFF; } inline int ProceduralTexture::pointOfPerlinNoise(int x, int y, int cellSize){ int dx = x-(x/cellSize)*cellSize; int dy = y-(y/cellSize)*cellSize; int z1 = getRnd(x-dx, y-dy); // z3 --- z4 int z2 = getRnd(x-dx+cellSize, y-dy); // | | int z3 = getRnd(x-dx, y-dy+cellSize); // + z + int z4 = getRnd(x-dx+cellSize, y-dy+cellSize); // z1 --- z2 int z1z3 = z1*(1.0f-interpolateTable[dy])+z3*interpolateTable[dy]; int z2z4 = z2*(1.0f-interpolateTable[dy])+z4*interpolateTable[dy]; return z1z3*(1.0f-interpolateTable[dx])+z2z4*interpolateTable[dx]; } unsigned char *ProceduralTexture::generatePerlinNoise(int octaves){ unsigned char *img = new unsigned char[n]; if (octaves<1) octaves = 1; if (octaves>maxOctaves) octaves = maxOctaves; for (int i=0; i<n; i++) img[i] = 0; float norm = 255.0f/INT_MAX; for (int j=1; j<=octaves; j++){ int f = 1<<(octaves-j); delete[] interpolateTable; interpolateTable = new float[f]; for (int i=0; i<f; i++){ float a = ((float)i/(float)f)*M_PI; interpolateTable[i] = (1.0f-cosf(a))*0.5f; } for (int i=0; i<n; i++) img[i] += pointOfPerlinNoise(i%w, i/w, f)*powTable[j]*norm; } return img; }
      
      









最初の最適化:補間係数の計算を取り除きます(1.0f-cosf(a))* 0.5f)-このために、単にすべてのオプションを事前に計算します。 そして、あまり多くの選択肢はありません-ラウンドの最大数。 したがって、各オクターブを計算する前に、すべての係数を計算し、interpolateTable配列に入れます。

2番目の最適化は最初の最適化と似ています-平方が計算されるすべての場所を事前に計算された値(powTable)に置き換えます。

また、RNGを可能な限り簡素化し、浮動小数点数を可能な限り取り除くようにしました。



ソフトステップ効果


自分でこの効果を思いつきました(自転車を発明したかもしれません-知りません)。 しかし、彼はより良い名前を思い付くことができませんでした-申し訳ありません。 Photoshopのポスタリゼーションの効果に似ていますが、パレットをカットするだけでなく、トランジションもスムーズにします。 その本質をグラフの形で表現しようと思います。

線形勾配の直接折れ線グラフを滑らかな角度を持つ一種のはしごに変換する必要があります。



これを行うには、現在のピクセルの色強度の値を取得し、それを反復回数(ステップ)で除算します。 部門全体と残りの部門を覚えています。 最大範囲の半分を剰余から減算し、平滑化係数を乗算します。 結果の数値は、整数部に戻されます。 ああ、コードで表示する方が簡単です:

 void ProceduralTexture::postEffect(unsigned char *img, int iterations, float smooth){ for (int i=0; i<n; i++){ float s = (float)img[i]/255.0f; float ds = s*(float)iterations-(float)((int)(s*iterations)); ds = smooth*(ds-0.5f)+0.5f; if (ds>1) ds = 1; if (ds<0) ds = 0; s = ((float)((int)(s*(float)iterations))+ds)/(float)iterations; img[i] = s*255; } }
      
      





この関数は、変換が必要な画像への入力ポインター、反復回数、滑らかさの度合いを受け取ります。 この画像フィルターがどのように変化するかを感じるために、例を挙げます。

どこでも反復回数は5です。 左から右への滑らかさの度合い:1、1.5、2.5



ところで、このフィルターは投稿タイトルの2番目の画像で使用されます。



ミキシング


2つの画像を結合するには、ブレンドフィルターが必要です。 たとえば、セルラーテクスチャとノイズパーリン。 ミキシングはさまざまな方法で実装できます。 私は次のようにしました:

混合するとき、valueTestパラメータが指定されます-この数値は、それ以上のピクセル(ピクセル)が処理されないピクセルの強度を特徴づけます。 不透明度パラメータも表示されます-混合時の透明度自体。

コード
 void ProceduralTexture::mix(unsigned char *img1, unsigned char *img2, int valueTest, float opacity){ for (int i=0; i<n; i++) if (img2[i]<=valueTest){ int b = img2[i]; int r = img1[i]; b = ((float)b/valueTest)*255.0f; r = r-(255-b)*opacity; if (r<0) r = 0; img1[i] = r; } }
      
      











各小石には独自の色があります。 しかし、一般的に、これらの色はすべて互いに似ています。 たとえば、小石は緑または茶色と呼ばれますが、それらの中には緑または茶色のさまざまな色合いがあります。 RGBパレットは、「10%の範囲のグレーブラウンラズベリー色のすべての色合いを与える」ことは難しいため、このような目的にはあまり適していません。 そこで、 HSVパレットを選択しました。 それを通して、色を設定するのが便利です:石の明るさ、彩度、色相。 この場合、色相の範囲を指定できます。 そのため、たとえば、黄色の色合いが必要な場合、色相コンポーネントを60プラスまたは10に設定できます。RGBパレットでは、すべてのチャンネルを調整する必要があります。

ただし、HSVモデルを使用する場合は、最終画像にRGBモデルがあるため、色変換が必要です。 同じwikiからアルゴリズムを取得して、コードを記述します。

ネタバレ
 inline ColorRGB ProceduralTexture::hsvToRgb(ColorHSV &hsv){ ColorRGB rgb; if (hsv.s){ hsv.h %= 360; if (hsv.h<0) hsv.h += 360; int i = hsv.h/60; float f = ((float)hsv.h/60.0f)-(float)i; unsigned char c1 = (hsv.v*(100-hsv.s))*0.0255f; unsigned char c2 = (hsv.v*(100-hsv.s*f))*0.0255f; unsigned char c3 = (hsv.v*(100-hsv.s*(1.0ff)))*0.0255f; hsv.v *= 2.55f; switch (i){ case 0: rgb.r = hsv.v; rgb.g = c3; rgb.b = c1; break; case 1: rgb.r = c2; rgb.g = hsv.v; rgb.b = c1; break; case 2: rgb.r = c1; rgb.g = hsv.v; rgb.b = c3; break; case 3: rgb.r = c1; rgb.g = c2; rgb.b = hsv.v; break; case 4: rgb.r = c3; rgb.g = c1; rgb.b = hsv.v; break; case 5: rgb.r = hsv.v; rgb.g = c1; rgb.b = c2; break; } } else rgb.r = rgb.g = rgb.b = hsv.v; return rgb; }
      
      









今、小石を埋める機能を書くことが残っています。 メッシュテクスチャ生成関数をオーバーロードしました。 実際、シェーディング全体は、ランダムポイントの初期セットを生成するときに、それらに色を割り当てるという事実に基づいています。

ネタバレ
 ColorRGB *ProceduralTexture::generateCelluarTexture(int size, ColorHSV color, int hueRange){ if (size<2) size = 2; int cellX = w/size+2; int cellY = h/size+2; int pointsX[cellX][cellY]; int pointsY[cellX][cellY]; ColorRGB cellColor[cellX][cellY]; srand(seed); for (int i=0; i<cellX; i++) for (int j=0; j<cellY; j++){ pointsX[i][j] = i*size+rand()%((int)(size*0.7))+size*0.15-size; pointsY[i][j] = j*size+rand()%((int)(size*0.7))+size*0.15-size; } color.h -= (hueRange/2); for (int i=0; i<cellX; i++) for (int j=0; j<cellY; j++){ ColorHSV c = color; ch += rand()%hueRange; cellColor[i][j] = hsvToRgb(c); } ColorRGB *img = new ColorRGB[n]; for (int i=0; i<n; i++){ int x = i%w; int y = i/w; int px = 0; int py = 0; int min = INT_MAX; int startX = x/size; int finishX = startX+3; for (; startX<finishX; startX++){ int startY = y/size; int finishY = startY+3; for (; startY<finishY; startY++){ if (startX<0 || startX>=cellX || startY<0 || startY>=cellY) continue; int d = distance(x, y, pointsX[startX][startY], pointsY[startX][startY]); if (d<min){ px = startX; py = startY; min = d; } } } img[i] = cellColor[px][py]; } return img; }
      
      









法線マップ


安心するために、テクスチャと法線マップを使用して、投稿タイトルで見た絵を作成するシェーダーを作成しました。 法線マップは、元の画像から生成できます。 これを行うには、隣接するピクセル間の差を計算します-これが通常の偏差角です。 すべてのピクセルを列挙し、最小および最大角度を見つけた後、それらを正規化します(角度)。

ネタバレ
 void ProceduralTexture::generateNormalMap(unsigned char *img){ normalMap = new ColorRG[n]; int maxR = 0; int maxG = 0; for (int i=0; i<n; i++){ int x = i%w; int y = i/w; if (x>0){ int dr = (int)img[y*w+x-1]-(int)img[y*w+x]; if (dr>maxR) maxR = dr; } if (y>0){ int dg = (int)img[(y-1)*w+x]-(int)img[y*w+x]; if (dg>maxG) maxG = dg; } } maxR *= 2; maxG *= 2; for (int i=0; i<n; i++){ int x = i%w; int y = i/w; if (x>0){ int r = 127+(((int)img[y*w+x-1]-(int)img[y*w+x])*255)/maxR; if (r>255) r = 255; if (r<0) r = 0; normalMap[y*w+x].r = r; } else { int r = 127-(((int)img[y*w+x+1]-(int)img[y*w+x])*255)/maxR; if (r>255) r = 255; if (r<0) r = 0; normalMap[y*w+x].r = r; } if (y>0){ int g = 127+(((int)img[(y-1)*w+x]-img[y*w+x])*255)/maxG; if (g>255) g = 255; if (g<0) g = 0; normalMap[y*w+x].g = g; } else { int g = 127-(((int)img[(y+1)*w+x]-img[y*w+x])*255)/maxG; if (g>255) g = 255; if (g<0) g = 0; normalMap[y*w+x].g = g; } } }
      
      









そして、これがシェーダーそのものです。 わかりやすくするために、マウスカーソルの位置にスポットライトを作成しました(ヘッダーの画像では、マウスカーソルは左上隅にあります)

ネタバレ
 // varying vec4 texCoord; void main(){ gl_Position = gl_ModelViewProjectionMatrix*gl_Vertex; texCoord = gl_MultiTexCoord0; } // uniform sampler2D colorMap; uniform sampler2D normalMap; varying vec4 texCoord; uniform vec2 light; uniform vec2 screen; uniform float dist; void main() { vec3 normal = texture2D(normalMap, texCoord.st).rgb; normal = 2.0*normal-1.0; vec3 n = normalize(normal); vec3 l = normalize(vec3((gl_FragCoord.xy-light.xy)/screen, dist)); float a = dot(n, l); gl_FragColor = a*texture2D(colorMap, texCoord.st); }
      
      









すべてを一緒に接着する



上記の機能を次の順序で適用しました。

-メッシュテクスチャの作成

-輝度/コントラストフィルターの適用

-パーリンノイズでテクスチャを作成する

-パーリンノイズへのステップ効果の適用

-明るさ/コントラストフィルターをパーリンノイズに適用する

-セルラーテクスチャとパーリンノイズの混合

-法線マップの作成

-着色

説明されている各ステップには、独自の入力パラメーターがあります。 それらを変更すると、膨大な数のユニークなテクスチャを取得できます。



しかし、シームレステクスチャはどうでしょうか。



正直なところ、シームレスなテクスチャを作成できるようにしたかったのです。 しかし、何らかの理由で怠itでした。 レシピは簡単です。境界で距離を検索するときにメッシュテクスチャを生成する場合、テクスチャの反対側のポイントを使用する必要があります。 そして、パーリンノイズを生成するとき...正直なところ、私も考えませんでした。 おそらく似たようなもの。



最適化はほとんどありません。 それは価値がありましたか?



実際、コードの初期実装は非常に遅く、多くの浮動小数点数、不必要な反復、計算がありました。 できる限り、計算を行い、それらを固定小数点数に変換するだけでした。 その結果、元のバージョンと現在のバージョンの間のパフォーマンスは約10倍になりました。 アルゴリズムを並列化するというアイデアがありましたが、すべてがメモリの操作にかかっているという疑念のため、私はこの考えを捨てました。



ソースコード



生成のメインクラスは、ProceduralTextureと呼ばれます。 多かれ少なかれ、5年生の生徒がどれだけできるかを書いてみました。 それ以外はすべてボディキットであり、インターフェイス部分はデモンストレーションにのみ必要です。 美しく、私は本当にそこに書き込もうとしませんでした。

ソースコード



さらにいくつかの例









All Articles