2012年12月20日木曜日

文字表示のしくみ

ゲーム日本語化 Advent Calendar 2012


ゲームプログラムがテキストから文字を取り出すとき、1byte系の言語なら1byteが1文字と簡単である。
しかしCJK対応するには、1byteの文字なのか、後続のデータを含めて2-3byteで1文字なのかを判定して取り出す必要がある。

また、フォント処理でも、1byte系言語なら256の表を用意して
'A' => 文字コード41h => 表の41h番目のフォント
という簡単な処理だが、CJK対応には、たくさんの文字から適切にフォントを探すようなプログラムが必要になる。


1byte対応ゲームである
DOOM 3 のソースコードが公開されているので、具体例を見てみる。

DOOM3

フォントデータや、テキストから1文字を切り出す部分が1byte(文字コード0-255)を前提としたつくりになっている。

フォント情報の内部形式

neo/renderer/RenderSystem.h

// font support 
const int GLYPH_START   = 0;
const int GLYPH_END    = 255;
const int GLYPH_CHARSTART  = 32;
const int GLYPH_CHAREND   = 127;
const int GLYPHS_PER_FONT  = GLYPH_END - GLYPH_START + 1;

typedef struct {
 int     height;   // number of scan lines
 int     top;   // top of glyph in buffer
 int     bottom;   // bottom of glyph in buffer
 int     pitch;   // width for copying
 int     xSkip;   // x adjustment
 int     imageWidth;  // width of actual image
 int     imageHeight; // height of actual image
 float    s;    // x offset in image where glyph starts  // テクスチャ座標
 float    t;    // y offset in image where glyph starts
 float    s2;
 float    t2;
 const idMaterial * glyph;   // shader with the glyph
 char    shaderName[32];
} glyphInfo_t;

typedef struct {
 glyphInfo_t   glyphs [GLYPHS_PER_FONT];  // 256文字固定
 float    glyphScale;
 char    name[64];
} fontInfo_t;

フォントのロード

フォントデータには個数や文字コード情報がなく、コード順に256文字並んでいることを前提にしている

bool idRenderSystemLocal::RegisterFont( const char *fontName, fontInfoEx_t &font ) {
...
 fileSystem->ReadFile( name, &faceData, &ftime );
  fdOffset = 0;
  fdFile = reinterpret_cast<unsigned char*>(faceData);
  for( i = 0; i < GLYPHS_PER_FONT; i++ ) {  // 256文字固定
   outFont->glyphs[i].height  = readInt();
   outFont->glyphs[i].top   = readInt();
   outFont->glyphs[i].bottom  = readInt();
   outFont->glyphs[i].pitch  = readInt();
   outFont->glyphs[i].xSkip  = readInt();
   outFont->glyphs[i].imageWidth = readInt();
   outFont->glyphs[i].imageHeight = readInt();
   outFont->glyphs[i].s   = readFloat();
   outFont->glyphs[i].t   = readFloat();
   outFont->glyphs[i].s2   = readFloat();
   outFont->glyphs[i].t2   = readFloat();
   int junk /* font.glyphs[i].glyph */  = readInt();
   //FIXME: the +6, -6 skips the embedded fonts/ 
   memcpy( outFont->glyphs[i].shaderName, &fdFile[fdOffset + 6], 32 - 6 );
   fdOffset += 32;
  }
...
}

テキストの表示

int idDeviceContext::DrawText(float x, float y, float scale, idVec4 color, const char *text, float adjust, int limit, int style, int cursor) {
  ...

  while (s && *s && count < len) {
   if ( *s < GLYPH_START || *s > GLYPH_END ) {
    s++;
    continue;
   }
   glyph = &useFont->glyphs[*s]; // 1byte取り出し
       // 文字コード -> 文字形(glyph) は単純な表参照

   ...

    float yadj = useScale * glyph->top;
    PaintChar(x,y - yadj,glyph->imageWidth,glyph->imageHeight,useScale,glyph->s,glyph->t,glyph->s2,glyph->t2,glyph->glyph);

    if (cursor == count) {
     DrawEditCursor(x, y, scale);
    }
    x += (glyph->xSkip * useScale) + adjust;
    s++;
    count++;    // 1byte進める
   }
  }
...
}

日本語化MOD

このように1byteコードのみにしか対応していないゲームは、基本的にかな化はできるが漢字日本語化はできない。
しかしソースコード(プログラムの元)が公開されている場合、プログラムを変更することによって日本語表示が可能になる。

フォント情報の内部形式


ここでは元のフォント情報内部形式はそのままに、

glyphs[0-254] は元のまま、
glyphs[255] に 独自のmy_glyphs_tをぶち込むという
少々トリッキーなことをしている。


neo/renderer/myfont.h

typedef struct {
 int     height;   // 文字コードとして代用
 int     top;   // top of glyph in buffer
 int     bottom;   // bottom of glyph in buffer
 int     pitch;   // width for copying
 int     xSkip;   // x adjustment
 int     imageWidth;  // width of actual image
 int     imageHeight; // height of actual image
 float    s;    // x offset in image where glyph starts
 float    t;    // y offset in image where glyph starts
 float    s2;
 float    t2;
 const idMaterial * glyph;   // shader with the glyph
 int     id;
 //char    shaderName[32];
} my_glyphInfo_t;

typedef struct {
 int num;  // グリフ数を管理
 my_glyphInfo_t*  glyphs;
} my_glyphs_t;

フォントのロード

neo/renderer/tr_font.cpp

  fileSystem->ReadFile( name, &faceData, &ftime );
  fdOffset = 0;
  fdFile = reinterpret_cast<unsigned char*>(faceData);
  int mw = 0;
  int mh = 0;
  if (memcmp(fdFile,"BMF\x03",4)==0) {   // Bitmap Font Generator形式に対応
   ...
   
      my_glyphInfo_t* myglyphs = (my_glyphInfo_t*)Mem_ClearedAlloc(sizeof(my_glyphInfo_t)*num);
      int n=0;
      float inv_w = 1.0f/scaleW,inv_h = 1.0f/scaleH;
      for(;p < next;p+=20) {
       int id = get32(p);
       int x = get16(p+4);
       int y = get16(p+6);
       int w = get16(p+8);
       int h = get16(p+10);
       int xoffset = get16(p+12);
       int yoffset = get16(p+14);
       int xadvance = get16(p+16);
       int page = p[18];
       int xSkip = xadvance;
       my_glyphInfo_t* glyph = (id < GLYPH_END)?(my_glyphInfo_t*)&outFont->glyphs[id]:&myglyphs[n++]; // 0-254はそのまま、それ以上はmyglyphsに入れる

       glyph->id = id;
       glyph->height = h+yoffset;
       glyph->top = base-yoffset;
       glyph->imageWidth = w;
       glyph->imageHeight = h;
       glyph->xSkip = xSkip;
       glyph->s = x*inv_w;
       glyph->t = y*inv_h;
       glyph->s2 = (x+w)*inv_w;
       glyph->t2 = (y+h)*inv_h;
       glyph->glyph = materials[page];
       // shaderName not used
       if (mw<xSkip) mw=xSkip;
      }

      my_glyphs_t* t = (my_glyphs_t*)&outFont->glyphs[GLYPH_END];
      t->glyphs = myglyphs; // &outFont->glyphs[255] にmyglyphsをぶち込む
      t->num = n;

テキストの表示

neo/ui/DeviceContext.cpp

 while (s && *s && count < len) {
   if ( *s < GLYPH_START ) {
    s++;
    continue;
   }
  ....
    int ch = *s;
    int l=1;
    if (utf8) {
     if      (ch<0x80) {
     } else if (ch<0xc0) {
     } else if (ch<0xe0) {
      if (!is_leading(s[1])) break;
      ch = ((ch&0x1f)<< 6) | (s[1] & 0x3f); l=2;
     } else {
      if (!is_leading(s[1]) || !is_leading(s[2])) break;
      ch = ((ch&0x0f)<<12) | ((s[1] & 0x3f)<<6) | (s[2] & 0x3f); l=3;
     } // 1文字取り出してbyte数をlに代入
    }
    glyph = get_glyph(useFont, ch); // 対応する文字画像を取り出す
    float yadj = useScale * glyph->top;
    PaintChar(x,y - yadj,glyph->imageWidth,glyph->imageHeight,useScale,glyph->s,glyph->t,glyph->s2,glyph->t2,glyph->glyph); // 文字画像表示部分は元のまま

    if (cursor == count) {
     DrawEditCursor(x, y, scale);
    }
    x += (glyph->xSkip * useScale) + adjust;
    s+=l;
    count+=l; // 1文字分進める

DOOM3 BFG

最近DOOM3 BFG Editionのソースコードが公開されてたので、どう変更されたか見てみる。
(ところでid softwareは最初にDOOMのソースコードを公開してから毎回クリスマスに公開してたのだが、Bethesda Softworksに買収されてから時期が関係なくなった)

フォント情報の内部形式

 struct fontInfo_t {
  struct oldInfo_t {
   float maxWidth;
   float maxHeight;
  } oldInfo[3];

  short  ascender;
  short  descender;

  short  numGlyphs; // 個数を管理している
  glyphInfo_t * glyphData;

  // This is a sorted array of all characters in the font
  // This maps directly to glyphData, so if charIndex[0] is 42 then glyphData[0] is character 42
  uint32 * charIndex;

  // As an optimization, provide a direct mapping for the ascii character set
  char  ascii[128];

  const idMaterial * material;
 };

フォントのロード


bool idFont::LoadFont() {
....
 fd->ReadBig( fontInfo->numGlyphs ); // フォントデータから個数を読んでいる

 fontInfo->glyphData = (glyphInfo_t *)Mem_Alloc( sizeof( glyphInfo_t ) * fontInfo->numGlyphs, TAG_FONT );
 fontInfo->charIndex = (uint32 *)Mem_Alloc( sizeof( uint32 ) * fontInfo->numGlyphs, TAG_FONT );

 fd->Read( fontInfo->glyphData, fontInfo->numGlyphs * sizeof( glyphInfo_t ) );

 for( int i = 0; i < fontInfo->numGlyphs; i++ ) {
  idSwap::Little( fontInfo->glyphData[i].width );
  idSwap::Little( fontInfo->glyphData[i].height );
  idSwap::Little( fontInfo->glyphData[i].top );
  idSwap::Little( fontInfo->glyphData[i].left );
  idSwap::Little( fontInfo->glyphData[i].xSkip );
  idSwap::Little( fontInfo->glyphData[i].s );
  idSwap::Little( fontInfo->glyphData[i].t );
 }
...
}

テキストの表示


int idDeviceContext::DrawText(float x, float y, float scale, idVec4 color, const char *text, float adjust, int limit, int style, int cursor) {
 ...
  while ( charIndex < len ) {
   uint32 textChar = drawText.UTF8Char( charIndex ); // マルチバイトを考慮してcharIndexから1文字取りだし、charIndexを1文字分(1-3byte)進める。 
 ...
    scaledGlyphInfo_t glyphInfo;
    activeFont->GetScaledGlyph( scale, textChar, glyphInfo ); // textCharのUnicodeの文字情報をscale倍してglyphInfoに読み込む。
    prevGlyphSkip = glyphInfo.xSkip;

    PaintChar( x, y, glyphInfo ); // 1文字表示

    if (cursor == charIndex-1) {
     DrawEditCursor(x, y, scale);
    }
    x += glyphInfo.xSkip + adjust;

drawText.UTF8Char( charIndex )
neo/idlib/Str.hneo/idlib/Str.cpp

activeFont->GetScaledGlyph( scale, textChar, glyphInfo )
neo/renderer/Font.cpp


というわけで変更点はだいたい似通っている。

ゲームのソースコードは商売のタネなので基本的には公開されることはないのだが、
デベロッパによっては古いゲームのソースコードをファンサービスで公開することがある。
(それ以上非公開にしていても利益を生むことは無いという判断?)

元々マルチバイト対応してないゲームでも、ソースコードが公開されていれば(そしてライセンス上の問題が無ければ - 普通は問題無い形で公開されている)、表示できるように改造することで日本語化可能になる。

FreeSpace 2/日本語化プロジェクト
Aquaria 日本語化

元々1byteにしか対応していないものを日本語化するにはこのようなプログラムの変更が必要となるので、海外ゲームの日本語版が割高になるのは翻訳も含めてやむを得ない面もある。

しかし元々日本語化可能なゲームであるにもかかわらず、英語版のまま割高で国内販売してるのはフサケンナって思う。

0 件のコメント:

コメントを投稿