Failed to do Orthographic Projection with vt.orthoProjectionMatrix()

I recently got great help from @mdc to perform a perspective projection and create an optic flow animation. Now that I managed to draw circles with fragment shaders, I would like to create another stim, with the same code, but instead of the perspective projection, I want the orthographic projection. I replaced then the perspective projection transformation with vt.orthoProjectionMatrix(). But I am just getting a black window with nothing drawn on it. Is there something I am missing, do I have to change other parameters?

Thanks in advance for your help!!

scrDist = 0.50  # 50cm
scrWidth = 0.53  # 53cm
scrAspect = 1.0

 # Create a window
 
 win = psychopy.visual.Window(
      monitor=mon, 
      size=(800, 800),
      color='Black',
      colorSpace='rgb',
      units='deg',
      screen=scrn,
      allowGUI=True,
      fullscr=False)
 # Frustum
frustum = vt.computeFrustum(scrWidth, scrAspect, scrDist, eyeOffset = 0.0, nearClip=0.05, farClip=10000.0)
# Frustum
frustum = vt.computeFrustum(scrWidth, scrAspect, scrDist, eyeOffset = 0.0, nearClip=0.05, farClip=10000.0)

# Transformation for points (model/view matrix)
MV = mt.translationMatrix((0.0, 0.0, -scrDist))  # X, Y, -Z (in camera view, the camera looks down the -Z axis)

# Perspective Projection
O = vt.orthoProjectionMatrix(*frustum)


# Set the window matrices
win.orthoMatrix = O
win.viewMatrix = MV

# create array of random points
nPoints = 900;
pos = np.zeros((nPoints, 3), dtype=np.float32)   # create empty vector for each point, 3 coordinates (x,y,z)


# random X, Y : fill X and Y coordinates with uniform random values that serve as coordinates 
pos[:, :2] = np.random.uniform(-500, 500, (nPoints, 2))

# random Z to far clipping plane, -1000.0 is -farClip
pos[:, -1] = np.random.uniform(0.0, -1000.0, (nPoints,))
while 1:
    
    # --- render loop ---
    
    # Before rendering
    win.applyEyeTransform()
    
    # draw 3D stuff here
    GL.glColor3f(1.0, 1.0, 1.0)
    GL.glPointSize(4.0);
    
    GL.glBegin(GL.GL_POINTS)
    for i in range(nPoints):   # go over our array and draw the points using the coordinates
        # color can be varied with distance if you like
        #GL.glPointSize = GL.glPointSize / pos[i, :]
        GL.glVertex3f(*pos[i, :])  # position

    GL.glEnd()

    win.flip()

    # transform points -Z direction, in OpenGL -Z coordinates are forward
    pos[:, -1] += 12.0  # distance to move the dots per frame towards the viewer

    # if a point is behind us, return to initial -Z position
    pos[:, -1] = np.where(pos[:, -1] > 0.0, -1000.0, pos[:, -1])

   # check events to break out of the loop!

    if len(event.getKeys())>0:
        break
    event.clearEvents()
    
win.close()

Hello Kathia,

There are a few issues here (one is actually my fault :/).

First off the following is incorrect:

win.orthoMatrix = O

it should be …

win.projectionMatrix = O

Secondly, orthographic projections have frustums that don’t widen with distance, therefore sampling points within a range of -500 to 500 will result in very few if any projecting on your screen plane. You need to sample within the range of your frustum’s left, right, top and bottom dimensions to see anything. Furthermore, the points will not appear to move as they approach your viewer since they project on perpendicular lines (orthogonal) to the screen. You can use orthographic projections if you wish the manually compute the locations of points to get the affect you want, much like the star field demo that ships with PsychoPy.

Lastly, here is where I screwed up but hopefully fixed. My orthographic function does not convert points properly assuming how OpenGL’s coordinates are represented. Here is a working version of the function you can use…

def orthoProjectionMatrix(left, right, bottom, top, nearClip, farClip,
                          out=None, dtype=None):
    """Compute an orthographic projection matrix with provided frustum
    parameters.

    Parameters
    ----------
    left : float
        Left clipping plane coordinate.
    right : float
        Right clipping plane coordinate.
    bottom : float
        Bottom clipping plane coordinate.
    top : float
        Top clipping plane coordinate.
    nearClip : float
        Near clipping plane distance from viewer.
    farClip : float
        Far clipping plane distance from viewer.
    out : ndarray, optional
        Optional output array. Must be same `shape` and `dtype` as the expected
        output if `out` was not specified.
    dtype : dtype or str, optional
        Data type for arrays, can either be 'float32' or 'float64'. If `None` is
        specified, the data type is inferred by `out`. If `out` is not provided,
        the default is 'float64'.

    Returns
    -------
    ndarray
        4x4 projection matrix

    See Also
    --------
    perspectiveProjectionMatrix : Compute a perspective projection matrix.

    Notes
    -----

    * The returned matrix is row-major. Values are floats with 32-bits of
      precision stored as a contiguous (C-order) array.

    """
    if out is None:
        dtype = np.float64 if dtype is None else np.dtype(dtype).type
    else:
        dtype = np.dtype(out.dtype).type

    projMat = np.zeros((4, 4,), dtype=dtype) if out is None else out
    projMat.fill(0.0)

    u = dtype(2.0)
    projMat[0, 0] = u / (right - left)
    projMat[1, 1] = u / (top - bottom)
    projMat[2, 2] = -u / (farClip - nearClip)
    projMat[0, 3] = -((right + left) / (right - left))
    projMat[1, 3] = -((top + bottom) / (top - bottom))
    projMat[2, 3] = -((farClip + nearClip) / (farClip - nearClip))
    projMat[3, 3] = 1.0

    return projMat

I’ve already patched it in PsychoPy so no one else should have this issue in the future. However, most of the time people would just call resetEyeTransform to get an orthographic projection or never bother because PsychoPy defaults on an orthographic projection. But I’m glad you caught this issue!

Thanks for your feedback!
I am not sure I understood everything regarding the way to compute the orthographic projection. As it seems to be the default mode of PsychoPy, shall I skip defining the frustum and the transformations, and just call win.resetEyeTransform() instead of win.applyEyeTransform() ? Is there a demo or an example on how to perform an orthographic projection, or use the function orthoProjectionMatrix(left, right, bottom, top, nearClip, farClip,out=None, dtype=None). Do I have to update PsychoPy to integrate the modifications you performed on this function?

When I tried to play with the values of the function as follows:

# Frustum
frustum = vt.computeFrustum(scrWidth, scrAspect, scrDist, eyeOffset = 0.0, nearClip=0.05, farClip=50.0)

# Transformation for points (model/view matrix)
MV = mt.translationMatrix((0.0, 0.0, -scrDist))  # X, Y, -Z (in camera view, the camera looks down the -Z axis)

# Perspective Projection
O = vt.orthoProjectionMatrix(20.0,20.0,20.0,20.0,nearClip=0.05,farClip=50.0)


# Set the window matrices
win.projectionMatrix = O
win.viewMatrix = MV

# create array of random points
nPoints = 900;
pos = np.zeros((nPoints, 3), dtype=np.float32)   # create empty vector for each point, 3 coordinates (x,y,z)


# random X, Y
pos[:, :2] = np.random.uniform(-10.0, 10.0, (nPoints, 2))

# random Z 
pos[:, -1] = np.random.uniform(0.0, -50.0, (nPoints,))

I got the following error:

---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
<ipython-input-1-c231b7a0a7a2> in <module>()
     53 
     54 # Perspective Projection
---> 55 O = vt.orthoProjectionMatrix(20.0,20.0,20.0,20.0,nearClip=0.05,farClip=50.0)
     56 
     57 

~/anaconda3/lib/python3.6/site-packages/psychopy/tools/viewtools.py in orthoProjectionMatrix(left, right, bottom, top, nearClip, farClip)
    279     """
    280     projMat = np.zeros((4, 4), np.float32)
--> 281     projMat[0, 0] = 2.0 / (right - left)
    282     projMat[1, 1] = 2.0 / (top - bottom)
    283     projMat[2, 2] = -2.0 / (farClip - nearClip)

ZeroDivisionError: float division by zero

Is this the mistake you are referring to?

Thank in advance for your help :slight_smile:

Hi,

You need to put the new function into the script your running, or in a separate file and import it. It fixes an issue with the version that ships with psychopy. No need to update yet.

For the zero division error, your left/right, top/bottom values are the same, they need to be:

O = vt.orthoProjectionMatrix(-20.0,20.0,-20.0,20.0,nearClip=0.05,farClip=50.0)

You need to call applyEyeTransform if using a custom frustum. By default, the PsychoPy ortho projection is using a fixed frustum size of 2 in all dimensions. This is typically what is used by 2D stimuli, but you need to use a custom one if you want a larger field.

  • mdc

Hi mdc,

Great, thanks for the clarifications! :slight_smile:
I have found the new function here, I copied pasted it into my code, and I did not get the error anymore. I also followed your correction of vt.orthoProjectionMatrix, and kept applyEyeTransform as I want to use a custom frustum. For some reason, I just got a black window, nothing was drawn ont it, and no errors were thrown :

Again, thanks so much for helping!!

 # Frustum
frustum = vt.computeFrustum(scrWidth, scrAspect, scrDist, eyeOffset = 0.0, nearClip=0.05, farClip=50.0)

# Transformation for points (model/view matrix)
MV = mt.translationMatrix((0.0, 0.0, -scrDist))  # X, Y, -Z (in camera view, the camera looks down the -Z axis)

# Perspective Projection
O = vt.orthoProjectionMatrix(-20.0,20.0,-20.0,20.0,nearClip=0.05,farClip=50.0)


# Set the window matrices
win.projectionMatrix = O
win.viewMatrix = MV

# create array of random points
nPoints = 100;
pos = np.zeros((nPoints, 3), dtype=np.float32)   # create empty vector for each point, 3 coordinates (x,y,z)


# random X, Y : fill X and Y coordinates with uniform random values that serve as coordinates 
pos[:, :2] = np.random.uniform(-10.0, 10.0, (nPoints, 2))

# random Z to far clipping plane, -1000.0 is -farClip
pos[:, -1] = np.random.uniform(0.0, -50.0, (nPoints,))
while 1:
    
    # --- render loop ---
    
    # Before rendering
    win.resetEyeTransform()
    
    # draw 3D stuff here
    GL.glColor3f(1.0, 1.0, 1.0)
    GL.glPointSize(4.0);
    
    GL.glBegin(GL.GL_POINTS)
    for i in range(nPoints):   # go over our array and draw the points using the coordinates
        # color can be varied with distance if you like
        #GL.glPointSize = GL.glPointSize / pos[i, :]
        GL.glVertex3f(*pos[i, :])  # position

    GL.glEnd()

    win.flip()

    # transform points -Z direction, in OpenGL -Z coordinates are forward
    pos[:, -1] += 12.0  # distance to move the dots per frame towards the viewer

    # if a point is behind us, return to initial -Z position
    pos[:, -1] = np.where(pos[:, -1] > 0.0, -50.0, pos[:, -1])

   # check events to break out of the loop!

    if len(event.getKeys())>0:
        break
    event.clearEvents()
    
win.close()

Sorry for the late reply, I didn’t get a notification for some reason.

Try replacing

win.resetEyeTransform()

with

win.applyEyeTransform()

Key in mind that nothing will render with perspective, so dots will looks static and blink in an out of existence.

Also consider using using a perspective projection and vary point sizes as below…

# create array of random points
nPoints = 400
pos = np.zeros((nPoints, 3), dtype=np.float32)  # create empty vector for each point, 3 coordinates (x,y,z)

# random X, Y : fill X and Y coordinates with uniform random values that serve as coordinates
pos[:, :2] = np.random.uniform(-10.0, 10.0, (nPoints, 2))

# random Z to far clipping plane, -1000.0 is -farClip
pos[:, -1] = np.random.uniform(0.0, -50.0, (nPoints,))
while 1:
    win.setPerspectiveView()
    # --- render loop ---
    #win.applyEyeTransform()

    # draw 3D stuff here
    GL.glColor3f(1.0, 1.0, 1.0)

    for i in range(nPoints):  # go over our array and draw the points using the coordinates
        # color can be varied with distance if you like
        # GL.glPointSize = GL.glPointSize / pos[i, :]
        GL.glPointSize(np.abs(100.0 / pos[i, 2]))
        GL.glBegin(GL.GL_POINTS)
        GL.glVertex3f(*pos[i, :])  # position
        GL.glEnd()

    win.flip()

    # transform points -Z direction, in OpenGL -Z coordinates are forward
    pos[:, -1] += 0.1  # distance to move the dots per frame towards the viewer

    # if a point is behind us, return to initial -Z position
    pos[:, -1] = np.where(pos[:, -1] > -10.0, -50.0, pos[:, -1])

    # check events to break out of the loop!

    if len(event.getKeys()) > 0:
        break
    event.clearEvents()