[GameMaker: Studio] 光源周辺だけ明るくする方法・IE対応版

IE等のシェーダー非対応な環境で、光源周辺だけを明るく照らすサンプルです。

思いっきり簡略化してブロック単位(この例では4×4ピクセル)でのマスクにしているため、レトロゲームのようにカクカクした照明になっています。

まず暗くしたいフィールドであるroomまたはviewの縦横それぞれをブロックサイズ(4ピクセル)で割り、そのサイズでマスク情報格納用のds_gridを作成しておきます。
画面を揺らす演出等が必要ならば、4方向それぞれ数ブロック大きめにしてください。
それと、照明座標を格納するds_queueもx、yそれぞれ作っておきます。

local_sprite_id = マスクに使う黒ベタスプライトのID;
// マスク用スプライトのサイズ
global.field_mask_blocks_bw = sprite_get_width(local_sprite_id);
global.field_mask_blocks_bh = sprite_get_height(local_sprite_id);
// マスク情報格納用配列のサイズ(画面を揺らしたりしてもいいように、四方それぞれ1ブロック大きめに)
global.field_mask_blocks_lw = ceil((global.field_mask_blocks_bw * 2 + view_wview[0]) / global.field_mask_blocks_bw);
global.field_mask_blocks_lh = ceil((global.field_mask_blocks_bh * 2 + view_hview[0]) / global.field_mask_blocks_bh);
// マスク情報格納用配列を作成
global.field_mask_blocks = ds_grid_create(global.field_mask_blocks_lw, global.field_mask_blocks_lh);

// 照明座標格納用配列
global.field_mask_light_px = ds_queue_create();
global.field_mask_light_py = ds_queue_create();

ここまでで下準備はOKです。

メインループのbegin stepイベントで光源座標のキューバッファを毎回クリアしておきます。通常はここに処理が来た時点で空になっているはずですが、念のためやっておきましょう。

ds_queue_clear(global.field_mask_light_px);
ds_queue_clear(global.field_mask_light_py);

次に光源となるobjectstep(またはend step)イベントで、そのキューバッファにxy座標を格納していきます。

ds_queue_enqueue(global.field_mask_light_px, x);
ds_queue_enqueue(global.field_mask_light_py, y);

これで光源の位置が指定できました。
ここまでの流れでは画面は通常の明るい描画になっています。

いよいよ、ここにマスク処理を施します。
マスク処理はメインループのdraw endイベントで行います。

まず、一番初めに作成しておいたマスク情報格納用のds_gridを環境光の明るさの逆の値(マスクの濃度)でクリアしておきます。
真っ暗にするなら1.0、薄明かりにするなら0.9~0.8程度でいいでしょう。

ds_grid_clear(global.field_mask_blocks, 0.9);

このマスクバッファを照らすために、光源座標のキューバッファから光源座標を一つずつ取り出して加工していきます。
この例では光源の座標だけではなく、半径もキューバッファから取得しています。

var lp = 0;
var px = 0;
var py = 0;
var dx = 0;
var dy = 0;
var r = 0;
var a = 0;
var a_base = 0;
// 登録された照明の数
var lpn = ds_queue_size(global.field_mask_light_px);
// 照明数分ループ
for(lp=0; lp<lpn; lp++) {
    // 照明の座標
    px = floor(ds_queue_dequeue(global.field_mask_light_px) / global.field_mask_blocks_bw) + 1;
    py = floor(ds_queue_dequeue(global.field_mask_light_py) / global.field_mask_blocks_bh) + 1;
    // 照明の半径(半径とするブロック数が入っている)
    r = ds_queue_dequeue(global.field_mask_light_radius);
    // 照明倍率
    a_base = 0.9;
    // 周囲をボカす
    if(r > 4) {
        ds_grid_multiply_region(global.field_mask_blocks, px - r + 3, py - r, px + r - 3, py - r, a_base);
        ds_grid_multiply_region(global.field_mask_blocks, px - r + 3, py + r, px + r - 3, py + r, a_base);
        ds_grid_multiply_region(global.field_mask_blocks, px - r, py - r + 3, px - r, py + r - 3, a_base);
        ds_grid_multiply_region(global.field_mask_blocks, px + r, py - r + 3, px + r, py + r - 3, a_base);
        r--;
        a_base -= 0.2;
    }
    if(r > 3) {
        ds_grid_multiply_region(global.field_mask_blocks, px - r + 2, py - r, px + r - 2, py - r, a_base);
        ds_grid_multiply_region(global.field_mask_blocks, px - r + 2, py + r, px + r - 2, py + r, a_base);
        ds_grid_multiply_region(global.field_mask_blocks, px - r, py - r + 2, px - r, py + r - 2, a_base);
        ds_grid_multiply_region(global.field_mask_blocks, px + r, py - r + 2, px + r, py + r - 2, a_base);
        r--;
        a_base -= 0.2;
    }
    if(r > 2) {
        ds_grid_multiply_region(global.field_mask_blocks, px - r + 1, py - r, px + r - 1, py - r, a_base);
        ds_grid_multiply_region(global.field_mask_blocks, px - r + 1, py + r, px + r - 1, py + r, a_base);
        ds_grid_multiply_region(global.field_mask_blocks, px - r, py - r + 1, px - r, py + r - 1, a_base);
        ds_grid_multiply_region(global.field_mask_blocks, px + r, py - r + 1, px + r, py + r - 1, a_base);
        r--;
        a_base -= 0.2;
    }
    if(r > 1) {
        ds_grid_multiply_region(global.field_mask_blocks, px - r + 0, py - r, px + r - 0, py - r, a_base);
        ds_grid_multiply_region(global.field_mask_blocks, px - r + 0, py + r, px + r - 0, py + r, a_base);
        ds_grid_multiply_region(global.field_mask_blocks, px - r, py - r + 0, px - r, py + r - 0, a_base);
        ds_grid_multiply_region(global.field_mask_blocks, px + r, py - r + 0, px + r, py + r - 0, a_base);
        r--;
        a_base -= 0.2;
    }
    ds_grid_set_region(global.field_mask_blocks, px - r, py - r, px + r, py + r, 0.0);
}

周囲をボカす処理の部分は配列化してwhileで回せばもっと短くすっきりと書けますが、これくらいの量であればループさせるより縦に並べた方が高速に処理できます。

ここでのポイントは、周囲をボカすためにds_grid_multiply命令を使用している点です。
マスクバッファの初期値を明るくするのではなく暗くしておき、掛け算によってその濃度を下げていくことで、複数の光源が重なっても悪影響が出なくなります。

周囲のボカし部分はds_grid_multiply命令を使用し、ボカす必要が無く完全に明るくする部分はds_grid_set命令を使うことで少しは高速化できます。

すべての光源の処理が終わってマスク情報が完成したので、あとはこの情報に従ってマスク用スプライトを描画します。

var lpx = 0;
var lpy = 0;
dy = view_yview[global.field_mask_view_id] - global.field_mask_blocks_bh;
for(lpy=0; lpy<global.field_mask_blocks_lh; lpy++) {
    dx = view_xview[global.field_mask_view_id] - global.field_mask_blocks_bw;
    for(lpx=0; lpx<global.field_mask_blocks_lw; lpx++) {
        a = ds_grid_get(global.field_mask_blocks, lpx, lpy);
        if(a > 0) draw_sprite_ext(マスク用黒ベタスプライトのID, 0, dx, dy, 1.0, 1.0, 0.0, c_white, a);
        dx += global.field_mask_blocks_bw;
    }
    dy += global.field_mask_blocks_bh;
}

これで光源周辺だけが明るくなりました。
光源の周囲はグラデーション状に少しずつ暗くなっていきます。

一番上の実行例では照明の半径をランダムに変化させて照明らしさが出るように演出を加えています。

このブロック化バッファを使った手法は、照明以外でも色々と応用ができますので、是非覚えておいてください。