~ Wood ~






■はじめに

今回は、Perlinノイズをシェーダでやってみようと思いました。 適当なサンプルとして、Cg coding contest のProcedural Woodが、あったので見てみたところ、Perlinノイズではありませんでした。まぁ、適当なノイズなので、簡単に解析してみました。
結果的には、ボリュームテクスチャ生成のサンプルになっています。

まぁ、いつものように適当にファイルが入っています。
今回は、天球を半球にしてみました。

vp.cg頂点プログラム。
fp.cgフラグメントプログラム。
noise.h3次元ノイズテクスチャの作成。
noise.cpp3次元、ノイズテクスチャの作成。
main.cppメインループ。
matrix.h行列計算。
matrix.cpp行列計算。

あと、実行ファイル及び、プロジェクトファイルが入っています。

■ボリュームテクスチャの作り方

ボリュ-ムテクスチャーを作るには、glGenTextures でテクスチャを作った後に、 glTexImage3DEXT で、テクスチャの実態を作ります。
テクスチャのサイズやフォーマットは、glGenTextures で指定します。

noise.cpp
0090:     GLuint texid;
0091:     glGenTextures(1, &texid);
0092:     glBindTexture(GL_TEXTURE_3D, texid);
0093: 
0094:     glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
0095:     glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
0096:     glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_S, GL_REPEAT);
0097:     glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_T, GL_REPEAT);
0098:     glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_R, GL_REPEAT);
0099:     glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
0100: 
0101:     PFNGLTEXIMAGE3DEXTPROC glTexImage3DEXT;
0102:     glTexImage3DEXT = (PFNGLTEXIMAGE3DEXTPROC) wglGetProcAddress("glTexImage3DEXT");
0103:     if (glTexImage3DEXT != NULL)
0104:         glTexImage3DEXT(GL_TEXTURE_3D, 0, GL_LUMINANCE, w, h, d
0105:                         , 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, img);

テクスチャに受け渡すテクスチャの中身は、場所に依存した乱数を放り込みました。
new float[] で、m_pBuf にテクスチャサイズ分のメモリを確保しました。
そのメモリに、一様な乱数を埋め込んだ後、ボリュームテクスチャに受け渡すメモリを生成します。

noise.cpp
0050: // ---------------------------------------------------------------------------
0051: // OpenGL 3D テクスチャを作る
0052: // ---------------------------------------------------------------------------
0053: GLuint CNoise::CreateNoiseTexture3D(int w, int h, int d, float scale, float amp)
0054: {
0055:     int i, j, k;
0056: 
0057:     m_size[0] = w;
0058:     m_size[1] = h;
0059:     m_size[2] = d;
0060:     
0061:     // ノイズの種になる乱数の設定
0062:     SAFE_DELETE_ARRAY(m_pBuf);// すでに作られていたら、開放
0063:     m_pBuf = new float[w * h * d];
0064: 
0065:     // -1~1の範囲で3D空間全体に乱数を作る
0066:     float *ptr = m_pBuf;
0067:     for( i=0; i<w*h*d ; i++ ) {
0068:         *ptr++ = (2.0 * rand() / (float) RAND_MAX) - 1.0;
0069:     }
0070: 
0071:     GLubyte *img = new GLubyte[w * h * d];
0072: 
0073:     GLubyte *p = img;
0074:     for( i=0; i<d; i++ ) {
0075:         for( j=0; j<h; j++ ) {
0076:             for( k=0; k<w; k++ ) {
0077: 
0078:                 float fx = (k / scale) - 0.5;// 0.5 は、切捨てで問題が出ないような調整
0079:                 float fy = (j / scale) - 0.5;
0080:                 float fz = (i / scale) - 0.5;
0081: 
0082:                 float n;
0083:                 n = (noise3D((int)fx, (int)fy, (int)fz) + 1.0) * 0.5;
0084:                 n = clamp(n, 0.0, 1.0);
0085:                 *p++ = (GLubyte) (n * amp * 0xff);
0086:             }
0087:         }
0088:     }

生成するノイズは、一様な乱数を位置に応じて設定します。
今回は、scale(ノイズの間隔)をパラメータとする乱数にしました。

noise.cpp
0041: // ---------------------------------------------------------------------------
0042: float CNoise::noise3D(int x, int y, int z)
0043: {
0044:     x = x & (m_size[0] - 1);
0045:     y = y & (m_size[1] - 1);
0046:     z = z & (m_size[2] - 1);
0047: 
0048:     return m_pBuf[(z*m_size[0]*m_size[1]) + (y*m_size[0]) + x];
0049: }

生成したノイズ(の断面)は、次のようになります。

■頂点プログラム

プログラマブルシェーダを見ましょう。
頂点プログラムは、入力は位置と法線で、出力に関しては、テクスチャ座標に、ライト方向や、ハーフベクトルなどを仕込みます。

vp.cg
0001: // ---------------------------------------------------------------------------
0002: // 頂点シェーダ入力データ
0003: // ---------------------------------------------------------------------------
0004: struct appdata
0005: {
0006:     float4 position : POSITION;
0007:     float3 normal   : NORMAL;
0008: };
0009: // ---------------------------------------------------------------------------
0010: // 頂点シェーダ出力データ
0011: // ---------------------------------------------------------------------------
0012: struct vert2frag
0013: {
0014:     float4 hpos     : HPOS; // 頂点座標
0015:     float4 tcoords  : TEX0; // テクスチャ座標
0016:     float4 normal   : TEX1; // 法線ベクトル
0017:     float3 vlight   : TEX2; // ビュー座標のライト
0018:     float3 vhalf    : TEX3; // ビュー座標のハーフベクトル
0019: };

シェーダプログラムでは、それぞれのベクトル値を座標変換等を行って、変換します。

vp.cg
0020: // ---------------------------------------------------------------------------
0021: // Perlin noise 頂点シェーダプログラム
0022: // ---------------------------------------------------------------------------
0023: vert2frag main(appdata IN
0024:             , uniform float4    LightPosition
0025:             , uniform float4x4  ModelViewProj
0026:             , uniform float4x4  ModelView
0027:             , uniform float4x4  ShaderMatrix
0028:             , uniform float4x4  ModelViewIT
0029:             )
0030: {
0031:     vert2frag OUT;
0032: 
0033:     float4 pos = IN.position.xyzw;
0034:     float4 normal = IN.normal.xyzz;
0035:     normal.w = 1.0;
0036: 
0037:     // 座標変換
0038:     OUT.hpos = mul(ModelViewProj, pos);
0039: 
0040:     // オブジェクト空間をテクスチャ座標にする
0041:     OUT.tcoords = mul(ShaderMatrix, pos);
0042: 
0043:     // 法線ベクトル
0044:     OUT.normal = mul(ModelViewIT, normal);
0045: 
0046:     // ライトベクトル
0047:     float4 pos_view = mul(ModelView, pos);  // ビュー空間での位置
0048:     float3 L = normalize(LightPosition.xyz - pos_view.xyz);
0049:     OUT.vlight = L;
0050: 
0051:     // ハーフベクトル
0052:     float3 eye = float3(0.0, 0.0, 10.0) - pos_view.xyz;
0053:     OUT.vhalf = normalize(eye + L);
0054: 
0055:     return OUT;
0056: }

■フラグメントプログラム

フラグメントプログラムでは、乱数のテクスチャを読み込んで模様を作ります。
法線やハーフベクトルを正規化して拡散光や鏡面反射光を求めます。
テクスチャ座標を求めるときに、2つのテクスチャを使います。 1つは先ほど作成した乱数の3次元テクスチャNoiseMapで、もう1つは1次元の木のグラデーションを決定するためのテクスチャPulseTrainMapです。
2つのテクスチャから合成するための重みを求めて、暗い色と明るい色を合成します。 また、鏡面反射光も暗い部分は色を抑えます。
さらに、少しだけ赤い色をランダムで混ぜて、いわゆる「CGらしさ」を低減させています。

fp.cg
0040: // ---------------------------------------------------------------------------
0041: // noise フラグメントプログラム
0042: // ---------------------------------------------------------------------------
0043: fragout main(vert2frag I
0044:             , uniform float4   fLightWood
0045:             , uniform float4   fDarkWood
0046:             , uniform float    fHighNoiseLevel
0047:             , uniform float    fBaseRadiusFreq
0048:             , uniform float    fLowPulseAmp
0049:             , uniform texobj3D NoiseMap : texunit0
0050:             , uniform texobj1D PulseTrainMap : texunit1
0051:     )
0052: {
0053:     fragout O;
0054: 
0055:     float3 fvNormal  = normalize(I.normal.xyz);
0056:     float3 fvLight   = normalize(I.vlight.xyz);
0057:     float3 fvHalfAng = normalize(I.vhalf.xyz);
0058: 
0059:     // 平行光源
0060:     float fDiffuseTerm = max(dot(fvNormal, fvLight), 0);    // ランバート
0061:     fDiffuseTerm += 0.22;                                   // 環境光
0062:     fDiffuseTerm = min(fDiffuseTerm, 1);                    // 0~1クランプ
0063: 
0064:     // 鏡面反射
0065:     float fDiffuseSpecTerm = min(fDiffuseTerm * 3, 1);      // スペキュラーの強さ
0066:     float fSpecularTerm = fDiffuseSpecTerm * pow(max(dot(fvNormal, fvHalfAng), 0), 30);
0067: 
0068: 
0069:     // 変位する大きさを決める
0070:     float fRadius = getRadius(NoiseMap, I.tcoords.xyz) * fBaseRadiusFreq;
0071: 
0072:     // 明るい色と暗い色を合成する合成係数
0073:     float t = 0.5 * (         f1tex1D(PulseTrainMap, fRadius)           // 普通に見える縞
0074:              + fLowPulseAmp * f1tex1D(PulseTrainMap, 0.131 * fRadius)); // グローバルな明暗
0075: 
0076:     // 最後の計算
0077:     O.col.xyz = (lerp(fLightWood, fDarkWood, t) * fDiffuseTerm).xyz     // 拡散光
0078:                 + ((1.3 - t) * fSpecularTerm).xxx                       // 鏡面反射
0079:                 + fHighNoiseLevel * myNoise(NoiseMap, I.tcoords.xyz);   // 高周波ノイズ
0086: 
0087:     return O;
0088: }

縞の間隔を決定するための関数getRadiusは、次の形をしています。
大きさを変えて、複数の波長を持つノイズを合成しています。 ここを適当なノイズの合成方法をもちいれは、Perlinノイズもできるでしょう。

fp.cg
0012: //-----------------------------------------------------------------------------
0013: // -1から1の範囲のノイズをサンプリングする
0014: //-----------------------------------------------------------------------------
0015: float myNoise(texobj3D NoiseMap, float3 TexCoords)
0016: {
0017:     return 2.0 * f1tex3D(NoiseMap, TexCoords) - 1.0;
0018: }
0019: //-----------------------------------------------------------------------------
0020: // 木のパターンの半径を計算する
0021: //-----------------------------------------------------------------------------
0022: float getRadius(texobj3D NoiseMap, float3 fPos)
0023: {
0024:     float3 fTemp = fPos;    // 基本はテクスチャ座標
0025:     
0026:     // XY平面にノイズを追加
0027:     fTemp.x += myNoise(NoiseMap, 0.002 * fPos.xyz);
0028:     fTemp.y += myNoise(NoiseMap, 0.002 * fPos.xyz);
0029: 
0030:     float r = sqrt(dot(fTemp.xy, fTemp.xy));
0031: 
0032:     // 適当なノイズ
0033:     r += 0.1 * myNoise(NoiseMap,         fPos.xyz);
0034:     // Zで決まるノイズ
0035:     r += 0.5 * myNoise(NoiseMap, 0.001 * fPos.zzz);
0036: 
0037:     return  r;
0038: }

■テクスチャの使用

さて、いままで登場しなかったのは、テクスチャの使い方です。
テクスチャを使うためには、生成したときのIDを持つ必要があります。
また、Cgで使うために、イテレータを準備する必要があります。

0033: static int              g_iNoiseTex;
0034: static int              g_iPilseTrainTex;

0052: static cgBindIter       *NoiseMapBind           = NULL;
0053: static cgBindIter       *PulseTrainMapBind      = NULL;

準備したオブジェクトは、イテレータはcgGetBindByNameの返り値、 テクスチャIDはglGenTexturesで確保したときの番号を代入します。

0402:         NoiseMapBind        = cgGetBindByName(FragmentProgramIter, "NoiseMap");
0403:         PulseTrainMapBind   = cgGetBindByName(FragmentProgramIter, "PulseTrainMap");

0451:     g_iNoiseTex = noise.CreateNoiseTexture3D(64,64,64,1.0,1.0);
0452:     g_iPilseTrainTex = generateSmoothPulseTrain();

1次元のテクスチャを作る関数は、次のようになります。
やはり、メモリに1次元のイメージを作成してから、そのイメージを使ってテクスチャを生成します。

0120: // ---------------------------------------------------------------------------
0121: // いい感じの山になっているテクスチャを作る
0122: // ---------------------------------------------------------------------------
0123: GLint generateSmoothPulseTrain()
0124: {
0125:     const int iWidth = 512;
0126:     unsigned char* pCharBuf = new unsigned char[iWidth];
0127: 
0128:     for (int i = 0; i < iWidth; ++i) {
0129:         float fX = float(i) / float(iWidth);
0130:         float fVal = getSmoothStep(0.1, 0.65, fX) - getSmoothStep(0.8, 0.95, fX);
0131:         pCharBuf[i] = 255 * fVal;
0132:     }
0133: 
0134:     GLuint texid;
0135:     glGenTextures(1, &texid);
0136:     glBindTexture(GL_TEXTURE_1D, texid);
0137:     glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
0138:     glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST);
0139:     glTexParameteri(GL_TEXTURE_1D, GL_GENERATE_MIPMAP_SGIS, GL_TRUE);
0140:     glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_WRAP_S, GL_REPEAT);
0141:     glTexImage1D(GL_TEXTURE_1D, 0, GL_LUMINANCE, iWidth, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, pCharBuf);
0142: 
0143:     delete [] pCharBuf;
0144:     return texid;
0145: }

使うときは、cgGLActiveTextureで、次に設定するイテレータを指定してから、 glEnableでテクスチャを有効にしてから、glBindTextureで、使うテクスチャを指定します。
使い終わったら、glDisableでテクスチャを使用しないことを宣言します。

0203:     // -----------------------------------------------------------------------
0204:     // テクスチャの設定
0205:     // -----------------------------------------------------------------------
0206:     cgGLActiveTexture(NoiseMapBind);
0207:     glEnable(GL_TEXTURE_3D_EXT);
0208:     glBindTexture(GL_TEXTURE_3D_EXT, g_iNoiseTex); 
0209: 
0210:     cgGLActiveTexture(PulseTrainMapBind);
0211:     glEnable(GL_TEXTURE_1D);
0212:     glBindTexture(GL_TEXTURE_1D, g_iPilseTrainTex); 

****:     描画

0264:     // -----------------------------------------------------------------------
0265:     // 描画の終了
0266:     // -----------------------------------------------------------------------
0267:     // テクスチャの無効化
0268:     cgGLActiveTexture(NoiseMapBind);        glDisable(GL_TEXTURE_3D_EXT);
0269:     cgGLActiveTexture(PulseTrainMapBind);   glDisable(GL_TEXTURE_1D);

■描画

描画するには、それぞれの定数に適当な値を入れます。 これらの定数を入れ替えれば、見た目が大幅に変わります。

0235:     cgGLBindUniform4f(FragmentProgramIter, fLightWoodColorBind, 0.76, 0.54, 0.32, 0.0);
0236:     cgGLBindUniform4f(FragmentProgramIter, fDarkWoodColorBind,  0.62, 0.38, 0.20, 0.0);
0237:     cgGLBindUniform4f(FragmentProgramIter, fBaseRadiusFreqBind, 1.10, 0.0, 0.0, 0.0);
0238:     cgGLBindUniform4f(FragmentProgramIter, fHighNoiseLevelBind, 0.03, 0.0, 0.0, 0.0);
0239:     cgGLBindUniform4f(FragmentProgramIter, fLowPulseAmpBind,     1.0, 0.0, 0.0, 0.0);   

各行列を設定するためには、cgGLModelViewProjectionMatrix等の、適当なパラメータを入れれば、 それぞれのぎょうれつを設定することができます。

0249:     // local ⇒ proj
0250:     cgGLBindUniformStateMatrix(VertexProgramIter, WPMatrix, 
0251:         cgGLModelViewProjectionMatrix, cgGLMatrixIdentity);
0252:     // local ⇒ view
0253:     cgGLBindUniformStateMatrix(VertexProgramIter, WMatrix, 
0254:         cgGLModelViewMatrix, cgGLMatrixIdentity);
0255:     // view ⇒ local
0256:     cgGLBindUniformStateMatrix(VertexProgramIter, WITMatrix, 
0257:         cgGLModelViewMatrix, cgGLMatrixTranspose | cgGLMatrixInverse);

描画自体は、設定されているティーポットを描画しました。

0259:     // -----------------------------------------------------------------------
0260:     // 描画
0261:     // -----------------------------------------------------------------------
0262:     glutSolidTeapot(1);             // ティーポットの描画

■おまけ

パラメータを適当に変えたら、次のような絵ができました。

急須の質感が出ていて、結構気に入っています。

■最後に

明るい部分と暗い部分で、鏡面反射率を変えているところは結構こっていますね。
GeForce FX 世代のシェーダとしては、最低限の演出であろうピクセルノイズですが、 手始めとしては、こんなシェーダがいいのではないでしょうか。





もどる

imagire@gmail.com