Procedurally efficient way to create vertices, faces, etc

Hi everyone,

I would like to know if there is a function to add vertices from a list to the GeomVertexData instead of calling GeomVertexWriter and doing a for loop?
Is there the same for GeomTrifans?

Cheers all!

Firstly, I should point out that to get good performance out of filling in a GeomVertexData, you first need to call setNumRows(n) (or even uncleanSetNumRows(n) if you are starting with an empty one and are going to overwrite all the rows). Otherwise, Panda will need to reallocate memory many times as you add more rows. Similar for GeomTrifans, there is a reserveNumVertices that preallocates the right amount of memory ahead of time.

Secondly, you can directly access the raw rows via python and load it in via a memoryview, though this is a more difficult interface that requires you to understand how the data is laid out in memory. This method has you directly access interleaved vertex, normal, uv (whatever columns you have) data as an array of floating-point numbers. This can be more efficient, however. Let me know whether you need additional help with this.

1 Like

Here is an example of using a memoryview, together with either a struct or an array (note that I’m not yet all that comfortable using these data types myself, so I can’t really say which is more efficient - or if I’m even doing it efficiently at all!):

from panda3d.core import *
from direct.showbase.ShowBase import ShowBase
import array
import struct


class MyApp(ShowBase):

    def __init__(self):

        ShowBase.__init__(self)

        # define the vertex positions as a sequence of x, y, z coordinates;
        coords = [1., -3., 7., -4., 12., 5., 8., 2., -1., -6., 14., -11.]
        # either convert this sequence to an array of bytes...
        # (note that if you call tobytes() on it, you don't need to call
        # cast("f") on the memoryview, below)
        pos_data = array.array("f", coords)#.tobytes()
        # ...or, alternatively, use a struct:
#        pos_data = struct.pack("{:d}f".format(len(coords)), *coords)

        vertex_format = GeomVertexFormat.get_v3()
        vertex_data = GeomVertexData("vertex_data", vertex_format, Geom.UH_static)
        # make sure the vertex data has the correct size by setting the exact
        # number of rows *before* using a memoryview, since you cannot change
        # its size while working with that memoryview; call unclean_set_num_rows
        # if there isn't any data in the vertex table yet, as in this example
        vertex_data.unclean_set_num_rows(4)
        pos_array = vertex_data.modify_array(0)
        # create a memoryview from the GeomVertexArrayData
        # (note that you should not call cast("f") on it if you used a struct
        # for the position data)
        memview = memoryview(pos_array).cast("B").cast("f")
        # assign the vertex position data to a slice of the memoryview
        # (in this case the entire memoryview)
        memview[:] = pos_data
        print("Array:\n", vertex_data.get_array(0))


app = MyApp()
app.run()
1 Like

Hey,

Thx for all the answers ^^
After adding the vertices, I tried to do the same for normals by changing the index of modify_array but I get the following message error:
ValueError: memoryview assignment: lvalue and rvalue have different structures
Any idea on how to solve this?

Cheers all!

EDIT : It’s fine I solved my problem I just had to append the 2 lists into 1 such following the rows such that:
n=[xn1, yn1, zn1, …], v=[xv1, yv1, zv1, …] => nv = [xv1, yv1, zv1, xn1, yn1, zn1, …]
EDIT : I timed the copy from the list to the GeomVertexData and memview method halves the time it takes compared to the GeomVertexWriter but the time it takes to transform the list for memview makes the GeomVertexWriter slightly faster ^^

That’s good to know! Thanks for testing this :slight_smile: !

If converting a list into an array (which I only did because I didn’t know how you were getting your data) is too slow (if I understood you correctly), you could try to gather your data into an array to start with, instead of bothering with a list at all.

1 Like

I didn’t thought of that ^^
EDIT : Well now it works, I read a stl file and append the vertices and normals in a single array. Then I use Numpy to do some calculus and finally the memview with Numpy by being aware of the float precision and the shape of the array : numpy.float32 with the shape (nb of elements, ) XD

Note that instead of array.array, you can also directly use a numpy array.

Hi rdb,

How can I use memview with a GeomTrisFans or a GeomTriangles?

Cheers all!

Hmmm, I was going to suggest to simply call cast("I") on the memoryview and also use "I" as the array format (since we’re dealing with a GeomVertexArrayData containing integers instead of floats), but it doesn’t seem to be that simple. The size of the resulting memoryview is half of what it should be:

from panda3d.core import *
from direct.showbase.ShowBase import ShowBase
import array


class MyApp(ShowBase):

    def __init__(self):

        ShowBase.__init__(self)

        tri_data = array.array("I", [0, 1, 2, 0, 2, 3])
        tris_prim = GeomTriangles(Geom.UH_static)
        tris_prim.reserve_num_vertices(len(tri_data))
        tris_array = tris_prim.modify_vertices()
        tris_array.unclean_set_num_rows(len(tri_data))
        memview = memoryview(tris_array).cast("B").cast("I")
        print("memview size:", len(memview))
        memview[:] = tri_data


app = MyApp()
app.run()

Output:

memview size: 3
Traceback (most recent call last):
  File "bug.py", line 22, in <module>
    app = MyApp()
  File "bug.py", line 19, in __init__
    memview[:] = tri_data
ValueError: memoryview assignment: lvalue and rvalue have different structures

@rdb Any idea what I’m doing wrong?

Hi Epihaius,

I searched a little and have done some experimentation to make it work ^^

from panda3d.core import *
from direct.showbase.ShowBase import ShowBase
import array

class MyApp(ShowBase):

    def __init__(self):

        ShowBase.__init__(self)

        tri_data = array.array("H", [0, 1, 2, 0, 2, 3])
        tris_prim = GeomTriangles(Geom.UH_static)
        tris_prim.reserve_num_vertices(len(tri_data))
        tris_array = tris_prim.modify_vertices()
        
        tris_array.unclean_set_num_rows(len(tri_data));
        memview = memoryview(tris_array)
        print('Memview content : {}'.format(memview.tolist()))
        print('Memview format : {}'.format(memview.format))
        print('Memview length : {}'.format(len(memview)))
        memview[:] = tri_data
        print('Copy to memview : {}'.format(memview.tolist()))
        print('tri_data :')
        print(tris_prim)

app = MyApp()

memview content : [28472, 795, 0, 0, 0, 0]
memview format : H
memview length : 6
Copy to memview : [0, 1, 2, 0, 2, 3]
tri_data :
GeomTriangles (indexed):
  [ 0 1 2 ]
  [ 0 2 3 ]

Hey, that’s great! Then it’s even simpler than I thought :slight_smile: .
Still strange that it doesn’t work using the I format, but perhaps someone will explain that to us one day :wink: .

My example in this post has been updated to make use of this now.

It seems the ‘H’ is a uint16 extension.:sweat_smile:
EDIT : It seems if you have a large number of faces, you should use an array with ‘I’ extension (uint32).

Panda uses 16-bits indices (H) unless you add enough vertices to require 32-bit indices, since 16-bit indices are more compact and more universally supported by older drivers. If you have more than 65534 vertices, use 32-bit indices (it’s best not to use the highest possible index since that is often used as a special strip cut index).

You can call tris_prim.set_index_type(Geom.NT_uint32) to explicitly force 32-bit indices.

2 Likes