DOOM on obniz Board 1Y


2021/3/20公開
2021/4/18更新

概要
あらゆるハードウェアに移植されている往年の名作FPS、DOOMをobnizの画面でプレイできるようにしてみました。
と言ってもイチから移植したわけではなく、先人が開発したJS-DOS版DOOMというものをobnizで表示できるようグラフィック変換処理を追加しただけだったりします。
変換処理の流れはオリジナルグラフィック⇒表示領域クロップ⇒ハイコントラスト化⇒縮小⇒二値化(配列ディザリング法)といった流れで、これをリアルタイムでやっとります。
最初は二値化のアルゴリズムを誤差拡散法で処理していたのですが表示品質がイマイチだったのであえて配列ディザリング法でやってます。

準備
DOOMが起動したら下記の設定を行います。

コードサンプル
<!doctype html>
<html lang="en-us">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <title>DOOM</title>
    <style type="text/css">
      .dosbox-container { width: 320px; height: 200px; }
      .dosbox-container > .dosbox-overlay { background: url(https://js-dos.com/cdn/DOOM.png); }
    </style>

    <script src="https://unpkg.com/obniz@3.x.0/obniz.js"></script>

    <script>
        var obniz = new Obniz("OBNIZ_ID_HERE");
    </script>

  </head>
  <body>
    <div id="dosbox"></div>
    <br>

    <!-- クロップ用キャンバス -->
    <canvas id="crop" width="196" height="96"></canvas>

    <!-- obniz用キャンバス -->
    <canvas id="obn" width="128" height="64"></canvas>

    <script type="text/javascript" src="https://js-dos.com/cdn/js-dos-api.js"></script>

    <script type="text/javascript">
      // ハイコントラスト化
      function contrastImage(imgData, contrast){  //input range [-100..100]
        var d = imgData.data;
        contrast = (contrast/100) + 1;  //convert to decimal & shift range: [0..2]
        var intercept = 128 * (1 - contrast);
        for(var i=0;i<d.length;i+=4){   //r,g,b,a
            d[i] = d[i]*contrast + intercept;
            d[i+1] = d[i+1]*contrast + intercept;
            d[i+2] = d[i+2]*contrast + intercept;
        }
        return imgData;
      }

      // グレイスケール化
      function toGrayscale(array, width, height) {
        let outputArray = new Uint8Array(width * height);
        for (let i = 0; i < height; i += 4) {
          for (let j = 0; j < width; j += 4) {
            for (let dy = 0; dy < 4; ++dy) {
              for (let dx = 0; dx < 4; ++dx) {
                const r = array[((i + dy) * width + (j + dx)) * 4 + 0];
                const g = array[((i + dy) * width + (j + dx)) * 4 + 1];
                const b = array[((i + dy) * width + (j + dx)) * 4 + 2];
                const gray = (r + g + b) / 3 | 0;
                outputArray[(i + dy) * width + (j + dx)] = gray;
              }
            }
          }
        }
        return outputArray;
      }

      // 二値化(ディザ法)
      function dither1CH(u8array, width, height) {
        const bayer = [
          0, 8, 2, 10,
          12, 4, 14, 6,
          3, 11, 1, 9,
          15, 7, 13, 5
        ];
        const bayer2 = new Uint8Array(bayer.map(x => x * 16 + 8));
        let outputData = new Uint8Array(width * height);
        for (let i = 0; i < height; i += 4) {
          for (let j = 0; j < width; j += 4) {
            for (let dy = 0; dy < 4; ++dy) {
              for (let dx = 0; dx < 4; ++dx) {
                const value = u8array[(i + dy) * width + (j + dx)];

                if (value >= bayer2[dy * 4 + dx]) {
                  outputData[(i + dy) * width + (j + dx)] = 0xff;
                } else {
                  outputData[(i + dy) * width + (j + dx)] = 0x00;
                }
              }
            }
          }
        }
        return outputData;
      }

      // 二値化メイン処理
      function processGrayAndOutput() {
        const cvs = document.getElementById("crop");
        const ctx = cvs.getContext('2d');
        const inputData = ctx.getImageData(0, 0, cvs.width, cvs.height).data;

        const output = ctx.createImageData(cvs.width, cvs.height);
        let outputData = output.data;

        const grayArray = toGrayscale(inputData, cvs.width, cvs.height);
        const funcOutput = dither1CH(grayArray, cvs.width, cvs.height)
        for (let i = 0; i < cvs.height; i += 1) {
          for (let j = 0; j < cvs.width; j += 1) {
            const value = funcOutput[i * cvs.width + j];

            outputData[(i * cvs.width + j) * 4 + 0] = value;
            outputData[(i * cvs.width + j) * 4 + 1] = value;
            outputData[(i * cvs.width + j) * 4 + 2] = value;
            outputData[(i * cvs.width + j) * 4 + 3] = 0xff;
          }
        }
        ctx.putImageData(output, 0, 0);
      }

      var dosbox = new Dosbox({
        id: "dosbox",
        onload: function (dosbox) {
          dosbox.run("https://js-dos.com/cdn/upload/DOOM-@evilution.zip", "./DOOM/DOOM.EXE");
        },
        onrun: function (dosbox, app) {
          console.log("App '" + app + "' is runned");

          // DOS-BOX用キャンバス
          var srccanvas = document.getElementsByClassName("dosbox-canvas")[0];
          var srcctx = srccanvas.getContext("2d");

          // クロップ用キャンバス
          var dstcanvas = document.getElementById("crop");
          var dstctx = dstcanvas.getContext("2d");

          // obniz用キャンバス
          var obncanvas = document.getElementById("obn");
          var obnctx = obncanvas.getContext("2d");

          setInterval(function() {
              // クロップ開始位置:上224,左120から 幅192px,高96px⇒アスペクト比2:1なのでこのまま縮小
              var image = srcctx.getImageData(224, 120, 192, 96);

              // あらかじめクロップ領域をハイコントラスト化
              contrastImage(image, 100);
              dstcanvas.getContext('2d').putImageData(image, 0, 0);

              // 二値化フィルタかける
              processGrayAndOutput();

              // obniz用キャンバスに転送
              obnctx.drawImage(dstcanvas, 0, 0, 192, 96, 0, 0, 128, 64);

              obniz.display.draw(obnctx);
          }, 1000/15);  // 15fpsで描画
        }
      });
    </script>
  </body>
</html>


戻る