スムージング角度(2)
後でやろう後でやろうと思いつつ、早4ヶ月。「このままでは見た目が悪すぎる!」ということで、スムージング角度を反映させた法線を作るようにしました。
特に下のブロックのように角ばった図だと明らかに見た目が変わります。青い線が面法線で、赤い線が頂点法線を表しているのですが、スムージング角度を反映させる前は頂点法線が明らかにおかしな方向を向いているのがわかります。平らな面のはずなのに陰影が…
また、下の図は前回述べていた矢印のエッジです。スムージング角度適用前は直角な角も滑らかになっていますが、適用後は鋭利な角になっています。ちなみに、スムージング角度はMetasequoiaのデフォルト値である59.5度に設定しました。
スムージング角度の適用方法は意外と簡単です。
<ある面Aの頂点Pの頂点法線Nを求めるとする>
- 頂点法線Nには面Aの面法線をセットしておく。
- 次に、頂点Pを共有する面B1,B2,B3,...Bnを見つける。
- 面Aの面法線と面B1の面法線の成す角度がスムージング角度以下なら、頂点法線Nに面B1の面法線を加算する。
- 同様に、面Aの面法線と面B2の面法線について、面B3の面法線について……と繰り返す。
- Bnまで終わったら、頂点法線Nを正規化して修了。
これを全ての頂点に適用するだけです。
ただしここで注意すべきなのは、頂点法線は「面の数×3」個必要だということです。トライアングルストリップやインデックスバッファを使って頂点を共有している場合はデータ構造の変更を考えなくてはいけません。
プログラムコードは次のようになりました。かなり依存したコードなので、後で書き直す予定です。
//================================================================= // メインチャンク(EDIT3DS, VER3DS, KEYF3DSの階層)のチャンクを読む //================================================================= bool CLoader3ds::ReadMainChunk( SLoaderTmpMesh *pMesh ) { S3dsChunk parent_chunk; // MAIN3DSチャンクヘッダの読み取り if ( !ReadChunk( &parent_chunk ) ) return false; if ( parent_chunk.id != MAIN3DS ) return false; // メインチャンク層(Level1)のチャンクを読む while ( IsValidPosition( &parent_chunk ) ) { S3dsChunk chunk; // チャンク読み取り if ( !ReadChunk( &chunk ) ) return false; // チャンクの判別 switch ( chunk.id ) { // バージョンの読み取り=================================== case MY3DS_VER: if ( !ReadLong( &m_version ) ) return false; break; // エディット部の読み取り=================================== case MY3DS_EDIT: if ( !ReadEditpart( &chunk, pMesh ) ) return false; break; // キーフレームの読み取り=================================== case MY3DS_KEYFRAME: if ( !ReadKeyframe( &chunk ) ) return false; break; // それ以外のチャンクは読みとばす=========================== default: if ( !ReadSkipChunk( &chunk ) ) return false; break; } } return true; } void CLoader3ds::CalcNormals( STempObjectData *pData ) { MYERR_ASSERT( pData != NULL ); // 各面の法線ベクトルを計算 Vector3d *faceNormals = new Vector3d[pData->num_faces]; int i; for ( i=0; i<pData->num_faces; ++i ) { Vector3d vec1, vec2; vec1 = pData->verts[ pData->faces[i].poly[1] ] - pData->verts[ pData->faces[i].poly[0] ]; vec2 = pData->verts[ pData->faces[i].poly[2] ] - pData->verts[ pData->faces[i].poly[1] ]; faceNormals[i].cross( vec1, vec2 ); faceNormals[i].normalize(); } float smoothTh = cos( nag32::DegToRad(m_smoothAgl) ); // 頂点法線を計算 // ただしここでの頂点法線とは、i番目の面のj個目の頂点法線のこと. // つまり、面による頂点法線の共有はない if ( pData->normals != NULL ) delete [] pData->normals; pData->normals = new Vector3d[ pData->num_faces*3 ]; for ( i=0; i < pData->num_faces*3; ++i ) pData->normals[i].set( 0.0f, 0.0f, 0.0f ); for ( i=0; i < pData->num_faces; ++i ) { for ( int j=0; j<3; ++j ) { int idx = pData->faces[i].poly[j]; pData->normals[i*3+j] += faceNormals[i]; // 面iのj個目の頂点を共有する面kを検索 for ( int k=i+1; k < pData->num_faces; ++k ) { for ( int l=0; l<3; ++l ) { if ( pData->faces[k].poly[l] == idx ) { // 面iと面kの面法線の成す角度がm_smoothAgl以下ならばスムージング処理を行う if ( Dot( faceNormals[i], faceNormals[k] ) >= smoothTh ) { pData->normals[i*3+j] += faceNormals[k]; pData->normals[k*3+l] += faceNormals[i]; } } } } // 法線ベクトルを正規化 pData->normals[i*3+j].normalize(); } } delete [] faceNormals; }