Do you really need a grid? You could reduce the number of vertices by simply creating 4 vertices for each sprite.
Here is an example of what the sprite system could look like (everything is one single Geom):
sprite.py
:
from panda3d.core import *
import array
class Sprite:
registry = {}
_count = 0
_model = None
@classmethod
def __create_model(cls):
# Create empty model
# define custom vertex format, with each column in a separate array
GE = GeomEnums
fl32 = GE.NT_float32
vf = GeomVertexFormat()
af = GeomVertexArrayFormat()
af.add_column(InternalName.get_vertex(), 3, fl32, GE.C_point)
vf.add_array(af)
af = GeomVertexArrayFormat()
af.add_column(InternalName.get_color(), 4, fl32, GE.C_color)
vf.add_array(af)
af = GeomVertexArrayFormat()
af.add_column(InternalName.get_texcoord(), 2, fl32, GE.C_texcoord)
vf.add_array(af)
vertex_format = GeomVertexFormat.register_format(vf)
v_data = GeomVertexData("chunk_data", vertex_format, GE.UH_dynamic)
geom = Geom(v_data)
prim = GeomTriangles(GE.UH_dynamic)
prim.set_index_type(GE.NT_uint32)
geom.add_primitive(prim)
geom_node = GeomNode("sprite_chunk")
geom_node.add_geom(geom)
cls._model = NodePath(geom_node)
cls._model.set_transparency(TransparencyAttrib.M_alpha)
def __init__(self, width, height, uvs):
if not self._model:
self.__create_model()
self.width = width
self.height = height
self._coords = array.array("f", [
0., 0., 0.,
0., 0., -height,
width, 0., -height,
width, 0., 0.,
])
self._pos = (0., 0.)
self._color_array = array.array("f", [0.] * 16)
self._uv_array = array.array("f", uvs)
self.start_vert_row = 0
self.start_vert_index_row = 0
self._id = Sprite._count
Sprite._count += 1
Sprite.registry[self._id] = self
self.__add_geometry()
def __add_geometry(self):
geom = self._model.node().modify_geom(0)
v_data = geom.modify_vertex_data()
old_vert_count = v_data.get_num_rows()
self.start_vert_row = old_vert_count
v_data.set_num_rows(old_vert_count + 4)
pos_view = memoryview(v_data.modify_array(0)).cast("B").cast("f")
pos_view[old_vert_count*3:] = self._coords
uv_view = memoryview(v_data.modify_array(2)).cast("B").cast("f")
uv_view[old_vert_count*2:] = self._uv_array
prim = geom.modify_primitive(0)
prim_array = prim.modify_vertices()
old_index_count = prim_array.get_num_rows()
self.start_vert_index_row = old_index_count
prim_array.set_num_rows(old_index_count + 6)
vert_indices = array.array("I", [
0, 1, 2,
2, 3, 0
])
prim_view = memoryview(prim_array).cast("B").cast("I")
prim_view[old_index_count:] = vert_indices
prim.offset_vertices(old_vert_count, old_index_count, old_index_count + 6)
def __remove_geometry(self):
geom = self._model.node().modify_geom(0)
v_data = geom.modify_vertex_data()
old_vert_count = v_data.get_num_rows()
start = self.start_vert_row * 3
size = 4 * 3
pos_view = memoryview(v_data.modify_array(0)).cast("B").cast("f")
pos_view[start:-size] = pos_view[start+size:]
start = self.start_vert_row * 4
size = 4 * 4
color_view = memoryview(v_data.modify_array(1)).cast("B").cast("f")
color_view[start:-size] = color_view[start+size:]
start = self.start_vert_row * 2
size = 4 * 2
uv_view = memoryview(v_data.modify_array(2)).cast("B").cast("f")
uv_view[start:-size] = uv_view[start+size:]
v_data.set_num_rows(old_vert_count - 4)
prim = geom.modify_primitive(0)
prim_array = prim.modify_vertices()
old_index_count = prim_array.get_num_rows()
start = self.start_vert_index_row
end = old_index_count - 6
view = memoryview(prim_array).cast("B").cast("I")
view[start:-6] = view[start+6:]
prim_array.set_num_rows(old_index_count - 6)
prim.offset_vertices(-4, start, end)
sprites = list(self.registry.values())
index = sprites.index(self)
start_row = self.start_vert_row
for sprite in sprites[index+1:]:
sprite.start_vert_row = start_row
start_row += 4
start_row = self.start_vert_index_row
for sprite in sprites[index+1:]:
sprite.start_vert_index_row = start_row
start_row += 6
def destroy(self):
if self._id not in Sprite.registry:
return False
self.__remove_geometry()
del Sprite.registry[self._id]
return True
@property
def model(self):
return self._model
@property
def id(self):
return self._id
def get_pos(self):
return self._pos
def set_pos(self, x, y):
x_old, y_old = self._pos
mat = Mat4.translate_mat(x - x_old, 0., y - y_old)
v_data = self._model.node().modify_geom(0).modify_vertex_data()
start_row = self.start_vert_row
v_data.transform_vertices(mat, start_row, start_row + 4)
self._pos = (x, y)
def move(self, x, y):
mat = Mat4.translate_mat(x, 0., y)
v_data = self._model.node().modify_geom(0).modify_vertex_data()
start_row = self.start_vert_row
v_data.transform_vertices(mat, start_row, start_row + 4)
x_old, y_old = self._pos
self._pos = (x_old + x, y_old + y)
@property
def color(self):
if self._has_model:
return tuple(self._color_array[:4])
@color.setter
def color(self, color):
if self.color != color:
self._color_array = array.array("f", color * 4)
v_data = self._model.node().modify_geom(0).modify_vertex_data()
color_view = memoryview(v_data.modify_array(1)).cast("B").cast("f")
start = self.start_vert_row * 4
end = start + 16
color_view[start:end] = self._color_array
@property
def uvs(self):
return tuple(self._uv_array)
@uvs.setter
def uvs(self, uvs):
self._uv_array = array.array("f", uvs)
v_data = self._model.node().modify_geom(0).modify_vertex_data()
uv_view = memoryview(v_data.modify_array(2)).cast("B").cast("f")
start = self.start_vert_row * 2
end = start + 8
uv_view[start:end] = self._uv_array
main.py
:
#!/usr/bin/env python
from panda3d.core import *
from direct.showbase.ShowBase import ShowBase
from sprite import Sprite
import random
load_prc_file_data("", "sync-video false")
class MyGame(ShowBase):
def __init__(self):
ShowBase.__init__(self)
self.set_frame_rate_meter(True)
self.add_sprites()
self.task_mgr.do_method_later(.5, self.__move_sprites, "move_sprites")
self.task_mgr.do_method_later(1.5, self.__delete_sprites, "delete_sprites")
self.run()
def add_sprites(self):
self.sprites = []
uvs = (
0., 1., # upper left corner
0., 0., # lower left corner
1., 0., # lower right corner
1., 1. # upper right corner
)
sprite = Sprite(100, 70, uvs)
# after creating the first sprite, the model to represent all sprites
# has been created
model = sprite.model
model.reparent_to(self.pixel2d)
tex_atlas = self.loader.load_texture("atlas.png")
model.set_texture(tex_atlas)
sprite.set_pos(200, -100)
self.sprites.append(sprite)
sprite = Sprite(80, 110, uvs)
sprite.set_pos(100, -200)
self.sprites.append(sprite)
# fill the window with sprites
for i in range(500):
x = (i % 20) * 50
y = -(i // 20) * 50
sprite = Sprite(50, 50, uvs)
sprite.set_pos(x, y)
self.sprites.append(sprite)
def __move_sprites(self, task):
for sprite in self.sprites:
sprite.move(random.randint(-5, 5), random.randint(-5, 5))
return task.again
def __delete_sprites(self, task):
if self.sprites:
sprite = self.sprites[random.randint(0, len(self.sprites) - 1)]
sprite.destroy()
self.sprites.remove(sprite)
return task.again
MyGame()
Enjoy the improved framerate
.
The code shows an entire window filled with sprites, each of them moving randomly, while they get removed one by one.
The Sprite
class has a uvs
property that you can easily assign the desired UV coordinates to, as a tuple of eight float values: 2 for the upper left corner of the sprite, 2 for the lower left corner, 2 for the lower right and finally 2 for the upper right corner.
It’s also possible to change the vertex colors of any sprite.
And last but not least, you can set the position of a sprite in two different ways: by setting exact coordinates (calling set_pos
) or by offsetting its current position (using move
).
If you have any questions, please ask
!