Repository: vispy/vispy

Issue:
Scaling not working correctly for markers
When I set `scaling=True` when setting the data in `MarkersVisual#set_data()`, only rotating around the scene (no zooming) makes the points' sizes to vary. I hope this is not the expected behaviour.

Here is a snippet which demonstrates this. Run it and rotate the camera around.

``` python
import sys
import numpy as np
from vispy import scene
from vispy.scene import visuals

# create data cloud
pos = np.random.normal(size=(1000, 3), scale=0.2)


#
# Make a canvas and add simple view
#
canvas = scene.SceneCanvas(keys='interactive', show=True, bgcolor='w')

view = canvas.central_widget.add_view()
view.camera = 'turntable'
view.camera.fov = 60

scatter = visuals.Markers(parent=view.scene)
scatter.antialias = 0
scatter.set_data(pos=pos, edge_color=(0.0, 0.0, 0.0, 1.0), face_color=(0.6, 0.5, 0.4, 1.0), size=.03, scaling=True)

scatter.set_gl_state(depth_test=True, blend=True, blend_func=('src_alpha', 'one_minus_src_alpha'))

# Add axes
axis = visuals.XYZAxis(parent=view.scene)

if __name__ == '__main__' and sys.flags.interactive == 0:
    canvas.app.run()
```



Discussion:
Over three years later and I just encountered the same bug. Would be awesome if this could be fixed :-)
The starting point is right here: https://github.com/vispy/vispy/blob/master/vispy/visuals/markers.py#L621-L624

I'm not sure what has to change for this but seeing as the calculation only uses `x` and `y` coordinates I'm sure that's not helping. I don't have the time to look in to this, but pull requests are always welcome. @mbrieske since you are the latest person to be effected by this, maybe you could try some things out?
@djhoese thanks for the hint, I'll see what I can do!
Okay, so I found that if I replace that line you directed me to by:

`scale = 1000 / np.linalg.norm(tr.map([0, 0, 0])[:2])`

it works for me. I have no idea if this solves the issue or if this is just a working solution for me though.

To verify it for the example above you also need to change the size of the markers to something that makes more sense, i.e. "3" or so.

Cheers, Malte
Glad that works, but does it still adjust them properly when you zoom in and out? I think you've removed the part of this calculation that is supposed to see how "wide" our current display is and scale the markers accordingly. The calculation as it is right now is taking the difference between the edges/corners of the canvas.
You're right, resizing doesn't work anymore. It scales actually in the wrong direction, when I make the window smaller, the circles become bigger.
That sucks. Any idea?
So right now the calculation is using two corners of the canvas to come up with a "distance". The problem is that for a 3D Scene the x/y coordinates corresponding to the corners of the canvas change as you rotate the Scene so this calculation doesn't make sense.

I wonder if a better calculation could be done with the center of the canvas. Like if the center of the canvas doesn't change then no need to update the scale factor. Wait, that won't work because if you zoomed in to the exact center of the canvas you'd still want the size to scale.

I'm not sure.
I'm pretty sure that this can be fixed by using the norm of the whole transformation matrix instead of the norm of one or two of the projections. I was able to get the above example working with the following code snip-it modifying the lines @djhoese pointed to above:

```python
def _prepare_draw(self, view):
    if self._symbol is None:
        return False
    view.view_program['u_px_scale'] = view.transforms.pixel_scale
    if self.scaling:
        tr = view.transforms.get_transform('visual', 'document').simplified
        mat = (tr.map(np.eye(3)) - tr.map(np.zeros((3, 3))))[:, :2]
        scale = np.linalg.norm(mat)
        view.view_program['u_scale'] = scale
    else:
        view.view_program['u_scale'] = 1
```

If you compare the before and after gifs, you'll see the size changes on rotation are now gone, as the norm of the matrix is invariant to rotations of the underlying 3D coordinate system

Before:
![before_fix](https://user-images.githubusercontent.com/6531703/64068613-55494c00-cbef-11e9-90fb-acd8f77728d5.gif)

After:
![after_fix](https://user-images.githubusercontent.com/6531703/64068616-5b3f2d00-cbef-11e9-9015-409a5c6db582.gif)

I'm happy to make a PR to add this change, though it may cause absolute changes in the marker size from before the PR that could throw some people off. I also havn't tested this in the 2D case (where you might want to use `np.eye(2)` and `np.zeros((2, 2))`, but where it should would in the same way in principle).

Please let me know what the best way to proceed is.

Awesome work @sofroniewn! I think a PR would be great. You could try testing a "2D" environment by switching to a 'panzoom' camera and see how that works. Theoretically this should work with `(3, 3)` in a 2D scene since typically a 2D scene is just one where the `z` dimension is ignored.

Could you explain why you `simplified` the transform instead of `get_transform()`? I'm unfamiliar with what `simplified` does. I'm also unfamiliar with all the different types of norms that can be done with `np.linalg.norm`, should we maybe be using one of the other formats/orders `norm` accepts (one that uses X/Y/Z instead of cutting off the Z with `[:2]`)?
Thanks @djhoese.

The code currently uses `simplified` so I just stuck with it, I'm not familiar with what it does either or if it should be dropped. Dropping it causes no noticeable change in performance in my simple example, so I'm happy to drop it.

The `[:, :2]` isn't really cutting off the `z` dimension of the axis with the markers - that would be achieved by `[:2, :]` which is more similar to code there now. I'm not quite sure how to interpret the last two dimensions of the 4-vectors outputted by the `.map` method, but I think they relate more to the coordinate system of the screen which is why I was just keeping the first two, `x` and `y`. Including all of 3 or 4 of them doesn't really change anything as the vectors corresponding to them have norms very close to 1.
i.e. 
```python
mat = (tr.map(np.eye(3)) - tr.map(np.zeros((3, 3))))
scale = np.linalg.norm(mat)
```
gives me basically identical performance (where mat used to be `3 x 2` but is now `3 x 4`) and will still be rotation invariant, so I'm happy to keep them in there.

Everything still works fine switching the camera to `panzoom` and making the points 2d or 3d.

I'm going to make a PR with the following change

```python
def _prepare_draw(self, view):
    if self._symbol is None:
        return False
    view.view_program['u_px_scale'] = view.transforms.pixel_scale
    if self.scaling:
        tr = view.transforms.get_transform('visual', 'document')
        mat = tr.map(np.eye(3)) - tr.map(np.zeros((3, 3)))
        scale = np.linalg.norm(mat)
        view.view_program['u_scale'] = scale
    else:
        view.view_program['u_scale'] = 1
```

I'll get to work on that soon and we can continue the discussion on the PR


Resolving patch:
diff --git a/vispy/visuals/markers.py b/vispy/visuals/markers.py
--- a/vispy/visuals/markers.py
+++ b/vispy/visuals/markers.py
@@ -620,7 +620,8 @@ def _prepare_draw(self, view):
         view.view_program['u_px_scale'] = view.transforms.pixel_scale
         if self.scaling:
             tr = view.transforms.get_transform('visual', 'document').simplified
-            scale = np.linalg.norm((tr.map([1, 0]) - tr.map([0, 0]))[:2])
+            mat = tr.map(np.eye(3)) - tr.map(np.zeros((3, 3)))
+            scale = np.linalg.norm(mat[:, :3])
             view.view_program['u_scale'] = scale
         else:
             view.view_program['u_scale'] = 1


Test patch: