RaspberryPi Pico(RP2040)のハードウェア補間機能(interpolator)でテクスチャマッピング

以前こちらの記事で、RP2040のハードウェア補間の機能が興味深いと書きましたが、pico-examplesのコードを見ていたら、この機能を使ったテクスチャマッピングの例がありました。この例は標準出力に結果をプリントするだけなので、コードを見ないと意味をつかめないですが、グラフィック画面上の座標(u, v)から、その座標のテクスチャ上でのピクセル値を求める処理を行っています。

今回は、このコードを参考に、LCD の画面をテクスチャで塗りつぶすデモを作りましたので、その処理内容を解説します。

まず、デモの動作の様子はこんな感じです。毎回LCDのすべてのピクセルを塗りつぶしていますが、速度的には十分速いです。

今回、解説はコード抜きで行いますが、コード自体はこちらにアップロードしてありますので、具体的なAPIを見たい方はこちらも参照してください。

pico_test_projects/interpolator at main · boochow/pico_test_projects
Some projects to test Raspberry Pi Pico unique functionalities, such as interpolators or scanvideo library. - boochow/pi...

用意したテクスチャはこちらです。16×16ピクセルで、1ピクセル1バイトになっています。

LCD の各ピクセルを左上から順番に塗りつぶしていく際に、このテクスチャを参照してピクセルの色を決定します。

具体的には、 LCD の各ピクセルの座標をテクスチャの特定のピクセルにマップします。下の図で赤丸が LCD のピクセル、正方形が テクスチャのピクセルを表しています。

青い数字はテクスチャのピクセルのアドレスを表しています。テクスチャーは縦横16ピクセルですので、左上から順番にアドレスを振っていくと、左上が16進数で$00、右上が$0F、2行目の左端が$10、・・・右下が$FF、というようになります。

全ての赤丸(の座標)について、対応する青い数字を求めることが今回の処理の主な内容になります。

なお、テクスチャのサイズが16×16、すなわち4ビット×4ビットなので、テクスチャの各ピクセルのアドレスは、その下位4ビットがそのピクセルのx座標、上位4ビットがそのピクセルのy座標となっていることを憶えておいてください。

アドレス:$12 ⇔ (x, y) = (2, 1)

ではまず、LCD の 横一列分のピクセルを、このテクスチャのアドレスに対応付けることを考えます。
下の図の黒い枠がLCDのピクセル、青い枠がテクスチャのピクセルだとします。便宜上、LCDのピクセルの座標をピクセルの左上角に取ります(赤い丸)。

LCDのピクセルの座標系では、赤い丸は(0,0), (1,0), (2,0),…と並んでいます。
テクスチャのピクセルの座標系では、テクスチャのピクセルは同様に(0,0), (1,0), (2,0),…と並んでいます。

ここで、LCDの座標系における(1,0)がテクスチャのピクセルにおける(du, dv)に一致しているとします。すると、LCDのピクセルの座標(x, 0)は (x * du, x * dv)に変換できます。
例えば(du, dv) =(0.7, 0.4)だったら

(0,0) → (0, 0)
(1,0) → (0.7, 0.4)
(2,0) → (1.4, 0.8)

というようにテクスチャ上の座標に変換されます。

この変換結果を(u, v)としますと、u、vはどちらも0~15の範囲の整数でなければなりません。そこで、u、vそれぞれについて4ビットの整数とするために、小数点以下を切り捨ててから、$0FとのANDを取ります。

(0,0) → (0, 0) → (0, 0)
(1,0) → (0.7, 0.4) → (0, 0)
(2,0) → (1.4, 0.8) → (1, 0)

さらに、こうして得られた1組の4ビット整数から、vを上位4ビット、uを下位4ビットとする8ビットの値を作ると、テクスチャのピクセルのアドレスが得られます。

(0,0) → (0, 0) → (0, 0) → $00
(1,0) → (0.7, 0.4) → (0, 0) → $00
(2,0) → (1.4, 0.8) → (1, 0) → $01

これで、目的としていた「LCDのピクセルをテクスチャのピクセルのアドレスに変換する」ことができました。

次に、RP2040のハードウェアInterpolatorを使うと、以上の処理がどのように行えるのかを見ていきます。

次の図がInterpolatorの全体像です。同じ回路が上下あわせて2つあり、両方の結果を加算することもできます。

5つの青い箱、accumlator0、accumulator1、base0、base1、base2は外部から与えることができるパラメータで、関数の引数に相当します。型はいずれも32ビット整数(符号付きも可)ですが、今回は上で述べたように小数点数を扱いたいので、整数部16ビット小数部16 ビットの固定小数点数として使います。

機能ブロックRight Shiftは、入力を右へビットシフトします。シフト量は設定できます。

Maskは、あらかじめ設定した32ビットのマスク値と入力とのANDを取ります。

Sign-Extend from Maskは、Maskでの処理でマスクされなかった(マスク値が1だった)ビットの中での最上位のビットを符号ビットと見なして、32ビット整数での符号ビットに変換する(符合ビットよりもMSB側でマスク値が0だった部分を符号ビットの値で埋める)処理を行います。今回は使用していません。

丸の中に+が書かれているのは、加算器です。

「0, 1」と書かれている、入力2つ出力1つの箱が8個ありますが、これは「フラグの値によって入力のうちどちらか一方を選択する」というブロックです。各ブロックを制御するフラグの名称は以下の図のようになっています。フラグの値は、リセット時(デフォルト値)はすべて0です。

このハードウェアを、フラグ等を設定することによって、今回は以下の図のように使用します。

上半分のブロックは、uの初期値とduを与えると、

u ← u+du

を繰り返し計算します。また同時に、右16ビットシフトして整数部を取り出し、さらにその最下位4ビットを出力します。繰り返し処理は、Interpolatorから結果を読みだすたびに行われます。

下半分は、同じくv+dvを繰り返し計算し、整数部の下位4ビットを出力しますが、その4ビットのデータは結果の出力の中のbit4~bit7に入り、bit0~bit3は0が入ります。

これら2つを加算すると、8ビットの値が得られ、その上位4ビットはv、下位4ビットはuの値になります。さらに、これにテクスチャの先頭へのポインタ(output)を加算することで、テクスチャの特定のピクセルへのポインタを求めています。

Cのコードで表現すると
(int)output + (((u += du) >> 16) & 0x0f) + (((v += dv) >> 12) & 0xf0)
という感じです。

LCDに描画する際には、横1行分240ピクセルをInterpolatorで繰り返し計算し、行が変わるごとにu, vを再設定しています。

なおこのデモでは、横1行のピクセルの計算においてテクスチャの縦軸の増分は、以下の図のようにsというパラメータを与えています。sは歪み(skew)を表していて、sが0でないときはテクスチャを平行四辺形に歪ませます。

以上でこのデモの解説は終わりです。

du、dvを適切に(三角関数で)求めれば、テクスチャの回転も表現できると思います。また、より巨大なテクスチャ(例えば256×256)を用いることもできるはずです。

しかも、RP2040ではこのInterpolatorをCPUコアごとに2つずつ持っています。ゲームを作るのには有用そうですね。RP2040のデータシートには、わざわざ以下のような注釈が載っています(P.30)

By sheer coincidence, the interpolators are extremely well suited to SNES MODE7-style graphics routines. For example, on each core, INTERP0 can provide a stream of tile lookups for some affine transform, and INTERP1 can provide offsets into the tiles for the same transform.
全くの偶然なのですが、interpolatorはSNES(スーパーファミコン)のMODE7のようなスタイルのグラフィックス処理にはとても向いています。例えば、各コアにおいて、INTERP0はアフィン変換のためにタイルのルックアップ処理を連続的に行わせ、INTERP1は同じ変換のためにタイル内部へのオフセットの算出処理を行わせることが可能です。

ちなみにMODE7の解説として以下のようなビデオがありました。

コメント