今回は、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 世代のシェーダとしては、最低限の演出であろうピクセルノイズですが、
手始めとしては、こんなシェーダがいいのではないでしょうか。