-
Notifications
You must be signed in to change notification settings - Fork 619
Trash. Transforms
The package vispy.transform handles all the transformation logic used to go from one coordinate system to another, on both CPU and GPU. Both linear and non-linear transformations should be supported.
A coordinate system represents a particular numerical representation of the data. A given coordinate system is specified by some parameter(s). For instance, a cartesian system is specified by an origin and a non-singular matrix containing the vectors of its base.
class CoordinateSystem(object):
pass
class CartesianSystem(CoordinateSystem):
def __init__(self, origin, base):
# origin is a d-long vector, base is a d*d matrix
class PolarSystem(CoordinateSystem):
pass
A Transformer defines how data are changed when they're represented in a new coordinate system.
class Transformer(object):
direct_glsl
direct_python
inverse_glsl
inverse_python
def __init__(self, system1, system2):
pass
def __call__(self, data):
pass
class CartesianTransformer(Transformer):
pass
class CartesianPolarTransformer(Transformer):
pass
import vispy.transform as vt
transformer = vt.transform(system1, system2)
data_new = transformer(data)
A Transformer object contains direct and inverse Python functions to transform a NumPy n*d array from the old to the new coordinate system. The same functions can be obtained in GLSL too so that transformations can be done either on the CPU or GPU.
LC: I have basically the same concept implemented in shaders/transforms.py. There is a Transform class which defines a GLSL function (forward mapping only; usually inverse mapping is not needed on the GPU?) as well as map() and imap() methods which perform the forward/inverse mapping in python. Each Transform subclass may define a different type of transformation such as scale/translate, affine, log, polar, etc.
CR: we might need the inverse mapping on the GPU for a Grid/Tick visual implemented in the shader, that would need to have access to the data coordinate system (e.g. to show the x=0, y=0 lines)...?
We can specify a transformation with two equations: the direct and indirect formulas, which should be provided in Python and GLSL. For example, the transformation from the canonical base to a cartesian system:
origin + np.dot(pos, base) # Python
origin + base * pos # GLSL
All transformation should occur in double-precision floating points.
The transformer contains the transformation from system 1 to base, and from base to system 2, unless the direct transformation from system 1 to system 2 is already implemented.
We could have a dictionary where keys are tuples (system1_class, system2_class), and values are Transformer classes.
We define the following cartesian systems:
data: original data, arbitrary coordinates
gpu: should be close to the origin
viewport: -1 to 1, includes zoom/pan
window: 0 to width/height pixels
LC: This is how I organize coordinate systems:
- data: original data, arbitrary coordinates (possibly non-cartesian).
- viewport: position of data within a scale/zoomable viewport. Usually this has cartesian coordinates which are offset relative to the document coordinate system, but shares the same scaling. In the case that a single viewport fills the entire canvas (such as in most of the examples we have written), the offset will be (0,0) and the viewport coordinates are exactly the same as document coordinates.
- document: cartesian coordinate system. Scaled such that values are expressed in physical units (px, cm, etc.). In most cases, the "document" is the Canvas we are drawing into, the origin is at one of the corners of the canvas, and units are in pixels. This is the coordinate system in which line widths, marker sizes, and tick lengths are specified. It is also the coordinate system in which we specify the position and size of viewports (imagine a figure with multiple plots). For export to SVG / PDF, etc., this is the final coordinate system; once data coordinates are mapped to this system, they may be written out to disk.
- normalized device coordinates: (cartesian; -1 to 1). When drawing with openGL, this is the final coordinate system (the values which are ultimately returned by the vertex shader). Usually, the corners of this box (at -1 or 1) will map exactly to the corners of the document. However, extra scaling / translation may take place if the entire document is placed inside a zoomable viewport (for example, imagine writing an application like inkscape/illustrator in which the user may zoom and pan over the document.)
- device coordinates: actual pixel locations on the window. We never deal directly with this coordinate system; OpenGL handles the transformation from normalized device coordinates based on calls to glViewport(). In most (but not all!) cases, the device coordinate system will be the same as the document coordinate system (it is somewhat irritating that we need to convert to normalized device coordinates first).
Even though we conceptualize all coordinates as being expressed in one of these coordinate systems, all of the work is in transforming between the systems. Between each system is one or more transformations, and to map a coordinate from one system to another requires mapping through each transformation in the chain, one at a time (although it is often possible to merge together adjacent transformations for efficiency).
Drawing a visual with the correct transformation requires mapping the visual's data through each of the transformations in the chain until the desired coordinate system is reached (normalized device coords for OpenGL rendering, document coords for SVG/PDF export). Thus each visual is provided a list of transformations (the "chain"), and ideally the GPU will handle all of the transformations. Each transformation defines a single GLSL function; to map data through the entire chain, we must generate GLSL code which calls each function in order (for example, see TransformChain in shaders/transforms.py). It may also be desirable to compute all or part of the transformation on the CPU.
In many cases, it will be necessary for both the vertex and fragment shaders to have access to vertex locations in multiple coordinate systems. To allow this, we may break up the transform chain into multiple pieces to allow access to each coordinate system. For example, consider a plot line with 3px line width. It is possible to represent the mapping from data coordinates to normalized device coordinates with a single transform chain, but the shader program will need some knowledge of the document coordinate system as well in order to correctly interpret the 3px line width. Thus, we would provide the program with two chains--one that maps from data coordinates to document coordinates, and a second mapping from document to normalized device coordinates.
Since each Transform subclass provides a single GLSL function for performing its transformation, chains of transforms are achieved by generating custom GLSL code which calls each transform function in order. For systems that can link multiple vertex shaders together, we may pre-compile one shader for each Transform and simply link in the necessary transforms when producing the final program. For ES2, only a single vertex shader is allowed, so we would instead simply append the GLSL code for each Transform to the end of the main vertex shader code. In later OpenGL versions (I think beginning in 4), function pointers have been added, which would make it unnecessary to generate any custom GLSL code to achieve transform chaining.