pyxen.Mesh

Mesh は、三角形またはクアッド(四角形)を頂点ごとに定義して作る描画コンポーネントです。各頂点は位置、画像上のサンプリング位置、色を持つため、単色のポリゴン、テクスチャ付きのストリップ、グラデーション、曲がったり波打ったりする面など、さまざまなものを作れます。

Sprite、Text、図形と同じコンポーネントです。スポーン時にエンティティへ渡すと、エンティティが毎フレーム描画します。

Meshは2Dです。 頂点には x と y があるだけで、奥行きはありません。z、深度バッファ、遠近法はありません。すべてCPU上で変換され、ほかのコンポーネントと同様にレイヤー順で平面的に描画されます。スプライトや図形では描けない形を描くための手段であり、3Dエンジンではありません。

from pyxen import *

def start():
    world.spawn(x=160, y=60,
                mesh=Mesh(points=(-40, 40, 0, -40, 40, 40), fill=(0.3, 0.9, 0.5, 1.0)))

コンストラクタ

Mesh(
    *,
    points=None,
    vertices=None,
    indices=None,
    fill=None,
    image=None,
    mode=TRIANGLES
)

すべてのパラメータはキーワード専用です。points か vertices のどちらかを指定する必要があり、どちらもなければ ValueError: Mesh needs points= or vertices= が発生します。


パラメータ

パラメータ型説明
pointsタプル、リスト、または FloatBuffer平坦な位置。1頂点につき2つのfloat: (x1, y1, x2, y2, …)。すべての頂点に fill の1色が使われます
verticesタプル、リスト、または FloatBuffer完全な頂点。1頂点につき8つのfloat: x, y, u, v, r, g, b, a
indicesタプル、リスト、または IntBufferどの頂点で各三角形・クアッドを作るか。省略すると頂点を順番に使います
fill色points 用の色。(r, g, b, a) のタプルまたは Color。デフォルトは不透明な白
imagestrメッシュにテクスチャを貼る画像アセットの名前
modeTRIANGLES または QUADS各プリミティブの種類。デフォルトは TRIANGLES

points と vertices の両方を渡した場合は vertices が優先されます。


頂点

1つの頂点は8つの数値です。

float意味
x, yピクセル単位の位置。エンティティからの相対。「変換」を参照
u, vこの頂点が表示する画像上の点。画像全体を 0.0〜1.0 で表します。(0, 0) が左上、(1, 1) が右下です。image がなければ無視されます
r, g, b, a頂点の色。それぞれ 0.0〜1.0

色は頂点間で補間されるため、クアッドの各隅に違う色を付けるとグラデーションになります。image がある場合、色は画像に乗算されます。白なら画像そのまま、赤なら赤成分だけが残ります。

短い形式の points は、テクスチャなしで1色のジオメトリ向けです。u = v = 0、すべての色が fill の vertices と同じ意味です。


インデックス

頂点は一度だけ並べます。インデックスは、どの頂点で各プリミティブを作るかを指定します。そのため、辺を共有する2つの三角形は、その2頂点を繰り返さずに共有できます。

  • mode=TRIANGLES では、3個のインデックスごとに三角形が1つできます。
  • mode=QUADS では、4個のインデックスごとにクアッドが1つできます。角はクアッドの周囲に沿った順(たとえば左上、右上、右下、左下)で並べます。

インデックスが数えるのは頂点であり、floatではありません。頂点の形式に関係なく、2 は3番目の頂点を指します。

# 0-2の辺を共有する2つの三角形でできた正方形
Mesh(vertices=square, indices=(0, 1, 2, 0, 2, 3))

# 同じ正方形を1つのクアッドで
Mesh(vertices=square, indices=(0, 1, 2, 3), mode=QUADS)

indices を省略すると頂点が順番に使われます。最初の3頂点が1つの三角形(または最初の4頂点が1つのクアッド)、というように続きます。頂点を1つずつ積み上げて作るストリップに向いています。

2つの点は、エラーにせず許容されます。最後の頂点を超えるインデックスは、その三角形またはクアッドだけが破棄され、残りは描画されます。末尾の3個(または4個)に満たないインデックスの組は無視されます。古いインデックスリストが残ったまま頂点を作り直すゲームでも、停止せず、1フレームだけ三角形が欠けるだけです。


テクスチャ

world.spawn(x=40, y=40, mesh=Mesh(
    vertices=(0,   0,  0, 0, 1, 1, 1, 1,
              64,  0,  1, 0, 1, 1, 1, 1,
              64, 64,  1, 1, 1, 1, 1, 1,
              0,  64,  0, 1, 1, 1, 1, 1),
    image="tiles",
    mode=QUADS,
))

image はプロジェクト内の画像アセットの名前(拡張子なし)で、Sprite と同じ方法で見つけられます。各頂点の u と v が表示する画像上の点を選び、その間で画像がプリミティブに引き伸ばされます。

u と v はピクセルではなく画像全体に対する割合です。0.0〜1.0 の範囲外の値はタイル状に繰り返されず、画像の端のピクセルが繰り返されます。


変換

メッシュの頂点はエンティティに対してローカルです。エンティティの x、y、rotation、scale(親がある場合は親のもの)が、描画時にすべての頂点へ適用されます。ジオメトリを (0, 0) の周りに作り、ほかのエンティティと同じように移動・回転・拡大縮小してください。

def start():
    world.spawn(name="diamond", x=160, y=90,
                mesh=Mesh(points=(-20, 0, 0, -30, 20, 0, 0, 30),
                          indices=(0, 1, 2, 3), mode=QUADS,
                          fill=(1.0, 0.6, 0.2, 1.0)))

def update():
    world.get(name="diamond").rotation += 1

Meshのアタッチ

スポーン時:

e = world.spawn(x=100, y=60, mesh=Mesh(points=(0, 0, 50, 0, 25, 40)))

スポーンはジオメトリをエンティティへコピーします。その後、作成した Mesh オブジェクトとエンティティの接続は切れます。実際のジオメトリは、以下で説明する entity.mesh を通して変更してください。同じ Mesh オブジェクトを複数回スポーンして、独立したコピーを作ることもできます。


コンポーネントのプロパティ

entity.mesh でアクセスします。

メンバーR/W説明
points = bufW平坦な (x, y, …) の並びからすべての頂点の位置を置き換えます。色と u, v はそのまま残ります
vertices = bufW平坦な並び(1頂点8個のfloat)からすべての頂点を置き換えます。頂点数を変えることもできます
indices = bufWインデックスリストを置き換えます
visible = flagWジオメトリを失わずに、メッシュを描画するかスキップするかを切り替えます
modeRTRIANGLES または QUADS
set_vertex(i, x, y, [u, v, [r, g, b, a]])1つの頂点を変更します。u, v、続いて r, g, b, a は省略可能ですが、各グループは全部そろえて指定します
count()頂点の数

mode はメッシュの作成時に決まります。

e.mesh.points = (0, 0, 60, 0, 30, 50)    # 3つの頂点を動かす
e.mesh.set_vertex(1, 70, 5)               # さらに1つだけずらす
e.mesh.visible = False                    # 非表示にする

存在しない頂点番号で set_vertex を呼ぶとエラーになります。メッシュを持たないエンティティで entity.mesh を使った場合も同様です。

メッシュより短い points は先頭の頂点だけを動かし、長い場合は最後の頂点で切り捨てられます。


メッシュのアニメーション

毎フレーム動くものには points を使ってください。1回の代入ですべての位置が置き換わり、頂点を回すループはエンジン内で実行されます。同じ更新を set_vertex で行うと、頂点ごとにPythonの呼び出しが1回必要です。

安くなるのはアップロードであり、計算ではありません。 新しい位置は毎フレーム自分のコードで計算する必要があり、頂点数の多いメッシュでは時間がかかるのはそこです。一度作って、エンティティと一緒に移動・回転・拡大縮小するだけのメッシュは、フレームあたりほとんどコストがかかりません。毎フレーム形が変わるメッシュは、そのジオメトリの計算コストがそのままかかります。

この波打つリボンは、ジオメトリを一度だけ作り、その後 entity.mesh.points で頂点を動かします。アセットは不要です。

from pyxen import *
import math

COLUMNS = 20
STEP = 14

def ribbon(t):
    points = []
    for i in range(COLUMNS + 1):
        y = math.sin(t * 0.08 + i * 0.5) * 14
        points.append(i * STEP)
        points.append(y - 12)     # 上の辺
        points.append(i * STEP)
        points.append(y + 12)     # 下の辺
    return points

def start():
    vertices = []
    indices = []
    for i in range(COLUMNS + 1):
        r = i / COLUMNS
        # この列の上と下の頂点: x, y, u, v, r, g, b, a
        vertices.extend((0, 0, 0, 0, 1.0 - r, 0.4, r, 1.0))
        vertices.extend((0, 0, 0, 0, 1.0 - r, 0.4, r, 1.0))
    for i in range(COLUMNS):
        a = i * 2
        # 左上、左下、右下、右上
        indices.extend((a, a + 1, a + 3, a + 2))
    world.spawn(name="ribbon", x=20, y=90,
                mesh=Mesh(vertices=vertices, indices=indices, mode=QUADS))

def update():
    world.get(name="ribbon").mesh.points = ribbon(time.frame)

FloatBufferとIntBuffer

メッシュはタプルやリストから作れますが、ジオメトリが大きい場合や頻繁に作り直す場合は、2種類のバッファを使うと毎回新しいタプルを作る手間を避けられます。

positions = FloatBuffer(64)     # 容量
positions.append(0.0)
positions.append(0.0)
positions[0] = 5.0              # インデックスで読み書き

indices = IntBuffer(64)
indices.append(0)
FloatBuffer(capacity)IntBuffer(capacity)
保持するものfloat: 位置、頂点整数: インデックス
append(value)末尾に追加末尾に追加
clear()空にする空にする
buf[i]読み取りと代入読み取りと代入

capacity は初期サイズで、それを超えて追加するとバッファは拡張されます。Mesh の points=、vertices=、indices= に渡すか、entity.mesh の対応するプロパティに代入できます。


Meshか図形か

長方形、円、三角形、線には図形が適しており、毎フレーム描き直す絵には draw が適しています。Meshは、それらにできないことが必要なときに使います。長方形ではないテクスチャ付きポリゴン、隅ごとに異なる色、旗・道路・リボン・水面のように頂点が独立して動く面などです。


よくあるエラー

ジオメトリがない

Mesh(fill=(1, 0, 0, 1))
ValueError: Mesh needs points= or vertices=