We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Bug: values of zero in 3D MIPs render as transparent, rather than black, even in opaque blending mode
## 🐛 Bug
noticed by @maweigert:

if a volume with a bunch of zeros is in front of another volume, we would have expected that it would essentially occluded any layers behind it, assuming, of course, that the blending mode is opaque.  (in additive blending, naturally, any nonzero values in other layers would result in a non-black MIP pixel).

However, MIP values of zero appear to render as transparent, even in opaque blending mode, showing layers behind it:

```python
import numpy as np 
import napari 

x = np.zeros((20,20,20), np.float32)
x[10,10,10] = 100

y = np.zeros((20,20,20), np.float32)
y[1:-1,1:-1,1:-1]  = 100

v = napari.Viewer()
v.add_image(y)
v.add_image(x)

v.dims.ndisplay=3
```

<img width="1311" alt="Screenshot 2022-09-01 at 09 19 26" src="https://user-images.githubusercontent.com/1609449/187933993-1454960c-f9fc-4ab4-b23e-a6603e093281.png">

changing `viewer.layers['x'].rendering = 'additive'` _does_ then occlude all of `y`
not 100% sure it's a bug, but it did strike us as unexpected.  @brisvag?


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `napari/_vispy/layers/image.py`
Content:
```
1 import warnings
2 
3 import numpy as np
4 from vispy.color import Colormap as VispyColormap
5 from vispy.scene.node import Node
6 
7 from ...utils.translations import trans
8 from ..utils.gl import fix_data_dtype, get_gl_extensions
9 from ..visuals.image import Image as ImageNode
10 from ..visuals.volume import Volume as VolumeNode
11 from .base import VispyBaseLayer
12 
13 
14 class ImageLayerNode:
15     def __init__(self, custom_node: Node = None, texture_format=None):
16         if (
17             texture_format == 'auto'
18             and 'texture_float' not in get_gl_extensions()
19         ):
20             # if the GPU doesn't support float textures, texture_format auto
21             # WILL fail on float dtypes
22             # https://github.com/napari/napari/issues/3988
23             texture_format = None
24 
25         self._custom_node = custom_node
26         self._image_node = ImageNode(
27             None,
28             method='auto',
29             texture_format=texture_format,
30         )
31         self._volume_node = VolumeNode(
32             np.zeros((1, 1, 1), dtype=np.float32),
33             clim=[0, 1],
34             texture_format=texture_format,
35         )
36 
37     def get_node(self, ndisplay: int) -> Node:
38 
39         # Return custom node if we have one.
40         if self._custom_node is not None:
41             return self._custom_node
42 
43         # Return Image or Volume node based on 2D or 3D.
44         if ndisplay == 2:
45             return self._image_node
46         return self._volume_node
47 
48 
49 class VispyImageLayer(VispyBaseLayer):
50     def __init__(self, layer, node=None, texture_format='auto'):
51 
52         # Use custom node from caller, or our standard image/volume nodes.
53         self._layer_node = ImageLayerNode(node, texture_format=texture_format)
54 
55         # Default to 2D (image) node.
56         super().__init__(layer, self._layer_node.get_node(2))
57 
58         self._array_like = True
59 
60         self.layer.events.rendering.connect(self._on_rendering_change)
61         self.layer.events.depiction.connect(self._on_depiction_change)
62         self.layer.events.interpolation2d.connect(
63             self._on_interpolation_change
64         )
65         self.layer.events.interpolation3d.connect(
66             self._on_interpolation_change
67         )
68         self.layer.events.colormap.connect(self._on_colormap_change)
69         self.layer.events.contrast_limits.connect(
70             self._on_contrast_limits_change
71         )
72         self.layer.events.gamma.connect(self._on_gamma_change)
73         self.layer.events.iso_threshold.connect(self._on_iso_threshold_change)
74         self.layer.events.attenuation.connect(self._on_attenuation_change)
75         self.layer.plane.events.position.connect(
76             self._on_plane_position_change
77         )
78         self.layer.plane.events.thickness.connect(
79             self._on_plane_thickness_change
80         )
81         self.layer.plane.events.normal.connect(self._on_plane_normal_change)
82 
83         # display_change is special (like data_change) because it requires a self.reset()
84         # this means that we have to call it manually. Also, it must be called before reset
85         # in order to set the appropriate node first
86         self._on_display_change()
87         self.reset()
88         self._on_data_change()
89 
90     def _on_display_change(self, data=None):
91         parent = self.node.parent
92         self.node.parent = None
93 
94         self.node = self._layer_node.get_node(self.layer._ndisplay)
95 
96         if data is None:
97             data = np.zeros((1,) * self.layer._ndisplay, dtype=np.float32)
98 
99         if self.layer._empty:
100             self.node.visible = False
101         else:
102             self.node.visible = self.layer.visible
103 
104         if self.layer.loaded:
105             self.node.set_data(data)
106 
107         self.node.parent = parent
108         self.node.order = self.order
109         self.reset()
110 
111     def _on_data_change(self):
112         if not self.layer.loaded:
113             # Do nothing if we are not yet loaded. Calling astype below could
114             # be very expensive. Lets not do it until our data has been loaded.
115             return
116 
117         self._set_node_data(self.node, self.layer._data_view)
118 
119     def _set_node_data(self, node, data):
120         """Our self.layer._data_view has been updated, update our node."""
121 
122         data = fix_data_dtype(data)
123 
124         if self.layer._ndisplay == 3 and self.layer.ndim == 2:
125             data = np.expand_dims(data, axis=0)
126 
127         # Check if data exceeds MAX_TEXTURE_SIZE and downsample
128         if self.MAX_TEXTURE_SIZE_2D is not None and self.layer._ndisplay == 2:
129             data = self.downsample_texture(data, self.MAX_TEXTURE_SIZE_2D)
130         elif (
131             self.MAX_TEXTURE_SIZE_3D is not None and self.layer._ndisplay == 3
132         ):
133             data = self.downsample_texture(data, self.MAX_TEXTURE_SIZE_3D)
134 
135         # Check if ndisplay has changed current node type needs updating
136         if (
137             self.layer._ndisplay == 3 and not isinstance(node, VolumeNode)
138         ) or (self.layer._ndisplay == 2 and not isinstance(node, ImageNode)):
139             self._on_display_change(data)
140         else:
141             node.set_data(data)
142 
143         if self.layer._empty:
144             node.visible = False
145         else:
146             node.visible = self.layer.visible
147 
148         # Call to update order of translation values with new dims:
149         self._on_matrix_change()
150         node.update()
151 
152     def _on_interpolation_change(self):
153         self.node.interpolation = (
154             self.layer.interpolation2d
155             if self.layer._ndisplay == 2
156             else self.layer.interpolation3d
157         )
158 
159     def _on_rendering_change(self):
160         if isinstance(self.node, VolumeNode):
161             self.node.method = self.layer.rendering
162             self._on_attenuation_change()
163             self._on_iso_threshold_change()
164 
165     def _on_depiction_change(self):
166         if isinstance(self.node, VolumeNode):
167             self.node.raycasting_mode = str(self.layer.depiction)
168 
169     def _on_colormap_change(self):
170         self.node.cmap = VispyColormap(*self.layer.colormap)
171 
172     def _on_contrast_limits_change(self):
173         self.node.clim = self.layer.contrast_limits
174         if isinstance(self.node, VolumeNode):
175             self.node.mip_cutoff = self.node._texture.clim_normalized[0]
176             self.node.minip_cutoff = self.node._texture.clim_normalized[1]
177 
178     def _on_gamma_change(self):
179         if len(self.node.shared_program.frag._set_items) > 0:
180             self.node.gamma = self.layer.gamma
181 
182     def _on_iso_threshold_change(self):
183         if isinstance(self.node, VolumeNode):
184             self.node.threshold = self.layer.iso_threshold
185 
186     def _on_attenuation_change(self):
187         if isinstance(self.node, VolumeNode):
188             self.node.attenuation = self.layer.attenuation
189 
190     def _on_plane_thickness_change(self):
191         if isinstance(self.node, VolumeNode):
192             self.node.plane_thickness = self.layer.plane.thickness
193 
194     def _on_plane_position_change(self):
195         if isinstance(self.node, VolumeNode):
196             self.node.plane_position = self.layer.plane.position
197 
198     def _on_plane_normal_change(self):
199         if isinstance(self.node, VolumeNode):
200             self.node.plane_normal = self.layer.plane.normal
201 
202     def reset(self, event=None):
203         super().reset()
204         self._on_interpolation_change()
205         self._on_colormap_change()
206         self._on_contrast_limits_change()
207         self._on_gamma_change()
208         self._on_rendering_change()
209         self._on_depiction_change()
210         self._on_plane_position_change()
211         self._on_plane_normal_change()
212         self._on_plane_thickness_change()
213 
214     def downsample_texture(self, data, MAX_TEXTURE_SIZE):
215         """Downsample data based on maximum allowed texture size.
216 
217         Parameters
218         ----------
219         data : array
220             Data to be downsampled if needed.
221         MAX_TEXTURE_SIZE : int
222             Maximum allowed texture size.
223 
224         Returns
225         -------
226         data : array
227             Data that now fits inside texture.
228         """
229         if np.any(np.greater(data.shape, MAX_TEXTURE_SIZE)):
230             if self.layer.multiscale:
231                 raise ValueError(
232                     trans._(
233                         "Shape of in dividual tiles in multiscale {shape} cannot exceed GL_MAX_TEXTURE_SIZE {texture_size}. Rendering is currently in {ndisplay}D mode.",
234                         deferred=True,
235                         shape=data.shape,
236                         texture_size=MAX_TEXTURE_SIZE,
237                         ndisplay=self.layer._ndisplay,
238                     )
239                 )
240             warnings.warn(
241                 trans._(
242                     "data shape {shape} exceeds GL_MAX_TEXTURE_SIZE {texture_size} in at least one axis and will be downsampled. Rendering is currently in {ndisplay}D mode.",
243                     deferred=True,
244                     shape=data.shape,
245                     texture_size=MAX_TEXTURE_SIZE,
246                     ndisplay=self.layer._ndisplay,
247                 )
248             )
249             downsample = np.ceil(
250                 np.divide(data.shape, MAX_TEXTURE_SIZE)
251             ).astype(int)
252             scale = np.ones(self.layer.ndim)
253             for i, d in enumerate(self.layer._dims_displayed):
254                 scale[d] = downsample[i]
255             self.layer._transforms['tile2data'].scale = scale
256             self._on_matrix_change()
257             slices = tuple(slice(None, None, ds) for ds in downsample)
258             data = data[slices]
259         return data
260
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/napari/_vispy/layers/image.py b/napari/_vispy/layers/image.py
--- a/napari/_vispy/layers/image.py
+++ b/napari/_vispy/layers/image.py
@@ -4,6 +4,7 @@
 from vispy.color import Colormap as VispyColormap
 from vispy.scene.node import Node
 
+from ...layers.base._base_constants import Blending
 from ...utils.translations import trans
 from ..utils.gl import fix_data_dtype, get_gl_extensions
 from ..visuals.image import Image as ImageNode
@@ -169,11 +170,29 @@
     def _on_colormap_change(self):
         self.node.cmap = VispyColormap(*self.layer.colormap)
 
+    def _update_mip_minip_cutoff(self):
+        # discard fragments beyond contrast limits, but only with translucent blending
+        if isinstance(self.node, VolumeNode):
+            if self.layer.blending in {
+                Blending.TRANSLUCENT,
+                Blending.TRANSLUCENT_NO_DEPTH,
+            }:
+                self.node.mip_cutoff = self.node._texture.clim_normalized[0]
+                self.node.minip_cutoff = self.node._texture.clim_normalized[1]
+            else:
+                self.node.mip_cutoff = None
+                self.node.minip_cutoff = None
+
     def _on_contrast_limits_change(self):
         self.node.clim = self.layer.contrast_limits
-        if isinstance(self.node, VolumeNode):
-            self.node.mip_cutoff = self.node._texture.clim_normalized[0]
-            self.node.minip_cutoff = self.node._texture.clim_normalized[1]
+        # cutoffs must be updated after clims, so we can set them to the new values
+        self._update_mip_minip_cutoff()
+
+    def _on_blending_change(self):
+        super()._on_blending_change()
+        # cutoffs must be updated after blending, so we can know if
+        # the new blending is a translucent one
+        self._update_mip_minip_cutoff()
 
     def _on_gamma_change(self):
         if len(self.node.shared_program.frag._set_items) > 0:

{"golden_diff": "diff --git a/napari/_vispy/layers/image.py b/napari/_vispy/layers/image.py\n--- a/napari/_vispy/layers/image.py\n+++ b/napari/_vispy/layers/image.py\n@@ -4,6 +4,7 @@\n from vispy.color import Colormap as VispyColormap\n from vispy.scene.node import Node\n \n+from ...layers.base._base_constants import Blending\n from ...utils.translations import trans\n from ..utils.gl import fix_data_dtype, get_gl_extensions\n from ..visuals.image import Image as ImageNode\n@@ -169,11 +170,29 @@\n     def _on_colormap_change(self):\n         self.node.cmap = VispyColormap(*self.layer.colormap)\n \n+    def _update_mip_minip_cutoff(self):\n+        # discard fragments beyond contrast limits, but only with translucent blending\n+        if isinstance(self.node, VolumeNode):\n+            if self.layer.blending in {\n+                Blending.TRANSLUCENT,\n+                Blending.TRANSLUCENT_NO_DEPTH,\n+            }:\n+                self.node.mip_cutoff = self.node._texture.clim_normalized[0]\n+                self.node.minip_cutoff = self.node._texture.clim_normalized[1]\n+            else:\n+                self.node.mip_cutoff = None\n+                self.node.minip_cutoff = None\n+\n     def _on_contrast_limits_change(self):\n         self.node.clim = self.layer.contrast_limits\n-        if isinstance(self.node, VolumeNode):\n-            self.node.mip_cutoff = self.node._texture.clim_normalized[0]\n-            self.node.minip_cutoff = self.node._texture.clim_normalized[1]\n+        # cutoffs must be updated after clims, so we can set them to the new values\n+        self._update_mip_minip_cutoff()\n+\n+    def _on_blending_change(self):\n+        super()._on_blending_change()\n+        # cutoffs must be updated after blending, so we can know if\n+        # the new blending is a translucent one\n+        self._update_mip_minip_cutoff()\n \n     def _on_gamma_change(self):\n         if len(self.node.shared_program.frag._set_items) > 0:\n", "issue": "Bug: values of zero in 3D MIPs render as transparent, rather than black, even in opaque blending mode\n## \ud83d\udc1b Bug\r\nnoticed by @maweigert:\r\n\r\nif a volume with a bunch of zeros is in front of another volume, we would have expected that it would essentially occluded any layers behind it, assuming, of course, that the blending mode is opaque.  (in additive blending, naturally, any nonzero values in other layers would result in a non-black MIP pixel).\r\n\r\nHowever, MIP values of zero appear to render as transparent, even in opaque blending mode, showing layers behind it:\r\n\r\n```python\r\nimport numpy as np \r\nimport napari \r\n\r\nx = np.zeros((20,20,20), np.float32)\r\nx[10,10,10] = 100\r\n\r\ny = np.zeros((20,20,20), np.float32)\r\ny[1:-1,1:-1,1:-1]  = 100\r\n\r\nv = napari.Viewer()\r\nv.add_image(y)\r\nv.add_image(x)\r\n\r\nv.dims.ndisplay=3\r\n```\r\n\r\n<img width=\"1311\" alt=\"Screenshot 2022-09-01 at 09 19 26\" src=\"https://user-images.githubusercontent.com/1609449/187933993-1454960c-f9fc-4ab4-b23e-a6603e093281.png\">\r\n\r\nchanging `viewer.layers['x'].rendering = 'additive'` _does_ then occlude all of `y`\r\nnot 100% sure it's a bug, but it did strike us as unexpected.  @brisvag?\r\n\n", "before_files": [{"content": "import warnings\n\nimport numpy as np\nfrom vispy.color import Colormap as VispyColormap\nfrom vispy.scene.node import Node\n\nfrom ...utils.translations import trans\nfrom ..utils.gl import fix_data_dtype, get_gl_extensions\nfrom ..visuals.image import Image as ImageNode\nfrom ..visuals.volume import Volume as VolumeNode\nfrom .base import VispyBaseLayer\n\n\nclass ImageLayerNode:\n    def __init__(self, custom_node: Node = None, texture_format=None):\n        if (\n            texture_format == 'auto'\n            and 'texture_float' not in get_gl_extensions()\n        ):\n            # if the GPU doesn't support float textures, texture_format auto\n            # WILL fail on float dtypes\n            # https://github.com/napari/napari/issues/3988\n            texture_format = None\n\n        self._custom_node = custom_node\n        self._image_node = ImageNode(\n            None,\n            method='auto',\n            texture_format=texture_format,\n        )\n        self._volume_node = VolumeNode(\n            np.zeros((1, 1, 1), dtype=np.float32),\n            clim=[0, 1],\n            texture_format=texture_format,\n        )\n\n    def get_node(self, ndisplay: int) -> Node:\n\n        # Return custom node if we have one.\n        if self._custom_node is not None:\n            return self._custom_node\n\n        # Return Image or Volume node based on 2D or 3D.\n        if ndisplay == 2:\n            return self._image_node\n        return self._volume_node\n\n\nclass VispyImageLayer(VispyBaseLayer):\n    def __init__(self, layer, node=None, texture_format='auto'):\n\n        # Use custom node from caller, or our standard image/volume nodes.\n        self._layer_node = ImageLayerNode(node, texture_format=texture_format)\n\n        # Default to 2D (image) node.\n        super().__init__(layer, self._layer_node.get_node(2))\n\n        self._array_like = True\n\n        self.layer.events.rendering.connect(self._on_rendering_change)\n        self.layer.events.depiction.connect(self._on_depiction_change)\n        self.layer.events.interpolation2d.connect(\n            self._on_interpolation_change\n        )\n        self.layer.events.interpolation3d.connect(\n            self._on_interpolation_change\n        )\n        self.layer.events.colormap.connect(self._on_colormap_change)\n        self.layer.events.contrast_limits.connect(\n            self._on_contrast_limits_change\n        )\n        self.layer.events.gamma.connect(self._on_gamma_change)\n        self.layer.events.iso_threshold.connect(self._on_iso_threshold_change)\n        self.layer.events.attenuation.connect(self._on_attenuation_change)\n        self.layer.plane.events.position.connect(\n            self._on_plane_position_change\n        )\n        self.layer.plane.events.thickness.connect(\n            self._on_plane_thickness_change\n        )\n        self.layer.plane.events.normal.connect(self._on_plane_normal_change)\n\n        # display_change is special (like data_change) because it requires a self.reset()\n        # this means that we have to call it manually. Also, it must be called before reset\n        # in order to set the appropriate node first\n        self._on_display_change()\n        self.reset()\n        self._on_data_change()\n\n    def _on_display_change(self, data=None):\n        parent = self.node.parent\n        self.node.parent = None\n\n        self.node = self._layer_node.get_node(self.layer._ndisplay)\n\n        if data is None:\n            data = np.zeros((1,) * self.layer._ndisplay, dtype=np.float32)\n\n        if self.layer._empty:\n            self.node.visible = False\n        else:\n            self.node.visible = self.layer.visible\n\n        if self.layer.loaded:\n            self.node.set_data(data)\n\n        self.node.parent = parent\n        self.node.order = self.order\n        self.reset()\n\n    def _on_data_change(self):\n        if not self.layer.loaded:\n            # Do nothing if we are not yet loaded. Calling astype below could\n            # be very expensive. Lets not do it until our data has been loaded.\n            return\n\n        self._set_node_data(self.node, self.layer._data_view)\n\n    def _set_node_data(self, node, data):\n        \"\"\"Our self.layer._data_view has been updated, update our node.\"\"\"\n\n        data = fix_data_dtype(data)\n\n        if self.layer._ndisplay == 3 and self.layer.ndim == 2:\n            data = np.expand_dims(data, axis=0)\n\n        # Check if data exceeds MAX_TEXTURE_SIZE and downsample\n        if self.MAX_TEXTURE_SIZE_2D is not None and self.layer._ndisplay == 2:\n            data = self.downsample_texture(data, self.MAX_TEXTURE_SIZE_2D)\n        elif (\n            self.MAX_TEXTURE_SIZE_3D is not None and self.layer._ndisplay == 3\n        ):\n            data = self.downsample_texture(data, self.MAX_TEXTURE_SIZE_3D)\n\n        # Check if ndisplay has changed current node type needs updating\n        if (\n            self.layer._ndisplay == 3 and not isinstance(node, VolumeNode)\n        ) or (self.layer._ndisplay == 2 and not isinstance(node, ImageNode)):\n            self._on_display_change(data)\n        else:\n            node.set_data(data)\n\n        if self.layer._empty:\n            node.visible = False\n        else:\n            node.visible = self.layer.visible\n\n        # Call to update order of translation values with new dims:\n        self._on_matrix_change()\n        node.update()\n\n    def _on_interpolation_change(self):\n        self.node.interpolation = (\n            self.layer.interpolation2d\n            if self.layer._ndisplay == 2\n            else self.layer.interpolation3d\n        )\n\n    def _on_rendering_change(self):\n        if isinstance(self.node, VolumeNode):\n            self.node.method = self.layer.rendering\n            self._on_attenuation_change()\n            self._on_iso_threshold_change()\n\n    def _on_depiction_change(self):\n        if isinstance(self.node, VolumeNode):\n            self.node.raycasting_mode = str(self.layer.depiction)\n\n    def _on_colormap_change(self):\n        self.node.cmap = VispyColormap(*self.layer.colormap)\n\n    def _on_contrast_limits_change(self):\n        self.node.clim = self.layer.contrast_limits\n        if isinstance(self.node, VolumeNode):\n            self.node.mip_cutoff = self.node._texture.clim_normalized[0]\n            self.node.minip_cutoff = self.node._texture.clim_normalized[1]\n\n    def _on_gamma_change(self):\n        if len(self.node.shared_program.frag._set_items) > 0:\n            self.node.gamma = self.layer.gamma\n\n    def _on_iso_threshold_change(self):\n        if isinstance(self.node, VolumeNode):\n            self.node.threshold = self.layer.iso_threshold\n\n    def _on_attenuation_change(self):\n        if isinstance(self.node, VolumeNode):\n            self.node.attenuation = self.layer.attenuation\n\n    def _on_plane_thickness_change(self):\n        if isinstance(self.node, VolumeNode):\n            self.node.plane_thickness = self.layer.plane.thickness\n\n    def _on_plane_position_change(self):\n        if isinstance(self.node, VolumeNode):\n            self.node.plane_position = self.layer.plane.position\n\n    def _on_plane_normal_change(self):\n        if isinstance(self.node, VolumeNode):\n            self.node.plane_normal = self.layer.plane.normal\n\n    def reset(self, event=None):\n        super().reset()\n        self._on_interpolation_change()\n        self._on_colormap_change()\n        self._on_contrast_limits_change()\n        self._on_gamma_change()\n        self._on_rendering_change()\n        self._on_depiction_change()\n        self._on_plane_position_change()\n        self._on_plane_normal_change()\n        self._on_plane_thickness_change()\n\n    def downsample_texture(self, data, MAX_TEXTURE_SIZE):\n        \"\"\"Downsample data based on maximum allowed texture size.\n\n        Parameters\n        ----------\n        data : array\n            Data to be downsampled if needed.\n        MAX_TEXTURE_SIZE : int\n            Maximum allowed texture size.\n\n        Returns\n        -------\n        data : array\n            Data that now fits inside texture.\n        \"\"\"\n        if np.any(np.greater(data.shape, MAX_TEXTURE_SIZE)):\n            if self.layer.multiscale:\n                raise ValueError(\n                    trans._(\n                        \"Shape of in dividual tiles in multiscale {shape} cannot exceed GL_MAX_TEXTURE_SIZE {texture_size}. Rendering is currently in {ndisplay}D mode.\",\n                        deferred=True,\n                        shape=data.shape,\n                        texture_size=MAX_TEXTURE_SIZE,\n                        ndisplay=self.layer._ndisplay,\n                    )\n                )\n            warnings.warn(\n                trans._(\n                    \"data shape {shape} exceeds GL_MAX_TEXTURE_SIZE {texture_size} in at least one axis and will be downsampled. Rendering is currently in {ndisplay}D mode.\",\n                    deferred=True,\n                    shape=data.shape,\n                    texture_size=MAX_TEXTURE_SIZE,\n                    ndisplay=self.layer._ndisplay,\n                )\n            )\n            downsample = np.ceil(\n                np.divide(data.shape, MAX_TEXTURE_SIZE)\n            ).astype(int)\n            scale = np.ones(self.layer.ndim)\n            for i, d in enumerate(self.layer._dims_displayed):\n                scale[d] = downsample[i]\n            self.layer._transforms['tile2data'].scale = scale\n            self._on_matrix_change()\n            slices = tuple(slice(None, None, ds) for ds in downsample)\n            data = data[slices]\n        return data\n", "path": "napari/_vispy/layers/image.py"}], "after_files": [{"content": "import warnings\n\nimport numpy as np\nfrom vispy.color import Colormap as VispyColormap\nfrom vispy.scene.node import Node\n\nfrom ...layers.base._base_constants import Blending\nfrom ...utils.translations import trans\nfrom ..utils.gl import fix_data_dtype, get_gl_extensions\nfrom ..visuals.image import Image as ImageNode\nfrom ..visuals.volume import Volume as VolumeNode\nfrom .base import VispyBaseLayer\n\n\nclass ImageLayerNode:\n    def __init__(self, custom_node: Node = None, texture_format=None):\n        if (\n            texture_format == 'auto'\n            and 'texture_float' not in get_gl_extensions()\n        ):\n            # if the GPU doesn't support float textures, texture_format auto\n            # WILL fail on float dtypes\n            # https://github.com/napari/napari/issues/3988\n            texture_format = None\n\n        self._custom_node = custom_node\n        self._image_node = ImageNode(\n            None,\n            method='auto',\n            texture_format=texture_format,\n        )\n        self._volume_node = VolumeNode(\n            np.zeros((1, 1, 1), dtype=np.float32),\n            clim=[0, 1],\n            texture_format=texture_format,\n        )\n\n    def get_node(self, ndisplay: int) -> Node:\n\n        # Return custom node if we have one.\n        if self._custom_node is not None:\n            return self._custom_node\n\n        # Return Image or Volume node based on 2D or 3D.\n        if ndisplay == 2:\n            return self._image_node\n        return self._volume_node\n\n\nclass VispyImageLayer(VispyBaseLayer):\n    def __init__(self, layer, node=None, texture_format='auto'):\n\n        # Use custom node from caller, or our standard image/volume nodes.\n        self._layer_node = ImageLayerNode(node, texture_format=texture_format)\n\n        # Default to 2D (image) node.\n        super().__init__(layer, self._layer_node.get_node(2))\n\n        self._array_like = True\n\n        self.layer.events.rendering.connect(self._on_rendering_change)\n        self.layer.events.depiction.connect(self._on_depiction_change)\n        self.layer.events.interpolation2d.connect(\n            self._on_interpolation_change\n        )\n        self.layer.events.interpolation3d.connect(\n            self._on_interpolation_change\n        )\n        self.layer.events.colormap.connect(self._on_colormap_change)\n        self.layer.events.contrast_limits.connect(\n            self._on_contrast_limits_change\n        )\n        self.layer.events.gamma.connect(self._on_gamma_change)\n        self.layer.events.iso_threshold.connect(self._on_iso_threshold_change)\n        self.layer.events.attenuation.connect(self._on_attenuation_change)\n        self.layer.plane.events.position.connect(\n            self._on_plane_position_change\n        )\n        self.layer.plane.events.thickness.connect(\n            self._on_plane_thickness_change\n        )\n        self.layer.plane.events.normal.connect(self._on_plane_normal_change)\n\n        # display_change is special (like data_change) because it requires a self.reset()\n        # this means that we have to call it manually. Also, it must be called before reset\n        # in order to set the appropriate node first\n        self._on_display_change()\n        self.reset()\n        self._on_data_change()\n\n    def _on_display_change(self, data=None):\n        parent = self.node.parent\n        self.node.parent = None\n\n        self.node = self._layer_node.get_node(self.layer._ndisplay)\n\n        if data is None:\n            data = np.zeros((1,) * self.layer._ndisplay, dtype=np.float32)\n\n        if self.layer._empty:\n            self.node.visible = False\n        else:\n            self.node.visible = self.layer.visible\n\n        if self.layer.loaded:\n            self.node.set_data(data)\n\n        self.node.parent = parent\n        self.node.order = self.order\n        self.reset()\n\n    def _on_data_change(self):\n        if not self.layer.loaded:\n            # Do nothing if we are not yet loaded. Calling astype below could\n            # be very expensive. Lets not do it until our data has been loaded.\n            return\n\n        self._set_node_data(self.node, self.layer._data_view)\n\n    def _set_node_data(self, node, data):\n        \"\"\"Our self.layer._data_view has been updated, update our node.\"\"\"\n\n        data = fix_data_dtype(data)\n\n        if self.layer._ndisplay == 3 and self.layer.ndim == 2:\n            data = np.expand_dims(data, axis=0)\n\n        # Check if data exceeds MAX_TEXTURE_SIZE and downsample\n        if self.MAX_TEXTURE_SIZE_2D is not None and self.layer._ndisplay == 2:\n            data = self.downsample_texture(data, self.MAX_TEXTURE_SIZE_2D)\n        elif (\n            self.MAX_TEXTURE_SIZE_3D is not None and self.layer._ndisplay == 3\n        ):\n            data = self.downsample_texture(data, self.MAX_TEXTURE_SIZE_3D)\n\n        # Check if ndisplay has changed current node type needs updating\n        if (\n            self.layer._ndisplay == 3 and not isinstance(node, VolumeNode)\n        ) or (self.layer._ndisplay == 2 and not isinstance(node, ImageNode)):\n            self._on_display_change(data)\n        else:\n            node.set_data(data)\n\n        if self.layer._empty:\n            node.visible = False\n        else:\n            node.visible = self.layer.visible\n\n        # Call to update order of translation values with new dims:\n        self._on_matrix_change()\n        node.update()\n\n    def _on_interpolation_change(self):\n        self.node.interpolation = (\n            self.layer.interpolation2d\n            if self.layer._ndisplay == 2\n            else self.layer.interpolation3d\n        )\n\n    def _on_rendering_change(self):\n        if isinstance(self.node, VolumeNode):\n            self.node.method = self.layer.rendering\n            self._on_attenuation_change()\n            self._on_iso_threshold_change()\n\n    def _on_depiction_change(self):\n        if isinstance(self.node, VolumeNode):\n            self.node.raycasting_mode = str(self.layer.depiction)\n\n    def _on_colormap_change(self):\n        self.node.cmap = VispyColormap(*self.layer.colormap)\n\n    def _update_mip_minip_cutoff(self):\n        # discard fragments beyond contrast limits, but only with translucent blending\n        if isinstance(self.node, VolumeNode):\n            if self.layer.blending in {\n                Blending.TRANSLUCENT,\n                Blending.TRANSLUCENT_NO_DEPTH,\n            }:\n                self.node.mip_cutoff = self.node._texture.clim_normalized[0]\n                self.node.minip_cutoff = self.node._texture.clim_normalized[1]\n            else:\n                self.node.mip_cutoff = None\n                self.node.minip_cutoff = None\n\n    def _on_contrast_limits_change(self):\n        self.node.clim = self.layer.contrast_limits\n        # cutoffs must be updated after clims, so we can set them to the new values\n        self._update_mip_minip_cutoff()\n\n    def _on_blending_change(self):\n        super()._on_blending_change()\n        # cutoffs must be updated after blending, so we can know if\n        # the new blending is a translucent one\n        self._update_mip_minip_cutoff()\n\n    def _on_gamma_change(self):\n        if len(self.node.shared_program.frag._set_items) > 0:\n            self.node.gamma = self.layer.gamma\n\n    def _on_iso_threshold_change(self):\n        if isinstance(self.node, VolumeNode):\n            self.node.threshold = self.layer.iso_threshold\n\n    def _on_attenuation_change(self):\n        if isinstance(self.node, VolumeNode):\n            self.node.attenuation = self.layer.attenuation\n\n    def _on_plane_thickness_change(self):\n        if isinstance(self.node, VolumeNode):\n            self.node.plane_thickness = self.layer.plane.thickness\n\n    def _on_plane_position_change(self):\n        if isinstance(self.node, VolumeNode):\n            self.node.plane_position = self.layer.plane.position\n\n    def _on_plane_normal_change(self):\n        if isinstance(self.node, VolumeNode):\n            self.node.plane_normal = self.layer.plane.normal\n\n    def reset(self, event=None):\n        super().reset()\n        self._on_interpolation_change()\n        self._on_colormap_change()\n        self._on_contrast_limits_change()\n        self._on_gamma_change()\n        self._on_rendering_change()\n        self._on_depiction_change()\n        self._on_plane_position_change()\n        self._on_plane_normal_change()\n        self._on_plane_thickness_change()\n\n    def downsample_texture(self, data, MAX_TEXTURE_SIZE):\n        \"\"\"Downsample data based on maximum allowed texture size.\n\n        Parameters\n        ----------\n        data : array\n            Data to be downsampled if needed.\n        MAX_TEXTURE_SIZE : int\n            Maximum allowed texture size.\n\n        Returns\n        -------\n        data : array\n            Data that now fits inside texture.\n        \"\"\"\n        if np.any(np.greater(data.shape, MAX_TEXTURE_SIZE)):\n            if self.layer.multiscale:\n                raise ValueError(\n                    trans._(\n                        \"Shape of in dividual tiles in multiscale {shape} cannot exceed GL_MAX_TEXTURE_SIZE {texture_size}. Rendering is currently in {ndisplay}D mode.\",\n                        deferred=True,\n                        shape=data.shape,\n                        texture_size=MAX_TEXTURE_SIZE,\n                        ndisplay=self.layer._ndisplay,\n                    )\n                )\n            warnings.warn(\n                trans._(\n                    \"data shape {shape} exceeds GL_MAX_TEXTURE_SIZE {texture_size} in at least one axis and will be downsampled. Rendering is currently in {ndisplay}D mode.\",\n                    deferred=True,\n                    shape=data.shape,\n                    texture_size=MAX_TEXTURE_SIZE,\n                    ndisplay=self.layer._ndisplay,\n                )\n            )\n            downsample = np.ceil(\n                np.divide(data.shape, MAX_TEXTURE_SIZE)\n            ).astype(int)\n            scale = np.ones(self.layer.ndim)\n            for i, d in enumerate(self.layer._dims_displayed):\n                scale[d] = downsample[i]\n            self.layer._transforms['tile2data'].scale = scale\n            self._on_matrix_change()\n            slices = tuple(slice(None, None, ds) for ds in downsample)\n            data = data[slices]\n        return data\n", "path": "napari/_vispy/layers/image.py"}]}