Source code for pi3d.Buffer

from __future__ import absolute_import, division, print_function, unicode_literals

import ctypes, itertools
import numpy as np
import logging

from ctypes import c_float, c_int, c_short

from pi3d.constants import (opengles, GL_ARRAY_BUFFER, GL_BLEND, GL_DEPTH_TEST,
               GL_ELEMENT_ARRAY_BUFFER, GL_FLOAT, GL_OUT_OF_MEMORY, GL_STATIC_DRAW,
               GL_TEXTURE0, GL_TEXTURE_2D, GL_TRIANGLES, GL_UNSIGNED_SHORT,
               GLint, GLuint, GLfloat, GLsizei, GLboolean, GLintptr)
from pi3d.Shader import Shader
from pi3d.util import Log
from pi3d.util import Utility
from pi3d.util.Loadable import Loadable
from pi3d.util.Ctypes import c_floats, c_shorts

LOGGER = logging.getLogger(__name__)

[docs]class Buffer(Loadable): """Holds the vertex, normals, incices and tex_coords for each part of a Shape that needs to be rendered with a different material or texture Shape holds an array of Buffer objects. """ def __init__(self, shape, pts, texcoords, faces, normals=None, smooth=True): """Generate a vertex buffer to hold data and indices. If no normals are provided then these are generated. Arguments: *shape* Shape object that this Buffer is a child of *pts* array of vertices tuples i.e. [(x0,y0,z0), (x1,y1,z1),...] *texcoords* array of texture (uv) coordinates tuples i.e. [(u0,v0), (u1,v1),...] *faces* array of indices (of pts array) defining triangles i.e. [(a0,b0,c0), (a1,b1,c1),...] Keyword arguments: *normals* array of vector component tuples defining normals at each vertex i.e. [(x0,y0,z0), (x1,y1,z1),...] *smooth* if calculating normals then average normals for all faces meeting at this vertex, otherwise just use first (for speed). """ super(Buffer, self).__init__() # Uniform variables all in one array! self.unib = (c_float * 15)(0.0, 0.0, 0.0, 0.5, 0.5, 0.5, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.5, 0.5, 0.5) """ pass to shader array of vec3 uniform variables: ===== ============================== ==== == vec3 description python ----- ------------------------------ ------- index from to ===== ============================== ==== == 0 ntile, shiny, blend 0 2 1 material 3 5 2 umult, vmult, point_size 6 8 3 u_off, v_off, line_width/bump 9 10 4 specular RGB value *_reflect 11 14 ===== ============================== ==== == NB line width and bump factor clash but shouldn't be an issue """ #self.shape = shape self.textures = [] self.shader = None #self.indices = np.array(faces, dtype="short") # needed in calc_normals self.element_array_buffer = np.array(faces, dtype="short") self.ntris = len(self.element_array_buffer) self.element_normals = None # filled by calc_normals() to speed up ElevationMap.calcHeight() n_verts = len(pts) if len(texcoords) != n_verts: if normals is not None and len(normals) != n_verts: self.N_BYTES = 12 # only use vertices bufw = 3 # width to create array_buffer else: self.N_BYTES = 24 # use pts and normals bufw = 6 else: self.N_BYTES = 32 # use all three NB doesn't check that normals are there bufw = 8 self.array_buffer = np.zeros((n_verts, bufw), dtype="float32") if n_verts > 0: # TODO Mergeshape starts out with an empty buffer self.array_buffer[:,0:3] = np.array(pts, dtype="float32") if bufw == 8: self.array_buffer[:,6:8] = np.array(texcoords, dtype="float32") if bufw > 3: if normals is None: #i.e. normals will only be generated if explictly None self.array_buffer[:,3:6] = self.calc_normals() else: self.array_buffer[:,3:6] = np.array(normals, dtype="float32") self.material = (0.5, 0.5, 0.5, 1.0) self.draw_method = GL_TRIANGLES from pi3d.Display import Display self.disp = Display.INSTANCE # rely on there always being one!
[docs] def calc_normals(self): normals = np.zeros((len(self.array_buffer), 3), dtype="float32") #empty array rights size fv = self.array_buffer[self.element_array_buffer,0:3] #expand faces with x,y,z values for each vertex #cross product of two edges of triangles self.element_normals = np.cross(fv[:,1] - fv[:,0], fv[:,2] - fv[:,0]) self.element_normals = Utility.normalize_v3(self.element_normals) normals[self.element_array_buffer[:,0]] += self.element_normals #add up all normal vectors for a vertex normals[self.element_array_buffer[:,1]] += self.element_normals normals[self.element_array_buffer[:,2]] += self.element_normals return Utility.normalize_v3(normals)
def __del__(self): #super(Buffer, self).__del__() #TODO supposed to always call super.__del__ if not self.opengl_loaded: return True self.disp.vbufs_dict[str(self.vbuf)][1] = 1 self.disp.ebufs_dict[str(self.ebuf)][1] = 1 self.disp.tidy_needed = True
[docs] def re_init(self, pts=None, texcoords=None, normals=None, offset=0): """Only reset the opengl buffer variables: vertices, tex_coords, normals (which will not be generated if not supplied) **NB this method will go horribly wrong if you change the size of the arrays supplied in the argument as the opengles buffers are reused At least one of pts, texcoords or normals must be a list** This method will run faster if the new data is passed as numpy 2D arrays. Arguments: *pts* numpy 2D array or list of (x,y,z) tuples, default None *texcoords* numpy 2D array or list of (u,v) tuples, default None *normals* numpy 2D array or list of (x,y,z) tuples, default None *offset* number of vertices offset from the start of vertices, default 0 """ if self.disp is None: return # can't re_init until after initial drawing! if pts is not None: n = len(pts) if not (isinstance(pts, np.ndarray)): pts = np.array(pts) self.array_buffer[offset:(offset + n), 0:3] = pts[:,:] if normals is not None: n = len(normals) if not (isinstance(normals, np.ndarray)): normals = np.array(normals) self.array_buffer[offset:(offset + n), 3:6] = normals[:,:] if texcoords is not None: n = len(texcoords) if not (isinstance(texcoords, np.ndarray)): texcoords = np.array(texcoords) self.array_buffer[offset:(offset + n), 6:8] = texcoords[:,:] try: getattr(self, "vbuf") except: self.load_opengl() # vbuf and ebuf need to exist prior to _select() self._select() opengles.glBufferSubData(GL_ARRAY_BUFFER, GLintptr(0), self.array_buffer.nbytes, self.array_buffer.ctypes.data_as(ctypes.POINTER(GLfloat)))
def _load_opengl(self): self.vbuf = GLuint() opengles.glGenBuffers(GLsizei(1), ctypes.byref(self.vbuf)) self.ebuf = GLuint() opengles.glGenBuffers(GLsizei(1), ctypes.byref(self.ebuf)) self.disp.vbufs_dict[str(self.vbuf)] = [self.vbuf, 0] self.disp.ebufs_dict[str(self.ebuf)] = [self.ebuf, 0] self._select() opengles.glBufferData(GL_ARRAY_BUFFER, self.array_buffer.nbytes, self.array_buffer.ctypes.data_as(ctypes.POINTER(GLfloat)), GL_STATIC_DRAW) opengles.glBufferData(GL_ELEMENT_ARRAY_BUFFER, self.element_array_buffer.nbytes, self.element_array_buffer.ctypes.data_as(ctypes.POINTER(GLfloat)), GL_STATIC_DRAW) if opengles.glGetError() == GL_OUT_OF_MEMORY: LOGGER.critical('Out of GPU memory in Buffer._load_opengl') def _unload_opengl(self): opengles.glDeleteBuffers(1, ctypes.byref(self.vbuf)) opengles.glDeleteBuffers(1, ctypes.byref(self.ebuf)) def _select(self): """Makes our buffers active.""" opengles.glBindBuffer(GL_ARRAY_BUFFER, self.vbuf) opengles.glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, self.ebuf)
[docs] def set_draw_details(self, shader, textures, ntiles=0.0, shiny=0.0, umult=1.0, vmult=1.0, bump_factor=1.0): """Can be used to set information needed for drawing as a one off rather than sending as arguments to draw(). Arguments: *shader* Shader object *textures* array of Texture objects Keyword arguments: *ntiles* multiple for tiling normal map which can be less than or greater than 1.0. 0.0 disables the normal mapping, float *shiny* how strong to make the reflection 0.0 to 1.0, float *umult* multiplier for tiling the texture in the u direction *vmult* multiplier for tiling the texture in the v direction *bump_factor* multiplier for the normal map surface distortion effect """ self.shader = shader self.textures = textures # array of Textures self.unib[0] = ntiles self.unib[1] = shiny self.unib[6] = umult self.unib[7] = vmult self.unib[11] = bump_factor
[docs] def set_material(self, mtrl): self.unib[3:6] = mtrl[0:3]
[docs] def set_textures(self, textures): self.textures = textures
[docs] def set_offset(self, offset=(0.0, 0.0)): self.unib[9:11] = offset
[docs] def draw(self, shape=None, M=None, unif=None, shader=None, textures=None, ntl=None, shny=None, fullset=True): """Draw this Buffer, called by the parent Shape.draw() Keyword arguments: *shape* Shape object this Buffer belongs to, has to be passed at draw to avoid circular reference *shader* Shader object *textures* array of Texture objects *ntl* multiple for tiling normal map which can be less than or greater than 1.0. 0.0 disables the normal mapping, float *shiny* how strong to make the reflection 0.0 to 1.0, float """ self.load_opengl() shader = shader or self.shader or shape.shader or Shader.instance() shader.use() opengles.glUniformMatrix4fv(shader.unif_modelviewmatrix, GLsizei(3), GLboolean(0), M.ctypes.data) opengles.glUniform3fv(shader.unif_unif, GLsizei(20), unif) textures = textures or self.textures if ntl is not None: self.unib[0] = ntl if shny is not None: self.unib[1] = shny self._select() opengles.glVertexAttribPointer(shader.attr_vertex, GLint(3), GL_FLOAT, GLboolean(0), self.N_BYTES, 0) opengles.glEnableVertexAttribArray(shader.attr_vertex) if self.N_BYTES > 12: opengles.glVertexAttribPointer(shader.attr_normal, GLint(3), GL_FLOAT, GLboolean(0), self.N_BYTES, 12) opengles.glEnableVertexAttribArray(shader.attr_normal) if self.N_BYTES > 24: opengles.glVertexAttribPointer(shader.attr_texcoord, GLint(2), GL_FLOAT, GLboolean(0), self.N_BYTES, 24) opengles.glEnableVertexAttribArray(shader.attr_texcoord) opengles.glDisable(GL_BLEND) self.unib[2] = 0.6 for t, texture in enumerate(textures): if (self.disp.last_textures[t] != texture or self.disp.last_shader != shader or self.disp.offscreen_tex): # very slight speed increase for sprites opengles.glActiveTexture(GL_TEXTURE0 + t) assert texture.tex(), 'There was an empty texture in your Buffer.' opengles.glBindTexture(GL_TEXTURE_2D, texture.tex()) opengles.glUniform1i(shader.unif_tex[t], GLint(t)) self.disp.last_textures[t] = texture if texture.blend: # i.e. if any of the textures set to blend then all will for this shader. self.unib[2] = 0.0035 # i.e. alpha byte in image 0b00000001 is 0.00392 if self.unib[2] != 0.6 or shape.unif[16] < 1.0 or shape.unif[17] < 1.0: #use unib[2] as flag to indicate if any Textures to be blended #needs to be done outside for..textures so materials can be transparent opengles.glEnable(GL_BLEND) self.unib[2] = 0.0035 self.disp.last_shader = shader opengles.glUniform3fv(shader.unif_unib, GLsizei(5), self.unib) opengles.glEnable(GL_DEPTH_TEST) # TODO find somewhere more efficient to do this if self.draw_method != GL_TRIANGLES: opengles.glLineWidth(GLfloat(self.unib[11])) opengles.glDrawElements(self.draw_method, GLsizei(self.ntris * 3), GL_UNSIGNED_SHORT, 0)
# Implement pickle/unpickle support def __getstate__(self): return { 'unib': list(self.unib), 'array_buffer': self.array_buffer, 'element_array_buffer': self.element_array_buffer, 'element_normals': self.element_normals, 'material': self.material, 'textures': self.textures, 'draw_method': self.draw_method, 'ntris': self.ntris, 'N_BYTES': self.N_BYTES } def __setstate__(self, state): unib_tuple = tuple(state['unib']) self.unib = (ctypes.c_float * 15)(*unib_tuple) self.array_buffer = state['array_buffer'] self.element_array_buffer = state['element_array_buffer'] self.element_normals = state['element_normals'] self.material = state['material'] self.textures = state['textures'] self.draw_method = state['draw_method'] self.opengl_loaded = False self.disk_loaded = True self.ntris = state['ntris'] self.N_BYTES = state['N_BYTES'] from pi3d.Display import Display self.disp = Display.INSTANCE # rely on there always being one!