Writing Arbitrary Components to custom GeomVertexArrayData

Hey Epihaius!

Thanks for getting back to me and for the memoryview tip, that definitely helped. I figured out a way to fill the vertex data using NumPy arrays, but the method I had got to be pretty slow with more and more rows in the GeomVertexData. I found this forum post from MaXL130 from last year and tried out their method for filling data from NumPy arrays

It seems like they manage to speed things up with the NumPy arrays by altering the format of the GeomVertexData such that it has one GeomVertexArrayData for each different type of data you want to hold in a vertex, (then each array has only one column). Before the format I was using had one GeomVertexArrayData with multiple columns, where each column represented the different types of data (i.e. vertex, texcoord, spectrum, etc.)

Anyways, I got that to work and compared them for efficiency, and it looks like the altered GeomVertexData format is about 30x faster on average. Pretty sweet! Thanks for the memoryview tip either way, that definitely cleared the roadblock I had with this in the first place!

Here’s the script I have to test the speed of the 2 methods. It’s using 501 rows, 501 components to the ‘spectrum’ data, and is averaging over 1000 loops

# -*- coding: UTF-8 -*-

# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ #
import sys
import os
import array
import numpy as np

from direct.gui.DirectGui import *
from panda3d.core import TextNode, GeomVertexReader, GeomVertexWriter, GeomVertexArrayFormat, GeomVertexArrayData, GeomVertexData, GeomVertexFormat, Geom, LVecBase4f, LVecBase3f, LVecBase2f

import time

# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ #

num_components = 501
num_rows = 501

# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ #

# Create custom GeomVertexArrayFormat with multiple columns
spectrum = GeomVertexArrayFormat( )
spectrum.addColumn( "vertex", 3, Geom.NTFloat32, Geom.CPoint )
spectrum.addColumn( "texcoord", 2, Geom.NTFloat32, Geom.CTexcoord)
spectrum.addColumn( "spectrum", num_components, Geom.NTFloat32, Geom.CPoint )

# Create custom GeomVertexArray - 1 array with multiple columns
spectrum_format = GeomVertexFormat( )
spectrum_format.add_array( spectrum )
spectrum_format = GeomVertexFormat.registerFormat( spectrum_format )

# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ #

# Create custom array formats for each set of data individually, with 1 column each
vertex_col_format = GeomVertexArrayFormat( "vertex", 3, Geom.NTFloat32, Geom.C_point )
texcoord_col_format = GeomVertexArrayFormat( "texcoord", 2, Geom.NTFloat32, Geom.C_texcoord )
spectrum_col_format = GeomVertexArrayFormat( "spectrum", num_components, Geom.NTFloat32, Geom.C_point )

# Add these to a custom vertex format and register it, give a GeomVertexData with multiple arrays, each having 1 column
custom_vertex_format = GeomVertexFormat()
custom_vertex_format.addArray( vertex_col_format )
custom_vertex_format.addArray( texcoord_col_format )
custom_vertex_format.addArray( spectrum_col_format )
custom_vertex_format = GeomVertexFormat.registerFormat( custom_vertex_format )

# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ #

def fill_custom_vertices( num_rows ):

  custom_vdata = GeomVertexData( 'custom_vdata', spectrum_format, Geom.UHStatic )

  custom_vdata.setNumRows( num_rows )
  view = memoryview(custom_vdata.modify_array(0)).cast("B").cast("f")

  vertex_arr = np.arange( 3 )
  texcoord_arr = np.arange( 2 )
  spec_arr = np.arange( num_components )

  values = array.array("f", [])  # a numpy array should also work

  for row in range( num_rows ):
    values.extend(vertex_arr)
    values.extend(texcoord_arr)
    values.extend(spec_arr)

  view[:] = values

  view_obj = view.obj

  # print( view_obj )

# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ #

def fill_custom_vertices3( num_rows ):

  # Create GeomVertexData with custom format and set number of rows
  custom_vdata = GeomVertexData( 'custom_vdata', custom_vertex_format, Geom.UHStatic )

  custom_vdata.setNumRows( num_rows )

  # NumPy arrays to copy to GeomVertexData
  vertex_arr = np.ones( ( num_rows, 3 ) )
  texcoord_arr = np.zeros( ( num_rows, 2 ) )
  spec_arr = 2*np.ones( ( num_rows, num_components ) )

  arrayHandle0: GeomVertexArrayData = custom_vdata.modifyArray( 0 )
  arrayHandle1: GeomVertexArrayData = custom_vdata.modifyArray( 1 )
  arrayHandle2: GeomVertexArrayData = custom_vdata.modifyArray( 2 )

  # Make sure the data is copied using the right type
  arrayHandle0.modifyHandle().copyDataFrom( vertex_arr.astype( np.float32 ) )
  arrayHandle1.modifyHandle().copyDataFrom( texcoord_arr.astype( np.float32 ) )
  arrayHandle2.modifyHandle().copyDataFrom( spec_arr.astype( np.float32 ) )

  # print( custom_vdata )

# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ #

if __name__ == "__main__":

  time_loops = 1000

  # -------------------------------------------------------------------------- #

  start_time = time.time()
  for t in range( time_loops ):
    fill_custom_vertices( num_rows = num_rows )
  elapsed_time = time.time( ) - start_time
  avg_time = elapsed_time / time_loops

  print( elapsed_time )
  print( f"Average time for memory view custom vertices: {avg_time} seconds" )

  # -------------------------------------------------------------------------- #

  start_time = time.time()
  for t in range( time_loops ):
    fill_custom_vertices3( num_rows = num_rows )
  elapsed_time = time.time( ) - start_time
  avg_time = elapsed_time / time_loops

  print( elapsed_time )
  print( f"Average time for alt numpy array custom vertices: {avg_time} seconds" )