組み込みの埋まってるとこ

システム寄りの組み込みCプログラミングBLOG

組み込みでlongjmp/setjmpは使えるか?

構造化言語でなくなる、よく分からない等の理由でなんとなく嫌われ者のlongjmp/setjmpですが、 その気になれば例外処理やスレッドも実装できる知る人ぞ知る便利な関数でもあります。 そんなlongjmp/setjmpが組み込みでも使用できるのかを調べてみました。

longjmp/setjmpの動作

簡単にlogjmp/setjmpの動作を説明すると、setjmpでラベルを付けて、longjmpでそのラベルへ分岐します。 C言語のgotoは関数の外側へ分岐することはできないのですが、longjmpはどこへでも分岐できるところが違います。

longjmp/setjmpの使用例としてC++の例外処理のようなことをするプログラムを示します。

#include <setjmp.h>

void func();
jmp_buf except;

int main()
{
    if (setjmp(except) == 0) { /* try */
        func();
    } else { /* catch */
        // ...
        return 0;
    }

    return 1;
}

void func()
{
    // ...
    if (fatal_error) {
        longjmp(except, 1); /* throw */
    }
    // ...
}

setjmpでの処理

longjmpで関数の外側へ分岐してしまうと関数は呼び出し元へ戻るものという構造が壊れてしまいます。 そこでsetjmpで戻り位置を決めたときタスクを切り替えるようにその場でレジスタを退避します。

実際にarm-none-eabi-gccのlongjmp/setjmpがどんな処理をしているかを調べた結果が次のアセンブラです。 setjmp関数の中でstm命令でスタックにレジスタを退避し、 longmp関数の中でldm命令で退避したレジスタを復旧していることが分かります。

0000802c <setjmp>:
    802c:   46ec        mov ip, sp
    802e:   e8a0 5ff0   stmia.w r0!, {r4, r5, r6, r7, r8, r9, sl, fp, ip, lr}
    8032:   f04f 0000   mov.w   r0, #0
    8036:   4770        bx  lr

00008038 <longjmp>:
    8038:   e8b0 5ff0   ldmia.w r0!, {r4, r5, r6, r7, r8, r9, sl, fp, ip, lr}
    803c:   46e5        mov sp, ip
    803e:   0008        movs    r0, r1
    8040:   bf08        it  eq
    8042:   2001        moveq   r0, #1
    8044:   4770        bx  lr
    8046:   bf00        nop

組み込みでlongjmp/setjmpは使えるか?

ライブラリにlongjmp/setjmpが存在しているようなので、組み込みでも使えないことはなさそうです。

しかしよく見ると浮動小数レジスタが退避されていません。 s0-s15は壊してもかまわない呼び出し規約になっているのですが、SIMD拡張を使うとs16-s31が使われるようになるのでこのとき問題になりそうです。

もしかするとPCで使うようなリッチなOSでは毎回浮動小数レジスタを退避しているとオーバーヘッドになるので、 浮動小数点命令が実行されたときだけCPU例外を起こしてOSが浮動小数レジスタの退避を代行するような仕組みがあるのかもしれません。

しかしRTOSでそういうことをしているのは見たことがありません。 タスクの属性にFPU例外が設定できるようになっているとか、 FPUにvfpv3-d16を明示してSIMD拡張を禁止していればまだましな方で、 最初から浮動小数点演算がハードでできるマイコンは考慮されていません、ましてやSIMD拡張なんて、というのがほとんどです。

まとめ

arm-none-eabi-gccでlongjmp/setjmpは使えそうです。 ですがSIMD拡張があるマイコン浮動小数レジスタをすべて使い切るようなヘビーなプログラムをするときは s16-s31が退避されないことを念頭において慎重にプログラミングしましょう。

Premultiplied Alphaというカラーフォーマットについて

OpenVGやWin2D等、グラフィックスライブラリのAPIでたまにプリマルマルチプライドαというカラーフォーマットが使われているのを見かけることがあります。 これはどんなカラーフォーマットで何のために使われるのでしょう。

αブレンド

一般に半透明の色を扱いたいときはRGBのカラーと透明度を表すαをセットにしたカラーフォーマットを使います。

描き込み元の色と透明度をそれぞれsrc.RGB、src.A、描き込み先の色をdst.RGBとすると、 これらを透明度の割合に応じてブレンドして得られる色result.RGBは次の式のようになります。

result.RGB = (src.RGB * src.A) + (dst.RGB * ( 1 - src.A) … 式(1)

乗算の演算コストを下げる

上記の式(1)をよくみるとコンピュータでは演算コストが高いと嫌われる乗算が2つも使われています。 これをなんとか減らせないものかと考えられたカラーフォーマットがプリマルチプライドα(以下、プリマルαと省略)です。

プリマルαでは予めのカラーRGBに透明度αを乗算した状態でデータを用意します。 これによりブレンドするときわざわざその場で乗算する演算コストを省くことができるというわけです。

予め透明度αを乗算したプリマルαの状態であるカラーRGBをsrc.R'G'B'とすると先ほどの式(1)は次のようになり、 乗算が1つになっているのが分かります。

src.R'G'B' = src.RGB * src.A

result.RGB = src.R'G'B' * ( 1 - src.A) … 式(2)

プリマルチプライドαであるデータの作り方

一般的な画像ツールでプリマルαでデータを保存できるものはあまり多くありません。 プリマルαはコンピュータに優しすぎるフォーマットでデザイン等の用途にはあまりにも不向きです。 プリマルαが必要になったときは簡単な自作プログラムで変換することになるでしょう。

しかし何もしなくてもプリマルαとして扱うことができる画像は結構あります。 写真のように透明なピクセルがまったくない画像はそのままでRGBにα=1が乗算済みのプリマルα対応であると見なすことができます。 記号等の画像で背景を抜くために完全透明(α=0)と完全不透明(α=1)の2種類でしか透明を使っていない画像も完全透明のピクセルが黒(R=0,G=0,B=0)であれば同様にプリマルα対応と見なすことができます。 本当にプリマルαに変換する必要があるのは縁取りのアンチエイリアスやカラーステンドガラスのように多段階で透明度が使われている画像だけになります。

まとめ

OpenVGではブレンドにプリマルαを前提としたブレンドモードしかなくて面食らうのですが、 落ち着いて考えるとプリマルα対応済として扱える画像は結構あります。 それも見越してのプリマルαなのかもしれません。

半透明にしたときどうしても色が少しおかしいというときはブレンドモードがプリマルαを前提としていないかも確認してみるとよいでしょう。

関連記事

arm-none-eabi-gccでundefined referenceエラーになる原因

組み込みでのコンパイルは見たこともない難しいエラーに直面することがよくあります。 Arm用のクロスコンパイラであるarm-none-eabi-gccコンパイルしていると 次のような未定義シンボルエラーの嵐に見舞われることがありますが、この原因はなんでしょう?

エラーの原因は未定義のシステムコール

例えばarm-none-eabi-gccでprintfを使用したプログラムをコンパイルすると次のようなエラーが出力されます。 アンダーバーで始まる使った覚えがない関数がいくつも未定義となっているのが分かります。 実はこれらアンダーバーで始まる関数はシステムコールと呼ばれる本来であればOSのカーネルに存在する関数なのです。

c:/msys64/mingw64/bin/../lib/gcc/arm-none-eabi/8.4.0/../../../../arm-none-eabi/bin/ld.exe: c:/msys64/mingw64/bin/../lib/gcc/arm-none-eabi/8.4.0/../../../../arm-none-eabi/lib\libc.a(lib_a-exit.o): in function \`exit':
exit.c:(.text+0x2c): undefined reference to \`\_exit'
c:/msys64/mingw64/bin/../lib/gcc/arm-none-eabi/8.4.0/../../../../arm-none-eabi/bin/ld.exe: c:/msys64/mingw64/bin/../lib/gcc/arm-none-eabi/8.4.0/../../../../arm-none-eabi/lib\libc.a(lib_a-sbrkr.o): in function \`\_sbrk_r':
sbrkr.c:(.text+0x18): undefined reference to \`\_sbrk'
c:/msys64/mingw64/bin/../lib/gcc/arm-none-eabi/8.4.0/../../../../arm-none-eabi/bin/ld.exe: c:/msys64/mingw64/bin/../lib/gcc/arm-none-eabi/8.4.0/../../../../arm-none-eabi/lib\libc.a(lib_a-writer.o): in function \`\_write_r':
writer.c:(.text+0x24): undefined reference to \`\_write'
c:/msys64/mingw64/bin/../lib/gcc/arm-none-eabi/8.4.0/../../../../arm-none-eabi/bin/ld.exe: c:/msys64/mingw64/bin/../lib/gcc/arm-none-eabi/8.4.0/../../../../arm-none-eabi/lib\libc.a(lib_a-closer.o): in function \`\_close_r':
closer.c:(.text+0x18): undefined reference to \`\_close'
...
collect2.exe: error: ld returned 1 exit status

なぜシステムコールを使おうとするのか

arm-none-eabi-gccに付属している標準CライブラリはLinux用に開発されたNewlibを組み込み用にポーティングしたものです。 このとき組み込みでは特定のOSというものが存在しないためカーネルシステムコールを呼び出す低レベルの関数が未実装のままになりました。 そのためコンパイルの最後でプログラムをリンクするときに未定義シンボルエラーになります。

システムコールのインターフェースとなる関数

Interface Description
_exit プログラムを終了
_sbrk プログラムデータスペースを増加
_write ファイルへ書き込み
_close ファイルを閉じる
_lseek ファイル中での位置を設定
_read ファイルから読み込み
_fstat 開いているファイルの状態
_isatty 出力ストリームがターミナルであるか

「undefined reference to」を解決する方法

たいていの組み込みではこれらのシステムコールを必要とする関数はMyPrintfのように代替する関数を自分で用意して使わないようにしています。 memsetやsprintf等のように最終的にシステムコールを必要とせずNewlibの実装だけで完結する関数であれば使用しても問題ありません。

もし動作が確認されているプロジェクトで未定義シンボルエラーに出くわしたのであれば、まずコンパイラが参照しているライブラリのファイル名、パス指定、環境変数等に間違いがないことをを確認するとよいでしょう。 オープンソースRTOSではNewlibではない独自のCライブラリが用意されていてそちらをリンクするようになっていることもあります。

Pico SDKの場合

Raspberry PiのPico SDKの場合はNewlibが想定しているシステムコールについてはソースファイルの中に定義がありますので、 ボードとコンパイラを正しく選択できていればundefined referenceエラーになることはないでしょう。

定義の例

引用元: https://raw.githubusercontent.com/raspberrypi/pico-sdk/master/src/rp2_common/pico_stdio/stdio.c

int __attribute__((weak)) _write(int handle, char *buffer, int length) {
    if (handle == STDIO_HANDLE_STDOUT || handle == STDIO_HANDLE_STDERR) {
        stdio_put_string(buffer, length, false, false);
        return length;
    }
    return -1;
}        

まとめ

arm-none-eabiは特定のOSを想定していないコンパイラです。 そこへLinux用の標準Cライブラリをポーティングしたため本来であればOSに存在しているシステムコールを呼び出すところでundefined referenceエラーになってしまいます。 商用の組み込み用コンパイラでは標準Cライブラリの標準的な関数をサポートしつつも その最下層でシステムコールを呼び出すことになる部分は組み込みでも実装できる程度の限られた簡単なものだけにするか、 ユーザーが実装しなければスタブとして無害な関数がリンクされるようになっているものが多いようです。

もし将来は自分でOSを作ってみたいと考えているような人であれば、 システムコールを自作してみるというのも面白いでしょう。 まずはシステムコールの中身を空っぽのスタブとして実装する例がNewlibのドキュメントで解説されていますので この辺から試してみることをおすすめします。

sourceware.org

関連リンク

Arm用GCCでのコンパイルオプションの選び方

組み込みで使用するARM用クロスコンパイラであるarm-none-eabi-gccを使用する際に必要となる ARM固有のオプションについて紹介します。

Downloads | GNU Arm Embedded Toolchain Downloads – Arm Developer

コンパイラオプションがとる引数の詳細はGCCの公式でご確認ください。

ARM Options (Using the GNU Compiler Collection (GCC))

ARM用GCCに伝えるべきこと

arm-none-eabi-gccは組み込み用であるた細かいことをいろいろとオプションで指定しなければ正しく動作するバイナリを生成することができません。 ここはホストで動作するLinuxgccのようにあらゆるオプションが既に合わせこまれているGCCとは大きく違うところです。

arm-none-eabi-gccに伝えておかなければならないことには次のようなことがあります。

ARMアーキテクチャの指定

まずはアーキテクチャの指定から始めます。 アーキテクチャを指定する方法には-marchを使う方法と-mcpuを使う方法があります。 -mcpuはそれを1つ指定するだけで-marchとさらに関連する-mtuneやアーキテクチャ拡張をまとめて指定したときと同じ効果があります。 -mcpuと-march等を同時し指定した場合は校舎での指定が優先します。

またアーキテクチャには多くの拡張があり、 対象としているチップのコアやOSがサポートしているアーキテクチャ拡張にGCCの想定外のものがあれば、そらも同時に指定します。

(例) -mcpu=cortex-a9
(例) -march=armv7-a -mtune=cortex-a9

(例) -mcpu=cortex-a9+nosimd
(例) -march=armv7-a+simd -mtune=cortex-a9

命令セットの指定

ARMにはARM命令とThumb命令があり、どちらの命令セットでコードを生成するかを指定します

(例) -marm
(例) -mthumb

VFPの指定

浮動小数点演算器はアーキテクチャ拡張としてして指定できますが、 他のCPU用のGCCを同じように-mfpuオプションを使用して指定することもできます。

(例) -mfpu=vfpv3-d16
(例) -mfpu=neon

浮動小数点ABIの指定

浮動小数点ABIは関数の引数として浮動小数レジスタを使用する場合の受け渡し方法になります。 ここは自由に決めることはできず、リンクするライブラリやOSと同じにしなければなりません。

浮動小数点ABIは-mfloat-abiでsoft, softfp, hardの3種類から1つを指定します。 softは浮動小数点命令も浮動小数レジスタも無し、softfpは浮動小数点命令は生成するが関数の引数では整数レジスタを使用、 hardは浮動小数点命令を生成し関数の引数にも浮動小数レジスタを使用します。

関数での引数の受け渡しとは関係なく-mfloat-abiでの指定は単純にコンパイラ浮動小数点命令を生成するかどうかにも影響します。 float型やdouble型を使用しいければ浮動小数点命令が生成されないという保障はなく、 組み込みOSには浮動小数点命令自体に対応していないものもありますので注意が必要です。

(例) -mfloat-abi=hard

古いアーキテクチャやABIに由来するオプション

古いARM用プロジェクトのコンパイラオプションには今となっては何のためにあるのか分からないものがあります。 そのようなオプションをいくつか取り上げます。

アラインメントの指定

-maligned-access -mno-unaligned-accessによってワード界を跨いだメモリアクセスの生成を有効または無効にすることができます。 このオプションは通常アーキテクチャに連動してコンパイラが自動的に決定します。

Abort例外の発生を回避するために-mno-unaligned-accessを使用することは、 重大なシステム設定の間違いを温存してしまう危険性があるためどうしてもやむを得ないとき以外はやめましょう。

インターワーキングの指定

ARMv5Tよりも古いARMアーキテクチャでARM命令のコードとThumb命令のコードを一緒にリンクすることがある場合に -mthumb-interworkによってインターワーキングを有効にします。

Documentation – Arm Developer

ただしarm-none-eabiを使うのであればデフォルトでABIがAAPCSになっているためこのオプションは無効です。 かつてIAR CにARMv5T以降にもインターワーキングのオプションがあったのはEABIではない独自ABIだったからではないかと思います。

プラットフォームに由来するオプション

arm-none-eabi-gccには標準CライブラリとしてNewlibがポーティングされていますが、これがすべてそのまま正常に動作するようなプラットフォームは組み込みの規模では無理であり、動作するところだけを使うか、あるいは別途提供される標準Cライブラリに差し替える必要がありそうです。またスタートアッププログラムもBSP等として提供されているものやCMSISをカスタマイズしたものを用意しなければならないはずです。

ここでその詳細に触れることはできませんが、そういう意味では--sysroot -nostdlib -nostartfiles のようなオプションの設定もおそらく必要になるでしょう。

まとめ

一緒に使おうとするOSやライブラリがバイナリで提供されている場合は原則としてそれと同じオプションを選択します。 自由に決めてよいときはまずCPUのコア構成を調べるところから始めるとよいでしょう。

GCCは歴史がありレガシーなオプションがたくさん残っていますので、今から始めようとするとある程度歴史の勉強が必要になることもあります。

細かいコンパイラオプションがたくさんついているのを見かけたときは、EABI用としてコンパイルされていないGCCを転用していないかや、OSやブートローダー等にソフト的な制約がないかを確認するとよいでしょう。

100と99.999…は等しいか否か? ~組み込みの場合~

数学では100と99.999…は等しい数になるそうです。 厳密な証明は私には難しいのですが次のように理解できます。

3分の1してから3倍すると元の数と等しい数になる。だとすれば、 3分の1したときに少数で書き直したものを3倍した数も元の数と等しい。


\frac{100}{3} = 33.333\cdots
\\
3 \times \frac{100}{3} = 3 \times 33.333\cdots
\\
100 = 99.999\cdots

ところがある日そうではない事件が発生したのです!

100.00MHzの水晶と99.99MHzの水晶はまったく別物だった件

数学の世界では100と99.99…は同じ数なのですが、 電子回路に載せる水晶振動子では100.00MHzと99.99MHzはまったく別物だったのです!

ある日のこと、部品表に載っていた100MHzの水晶の見積書を先輩に見せたところ「あり得ない値段」と怪訝な顔。 そこで商社さんに見積もりの確認をお願いすると「100MHzの水晶は特注品になります」というご回答。「聞き違えたか?」と思いつつもそのことを先輩に報告にいくとその先輩、何が起こっているのかそこで察しがついたらしく「100MHzの水晶というのは無いんだ。99.99MHzの水晶を発注しなさい」とのお言葉。 99.99MHzで度見積もりを取り直し事なきを得ることができましたとさ。

水晶というのは結晶軸に対する切り出し角度で振動数が決り、 よくあるのが33.33…MHzで切り出した系列のオーバートーンの製品だったらしいのです。 100.00MHzもあるのですが25.00MHz系列の製品はその時はあまり流通していなかったようです。

第4回 水晶振動子の発振周波数はどう決まるのか:水晶デバイス基礎講座 - EE Times Japan

そういえば16MHzとして売られている水晶も実は16.666…MHzのことがあって、 自分が昔使っていた16MHzのパソコンが実は16.67MHz だと知らされたときなんだかとっても得した気分になったことがありました。

#define HZ 33000000 ちょっと待った!

組み込みでプログラムの仕事をしているとPLLにクロック分周の設定をする機会があります。 そこで見かけたのが次のクロック周波数の定義。

#define HZ 33000000ul

「こっ、これはもしや若いときに経験したあの事件と同じパターンでは!」と咄嗟に悟り、 急いで確認するとやはり33333333MHzが正しい値でした。

ちゃんとulがついているあたりおっちょこちょいな人が間違えたわけではなさそうですが、ハード屋さんでもなければカタログに33MHzと書いてあったらそれを疑う人はまずいません。

#define HZ 33333333 にすれば正しいのか?

しかし33333333で良いかというとこれも少し引っかかるところがあります。

定義が無限ではなくて有限なのです!

そのせいで3倍しても100000000にならず99999999になります。 カーネルのログに99999999MHzって出てるのを見るとなんか違和感ありますよね? そういう理由だったのです。

そこで何かよい方法がないかと考えたのが次のマクロ定義

#define HZ 100000000ul/3ul

これだとプログラムの中で3倍したときちゃんと100000000になります。ヤッター!

・・・しかしこれを先ほどのulの人にレビューしてもらうと「ちゃんと括弧で括りましょう」と指摘を受けてしまいそうです。

#define HZ (100000000ul/3ul)

確かにどこでどう使われるかわからないマクロを演算子を剥き出しのまま定義するマナーのソフト技術者はいません。除算の演算子C言語の算術演算の演算子の中では優先度高い方なので、このままでもおかしなバグになることは無さそうですがやめておくべきでしょう。 でも括弧で括ると折角のアイデアもご破算です。 何か上手い方法はないものでしょうかね?

undefined reference to `_exit' の原因

組み込みでソフトウェアを開発していると身に覚えがない関数が無くてリンクできないことがあります。 その関数はいったいどこで何のために必要とされているのでしょうか?

undefined reference to `_exit'

試しにarm-none-eabi-gccでmainだけのプログラムをコンパイルしてみると _exitが未定義でリンクできないという理由でエラーになります。

int main()
{
  return 0;
}  

$ arm-none-eabi-gcc main.c e:/msys64/mingw64/bin/../lib/gcc/arm-none-eabi/8.4.0/../../../../arm-none-eabi/bin/ld.exe: e:/msys64/mingw64/bin/../lib/gcc/arm-none-eabi/8.4.0/../../../../arm-none-eabi/lib\libc.a(lib_a-exit.o): in function `exit': exit.c:(.text+0x2c): undefined reference to `_exit' collect2.exe: error: ld returned 1 exit status

_exitを必要とするのは誰なのか

C言語のプログラムはmainから実行されると習ったと思いますが、 実際にはmainの手前にスタートアップと呼ばれるプログラムがリンクしていて、 プログラムの実行はこのスタートアップから始まります。

スタートアップではまずシステムやプログラム全体の初期化を行いその後でmainを実行します。 またmainから戻った後プログラム全体の終了処理もするのですが、 ここで_exitが実行されます。

何故_exitが残っているのか

スタートアップがmainを実行する前の初期化をしてくれるのであれば、ついでに実行後の終了処理も全部やってくれればいいのにと思いたくなるのが正直なところです。 しかしながらもともとLinux等のリッチなOSが存在することを前提に開発されたスタートアップを組み込み用にポーティングする際に、 _exitは組み込みでどうすべきか一概に決められないというやむにやまれぬ理由があり、ここの実装はユーザー任せになっています。

_exitを実装する

それではとりあえずプログラム全体をリンクするために自分で _exitを実装することにします。 そこで_exitで何をすべきかを考えるのですが、そもそも組み込みでmainから戻ることがあるのでしょうか? 仮にプログラムを終了することがあったとしても、そのために必要な処理はmainから戻ることなく実行されるのではないでしょうか?

というわけで今回の_exitは次のように空っぽのプログラムとして実装することにします。

#include <stdnoreturn.h>

_Noreturn
void _exit(int code)
{
  while(1);
}

gccでは_exitは_Noreturnで宣言されているようなので、コンパイル時に警告されることを防ぐため同様に定義しています。

これで無事リンクできるようになりました。

これで実行可能なのか

ではこのプログラムが実際にターゲットボードで実行可能かというとそういうことは普通ありません。 組み込みではスタートアップは製品仕様に合わせて自分で実装するのが原則です。しかしそれはあまりに難しいため一般的にはCPU、ボード、コンパイラRTOS等何かしらのベンダーから提供されたものをノーチェックで使っているのが現状です。これを本当にそれで正しいと保障できるようになるためにはマイコンプログラミング言語についてもっとディープに勉強する必要があると思う今日この頃です。

関連記事

OpenVGで画像を狙いどおり表示する方法

OpenVGで画像を表示するとき単に座標だけを指定して表示しているときは問題ないのですが、拡大や回転を加えると思いどおり表示されなくなることがよくあります。 その原因をOpenVGの内部的な計算方法から探り、最終的に画像を意図どおり自由自在に表示できるように頭の中を整理します。

行列変換は順序が大事

OpenVGでは行列変換を使って画像を描画しているということは既にどこかで見聞きしたことがあると思います。 一般的な2Dグラフィックスでは画像を表示する座標を指定したうえでて回転して表示しようとするときこれらは互いに独立したパラメータになっていて設定の順序を入れ換えても結果は同じになります。 ところがOpenVGでは座標と回転はどちらも行列変換の一要素として扱われるため設定順序を入れ換えると結果が変わってしまいます。 数学の授業で行列演算では必ずしも交換則が成り立たないと習ったとおりです。 このことはOpenVGを使いこなす上で常に意識しておくべきポイントになります。

OpenVGでの行列演算の仕組み

OpenVGでは画像変換やパス変換等の目的別にいくつか行列があり、その中から目的に合わせてどれか1つの行列を選択して使用します。 選択した行列はまず最初に単位行列に初期化します。 この後プログラム中で行列変換の関数が実行される度に現在選択している行列に対して乗算が起こり変換が合成されていきます。 そして描画の関数が実行されたときそれまで変換を合成してきた行列により画像上の点に対して変換が始まります。

この様子を下記のようなプログラムにより 画像上の点(x,y)が点(x',y')に変換されるとして式に表すと次のようになります。

vgSeti(VG_MATRIX_MODE, VG_MATRIX_IMAGE_USER_TO_SURFACE);
vgLoadIdentity();
vgTranslate(tx,ty);
vgRotate(rz);
vgScale(sx,sy);
vgDrawImage(img);

\begin{bmatrix}
1 & 0 & 0 \\
0 & 1 & 0 \\
0 & 0 & 1 \\
\end{bmatrix}
\begin{bmatrix}
1 & 0 & tx \\
0 & 1 & ty \\
0 & 0 & 1 \\
\end{bmatrix}
\begin{bmatrix}
cos(rz) & -sin(rz) & 0 \\
sin(rz) & cos(rz)& 0 \\
0 & 0 & 1 \\
\end{bmatrix}
\begin{bmatrix}
sx & 0 & 0 \\
0 & sy & 0 \\
0 & 0 & 1 \\
\end{bmatrix}
\begin{bmatrix}
x \\
y \\
1 \\
\end{bmatrix}
=
\begin{bmatrix}
x' \\
y' \\
1 \\
\end{bmatrix}

ここで重要なことはプログラムでは変換が移動(vgTranslate)、回転(vgRotate)、拡大(vgScale)の順序になっているのに対し、 式を見ると分かるように画像上の点(x,y)に対しては拡大、回転、移動といった具合にプログラムとは逆の順序で変換が作用しているということです。

ハードウェア構成上のメリットがあって画像に対して逐次変換せず、マトリクスとして溜め込んでから変換するようになっているため数学的にはやや直感的ではない使い方になっています。しかしこのことは重要なのでもしピンとこなかったとしてもOpenVGでは変換は順序を逆にしてプログラムするということを覚えておいてください。

画像は最初どこにあるのか

画像上の点(x,y)と言われてもそもそも画像が存在する座標が分からなければ始まりません。 OpenVGでは画像の最初の位置は画面の左下です。 例えばvgLoadIdentity()した直後にvgDrawImage()すると画面の左下に画像が表示されます。

画面の左下が座標系の原点になっていて、画像も最初は原点に位置するということです。

拡大・回転の原点をどこにとるか

画像の回転や拡大は大昔のスプライトベースの2Dグラフィックスのように画像単独で変形すると考えるのは間違いです。 OpenVGでは回転と拡大は行列変換の一要素ですので画像と行列変換の原点との関係を常に意識しなければなりません。

例えば風車が回転するときのように画像の中心を原点にして回転させたいのであれば、回転行列で回転させる前に画像の中心を座標系の原点、つまり画面の左下に予め移動させておく必要があります。目的の座標へは回転の後で移動させることになります。

まとめ ~それでもやはり難しい?~

ここまでの説明でOpenVGでの行列演算の仕組みとプログラムする上での注意点をまとめました。 OpenVGで画像を意図どおり表示できるようになるための基本はこれで十分です。

しかしながら実際にやってみると どこかでレガシーな2Dグラフィックスライブラリでの考え方から抜け切れずに分からなくなってしまうこともあるでしょう。 それでも基本に戻って落ち着いて考えればきっと解決することができると思います。

関連記事