プログラム講座 上級編6
- 炎を表現する -
 上級編6です。今回は「炎」を表現してみます。
◆炎の表現
 炎を表現する前に、炎を表現したものを見てみます。メガデモ(デモ)のものに関しては、私がPhotoshopで描きましたので、だいたい、こんな感じと思ってくれれば良いでしょう。
| Eye Candy (Photoshop PlugIn) | 
  | 
 | 
| LightWave 3D (3D Soft) | 
  | 
 | 
| Photoshop (2.0以上) | 
  | 
 | 
| Mega Demo 1 (DOS/V) | 
  | 
 | 
| Mega Demo 2 (DOS/V) | 
  | 
 | 
| Future BASIC | 
  | 
| 
 | 
|---|
 一番下のものが今回作成したものですが、上記のものでおおよそアルゴリズム(描画方法)について推測がつくものに関して解説しておきます。
◆Eye Candy (Photoshop PlugIn 3.0x, 4.0x)
 これはフォトショップのプラグインで発売されているものです。幸いにして炎のシード(発火点)や、ゆらぎ具合を変更できるので、結構簡単にアルゴリズムを読むことが出来ます。
 まず、発火点を決めます。サイン波形(またはコサイン等の波形)の振幅(ゆれ具合)に応じて描画位置を設定します。下の方から描画していきますが、最初の色から最後の色に次第に変化するように色を決めます。決めた色を上にいくに従ってぼかしをかけます。
 多分、こんな具合に処理をしていると思われます。発火点は複数設定できますので、演算には若干時間がかかります。
◆LightWave 3D (3D Soft)
 AMIGAでおなじみ・・・と言っても知らない人がいるかもしれません。結構有名な3Dソフトです。
 LightWave 3Dでも炎を表現する事ができます。まず、点ポリゴンを用意し、グロウ(自分自身を光らせる)をかけます。色設定をすればおおそよできあがり(のはず)です。ようするに点1つ1つに色を付けて周辺をぼかしている、といった感じです。
◆Photoshop (2.0以上)
 Photoshopでもプラグインなしで簡単に炎を描くことが出来ます。まず、グレースケールモードで背景を真っ黒にし、発火点を描きます。発火点は単純な太い横棒で構いません。指ツールで強さ40%程度にし、上に向かってこすります。これが炎になりますので、うまく形を描きます。次に「インデックスカラーモード」に変更します。「カラーパレット...」でカラーを選択しますが、このときに「黒体輻射」のパレットを選択すればできあがりです。
 黒体輻射のパレットは以下のようになっています。
 また、別の方法もあります。指ツールを使用せずに以下のように処理します。
1:文字(発火点)を描画
2:90度画像を回転
3:プラグインの「風」(またはKPTの微風など)をかけます
4:ぼかします
5:-90度画像を回転
6:インデックスカラーにし「黒体輻射」パレットに変更する
 これ以外にも炎を表現する方法があり、こちらのページにも解説されています。フィルタで炎の揺らぎ、ぼかし等を指定しグラデーションを描画しレイヤー合成で作成するものです。
◆Mega Demo 1, 2

 メガデモ(デモ)を知らない人もいるかもしれません。端的に言ってしまえばサウンドに載せて(主に3D)グラフィックが動くというものです。デモの中には炎を表現しているものがいくつかあります。
 まず、デモ1の方ですがこちらは幸いにしてアルゴリズムの解説がありました。発火点を描画。上からぼかす。ぼかした後に上に移動。この繰り返しで描画すれば炎が表現できます。パレットは「黒体輻射」と同じ感じにしておきます。
 次にデモ2の方ですが、これはよくわかりませんが、なんとなくフラクタルっぽくなっています。英文で解説があったと思うのですが、見ていませんので・・・
◆Future BASICで炎を表現!
 という事で最後のFuture BASICで炎を表現するものですが、他のを真似るのも何なので(笑)、別の方法で実現してみました。ちなみに結果は遅くて、しょぼい(泣)ので参考にならないかもしれませんが、一応解説を・・・
 まず発火点を描きます(1ピクセル)。上から以下のように処理していきます。
1:現在の点の色を読む
2:左上、真上、右上に対して一定の割合で半透明合成を行う
 これだけです。この割合を変化させる事で高い炎や低い炎を表現できます。また、操作する点の間隔を広くする事によりガスコンロのような複数の発火点を持つ炎も表現することができます。
 とはいっても、非常に低速なのでオフスクリーンは小さくしておき、数倍に拡大して表示しています。フルカラーで処理していますが、色の分散値を変更すれば、より黄色い炎や明るい炎を表現できます。
◆終わりに
 今回のプログラムの雛形は中級編の「画像処理アプリケーションを作成する」ものを流用しました。炎以外の処理部分に関しては中級編を参考にしてください。
◆今回のプログラムリスト
'-----------------------------------------------
' "仮想画面(オフスクリーン)のrowBytesを求めて直接描画する"
'-----------------------------------------------
DIM offScreen&,cport&,rect;8
rowBytes% = 0:                                    ' "オフスクリーンのrowBytes"
GRAM& = 0:                                        ' "オフスクリーンのアドレス"
ImageX = 40:                                      ' "画像の横の長さ"
ImageY = 30:                                      ' "画像の縦の長さ"
offScreen& = 0:                                   ' "0の時は確保されていない!"
R = 0
G = 0
B = 0
DIM Gpal(256)
END GLOBALS
'-----------------------------------------------
' "オフスクリーンのrowBytesを求める"
'-----------------------------------------------
LOCAL FN getRowBytes
  PixMapH& = FN GETGWORLDPIXMAP(offScreen&)
  err% = FN LOCKPIXELS(PixMapH&)
  LONG IF err%
    GRAM& = FN GETPIXBASEADDR(PixMapH&)
    rowBytes% = {[PixMapH&] + _rowBytes} AND &H3FFF
  END IF
END FN
' -----------------------------------------------
'  "オフスクリーンを確保する"
' offScreen& = "オフスクリーンのアドレス"
' -----------------------------------------------
LOCAL FN setOffscreen
  LONG IF offScreen& > 0
    CALL DISPOSEGWORLD(offScreen&)
    WINDOW CLOSE #1
    WINDOW #1,"Image Effecter",(16,45)-(16+ImageX,45+ImageY),_docNoGrow
  END IF
  CALL SETRECT(rect,0,0,ImageX,ImageY):           '"320x240の画面を作成"
  err% = FN NEWGWORLD(offScreen&,32,rect,0,0,0)
  LONG IF err%
    BEEP
    BEEP
    END:                                          ' "多くの場合、メモリ不足"
  END IF
  FN getRowBytes:                                 ' "rowBytesを求める"
END FN
'--------------------------------
' Copy Offscreen -> Window
'--------------------------------
CLEAR LOCAL
LOCAL FN transfer
  DIM rect2;8
  
  LONG IF offScreen& > 0
    CALL SETRECT(rect,0,0,ImageX,ImageY)
    CALL SETRECT(rect2,0,0,ImageX*2,ImageY*4)
    CALL COPYBITS(#offScreen&+2,#cport&+2,rect,rect2,_srcCopy,0)
  END IF
END FN
'-----------------------------------------------
' "オフスクリーンのカラーを読み出す"
'-----------------------------------------------
LOCAL FN myPOINT(x,y)
  adrs& = GRAM& + y*rowBytes% + x*4:              '"32ビットオフスクリーンなので1pixel=4byte。そのため4倍している"
  R = PEEK WORD(adrs&) AND 255:                   ' "32ビットオフスクリーンはaRGB順に並んでいる。"
END FN
'-----------------------------------------------
' "オフスクリーンに点を表示する"
'-----------------------------------------------
LOCAL FN myPSET(x,y)
  adrs& = GRAM& + y*rowBytes% + x*4:              '"32ビットオフスクリーンなので1pixel=4byte。そのため4倍している"
  POKE LONG adrs&,(R<<16)+(G<<8):                 ' "32ビットオフスクリーンはaRGB順に並んでいる。"
END FN
'--------------------------------------------------------
' "画面を消去する"
'--------------------------------------------------------
LOCAL FN myCLS
  FOR y = 0 TO ImageY-1
    FOR x = 0 TO ImageX-1
      adrs& = GRAM& + y*rowBytes% + x*4:          '"32ビットオフスクリーンなので1pixel=4byte。そのため4倍している"
      POKE LONG adrs&,0:                          ' "32ビットオフスクリーンはaRGB順に並んでいる。"
    NEXT
  NEXT
END FN
'===============================================
'   "今回作成した部分(炎!)"
'===============================================
LOCAL FN Fire
  FOR x = 1 TO ImageX-2
    '以下の2行はFN myPSET(x,ImageY-1-RND(1))と同じ
    adrs& = GRAM& + (ImageY-1-RND(1))*rowBytes% + x*4:'"32ビットオフスクリーンなので1pixel=4byte。そのため4倍している"
    POKE LONG adrs&,RND(256)<<16:                 ' "32ビットオフスクリーンはaRGB順に並んでいる。"
  NEXT
  
  FOR y = 1 TO ImageY-1
    adrs& = GRAM& + (y-1)*rowBytes% + 4:          ' "書き込むアドレスを設定しておく"
    FOR x = 1 TO ImageX-2
      FN myPOINT(x,y):R0! = R:                    ' "現在の点の色を取得"
      FN myPOINT(x-1,y-1):R1! = R:                ' "左上の点の色を取得"
      FN myPOINT(x,y-1):R2! = R:                  ' "真上の点の色を取得"
      FN myPOINT(x+1,y-1):R3! = R:                ' "右上の点の色を取得"
      R& = (R0! * 0.58) + (R1! * 0.1) + (R2! * 0.2) + (R3! * 0.1):' "重みを付けて演算"
      '以下の2行はFN myPSET(x,y-1)と同じ
      POKE LONG adrs&,(R&<<16)+Gpal(R&):          ' "32ビットオフスクリーンはaRGB順に並んでいる。"
      adrs& = adrs& + 4
    NEXT
  NEXT
  FN transfer:                                    ' "できあがった画像を転送する"
END FN
'--------------------------------------------------------
' "アップデートなどのイベントを取得する"
'--------------------------------------------------------
LOCAL FN doDialog
  evnt = DIALOG(0)
  id = DIALOG(evnt)
  SELECT evnt
    CASE _wndRefresh
      FN transfer:                                '"アップデートイベントなので画面を再描画する"
  END SELECT
END FN
'--------------------------------------------------------
' "メニューを構築する"
'--------------------------------------------------------
LOCAL FN initMenu
  'File Menu
  MENU 1,0,_enable,"ファイル"
  MENU 1,1,_enable,"/Q終 了"
  
END FN
'---------------------------------------------
' "メニューの選択"
'---------------------------------------------
LOCAL FN doMenus
  menuID = MENU(_menuID)
  itemID = MENU(_itemID)
  IF menuID = 1 AND itemID = 1 THEN CALL DISPOSEGWORLD(offScreen&):END
  MENU
END FN
WINDOW OFF
WINDOW #1,"Fire!",(16,45)-(16+ImageX*2,45+ImageY*4),_docNoGrow
CALL GETPORT(cport&):                             ' "ウィンドウのグラフポートを確保しておきます"
ON MENU FN doMenus
ON DIALOG FN doDialog
FN initMenu
FN setOffscreen
FN myCLS:                                         '"画面を消去する"
FN transfer
FOR i=0 TO 255
  IF i > 128 THEN Gpal(i) = i / 2
  Gpal(i) = Gpal(i) << 8:                         ' "シフト演算しておく(256倍)"
NEXT
DO
  HANDLEEVENTS
  FN Fire
UNTIL _false
END