"""
Base stimulus class.
Handles GL context, shader programs common to all stimpack.visual_stim stim classes.
See stimpack.visual_stim.stimuli for available child stimulus classes. Overwrite methods in child classes like:
configure
eval_at
"""
import moderngl
[docs]
class BaseProgram:
def __init__(self, screen, num_tri=500):
"""
:param screen: Object containing screen size information
"""
# set screen
self.screen = screen
self.num_tri = num_tri
self.use_texture = False
self.rgb_texture = False
self.texture = None
self.draw_mode = 'TRIANGLES' # TRIANGLES, POINTS
self.point_size = 2 # pixels on screen, only for POINTS draw_mode
[docs]
def initialize(self, ctx):
"""
:param ctx: ModernGL context
"""
# save context
self.ctx = ctx
self.prog = self.create_prog()
self.update_vertex_objects()
# Default texture booleans for the shader program
self.prog['use_texture'].value = False
self.prog['rgb_texture'].value = False
[docs]
def destroy(self):
pass
[docs]
def paint_at(self, t, viewports, perspectives, subject_position={'x':0, 'y':0, 'z':0, 'theta':0, 'phi':0}):
"""
:param t: current time in seconds
:param viewports: list of viewport arrays for each subscreen - (xmin, ymin, width, height) in display device pixels
:param perspectives: list of perspective matrices for each subscreen, generated using perspective.GenPerspective and subscreen corners
:param subject_position: x, y, z position of subject (meters)
"""
self.eval_at(t, subject_position=subject_position) # update any stim objects that depend on subject position
data = self.stim_object.data # get stim object vertex data
self.update_vertex_objects()
if self.use_texture:
# x, y, z, r, g, b, a, texture x, texture y
vertices = len(data) // 9
else:
# x, y, z, r, g, b, a
vertices = len(data) // 7
# write data to VBO
self.vbo.write(data.astype('f4'))
# Render to each subscreen
for v_ind, vp in enumerate(viewports):
# set the perspective matrix
self.prog['Mvp'].write(perspectives[v_ind])
# set the viewport
self.ctx.viewport = vp
# render the object
if self.draw_mode == 'POINTS':
self.vao.render(mode=moderngl.POINTS, vertices=vertices)
self.ctx.point_size=self.point_size
elif self.draw_mode == 'TRIANGLES':
self.vao.render(mode=moderngl.TRIANGLES, vertices=vertices)
[docs]
def update_vertex_objects(self):
if self.use_texture:
# 3 points, 9 values (3 for vert, 4 for color, 2 for tex_coords), 4 bytes per value
self.vbo = self.ctx.buffer(reserve=self.num_tri*3*9*4)
self.vao = self.ctx.vertex_array(self.prog, self.vbo, 'in_vert', 'in_color', 'in_tex_coord')
else:
# basic, no-texture vbo and vao:
self.vbo = self.ctx.buffer(reserve=self.num_tri*3*7*4) # 3 points, 7 values, 4 bytes per value
self.vao = self.ctx.vertex_array(self.prog, self.vbo, 'in_vert', 'in_color')
[docs]
def add_texture_gl(self, texture_image, texture_interpolation='LINEAR'):
# Update the texture booleans for the shader program
self.prog['rgb_texture'].value = self.rgb_texture
self.prog['use_texture'].value = self.use_texture
if self.rgb_texture:
# RGB texture, shape = x, y, 3 (rgb)
components = 3
else:
# Monochromatic texture, shape = x, y
components = 1
self.texture = self.ctx.texture(size=(texture_image.shape[1], texture_image.shape[0]),
components=components,
data=texture_image.tobytes()) # size = (width, height)
if texture_interpolation == 'NEAREST':
self.texture.filter = (moderngl.NEAREST, moderngl.NEAREST)
elif texture_interpolation == 'LINEAR':
self.texture.filter = (moderngl.LINEAR, moderngl.LINEAR)
else:
self.texture.filter = (moderngl.LINEAR, moderngl.LINEAR)
self.prog['texture_matrix'].value = self.prog.ctx.extra['n_textures_loaded']
self.texture.use(self.prog.ctx.extra['n_textures_loaded'])
self.prog.ctx.extra['n_textures_loaded'] += 1
[docs]
def update_texture_gl(self, texture_image):
self.texture.write(data=texture_image.tobytes())
[docs]
def eval_at(self, t, subject_position={'x':0, 'y':0, 'z':0, 'theta':0, 'phi':0, 'roll':0}):
"""
:param t: current time in seconds
"""
pass
[docs]
def create_prog(self):
return self.ctx.program(vertex_shader=self.get_vertex_shader(), fragment_shader=self.get_fragment_shader())
[docs]
def get_vertex_shader(self):
vertex_shader = '''
#version 330
in vec3 in_vert;
in vec4 in_color;
in vec2 in_tex_coord;
out vec4 v_color;
out vec2 v_tex_coord;
uniform mat4 Mvp;
void main() {
v_color = in_color;
v_tex_coord = in_tex_coord;
gl_Position = Mvp * vec4(in_vert, 1.0);
}
'''
return vertex_shader
[docs]
def get_fragment_shader(self):
fragment_shader = '''
#version 330
in vec4 v_color;
in vec2 v_tex_coord;
uniform bool use_texture;
uniform bool rgb_texture;
uniform sampler2D texture_matrix;
out vec4 f_color;
void main() {
if (use_texture) {
vec4 texFrag = texture(texture_matrix, v_tex_coord);
if (rgb_texture) {
f_color.rgb = texFrag.rgb * v_color.rgb;
} else {
f_color.rgb = texFrag.r * v_color.rgb;
}
f_color.a = v_color.a;
} else {
f_color.rgb = v_color.rgb;
f_color.a = v_color.a;
}
}
'''
return fragment_shader