File: transform_system.py

package info (click to toggle)
python-vispy 0.6.6-1
  • links: PTS, VCS
  • area: main
  • in suites: bullseye
  • size: 21,240 kB
  • sloc: python: 57,407; javascript: 6,810; makefile: 63; sh: 5
file content (355 lines) | stat: -rw-r--r-- 13,940 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
# -*- coding: utf-8 -*-
# Copyright (c) Vispy Development Team. All Rights Reserved.
# Distributed under the (new) BSD License. See LICENSE.txt for more info.

from __future__ import division

from .linear import STTransform, NullTransform
from .chain import ChainTransform
from ._util import TransformCache
from ...util.event import EventEmitter

import numpy as np


class TransformSystem(object):
    """ TransformSystem encapsulates information about the coordinate
    systems needed to draw a Visual.

    Visual rendering operates in six coordinate systems:

    * **Visual** - arbitrary local coordinate frame of the visual. Vertex
      buffers used by the visual are usually specified in this coordinate
      system.

    * **Scene** - This is an isometric coordinate system used mainly for 
      lighting calculations.

    * **Document** - This coordinate system has units of _logical_ pixels, and
      should usually represent the pixel coordinates of the canvas being drawn
      to. Visuals use this coordinate system to make measurements for font
      size, line width, and in general anything that is specified in physical
      units (px, pt, mm, in, etc.). In most circumstances, this is exactly the
      same as the canvas coordinate system.

    * **Canvas** - This coordinate system represents the logical pixel
      coordinates of the canvas. It has its origin in the top-left corner of
      the canvas, and is typically the coordinate system that mouse and touch 
      events are reported in. Note that, by convention, _logical_ pixels
      are not necessarily the same size as the _physical_ pixels in the
      framebuffer that is being rendered to.

    * **Framebuffer** - The buffer coordinate system has units of _physical_ 
      pixels, and should usually represent the coordinates of the current 
      framebuffer (on the canvas or an FBO) being rendered to. Visuals use this
      coordinate system primarily for antialiasing calculations. It is also the
      coorinate system used by glFragCoord. In most cases,
      this will have the same scale as the document and canvas coordinate 
      systems because the active framebuffer is the
      back buffer of the canvas, and the canvas will have _logical_ and
      _physical_ pixels of the same size. However, the scale may be different
      in the case of high-resolution displays, or when rendering to an 
      off-screen framebuffer with different scaling or boundaries than the
      canvas.

    * **Render** - This coordinate system is the obligatory system for
      vertices returned by a vertex shader. It has coordinates (-1, -1) to
      (1, 1) across the current glViewport. In OpenGL terminology, this is
      called clip coordinates.

    Parameters
    ----------

    canvas : Canvas
        The canvas being drawn to.
    dpi : float
        The dot-per-inch resolution of the document coordinate system. By
        default this is set to the resolution of the canvas.

    Notes
    -----

    By default, TransformSystems are configured such that the document
    coordinate system matches the logical pixels of the canvas,

    Examples
    --------

    1. To convert local vertex coordinates to normalized device coordinates in
    the vertex shader, we first need a vertex shader that supports configurable
    transformations::

        vec4 a_position;
        void main() {
            gl_Position = $transform(a_position);
        }

    Next, we supply the complete chain of transforms when drawing the visual:

        def draw(tr_sys):
            tr = tr_sys.get_full_transform()
            self.program['transform'] = tr.shader_map()
            self.program['a_position'] = self.vertex_buffer
            self.program.draw('triangles')

    2. Draw a line whose width is given in mm. To start, we need normal vectors
    for each vertex, which tell us the direction the vertex should move in
    order to set the line width::

        vec4 a_position;
        vec4 a_normal;
        float u_line_width;
        float u_dpi;
        void main() {
            // map vertex position and normal vector to the document cs
            vec4 doc_pos = $visual_to_doc(a_position);
            vec4 doc_normal = $visual_to_doc(a_position + a_normal) - doc_pos;

            // Use DPI to convert mm line width to logical pixels
            float px_width = (u_line_width / 25.4) * dpi;

            // expand by line width
            doc_pos += normalize(doc_normal) * px_width;

            // finally, map the remainder of the way to normalized device
            // coordinates.
            gl_Position = $doc_to_render(a_position);
        }

    In this case, we need to access
    the transforms independently, so ``get_full_transform()`` is not useful
    here::

        def draw(tr_sys):
            # Send two parts of the full transform separately
            self.program['visual_to_doc'] = tr_sys.visual_to_doc.shader_map()
            doc_to_render = (tr_sys.framebuffer_transform *
                             tr_sys.document_transform)
            self.program['visual_to_doc'] = doc_to_render.shader_map()

            self.program['u_line_width'] = self.line_width
            self.program['u_dpi'] = tr_sys.dpi
            self.program['a_position'] = self.vertex_buffer
            self.program['a_normal'] = self.normal_buffer
            self.program.draw('triangles')

    3. Draw a triangle with antialiasing at the edge.

    4. Using inverse transforms in the fragment shader
    """

    def __init__(self, canvas=None, dpi=None):
        self.changed = EventEmitter(source=self, type='transform_changed')
        self._canvas = None
        self._fbo_bounds = None
        self.canvas = canvas
        self._cache = TransformCache()
        self._dpi = dpi
        self._mappings = {'ct0': None, 'ct1': None, 'ft0': None}

        # Assign a ChainTransform for each step. This allows us to always
        # return the same transform objects regardless of how the user
        # configures the system.
        self._visual_transform = ChainTransform([NullTransform()])
        self._scene_transform = ChainTransform([NullTransform()])
        self._document_transform = ChainTransform([NullTransform()])
        self._canvas_transform = ChainTransform([STTransform(),
                                                 STTransform()])
        self._framebuffer_transform = ChainTransform([STTransform()])
        
        for tr in (self._visual_transform, self._scene_transform, 
                   self._document_transform, self._canvas_transform,
                   self._framebuffer_transform):
            tr.changed.connect(self.changed)

    def _update_if_maps_changed(self, transform, map_key, new_maps):
        """Helper to store and check current (from, to) maps against new
        ones being provided. The new mappings are only applied if a change
        has occurred (and also stored in the current mappings).
        """
        if self._mappings[map_key] is None:
            self._mappings[map_key] = new_maps
            transform.set_mapping(new_maps[0], new_maps[1])
        else:
            if np.any(self._mappings[map_key] != new_maps):
                self._mappings[map_key] = new_maps
                transform.set_mapping(new_maps[0], new_maps[1])

    def configure(self, viewport=None, fbo_size=None, fbo_rect=None,
                  canvas=None):
        """Automatically configure the TransformSystem:

        * canvas_transform maps from the Canvas logical pixel
          coordinate system to the framebuffer coordinate system, taking into 
          account the logical/physical pixel scale factor, current FBO 
          position, and y-axis inversion.
        * framebuffer_transform maps from the current GL viewport on the
          framebuffer coordinate system to clip coordinates (-1 to 1). 
          
          
        Parameters
        ==========
        viewport : tuple or None
            The GL viewport rectangle (x, y, w, h). If None, then it
            is assumed to cover the entire canvas.
        fbo_size : tuple or None
            The size of the active FBO. If None, then it is assumed to have the
            same size as the canvas's framebuffer.
        fbo_rect : tuple or None
            The position and size (x, y, w, h) of the FBO in the coordinate
            system of the canvas's framebuffer. If None, then the bounds are
            assumed to cover the entire active framebuffer.
        canvas : Canvas instance
            Optionally set the canvas for this TransformSystem. See the 
            `canvas` property.
        """
        # TODO: check that d2f and f2r transforms still contain a single
        # STTransform (if the user has modified these, then auto-config should
        # either fail or replace the transforms)
        if canvas is not None:
            self.canvas = canvas
        canvas = self._canvas
        if canvas is None:
            raise RuntimeError("No canvas assigned to this TransformSystem.")
       
        # By default, this should invert the y axis--canvas origin is in top
        # left, whereas framebuffer origin is in bottom left.
        map_from = [(0, 0), canvas.size]
        map_to = [(0, canvas.physical_size[1]), (canvas.physical_size[0], 0)]
        self._update_if_maps_changed(self._canvas_transform.transforms[1],
                                     'ct1', np.array((map_from, map_to)))
        if fbo_rect is None:
            self._canvas_transform.transforms[0].scale = (1, 1, 1)
            self._canvas_transform.transforms[0].translate = (0, 0, 0)
        else:
            # Map into FBO coordinates
            map_from = [(fbo_rect[0], fbo_rect[1]),
                        (fbo_rect[0] + fbo_rect[2], fbo_rect[1] + fbo_rect[3])]
            map_to = [(0, 0), fbo_size]
            self._update_if_maps_changed(self._canvas_transform.transforms[0],
                                         'ct0', np.array((map_from, map_to)))
        if viewport is None:
            if fbo_size is None:
                # viewport covers entire canvas
                map_from = [(0, 0), canvas.physical_size]
            else:
                # viewport covers entire FBO
                map_from = [(0, 0), fbo_size]
        else:
            map_from = [viewport[:2], 
                        (viewport[0] + viewport[2], viewport[1] + viewport[3])]
        map_to = [(-1, -1), (1, 1)]
        self._update_if_maps_changed(self._framebuffer_transform.transforms[0],
                                     'ft0', np.array((map_from, map_to)))

    @property
    def canvas(self):
        """ The Canvas being drawn to.
        """
        return self._canvas
    
    @canvas.setter
    def canvas(self, canvas):
        self._canvas = canvas

    @property
    def dpi(self):
        """ Physical resolution of the document coordinate system (dots per
        inch).
        """
        if self._dpi is None:
            if self._canvas is None:
                return None
            else:
                return self.canvas.dpi
        else:
            return self._dpi

    @dpi.setter
    def dpi(self, dpi):
        assert dpi > 0
        self._dpi = dpi

    @property
    def visual_transform(self):
        """ Transform mapping from visual local coordinate frame to scene
        coordinate frame.
        """
        return self._visual_transform

    @visual_transform.setter
    def visual_transform(self, tr):
        self._visual_transform.transforms = tr

    @property
    def scene_transform(self):
        """ Transform mapping from scene coordinate frame to document
        coordinate frame.
        """
        return self._scene_transform

    @scene_transform.setter
    def scene_transform(self, tr):
        self._scene_transform.transforms = tr

    @property
    def document_transform(self):
        """ Transform mapping from document coordinate frame to the framebuffer
        (physical pixel) coordinate frame.
        """
        return self._document_transform

    @document_transform.setter
    def document_transform(self, tr):
        self._document_transform.transforms = tr

    @property
    def canvas_transform(self):
        """ Transform mapping from canvas coordinate frame to framebuffer
        coordinate frame.
        """
        return self._canvas_transform

    @canvas_transform.setter
    def canvas_transform(self, tr):
        self._canvas_transform.transforms = tr

    @property
    def framebuffer_transform(self):
        """ Transform mapping from pixel coordinate frame to rendering
        coordinate frame.
        """
        return self._framebuffer_transform

    @framebuffer_transform.setter
    def framebuffer_transform(self, tr):
        self._framebuffer_transform.transforms = tr

    def get_transform(self, map_from='visual', map_to='render'):
        """Return a transform mapping between any two coordinate systems.
        
        Parameters
        ----------
        map_from : str
            The starting coordinate system to map from. Must be one of: visual,
            scene, document, canvas, framebuffer, or render.
        map_to : str
            The ending coordinate system to map to. Must be one of: visual,
            scene, document, canvas, framebuffer, or render.
        """
        tr = ['visual', 'scene', 'document', 'canvas', 'framebuffer', 'render']
        ifrom = tr.index(map_from)
        ito = tr.index(map_to)
        
        if ifrom < ito:
            trs = [getattr(self, '_' + t + '_transform')
                   for t in tr[ifrom:ito]][::-1]
        else:
            trs = [getattr(self, '_' + t + '_transform').inverse
                   for t in tr[ito:ifrom]]
        return self._cache.get(trs)
    
    @property
    def pixel_scale(self):
        tr = self._canvas_transform
        return (tr.map((1, 0)) - tr.map((0, 0)))[0]