Source code for fieldanimation

import os
import numpy as np
import ctypes
import OpenGL.GL as gl

# Local imports
from .shader import Shader
from .texture import Texture
from .__version__ import __version__

# Fix ramdom sequence seed
np.random.seed(123)

GLSLDIR = os.path.join(os.path.dirname(__file__ ), 'glsl')
WORKGROUP_SIZE = 32

#------------------------------------------------------------------------------
[docs]def glInfo(): """ Return OpenGL information dict WARNING: OpenGL context MUST be initialized !!! Args: None Returns: OpenGL information dict """ major = gl.glGetIntegerv(gl.GL_MAJOR_VERSION) minor = gl.glGetIntegerv(gl.GL_MINOR_VERSION) version = gl.glGetString(gl.GL_VERSION) vendor = gl.glGetString(gl.GL_VENDOR) renderer = gl.glGetString(gl.GL_RENDERER) glsl = gl.glGetString(gl.GL_SHADING_LANGUAGE_VERSION) glversion = float("%d.%d" % (major, minor)) retval = { 'glversion': glversion, 'version': version, 'vendor': vendor, 'renderer': renderer, 'glsl': glsl, } if glversion >= 4.3: count = np.zeros(3, dtype=np.int32) size = np.zeros(3, dtype=np.int32) count[0] = gl.glGetIntegeri_v(gl.GL_MAX_COMPUTE_WORK_GROUP_COUNT, 0)[0] count[1] = gl.glGetIntegeri_v(gl.GL_MAX_COMPUTE_WORK_GROUP_COUNT, 1)[0] count[2] = gl.glGetIntegeri_v(gl.GL_MAX_COMPUTE_WORK_GROUP_COUNT, 2)[0] size[0] = gl.glGetIntegeri_v(gl.GL_MAX_COMPUTE_WORK_GROUP_SIZE, 0)[0] size[1] = gl.glGetIntegeri_v(gl.GL_MAX_COMPUTE_WORK_GROUP_SIZE, 1)[0] size[2] = gl.glGetIntegeri_v(gl.GL_MAX_COMPUTE_WORK_GROUP_SIZE, 2)[0] retval['maxComputeWorkGroupCount'] = count retval['maxComputeWorkGroupSize'] = size return retval
#------------------------------------------------------------------------------
[docs]def field2RGB(field): """ Return 2D field converted to uint8 RGB image (i.e. scaled in [0, 255]) Args: field (:class:`numpy.ndarray`): (u, v) 2D vector field instance Returns: ( rgb (:class:`numpy.ndarray`): uint8 RGB image, uMin (float): u min, uMax (float): u max, vMin (float): v min, vMax (float): v max, ) (tuple): Return value """ rows = field.shape[0] cols = field.shape[1] u = field[:, :, 0] v = field[:, :, 1] uMin = u.min() uMax = u.max() vMin = v.min() vMax = v.max() rgb = np.zeros((rows, cols, 3), dtype=np.uint8) rgb[:, :, 0] = 255 * (u - uMin) / ( uMax - uMin) rgb[:, :, 1] = 255 * (v - vMin) / ( vMax - vMin) return rgb, uMin, uMax, vMin, vMax
#------------------------------------------------------------------------------
[docs]def modulus(field): """ Return normalized modulus of 2D field image Returns: normalized modulus of 2D field image (i.e. scaled in [0, 1.]) """ modulus = np.flipud(np.hypot(field[:, :, 0], field[:,:,1])) return np.asarray(modulus/modulus.max(), np.float32)
#==============================================================================
[docs]class FieldAnimation(object): """ Field Animation with OpenGL 1. draw the modulus of the vector field or a user defined image if requested; 2. set a framebuffer texture (screen texture) as the main rendering target: (a) draw the background texture on the screen texture with a fixed opacity; (b) decode the particles positions from the currentTracersPosition texture and draw them on the screen texture; 3. set the rendering target to the active window; 4. draw screen texture on the active window; 5. swap screen texture and background texture; 6. calculate the new particles positions (in the update shader) and encode them in the nextTracersPosition texture; 7. swap nextTracersPosition texture and currentTracersPosition texture; """
[docs] def __init__(self, width, height, field, computeSahder=False, image=None): """ Animate 2D vector field Args: width (int): width in pixels height (int): height in pixels field (np.ndarray): 2D vector field cs = True selects the compute shader version image = Optional background image """ self.useComputeShader = computeSahder self.image = image # Parameters that can be changed later self.periodic = True self.drawField = False self.fadeOpacity = 0.996 self.decayBoost = 0.01 self.speedFactor = 0.25 self.decay = 0.003 self.palette = True self.color = (0.5, 1.0, 1.0) self.pointSize = 1.0 self._tracersCount = 10000 self.fieldScaling = 1.0 # These are fixed self.w_width = width self.w_height = height # Since points are in [0, 1] a traslation and a scaling is needed on # the model matrix T = np.eye(4, dtype=np.float32) T[:, -1] = (-1., 1., 0, 1) S = (np.eye(4, dtype=np.float32) * np.array((2., -2., 1., 1.), dtype=np.float32)) # Model transform matrix model = np.dot(T, S) # View matrix view = np.eye(4) # Projection matrix proj = np.eye(4) self.drawMVP = np.dot(model, np.dot(view, proj)) self.fieldMVP = np.eye(4) # CubeHelix color palette parameters cubeHelixParams =( ('start', 'f'), ('gamma', 'f'), ('rot', 'f'), ('reverse', 'b'), ('minSat', 'f'), ('maxSat', 'f'), ('minLight', 'f'), ('maxLight', 'f'), ('startHue', 'f'), ('endHue', 'f'), ('useHue', 'b'), ) # Create Shader program and uniforms for the vector field self.fieldProgram = Shader(vertex='field.vert', fragment='field.frag', path=GLSLDIR) self.fieldProgram.addUniforms((('gMap', 'i'), ('MVP', 'mat4'), ) + cubeHelixParams) # Create Shader program and uniforms for the tracers self.drawProgram = Shader(vertex='draw.vert', fragment='draw.frag', path=GLSLDIR) self.drawProgram.addUniforms(( ('MVP', 'mat4'), ('u_tracers', 'i'), ('u_tracersRes', 'f'), ('palette', 'b'), ('pointSize', 'f'), ('u_field', 'i'), ('u_fieldMin', '2f'), ('u_fieldMax', '2f')) + cubeHelixParams) # Create Shader program and uniforms for updating the screen self.screenProgram = Shader(vertex='quad.vert', fragment='screen.frag', path=GLSLDIR) self.screenProgram.addUniforms(( ('u_screen', 'i'), ('u_opacity', 'f'))) # Create image background shader if self.image is not None: self.imageProgram = Shader(vertex='field.vert', fragment='image.frag', path=GLSLDIR) self.imageProgram.addUniforms((('gMap', 'i'), ('MVP', 'mat4'),)) # Create Shader program and uniforms for updating the tracers position if self.useComputeShader: self.updateProgram = Shader(compute='update.comp', path=GLSLDIR) else: self.updateProgram = Shader(vertex='quad.vert', fragment='update.frag', path=GLSLDIR) self.updateProgram.addUniforms(( ('u_tracers', 'i'), ('u_field', 'i'), ('u_fieldRes', '2f'), ('u_fieldMin', '2f'), ('u_fieldMax', '2f'), ('u_rand_seed', 'f'), ('u_speed_factor', 'f'), ('u_decay', 'f'), ('u_decay_boost', 'f'), ('fieldScaling', 'f'), ('periodic', 'b'))) # Set the vector field self.setField(field) self._initTracers()
[docs] def setField(self, field): """ Set the 2D vector field. Must be called every time a new vetor field is selected. Args: field (np.ndarray): 2D vector field """ # Automatic field scalking self.fieldScaling = self.speedFactor * 0.01 / field.max() # Prepare the data self._fieldAsRGB, uMin, uMax, vMin, vMax = field2RGB(field) # Compute field modulus self.modulus = modulus(field) # Set values that will not change self.drawProgram.bind() self.drawProgram.setUniform('u_fieldMin', (uMin, vMin)) self.drawProgram.setUniform('u_fieldMax', (uMax, vMax)) self.drawProgram.unbind() # Set values that will not change self.updateProgram.bind() self.updateProgram.setUniform('u_fieldMin', (uMin, vMin)) self.updateProgram.setUniform('u_fieldMax', (uMax, vMax)) self.updateProgram.unbind() self._initTracers()
[docs] def setRenderingTarget(self, texture): """ Set texture as rendering target Args: texture (class:`texture` instance): 2D vector field """ gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, self.frameBuffer) gl.glFramebufferTexture2D(gl.GL_FRAMEBUFFER, gl.GL_COLOR_ATTACHMENT0, gl.GL_TEXTURE_2D, texture.handle(), 0)
[docs] def setSize(self, width, height): """ Set instance size. Must be called when the window is resized. Args: width (int): window width in pixels height (int): window height in pixels """ self.w_width = width self.w_height = height self._initTracers()
[docs] def resetRenderingTarget(self): """ Bind first (default) framebuffer and reset the viewport. """ gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, 0) gl.glViewport(0, 0, self.w_width, self.w_height)
@property def tracersCount(self): """ Return tracers count Returns: number of tracers """ return self._tracersCount @tracersCount.setter def tracersCount(self, value): """ Tracer count setter method, calls self._initTracers under the hood. Args: value (int): number of tracers to create """ self._tracersCount = value self._initTracers()
[docs] def _initTracers(self): """ Initialize the tracers positions """ # Create a buffer for the tracers self.emptyPixels = np.zeros((self.w_width * self.w_height * 4), np.uint8) # Initial random tracers position self.tracers = np.asarray(255.0 * np.random.random( self._tracersCount * 4), dtype=np.uint8, order='C') self.tracersRes = np.ceil(np.sqrt(self.tracers.size / 4)) self.iTracers = np.arange(self.tracers.size / 4, dtype=np.float32) # Create all textures # Tracers position stored in texture 0 self._currentTracersPos = Texture( data=self.tracers, width=self.tracersRes, height=self.tracersRes) # Initial random tracers position stored in texture 1 self._nextTracersPos = Texture( data=self.tracers, width=self.tracersRes, height=self.tracersRes) self.fieldTexture = Texture(data=self._fieldAsRGB, width=self._fieldAsRGB.shape[1], height=self._fieldAsRGB.shape[0], filt=gl.GL_LINEAR) self.backgroundTexture = Texture(data=self.emptyPixels, width=self.w_width, height=self.w_height) self.screenTexture = Texture(data=self.emptyPixels, width=self.w_width, height=self.w_height) # VAOS values = np.zeros(int(self.tracers.size/4), [('a_index', 'f4')]) values['a_index'] = np.asarray(self.iTracers, dtype=np.float32, order='C') if self.image is not None: self.imageTexture = Texture(data=self.image, dtype=gl.GL_UNSIGNED_BYTE) self.modulusTexture = Texture(data=self.modulus, dtype=gl.GL_FLOAT) ## VAO index self._vao = gl.glGenVertexArrays(1) vbo = gl.glGenBuffers(1) gl.glBindVertexArray(self._vao) gl.glBindBuffer(gl.GL_ARRAY_BUFFER, vbo) gl.glBufferData(gl.GL_ARRAY_BUFFER, values, gl.GL_STATIC_DRAW) gl.glEnableVertexAttribArray(0) gl.glVertexAttribPointer(0, 1, gl.GL_FLOAT, gl.GL_FALSE, 0, ctypes.c_void_p(0)) gl.glBindBuffer(gl.GL_ARRAY_BUFFER, 0) gl.glBindVertexArray(0) # Screen quad quad = np.zeros(4, dtype=[('vert', 'f4', 2),('tex', 'f4', 2)]) quad['vert'] = np.array([ [-1, 1], [1, 1], [1, -1], [-1, -1]], np.float32) quad['tex'] = np.array([ [0, 1], [1, 1], [1, 0], [0, 0]], np.float32) indices = np.array([0, 1, 2, 2, 3, 0], np.int32) self._vaoQuad = gl.glGenVertexArrays(1) gl.glBindVertexArray(self._vaoQuad) quadVBO = gl.glGenBuffers(1) gl.glBindBuffer(gl.GL_ARRAY_BUFFER, quadVBO) gl.glBufferData(gl.GL_ARRAY_BUFFER, quad, gl.GL_STATIC_DRAW) self.IBO = gl.glGenBuffers(1) gl.glBindBuffer(gl.GL_ELEMENT_ARRAY_BUFFER, self.IBO) gl.glBufferData(gl.GL_ELEMENT_ARRAY_BUFFER, indices, gl.GL_STATIC_DRAW) gl.glEnableVertexAttribArray(0) gl.glVertexAttribPointer(0, 2, gl.GL_FLOAT, gl.GL_FALSE, 4 * 4, ctypes.c_void_p(0)) gl.glEnableVertexAttribArray(1) gl.glVertexAttribPointer(1, 2, gl.GL_FLOAT, gl.GL_FALSE, 4 * 4, ctypes.c_void_p(8)) gl.glBindBuffer(gl.GL_ARRAY_BUFFER, 0) gl.glBindBuffer(gl.GL_ELEMENT_ARRAY_BUFFER, 0) gl.glBindVertexArray(0) self.frameBuffer = gl.glGenFramebuffers(1)
[docs] def draw(self): """ Render the OpenGL scene. This method is called automatically when the scene has to be rendered and is responsible for the animation. """ if self.drawField: self.drawModulus(1.0) if self.image is not None: self.drawImage() self.fieldTexture.bind(0) ## Bind texture with random tracers position self._currentTracersPos.bind(1) self.drawScreen() if self.useComputeShader: self.updateTracersCS() else: self.updateTracers()
[docs] def drawScreen(self): """ Draw background texture and tracers on screen framebuffer texture """ # Draw background texture and tracers on screen framebuffer texture self.setRenderingTarget(self.screenTexture) gl.glViewport(0, 0, self.w_width, self.w_height) self.drawTexture(self.backgroundTexture, self.fadeOpacity) self.drawTracers() self.resetRenderingTarget() # Draw the screen framebuffer texture on the monitor window # Enable blending to support drawing on top of an existing background #(e.g. a map) gl.glEnable(gl.GL_BLEND) gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA) self.drawTexture(self.screenTexture, 1.0) gl.glDisable(gl.GL_BLEND) self.backgroundTexture , self.screenTexture = ( self.screenTexture, self.backgroundTexture)
[docs] def drawModulus(self, opacity): """ Draw the modulus texture. Args: opacity (float): opacity (alpha) of the texture: 0 --> transparent 1 --> opaque """ self.modulusTexture.bind() self.fieldProgram.bind() self.fieldProgram.setUniforms(( ('gMap', 0), ('MVP', self.fieldMVP), ('start', 1.0), ('gamma', 0.9), ('rot', -5.3), ('reverse', False), ('minSat', 0.2), ('maxSat', 5.0), ('minLight', 0.5), ('maxLight', 0.9), ('startHue', 240.0), ('endHue', -300.0), ('useHue', True), )) gl.glBindVertexArray(self._vaoQuad) gl.glBindBuffer(gl.GL_ELEMENT_ARRAY_BUFFER, self.IBO) gl.glDrawElements(gl.GL_TRIANGLES, 6, gl.GL_UNSIGNED_INT, ctypes.c_void_p(0)) self.fieldProgram.unbind() gl.glBindBuffer(gl.GL_ELEMENT_ARRAY_BUFFER, 0) gl.glBindVertexArray(0) self.modulusTexture.unbind()
[docs] def drawImage(self): """ Draw an image texture in background. """ self.imageProgram.bind() self.imageTexture.bind() self.imageProgram.setUniforms((('gMap', 0), ('MVP', self.fieldMVP), )) gl.glBindVertexArray(self._vaoQuad) gl.glBindBuffer(gl.GL_ELEMENT_ARRAY_BUFFER, self.IBO) gl.glDrawElements(gl.GL_TRIANGLES, 6, gl.GL_UNSIGNED_INT, ctypes.c_void_p(0)) self.imageProgram.unbind() gl.glBindBuffer(gl.GL_ELEMENT_ARRAY_BUFFER, 0) gl.glBindVertexArray(0) self.imageTexture.unbind()
[docs] def drawTexture(self, texture, opacity): """ Draw `texture` on the screen. Args: texture (:class:Texture): texture instance opacity (float): opacity (alpha) of the texture 0 --> transparent 1 --> opaque """ self.screenProgram.bind() gl.glBindVertexArray(self._vaoQuad) gl.glBindBuffer(gl.GL_ELEMENT_ARRAY_BUFFER, self.IBO) texture.bind(2) self.screenProgram.setUniforms(( ('u_screen', 2), ('u_opacity', opacity), )) gl.glDrawElements(gl.GL_TRIANGLES, 6, gl.GL_UNSIGNED_INT, ctypes.c_void_p(0)) self.screenProgram.unbind() gl.glBindBuffer(gl.GL_ELEMENT_ARRAY_BUFFER, 0) gl.glBindVertexArray(0) texture.unbind()
[docs] def drawTracers(self): """ Draw the tracers on the screen """ gl.glBindVertexArray(self._vao) self.drawProgram.bind() self.drawProgram.setUniforms(( ('u_field', 0), ('palette', bool(self.palette)), ('u_tracers', 1), ('MVP', self.drawMVP), ('pointSize', self.pointSize), # CubeHelix ('start', 1.0), ('gamma', 0.9), ('rot', -5.3), ('reverse', False), ('minSat', 0.2), ('maxSat', 5.0), ('minLight', 0.5), ('maxLight', 0.9), ('startHue', 240.0), ('endHue', -300.0), ('useHue', True), ('u_tracersRes', float(self.tracersRes)), )) gl.glEnable(gl.GL_VERTEX_PROGRAM_POINT_SIZE) gl.glDrawArrays(gl.GL_POINTS, 0, int(self.tracers.size / 4)) self.drawProgram.unbind() gl.glBindVertexArray(0)
[docs] def updateTracers(self): """ Update tracers position using the fragment shader provided by the graphic card for computing. """ self.setRenderingTarget(self._nextTracersPos) gl.glViewport(0, 0, int(self.tracersRes), int(self.tracersRes)) self.updateProgram.bind() gl.glBindVertexArray(self._vaoQuad) gl.glBindBuffer(gl.GL_ELEMENT_ARRAY_BUFFER, self.IBO) self.updateProgram.setUniforms(( ('u_field', 0), ('u_tracers', 1), ('periodic', self.periodic), ('u_rand_seed', np.random.random()), ('u_speed_factor', self.speedFactor), ('u_decay', self.decay), ('u_decay_boost', self.decayBoost), ('fieldScaling', self.fieldScaling), ('u_fieldRes', self._fieldAsRGB.shape), )) gl.glDrawElements(gl.GL_TRIANGLES, 6, gl.GL_UNSIGNED_INT, ctypes.c_void_p(0)) # Replace current tracers positions with the new ones self._currentTracersPos = self._nextTracersPos self.resetRenderingTarget()
[docs] def updateTracersCS(self): """ Update tracers position using the compute shader provided by the graphic card for computing. """ self.updateProgram.bind() self.updateProgram.setUniforms(( ('u_field', 0), ('u_tracers', 1), ('periodic', self.periodic), ('u_rand_seed', np.random.random()), ('u_speed_factor', self.speedFactor), ('u_decay', self.decay), ('u_decay_boost', self.decayBoost), ('fieldScaling', self.fieldScaling), ('u_fieldRes', self._fieldAsRGB.shape), )) gl.glBindImageTexture(0, self._currentTracersPos._handle, 0, gl.GL_FALSE, 0, gl.GL_READ_ONLY, gl.GL_RGBA8) gl.glBindImageTexture(1, self._nextTracersPos._handle, 0, gl.GL_FALSE, 0, gl.GL_WRITE_ONLY, gl.GL_RGBA8) gl.glDispatchCompute(int(self.tracersRes / WORKGROUP_SIZE), int(self.tracersRes / WORKGROUP_SIZE), 1) ## Lock memory access until image process ends gl.glMemoryBarrier(gl.GL_SHADER_IMAGE_ACCESS_BARRIER_BIT) self._currentTracersPos = self._nextTracersPos self.resetRenderingTarget()