Repository: vispy/vispy

Issue:
Shading mode switching for MeshVisual doesn't work if initialised with shading=None
If a `MeshVisual` is initialised with `shading=None` then switched to `shading='flat'` or `shading='smooth'` there are some shader compilation errors 

<details>
<summary>example adapted from tube</summary>
<br>

```python
import sys
from vispy import scene
from vispy.geometry.torusknot import TorusKnot

from colorsys import hsv_to_rgb
import numpy as np

canvas = scene.SceneCanvas(keys='interactive')
view = canvas.central_widget.add_view()

points1 = TorusKnot(5, 3).first_component[:-1]
points1[:, 0] -= 20.
points1[:, 2] -= 15.


colors = np.linspace(0, 1, len(points1))
colors = np.array([hsv_to_rgb(c, 1, 1) for c in colors])

vertex_colors = np.random.random(8 * len(points1))
vertex_colors = np.array([hsv_to_rgb(c, 1, 1) for c in vertex_colors])

l1 = scene.visuals.Tube(points1,
                        shading=None,
                        color=colors,  # this is overridden by
                                       # the vertex_colors argument
                        vertex_colors=vertex_colors,
                        tube_points=8)



view.add(l1)
view.camera = scene.TurntableCamera()
# tube does not expose its limits yet
view.camera.set_range((-20, 20), (-20, 20), (-20, 20))

canvas.show()

if __name__ == '__main__':
    if sys.flags.interactive != 1:
        canvas.app.run()
    l1.shading = 'flat'
```
</details>

<details>
<summary>Error...</summary>
<br>

```python
WARNING: Unsubstituted placeholders in code: ['$light_dir']
  replacements made: ['normal', 'render2scene', 'visual2scene', 'scene2doc', 'doc2scene']
WARNING: Unsubstituted placeholders in code: ['$flat_shading', '$light_color', '$diffuse_color', '$shininess', '$shininess', '$light_color', '$specular_color', '$shading_enabled', '$ambient_color']
  replacements made: []
WARNING: Error drawing visual <Tube at 0x14e9d4df0>
WARNING: Traceback (most recent call last):
  File "/Applications/PyCharm.app/Contents/plugins/python/helpers/pydev/pydevconsole.py", line 483, in <module>
    pydevconsole.start_client(host, port)
  File "/Applications/PyCharm.app/Contents/plugins/python/helpers/pydev/pydevconsole.py", line 411, in start_client
    process_exec_queue(interpreter)
  File "/Applications/PyCharm.app/Contents/plugins/python/helpers/pydev/pydevconsole.py", line 244, in process_exec_queue
    inputhook()
  File "/Applications/PyCharm.app/Contents/plugins/python/helpers/pydev/pydev_ipython/inputhookqt5.py", line 112, in inputhook_qt5
    app.processEvents(QtCore.QEventLoop.AllEvents, 300)
  File "/Users/aburt/Programming/vispy/vispy/app/backends/_qt.py", line 526, in event
    out = super(QtBaseCanvasBackend, self).event(ev)
  File "/Users/aburt/Programming/vispy/vispy/app/backends/_qt.py", line 846, in paintGL
    self._vispy_canvas.events.draw(region=None)
  File "/Users/aburt/Programming/vispy/vispy/util/event.py", line 452, in __call__
    self._invoke_callback(cb, event)
  File "/Users/aburt/Programming/vispy/vispy/util/event.py", line 470, in _invoke_callback
    _handle_exception(self.ignore_callback_errors,
  << caught exception here: >>
  File "/Users/aburt/Programming/vispy/vispy/util/event.py", line 468, in _invoke_callback
    cb(event)
  File "/Users/aburt/Programming/vispy/vispy/scene/canvas.py", line 218, in on_draw
    self._draw_scene()
  File "/Users/aburt/Programming/vispy/vispy/scene/canvas.py", line 267, in _draw_scene
    self.draw_visual(self.scene)
  File "/Users/aburt/Programming/vispy/vispy/scene/canvas.py", line 305, in draw_visual
    node.draw()
  File "/Users/aburt/Programming/vispy/vispy/scene/visuals.py", line 99, in draw
    self._visual_superclass.draw(self)
  File "/Users/aburt/Programming/vispy/vispy/visuals/mesh.py", line 346, in draw
    Visual.draw(self, *args, **kwds)
  File "/Users/aburt/Programming/vispy/vispy/visuals/visual.py", line 446, in draw
    self._program.draw(self._vshare.draw_mode,
  File "/Users/aburt/Programming/vispy/vispy/visuals/shaders/program.py", line 102, in draw
    Program.draw(self, *args, **kwargs)
  File "/Users/aburt/Programming/vispy/vispy/gloo/program.py", line 498, in draw
    raise RuntimeError('All attributes must have the same size, got:\n'
RuntimeError: All attributes must have the same size, got:
<VertexBuffer size=4704 last_dim=4>: 4704
<VertexBuffer size=0 last_dim=3>: 0
<VertexBuffer size=4704 last_dim=3>: 4704
ERROR: Invoking <bound method SceneCanvas.on_draw of <SceneCanvas (PyQt5) at 0x135877fd0>> for DrawEvent
WARNING: Traceback (most recent call last):
  File "/Applications/PyCharm.app/Contents/plugins/python/helpers/pydev/pydevconsole.py", line 483, in <module>
    pydevconsole.start_client(host, port)
  File "/Applications/PyCharm.app/Contents/plugins/python/helpers/pydev/pydevconsole.py", line 411, in start_client
    process_exec_queue(interpreter)
  File "/Applications/PyCharm.app/Contents/plugins/python/helpers/pydev/pydevconsole.py", line 244, in process_exec_queue
    inputhook()
  File "/Applications/PyCharm.app/Contents/plugins/python/helpers/pydev/pydev_ipython/inputhookqt5.py", line 112, in inputhook_qt5
    app.processEvents(QtCore.QEventLoop.AllEvents, 300)
  File "/Users/aburt/Programming/vispy/vispy/app/backends/_qt.py", line 526, in event
    out = super(QtBaseCanvasBackend, self).event(ev)
  File "/Users/aburt/Programming/vispy/vispy/app/backends/_qt.py", line 846, in paintGL
    self._vispy_canvas.events.draw(region=None)
  File "/Users/aburt/Programming/vispy/vispy/util/event.py", line 452, in __call__
    self._invoke_callback(cb, event)
  File "/Users/aburt/Programming/vispy/vispy/util/event.py", line 470, in _invoke_callback
    _handle_exception(self.ignore_callback_errors,
  << caught exception here: >>
  File "/Users/aburt/Programming/vispy/vispy/util/event.py", line 468, in _invoke_callback
    cb(event)
  File "/Users/aburt/Programming/vispy/vispy/gloo/context.py", line 172, in flush_commands
    self.glir.flush(self.shared.parser)
  File "/Users/aburt/Programming/vispy/vispy/gloo/glir.py", line 579, in flush
    self._shared.flush(parser)
  File "/Users/aburt/Programming/vispy/vispy/gloo/glir.py", line 501, in flush
    parser.parse(self._filter(self.clear(), parser))
  File "/Users/aburt/Programming/vispy/vispy/gloo/glir.py", line 819, in parse
    self._parse(command)
  File "/Users/aburt/Programming/vispy/vispy/gloo/glir.py", line 789, in _parse
    ob.set_data(*args)
  File "/Users/aburt/Programming/vispy/vispy/gloo/glir.py", line 926, in set_data
    raise RuntimeError("Shader compilation error in %s:\n%s" %
RuntimeError: Shader compilation error in GL_VERTEX_SHADER:
    on line 330: '<' : syntax error: syntax error
      vec3 light = normalize($light_dir.xyz);
ERROR: Invoking <bound method GLContext.flush_commands of <GLContext at 0x14d1e6130>> for DrawEvent
WARNING: Error drawing visual <Tube at 0x14e9d4df0>
ERROR: Invoking <bound method SceneCanvas.on_draw of <SceneCanvas (PyQt5) at 0x135877fd0>> repeat 2
```
</details>

This error does not happen if the `MeshVisual` is initialised with `shading='flat'`, you can then switch freely between all three shading modes

I guess this is mostly related to #1463 (@asnt) since this switching wasn't possible before with the separate shaders for shaded and unshaded meshes




Discussion:

Resolving patch:
diff --git a/vispy/visuals/filters/mesh.py b/vispy/visuals/filters/mesh.py
--- a/vispy/visuals/filters/mesh.py
+++ b/vispy/visuals/filters/mesh.py
@@ -332,6 +332,9 @@ def _attach(self, visual):
         self.vshader['scene2doc'] = scene2doc
         self.vshader['doc2scene'] = doc2scene
 
+        if self._visual.mesh_data is not None:
+            self._update_data()
+
         visual.events.data_updated.connect(self.on_mesh_data_updated)
 
     def _detach(self, visual):
diff --git a/vispy/visuals/mesh.py b/vispy/visuals/mesh.py
--- a/vispy/visuals/mesh.py
+++ b/vispy/visuals/mesh.py
@@ -117,6 +117,8 @@ def __init__(self, vertices=None, faces=None, vertex_colors=None,
 
         self.events.add(data_updated=Event)
 
+        self._meshdata = None
+
         # Define buffers
         self._vertices = VertexBuffer(np.zeros((0, 3), dtype=np.float32))
         self._cmap = get_colormap('cubehelix')
@@ -158,9 +160,9 @@ def shading(self, shading):
         if self.shading_filter is None:
             from vispy.visuals.filters import ShadingFilter
             self.shading_filter = ShadingFilter(shading=shading)
+            self.attach(self.shading_filter)
         else:
             self.shading_filter.shading = shading
-        self.attach(self.shading_filter)
 
     def set_data(self, vertices=None, faces=None, vertex_colors=None,
                  face_colors=None, color=None, vertex_values=None,


Test patch:
diff --git a/vispy/visuals/tests/test_mesh.py b/vispy/visuals/tests/test_mesh.py
--- a/vispy/visuals/tests/test_mesh.py
+++ b/vispy/visuals/tests/test_mesh.py
@@ -29,6 +29,25 @@ def test_mesh_color():
     np.testing.assert_allclose(vertices['position'], new_vertices)
 
 
+@requires_pyopengl()
+@requires_application()
+@pytest.mark.parametrize('shading', [None, 'flat', 'smooth'])
+def test_mesh_shading_change_from_none(shading):
+    # Regression test for #2041: exception raised when changing the shading
+    # mode with shading=None initially.
+    size = (45, 40)
+    with TestingCanvas(size=size) as c:
+        v = c.central_widget.add_view(border_width=0)
+        vertices = np.array([(0, 0, 0), (0, 0, 1), (1, 0, 0)], dtype=float)
+        faces = np.array([(0, 1, 2)])
+        mesh = scene.visuals.Mesh(vertices=vertices, faces=faces, shading=None)
+        v.add(mesh)
+        c.render()
+        # This below should not fail.
+        mesh.shading = shading
+        c.render()
+
+
 @requires_pyopengl()
 @requires_application()
 @pytest.mark.parametrize('shading', [None, 'flat', 'smooth'])