3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ReactAdvent Calendar 2024

Day 22

React+WebGPUで2次元データをビューポート内でタイル状に可視化するコンポーネントを作った

Last updated at Posted at 2024-12-21

これは何?

hokusai_demo_movie.gif

WebGPU-React-Bitmap-Viewport - a React component that uses WebGPU to render bitmap data within a set of viewports synchronously

をつくりました。ビットマップ画像データをビューポート内に表示して、Zoom / Pan / Pinch 操作ができます。

  • ビューポート内のマウスドラッグ操作、マウスホイール操作、ビューポート下端および右端に表示されたスクロールバーでの操作により、ビューポートをインタラクティブに移動・拡大・縮小できます

  • 境界を超えてスクロールしようとするとバウンスします

  • ひとつの画像データをもとに、複数のビューポートを連動させて、互いのビューポートの表示範囲を強調した形で表示できます

  • JavaScript的にUint32ArrayやUint8Arrayとして保持している画像データの内容を、Reactコンポーネントとして、任意のキャンバスサイズ・表示範囲でのビューポート内容として表示する目的に特化しています

  • WebGPUを用いることで、ビューポートで表示される必要な最小限の範囲のピクセルだけを抽出し、それに対応するポリゴンを描画するためのインスタンス群を64並列でワークグループにディスパッチするという実装になっています。そのため、極めて高速に、滑らかに動作をします

  • 1ピクセルは、拡大時には8角形のタイルとして、縮小時には正方形のタイルとして描画します(このあたりは、コードを書き換えれば自由に変えられるだろうと思います)

  • 画像データの内容は、1ピクセルあたりRGBA値の32ビット値をUint32Arrayとして格納したビットマップデータ、RGBやHSVなどのいずれかの8ビット値をUint8Arrayとして格納したビットマップデータなど、表示内容に応じて変えることができます

  • フォーカスされているセル、選択されているセルの状態を内部的に管理し、マウス操作に応じてフォーカスされているセル位置の変化、選択されているセル範囲の変化に応じたコールバック関数をコンポーネントへのプロパティで与えることができます

  • 表示する内容が動的に変化した場合に応じて、随時、再描画を行わせることができます

ライブデモ

このコンポーネントを用いて開発したデモを、GitHub Pagesで公開しています。

こちらから試せます。次のような3種類の内容です。

  • URLから取得した静的な画像をビューポート表示するデモ
  • JavaScriptで生成した動的な画像をビューポート表示するデモ
  • シミュレーションプログラム(シェリングの分居モデル)のWebGPU Compute Shaderによる実行内容をアニメーション表示するデモ

とくに、3つめについては、以下ツイートを参照してください。

最新のコードでは、MacBook Pro 16インチ 2021(Apple M1 Max 64GB)で動作させたところ、
1024x1024のサイズの画像の動的な表示であれば80fps程度、256x256のサイズであればMacBookProのProMotionの上限のフレームレートである120fpsのパフォーマンスが出ています。

ダウンロードとインストール

こちらのデモを手元で動かしてみたい場合は、次のように実行してください。

git clone https://github.com/kubohiroya/webgpu-react-bitmap-viewport.git
cd webgpu-react-bitmap-viewport
pnpm install
cd examples
pnpm install
vite dev

NPMパッケージとして公開しているので、自分のアプリに組み込みたい場合には、次のように実行してください。

pnpm add webgpu-react-bitmap-viewport

つかいかた

import { Grid, GridHandles } from "webgpu-react-bitmap-viewport";
import { useRef } from "react";

// 表示するビットマップ画像データの大きさ
const gridSize = { numColumns: 128, numRows: 128 };

// 表示するビットマップ画像データをランダムな内容で作成
const data =  new Float32Array(gridSize.numRows * gridSize.numColumns);
for (let i = 0; i < data.length; i++) {
  if (Math.random() < 0.99) {
    data[i] = i / data.length;
  } else {
    data[i] = Infinity;
  }
}

// フォーカス位置を保持する配列
const focusedCellPosition = new Uint32Array([-1, -1]);

// セルの選択状態を保持する配列
const selectedStates = new Uint32Array(Math.ceil(gridSize.numRows * gridSize.numColumns / 32));

// ビューポートの表示範囲を保持する配列
const viewportStates = new Float32Array([
  0.0, 0.0, 16.0, 16.0, // viewport index 0: left, top, right, bottom
  8.0, 8.0, 24.0, 24.0, // viewport index 1: left, top, right, bottom
]);

export const App = () => {

  // 連動する2つのビューポート(Grid)へのハンドラ
  const gridHandlerRefs =
    [
      useRef<GridHandles>(null),
      useRef<GridHandles>(null)
    ];

  return (
    <>
      <Grid
        index={0}
        ref={gridHandlerRefs[0]}
        numViewports={2}
        headerOffset={{ left: 10, top: 10 }}
        canvasSize={{ width: 300, height: 300 }}
        numColumns={gridSize.numColumns}
        numRows={gridSize.numRows}
        scrollBar={{
          radius: 5.0,
          margin: 2.0,
        }}
        data={data}
        focusedCellPosition={focusedCellPosition}
        selectedStates={selectedStates}
        viewportStates={viewportStates}
        onFocusedCellPositionChange={(sourceIndex: number, columnIndex: number, rowIndex: number) => {
          gridHandlerRefs[1].current?.refreshFocusedCellPosition(sourceIndex, columnIndex, rowIndex);
        }}
        onSelectedStateChange={(sourceIndex:number, columnIndex: number, rowIndex: number) => {
          gridHandlerRefs[1].current?.refreshSelectedState(sourceIndex, columnIndex, rowIndex);
        }}
        onViewportStateChange={(sourceIndex: number) => {
          gridHandlerRefs[1].current?.refreshViewportState(sourceIndex);
        }}
      />
      <Grid
        index={1}
        ref={gridHandlerRefs[1]}
        numViewports={2}
        headerOffset={{ left: 10, top: 10 }}
        canvasSize={{ width: 300, height: 300 }}
        numColumns={gridSize.numColumns}
        numRows={gridSize.numRows}
        scrollBar={{
          radius: 5.0,
          margin: 2.0,
        }}
        data={data}
        focusedCellPosition={focusedCellPosition}
        selectedStates={selectedStates}
        viewportStates={viewportStates}
        onFocusedCellPositionChange={(sourceIndex: number, columnIndex: number, rowIndex: number) => {
          gridHandlerRefs[0].current?.refreshFocusedCellPosition(sourceIndex, columnIndex, rowIndex);
        }}
        onSelectedStateChange={(sourceIndex: number, columnIndex: number, rowIndex: number) => {
          gridHandlerRefs[0].current?.refreshSelectedState(sourceIndex, columnIndex, rowIndex);
        }}
        onViewportStateChange={(sourceIndex: number) => {
          gridHandlerRefs[0].current?.refreshViewportState(sourceIndex);
        }}
      />
    </>
  );
}

類似のライブラリとの比較

他のライブラリでも、似たようなものがあるかと思います。

MapLibre GL JS

MapLibre GL JSは、タイル状の地図データの表示コンポーネントです。

これに対して本実装では、画像データのサイズは、WebGPUでストレージとして保持できるの最大サイズにまで対応し、それ以上のものは扱わないこととしています。
VRAMに載らないくらいの巨大なサイズの画像データを拡大縮小しながら表示したい場合には、MapLibre GL JSを使って、事前にラスタータイル化したものを表示するのがよいのではないかと思います。

react-zoom-pan-pinch

react-zoom-pan-pinch は、Reactコンポーネントをビューポート内で表示するものです。

これに対して本実装では、ビットマップ画像をZoom / Pan / Pinchしながら表示することに特化しています。
ReactコンポーネントをZoom / Pan / Pinchしながら表示するためにはreact-zoom-pan-pinchを使うのがよいでしょう。

おわりに

まだまだ荒削りな実装ですが、高速・滑らかな操作が実現できたので、よかったです。
(バウンスの動作にまだちょっと違和感がありますが...)

React+WebGPUでのプログラミング、たいへんですけど、たのしいです!

3
1
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
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?