7
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

HTML/Canvas ImageDataのバイトデータ操作による描画をWebAssembly(AssemblyScript)でやってみた

Last updated at Posted at 2021-12-01

この記事は Web グラフィックス Advent Calendar 2021 の1日目の記事です。

発端

Flutter

仕事でAndroidの業務アプリを作ることになり、IonicまたはFlutterの二択ということになりました。筆者はAngular使いなのでIonicを選んだのですが、小心者なので、選ばなかったFlutterのことが気になってしかたなかったです。

それでFlutterについて後追いで調べていて、これは良いなと思ったのが、2DグラフィックスライブラリのSkiaをWebAssemblyにしたもの(CanvasKit)を使っているところでした。

描画をWebAssemblyにしてしまえばどのブラウザでもネイティブでも同じ描画結果にできるわけで、それはすごい安心感があります。

WebViewに依存しないのもいいですね。IonicはWebView依存なのでOSに更新が入ると何が起こるか不安で夜しか眠れないようなところがあります。

WebAssemblyでの描画は速いのか?

気になるのは実行速度です。

ベンチマークみたいのは面倒なので、Flutterのパーティクルのサンプルあたりを見たところ、「めちゃくちゃ速い」とも「めちゃくちゃ遅い」ともいえないと感じました(技術者にあるまじき役に立たない感想)。描画面積が大きいと面積に比例してFPSが下がるのがはっきり分かります。低スペックなモバイル端末だときつそうな気もしますが、解像度が低いのであれば問題ないのかもしれません。

しかし自分が知りたかったのはFlutterというよりWebAssemblyによる描画がどうなのかだったので、最小構成の描画処理を作ってみることにしました。それで速かったらなんか嬉しいし、遅かったら、遅いことが分かります。分かって何が嬉しいのかは分かりませんが、それでもいい…とりあえずプログラマーはそういう生き物だと思います。

JavaScriptで作ってみた

最小構成ということで、まずGLSLをJavaScriptで再現するプログラムを作ってみました。

ライブデモ
ソースコード

作ったのは、ピクセル単位にバイトデータを計算してグラフィックスを描画するプログラムです。ラスタライザというやつです。
canvasの描画機能はほとんど使わず、描画結果を画面に表示する目的でのみcanvasを使用します。canvasに表示するバイトデータを扱うにはImageDataを利用しますので、ImageData.dataへの書き込みが主たる処理ということになります。

ここはWebAssemblyは使わないため、CodePenにしてみました。

See the Pen HTML/Canvas ImageDataのバイトデータ操作による描画(JavaScript版) by 柏崎ワロタロ (@warotarock) on CodePen.

なおGLSLはGLSL Sandboxから拝借しました。

AssemblyScriptでも作ってみた

WebAssemblyを書く方法はたくさんありますが、自分はTypeScript使いなので、TypeScript風でとっつきやすそうなAssemblyScriptで作ってみました。i32とかf64とか暗黙で型変換されるところを合わせるのが少し面倒でしたが、移植は難しくありませんでした。ImageDataにメモリをバインドするやりかたを見つけるのが一番大変だったかも。

ライブデモ
ソースコード

なお、微妙に異なる二種類の描画方法で実装してみています。

  • JavaScriptから渡された関数をAssemblyScriptから呼んでピクセルデータを書き込む方法
  • AssemblyScriptで確保したメモリを共有し書き込む方法

下の方法のほうが若干速いです。
拡張子はtsですがAssemblyScriptの拡張子はtsが標準みたいです。

index.ts(AssemblyScript)
// instantiate時に渡されたモジュール
declare namespace main {
  function putPixel(offset: f64, r: f64, g: f64, b: f64, a: f64): void
}

// JavaScript関数経由で描画
export function drawViaFunction(width: u32, height: u32, time: f64): void {

  const pixelBytes: u32 = 4
  const lineBytes: u32 = width * pixelBytes
  const density: f64 = 0.05
  const amplitude: f64 = 0.3
  const frequency: f64 = 10.0
  const scroll: f64 = 0.1
  const fwidth = width as f64
  const fheight = height as f64
  const sin = Math.sin
  const abs = Math.abs

  let offsetY: u32 = 0

  for (let y: f64 = 0.0; y < fheight; y += 1.0) {

    const posy = y / fheight - 0.5
    let offset = offsetY

    for (let x: f64 = 0; x < fwidth; x += 1.0) {

      const posx = x / fwidth - 0.5

      // from https://glslsandbox.com/e#76927.0
      const line1 = (1.0 / abs((posy + (amplitude * sin((posx + time * scroll) * frequency)))) * density) * 255
      const line2 = (1.0 / abs((posy + (amplitude * sin(((posx - 0.1) + time * scroll) * frequency)))) * density) * 255
      const line3 = (1.0 / abs((posy + (amplitude * sin(((posx - 0.2) + time * scroll) * frequency)))) * density) * 255
      
      main.putPixel(
        offset,
        0.10 * line1 + 0.05 * line2 + 0.05 * line3,
        0.05 * line1 + 0.10 * line2 + 0.05 * line3,
        0.05 * line1 + 0.05 * line2 + 0.10 * line3,
        255.0
      )

      offset += pixelBytes
    }

    offsetY += lineBytes
  }
}

// メモリ共有で描画
export function drawToByteArray(data: Uint8Array, width: u32, height: u32, time: f64): void {

  const pixelBytes: u32 = 4
  const lineBytes: u32 = width * pixelBytes
  const density: f64 = 0.05
  const amplitude: f64 = 0.3
  const frequency: f64 = 10.0
  const scroll: f64 = 0.1
  const fwidth = width as f64
  const fheight = height as f64
  const sin = Math.sin
  const abs = Math.abs

  let offsetY: u32 = 0

  for (let y: f64 = 0.0; y < fheight; y += 1.0) {

    const posy = y / fheight - 0.5
    let offset = offsetY

    for (let x: f64 = 0; x < fwidth; x += 1.0) {

      const posx = x / fwidth - 0.5

      // from https://glslsandbox.com/e#76927.0
      const line1 = (1.0 / abs((posy + (amplitude * sin((posx + time * scroll) * frequency)))) * density) * 255
      const line2 = (1.0 / abs((posy + (amplitude * sin(((posx - 0.1) + time * scroll) * frequency)))) * density) * 255
      const line3 = (1.0 / abs((posy + (amplitude * sin(((posx - 0.2) + time * scroll) * frequency)))) * density) * 255
      
      data[offset + 0] = clampU8(line1 * 0.10 + line2 * 0.05 + line3 * 0.05)
      data[offset + 1] = clampU8(line1 * 0.05 + line2 * 0.10 + line3 * 0.05)
      data[offset + 2] = clampU8(line1 * 0.05 + line2 * 0.05 + line3 * 0.10)
      data[offset + 3] = 255

      offset += pixelBytes
    }

    offsetY += lineBytes
  }
}

function clampU8(value: f64): u8 {

  if (value > 255) {
    return 255
  }
  else {
    return value as u8
  }
}

// 型付き配列をJavaScriptと共有するために必要な型のエクスポート
export const Uint8ArrayID = idof<Uint8Array>()
index.html(JavaScript部分の抜粋)
  function initializeWasmRender(wasmModule) {

    // AssemblyScriptで実装した関数が登録されたオブジェクトを取得
    wasmRender = wasmModule.exports

    // WebAssembly内に画像用メモリを作成
    const dataLength = canvas.width * canvas.height * 4
    imageDataArrayPtr = wasmRender.__newArray(wasmRender.Uint8ArrayID, dataLength)
    wasmRender.__pin(imageDataArrayPtr) // GCで開放されないように設定

    // 画像用メモリをImageDataのバッファとして設定
    imageDataArrayView = wasmRender.__getArrayView(imageDataArrayPtr)
    viewArray = new Uint8ClampedArray(wasmRender.memory.buffer, imageDataArrayView.byteOffset, dataLength) // Uint8ClampedArrayにバッファを渡すことでメモリを参照するTypedArrayが作成されます
    wasm_ImageData = new ImageData(viewArray, canvas.width, canvas.height) // ImageDataにUint8ClampedArraを渡すことでバッファを参照するImageDataが作成されます
  }

  js_ImageData = new ImageData(canvas.width, canvas.height)
  data = js_ImageData.data
  function putPixel(offset, r, g, b, a) {

    data[offset + 0] = r
    data[offset + 1] = g
    data[offset + 2] = b
    data[offset + 3] = a
  }

速いのか

JavaScriptが思った以上に速かったのもあり、WebAssemblyで書いても正直いってそれほど速い気がしませんでした(WebAssemblyあるある)。Flutterのデモはもっと複雑そうなのにもかかわらず、面積が広くても同じかそれ以上にFPSが出ている感じがしますから、自分の作りが悪いかAssemblyScriptが遅いのかもしれません。もしくはsin関数が重かったりするのか…。

まとめ、将来性

まとめです。

  • JavaScriptとAssemblyScriptで単純なバイトデータ操作による描画処理を実装してみた
  • AssemblyScriptは使いやすかった
  • 速度は期待したほどではなかった

将来については、ブラウザだけでなく、どこででもWebAssemblyを実行できるようにしようという動きが、もうけっこう進んでいるらしいです。

JVMみたいなものかなーと最初は思いましたが、開発言語にJavaしか選択肢が無かった(当時)のと違い、WebAssemblyをサポートしている言語は圧倒的に多いです。言語が多ければ利用者の人口も多いということで、多くの人が動けば世界が動くという感じで、いずれ何か大きな流れになるんじゃないかなーという感じがします。

今からWebAssemblyを使いこなせる必要は少ないかもしれませんが、これから新しい言語を学ぶならWebAssemblyを活用しやすいものを選んでおくと、将来性があるかもしれません。自分はGo、Rust、Dartは挫折しました。

それではWebグラフィックス アドベントカレンダー初日でした。
最終日までよろしくおねがいいたします。

7
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?