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。默认为不透明白色 |
image | str | 为网格贴图所用的图像素材名称 |
mode | TRIANGLES 或 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=