Skip to content

Visual coordinate systems

Luke Campagnola edited this page Jul 4, 2014 · 5 revisions

Many Visuals will need to be aware of four different coordinate systems. Their definitions follow, but please suggest alternative names for these.

  1. The data coordinate system is an arbitrary system specified by the raw data. Ideally, this data is completely unprocessed by Python--it can be transferred directly from disk (or device) to the GPU, and all necessary transformations may be computed there.
  2. A document coordinate system that defines measurements like px, mm, etc. In most cases, this will be the same as the logical pixel coordinate system of the canvas -- if I ask for a line that is 5 px wide, that measurement usually refers to logical pixels (although there are cases when we will want to specify that a Visual should use a different document coordinate system). Likewise, if I ask for an ellipse that is 2 cm wide, then the physical coordinate system must have a scale factor to convert between logical pixels and cm (dpi).
  3. A pixel coordinate system that corresponds to the physical pixels of the canvas (the fragment shader executes once per physical pixel). This coordinate system is used primarily for antialiasing--for high-quality antialiasing, each fragment must know what portion of the pixel is covered by the visual's geometry. This coordinate system may also be used to dynamically adapt the geometry of a visual--for example, to determine the optimal number and spacing of vertices in a curve in order to generate a smooth appearance, or to dynamically downsample the data displayed in a plot curve containing more data that can be fit in VRAM.
  4. Normalized device coordinates are (-1, -1) to (1, 1) across the current glViewport. The output of the vertex shader must be expressed in this coordinate system.

I recommend that the base Visual class have access to transforms that map between each pair of these coordinate systems in order:

  • visual.transform maps data->document
  • visual.pixel_transform maps document->pixel
  • visual.nd_transform maps pixel->ND

We can then map all the way from data to ND by composing these three transforms, and optionally make adjustments at each step of the way. By default, each of these is a NullTransform, so if we only specify visual.transform, then it can map directly data->ND, for simple visuals that do not care about the document/pixel transforms.

Implementation examples:

Thick lines

Thick lines must be represented as a strip of triangles. The vertexes are initially passed to the GPU using the original, unmodified vertex coordinates--thus the line initially has a width of zero. To apply a line width, the GPU is given a transform that maps data->document coordinates. After passing each vertex through this transform, it is then possible to adjust the line coordinates to have a specific width by adding a small vector to each vertex. Note that this transform determines more than the length of this vector; it may also affect the direction and apply any nonlinear transforms (for example, if the line is displayed with log scaling, this scaling must be done before the line width is applied, or else the line will not have consistent width). The adjusted vertexes are then mapped through a second transform (document -> ND) before output to gl_Position.

Adaptively sampling mathematical curves

For adaptively sampling a mathematical curve, we must consider the shape of the curve in pixel coordinates, because that system determines the limit of resolution for a curve. This will need to be done in python until we have geometry / tesselation shaders available. The details will differ for each case, but let's consider the most general solution (slow, but supports any arbitrary transform): 1. Pick an endpoint P of the curve. 2. Map this to pixel coordinate system. 3. Make two new points Px and Py, which are one pixel to the right and upward, respectively 4. Map these points back to the data coordinate system. Now we have vectors P-Px and P-Py that tell us the size and shape of a pixel at that particular location on the curve (remember, for nonlinear transforms the pixel shape may vary along the curve ). 5. Use these two vectors to determine how far to travel along the curve to the next point P1 6. Go back to step 2, repeat until the end of the curve.

Note1: We could (and should) also implement more efficient methods that only work under linear transforms. For example, under linear transforms it is not necessary to recompute Px and Py at every vertex.

Note2: It is also possible to under-sample mathematical curves and have the fragment shader draw their shape correctly. I believe Nicolas has written many examples of this, but there are situations in which either technique (or both together) may be more appropriate.

Antialiasing edges:

There are a couple of options.

  • In the vertex shader we can map each vertex to the pixel coordinate system in order to send information about the visual's geometry to the fragment shader. There, we can obtain the current pixel coordinate of the fragment and compare that to the geometry given by the vertex shader to determine the amount of pixel coverage (often, all we need to know is the distance from the current fragment to the edge of the visual).
  • We can first obtain the pixel coordinate from the fragment shader, and then map backward to determine what part of the original geometry lies under the pixel. This is mathematically equivalent to the first option, but may be more or less efficient depending on the details of the visual. This technique is used in the Image visual to achieve nonlinear transformations on images.
Clone this wiki locally