3D レンダラの概念

ヘッダ: shapoco/gfx3d/gfx3d.hpp

描画の流れ

  1. init() で画面サイズと作業メモリ (アリーナ) を与えます。 細かい調整をする場合は defaultConfig() で得た Config を書き換えて渡します。

  2. 毎フレーム beginScene() 〜 endScene() の間にカメラ、ライト、プリミティブを投入します。 この段階で頂点の変換・ライティング・投影まで行い、結果を内部の三角形バッファに保持します。

  3. beginRender() で三角形を奥から順にソートします。

  4. render(x, y, w, h, dst, dstX, dstY) で画面の矩形領域をラスタライズして Surface に書きます。 領域を分けて何度呼んでも構いません。

  5. endRender() を呼びます。

g3d.beginScene();
g3d.lookAt({0, 3, 8}, {0, 0, 0});
g3d.enableParallelLight({-0.5f, -1, -0.6f}, {1, 1, 1, 1});
g3d.setMaterial(mat);
g3d.putSphereUV({0, 0, 0}, 1.0f);
g3d.endScene();

g3d.beginRender();
for (int y = 0; y < H; y += BAND_H) {
  g3d.render(0, y, W, BAND_H, band);   // 帯ごとに描いて転送
  display.write(0, y, band);
}
g3d.endRender();

フレームバッファも Z バッファも持たないので、RAM は「アリーナ + 転送バッファ」だけで済みます。

座標系

  • OpenGL 互換の右手系です。ビュー空間ではカメラは -Z を見ます。

  • スクリーン空間は左上原点、y は下向きです。

  • 表面はスクリーン空間で反時計回り (CCW) です。既定ではバックフェイスカリングが有効で、 MaterialFlags::DOUBLE_SIDED で無効化できます。

  • 角度は全てラジアンです。

  • 現在の変換行列は loadIdentity() / translate() / rotate() / scale() / transform() / lookAt() で操作し、 pushState() / popState() で保存・復元します (深さ 16)。マテリアルもスタックに含まれます。

  • カメラは専用の API ではなく、lookAt() (または平行移動・回転) で現在の行列に組み込みます。 ライトの向き enableParallelLight(dir, col) は呼び出し時点の行列で変換されるので、 カメラ設定の後、モデル変換の前に呼べばワールド空間の向きとして扱えます。

ライティングとマテリアル

グーローシェーディングです。頂点ごとに次の色を求め、線分内で線形補間します。

color = ambient * environmentLight                 (環境光が有効なとき)
      + diffuse * parallelLight * max(0, n . -L)   (平行光源が有効なとき)
  • どちらのライトも無効なら diffuse がそのまま頂点色になります。

  • MaterialFlags::VERTEX_COLOR が立っていると、上の結果に Vertex::color (RGB) を乗じます。

  • テクスチャがあれば、頂点色でテクセルを変調します。

  • BlendMode::ADD では色に不透明度をあらかじめ乗じてから加算します。

  • 環境マッピング (MaterialFlags::ENV_MAP) はビュー空間の法線 n から u = 0.5 + 0.5 n.x, v = 0.5 - 0.5 n.y でテクスチャを参照します。

  • SHAPOGFX3D_GOURAUD=0 でビルドすると、プリミティブの最初の頂点の色をそのまま全体に使います (フラットシェーディング)。滑らかな曲面は面ごとの色になります。

  • 頂点法線は単位ベクトルにしてください。現在の行列の 3x3 部分が回転と一様スケールの積なら (通常はそうです)、ライト方向をプリミティブごとに 1 回モデル空間へ変換し、頂点ごとの計算は 内積 1 回で済みます。非一様スケールやせん断がある場合と環境マッピングでは、従来どおり 法線をビュー空間へ変換して正規化します。

半透明

マテリアルの blendMode が NONE 以外、またはテクスチャが ARGB4444 の三角形は半透明として扱われます。

  • 不透明な線分は x 順のリストで管理され、重なった部分は奥側が削除されます。 どちらが手前かは、レイヤが違えば後のレイヤ、深度を持たないレイヤの中では後から投入した方、 それ以外は深度比較で決まります。 レイヤ内では三角形単位のソートに頼らないので、大きなポリゴンと小さなポリゴンの前後関係も正しく出ます。

  • 半透明な線分は奥から順のリストで管理され、不透明部分を描いた後にブレンドモードに従って合成されます。

  • ARGB4444 テクスチャではテクセルの α がピクセル単位で不透明度に乗算されます。 α = 0 のテクセルは描かれないので、抜きのあるスプライト的な板ポリが作れます。

点と線

POINTS / LINES / LINE_STRIP / LINE_LOOP は三角形と同じ putPrimitive() で描けます。

  • ライティングなし: 頂点色は diffuse (VERTEX_COLOR なら Vertex::color を乗算) です。 テクスチャ、環境マップ、バックフェイスカリングは適用されません。

  • 太さ: 線は 1 px 固定、点は setPointSize() の正方形 (既定 1 px) です。

  • 被覆: 線は Bresenham と同じピクセルを塗ります (立った線は 1 行 1 px、寝た線は 1 列 1 px)。両端点は描かれます。

  • ニア平面: 線はビュー空間でクリップされ、カメラの後ろへ伸びる線分も手前の部分だけ描かれます。点は手前なら破棄。

  • 隠線消去: 線と点も通常のスパンとして深度解決に参加するので、手前のポリゴンに隠れた部分は消えます。 半透明 (ALPHA / ADD) も同様に合成されます。

  • 深度バイアス: 塗りポリゴンの辺にワイヤーフレームを重ねると Z ファイトするので、 setDepthBias(-0.002f) のように少し手前へずらしてから線を描きます (三角形にも効きます)。

  • 点と線も三角形バッファを 1 レコードずつ消費します (テクスチャを持たないので小さいレコードです)。

g3d.setMaterial(matSolid);
g3d.putCube({0, 0, 0}, {1, 1, 1});
g3d.setMaterial(matWhite);
g3d.setDepthBias(-0.002f);
g3d.putWireCube({0, 0, 0}, {1, 1, 1});   // 12 本の辺
g3d.setDepthBias(0.0f);
g3d.putLine({-2, 0, 0}, {2, 0, 0});

レイヤ

シーンはレイヤの列です。後から開いたレイヤは、先に開いたレイヤより必ず手前に描かれます。 奥から順にレイヤを開くのはアプリケーション側の責任で、レイヤの中では通常どおり深度で前後関係が決まります。 beginLayer() を一度も呼ばないシーンは 1 枚のレイヤとして扱われ、従来どおりに描かれます。

  • beginLayer(flags) は現在のレイヤを閉じて新しいレイヤを開きます。 実際に確保されるのは最初のプリミティブが投入された時点なので、空のレイヤはコストになりません。 レイヤ数 (SHAPOGFX3D_LAYER_MAX、既定 8) を使い切っている場合、呼び出しは無視され Stats::layersDropped に数えられます (以降のプリミティブは現在のレイヤに入ります)。

  • endLayer() は現在のレイヤを閉じます。以降のプリミティブは既定フラグの新しいレイヤに入ります (それでも手前であることは変わりません)。

  • LayerFlags::NO_DEPTH を付けたレイヤは深度を一切持ちません。 投入順がそのまま前後関係になり (後のものが手前)、レコードから深度平面 (12 バイト、SHAPOGFX3D_DEPTH_BITS=16 なら 8 バイト) が省かれ、 ソートも行われません。既に奥から順に並んでいる背景などに向きます。 このレイヤでは setDepthBias() は効きません。

レイヤの前後関係は線分が重なった時点で解決されるので、深度比較では解決できないケース (奥行き方向に大きく広がるポリゴンと他のオブジェクトの交差など) もレイヤを分ければ正しく出せます。

g3d.beginScene();
g3d.beginLayer(g3::LayerFlags::NO_DEPTH);   // 奥から順に並べた背景
drawBackground(g3d);
g3d.beginLayer();                           // 前景は通常の深度解決
drawCharacters(g3d);
g3d.endScene();

背景

線分で覆われないピクセルは setClearColor() の色で塗られます。 disableClear() を呼ぶと塗らずに描画先の内容を残すので、Graphics2D で描いた背景の上に 3D を重ねられます (demo3d はこの方法で背景を描いています)。

透視補正

テクスチャ座標の補間方法はコンパイル時オプション SHAPOGFX3D_CORRECT_PERSPECTIVE で選びます。

レベル

動作

コスト

0

全てアフィン補間 (大きなポリゴンで PS1 風に歪む)

なし

1 (既定)

描く線分の最初と最後のピクセルを透視補正し、線分内部はアフィン補間

描く線分あたり 32 bit 除算 2 回と 32x16 bit 乗算 4 回 (+ 長さでの除算 2 回)、三角形あたり +12 バイト

2

完全補正。(u/w, v/w, 1/w) を補間して SHAPOGFX3D_PERSPECTIVE_STEP (既定 16) ピクセルごとに除算し、間は線形補間

テクスチャ付き 16 ピクセルあたり除算 1 回、三角形 +12 バイト

レベル 1 は、ロールのないカメラから見た水平な面 (床、天井) では歪みが完全になくなります。 スキャンラインに沿って奥行きが変わる面 (横に伸びる壁) では線分内部に歪みが残るので、 分割するかレベル 2 を使ってください。

RP2040 / RP2350 では SHAPOGFX3D_RP2_INTERP (Pico SDK の hardware_interp が使えれば既定で有効) により、 RGB565_SWAPPED、RGB565、ARGB4444 のテクスチャ (stride が 2 の冪のもの) のテクセルアドレス計算を SIO interpolator (interp0) に任せます。不透明・テクスチャなし・グーロー補間の線分 (RGB565_SWAPPED / RGB565 出力) では、 interp1 が赤と緑を歩進して RGB565 の赤緑部分を 1 回の読み出しで返します。 render() は呼び出したコアの interp0 と interp1 を保存・復元するので、render() 中に動く 割り込みハンドラではこれらを使わないでください。

省略できる機能

使わない機能はコンパイル時に外せます。コードだけでなく作業メモリも減るので、 同じアリーナにより多くの形状を保持できます。 外した機能に対応するメンバは実行時に無視されるだけなので (無効化したピクセルフォーマットと同じ扱い)、 フル機能向けに書いたシーンデータもそのままコンパイルでき、描画できます。

マクロ

0 にしたときの見た目

SHAPOGFX3D_TEXTURE

Material::texture と TEXTURE / ENV_MAP フラグが無視され、マテリアルの陰影付き単色で描かれる

SHAPOGFX3D_GOURAUD

プリミティブの最初の頂点の色を全体に使う。滑らかな曲面が面ごとの色になる

SHAPOGFX3D_BLEND

全て不透明に描かれる。ブレンドモードと ARGB4444 テクスチャの α が無視される

SHAPOGFX3D_LINES

線分プリミティブが描かれない (putLine() と putWireCube() も含む)

SHAPOGFX3D_POINTS

点プリミティブが描かれない

ただし、プリミティブは必要な属性だけを持つレコード形式で保持されるため、 機能を外さなくてもレコードは小さくなります。 例えばフル機能のビルドでも、頂点色が一様でテクスチャのないプリミティブのレコードは、 両方を外したビルドと同じ大きさです。 32bit ターゲット、既定の透視補正レベルでのレコードのバイト数です (エントリ 4 バイトを含む)。

レコード

バイト

選ばれる条件

ヘッダと単色

48

—

  • 深度平面

60 (56)

LayerFlags::NO_DEPTH でないレイヤ

  • 補間色

76 (72)

3 頂点の色が異なる (SHAPOGFX3D_GOURAUD)

  • テクスチャ座標

104 (100)

テクスチャ付きマテリアル (SHAPOGFX3D_TEXTURE)

全部

120 (116)

括弧内は SHAPOGFX3D_DEPTH_BITS=16 の場合です。深度を 16 ビットにすると深度平面が 12 バイトから 8 バイトになりますが、 遠くで交差する面の境目がわずかにずれることがあります。 補間色は 8.8 の勾配で持つので、1 ピクセルあたり 127 階調を超える急な色変化 (真横から見た細長い三角形など) は 最初の頂点の色の単色で描かれます。

機能を外すと、対応するレコード形式とそのコードがビルドから消えます。 線分は 1 個 16 バイト (x 範囲、レイヤ、レコードと次の線分へのポインタ) で、設定によらず同じ大きさです。 色やテクスチャ座標は線分を描くときにレコードの平面から求めます。

メモリ

init() に渡したアリーナは次のように分割されます (32bit ターゲットの目安)。

領域

サイズ

ラインバケット

画面高さ x 4 バイト

レイヤ表 (SHAPOGFX3D_LAYER_MAX 枚、既定 8)

1 枚 8 バイト

行列スタック (SHAPOGFX3D_STACK_DEPTH 段、既定 16)

約 1.1 KB

頂点キャッシュ (SHAPOGFX3D_VCACHE_SIZE エントリ、既定 64)

約 2.8 KB

線分プール

Config::spanCapacity 個 x 16 バイト (0 なら残りの 1/4 を 32〜512 個に収めた既定値。通常 512 個 = 8 KB)

三角形バッファ

残り全部

三角形バッファはレコードの大きさが一定でないため、個数ではなくバイト数の予算です。 レコードは領域の末尾から下へ、エントリ (レコード位置とスキャンラインのリンク、1 個 4 バイト) は 先頭から上へ伸び、両者がぶつかった時点で満杯です。 使用量と総量は Stats::triBytes / Stats::triBytesTotal で分かります。 例えば 480x320 で 128 KB のアリーナなら、既定設定で三角形バッファは約 111 KB になり、 全部入りのレコードなら約 970 個、ライティングしたメッシュの標準的なレコード (深度 + 補間色) なら約 1490 個入ります。 なお、レコードは 4 バイト単位のオフセットで参照するため、三角形バッファは 256 KB が上限です (これを超える分のアリーナは使われません)。

線分プールは Config::spanCapacity で明示できます。あふれた線分は描かれず絵に穴が開くので、 Stats::spanPeak (最悪フレームのピーク) に余裕を持たせた値にしてください。 減らした分はそのまま三角形バッファに回ります。 固定部の 2 つは段数・エントリ数のマクロで減らせます。頂点キャッシュを小さくしても描画結果は変わらず、 共有頂点の再計算が増えるだけです。 あふれた三角形や線分はそのフレームでは破棄され、getStats() で使用量・ピーク・破棄数を確認できます。 2D API は描画先バッファ以外のメモリを必要としません。

g3::Config cfg = g3::defaultConfig(W, H, arena, sizeof(arena));
cfg.spanCapacity = 128;    // 実測ピークに余裕を持たせた値
g3d.init(cfg);

三角形が破棄される条件

putPrimitive() の段階で次の三角形は破棄されます。

  • ニア平面をまたぐ、または手前にある (クリップは行いません)

  • バックフェイスカリングが有効で裏を向いている

  • スキャンラインを 1 本も覆わない

  • 三角形バッファにレコードの空きがない (Stats::triDropped)

  • 添字が頂点バッファの範囲外 (Stats::badIndices)。この検査により、出自の不確かなメッシュデータでも安全に描けます。

複数インスタンス

Graphics3D は状態を全てオブジェクト内に持つので、アリーナを分ければ複数を同時に使えます (例: 画面用と、小さなオフスクリーン用)。ムーブ可能、コピー不可です。