|
Table of Contents
MGL
A quick overviewmgl is a set of matlab functions for dispalying full screen visual stimuli from matlab. It is based on OpenGL functions, but abstracts these into more simple functions that can be used to code various kinds of visual stimuli. It can be used on both Linux and Mac OS X systems. Stimuli can be displayed full screen or in a window (helpful for debugging on a system that only has one display). With a single command that specifies the distance to and size of a monitor, the coordinate system can be specified in degrees of visual angle, thus obviating the need to explicitly convert from the natural coordinate frame of psychophysics experiments into pixels. The best way to see whether it will be useful to you is to try out the mglTest programs (see 1.2 below) and also the sample experiment testExperiment. A basic “hello world” program can be written in four lines: Open the screen, 0 specifies to draw in a window. 1 would be full screen in the main display, 2 would be full screen in a secondary display, etc… >> mglOpen(0); Select the coordinate frame for drawing (e.g. for a monitor 57 cm away, which has width and height of 16 and 12 cm). >> mglVisualAngleCoordinates(57,[16 12]); draw the text in the center (i.e. at 0,0) >> mglTextDraw('Hello World!',[0 0]);
The above is drawn on the back-buffer of the double-buffered display so to make it show up you flush the display (this function will wait till the end of the screen refresh) >> mglFlush; To close an open screen: >> mglClose; What is in the mgl distribution
GNU General Public LicenseThese programs are free to distribute under the GNU General Public License. See the file mgl/COPYING for details. Recompiling mglFor G4/5 based macs you should not need to recompile the distribution as it comes with precompiled .mexmac files. However, if you want to use the code on linux or if when you first start using the functions they do not work or cause an obvious error like a segmentation fault, then you may want to recompile. This requires mex to be setup properly on your machine. At a minimum you will need to have the apple developer tools installed on your machine (XCode) http://developer.apple.com/tools/: The command to recompile is: » mglMake(1); If all else fails, how to get back control over the display?If you can't do mglClose, you can always press: option-open apple-esc this will quit your matlab session as well. Can I get access to all OpenGL functions?We have only exposed parts of the OpenGL functionality. If you need to dig deeper to code your stimulus, consider writing your own mex file. This will allow you to use the full functionality of the OpenGL library. To do this, you could start by modifying one of our mex functions (e.g. mglClearScreen.c) and add your own GL code to do what you want and compile. Printing the wiki help pagesYou can print out all the wiki help pages at once, by using this link. MGL DownloadExternal UsersClick on the following link to receive a tar/zip file of the current stable version: After downloading you can just click on the tar file and it should expand itself or from a command line do: gunzip mgl.tar.gz tar xfv mgl.tar to extract the files. NYU Usersmgl is available from the following directory. This will always be the latest stable version. /share/wotan/heegerlab/mgl NYU Users who wish to make changes to the codeYou can use CVS to checkout a version that you can modify yourself. > cvs -d ~justin/src checkout mgl If you want to make changes only to the stable version: > cvs -d ~justin/src checkout -r v1_10 mgl Note that changes to the stable version should only be made to fix bugs. If you make a change to the stable version, make sure to make the corresponding change to the development version. You may also access the code repository via the web by adding the following to your .cshrc.mine file
You will need to change justin@cns.nyu.edu to your own login. Also, you will need to have the cvs binary somewhere in your path on your account. You can copy the cvs binary from ~justin/bin/cvs Latest MGL versionTo make sure you have the latest mgl version cd to your local folder of mgl and type: cvs update Initial setupSimply add the mgl directory to your path, and you are ready to go. >> addpath(genpath('MYPATH/mgl'));
where MYPATH should be replaced by the path to your version of mgl. You can see what functions are available by doing (in matlab): >> help mgl There are a bunch of test programs (names start with mglTest) that you can use to test the distribution and see how things are done. If you want to run under linux, then you will need to compile the distribution, using mglMake (see below). You may also need to recompile the distribution for your system if you are running an older version of matlab (we run Matlab version >= 7.3 on Mac OS >= 10.4.8). We have found that mex files created on Matlab 7.3 do not run on matlab 14.1 for instance (if you run -nojvm you will see that it complains that it cannot find a dynamic link library for the mx functions–if you run with the matlab desktop it will just crash the system). If this happens to you simply recompile and you should be good to go. Recompiling MGLWe (Apple developers) run the latest Mac OS (10.5.6 as of this writing) with the latest version of Matlab (7.6 as of this writing) or (Linux developers) Ubuntu 64-bit (Gutsy) and 32-bit (Feisty) with Matlab 7.4 and the binaries are created to run on these systems. As noted above, some older versions (notably Matlab 14.1) are not able to use these mex files and crash when you try to run mglOpen. If this happens, then all you need to do is recompile MGL using mglMake(1). License manager timing glitchThe Matlab license manager checks every 30 seconds for the license. This can cause there to be an apparent frame glitch in your stimulus code, especially if you are using a network license (on our machines it can take ~200 ms to check for the license). The only known workaround to this is to run on a machine that has a local copy of the license. You can check this for yourself by seeing how long it takes to do screen refreshes:
mglOpen;
global MGL;
checkTime = 30*MGL.frameRate;
timeTaken = zeros(1,checkTime);
mglFlush;
for i = 1:checkTime
flushStart = mglGetSecs;
mglFlush;
timeTaken(i) = mglGetSecs(flushStart);
end
mglClose;
plot((1:checkTime)/MGL.frameRate,timeTaken);
zoom on;xlabel('seconds');ylabel('Frame refresh time (seconds)');
If you have the same problem, you should see one large spike in the time course like this:
This one shows it taking about 65 ms. Note that you may see small deviations in which one frame takes longer and then the following frame takes shorter than the mean. These are normal flucations persumably due to multi-tasking and other events that are intermittently taking up time. As long as these are shorter than a frame refresh interval minus the time it takes you to process the stimuli for your display, you will not drop any frames. Note that in the above code, if you change mglFlush to any other command, such as WaitSecs(1/frameRate);, you will still see the big spike for the license manager check–confirming that this has nothing to do with drawing to the screen. Function not supported on LinuxNot all functions are currently supported on the Linux platform. The list of funcitons not supported yet are:
If you want to use text under the linux operating system, you can use mglStrokeText. Here is a more recent update from Jonas about the Linux version: I am in the process of upgrading the Linux version of MGL to run under Ubuntu (64-bit and 32-bit) with NVIDIA and ATI graphics cards. Although the upgrade is still incomplete, most functions work equally well under Linux at this stage. Some differences that will remain between the platforms are listed below.
For the time being, only NVIDIA and ATI cards will be supported (because I only have access to those two machines).
Functions not supported on Mac
Opening in a window with matlab desktop (Mac only)This issue had been resolved, but if you find you are still having problems, here is what the issue was and how to resolve it: On Mac OS X there seems to be some interaction with having mutliple threads in the workspace that causes working within a window (i.e. mglOpen(0) as opposed to fullscreen) to be unstable. The workaround for now is not to close the window once it is opened. This seems to work fairly well. When one is completely finished working with the window, one can call mglPrivateClose to close the window. But after that, calling mglOpen(0) is likely to crash. mglOpen(0) works fine if running matlab -nojvm or -nodesktop. Precise timing of key press eventsOn Mac OS X if you want to get key press events, you can get them with a system (nanosecond precision) time stamp in the cocoa beta (see below). For the previous version of mgl you could only get them with a resolution of 1/60 second. If you are willing to sit in a loop testing the keyboard, you can use mglGetKeys and get acceptable time resolution (your time resolution will depend on how fast you poll the keyboard status). On Linux, the time resolution depends on the kernel's hard-coded HZ setting, which is usually between 100-500; the value can be retrieved (at least on Ubuntu) by typing cat /boot/config-`uname -r` | grep '^CONFIG_HZ=' The time resolution is 1/HZ sec; so on the current developer system, with HZ=250, key presses can be timed with a resolution of 1/250=4msec. 64bit Matlab on Mac OS XApple has given up support for Carbon based GUIs for 64 bit applications, so internal functions have been rewritten to use the Cocoa based interface. For those of you who don't know, Carbon is Mac's really old C API from OS 9 days and Cocoa is Mac's slick new object-oriented API which uses an extension to the C language called Objective-C. Essentially Cocoa is NextStep (and even uses NS as a prefix to most classes). Everything in the mgl cocoa beta is now working. It should be 100% backwards compatible with any mgl stimulus code you wrote in the past. There are also a few improvements:
A beta for the new Cocoa based mgl can be retrieved using svn: svn checkout http://yoyodyne.cns.nyu.edu/svn/mgl/trunk mgl To use the code, simply add mgl to your path: >> addpath(genpath('mgl'));
And make sure to enable access for assistive devices so that you can use the function mglGetKeyEvent, mglGetMouseEvent, mglPostEvent, mglSimulateRun and mglEatKeys. To see a list of changes (there are continuing to be a lot of changes, so if you have checked out the beta, make sure to do periodic updates), you can check the commit logs.
For non-NYU users, if your institution uses a proxy-server, you might have to set up svn to allow you to access the svn respository. Check System Preferences→Network→Advanced→Proxies and note down the name of the proxy server and port (e.g. blah.nottingham.ac.uk, port 3128). You can then edit the subversion setup file (
Here are a couple of lines from the [groups] group1 = yoyodyne.cns.nyu.edu [group1] http-proxy-host = blah.nottingham.ac.uk http-proxy-port = 3128 If you are interested in understanding the cocoa code, you may find the book Cocoa programming for Mac OS X by Aaron Hillegass to be helpful. Also Wikipedia has a pretty good entry on Objective-C programming. Working notesFunctions affected and status
Bug list
List of functions to be checked with LinuxEvery function with os-specific code has been touched in the beta. Generally this has been done to mark where the os-specific code is, or to break-out an os-specific function so that it is better determined what the inputs/outputs of those functions need to be. In general, I think the goal should be to have mgl compile cleanly for all functions on both platforms (if something isn't implemented, it should just print out a message that it is not implemented). I have tried to structure the code so that this will be easy to do.
Keyboard events
The functions to get keyboard and mouse events rely on a low level system called “event taps”. You need to enable this system (it is usually used as an accessibility option), by going to your System Preferences/Universal Access and clicking “Enable access for assistive devices”.
Main screen functionsmglOpen: Opens the screen
usage: mglOpen(whichScreen, <screenWidth>, <screenHeight>, <frameRate>, <bitDepth>)
Open last monitor in list with current window settings mglOpen Open with resolution 800×600 60Hz 32bit fullscreen mglOpen(1,800,600,60,32); Open in a window mglOpen(0);
mglFlush: Flips front and back buffer
purpose: swap front and back buffer (waits for one frame tick) mglClose: Closes the screen
purpose: close OpenGL screen Other screen functionsmglSwitchDisplay: Switch between multiple monitors
usage: mglSwitchDisplay(<displayID>)
You can use this to open up two separate screens and control them independently. For example, say you have two monitors 1 and 2. You open the first in the usual way: mglOpen(1) Then you switch monitors so that you can open up the other one mglSwitchDisplay mglOpen(2) Now if you want draw to the first display, you can do mglSwitchDisplay(1) mglClearScreen(0.5); mglFlush; Similarly, to draw to the second display mglSwitchDisplay(2); mglClearScreen(1); mglFlush; You can check the status of all open displays with: mglSwitchDisplay(-2); If you want to close all displays at once, you can do: mglSwitchDisplay(-1); mglMoveWindow: Moves windows created by mglOpen(0)
usage: mglMoveWindow(leftPos,topPos)
mglOpen(0); mglMoveWindow(100,100);
mglDescribeDisplays: Get information about your monitor and computer system
usage: [displayInfo computerInfo] = mglDescribeDisplays() mglFrameGrab: Frame grab to a matlab matrix
usage: mglFrameGrab(<frameRect>)
mglOpen();
mglScreenCoordinates;
mglClearScreen([0 0 0]);
global MGL;
mglPoints2(MGL.screenWidth*rand(5000,1),MGL.screenHeight*rand(5000,1));
mglPolygon([0 0 MGL.screenWidth MGL.screenWidth],[MGL.screenHeight/3 MGL.screenHeight*2/3 MGL.screenHeight*2/3 MGL.screenHeight/3],0);
mglTextSet('Helvetica',32,[1 1 1]);
mglTextDraw('Frame Grab',[MGL.screenWidth/2 MGL.screenHeight/2]);
frame = mglFrameGrab;
mglFlush
imagesc(mean(frame,3)');colormap('gray')
Functions to adjust the coordinate framemglVisualAngleCoordinates: Visual angle coordinates
purpose: Sets view transformation to correspond to visual angles (in degrees) given size and distance of display. Display must be open and have valid width and height (defined in MGL variable)
mglOpen mglVisualAngleCoordinates(57,[16 12]); mglScreenCoordinates: Pixel coordinate frame
purpose: Set coordinate frame so that it is in pixels with 0,0 in the top left hand corrner mglTransform: Low-level function to adjust transforms
purpose: applies view transformations
You can also specifiy one of GL_MODELVIEW, GL_PROJECTION, or GL_TEXTURE and a return variable current matrix values. If two outputs are specified, the result of the computation will be returned. This function is usually not called directly, but called by mglVisualAngleCoordinates or mglScreenCoordinates to set the transforms mglHFlip: Horizontally flip coordinates
purpose: flips coordinate frame horizontally, useful for when the display is viewed through a mirror mglOpen
mglVisualAngleCoordinates(57,[16 12]);
mglHFlip
mglTextSet('Helvetica',32,1,0,0,0,0,0,0,0);
mglTextDraw('Mirror reversed',[0 0]);
mglFlush;
mglVFlip: Vertically flip coordinates
purpose: flips coordinate frame vertically mglOpen
mglVisualAngleCoordinates(57,[16 12]);
mglVFlip
mglTextSet('Helvetica',32,[1 1 1],0,0,0,0,0,0,0);
mglTextDraw('Vertically flipped',[0 0]);
mglFlush;
Texture functions used for displaying imagesmglCreateTexture: Create a texture from a matrix
purpose: Create a texture for display on the screen with mglBltTexture image can either be grayscale nxm, color nxmx3 or color+alpha nxmx4
mglOpen; mglClearScreen mglScreenCoordinates texture = mglCreateTexture(round(rand(100,100)*255)); mglBltTexture(texture,[0 0]); mglFlush; mglBltTexture: Draw the texture to the screen
purpose: Draw a texture to the screen in desired position.
To display several textures at once, texture can be an array of n textures, position is nx2, or nx4 and hAlignment, vAlignment and rotation are either a single value or an array of n. multiple textures: mglOpen; mglVisualAngleCoordinates(57,[16 12]); image = rand(200,200)*255; imageTex = mglCreateTexture(image); mglBltTexture([imageTex imageTex],[-3 0;3 0],0,0,[-15 15]); mglFlush; single textures mglOpen; mglVisualAngleCoordinates(57,[16 12]); image = rand(200,200)*255; imageTex = mglCreateTexture(image); mglBltTexture(imageTex,[0 0]); mglFlush; mglDeleteTexture: Delete a texture
purpose: Deletes a texture. This will free up memory for textures that will not be drawn again. Note that when you call mglClose texture memory is freed up. You only need to call this if you are running out of memory and have textures that you do not need to use anymore.
mglOpen; mglClearScreen mglScreenCoordinates texture = mglCreateTexture(round(rand(100,100)*255)); mglBltTexture(texture,[0 0]); mglFlush; mglDeleteTexture(texture); Drawing textmglTextSet: Set parameters for drawing text
purpose: Set text properties for mglText
mglOpen;
mglVisualAngleCoordinates(57,[16 12]);
mglTextSet('Helvetica',32,[0 0.5 1 1],0,0,0,0,0,0,0);
mglTextDraw('Hello There',[0 0]);
mglFlush;
mglText: Create a texture from a string
purpose: Creates a texture from a string.
mglOpen;
mglVisualAngleCoordinates(57,[16 12]);
mglTextSet('Helvetica',32,[0 0.5 1 1],0,0,0,0,0,0,0);
thisText = mglText('hello')
mglBltTexture(thisText,[0 0],'left','top');
mglFlush;
Normally you will only set one output argument which is a texture usable by mglBltTexture. But if you have two output arguments [tex texMatrix] = mglText('hello');
texMatrix will contain a 2D matlab array that has a rendering of the text (i.e. it will have values from 0-255 that represent the string). You can modify this matrix as you want and then use mglCreateTexture to create it into a texture that can be displayed by mglBltTexture mglTextDraw: Draws text to screen (simple but slow)
purpose: wrapper around mglText and mglBltTexture to draw some text on the screen. If you need to draw text more quickly, you will have to pre-make the text textures with mglText and then use mglBltTexture when you want it. Otherwise, for non time-critical things this functions should be used.
mglOpen;
mglVisualAngleCoordinates(57,[16 12]);
mglTextSet('Helvetica',32,[0 0.5 1 1],0,0,0,0,0,0,0);
mglTextDraw('Hello There',[0 0]);
mglFlush;
mglStrokeText: Fast no-frills line-based text drawing (does not use texture memory)
purpose: Draws a stroked fixed-width character or string on MGL display. Default width is 1, default height 1.8 (in current screen coordinates)
mglOpen;
mglVisualAngleCoordinates(57,[16 12]);
mglStrokeText('Hello',0,0);
mglFlush;
Drawing functionsmglClearScreen
purpose: sets the background color
set to the level of gray (0-1) mglClearScreen(gray) set to the given [r g b] mglClearScreen([r g b]) full example mglOpen; mglClearScreen([0.7 0.2 0.5]); mglFlush(); mglPoints2: 2D points
purpose: plot 2D points on an OpenGL screen opened with mglOpen
mglOpen; mglVisualAngleCoordinates(57,[16 12]); mglPoints2(16*rand(500,1)-8,12*rand(500,1)-6,2,1); mglFlush mglPoints3: 3D points
purpose: plot 2D points on an OpenGL screen opened with mglOpen
mglOpen; mglVisualAngleCoordinates(57,[16 12]); mglPoints3(16*rand(500,1)-8,12*rand(500,1)-6,zeros(500,1),2,1); mglFlush
mglLines2: 2D lines
purpose: mex function to plot lines on an OpenGL screen opened with glopen
mglOpen mglVisualAngleCoordinates(57,[16 12]); mglLines2(-4, -4, 4, 4, 2, [1 0.6 1]); mglFlush mglFillOval: Ovals
purpose: draw filled oval(s) centered at x,y with size [xsize ysize] and color [rgb]. the function is vectorized, so if you provide many x/y coordinates (identical) ovals will be plotted at all those locations.
mglOpen; mglVisualAngleCoordinates(57,[16 12]); x = [-1 -4 -3 0 3 4 1]; y = [-1 -4 -3 0 3 4 1]; sz = [1 1]; mglFillOval(x, y, sz, [1 0 0]); mglFlush(); mglFillRect: Rectangles
purpose: draw filled rectangles(s) centered at x,y with size [xsize ysize] and color [rgb]. the function is vectorized, so if you provide many x/y coordinates (identical) ovals will be plotted at all those locations.
mglOpen; mglVisualAngleCoordinates(57,[16 12]); x = [-1 -4 -3 0 3 4 1]; y = [-1 -4 -3 0 3 4 1]; sz = [1 1]; mglFillRect(x, y, sz, [1 1 0]); mglFlush();
mglFixationCross: Cross
purpose: draws a fixation cross with no arguments, draws a fixation cross at origin (default width 0.2 with linewidth 1 in white at [0,0])
mglOpen; mglVisualAngleCoordinates(57,[16 12]); mglFixationCross; mglFlush; mglGluAnnulus: Annuli, rings
purpose: for annuli and rings, e.g. for retinotopic stimuli. The function is vectorized, such that multiple annuli can be rendered in one call. In this case, x,y, isize, and osize need to have the same number of elements. Color is also vectorized.
mglOpen(0); mglVisualAngleCoordinates(57,[16 12]); x = zeros(10, 1); y = zeros(10, 1); isize = ones(10,1) osize = 3*ones(10,1); startAngles = linspace(0,180, 10) sweepAngles = ones(1,10).*10; colors = jet(10)'; % nb! transpose mglGluPartialDisk(x, y, isize, osize, startAngles, sweepAngles, colors, 60, 2); mglFlush(); mglGluDisk: Circular dots
purpose: for plotting circular (rather than square dots), use this function. on slower machines, large number of dots may lead to dropped frames. there may be a way to speed this up a bit in future.
mglOpen; mglVisualAngleCoordinates(57,[16 12]); x = 16*rand(100,1)-8; y = 12*rand(100,1)-6; mglGluDisk(x, y, 0.1, [0.1 0.6 1], 24, 2); mglFlush();
mglGluPartialDisk: Segments, wedges
purpose: for segments and wedges, e.g. for retinotopic stimuli. The function is vectorized, such that multiple segments can be rendered in one call. In this case, x,y, isize, osize, startAngles, and sweepAngles need to have the same number of elements. Color is also vectorized (see mglGluAnnulus and the example below).
mglOpen(0); mglVisualAngleCoordinates(57,[16 12]); x = zeros(10, 1); y = zeros(10, 1); isize = linspace(1, 5, 10); osize = 3+isize; startAngles = linspace(0,180, 10) sweepAngles = ones(1,10).*10; colors = jet(10)'; mglGluPartialDisk(x, y, isize, osize, startAngles, sweepAngles, colors, 60, 2); mglFlush(); mglPolygon: Polygons
purpose: mex function to draw a polygon in an OpenGL screen opened with mglOpen. x and y can be vectors (the polygon will be closed)
mglOpen; mglVisualAngleCoordinates(57,[16 12]); x = [-5 -6 -3 4 5]; y = [ 5 1 -4 -2 3]; mglPolygon(x, y, [1 0 0]); mglFlush(); mglQuads: Quads
usage: mglQuad( vX, vY, rgbColor, [antiAliasFlag] );
mglOpen; mglScreenCoordinates mglQuad([100; 600; 600; 100], [100; 200; 600; 100], [1; 1; 1], 1); mglFlush(); Gamma tablesmglSetGammaTable: Sets the display card gamma table
purpose: Set the gamma table Setting a redMin, redMax, redGamma, greenMin, etc. mglSetGammaTable(0,1,0.8,0,1,0.9,0,1,0.75); or with a vector of length 9: mglSetGammaTable([0 1 0.8 0 1 0.9 0 1 0.75]); or set with a single table for all there colors. Note that the table values go from 0 to 1 (i.e. 0 is the darkest value and 1 is the brightest value). If you have a 10 bit gamma table (most cards do–see section on monitor calibration for a list), then the intermediate values will be interpreted with 10 bits of resolution. gammaTable = ((0:1/255:1).^0.8)'; mglSetGammaTable(gammaTable); or set all three colors with differnet tables redGammaTable = (0:1/255:1).^0.8; greenGammaTable = (0:1/255:1).^0.9; blueGammaTable = (0:1/255:1).^0.75; mglSetGammaTable(redGammaTable,greenGammaTable,blueGammaTable); can also be called with an nx3 table gammaTable(:,1) = (0:1/255:1).^0.8; gammaTable(:,2) = (0:1/255:1).^0.9; gammaTable(:,3) = (0:1/255:1).^0.75; mglSetGammaTable(gammaTable); can also be called with the structure returned by mglGetGammaTable mglSetGammaTable(mglGetGammaTable); Note that the gamma table will be restored to the original after mglClose. Timing. The setting of the gamma table is done by the OS in a way that seems to be asynchronous with mglFlush. For instance, the following code gives unexpected results: mglOpen(1); mglClearScreen(1); % set back buffer to white mglWaitSecs(2); % now set the gamma table to all black, this should insure that nothing will be displayed mglSetGammaTable(zeros(1,256)); mglFlush; % now the flush will bring the value 255, set by the mglClearScreen above, % to the front buffer, but because the gamma table is set to black, % nothing should be displayed mglWaitSecs(2); mglClose; This should keep the screen black, but on my machine, the screen temporarily flashes white. Presumably this is because the mglSetGammaTable happens after the mglFlush. It is recommended that you change the gamma while there is nothing displayed on the screen and wait for at least one screen refresh before assuming that the gamma table has actually changed. mglGetGammaTable: Gets the current gamma tablepurpose: returns what the gamma table is set to usage: table = mglGetGammaTable() mglOpen; gammaTable = mglGetGammaTable Stencils to control drawing only to specific parts of screenHere is a demonstration of how to use stencils using these these functions: mglOpen; mglScreenCoordinates; %Draw an oval stencil mglStencilCreateBegin(1); mglFillOval(300,400,[100 100]); mglStencilCreateEnd; mglClearScreen; % now draw some dots, masked by the oval stencil mglStencilSelect(1); mglPoints2(rand(1,5000)*500,rand(1,5000)*500); mglFlush; mglStencilSelect(0); mglStencilCreateBegin: Start drawing a stencil
purpose: Begin drawing to stencil. Until mglStencilCreateEnd is called, all drawing operations will also draw to the stencil. Check MGL.stencilBits to see how many stencil planes there are. If invert is set to one, then the inverse stencil is made
see example above. mglStencilCreateEnd: End drawing a stencilpurpose: Ends drawing to stencil usage: mglStencilCreateEnd see example above. mglStencilSelect: Select a stencilpurpose: Sets which stencil to use, 0 for no stencil usage: mglStencilSelect(stencilNumber)
See example above. Keyboard and mouse functionsmglDisplayCursor: Hide or display the mouse cursor
purpose: Hide or display the mouse cursor
When you call mglOpen the mouse cursor is hidden by default. You can get it to come back by doing: mglOpen mglDisplayCursor mglGetKeys: Get keyboard state
purpose: returns the status of the keyboard (regardless of whether the focus is on the mgl window)
mglGetMouse: Get mouse state
usage: mglGetMouse() mglGetKeyEvent: Get a key down event off of queuepurpose: returns a key down event waitTicks specifies how long to wait for a key press event in seconds. Note that the timing precision is system-dependent:
The default wait time is 0, which will return immediately and if no keypress event is found, will return an empty array []. The return structure contains the character (ASCII) code of the pressed key, the system-specific keycode, a keyboard identifier (on Linux, this is the keyboard state, or modifier field), and and the time (in secs) of the key press event. NOTE that to get a key event the focus *MUST* be on the mgl window. For faster timing, try mglGetKeys
mglOpen mglGetKeyEvent(0.5) mglGetMouseEvent: Get a mouse button down event off of queue
usage: mglGetMouseEvent(waitTicks)
The default wait time is 0, which will return immediately with the mouse position regardless of button state. The return structure contains the x,y coordinates of the mouse, the button identifier if pressed (on the button-challenged Mac this is always 1) and 0 otherwise, and the time (in secs) of the mouse event. NOTE that the mouse down event has to be *ON* the mgl window for this to work with waitTicks not equal to 0
mglOpen mglGetMouseEvent(0.5) mglCharToKeycode: Returns keycode of char
Purpose: Returns the keycodes of a (list of) keynames Note on special keys: On Linux (X), special keys and function keys have unique names, e.g., 'Escape', 'F1', etc., so obtaining the keycodes for these is done by mglCharToKeycode({'Escape','F1'}) etc. On Macs, this is not possible; instead, test for the keycode and name of a key using the mglShowKey function. The keycodes match those used by mglGetKeys and mglGetKeyEvent
Example: testing for specific keypresses: keycodes=mglCharToKeycode({'1','2' '3'}) % keys 1-3 on main keyboard
while (1); k=mglGetKeys(keycodes); if (any(k)),break;end;end
Technical note: the returned keycodes are identical to system keycodes+1 mglKeycodeToChar: Returns char of keycode
Purpose: Returns the keynames of a (list of) keycodes Note on special keys: This repeats the above entry, deleted.
Example: testing which keys were pressed: while (1); k=mglGetKeys; if (any(k)),break;end;end keycodes=find(k); keynames=mglKeycodeToChar(keycodes) Technical note: keycodes are identical to system keycodes+1 Timing functionsmglGetSecs: Get time in seconds
purpose: Get current or elapsed time
To get current time t=mglGetSecs Get elapsed time since t0 t0 = mglGetSecs; elapsedTime=mglGetSecs(t0) mglWaitSecs: Wait for a time in seconds
purpose: Wait for some time
Wait 3.3 seconds: mglWaitSecs(3.3); Sound functionsmglInstallSound: Install an .aiff file for playing with mglPlaySound
purpose: Install an .aiff file for playing with mglPlaySound
This will install sounds to be played using mglPlaySound. Note that if you just want to use systems sounds then you do not need to call this function directly, it will be called by mglOpen to install all your system sounds. Once the sound is installed you can play it with mglPlaySound soundNum = mglInstallSound('/System/Library/Sounds/Submarine.aiff');
mglPlaySound(soundNum);
With no arguments, mglInstallSound uninstalls all sounds mglInstallSound mglPlaySound: Play a system sound
purpose: Play a sound
Plays a system sound. After calling mglOpen, all of the system sounds will be installed and you can play a specific one as follows: mglOpen; global MGL; mglPlaySound(find(strcmp(MGL.soundNames,'Submarine'))); With no arguments mglPlaySound plays the system alert sound mglPlaySound Note that this function returns right after it starts playing the sound (it does not wait until the sound finishes playing). Test/Demo programsRun these test programs without any parameters and they should display on your second monitor. With an optional single argument you can pass the number of the display you want to display on.
A quick overviewThe task structure can be used to help code experiments, it is completely separate from the basic mgl libarary that is used to display to the screen (in that you do not have to use the task code to use the basic mgl functions). The structure for these experiments involves three main variables:
myscreen: Holds information about the screen parameters like resolution, etc. To create and run an experiment, your program will do the following:
The basic idea of how to set up your experiment with these structures requires defining some terms. Going from the largest organization down to the smallest:
A simple example experiment can be found in mgl/task: testExperiment testExperimentThe code for textExperiment is a good starting place for creating a new experiment since it contains all the essential elements for using these functions. Let's start by briefly going through each one of the steps above in reference to the function testExperiment. Note that when you actually want to program your own task, you can either start by editing testExperiment.m or use the function taskTemplate.m (be sure to copy these to a new name). taskTemplate.m is an even more stripped down version of testExperiment.m that contains only the necessary essentials to start using the code (and everywhere there is a comment that begins with fix: you will need to make changes to customize for your experiment). There are also some more templates that can be used as starting places:
Initialize the screenThis can be done very simply just by calling % initalize the screen myscreen = initScreen; This call will handle opening up of the screen with appropriate parameters and setting the gamma table. If you want to add specific parameters for your computer add a line like the following: myscreen.screenParams{1} = {'yoyodyne.cns.nyu.edu',[],2,1280,1024,57,[31 23],60,1,1,1.8,'calibFilename.mat',[0 0]};
myscreen = initScreen(myscreen);
This will set parameters for your screen. The parameters in order are
Setup the task structureIn the testExperiment, the task structure is a cell array that actually contains two separate tasks that will be run in the course of the experiment. This sets the first task to be the fixation staircase task. If you don't want to use the fixation task then you can omit this part: % set the first task to be the fixation staircase task
[task{1} myscreen] = fixStairInitTask(myscreen);
This is the first “phase” of our task. Not all tasks need to have different phases, but in this case we want the experiment to start with dots moving incoherently for 10 seconds and then we want trials to run in the next phase. % set our task to have two phases.
% one starts out with dots moving for incohrently for 10 seconds
task{2}{1}.waitForBacktick = 1;
task{2}{1}.seglen = 10;
task{2}{1}.numBlocks = 1;
task{2}{1}.parameter.dir = 0;
task{2}{1}.parameter.coherence = 0;
Each one of the fields in the task set the behavior of that phase of the task.
The next phase of the task will be the one that actually runs the trials. % the second phase has 2 second bursts of directions, followed by
% a top-up period of the same direction
task{2}{2}.segmin = [2 6];
task{2}{2}.segmax = [2 10];
task{2}{2}.parameter.dir = 0:60:360;
task{2}{2}.parameter.coherence = 1;
task{2}{2}.random = 1;
In this task, we have a block of trials in which we will show trials with different motion directions. You set what parameters you want to use in the “parameter” part of your task. Note that you can use any name for parameters that you like. Here we call them dir for direction and coherence for motion coherence. Note that we have only one value of motion coherence so all trials will be run with a motion coherence of 1. task{2}{2}.parameter.dir = 0:60:360;
task{2}{2}.parameter.coherence = 1;
We also have to decide the order in which parameters will be presented in a block of trials. The default is to run them sequentially (in this case directions 0 then 60 then 120 etc). To randomize the order, we set: task{2}{2}.random = 1;
Our trial will have two segments, a 2 second segment in which the stimulus is presented and a 6-10 second long intertrial interval: task{2}{2}.segmin = [2 6];
task{2}{2}.segmax = [2 10];
The task code will automatically keep track of the variables in the parameter field, so that you can later access them to find out which direction of motion was shown on what trial. You will be able to do this by using the function getTaskParameters. Initialize the stimulus.The stimulus is kept in a global variable so that if the variable is very large, we don't incur overhead with passing it around all the time. If you want to have the stimulus variable saved at the end of the experiment, you can call the function initStimulus as below. Note that you do not need to call initStimulus if you do not want to save the stimulus structure. % init the stimulus
global stimulus;
myscreen = initStimulus('stimulus',myscreen);
stimulus = initDots(stimulus,myscreen);
The function initDots is specific for creating the dots stimulus for this test experiment, you will substitute your own function for creating your stimulus. Create callback functionsCallbacks are the way that you control what happens on different portions of the trial and what gets drawn to the screen. A callback is simply a function that gets called at a specific time. You write the function and you let updateTask handle when that function needs to be called. There are two required callbacks: The first required callback that is used in this program is the one that gets called every time a segment starts. function [task myscreen] = startSegmentCallback(task, myscreen) global stimulus; if (task.thistrial.thisseg == 1) stimulus.dots.coherence = task.thistrial.coherence; else stimulus.dots.coherence = 0; end stimulus.dots.dir = task.thistrial.dir; What it does is it looks in the “thistrial” structure for what segment we are on, if we are not in segment one (i.e. the intertrial interval) it sets the motion coherence to 0, otherwise it sets it to whatever the parameter coherence is set to (defined in the task.parameter.coherence field). It also sets the direction of motion of the dots. The second (and most important) callback is the one used to draw the stimulus to the screen: function [task myscreen] = screenUpdateCallback(task, myscreen) global stimulus mglClearScreen; stimulus = updateDots(stimulus,myscreen); You can put your stimulus drawing routines in here. In this program, we simply clear the screen and draw the dots. This function gets called every display refresh.
% initialize our task with only the two required callbacks
for phaseNum = 1:length(task{1})
[task{1}{phaseNum} myscreen] = initTask(task{1}{phaseNum},myscreen,@startSegmentCallback,@screenUpdateCallback);
end
NOTE: It is necessary to register the callbacks in a specific order. The correct order for registering callbacks is: startSegmentCallback, screenUpdateCallback, getResponseCallback, startTrialCallback, endTrialCallback, startBlockCallback It doesn't matter exactly how you name the callbacks, what matters is what order you call them in. If there is a callback that you are not defining, you can enter it as [] in the initTask call, or leave it out: for example, [task myscreen] = initTask(task,myscreen,@startSegment, @screenUpdate, @getResponse, [],[], @startBlock); or [task myscreen] = initTask(task,myscreen, @startSegment, @screenUpdate, @getResponse); More details can be found in the callbacks section. Create a display loopNow that everything is setup to run your experiment all you need is a display loop that calls updateTask to run each one of the tasks that are being displayed. Then to flip the front and back buffer of the display to show your stimulus, you call tickScreen. This is the main loop in which your program is run. phaseNum = 1;
while (phaseNum <= length(task{2})) && ~myscreen.userHitEsc
% update the dots
[task{2} myscreen phaseNum] = updateTask(task{2},myscreen,phaseNum);
% update the fixation task
[task{1} myscreen] = updateTask(task{1},myscreen,1);
% flip screen
myscreen = tickScreen(myscreen,task);
end
At the very end you end the task which will save out information about your experiment. myscreen = endTask(myscreen,task); Experimental parametersBasicsFor your experiment you can choose what parameters you have and what values they can take on. You do this by adding parameters (of your choosing) into the parameter part of a task variable: task.parameter.myParameter1 = [1 3 5 10]; task.parameter.myParameter2 = [-1 1]; You can add any number of parameters that you want. updateTask will chose a value on each trial and put those values into the thistrial structure: task.thistrial.myParameter1 task.thistrial.myParameter2 would equal the setting on that particular trial. In each block every combination of parameters will be presented. You can randomize the order of the parameters by setting: task.random = 1; Note that parameter should really just be used for the parameters over which you want to randomize your experiment. For example, you may be testing several contrasts in your experiment, that should be coded as a parameter. You may also have some random variables, things like which segment that target should be presented in for example–things that need to be randomized, but are not a crucial parameter you are testing. For these types of variables, you should use randVars instead of parameter (see below). What if I have a group of parametersYou may have stimuli in which the parameters are grouped into different sets. For example you might want to show two types of grating patches. One tilted to the left with a high contrast and low spatial frequency and the other tilted to the right with low contrast and high spatial frequency. Then you could do task.parameter.groupNum = [1 2];
task.private.group{1}.orientation = -10;
task.private.group{1}.contrast = 1;
task.private.group{1}.sf = 0.2;
task.private.group{2}.orientation = 10;
task.private.group{2}.contrast = 0.1;
task.private.group{2}.sf = 4;
On each trial, you get the parameters by doing task.thistrial.thisgroup = task.private.group{task.thistrial.groupNum};
What if I have parameters that are not single numbersYou may have a parameter that is an array rather than a single number. Again, do something like the above (1.3) task.parameter.stringNum = [1 2 3];
task.strings = {'string1','string2','string3'}
and get the appropriate string on each trial by doing: task.thistrial.thisstring = task.strings{task.thistrial.stringNum};
randVarsFor variables that you just want to have some randomization over, you can declare them as randVars. For example, you might want to specify a target interval which should be either 1 or 2 on any given trial, but you don't want that to be block randomized. Then you can declare that variable as a uniform randomization: task.randVars.uniform.targetInterval = [1 2]; This variable will then be available in task.thistrial.targetInterval. You may also want to have the variable block randomized, like a parameter, but the blocks should be independent of the main parameter: task.randVars.block.blockedVar = [-1 0 1]; This will guarantee that on every three trials, blockedVar will be set to each one of the possible values -1,0 and 1. Note that with randVars the randomization is chosen at the beginning of the experiment and by default 250 trials are randomized after which you will cycle back through the variables. If you need more than 250 trials, you can set: task.randVars.len_ = 500; Using your own random sequenceYou might have your own randomization routine and want to use that to randomize parameters. You can do that with randVars: task.randVars.myRandomParameter = [...]; Then myRandomParameter will be available in task.thistrial.myRandomParameter in the order you specify in the array. Segment timesHow to setup segment timesEach trial can be divided into multiple segments where different things happen, like for instance you might have a stimulus segment and response segment that you want to have occur for 1.3 and 2.4 seconds respectively: task.seglen = [1.3 2.4]; At the beginning of each segment the callback startSegment will be called and you can find out which segment is being run by looking at: task.thistrial.thisseg How to randomize the length of segmentsIf you want to randomize the length of segments over a uniform distribution, like for instance when you want the first segment to be exactly 1.3 seconds and the second segments to be randomized over the interval 2-2.5 seconds: task.segmin = [1.3 2]; task.segmax = [1.3 2.5]; In this case, do not specify task.seglen. If you want the second interval to be randomized over the interval 2-2.5 seconds in intervals of 0.1 seconds (i.e. you want it to be either 2,2.1,2.2,2.3,2.4 or 2.5: task.segmin = [1.3 2]; task.segmax = [1.3 2.5]; task.segquant = [0 0.1]; You can also have a segment wait until a backtick happens, so that you can easily synch to volumes, for example: task.segmin = [1.3 2]; task.segmax = [1.3 2.5]; task.synchToVol = [0 1]; This will cause the second segment to last a random amount of time between 2 and 2.5 seconds and then wait until a backtick occurs before going on to the next trial. How to wait for user input before moving to next segmentSometimes you will want to wait for user input to decide when to end a segment of the trial, rather than pre-set a time. To do this, you need to: (1) set the segment length to inf, (2) take user input for that segment, and (3) in the responseCallback, end the segment when the subject responds. [Note that if you want to limit how much time the user has to respond, but still wait for input, you can set the segment length to something less than inf, e.g. 5 seconds; this means that the segment will end either when the subject responds, or when 5 seconds have elapsed, whichever comes first.] An example of how this might be implemented, in the case when the second of three segments waits for subject input before terminating: % in the main task body: task.seglen = [.5 inf 2]; task.getresponse = [0 1 0]; % At the end of the responseCallback function: task = jumpsegment(task); For other uses of jumpsegment, and for how to use jumpsegment(task, inf), see how to program a dual task below. Keeping time in seconds, volumes or refreshesTrial segments can keep time in either seconds (default), volumes or monitor refreshes. To change timing to use volumes: task.timeInVols = 1; To change timing to use monitor refreshes (note that is probably not a great idea to keep time in monitor refreshes since if you drop a frame, your timing will be altered). task.timeInTicks = 1; With timeInVols or timeInTicks, your segment times should now be integer values that specify time in Vols or monitor refreshes (e.g.): task.seglen = [3 2]; Note, that the default (time in seconds) adjusts for segment overruns that might occur when you drop monitor frames, but the timeInTicks will not and is therefore usually less accurate. CallbacksCallbacks are the way that you control what happens on different portions of the trial and what gets drawn to the screen. They are simply functions that get called at specific times in the experiment. It doesn't matter exactly what you call them, but it does matter exactly what order you register them in. There are two required callbacks, and the rest are optional. If for some reason you don't need one of the required callbacks, you can just leave it empty, but you must still define it. Callbacks are also discussed in the overview. Registering callbacksYou must register your callbacks with the initTask function, in the following order: [task myscreen] = initTask(task,myscreen,@startSegmentCallback,@screenUpdateCallback,@getResponseCallback,@startTrialCallback,@endTrialCallback,@startBlockCallback); You do not need to specify all the callbacks, only startSegmentCallback and screenUpdateCallback. To omit any of the callbacks, either don't pass it in to initTask or set the appropriate argument to []. Make sure that you return task and myscreen. For example, you might have [task myscreen] = initTask(task,myscreen,@startSegmentCallback,@screenUpdateCallback,[],@startTrialCallback,[],@startBlockCallback); or [task myscreen] = initTask(task,myscreen,@startSegmentCallback,@screenUpdateCallback,@getResponseCallback); screenUpdateCallback (required)function [task myscreen] = screenUpdateCallback(task, myscreen) % do your draw functions in here. Note that you will normally declare a global variable named stimulus that contains any textures or information about the stimulus and use that in here. Remember that screenUpdateCallback gets called every frame update. For a refresh rate of 60 Hz that means it definitely has to run within 1/60 th of a second, or else the program will start to drop frames and become slow. You should therefore make this function as simple as possible. For example, if you are using textures, call mglCreateTexture in your myInitStimulus function and only use the precomputed texture here in an mglBltTexture function. Another option that you can consider is that for many types of stimulus you don't have to update the screen every frame refresh. For something like moving dots or a drifting gabor you will need to update the frame every screen refresh, but if you just want to show a static gabor for a full segment, you can use the flushMode=1 feature that is described below in startSegmentCallback. startSegmentCallback (required)The other mandatory callback is the one that is called at the beginning of each segment: function [task myscreen] = startSegmentCallback(task, myscreen) The variable task.thistrial will have fields set to what the parameters are for that trial. For instance if you have dir as one of your parameters, then you will have the field task.thistrial.dir set to one of the directions (chosen by updateTask). If you are only drawing to the screen at the start of every segment, then you can use the flushMode=1 feature. Say for example you want to clear the screen and draw your texture to the screen and that is all that will happen in the segment then you can do something like: mglClearScreen; mglBltTexture(stimulus.tex,[4 0]); myscreen.flushMode = 1; Note that in this case you do not do any drawing in the screenUpdateCallback (this function will be empty). You only do drawing in the startSegmentCallback. This assumes that the only time the screen changes is when you start a new segment of your trial. getResponseCallback (optional)You can (optionally) define a callback for when the subject hits a response key: function [task myscreen] = getResponseCallback(task,myscreen) If you don't have subject responses in your experiment, you can just put this one line in with nothing after it. There is a field called task.thistrial.whichButton This will get filled with which button was pressed (a number from 1-9). Note that if two keys are pressed down at the same time, it will only return the first in the list (e.g. if 1 and 2 are simultaneously pressed, it will return 1). If you want to get all the keys that are pressed, you can look at task.thistrial.buttonState This will be an array where each element will have 0 or 1 depending on whether the key was pressed or not. Note that the getResponseCallback will only be called if in the task structure you have set the appropriate segment of the getResponse variable. For example, if you have a two segment trial, and you want to get subject responses in the second segment of the trial you would do: task.getResponse = [0 1]; You may also set a getResponse segment to 2. What this does is similar to setting myscreen.flushMode = 1. It prevents mglFlush from being called to update the screen while you are waiting for a keyboard press. This will get much more accurate keyboard timing, but will not allow the screen to update while you are waiting (i.e. you have to have a static display–no moving dots or flickering gratings or anything). task.getResponse = [0 2]; If you want to get other keys, rather than the defined keys 1-9, for example if you want the keypad numbers, you can override which keys will be checked with: myscreen.keyboard.nums = [84 85]; myscreen = initScreen(myscreen); This is called at the beginning of your program. Note that to get the keycodes that correspond to a key, you can either use: mglCharToKeycode({'a' 'b' 'c'})
or, for keys that you can't write like the keypad numbers or the esc key, run the program: mglTestKeys and type the keys you want and it will print out the correct keycode. The getResponseCallback will get called every time the subject presses a button, so if the subject presses two buttons one after the other during the response period, getResponseCallback will be called twice. If you want to ignore the 2nd button press you can do: if task.thistrial.gotResponse == 0 %your response code here end task.thistrial.gotResponse will be set to 1 the second time the subject presses a key. startTrialCallback (optional)You can (optionally) define a callback that gets called at the beginning of each trial function [task myscreen] = startTrialCallback(task,myscreen); endTrialCallback (optional)You can (optionally) define a callback that gets called at the end of each trial function [task myscreen] = endTrialCallback(task,myscreen); startBlockCallback (optional)You can (optionally) define a callback that gets called at the beginning of a block [task myscreen] = startBlockCallback(task,myscreen) Saving data into a stimfileStimfilesAfter you have run an experiment, all three variables (myscreen, task and your stimulus variable) will get saved into a file called yymmdd_stimnn.mat Where yymmdd is the current date, and nn is a sequential number starting at 01. This file will be stored in the current directory or in the directory ~/data if you have one. After these get saved, you can access all the variables for your experiment by using getTaskParameters(myscreen,task); This will return a strucutre that contains the starting volume of each trial, what each variable was set to, the response of the subject and reaction time, among other things. For most purposes this should contain all the information you need to reconstruct what was presented on what trial and what the subject's response was. Note that there is a variable called myscreen.saveData which tells the task structure whether to save the stim file or not. The default on your computer is probably set not to save the stim file. When you run on the computer in the scanner room, it will save the file automatically. For debugging purposes this is usually what you want so that you don't save unnecessary stim files every time you test your program. However if you want to save the stim file on your test computer to look at, you can add the following to your code where you call initScreen: myscreen.saveData = 1; myscreen = initScreen(myscreen); The variables stored in the stim file contain all the information you should need to recreate what happened in your experiment. In fact, it even contains a full listing of the file you used when running the experiment. This is useful since often you might make minor changes to the program and forget what version you were using when you ran an experiment. You can access a listing from the task variable: task{1}{1}.taskFileListing===== Directory to save stimfiles in =====
By default, mgl will save the data in ~/data if that directory exists, and in the current directory if ~/data doesn't exist. To save data to a specific directory instead of to these defaults, set myscreen.datadir = datadirname; where datadirname is the full path of the desired directory. Retrieving data from stimfilesgetTaskParameters
usage: e = getTaskParameters(myscreen,task);
getStimvol
usage: [stimvol stimNames var] = getStimvol(v,'varname',<taskNum=?>,<phaseNum=?>,<segmentNum=?>);
An example: v = newView; v = viewSet(v,'curGroup',3); v = viewSet(v,'curScan',1); [stimvol stimNames var] = getStimvol(v,'varname','taskNum=2','phaseNum=2'); getStimvolFromVarname
usage:[stimvol stimNames] = getStimvolFromVarname(varnameIn,myscreen,task,taskNum,phaseNum,segmentNum);
varnameIn can be the name of a parameter or randVar. e.g.: getStimvolFromVarname('dir',myscreen,task,2,2);
It can also be of the form varname(indexVar). For when you have used a parameterCode and an index variable. e.g.:
getStimvolFromVarname('localDir(dirIndex)',myscreen,task);
Or it can be _all_ which returns all trial numbers regardless of stimulus type: getStimvolFromVarname('_all_',myscreen,task);
If varnameIn is a cell array, then you can specify a set of matching conditions. For example, the following would return the stimvols for when var1 = 1 *and* var2 = either 2 or 3: {'var1=[1]','var2=[2 3]'}
If you want to return multiple sets of stimvols with matching conditions, you can make a cell array of cell arrays of the type form above. For example: {{'var1=[1]','var2=[2 3]'},{'var1=[2]','var2=[1]'}}
getTaskVarnames
usage:varnames = getTaskVarnames(task);
getParameterTrace
usage: trace = getParameterTrace(myscreen,task,'varname');
getVarFromParameters
usage: [varval taskNum phaseNum] = getVarFromParameters('varname',e);
makeTracesFor most people, using getTaskParameters is the easiest way to get what happened on each trial. But there is another mechanism that allows you to see the specific timing of events as traces. This is saved in the traces field of the myscreen variable. This field stores when each volume was collected and what stimulus was presented. Using this information you can reconstruct the volume when each stimulus occurred. It is set up so the first row contains an array which has a one every time a volume was acquired (i.e. whenever a backtick was received) and zeros elsewhere. The timebase for the array is in monitor refreshes, so every 60 elements shouls be one second. Take a look at what this trace has by doing: myscreen = makeTraces(myscreen); plot(myscreen.traces(1,:)); You can also plot in seconds, relative to the beginning of the experiment: plot(myscreen.time,myscreen.traces(1,:)); The other important trace is the one corresponding to myscreen.stimtrace: plot(myscreen.traces(myscreen.stimtrace,:)); This will contain the information about which trial was presented as long as you have set the writeTrace variable correctly (see next section). How-TosHow to end the experimentIn general, the easiest way to code the stimulus is to have it continue indefinitely until the scanner stops scanning. After the scan is finished and you want to stop the stimulus you hit the ESC key. This way you never have the stimulus stop before the scanner does, and it doesn't hurt to keep having the stimulus go past the end of the scan. If instead you want to only collect a specific number of blocks of trials and stop, then you would set: task{1}.numBlocks = 4;
say, to run for 4 blocks of trials and then stop. Or if you want to run for a specific number of trials and stop, then you can do: task{1}.numTrials = 17;
which would run for 17 trials and stop. These variables default to inf so that the experiment only stops when the user hits ESC. How to use 10-bit contrastIf you want to use 10-bits so as to be able to display finer contrast gradations, you need to remap the usual 8-bit contrast steps (0:255) into a subset of the larger 10-bit (1024) contrast table. This can be done using a piece of code called setGammaTable that can be included in your code as a subfunction (written by JG and FP and found at ~shani/matlab/MGLexpts/setGammaTable.m), but there are some details to be careful of. First, you will want to ‘reserve’ some colors that you will want to be able to use and leave unaffected by the resetting of the gamma table. This allows you to show, for example, a high-contrast fixation cross at the same time that you’re showing a low-contrast target. If you don’t reserve some colors, you won’t be able to have anything high-contrast at the same time as you use the 10-bit capacity. See example code taskTemplateContrast10bit.m where four colors are saved, and a low-contrast target is shown (written by SO and found at mgl/task/taskTemplateContrast10bit.m). How to run a dual taskIf you want to run two tasks at once, for example, an RSVP task at fixation and a detection task in the periphery, you will create two tasks and call one from within the other. You should construct it so one task (e.g. detection) is the main task and the other task (e.g. fixation-RSVP) is the subsidiary task. The subsidiary task needs to be constructed like a regular task, with its own initialization and callbacks, but without the updateTask loop. It will be updated from within the main task. The main task will be constructed as usual, but an extra line will appear to set the subsidiary task and to update it. For example, to set the fixation task as the subsidiary, you will add a line in the main task like this: task{2} = fixationTask(myscreen);
Then, the update loop of the main task will look like this: phaseNum = 1;
while (phaseNum <= length(task{1})) && ~myscreen.userHitEsc
% update the task
[task{1} myscreen phaseNum] = updateTask(task{1},myscreen,phaseNum);
[task{2} myscreen] = updateTask(task{2},myscreen,1);
% flip screen
myscreen = tickScreen(myscreen,task);
end
% if we got here, we are at the end of the experiment
myscreen = endTask(myscreen,task);
The key to getting this to work is to control the timing. One way to do this is to have the main task set some variables which tell the subsidiary task whether or not to run. In order to do this, have the stimulus variable set as a global variable in both tasks. Set two stimulus subfields as flags, e.g. stimulus.startSubsidiary and stimulus.endSubsidiary, in order to control the subsidiary task. Then have the subsidiary task check the status of these flags, and start or stop accordingly. In order to get the subsidiary task to start and stop when the appropriate flags are set, you will need to do the following: Set the first segment of the subsidiary task to have infinite length. That makes the subsidiary wait in the first segment until the main task calls it. When the main task wants to start the subsidiary task, it will set the stimulus.startSubsidary flag to 1, and this will cause the subsidiary to jump to the next segment as follows: In the screenUpdate callback of the subsidiary task, have a loop that checks to see whether the stimulus.startSubsidiary flag is set to 1. (This should be done in screenUpdate so that it can check all the time.) Have an if-loop that tells the task to skip ahead to the next segment as soon as the flag == 1. (It’s a good idea to reset the flag to 0): if(stimulus.startSubsidiary == 1) stimulus.startSubsidiary = 0; task = jumpSegment(task); end When you’re ready to end the subsidiary task, have the main task set the stimulus.endSubsidiary flag to 1, and have the following if-loop in the subsidiary’s screenUpdate callback: if(stimulus.endSubsidiary == 1) stimulus.endSubsidiary = 0; task = jumpSegment(task,inf); end The ‘inf’ argument in the jumpSegment function call tells the task to jump to the end of all the segments and start the next trial. This puts the subsidiary task back into the state of being in the infinite first segment, waiting for the start flag to be reset to 1 by the main task. Example code can be found in taskTemplateDualMain.m and taskTemplateDualSubsidiary.m How to calibrate the monitorMoncalibTo calibrate a monitor, you can use the program moncalib.m in the utils directory. It is set up to work with the PhotoResearch PR650 photometer/colorimeter (which the Lennie lab has) and a serial port adaptor (use the one from the Carrasco lab it is a white Keyspan USA-28 and says Carrasco Lab on it–the one that is in the bag with the photometer is a white translucent Keyspan USA-28X B and doesn't seem to work properly). The serial port interface for matlab is included in the mgl distribution but can also be found on the Mathworks website [1]. To use the Keyspan USA-28 adaptor you will need to download a driver from [2].
PR650 REMOTE MODE (XFER) s/w ver 1.02 CMD 51 NAK This indicates that you pressed the return while the photometer is waiting for a transfer signal (not sure what it is), and hence entered the XFER mode. If you wait another 2 secs or so it will enter the control mode, now press 'return' you should see this: PR650 REMOTE MODE (CTRL) s/w ver 1.19 CMD B Basically there is about 2-3 secs time window you should press 'return' to get to this state.
If you cannot install the serial port interface or don't want to automatically calibrate using the USB cable you can also use the program to run manually with any photometer by typing in the luminance measurements yourself. The program moncalib will save a calibration file in the local directory. For you to use this calibration file, you can store it in one of two places. Either in your own program directory under a directory called displays: ./displays Or you can store it in the general displays directory mgl/task/displays InitScreen should automatically find the correct table by checking your computer name and looking for the file in these two places. If you do not use the standard filename, or have multiple calibrations for the same computer (like if you have multiple monitors calibrated), you can use a specific file by setting myscreen.calibFilename myscreen.calibFilename = 'mycalibrationfile.mat'; myscreen = initScreen(myscreen); Note that the calibFilename can be a literal filename as in the above, or you can specify a portion of the name that will get matched in a file from the displays directory (e.g. computername_displayname would matcha any file in the displays directory that looks like *computername_displayname*.mat). The name of the file usually created by moncalib will be: xxxx_computername_yymmdd.mat Where xxxx is a sequential number starting at 0001 and yymmdd is the date of the calibration. This stores a variable called calib which contains all the information about the calibration. You can quickly plot the data in calib by doing: load 0001_stimulus-g5_LCD_061004 moncalib(calib); The most important field of calib is the table field which holds the inverse lookup table to linearize the monitor. 10 bit gamma tablesThe NVIDIA GeForce series of video cards have 10 bit gamma tables (these are the only ones we have tested):
ATI 10 bit cards:
It is always the best to use the bit test in moncalib because some drivers do not allow 10-bit control on 10-bit DAC cards. You can also query the display card to see if it says that it supports a 10 bit gamma: displayInfo = mglDescribeDisplays Check the field gammaTableWidth to see if it is 10. Calibration devicesNote that there are some commercially available devices to calibrate monitor screens which create color profiling information (e.g. [5] [6] [7]. We have tested one of these called Spyder2Pro which allows you to linearize the monitor output but found that is not yet suitable for psychophysics purposes. The calibration program crashes when you use the default settings to linearize the monitor (an email to the tech support confirmed this is a bug in their software). Using advanced settings it worked but it could only test luminance at 5 output levels. The linearization that it achieved was not accurate enough when tested with the PR650 (it looked like they are doing some sort of spline fit of the points and the luminance as a function of monitor output level looked like a wavy line around the ideal). How to run an experiment with the same random sequence as a previous oneYou can do this by calling initScreen with the randstate of the previous experiment initScreen([],previousMyscreen.randstate); This will insure that all the parameters, randVars and segment times are generated with the same random sequence as the previous experiment. Alternatively, you can run both experiments starting with the same randstate (which can be an integer value). For example initScreen([],11); Will run the experiment with exactly the same randomization sequence every time. Task function referenceinitScreen
purpose: initializes the screen
This function initializes the screen by calling mglOpen, and also handles a number of different default initialization procedures such as setting up the gamma table with the correct linearization table. You should call this once at the beginning of the experiment. The variable myscreen will contain many fields associated with the status of the screen and records events like volume acquisitions and trial/segment times etc. initStimulus
purpose: initializes the global stimulus variable name
Note that this function, only needs to be called if you want to save the stimulus in your stim file. Since stimulus is a global variable, if you call this function, at the end of the experiment it will get the global variable with the name you specified here and save it in your stim file. If you do not need to save your stimulus variable, you do not need to call this function. initTask
purpose: initializes a task variable
The task variable gets set up as explained above. Here is a list of valid fields:
updateTask
purpose: updates the task
tickScreen
purpose: updates the screen
This function calls mglFlush to update the screen when it is needed and also checks for volumes and keys etc. Called with in main loop. upDownStaircaseImplements a staircase for control of stimulus variable values. Type 'help upDownStaircase' for details. Also see taskTemplateFlashingStaircase.m and taskTemplateStaticStaircase.m for examples of using this function. jumpSegmentAllows you to force a move to the next segment or the next trial: task = jumpSegment(task) % this will end the segment and move to the next one task = jumpSegment(task,inf) % this will end the trial and start a new trial Sample programs can be found in the /Task subfolder, and include:
When things don’t work, there are some very simple bugs you should check for before losing hope. Return variablesMany of the routines return multiple values–it is really important that you receive these properly. For example, initTask returns both the task and myscreen. Make sure that you call it as follows: [task{1} myscreen] = initTask(task{1},myscreen,@startSegmentCallback,@screenUpdateCallback,@responseCallback);
If you did not have the myscreen in the left hand side, then initTask will not set fields (primarly myscreen.stimtrace) correctly in your myscreen–which can make saving out traces incorrect. Look out for this not only in initTask but in any function like updateTask to make sure you are always setting the appropriate return variables correctly (check the help on the function–or model your calls after the ones in the templates). Screen flashesSometimes the screen might appear to flash momentarily with your stimulus in a way you don't expect. Often this is due to not calling mglClearScreen at the appropriate place (like at the beginning of a segment or at the beginning of the screenUpdateCallback). The reason for this occurring is often because you are drawing to the back buffer of the double buffered screen and it already has something from before when it was being displayed. Clearing the screen as you start drawing will insure that you don't have any junk from the last screen draw that will show up. Matlab controlDon't let the window from which you are calling your code be on the screen that is taken over by MGL, or you will lose the ability to stop the code from running or exit if your code is interupted by an error. If you do this, you can always type option-command-ESC (brings up Force-quit applications) to close the window. This will also quit your matlab session. Clear allMgl keeps the status of the display in the global variable called MGL. You can look at the values set in MGL by doing: global MGL MGL Be aware that if you clear the MGL variable then mgl will no longer know the status of the screen. The current version handles this by closing the display if you clear the MGL variable. If for some reason, you get stuck with an open display that you cannot close, you can try to call mglOpen again and then close. If you have reset the gamma table, you can go into your System Preferences/Displays/Color and reset the gamma table back to normal there. On Linux, you can use xgamma to set the gamma table, or easier, use NVIDIA's settings manager or the ATI settings manager to restore gamma tables. If you still cannot close the display, you can always do option-open apple-escape and quit out of matlab (on Mac). On Linux, Ctrl-C may work, or you may have to kill the matlab process from a terminal. If you only have one screen and it is locked, you can type Ctrl+Alt+F5 to drop out of the X server temporarily; this will allow you to login and kill the Matlab process without restarting X (Ctrl+Alt+F7 to return to X). Spelling, capitalization, and periodsIf an important variable (e.g. stimulus) is misspelled (e.g. sitmulus), or a variable or function name mis-capitalized (e.g. inittask instead of initTask), or you forget to put the period between a variable name and a field name (e.g. stimuluscontrast instead of stimulus.contrast) things won’t work and it’ll be very confusing! MGL will check for some things that it knows about, but it can’t know how you’ve named your variables… Your task doesn't exit when it's overFirst, make sure you've specified a certain number of trials, otherwise it'll go on forever. Then, make sure you have a while loop around the updateTask call checking the phase number. Even if you only have one phase, you still need to give updateTask a phaseNum variable as input, because the code will only end when the phaseNum gets too big for the while loop (and updateTask increases the phaseNum after all the trials have been run) Set the data directorySo as to always know where your data will be saved, it's good to set the data directory by doing: myscreen.datadir = datadirname; where datadirname is the full path of the directory to which you'd like the data to be saved. Make sure your displays aren’t mirrored.Sometimes nothing at all will happen. This can be because your screens are mirrored. Some common error messages that have to do with cell arrays and how to fix themError message saying you haven't specified task.segmin and task.segmaxMake sure you've called initTask with the exact task and phase. For example: task{1}.seglen = 1;
task{1} = initTask(task{1},myscreen,@startSegmentCallback,@screenUpdateCallback,@responseCallback);
or task{1}{1}.seglen = 1;
task{1}{1} = initTask(task{1}{1},myscreen,@startSegmentCallback,@screenUpdateCallback,@responseCallback);
Error message saying 'Cell contents reference from a non-cell array object,'Make sure you are calling updateTask with a cell array. This can be confusing, because even if you only have one task and only one phase, you still need to define task{1} (rather than just 'task') and then call updateTask with 'task' - e.g.: task{1}.seglen = 1;
task{1} = initTask(task{1},myscreen,@startSegmentCallback,@screenUpdateCallback,@responseCallback);
phaseNum = 1; while (phaseNum <= length(task)) && ~myscreen.userHitEsc [task myscreen phaseNum] = updateTask(task,myscreen,phaseNum); end If you're running two tasks, and need to differentiate them, then even if they each only have one phase, you must define task{1}{1} and task{2}{1} and then call updateTask with task{1} and task{2}. For example: task{1}{1}.seglen = 1;
task{1}{1} = initTask(task{1}{1},myscreen,@startSegmentCallback,@screenUpdateCallback,@responseCallback);
task{2} = setSecondTask(myscreen); % as long as setSecondTask returns a cell array
phaseNum = 1;
while (phaseNum <= length(task{1})) && ~myscreen.userHitEsc
[task{1} myscreen phaseNum] = updateTask(task{1},myscreen,phaseNum);
[task{2} myscreen] = updateTask(task{2},myscreen,1);
end
Error message saying 'Attempt to reference field of non-structure array,'Make sure that you call updateTask and also return from updateTask with the same cell array (see above). Error message 'Input argument "tnum" is undefined,'Make sure you are passing in a phaseNum argument in your updateTask function call. Problem starting/stopping the eye trackerTo control the eye tracker you must have the file mgl/task/utils/readDigPort/writeDigPort.c mex'd. You can do cd mgl/task/utils/readDigPort mex writeDigPort.c This compiles the program to send the digital pulse to the eye tracker. Also, if your task crashes at the beginning after it prints out Dev1/port2 or something similar to that, you probably just need to recompile writeDigPort.c. If you continue to have problems and want to give up, you can delete the mexfile writeDigPort.mexmac and then mgl won't try and set the digital port (This is the default condition that the mgl library is in, because most computers don't have the NI card installed). ContributorsDesign and Implementation
Other contributors
|