Cコンパイラの学習

この記事

「低レイヤを知りたい人のためのCコンパイラ作成入門」を読んでCコンパイラ書いてみた感想などです。

実際書いたコードはここに置いています。

github.com

テキスト

www.sigbus.info

このテキストは結構前に公開されたもののようでいろんな人がブログに書いたりしていて評判もよく実際学びやすいと思います。

難しいところはあまりなく読んでいて自分でもコンパイラ書けそうと思えてきます。

実習的な内容で読んで自分でコードを書きながら進めるところがよかったです。

まず最小構成でコード生成まで作って、小さいステップでテストを書きながら機能を追加していくというやり方も取り組みやすいと思いました。

何かコンパイラ的なものを作ろうとしてパーサーだけ作ってやめてしまったり、 途中の実装を無駄に凝ってすすめられなくなった経験が少なからずあるのでコンパイラ以外を作る場合にも参考にしたいなあと思いました。

後半は少し難しくなっていく気がしますが そこまで読み進めて理解できていれば解決できるということで意図的に細かいことは書かれていないのかもしれません。

言語の選択

セルフホストするところまでやるのが切りがよさそうなのでやっぱりC言語でやるのがいいのかなと思います。

セルフホストを気にしない場合は好きな言語で書いていいと思います。

私はC言語をあまり書きたくなかったのと、サンプルと同じように書いてしまうとコピペになってしまうかなという気がして Go で書いてました。

Go 使うことにしたのは前に少し触って書き方を忘れそうなので Go の学習のためというのもありました。

ただよく知っていて調べずに書ける言語で書いていく方がコンパイラを学ぶことに集中できると思います。

Go で書いてよかったところや書きにくかったところを挙げてみます。(私が Go の書き方を知らないせいもあるかもしれません。)

よかったところ

  • 実装にあまり迷わなかった
  • C言語に近い構文は大体ある
  • スライスやマップが標準で使える
  • type が別名ではなく別の型
  • 関数から関数を返せる
  • 無名関数を使える

書きにくく感じたところなど

  • enumを簡単に文字列表示したい
  • 整数型のmin, maxが欲しい
  • スライスの比較が欲しい
  • mapにキーがあるかどうか調べる関数が欲しい

式と文

最初は四則演算などの式だけをコンパイルして、スタックには一つ値が残った状態になるのでこれを pop するという実装になっていました。

if などを導入したときにこの部分でバグって少しはまっていました。 文の場合は式と違ってスタックに値が残らない場合があるので常に pop するとスタックがずれて壊れます。

結局 IsExpr 関数を作って式だったら pop するような感じで修正しました。

最適化は今回は全然手をつけられていないですが 簡単な実装だと無駄なレジスタ、スタックの移動が発生してしまうので最適化でこの辺の無駄をなくしたりするのかなと思いました。

Cの型のパース

Cの型のパースは難しく感じました。

ただこのテキストで扱う型は限られているので最初からそんなに完全な実装で無くてもよくてここで長く悩む必要はないです。

配列、関数、ポインタなど型の修飾が適用される順番が読む順番と逆にになっているところが難しいような気がします。

ここはパーサーの戻り値に型を受け取って型を返す関数を使うと実装しやすかったと思います。

色々な整数型の扱い

int だけでなく char が出てきて、レジスタも64ビットの他に32ビット、16ビット、8ビット版を扱うあたりが難しかったです。

変数からレジスタに読み込むところと、変数に書き込む部分だけ変数型とレジスタのタイプを気にしておけば 途中計算は int にして大体よさそうな気がするんですがいまいちこのあたり理解できてません。

ステップ28

ステップ28 でテストをC言語で書き直すというところがあります。

書く前はそこまでできるのかちょっと疑問に思いましたがやってみると意外にできて感動がありました。

ただここでテストに使う式や構文をどうやってテストに入れて実行したり表示したりするのか最初わかりませんでした。

ここは書かれてないけど多分プリプロセッサでマクロを使って書くということになると思います。

プリプロセッサは実装していないので gccプリプロセスのみを処理してからコンパイルしました。

Blender でスクリプトを使ってレンダリング

概要

tkaaad97.hatenablog.com

この記事で 3D モデルのアニメーションをレンダリングして連番の画像を生成するようなことをしていました。

このときは手動でやっていたんですがアニメーションが複数種類あって、種類によってはモデルの向きを変えてレンダリングするものもあったため 手動でこれを切り替えてレンダリングするのはかなり面倒でした。

Blender では Python スクリプトで色々な操作をすることができるらしく、こうした作業は自動でできそうなため調べてやってみたという内容です。

使った Blender ファイルの構造

f:id:tkaaad97:20201027230014p:plain
Blender ファイル構造

キャラクターの 3D モデルが一つあって、アニメーションはそれぞれ別々のアーマチュアについています。

アニメーションを切り替えるときはアーマチュアモディファイアを変更するというやり方をしています。

アーマチュア別々になっているのは UE4 からエクスポートしたモデルをインポートしているためこのようになっています。

(この構造もちょっと変なのでスクリプトでアニメーションを一つのアーマチュアにまとめることもできるかもしれません。)

レンダリングに使ったスクリプトの内容

かなりやっつけで書いたスクリプトなので全然汎用性はありません。 オブジェクトの構造や名前が違うと動きませんが参考にスクリプトを載せておきます。

このスクリプトでやっていることは下のような操作です。

  • モデル側面配置でのレンダリング
    • ファイル出力パスの変更
    • シーンのフレーム数変更
    • 3D モデルの位置と向きを変更
    • アーマチュアモディファイアの変更
    • アニメーションレンダリング実行
  • モデル背面配置でのレンダリング
    • ファイル出力パスの変更
    • シーンのフレーム数変更
    • 3D モデルの位置と向きを変更
    • アーマチュアモディファイアの変更
    • アニメーションレンダリング実行
import bpy
import math

def render_animation(armature_name):
    bpy.context.scene.render.filepath = "//out/" + armature_name + "/"
    bpy.context.scene.frame_start = int(bpy.data.objects[armature_name].animation_data.action.frame_range[0] + 0.5)
    bpy.context.scene.frame_end = int(bpy.data.objects[armature_name].animation_data.action.frame_range[1] + 0.5)
    bpy.context.view_layer.objects.active = bpy.context.scene.objects["CommonerSK_2"]
    bpy.context.object.location[0] = -0.16
    bpy.context.object.location[1] = 0.0
    bpy.context.object.location[2] = 0.0
    bpy.context.object.rotation_euler[0] = 0.0
    bpy.context.object.rotation_euler[1] = 0.0
    bpy.context.object.rotation_euler[2] = math.pi * 0.5
    bpy.context.view_layer.objects.active = bpy.context.scene.objects["SkeletalMeshComponent0"]
    bpy.context.object.modifiers["ArmatureModifier"].object = bpy.data.objects[armature_name]
    bpy.ops.render.render(animation=True)

def render_behind_animation(armature_name):
    bpy.context.scene.render.filepath = "//out/" + armature_name + "_Behind/"
    bpy.context.scene.frame_start = int(bpy.data.objects[armature_name].animation_data.action.frame_range[0] + 0.5)
    bpy.context.scene.frame_end = int(bpy.data.objects[armature_name].animation_data.action.frame_range[1] + 0.5)
    bpy.context.view_layer.objects.active = bpy.context.scene.objects["CommonerSK_2"]
    bpy.context.object.location[0] = 0.0
    bpy.context.object.location[1] = 0.0
    bpy.context.object.location[2] = 0.0
    bpy.context.object.rotation_euler[0] = 0.0
    bpy.context.object.rotation_euler[1] = 0.0
    bpy.context.object.rotation_euler[2] = math.pi
    bpy.context.view_layer.objects.active = bpy.context.scene.objects["SkeletalMeshComponent0"]
    bpy.context.object.modifiers["ArmatureModifier"].object = bpy.data.objects[armature_name]
    bpy.ops.render.render(animation=True)

armature_names = [
  "ClimbEnd",
  "ClimbStart",
  "ClimbUp",
  "Die",
  "GetHit",
  "Idle",
  "JumpEnd",
  "JumpStart",
  "Roll",
  "RollBack",
  "Run",
  "Walk"
]

behind_armature_names = [
  "ClimbEnd",
  "ClimbStart",
  "ClimbUp"
]

for armature_name in armature_names:
  render_animation(armature_name)

for armature_name in behind_armature_names:
  render_behind_animation(armature_name)

CLI で実行

スクリプト実行は Blender 実行中に Scripting タブでもできますが、CLI から Blender を起動せずに実行することもできます。

オプションの渡し方がいまいちわかってないですが、下のような感じで実行できました。

blender --background -noaudio Model.blend --python render.py

CLI のマニュアルはこれのようです。

docs.blender.org

やりたい操作に対応する API の探し方

docs.blender.org

最初は API ドキュメントから探そうとしてたんですが量が多すぎてここから検索して探すというのは結構難しいです。

Blender を手動で操作した後に Scripting タブを開いて履歴を見ると対応する操作がわかるので、これを参考にするというのがやりやすそうでした。

このキャプチャ画像のように左下あたりに出ています。

f:id:tkaaad97:20201027230017p:plain
Scripting タブ

FFI のあれこれ

はじめに

少し前に FFI を使って Haskell から C++ の処理を呼び出すというのをやっていてなかなか大変だったのでこれについて書いておきます。

私がわからなくて調べたことなどあまり整理されてない雑多な内容になります。

FFI の情報

参考になりそうなページを貼っておきます。

wiki.haskell.org

xtech.nikkei.com

book.realworldhaskell.org

www.haskell.org

downloads.haskell.org

FFI を使ったところ

OpenGL であれこれ表示したりするコードを書いていて glTF をロードして3Dモデルを表示するというのをやっていました。 glTF で頂点データを圧縮することができる draco という拡張があります。 draco 拡張はどうも圧縮アルゴリズムが仕様としてあるわけではなくて draco 拡張を使う場合には draco ライブラリの関数を呼び出して頂点データをデコードする必要があるようです。

draco ライブラリは C++ で書かれていてこれを Haskell から呼び出すのには FFI が必要となりました。

実際の作業としては下のようなことをしましたが慣れないのもあってなかなか大変でした。

  • draco ライブラリから呼び出す必要がある処理を見つける
  • C言語でラッパーを作成
  • Haskell から呼び出す部分の実装
  • package.yaml の修正
  • CI でビルドできるようにする

FFIC++ 呼ぶ場合

Haskell からC言語の関数をインポートするのに foreign import ccall というのを使います。

(ccall の部分は呼び出し規約にあたるようで他に cplusplus とか jvm とかもあるようですが ghc には多分実装されてません。)

インポートしたC言語の関数は IO として使えて引数を渡して、戻り値を受け取ることができますが使える型には制限があります。

使える型はこのあたりに書かれてました。

https://hackage.haskell.org/package/base-4.14.0.0/docs/Foreign-Ptr.html#g:2

  • the argument types are marshallable foreign types, i.e. Char, Int, Double, Float, Bool, Int8, Int16, Int32, Int64, Word8, Word16, Word32, Word64, Ptr a, FunPtr a, StablePtr a or a renaming of any of these using newtype.
  • the return type is either a marshallable foreign type or has the form IO t where t is a marshallable foreign type or ().

構造体やクラスは値のままでは扱えないのでポインターにして受け渡す必要があります。

C++ はそのまま使えないので C でラップして使います。以下のものが必要になると思います。

それから C++ を使う場合はプロジェクトの設定で libstdc++ をリンクしたり cxx-options で C++ 用のオプションを指定する必要があります。

リソースの管理

FFI で生成したポインタはそのままでは GC で勝手に破棄されたりはしません。

https://xtech.nikkei.com/it/article/COLUMN/20080902/313965/

こちらに解説されているように newForeignPtr で Ptr から ForeignPtr を作って GC で回収されるときに破棄用の関数を呼ばせることができます。

https://qiita.com/tanakh/items/81fc1a0d9ae0af3865cb#with%E7%B3%BB%E9%96%A2%E6%95%B0

使い終わったらすぐに破棄する場合はこちらの記事のように bracket を使って with 関数を作ってあつかうと例外が投げられた場合にもリークしないので便利です。

Setup.hs でビルドできないか

使ったことないですが Setup.hs に書くことでビルド時に色々な処理を実行させることができます。

これを使って FFI でリンクするライブラリを一緒にビルドできないかと考えつきました。

しかし FFI を使っている色々なライブラリを見てもそういうことをしているプロジェクトはほとんどありませんでした。

この理由はいくつか考えられる気がしますが色々な環境に対応してビルドするのが難しいというのが一つの理由かなと思います。

それからライブラリによっては環境にインストールされている共有ライブラリをリンクするのが望ましいからという場合もあると思います。

draco の場合は Setup.hs で頑張ればなんとかビルドできなくもない気がするんですが Windows など色々な環境対応をしようとするとやっぱり大変そうです。

ほぼ自分しか使わないしビルドが少し複雑になってもまあいいかということで

draco は別にビルドして stack に --extra-include-dirs--extra-lib-dirs のオプションを指定するという感じにしています。

このあたりのオプションは stack.yaml でも指定できます。

ghc から使われるコンパイラなどの設定

ghc --infoコンパイラやリンカなどの設定が見れます。

ghc --print-libdir で表示されるディレクトリに settings というファイルがあってこれが設定ファイルのようです。

PIC

static ライブラリをビルドするときは普通は -fPIC のオプションは付けないらしいんですが ubuntu (18.04 と 20.04) で draco をビルドしてリンクして使おうとすると下のようなエラーが出てしまいました。

/usr/bin/ld.gold: エラー: /work/./third_party/draco/build/libdraco.a(kd_tree_attributes_decoder.cc.o): requires dynamic R_X86_64_PC32 reloc against 'stderr' which may overflow at runtime; recompile with -fPIC

Windows では -fPIC なしでも問題なくリンクできていました。 Windows 環境ではどうも gold ではなく ld が使われているようなのでリンカの違いによるかもしれません。 原因よく理解できてないですが -fPIC つけてビルドするとリンク成功したためビルド時には -fPIC つけるようにしています。

タイルマップ描画のメモ

はじめに

もうずいぶん前ですが Tiled Map Editor についてブログに少し書きました。

その後最近になって Tiled Map Editor で作ったマップを描画する部分を OpenGL で実装したりしていたためメモとして書いておきます。

描画の方法

タイルマップのデータを元にしてマス目状にタイルセットの画像を描画していくことになります。

基本的にはテクスチャを貼った三角形をたくさん表示できればいいはずです。

タイル1マスごとにドローコール呼び出したりすると処理に時間がかかってしまうのでまとめて描画するようにします。

大分基礎的なことなので詳しくは別の資料をみてもらった方がいいと思います。

下のページなどが参考になるかなと思います。

www.opengl-tutorial.org

描画順

タイルマップの種類によっては隣り合うタイルが重なりあっていることがあるので気を付ける必要があります。

デプスを使って制御することもできますが、頂点バッファを作るときに描画順に三角形をソートしておくという方法でも制御可能です。

デプスを使わなくてもいいようにしておくとパースペクティブで表示したりもできます (あまり必要ない気もしますが)。

下はパースペクティブで表示してみた画像です。

f:id:tkaaad97:20200831204817p:plain
タイルマップ

インスタンシング

インスタンシングという機能があって、これを使うと3Dモデルの位置や色などを変化させて大量に表示することができます。

インスタンシングについてこちらのページなどが参考になりそうです。

wgld.org

learnopengl.com

タイルマップ描画の場合は幅1で高さ1の四角形メッシュを位置、サイズ、UVを変えてたくさん表示するという感じになります。 インスタンシングで高速化するというよりは頂点バッファに持つデータの量を減らせて扱いやすくなるというのが利点かなと思います。

ちょっと検索してみたところタイルのインスタンシングだとパフォーマンスよくないという感じのことが書かれていました。

gamedev.stackexchange.com

しかし 2D ゲームで使うようなタイルマップのタイル数は多くないのでパフォーマンスで問題になることはそんなにないんじゃないかと思います。 モバイル端末の場合だと問題になったりインスタンシング自体対応していないということもあるかもしれません。

インスタンシングを使わない場合の頂点データは下のようになります。

インスタンシングを使う場合の頂点データは下のようになります。

  • アトリビュート
    • 位置座標
    • タイルサイズ
    • UV座標
    • UVサイズ
  • 頂点数
    • タイル数と同じ

タイルサイズとUVサイズが全体で固定の場合はユニフォームに持つこともできます。 この他に先に触れた幅1で高さ1の四角形メッシュを表す頂点バッファも必要です。

アニメーション表示するには

タイルマップによってはアニメーションするものもあります。 例えば海や川など水の表現ではアニメーションが使われることが多いです。 アニメーションを表示するには一定時間でUV座標を更新してタイル画像を切り替えることになります。

UV座標は頂点バッファに入っているので頂点バッファを定期的に書き換えるというのがやり方としては一番簡単かなと思います。

この他にアニメーションで切り替わるタイルの情報をユニフォームバッファに保持して 頂点バッファにはこれを参照するインデックスを持たせるというようなやり方もできます。 この場合はユニフォームバッファのタイル情報だけを更新するため更新が少なくて済みます。

3D モデルからドット絵生成

はじめに

ゲームの素材に使うようなドット絵のアニメーションを作ったりしたくて 3D モデルから生成するのを試してみました。

ドット絵の素材も売られてたりしますがなかなか自分の使いたいサイズやモーションの条件に合うものが見つかりませんでした。

3D モデルは Unity のアセットストアや Unreal Engineマーケットプレイスでたくさん販売されています。 また一部モーションがないようなときモーションだけ別のところから持ってきてリターゲットして使うということもできます。 Blender では深度も出力できるのでラインティングの効果に使ったりできるかもしれません。

前に Unreal Engineマーケットプレイスで無料でもらった 3D モデルがあったので これを使って Blender でドット絵生成するというのをやってみました。

UE4 からエクスポートして Blender にインポート

試したバージョンは以下です。

公式ドキュメントなどを見てもらった方が確実だと思うのでひっかかった点だけ書いておきます。

UE4 側で配置するときにトランスフォームの位置や回転を0にしておいた方がいいです。 Blender に読み込んだときにアニメーションの回転位置がずれてモデルが崩れてしまうことがありました。

それからメッシュに親子関係がついていて親のアニメーションに合わせて子も動くようなモデルは上手くとりこめませんでした。 この場合は Blender 側で親子関係を修正すると直せることがありました。 子のメッシュのオブジェクトプロパティから関係の設定を修正し、ペアレントを子のアーマチュアに設定します。 子のエンプティのペアレントを親の適切なボーンに設定します。

今回使わせてもらったモデルはこちらです。現在は無料ではなくなってます。

https://www.unrealengine.com/marketplace/ja/product/modular-rpg-heroes-polyart

このモデルはシンプルで色も少ないので向いてそうです。 複雑なモデルだと上手くいかないかもしれません。

ドット絵っぽくする方法

  • 解像度を下げる
  • 輪郭線を付ける
  • 色数を制限する

この三つを考えてやってみました。 Blender のシェーダーやコンポジットでかなり複雑なこともできるので他の効果も色々作れると思います。

シェーダー

シェーダーでは色数を制限する部分をやっています。 最初は全部コンポジットでやっていたんですがシェーダーでやる方がおそらく GPU が使えて速くなると思って作り直しました。

シェーダーは下のようになっています。 Blender 使いはじめたばかりなので使い方がおかしいところがあるかもしれません。

f:id:tkaaad97:20200723112156p:plain
シェーダー内容

ベースカラーが三段階の明るさでレンダリングされるようにしています。 今回使ったモデルは元々色数が少なく10色ぐらいが三段階で30色ぐらいになっています。

ベースカラーの色数が多いモデルの場合はテクスチャ画像を加工してパレット化して色数減らすなどできると思います。

コンポジット

コンポジットで解像度を下げるのと輪郭線を付けるのをやっています。

f:id:tkaaad97:20200723112348p:plain
コンポジットの内容

輪郭線用にシーンを分けてレンダリングしています。

輪郭を付けるのは freestyle の機能で簡単にできました。 シーンを分けているのは分けた方が輪郭がはっきり出やすいかなと思ったんですが実際はそんなに大きな違いはないかもしれません。

輪郭線の太さは解像度に合わせて調整する必要があります。

解像度を下げるのはピクセル化ノードでできます。

f:id:tkaaad97:20200723112427p:plain
ピクセル

https://docs.blender.org/manual/en/latest/compositing/types/filter/pixelate.html

ピクセル化ノードではアンチエイリアスは行われないので色数は変わりません。 ピクセル化ノードを使わず出力プロパティで低解像度にして、サンプリング数を1にするというのでも解像度下げることはできます。 ピクセル化ノード使う方が設定箇所が少ないのでいいかなと思って使っています。 ただ出力画像サイズは大きいままなので別にリサイズしてやる必要があります。

生成したドット絵

f:id:tkaaad97:20200723112927g:plain
ドット絵1

f:id:tkaaad97:20200723115203g:plain
ドット絵2

まあまあ上手くできている気がします。 完璧ではないですがこれを元に調整したりすれば素材として使えるかなと思います。

参考

www.youtube.com

dskjal.com

Lens パッケージの型の関係について

Lens パッケージの型の関係について

はじめに

たまに Lens を使うことがあって便利なんだけどよくわからないまま雰囲気で使っているところがあって、もう少し理解したいなという気がしていました。

なかなか難しくていまだに大半は理解できてないんですが Lens の型の図の意味が少し分かってきた気がするのでそれについて書きます。

使い方などについてはこの記事では触れません。

関係図

https://github.com/ekmett/lens/blob/b6a237453192f2b464f17e02929b57542a936b14/images/Hierarchy.png?raw=true

こういう図があってオブジェクト指向のクラス図のような感じなんですが、 Haskell にはクラスも継承もないのでこの矢印はどういう意味になっているのかよく分かってませんでした。

型の関係

Lens と Traversal を比べてみます。

type Lens s t a b = forall f. Functor f => (a -> f b) -> s -> f t

type Traversal s t a b = forall f. Applicative f => (a -> f b) -> s -> f t

図だと Lens の方が下にあって Lens から Traversal に矢印が伸びています。

しかし上の型を見ても二つがどういう関係にあるのか自分にはすぐ理解できませんでした。

比較1

簡単な例から考えてみることにします。

type E1 a = forall f. Functor f => f a

type E2 a = forall f. Applicative f => f a

E1 は Functor 制約が付いていて、E2 には Applicative 制約が付いています。

http://hackage.haskell.org/package/base-4.12.0.0/docs/Control-Applicative.html#g:1

Functor は Applicative のスーパークラスになっているので

  • Applicative であれば Functor でもある
  • Functor は Applicative ではない場合もある
  • E2 a の値は E1 a の値でもある
  • E1 a の値の数は E2 a の値の数より多い

のような感じのことが考えられると思います。

こう書くのが正しいかわかりませんが E2 aE1 a の部分集合になっていると思います。

$$ E1 \ a \supset E2 \ a $$

比較2

次に関数の引数に制約が付いている場合を考えてみます。

type E3 a b = forall f. Functor f => f a -> b

type E4 a b = forall f. Applicative f => f a -> b

E3 は Functor 制約の付いた引数を受け取る関数、E4 は Applicative 制約の付いた引数を受け取る関数です。

Functor の値の方が多いので E3 の方が入力となる値が多く、 E4 のほうが入力になる値が限定されていて関数が満たす必要がある要求が小さいというふうに考えられます。

この場合は比較1の例と逆に E3 a bE4 a b の部分集合になります。

$$ E3 \ a \ b \subset E4 \ a \ b $$

比較3

制約が付いていない引数が付いた場合です。

type E5 a b = forall f. Functor f => a -> f b

type E6 a b = forall f. Applicative f => a -> f b

この場合は E5 と E6 の入力になる値は同じだけあって、関数が満たす必要がある要求は変わりません。

関係を見る上ではこの引数は無視できるので比較1の場合と同じになります。

$$ E5 \ a \ b \supset E6 \ a \ b $$

比較4

Lens と Traversal の場合に戻ります。

type Lens s t a b = forall f. Functor f => (a -> f b) -> s -> f t

type Traversal s t a b = forall f. Applicative f => (a -> f b) -> s -> f t

無視できる引数を消してみます。

type Lens_ t b = forall f. Functor f => f b -> f t

type Traversal_ t b = forall f. Applicative f => f b -> f t

引数と戻り値の型の両方に制約が付いているんですが、この場合は引数と同じ制約を付けた型を返す関数なので引数の方だけ見るということでいいと思います。

結局 Lens と Traversal の場合は比較2と同じように考えられると思います。

Lens は Traversal の部分集合になっているようです。

$$ Lens \ s \ t \ a \ b \subset Traversal \ s \ t \ a \ b $$

まとめ

矢印は部分集合の関係にあることを表していると思います。

Lens の図の上の方に行くほど関数の入力が限定されて関数は作りやすくなり、集合に含まれる関数は多くなります。

下の方に行くほど関数の入力になる値が多くなり、関数は作りにくくなり、集合に含まれる関数は少なくなります。

一番下にある Equality は他の型の全ての部分集合になっていて、Equality の値である id は他の全ての型として使ったり、合成することができます。

OpenGL のユニフォームブロックについて

はじめに

OpenGL のシェーダにデータを受け渡す方法の一つにユニフォームがありますが、 ユニフォームを少し拡張したような機能でユニフォームブロックというものもあってこれについて調べたことについて書きます。

仕様

ユニフォームブロックにはユニフォームバッファオブジェクトを使うことでまとめてユニフォームを受け渡すことができます。

ライトやカメラの情報など複数のシェーダで使う情報を受け渡すときに便利なのではないかと思います。

しかしバッファにユニフォームのデータを書き込むときにはユニフォームブロックのメモリレイアウトについて考慮する必要があって、この部分が少し難しく間違いやすい気がします。

メモリレイアウトの種類として shared, packed, std140 があります。デフォルトは shared のようです。std140 を使うことが多いと思うのでこれのみ調べています。

OpenGL 仕様の 7.6.2.2 Standard Uniform Block Layout のあたりにかかれていました。

https://www.khronos.org/registry/OpenGL/specs/gl/glspec45.core.pdf

std140 のメモリレイアウト仕様の引用

  1. If the member is a scalar consuming N basic machine units, the base alignment is N.
  2. If the member is a two- or four-component vector with components consuming N basic machine units, the base alignment is 2N or 4N, respectively.
  3. If the member is a three-component vector with components consuming N basic machine units, the base alignment is 4N.
  4. If the member is an array of scalars or vectors, the base alignment and array stride are set to match the base alignment of a single array element, according to rules (1), (2), and (3), and rounded up to the base alignment of a vec4.
  5. If the member is a column-major matrix with C columns and R rows, the matrix is stored identically to an array of C column vectors with R components each, according to rule (4).
  6. If the member is an array of S column-major matrices with C columns and R rows, the matrix is stored identically to a row of S × C column vectors with R components each, according to rule (4).
  7. If the member is a row-major matrix with C columns and R rows, the matrix is stored identically to an array of R row vectors with C components each, according to rule (4).
  8. If the member is an array of S row-major matrices with C columns and R rows, the matrix is stored identically to a row of S × R row vectors with C components each, according to rule (4).
  9. If the member is a structure, the base alignment of the structure is N, where N is the largest base alignment value of any of its members, and rounded up to the base alignment of a vec4.
    The individual members of this substructure are then assigned offsets by applying this set of rules recursively, where the base offset of the first member of the sub-structure is equal to the aligned offset of the structure.
    The structure may have padding at the end; the base offset of the member following the sub-structure is rounded up to the next multiple of the base alignment of the structure.
  10. If the member is an array of S structures, the S elements of the array are laid out in order, according to rule (9).

訳してみたもの

  1. スカラーでNマシンユニットを消費する場合アラインメントはN。
  2. 2または4要素のベクトルで要素がNマシンユニットを消費する場合、アラインメントはそれぞれ2N、4N。
  3. 3要素ベクトルで要素がNマシンユニットを消費するときのアラインメントは4N。
  4. 配列のアラインメントは要素から計算される。ただしvec4で切り上げられる。
  5. 列優先C列R行行列はルール4からR要素ベクトルのサイズCの配列と同じ。
  6. 列優先C列R行行列のS要素配列の場合、R要素ベクトルを要素とするサイズS x Cの配列と同じ。
  7. 行優先C列R行行列の場合、C要素ベクトルのサイズRの配列と同じ。
  8. 行優先C列R行の行列S要素配列の場合、C要素ベクトルを要素とするサイズS x Rの配列と同じ。
  9. 構造体の場合、アラインメントは構造体のメンバーのうち一番大きいもののアラインメントになる。ただしvec4アラインメントに切り上げられる。
    構造体の個々のメンバーはこのルールを再帰的に適用され決まったオフセットの位置に配置される。構造体の最初のメンバーの配置オフセットは構造体自身のオフセットに一致する。
    構造体は末尾にパディングを持つ場合がある。構造体に続くメンバーのベースオフセットは、構造体のベースアライメントの次の倍数に切り上げられる。
  10. 構造体を要素とするサイズSの配列の場合、ルール9によって順番に配置される。

スカラー型は bool, float, int, uint, double があります。

スカラー型のアラインメントとバイトサイズは下のようになっています。

uniform type alignment byte size
bool 4 4
float 4 4
int 4 4
uint 4 4
double 8 8

それから行列のメモリレイアウトの列優先 (column-major)、行優先 (row-major) は glsl 側で指定できて column_major または row_major で指定します。 デフォルトは列優先です。

https://www.khronos.org/opengl/wiki/Interface_Block_(GLSL)#Matrix_storage_order

調べ方

仕様を読んだり google で検索したりしてたんですがはっきりした情報が見つからずコードを書いて実際にどうなるかみてみることにしました。

https://github.com/tkaaad97/uniformblock-experiment

使ったコードはここのリポジトリに置いています。

適当なユニフォームブロックが入ったシェーダーを書いてこれを OpenGLコンパイルし、ユニフォームの情報を取得して調べるという方法で試しました。

下のような感じでブロック一つ目のフィールドをアラインメントが一番小さい float にして二つ目のフィールドを調べたい型にし、三つ目を float にしてオフセットを取得することでアラインメントとサイズが多分わかると思います。

layout (std140) uniform ublock {
    float uniform1;
    #{uniformType} uniform2;
    float uniform3;
} ublock;

https://www.khronos.org/registry/OpenGL-Refpages/gl4/html/glGetActiveUniformsiv.xhtml

ユニフォームのパラメーターは glGetActiveUniformsiv で取得できます。

パラメーターの種類は GL_UNIFORM_SIZE, GL_UNIFORM_NAME_LENGTH, GL_UNIFORM_BLOCK_INDEX, GL_UNIFORM_OFFSET, GL_UNIFORM_ARRAY_STRIDE, GL_UNIFORM_MATRIX_STRIDE, GL_UNIFORM_IS_ROW_MAJOR GL_UNIFORM_ATOMIC_COUNTER_BUFFER_INDEX です。

また構造体についてはパターンが色々あって網羅できないためこの方法では調べていません。 他の部分が分かれば構造体については仕様のルールから理解できるはずと思います。

結果

下のような結果になりました。 仕様の文章からわかる結果だと思いますが、確証が持てなかったのではっきりしてよかったです。

uniform type alignment byte size array stride matrix stride matrix order
bvec2 8 8
bvec3 16 12
bvec4 16 16
vec2 8 8
vec3 16 12
vec4 16 16
ivec2 8 8
ivec3 16 12
ivec4 16 16
uvec2 8 8
uvec3 16 12
uvec4 16 16
dvec2 16 16
dvec3 32 24
dvec4 32 32
float[4] 16 64 16
vec2[4] 16 64 16
vec3[4] 16 64 16
vec4[4] 16 64 16
dvec2[4] 16 64 16
dvec3[4] 32 128 32
dvec4[4] 32 128 32
mat2 16 32 16 row
mat2x3 16 48 16 row
mat2x4 16 64 16 row
mat3 16 48 16 row
mat3x2 16 32 16 row
mat3x4 16 64 16 row
mat4 16 64 16 row
mat4x2 16 32 16 row
mat4x3 16 48 16 row
dmat2 16 32 16 row
dmat2x3 16 48 16 row
dmat2x4 16 64 16 row
dmat3 32 96 32 row
dmat3x2 32 64 32 row
dmat3x4 32 128 32 row
dmat4 32 128 32 row
dmat4x2 32 64 32 row
dmat4x3 32 96 32 row
mat2[4] 16 128 32 16 row
mat2x3[4] 16 192 48 16 row
mat2x4[4] 16 256 64 16 row
mat3[4] 16 192 48 16 row
mat3x2[4] 16 128 32 16 row
mat3x4[4] 16 256 64 16 row
mat4[4] 16 256 64 16 row
mat4x2[4] 16 128 32 16 row
mat4x3[4] 16 192 48 16 row
dmat2[4] 16 128 32 16 row
dmat2x3[4] 16 192 48 16 row
dmat2x4[4] 16 256 64 16 row
dmat3[4] 32 384 96 32 row
dmat3x2[4] 32 256 64 32 row
dmat3x4[4] 32 512 128 32 row
dmat4[4] 32 512 128 32 row
dmat4x2[4] 32 256 64 32 row
dmat4x3[4] 32 384 96 32 row
mat2 16 32 16 column
mat2x3 16 32 16 column
mat2x4 16 32 16 column
mat3 16 48 16 column
mat3x2 16 48 16 column
mat3x4 16 48 16 column
mat4 16 64 16 column
mat4x2 16 64 16 column
mat4x3 16 64 16 column
dmat2 16 32 16 column
dmat2x3 32 64 32 column
dmat2x4 32 64 32 column
dmat3 32 96 32 column
dmat3x2 16 48 16 column
dmat3x4 32 96 32 column
dmat4 32 128 32 column
dmat4x2 16 64 16 column
dmat4x3 32 128 32 column
mat2[4] 16 128 32 16 column
mat2x3[4] 16 128 32 16 column
mat2x4[4] 16 128 32 16 column
mat3[4] 16 192 48 16 column
mat3x2[4] 16 192 48 16 column
mat3x4[4] 16 192 48 16 column
mat4[4] 16 256 64 16 column
mat4x2[4] 16 256 64 16 column
mat4x3[4] 16 256 64 16 column
dmat2[4] 16 128 32 16 column
dmat2x3[4] 32 256 64 32 column
dmat2x4[4] 32 256 64 32 column
dmat3[4] 32 384 96 32 column
dmat3x2[4] 16 192 48 16 column
dmat3x4[4] 32 384 96 32 column
dmat4[4] 32 512 128 32 column
dmat4x2[4] 16 256 64 16 column
dmat4x3[4] 32 512 128 32 column