2026/07/07(火) [n年前の日記]
#1 [prog] スプライトのzソートについて調べてる。その4
スプライトの描画順をz値に基づいて決定したい。
AI (Google Gemini) に、Python + pygame でオーダリングテーブル(ot, Ordering table)の処理をするサンプルを作ってもらった。
AI (Google Gemini) に、Python + pygame でオーダリングテーブル(ot, Ordering table)の処理をするサンプルを作ってもらった。
- オーダリングテーブルを使うと、ソート処理が不要になる。
- その代わり、テーブル分のメモリを余計に使う。
- 整数値で扱うから、誤差(?)が大きくなる。
◎ サンプル1 :
矩形で塗り潰すだけの複数のオブジェクトを生成して、z値に応じてオーダリングテーブルで描画順を決めるサンプルスクリプト。
_01_ot_sample1_main.py
pygbag を使って、Webブラウザ上でも実行できるようにした。マウスクリック、もしくはRキーで、オブジェクトを生成し直す。
_01_ot_sample1
z値の大小で描画順が決定されてることが分かる。
_01_ot_sample1_main.py
"""
Ordering Table のサンプル(pygbag対応 + マウスクリック + Z値連動色変化版)
Windows11 x64 25H2 + Python 3.10.10 64bit + pygame-ce 2.5.7
"""
import asyncio
import random
import sys
import pygame
# 画面サイズと設定
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600
FPS = 60
# オーダリングテーブルの最大サイズ(Z値の範囲: 0 〜 99)
MAX_Z_LAYERS = 100
class CustomSprite:
"""独自のZ値を持つゲームオブジェクト"""
def __init__(self, x, y, width, height, z):
self.rect = pygame.Rect(x, y, width, height)
self.z = z # 浮動小数点数などの任意の数値
# --- Z値(0.0?99.9)に応じて色を計算 ---
# 0.0で128、99.9で約32になるように補間
ratio = z / 100.0
color_val = int(192 - (160 * ratio))
self.color = (color_val, color_val, color_val)
def draw(self, surface, font):
# 描画処理(わかりやすいようにZ値もテキストで表示)
pygame.draw.rect(surface, self.color, self.rect, 0)
# 枠線描画
pygame.draw.rect(surface, pygame.Color(255, 255, 255, 255), self.rect, 2)
text = font.render(f"Z: {self.z:.1f}", True, (255, 255, 255))
surface.blit(text, (self.rect.x + 5, self.rect.y + 5))
async def main():
pygame.init()
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption("Ordering Table Draw System")
clock = pygame.time.Clock()
font = pygame.font.Font(None, 24)
initfg = True
# メインループ
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
running = False
elif event.key == pygame.K_q:
running = False
elif event.key == pygame.K_r:
initfg = True
elif event.type == pygame.MOUSEBUTTONDOWN:
if event.button == 1:
initfg = True
if initfg:
# ----------------------------------------
# 1. オブジェクトの生成
# ----------------------------------------
sprites = []
for _ in range(15):
x = random.randint(100, 600)
y = random.randint(100, 400)
z = random.uniform(0.0, 99.9)
# colorの指定を無くし、内部でZ値から自動計算するように変更
sprites.append(CustomSprite(x, y, 100, 100, z))
initfg = False
# 画面クリア
screen.fill((0, 0, 0))
# -------------------------------------------------------------
# 2. オーダリングテーブルの初期化
# -------------------------------------------------------------
ordering_table = [[] for _ in range(MAX_Z_LAYERS)]
# -------------------------------------------------------------
# 3. オブジェクトを整数値に変換したZ値のレイヤーに登録
# -------------------------------------------------------------
for sprite in sprites:
z_index = int(sprite.z)
z_index = max(0, min(z_index, MAX_Z_LAYERS - 1))
ordering_table[z_index].append(sprite)
# -------------------------------------------------------------
# 4. オーダリングテーブルに基づいて奥から一気に描画
# -------------------------------------------------------------
for z_layer in reversed(ordering_table):
for sprite in z_layer:
sprite.draw(screen, font)
pygame.display.flip()
clock.tick(FPS)
await asyncio.sleep(0)
pygame.quit()
sys.exit()
if __name__ == "__main__":
asyncio.run(main())
pygbag を使って、Webブラウザ上でも実行できるようにした。マウスクリック、もしくはRキーで、オブジェクトを生成し直す。
_01_ot_sample1
z値の大小で描画順が決定されてることが分かる。
◎ サンプル2 :
先日作成した、
_スプライトが球状に描画されるスクリプト
を、オーダリングテーブルで処理するようにしてみた。
_image.png
_02_ot_sample2_main.py
これも pygbag でWebブラウザ上でも動かせるようにしてみた。
_02_ot_sample2
_image.png
_02_ot_sample2_main.py
"""
スプライトで球を表示するサンプル(オーダリングテーブル版・半径可変モデル)。
Google Gemini に作ってもらった。
Windows11 x64 25H2 + Python 3.10.10 64bit
"""
import pygame
import sys
import math
import asyncio
# 初期設定
pygame.init()
WIDTH, HEIGHT = 1280, 720
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("3D Point Cloud Sphere with Sprites (Ordering Table)")
clock = pygame.time.Clock()
# --- 球体上の点群(3D座標)を生成 ---
NUM_POINTS = 256
BASE_RADIUS = 300 # 基準となる半径
PULSE_AMPLITUDE = 200 # 半径の振り幅
# 最初は半径 1.0 の単位球として座標を保持しておくと、後から半径を掛け算しやすくなります
base_points = []
phi = math.pi * (math.sqrt(5.0) - 1.0)
for i in range(NUM_POINTS):
y = 1.0 - (i / float(NUM_POINTS - 1)) * 2.0
radius_at_y = math.sqrt(1.0 - y * y)
theta = phi * i
x = math.cos(theta) * radius_at_y
z = math.sin(theta) * radius_at_y
# 単位球の座標として格納
base_points.append([x, y, z])
# --- オーダリングテーブルの設定 ---
TABLE_SIZE = 200 # 解像度(バケツの数)
async def main():
# 画像の読み込み
try:
sprite_image = pygame.image.load("./image.png").convert_alpha()
except Exception:
print("image.png が見つからないため、仮のスプライトを作成します。")
sprite_image = pygame.Surface((32, 32), pygame.SRCALPHA)
pygame.draw.circle(sprite_image, (255, 0, 0), (16, 16), 16)
pygame.draw.circle(sprite_image, (255, 255, 255), (12, 12), 4)
SPRITE_W, SPRITE_H = sprite_image.get_size()
angle_y = 0
angle_x = 0
angle_radius = 0 # 半径変化用のラジアン値
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
running = False
elif event.key == pygame.K_q:
running = False
# 画面のクリア
screen.fill((15, 15, 15))
# アニメーション速度
angle_y += 0.015
angle_x += 0.001
angle_radius += 0.01 # 半径が変化するスピード(好みに応じて調整)
# --- 現在のフレームの半径を計算 ---
# BASE_RADIUS を中心に、±PULSE_AMPLITUDE の範囲で波打つ
current_radius = BASE_RADIUS + math.sin(angle_radius) * PULSE_AMPLITUDE
# --- オーダリングテーブルの初期化 ---
ordering_table = [[] for _ in range(TABLE_SIZE)]
# 3次元回転と座標変換、およびテーブルへの登録
for pt in base_points:
# 単位球の座標に、現在のフレームの半径を掛け合わせる
x = pt[0] * current_radius
y = pt[1] * current_radius
z = pt[2] * current_radius
# Y軸周りの回転
cos_y, sin_y = math.cos(angle_y), math.sin(angle_y)
x1 = x * cos_y - z * sin_y
z1 = x * sin_y + z * cos_y
# X軸周りの回転
cos_x, sin_x = math.cos(angle_x), math.sin(angle_x)
y2 = y * cos_x - z1 * sin_x
z2 = y * sin_x + z1 * cos_x
# --- Z値をテーブルのインデックスに変換 ---
# z2 はおおよそ -current_radius から +current_radius の範囲。
norm_z = (z2 + current_radius) / (2 * current_radius)
# 念のため範囲外をクリップ
table_idx = int(norm_z * (TABLE_SIZE - 1))
table_idx = max(0, min(TABLE_SIZE - 1, table_idx))
# 画面座標の計算
screen_x = int(x1 + (WIDTH // 2) - (SPRITE_W // 2))
screen_y = int(y2 + (HEIGHT // 2) - (SPRITE_H // 2))
# テーブルの該当する深度のバケツに追加
ordering_table[table_idx].append((screen_x, screen_y))
# --- 描画処理 (奥から手前へ) ---
for bucket in ordering_table:
for screen_x, screen_y in bucket:
screen.blit(sprite_image, (screen_x, screen_y))
# 画面更新
pygame.display.flip()
clock.tick(60)
await asyncio.sleep(0)
pygame.quit()
sys.exit()
if __name__ == "__main__":
asyncio.run(main())
これも pygbag でWebブラウザ上でも動かせるようにしてみた。
_02_ot_sample2
◎ 只のソート処理でも良さそう :
ここまで試しておいてアレだけど…。実は只のソート処理でも良さそうな気配があって…。
先日、 _C言語で作ったzソート処理をするサンプル では、4096個のスプライト相当をz値でクイックソートしても、CPU : Ryzen 5 5600X の環境なら 0.0005 - 0.0006 秒しかかからないようで…。60FPSなら1フレームの時間が 1/60 秒 = 0.0166667秒 だけど、1/27 とか 1/33 程度の処理時間でソート処理が終わってしまう。
2Dゲームを今時のハードウェア上で動かす分には、クイックソートで処理しちゃっても十分なのかもしれない…。非力なCPU上で動かすとか、もっと膨大なスプライト枚数を管理するなら話は違うのかもしれないけれど…。
先日、 _C言語で作ったzソート処理をするサンプル では、4096個のスプライト相当をz値でクイックソートしても、CPU : Ryzen 5 5600X の環境なら 0.0005 - 0.0006 秒しかかからないようで…。60FPSなら1フレームの時間が 1/60 秒 = 0.0166667秒 だけど、1/27 とか 1/33 程度の処理時間でソート処理が終わってしまう。
2Dゲームを今時のハードウェア上で動かす分には、クイックソートで処理しちゃっても十分なのかもしれない…。非力なCPU上で動かすとか、もっと膨大なスプライト枚数を管理するなら話は違うのかもしれないけれど…。
[ ツッコむ ]
以上です。