今回は、Perlinノイズをシェーダでやってみようと思いました。
適当なサンプルとして、Cg coding contest のProcedural Woodが、あったので見てみたところ、Perlinノイズではありませんでした。まぁ、適当なノイズなので、簡単に解析してみました。
結果的には、ボリュームテクスチャ生成のサンプルになっています。
まぁ、いつものように適当にファイルが入っています。
今回は、天球を半球にしてみました。
vp.cg | 頂点プログラム。 |
fp.cg | フラグメントプログラム。 |
noise.h | 3次元ノイズテクスチャの作成。 |
noise.cpp | 3次元、ノイズテクスチャの作成。 |
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 世代のシェーダとしては、最低限の演出であろうピクセルノイズですが、
手始めとしては、こんなシェーダがいいのではないでしょうか。