Repository: vispy/vispy

Issue:
Incorrect behavior with multipe clipping planes 
I was checking up on that nice trick where the clipping planes logic is done in the vertex shader and then interpolated to the fragment shader, with the intention of applying it in pygfx too. However, I found that this trick does not work in the case of multiple clipping planes.

This can be shown with the following example:
```py
import numpy as np
from vispy import app, scene, io
from vispy.visuals.filters.clipping_planes import PlanesClipper

canvas = scene.SceneCanvas(keys='interactive', size=(800, 600), show=True)
view = canvas.central_widget.add_view()

cube = scene.visuals.Box(100, 100, 100, color=(1, 0, 0, 1), parent=view.scene)

view.camera = scene.cameras.TurntableCamera(parent=view.scene, fov=60)

clip_center = (0, 20, 60)
clipping_planes = np.concatenate(
    [ np.array([[clip_center, [1, 0, 0]]]), np.array([[clip_center, [0, 1, 0]]])]
)

clipper = PlanesClipper()
clipper.clipping_planes = clipping_planes
cube.attach(clipper)

if __name__ == '__main__':
    app.run()
```

If you turn the camera to look from above, you'll see this:

![image](https://user-images.githubusercontent.com/3015475/135463433-c3a23bf3-842b-4982-ae78-7f0a3634300b.png)


I think this can be explained with the following figure:

![image](https://user-images.githubusercontent.com/3015475/135464082-98220a3e-7177-4a7e-9b18-cad18c8b67d1.png)

The black lines indicate two clipping planes (the shaded side is where they clip). The two blue dots represent two vertices with a line or polygon interpolating between them. Both dots are of equal distance two a plane, one on the + side and one on the - side. Now if the `min_plane_distance` (or whatever we ended up calling it :D ) is interpolated, it will have its zero point (the point where it starts clipping) in the middle.

cc @brisvag 


Discussion:
Ouch, I only tested meshes and lines with a single clipping plane :/

Ok, I think I get the problem. I don't see a way around in the vertex shader. so you have a working implementation in pygfx, it seems? We should port it over here :)
> so you have a working implementation in pygfx, it seems? We should port it over here :)

Yeah. It essentially does the same thing, but in the fragment shader. The source is [here](https://github.com/pygfx/pygfx/blob/93b31f28a0c81c74913128670afa90f4b4f28dc2/pygfx/renderers/wgpu/_shadercomposer.py#L151-L166) but the shader composition in pygfx and vispy is quite different, so not sure if you can reuse much of it :)

The gist is that a function `apply_clipping_planes()` is provided, that each object's (i.e. visual) fragment shader calls. I suppose vispu would use a filter though? Further, in the vertex shader we set the world_pos (i.e. scene pos) as a varying so we don't need to apply a transform. 

Resolving patch:
diff --git a/vispy/visuals/filters/clipping_planes.py b/vispy/visuals/filters/clipping_planes.py
--- a/vispy/visuals/filters/clipping_planes.py
+++ b/vispy/visuals/filters/clipping_planes.py
@@ -24,14 +24,15 @@ class PlanesClipper(Filter):
 
     VERT_CODE = """
     void clip() {
-        // Transform back to visual coordinates and clip based on that
-        $v_distance_from_clip = $clip_with_planes($itransform(gl_Position).xyz);
+        // pass position as varying for interpolation
+        $v_position = gl_Position;
     }
     """
 
     FRAG_CODE = """
     void clip() {
-        if ($v_distance_from_clip < 0.)
+        float distance_from_clip = $clip_with_planes($itransform($v_position).xyz);
+        if (distance_from_clip < 0.)
             discard;
     }
     """
@@ -47,9 +48,9 @@ def __init__(self, clipping_planes=None, coord_system='scene'):
             fcode=Function(self.FRAG_CODE), fhook='pre', fpos=1,
         )
 
-        v_distance_from_clip = Varying('v_distance_from_clip', 'float')
-        self.vshader['v_distance_from_clip'] = v_distance_from_clip
-        self.fshader['v_distance_from_clip'] = v_distance_from_clip
+        v_position = Varying('v_position', 'vec4')
+        self.vshader['v_position'] = v_position
+        self.fshader['v_position'] = v_position
 
         self.clipping_planes = clipping_planes
 
@@ -63,7 +64,7 @@ def coord_system(self):
 
     def _attach(self, visual):
         super()._attach(visual)
-        self.vshader['itransform'] = visual.get_transform('render', self._coord_system)
+        self.fshader['itransform'] = visual.get_transform('render', self._coord_system)
 
     @staticmethod
     @lru_cache(maxsize=10)
@@ -102,7 +103,7 @@ def clipping_planes(self, value):
         self._clipping_planes = value
 
         clip_func = self._build_clipping_planes_func(len(value))
-        self.vshader['clip_with_planes'] = clip_func
+        self.fshader['clip_with_planes'] = clip_func
 
         for idx, plane in enumerate(value):
             clip_func[f'clipping_plane_pos{idx}'] = tuple(plane[0])


Test patch: