1. はじめに
こんにちは、土居 意弘(ムンペイ)です。
こちらの読者ならおなじみのレトロゲームエンジン Pyxel を使ったプログラミング学習サービス「Code & Magic」を開発・運営しています。
前回に引き続き、上記サービス向けのネタ研究の一環として、AI(ChatGPT)とペアプログラミングしながら Pyxel で3Dエンジンを作成した話をまとめてみました。
今回は、High Color パレットで、Phong シェーディングまで駆け抜けます。
2. 陰線消去シェーディング
[Prompt]
Z-Bufferなしの陰線消去を実装お願いします。
[ChatGPT]
いいところに来ましたね。
Zバッファなしで「それっぽく」陰線消去するなら、面(ポリゴン)を使ったペインタ方式が現実的です。
ここでは:
- Camera クラスはそのまま(さっきの FOV / look_at 対応版)
- 新しく HiddenLineRenderer を作る
- メッシュは
- points: list[tuple[x,y,z]]
- faces: list[tuple[i0, i1, i2]](三角形)
という前提にします(球の面データはあとでいくらでも生成できます)
👁️ アルゴリズム概要(Zバッファなし)
1. 全頂点を world_to_camera → project でカメラ・画面座標に変換
2. 各三角形に対して
- カメラ空間で 平均Z を使って「奥行き」を計算
- 法線の Z 成分で「表向き / 裏向き」を判定
3. 平均Z の 遠い順(奥→手前)にソート
4. ソート順に
- まず三角形を 背景色で塗りつぶす(奥の線を消す)
- 表向きの三角形だけ 輪郭線を描く
こうすると、
背景で塗りつぶされたところにある奥の線が隠れるので、
「ワイヤーフレーム+陰線消去」っぽくなります。
ということで、ここまで作った Renderer に合わせて作ってくれたもの(を少し修正したもの)を使い、レンダリングされたのがこちらです。
ちょうど、表裏の判定が入ったということは、裏向きポリゴンの輪郭を暗い色で表示すれば奥行きのあるワイヤーフレームになるなぁとおもい、ちょっとだけ手動改造したのがこちらです。
いいですね、格好いいですね。
3. フラットシェーディング
[ChatGPT]
次、どれをやりましょう?
1. Scene クラス実装(複数 Mesh を描画)
2. Cube / Torus / Cylinder の Mesh 生成関数
3. Mesh のアニメーション API(rotate_x/y/z, translate)
4. シェーディング対応(簡易ライト + 頂点法線)
5. FPS カメラで自由に動くデモを作る
やってみたい番号を教えてください!
とのことでしたので、
[Prompt]
簡易ライトでシェーディングをやりましょう
とお願いしました。
了解です!
まずは Zバッファなし・三角形塗りつぶし+陰線消去と両立できる、最も軽量なシェーディング(フラットシェーディング) を導入しましょう。
- 光源
- 地の色
- 陰影の強さ
などを導入してRendererを作ってくれました。それがこちらです。
う、真っ白ですね。
[Prompt]
いい陰影が出てますが、ほとんど真っ白になっちゃいますね。
[ChatGPT]
✅ 改良案①:intensity を弱める(明るさ調整)
✅ 改良案②:base_color を下げる(白ベース→グレーに)
✅ 改良案③:shade_levels を大きめに(陰影段数を増やす)
といった改良案を出してくれました。
詳細なやり取りは省略しますが、陰影のパラメータ、アンビエントカラー、メッシュの輪郭は黒に変更、などを手動で入れて調整した結果、きれいになりました。
標準パレットのまま、最大の明るさを 7 、そこから暗くなるごとに小さい数値の色を描画するということにしたので、こんな色合いになっています。
(Pyxelの標準16色パレットでは、0〜15の整数で色を指定します)
でも雰囲気はわかるので、このまま進めてみましょう。
4. グーローシェーディング(失敗)
[Prompt]
Gouraudシェーディングやっちゃいますか??
[ChatGPT]
やっちゃいましょう!!!🔥🔥🔥
Pyxel で Gouraud shading(頂点ごとの明度 → 三角形内補間)
これは“リアル3D感”が一気に跳ね上がる、めちゃくちゃ美しいシェーディングです。
しかも今のコードは 頂点配列 + face(三角形) が揃っていて、
ライト方向も既にあるので Gouraud が自然に載せられる状態 です。
頂点法線の計算の追加、頂点明度の計算を追加し、Gouraud(グーロー)シェーディング用の三角形塗りつぶしルーチンを作ってくれました。
Rendererクラスの置き換えで使いたいので、さらにわがままを言います。
[Prompt]
GouraudRenderer クラスとして、統合版をお願いします
[ChatGPT]
了解です!!
あなたの現在の Wireframe/HiddenLineRenderer の構成に完全に統合できる形で、
新しい GouraudRenderer クラスを提供します。
健気に応えてくれます。
そして出来上がったのがこちら。
ん??
[Prompt]
なんかふちがギザギザになってしまいました
✅ 考えられる原因(優先順)
① 三角形描画順が「奥→手前」で、シルエットの重ね順が破綻
② 補間途中の四捨五入による “隙間ピクセル”
③ backface 判定(nz<=0)で微妙な境界面が突然消える
なるほど、どれもありそうです。対策を実行してみますが、効果がありませんでした。
5. グーローシェーディング(Zバッファ付き)
[Prompt]
対策①も③もほとんど効果が感じられません。 ZBuffer版GouraudRendererもやってみましょうか。
[ChatGPT]
お、いよいよZバッファ行きますね 😎
ギザギザが消えなかったのは、やっぱり「ソート+ペインタ方式の限界」です。
Zバッファを入れて ピクセル単位で手前判定 してしまいましょう。
ということで勢いよくコードを生成してくれたのですが、すぐには動きませんでした。
ちょっとずつ状況を説明しながら、コードを改善していきます。
[Prompt]
依然としてギザギザです!!
[Prompt]
今のコードはZBufferではない方のGouraud用ですよね?(ZBufferの処理がないので) 適用してみたところ、ギザギザが直らないだけでなく、回転が逆に見えるようになりました。 裏向きのポリゴンだけを描画しているような気がします。
[Prompt]
ZBuffer 付き GouraudRenderer (完全版)をフルコードで提示してください。
そしてとうとう出来上がりました。
[Prompt]
すばらしい・・・・完成していますよ シェーディングがこれであってるのかはわかりませんが・・
三角形を pset でレンダリングするようにしたため、一気に処理が重くなり、UTIL 100% に張り付いただけでなくFPSも著しく低下してしまいました。
6. フォンシェーディング(Zバッファ付き)
このまま一気に上り詰めましょう。テクスチャやシェーダーなしでは最も高級なレンダリングである Phong シェーディングにトライします。
[Prompt]
ZBuffer版 Phong シェーディング クラスを作ってください!
[ChatGPT]
了解!!
あなたが今持っている Zバッファ + GouraudRenderer を土台に、
Zバッファ版 Phong シェーディング(ピクセル単位法線計算) を完全に組み上げます。
🎯 Phong シェーディングとは?
- Gouraud → 頂点で光を計算して補間
- Phong → 法線を補間し、各ピクセルで光を計算する(=超なめらか)
メリット:
- 三角形が少なくても非常に滑らか
- モアレ状の「帯」が消える
- ハイライト(Specular)も自然に出せる
あなたの Icosphere subdiv=2 でも 球のように見えるレベルの品質 になります。
これであってるのかな。
7. ディザリングで中間色を表現
パレットが少ないうえ、整数に丸められているため、せっかくのPhongシェーディングですが中間色が出ず、虹色の帯状になってしまっています。
一旦中間色を表現するために、パレットの選択を確率的に変更しましょう。
[Prompt]
Phongシェーディングの三角形描画の色選択を、ディザ的なものにしてみたいです。 現状、パレットが整数値のため丸められてしまうため結局中間色表現はできずバンドになってしまっています。それを、小数値の場合は前後の整数から確率的に選択するということにすることで、ディザリング型の中間色表現にするのです。 現在のPhongRendererコードは下記です。
...
[ChatGPT]
いい着眼点です。
これは まさに「正しい方向」 で、レトロGPUや8bit機が実際にやっていた手法そのものです。
小数値の場合は前後の整数から確率的に選択する
これは 確率的ディザリング(stochastic dithering / noise dithering) と呼ばれるやり方で、
Pyxelのような「整数パレット制限環境」と Phong は 非常に相性が良い です。
...
解決方針(おすすめ順)
✔ 方針A:確率的ディザ(最も簡単・効果大)
✔ 方針B:座標ベースのディザ(揺れない)
(両方紹介します)
すごい、優秀です。
確率的ディザの結果がこちら。
そして、座標ベースのディザがこちらです。
確率的な方は、球の表面の同じ場所が同じ色になるとは限らないという意味でちらつき(揺れ)が起こるということですが、これはこれで悪くないなぁと思いました。
8. 32色グレースケールパレットに変更
さて、最後に、パレットを調整してみましょう。
[Prompt]
Pyxel のパレットを調整してみたいです。32色、0を黒、31を白として、全部グレースケールの中間色にした、pyxpal形式のファイルを生成してください。
[ChatGPT]
✅ グレースケール32色パレット(pyxpal)
ファイル名例: grayscale32.pyxpal
0 0 0
8 8 8
16 16 16
[ChatGPT]
② 初期化後にロード
pyxel.init(256, 256, fps=60)
pyxel.load("assets/grayscale32.pyxpal")
pyxel.load() はリソースを引数に取るものなので、この点は少し違っていました。
先人の知恵をお借りします。
パレットファイルも RGB を 000000 という表現で書くのが正しいんじゃないか。
[Prompt]
0 0 0 のような形ではなく、 000000 という16進ですよ。修正してください。
[ChatGPT]
了解です 👍
.pyxpal の正式仕様どおり、6桁16進(RRGGBB)形式 に修正します。
000000
080808
101010
...
こういうところでとぼけるのは相変わらず愛嬌があります。
しかし、できました。このパレット情報を書き込み、コード部分は手作業の方が早いので修正して、もう一度 Pyxel Editor を立ち上げてみると・・・。
おお、パレットの部分が確かに32色グレースケールになっている!!
この状態で再度 Phong バージョンを起動してみましょう。
パレット数を変更したので、シェーディングのレベルも32段階に変更します。
階調が増えたのでディザリングなしでももしかしたら行けるかな。ディザリングOFFにしてみましょう。
やはりバンドが出てしまいましたね。
ディザリング ON !!(確率ベースのディザです)
すごくスムーズ!!
FPSなどの文字はパレット 7 を使ったままなので、ちょっと暗くなってしまいましたがいいでしょう。
なんとか、スムーズシェーディングの球を描画することができたので、まずは満足です。
9. まとめ
さて、今回のトライアルをゲームに応用できるかと考えた場合、このFPSだと、複数のオブジェクトを表示するのは難しそうですね。
透視変換などの高速化もChatGPTは教えてくれたのですが、pset で点を打つ速度の方が支配的です。
昨年の Pyxel Advent Calendar の記事には、data_ptr を用いるという技もあると紹介されているので、このような実装も必要かと思います。
とはいえ、現在のCPUなら内蔵GPUであっても、このくらいの描画は余裕でハードウェア実行できると思います。
Pyxel も内部は OpenGL を呼んでいるんだった気がしますのでその能力を使った pyxel.tri の頂点カラー線形補完バージョン(ディザリング付き)があったらいいんじゃないかな・・・などと言い残して本稿を締めたいと思います。
ありがとうございました。
10. 付録
11. 言い訳
と、最後まで書いて読み直してみて気づきましたが、フラットシェーディング以前と、グーローシェーディング以降で光の当たる方向が逆に見えますね。ライティングの処理が反転しちゃってる可能性が高いです。AI任せにし過ぎました・・・。とはいえ、それっぽくレンダリングできてはいるということで、本編はこれにてご容赦くださいませ。。。












