Markers with Instanced Rendering#

Compare instanced and GL_POINTS rendering methods for Markers visual.

This example shows how instanced rendering works around platform point size limits (many platforms limit GL_POINTS to 64px or smaller) and demonstrates the canvas_size_limits feature for constraining marker sizes during zoom/pan.

Controls: * m: Toggle between ‘points’ and ‘instanced’ rendering methods * s: Cycle through marker sizes * c: Toggle canvas size clamping (min 10px, max 100px) * z: Toggle scaling mode: ‘fixed’ vs ‘scene’ (grows/shrinks with zoom) * l: Toggle spherical lighting (3D sphere effect) * k: Cycle through marker shapes (disc, arrow, ring, etc.)

Notes:

  • you may not see a difference between methods on your platform - that’s fine!

  • ‘instanced’ method should be the same across platforms (arbitrarily large markers)

  • Canvas size clamping keeps markers readable during zoom

  • Spherical lighting adds depth to markers with simulated 3D lighting

  • There may be lighting direction differences between methods (known issue)

instanced markers
method=instanced | symbol=disc | size=16px | clamp=off | scaling=fixed | lighting=off

from itertools import cycle

from vispy import scene, use
from vispy.visuals.markers import symbol_shaders

use(gl="gl+")


marker_sizes = cycle([16, 32, 64, 96, 128, 256, 512])
scaling_modes = cycle(["fixed", "scene"])
marker_symbols = cycle(symbol_shaders.keys())


class Canvas(scene.SceneCanvas):
    def __init__(self):
        scene.SceneCanvas.__init__(
            self, keys="interactive", size=(512, 512), title="Instanced Markers Demo"
        )
        self.unfreeze()
        self.view = self.central_widget.add_view()
        self.view.camera = scene.PanZoomCamera(rect=(0, 0, 512, 512), aspect=1.0)

        self.marker_positions, self.face_colors = _create_markers_pattern()
        self.method = "instanced"
        self.current_size = next(marker_sizes)
        self.current_symbol = next(marker_symbols)
        self.clamping_enabled = False
        self.scaling_mode = next(scaling_modes)
        self.spherical_enabled = False
        self.markers = scene.visuals.Markers(
            method=self.method,
            parent=self.view.scene,
            scaling=self.scaling_mode,
            spherical=self.spherical_enabled,
        )

        self.freeze()

        self.markers.set_data(
            self.marker_positions,
            face_color=self.face_colors,
            edge_color="black",
            size=self.current_size,
            edge_width=2,
            symbol=self.current_symbol,
        )

        self.view.bgcolor = "#2e3440"

        self.print_state()
        self.show()

    def print_state(self, changed=None):
        """Print current state with optional highlighting of what changed."""
        clamp_str = (
            f"{self.markers.canvas_size_limits}"
            if self.clamping_enabled
            else "off"
        )

        parts = {
            'method': f"method={self.method}",
            'symbol': f"symbol={self.current_symbol}",
            'size': f"size={self.current_size}px",
            'clamp': f"clamp={clamp_str}",
            'scaling': f"scaling={self.scaling_mode}",
            'lighting': f"lighting={'on' if self.spherical_enabled else 'off'}",
        }

        # highlight the changed part in bold
        if changed and changed in parts:
            parts[changed] = f"\033[1m{parts[changed]}\033[0m"

        state_line = " | ".join(parts.values())
        state_line = state_line.ljust(120)
        print(f"\r{state_line}", end="", flush=True)

    def on_key_press(self, event):
        if event.text == "m":
            self.method = "instanced" if self.method == "points" else "points"

            # recreate markers with new method, cannot change method on the fly
            self.markers.parent = None
            self.markers = scene.visuals.Markers(
                method=self.method, parent=self.view.scene
            )
            self.markers.set_data(
                self.marker_positions,
                face_color=self.face_colors,
                edge_color="black",
                size=self.current_size,
                edge_width=2,
                symbol=self.current_symbol,
            )
            self.markers.scaling = self.scaling_mode
            self.markers.spherical = self.spherical_enabled
            if self.clamping_enabled:
                self.markers.canvas_size_limits = (10, 100)

            self.print_state(changed='method')
            self.update()

        elif event.text == "s":
            self.current_size = next(marker_sizes)
            self.markers.set_data(
                self.marker_positions,
                face_color=self.face_colors,
                edge_color="black",
                size=self.current_size,
                edge_width=2,
                symbol=self.current_symbol,
            )
            self.print_state(changed='size')
            self.update()

        elif event.text == "c":
            self.clamping_enabled = not self.clamping_enabled
            if self.clamping_enabled:
                self.markers.canvas_size_limits = (10, 100)
            else:
                self.markers.canvas_size_limits = None
            self.print_state(changed='clamp')
            self.update()

        elif event.text == "z":
            self.scaling_mode = next(scaling_modes)
            self.markers.scaling = self.scaling_mode
            self.print_state(changed='scaling')
            self.update()

        elif event.text == "l":
            self.spherical_enabled = not self.spherical_enabled
            self.markers.spherical = self.spherical_enabled
            self.print_state(changed='lighting')
            self.update()

        elif event.text == "k":
            self.current_symbol = next(marker_symbols)
            self.markers.symbol = self.current_symbol
            self.print_state(changed='symbol')
            self.update()


def _create_markers_pattern():
    import numpy as np

    # Create positions in a circle plus one in the center
    n = 12
    angles = np.linspace(0, 2 * np.pi, n, endpoint=False)
    radius = 150
    center = np.array([256, 256])
    pos = np.column_stack(
        [center[0] + radius * np.cos(angles), center[1] + radius * np.sin(angles)]
    ).astype(np.float32)

    pos = np.vstack([pos, center])

    colors = np.zeros((n + 1, 4), dtype=np.float32)
    for i in range(n):
        hue = i / n
        h = hue * 6
        x = 1 - abs(h % 2 - 1)
        if h < 1:
            colors[i] = [1, x, 0, 1]
        elif h < 2:
            colors[i] = [x, 1, 0, 1]
        elif h < 3:
            colors[i] = [0, 1, x, 1]
        elif h < 4:
            colors[i] = [0, x, 1, 1]
        elif h < 5:
            colors[i] = [x, 0, 1, 1]
        else:
            colors[i] = [1, 0, x, 1]
    colors[-1] = [1, 1, 1, 1]

    return pos, colors


if __name__ == "__main__":
    from vispy import app
    print(__doc__)
    canvas = Canvas()
    app.run()

Total running time of the script: (0 minutes 0.744 seconds)

Gallery generated by Sphinx-Gallery