pyxen.Mesh

Mesh 是一个可绘制的组件,由你逐个顶点定义的三角形或四边形组成。每个顶点都有位置、要采样的图像上的一个点,以及一种颜色,因此 Mesh 可以是纯色多边形、带纹理的条带、渐变,或是会弯曲、起伏的表面。

它是与 Sprite、Text 和形状同类的组件:在 spawn 时把一个 Mesh 交给实体,实体就会在每一帧绘制它。

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扁平的位置,每个顶点两个浮点数:(x1, y1, x2, y2, …)。每个顶点都使用 fill 中的那一种颜色
vertices元组、列表或 FloatBuffer完整顶点,每个顶点八个浮点数:x, y, u, v, r, g, b, a
indices元组、列表或 IntBuffer由哪些顶点组成每个三角形或四边形。省略则按顺序使用顶点
fill颜色points 使用的颜色。(r, g, b, a) 元组或 Color。默认为不透明白色
imagestr为网格贴图所用的图像素材名称
modeTRIANGLES 或 QUADS每个图元的类型。默认为 TRIANGLES

如果同时传入 points 和 vertices,以 vertices 为准。


顶点

一个顶点由八个数字组成:

浮点数含义
x, y位置,单位为像素,相对于实体。见“变换”
u, v该顶点显示图像上的哪个点,在整幅图像上从 0.0 到 1.0。(0, 0) 是左上角,(1, 1) 是右下角。没有 image 时被忽略
r, g, b, a顶点颜色,每个分量 0.0 到 1.0

颜色会在顶点之间插值,所以给四边形的每个角不同的颜色,就能画出渐变。有 image 时,颜色会与图像相乘:白色保持图像不变,红色只保留它的红色部分。

简短形式 points 适用于没有纹理、只有一种颜色的几何体。它等同于 u = v = 0 且所有颜色都为 fill 的 vertices。


索引

顶点只列出一次。索引说明哪些顶点组成每个图元,因此共用一条边的两个三角形可以共用这条边的两个顶点,而不必重复。

  • 使用 mode=TRIANGLES 时,每 3 个索引组成一个三角形。
  • 使用 mode=QUADS 时,每 4 个索引组成一个四边形。按围绕四边形的顺序列出各角,例如左上、右上、右下、左下。

索引数的是顶点,而不是浮点数:无论顶点采用哪种形式,2 都表示第三个顶点。

# 由共用 0-2 边的两个三角形组成的正方形
Mesh(vertices=square, indices=(0, 1, 2, 0, 2, 3))

# 同一个正方形,用一个四边形表示
Mesh(vertices=square, indices=(0, 1, 2, 3), mode=QUADS)

省略 indices 时,按顺序使用顶点:前三个是一个三角形(或前四个是一个四边形),依此类推。这适用于逐个顶点构建的条带。

有两种情况会被容忍而不报错。指向最后一个顶点之后的索引,会丢弃它自己的三角形或四边形,其余的照常绘制。末尾不足 3 个(或 4 个)的索引组会被忽略。如果游戏在旧的索引列表仍然有效时重建顶点,只会丢掉一帧里的一个三角形,而不会中止运行。


纹理

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 的值不会平铺:它们会重复图像边缘的像素。


变换

Mesh 的顶点相对于它的实体是局部的。实体的 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

在 spawn 时:

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

spawn 会把几何体复制到实体中。此后,你构建的 Mesh 对象就不再与它相连:请通过下面介绍的 entity.mesh 修改实际的几何体。同一个 Mesh 对象可以 spawn 多次,得到相互独立的副本。


组件属性

通过 entity.mesh 访问:

成员读/写描述
points = buf写用扁平的 (x, y, …) 序列替换每个顶点的位置。颜色和 u、v 保持不变
vertices = buf写用扁平序列(每个顶点 8 个浮点数)替换每个顶点。顶点数量可以改变
indices = buf写替换索引列表
visible = flag写绘制或跳过该网格,而不丢失它的几何体
mode读TRIANGLES 或 QUADS
set_vertex(i, x, y, [u, v, [r, g, b, a]])修改单个顶点。u, v 以及随后的 r, g, b, a 是可选的,但每一组都必须完整给出
count()顶点数量

mode 在构建网格时就已确定。

e.mesh.points = (0, 0, 60, 0, 30, 50)    # 移动三个顶点
e.mesh.set_vertex(1, 70, 5)               # 再轻推其中一个
e.mesh.visible = False                    # 隐藏它

用不存在的顶点编号调用 set_vertex 是错误,对没有网格的实体使用 entity.mesh 同样是错误。

比网格短的 points 只会移动前面的顶点;更长的则在最后一个顶点处被截断。


让网格动起来

对每一帧都在移动的内容,请优先使用 points。一次赋值就替换所有位置,遍历顶点的循环在引擎内部运行;同样的更新如果用 set_vertex,每个顶点就要一次 Python 调用。

这让上传变便宜了,计算并没有。 每一帧你仍要在自己的代码里算出每个新位置,对于顶点很多的网格,时间就花在这里。只构建一次、之后随实体移动、旋转或缩放的网格,每帧几乎没有开销;每一帧形状都在变的网格,开销就是计算它的几何体所需的开销。

这条起伏的丝带只构建一次几何体,之后通过 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

网格可以用元组和列表构建,但当几何体很大或经常重建时,这两种缓冲区类型可以避免每次都创建新元组。

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)
存放浮点数:位置、顶点整数:索引
append(value)追加到末尾追加到末尾
clear()清空清空
buf[i]读取和赋值读取和赋值

capacity 是起始容量;追加超出时缓冲区会增长。可以把它们作为 points=、vertices= 或 indices= 传给 Mesh,也可以赋给 entity.mesh 上对应的属性。


Mesh 还是形状?

矩形、圆、三角形和线适合用形状,每一帧重新绘制的图像适合用 draw。当你需要它们做不到的事情时,再用 Mesh:不是矩形的带纹理多边形、每个角不同的颜色,或者顶点各自独立运动的表面,例如旗帜、道路、丝带或水面。


常见错误

缺少几何体

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