为PyOpenGL和NumPy构建交错缓冲区

7 投票
3 回答
1277 浏览
提问于 2025-04-15 19:49

我正在尝试把一堆顶点和纹理坐标放到一个交错数组里,然后再发送给pyOpengl的glInterleavedArrays/glDrawArrays。唯一的问题是,我找不到一个足够快的方法来把数据添加到numpy数组里。

有没有更好的办法呢?我本以为预先分配数组然后填充数据会更快,但实际上生成一个python列表再转换成numpy数组反而“更快”。不过,对于4096个四边形来说,15毫秒似乎还是慢了点。

我附上了一些示例代码和它们的时间记录。

#!/usr/bin/python

import timeit
import numpy
import ctypes
import random

USE_RANDOM=True
USE_STATIC_BUFFER=True

STATIC_BUFFER = numpy.empty(4096*20, dtype=numpy.float32)

def render(i):
    # pretend these are different each time
    if USE_RANDOM:
        tex_left, tex_right, tex_top, tex_bottom = random.random(), random.random(), random.random(), random.random()
        left, right, top, bottom = random.random(), random.random(), random.random(), random.random()
    else:
        tex_left, tex_right, tex_top, tex_bottom = 0.0, 1.0, 1.0, 0.0
        left, right, top, bottom = -1.0, 1.0, 1.0, -1.0

    ibuffer = (
        tex_left, tex_bottom,   left, bottom, 0.0,  # Lower left corner
        tex_right, tex_bottom,  right, bottom, 0.0, # Lower right corner
        tex_right, tex_top,     right, top, 0.0,    # Upper right corner
        tex_left, tex_top,      left, top, 0.0,     # upper left
    )

    return ibuffer



# create python list.. convert to numpy array at end
def create_array_1():
    ibuffer = []
    for x in xrange(4096):
        data = render(x)
        ibuffer += data

    ibuffer = numpy.array(ibuffer, dtype=numpy.float32)
    return ibuffer

# numpy.array, placing individually by index
def create_array_2():
    if USE_STATIC_BUFFER:
        ibuffer = STATIC_BUFFER
    else:
        ibuffer = numpy.empty(4096*20, dtype=numpy.float32)
    index = 0
    for x in xrange(4096):
        data = render(x)
        for v in data:
            ibuffer[index] = v
            index += 1
    return ibuffer

# using slicing
def create_array_3():
    if USE_STATIC_BUFFER:
        ibuffer = STATIC_BUFFER
    else:
        ibuffer = numpy.empty(4096*20, dtype=numpy.float32)
    index = 0
    for x in xrange(4096):
        data = render(x)
        ibuffer[index:index+20] = data
        index += 20
    return ibuffer

# using numpy.concat on a list of ibuffers
def create_array_4():
    ibuffer_concat = []
    for x in xrange(4096):
        data = render(x)
        # converting makes a diff!
        data = numpy.array(data, dtype=numpy.float32)
        ibuffer_concat.append(data)
    return numpy.concatenate(ibuffer_concat)

# using numpy array.put
def create_array_5():
    if USE_STATIC_BUFFER:
        ibuffer = STATIC_BUFFER
    else:
        ibuffer = numpy.empty(4096*20, dtype=numpy.float32)
    index = 0
    for x in xrange(4096):
        data = render(x)
        ibuffer.put( xrange(index, index+20), data)
        index += 20
    return ibuffer

# using ctype array
CTYPES_ARRAY = ctypes.c_float*(4096*20)
def create_array_6():
    ibuffer = []
    for x in xrange(4096):
        data = render(x)
        ibuffer += data
    ibuffer = CTYPES_ARRAY(*ibuffer)
    return ibuffer

def equals(a, b):

    for i,v in enumerate(a):
        if b[i] != v:
            return False
    return True



if __name__ == "__main__":
    number = 100

    # if random, don't try and compare arrays
    if not USE_RANDOM and not USE_STATIC_BUFFER:
        a =  create_array_1()
        assert equals( a, create_array_2() )
        assert equals( a, create_array_3() )
        assert equals( a, create_array_4() )
        assert equals( a, create_array_5() )
        assert equals( a, create_array_6() )

    t = timeit.Timer( "testing2.create_array_1()", "import testing2" )
    print 'from list:', t.timeit(number)/number*1000.0, 'ms'

    t = timeit.Timer( "testing2.create_array_2()", "import testing2" )
    print 'array: indexed:', t.timeit(number)/number*1000.0, 'ms'

    t = timeit.Timer( "testing2.create_array_3()", "import testing2" )
    print 'array: slicing:', t.timeit(number)/number*1000.0, 'ms'

    t = timeit.Timer( "testing2.create_array_4()", "import testing2" )
    print 'array: concat:', t.timeit(number)/number*1000.0, 'ms'

    t = timeit.Timer( "testing2.create_array_5()", "import testing2" )
    print 'array: put:', t.timeit(number)/number*1000.0, 'ms'

    t = timeit.Timer( "testing2.create_array_6()", "import testing2" )
    print 'ctypes float array:', t.timeit(number)/number*1000.0, 'ms'

使用随机数的时间记录:

$ python testing2.py
from list: 15.0486779213 ms
array: indexed: 24.8184704781 ms
array: slicing: 50.2214789391 ms
array: concat: 44.1691994667 ms
array: put: 73.5879898071 ms
ctypes float array: 20.6674289703 ms

编辑说明:更改代码以生成每次渲染的随机数,以减少对象重用,并模拟每次不同的顶点。

编辑说明2:添加了静态缓冲区,并强制所有numpy.empty()使用dtype=float32

备注 1/2010年4月:仍然没有进展,我觉得这些答案都没有解决问题。

3 个回答

0

我知道这听起来有点奇怪,但你试过用 fromfile 吗?

1

使用numpy的好处并不是仅仅把数据存储在一个数组里,而是可以对数组中的多个元素同时进行操作,而不是一个一个地处理。你的例子可以简化并优化成一个非常简单的解决方案,这样可以大幅度提高速度:

numpy.random.standard_normal(4096*20)

...虽然这并不是很有帮助,但它确实暗示了成本在哪里。

这里有一个逐步改进的方法,它比列表追加的解决方案稍微好一点,因为它消除了对4096个元素的逐个遍历。

xs = numpy.arange(4096)
render2 = numpy.vectorize(render)

def create_array_7():
    ibuffer = STATIC_BUFFER
    for i, a in enumerate(render2(xs)):
        ibuffer[i::20] = a
    return ibuffer

...但这还不是我们想要的速度提升。

真正的节省在于重新设计渲染过程,这样就不需要为每一个放入缓冲区的值创建一个python对象。tex_left、tex_right等等这些变量是从哪里来的?它们是计算出来的,还是读取的?

1

create_array_1之所以快,主要是因为在这个(python)列表里的所有项目都指向同一个对象。你可以通过测试来验证这一点:

print (ibuffer[0] is ibuffer[1])

在子程序里面。在create_array_1中,这种情况是成立的(在你创建numpy数组之前),而在create_array_2中,这种情况总是不会成立。我猜这意味着在数组转换的过程中,create_array_1只需要进行一次数据转换,而在create_array_2中则需要进行4096次。

如果真是这样的话,我想如果让render生成随机数据,时间会有所不同。create_array_5是最慢的,因为每次你在末尾添加数据时,它都会创建一个新的数组。

撰写回答