From 0ff3abf9658203fb758c751375769b67c2360097 Mon Sep 17 00:00:00 2001 From: Ashley Anderson Date: Wed, 10 May 2023 11:55:54 -0400 Subject: [PATCH 01/14] Face picking prototype --- examples/scene/mesh_shading.py | 113 ++++++++++++++++++++++++++++++--- 1 file changed, 104 insertions(+), 9 deletions(-) diff --git a/examples/scene/mesh_shading.py b/examples/scene/mesh_shading.py index 3fb4764bc9..f043a5ff82 100644 --- a/examples/scene/mesh_shading.py +++ b/examples/scene/mesh_shading.py @@ -10,10 +10,13 @@ Show mesh filter usage for shading (lighting) a mesh and displaying a wireframe. """ - import argparse +import time + +import numpy as np from vispy import app, scene +from vispy.geometry import MeshData from vispy.io import read_mesh, load_data_file from vispy.scene.visuals import Mesh from vispy.scene import transforms @@ -27,7 +30,7 @@ parser.add_argument('--wireframe-width', default=1) args, _ = parser.parse_known_args() -vertices, faces, normals, texcoords = read_mesh(args.mesh) +vertices, faces, _normals, _texcoords = read_mesh(args.mesh) canvas = scene.SceneCanvas(keys='interactive', bgcolor='white') view = canvas.central_widget.add_view() @@ -36,12 +39,31 @@ view.camera.depth_value = 1e3 # Create a colored `MeshVisual`. -mesh = Mesh(vertices, faces, color=(.5, .7, .5, 1)) +face_colors = np.tile((0.5, 0.0, 0.5, 1.0), (len(faces), 1)) +mesh = Mesh( + vertices, + faces, + # color=(0.5, 0.0, 0.5, 1.0) + face_colors=face_colors +) mesh.transform = transforms.MatrixTransform() mesh.transform.rotate(90, (1, 0, 0)) mesh.transform.rotate(-45, (0, 0, 1)) view.add(mesh) +colors = np.arange(1, len(faces) + 1, dtype=np.uint32).view(np.uint8).reshape(len(faces), 4) / 255 +picking_mesh = Mesh( + meshdata=MeshData( + mesh.mesh_data.get_vertices(), + mesh.mesh_data.get_faces(), + face_colors=colors, + ) +) +picking_mesh.transform = mesh.transform +picking_mesh.update_gl_state(depth_test=True, blend=False) +picking_mesh.visible = False +view.add(picking_mesh) + # Use filters to affect the rendering of the mesh. wireframe_filter = WireframeFilter(width=args.wireframe_width) # Note: For convenience, this `ShadingFilter` would be created automatically by @@ -50,6 +72,7 @@ shading_filter = ShadingFilter(shininess=args.shininess) # The wireframe filter is attached before the shading filter otherwise the # wireframe is not shaded. + mesh.attach(wireframe_filter) mesh.attach(shading_filter) @@ -91,6 +114,67 @@ def cycle_state(states, index): return states[new_index], new_index +camera_moving = False + + +@canvas.events.mouse_press.connect +def on_mouse_press(event): + global camera_moving + camera_moving = True + + +@canvas.events.mouse_release.connect +def on_mouse_release(event): + global camera_moving + camera_moving = False + + +@canvas.events.mouse_move.connect +def on_mouse_move(event): + with canvas.events.mouse_move.blocker(): + start = time.perf_counter() + readd_mesh = False + if mesh.visible: + mesh.visible = False + readd_mesh = True + picking_mesh.visible = True + picking_mesh.update_gl_state(depth_test=True, blend=False) + # if mesh.parent is not None: + # readd_mesh = True + # mesh.parent = None + # if picking_mesh.parent is None: + # picking_mesh.update_gl_state(depth_test=True, blend=False) + # view.add(picking_mesh) + # print("switch to picking visual", time.perf_counter() - start) + + start = time.perf_counter() + picking_render = canvas.render(bgcolor=(0, 0, 0, 0), alpha=True) + col, row = event.pos + # TODO: handle hidpi screens properly + # col *= 2 + # row *= 2 + # print(col, row, picking_render.shape) + id = picking_render.view(np.uint32) - 1 + # print("do picking", time.perf_counter() - start) + + start = time.perf_counter() + if readd_mesh: + mesh.visible = True + picking_mesh.visible = False + # picking_mesh.parent = None + # view.add(mesh) + + meshdata = mesh.mesh_data + if id[row, col] > 0 and id[row, col] < len(face_colors): + face_colors[id[row, col], :] = (0, 1, 0, 1) + # meshdata.set_face_colors(face_colors) + # mesh.set_data(meshdata=meshdata) + # this is unsafe but faster + meshdata._face_colors_indexed_by_faces[id[row, col]] = (0, 1, 0, 1) + mesh.mesh_data_changed() + # print("switch back", time.perf_counter() - start) + + @canvas.events.key_press.connect def on_key_press(event): global shading_state_index @@ -104,12 +188,23 @@ def on_key_press(event): elif event.key == 'w': wireframe_filter.enabled = not wireframe_filter.enabled mesh.update() - elif event.key == 'f': - state, wireframe_state_index = cycle_state(wireframe_states, - wireframe_state_index) - for attr, value in state.items(): - setattr(wireframe_filter, attr, value) - mesh.update() + elif event.key == 'p': + if mesh.visible: + picking_mesh.visible = True + mesh.visible = False + view.bgcolor = (0, 0, 0, 1) + elif picking_mesh.visible: + picking_mesh.visible = False + mesh.visible = True + view.bgcolor = (1, 1, 1, 1) + # if picking_mesh.parent is None: + # mesh.parent = None + # view.bgcolor = (0, 0, 0, 1) + # view.add(picking_mesh) + # else: + # picking_mesh.parent = None + # view.bgcolor = (1, 1, 1, 1) + # view.add(mesh) canvas.show() From 54094e1d613d8652d3b3c62097c49acb104f259f Mon Sep 17 00:00:00 2001 From: Ashley Anderson Date: Tue, 13 Jun 2023 15:25:56 -0400 Subject: [PATCH 02/14] Implement face picking as a mesh filter --- examples/scene/face_picking.py | 179 ++++++++++++++++++++++++++++++ examples/scene/mesh_shading.py | 113 ++----------------- vispy/visuals/filters/__init__.py | 2 +- vispy/visuals/filters/mesh.py | 82 ++++++++++++++ 4 files changed, 271 insertions(+), 105 deletions(-) create mode 100644 examples/scene/face_picking.py diff --git a/examples/scene/face_picking.py b/examples/scene/face_picking.py new file mode 100644 index 0000000000..22a2ca7f5b --- /dev/null +++ b/examples/scene/face_picking.py @@ -0,0 +1,179 @@ +# -*- coding: utf-8 -*- +# vispy: gallery 30 +# ----------------------------------------------------------------------------- +# Copyright (c) Vispy Development Team. All Rights Reserved. +# Distributed under the (new) BSD License. See LICENSE.txt for more info. +# ----------------------------------------------------------------------------- +""" +Picking Faces from a Mesh +========================= + +Demonstrate +""" +import argparse +import time + +import numpy as np + +from vispy import app, scene +from vispy.io import read_mesh, load_data_file +from vispy.scene.visuals import Mesh +from vispy.scene import transforms +from vispy.visuals.filters import ShadingFilter, WireframeFilter, FacePickingFilter + + +parser = argparse.ArgumentParser() +default_mesh = load_data_file('orig/triceratops.obj.gz') +parser.add_argument('--mesh', default=default_mesh) +parser.add_argument('--shininess', default=100) +parser.add_argument('--wireframe-width', default=1) +args, _ = parser.parse_known_args() + +vertices, faces, _normals, _texcoords = read_mesh(args.mesh) + +canvas = scene.SceneCanvas(keys='interactive', bgcolor='white') +view = canvas.central_widget.add_view() + +view.camera = 'arcball' +view.camera.depth_value = 1e3 + +# Create a colored `MeshVisual`. +face_colors = np.tile((0.5, 0.0, 0.5, 1.0), (len(faces), 1)) +mesh = Mesh( + vertices, + faces, + face_colors=face_colors +) +mesh.transform = transforms.MatrixTransform() +mesh.transform.rotate(90, (1, 0, 0)) +mesh.transform.rotate(-45, (0, 0, 1)) +view.add(mesh) + +# Use filters to affect the rendering of the mesh. +wireframe_filter = WireframeFilter(width=args.wireframe_width) +# Note: For convenience, this `ShadingFilter` would be created automatically by +# the `MeshVisual with, e.g. `mesh = MeshVisual(..., shading='smooth')`. It is +# created manually here for demonstration purposes. +shading_filter = ShadingFilter(shininess=args.shininess) +# The wireframe filter is attached before the shading filter otherwise the +# wireframe is not shaded. + +face_picking_filter = FacePickingFilter() + +mesh.attach(wireframe_filter) +mesh.attach(shading_filter) +mesh.attach(face_picking_filter) + + +def attach_headlight(view): + light_dir = (0, 1, 0, 0) + shading_filter.light_dir = light_dir[:3] + initial_light_dir = view.camera.transform.imap(light_dir) + + @view.scene.transform.changed.connect + def on_transform_change(event): + transform = view.camera.transform + shading_filter.light_dir = transform.map(initial_light_dir)[:3] + + +attach_headlight(view) + +shading_states = ( + dict(shading=None), + dict(shading='flat'), + dict(shading='smooth'), +) +shading_state_index = shading_states.index( + dict(shading=shading_filter.shading)) + +wireframe_states = ( + dict(wireframe_only=False, faces_only=False,), + dict(wireframe_only=True, faces_only=False,), + dict(wireframe_only=False, faces_only=True,), +) +wireframe_state_index = wireframe_states.index(dict( + wireframe_only=wireframe_filter.wireframe_only, + faces_only=wireframe_filter.faces_only, +)) + + +def cycle_state(states, index): + new_index = (index + 1) % len(states) + return states[new_index], new_index + + +camera_moving = False + + +@canvas.events.mouse_press.connect +def on_mouse_press(event): + global camera_moving + camera_moving = True + + +@canvas.events.mouse_release.connect +def on_mouse_release(event): + global camera_moving + camera_moving = False + + +@canvas.events.mouse_move.connect +def on_mouse_move(event): + restore_state = not face_picking_filter.enabled + + face_picking_filter.enabled = True + mesh.update_gl_state(depth_test=True, blend=False) + picking_render = canvas.render(bgcolor=(0, 0, 0, 0), alpha=True) + + if restore_state: + face_picking_filter.enabled = False + mesh.update_gl_state(depth_test=True, blend=True) + + id = picking_render.view(np.uint32) - 1 + + # modify for hidpi screens + cols, rows = canvas.size + col, row = event.pos + col = int(col / cols * id.shape[1]) + row = int(row / rows * id.shape[0]) + + # color the hovered face on the mesh + meshdata = mesh.mesh_data + if id[row, col] > 0 and id[row, col] < len(face_colors): + face_colors[id[row, col], :] = (0, 1, 0, 1) + # meshdata.set_face_colors(face_colors) + # mesh.set_data(meshdata=meshdata) + # this less safe, but faster + meshdata._face_colors_indexed_by_faces[id[row, col]] = (0, 1, 0, 1) + mesh.mesh_data_changed() + + +@canvas.events.key_press.connect +def on_key_press(event): + global shading_state_index + global wireframe_state_index + if event.key == 's': + state, shading_state_index = cycle_state(shading_states, + shading_state_index) + for attr, value in state.items(): + setattr(shading_filter, attr, value) + mesh.update() + elif event.key == 'w': + wireframe_filter.enabled = not wireframe_filter.enabled + mesh.update() + elif event.key == 'p': + if face_picking_filter.enabled: + face_picking_filter.enabled = False + mesh.update_gl_state(depth_test=True, blend=True) + view.update() + else: + face_picking_filter.enabled = True + mesh.update_gl_state(depth_test=True, blend=False) + view.update() + + +canvas.show() + + +if __name__ == "__main__": + app.run() diff --git a/examples/scene/mesh_shading.py b/examples/scene/mesh_shading.py index f043a5ff82..3fb4764bc9 100644 --- a/examples/scene/mesh_shading.py +++ b/examples/scene/mesh_shading.py @@ -10,13 +10,10 @@ Show mesh filter usage for shading (lighting) a mesh and displaying a wireframe. """ -import argparse -import time -import numpy as np +import argparse from vispy import app, scene -from vispy.geometry import MeshData from vispy.io import read_mesh, load_data_file from vispy.scene.visuals import Mesh from vispy.scene import transforms @@ -30,7 +27,7 @@ parser.add_argument('--wireframe-width', default=1) args, _ = parser.parse_known_args() -vertices, faces, _normals, _texcoords = read_mesh(args.mesh) +vertices, faces, normals, texcoords = read_mesh(args.mesh) canvas = scene.SceneCanvas(keys='interactive', bgcolor='white') view = canvas.central_widget.add_view() @@ -39,31 +36,12 @@ view.camera.depth_value = 1e3 # Create a colored `MeshVisual`. -face_colors = np.tile((0.5, 0.0, 0.5, 1.0), (len(faces), 1)) -mesh = Mesh( - vertices, - faces, - # color=(0.5, 0.0, 0.5, 1.0) - face_colors=face_colors -) +mesh = Mesh(vertices, faces, color=(.5, .7, .5, 1)) mesh.transform = transforms.MatrixTransform() mesh.transform.rotate(90, (1, 0, 0)) mesh.transform.rotate(-45, (0, 0, 1)) view.add(mesh) -colors = np.arange(1, len(faces) + 1, dtype=np.uint32).view(np.uint8).reshape(len(faces), 4) / 255 -picking_mesh = Mesh( - meshdata=MeshData( - mesh.mesh_data.get_vertices(), - mesh.mesh_data.get_faces(), - face_colors=colors, - ) -) -picking_mesh.transform = mesh.transform -picking_mesh.update_gl_state(depth_test=True, blend=False) -picking_mesh.visible = False -view.add(picking_mesh) - # Use filters to affect the rendering of the mesh. wireframe_filter = WireframeFilter(width=args.wireframe_width) # Note: For convenience, this `ShadingFilter` would be created automatically by @@ -72,7 +50,6 @@ shading_filter = ShadingFilter(shininess=args.shininess) # The wireframe filter is attached before the shading filter otherwise the # wireframe is not shaded. - mesh.attach(wireframe_filter) mesh.attach(shading_filter) @@ -114,67 +91,6 @@ def cycle_state(states, index): return states[new_index], new_index -camera_moving = False - - -@canvas.events.mouse_press.connect -def on_mouse_press(event): - global camera_moving - camera_moving = True - - -@canvas.events.mouse_release.connect -def on_mouse_release(event): - global camera_moving - camera_moving = False - - -@canvas.events.mouse_move.connect -def on_mouse_move(event): - with canvas.events.mouse_move.blocker(): - start = time.perf_counter() - readd_mesh = False - if mesh.visible: - mesh.visible = False - readd_mesh = True - picking_mesh.visible = True - picking_mesh.update_gl_state(depth_test=True, blend=False) - # if mesh.parent is not None: - # readd_mesh = True - # mesh.parent = None - # if picking_mesh.parent is None: - # picking_mesh.update_gl_state(depth_test=True, blend=False) - # view.add(picking_mesh) - # print("switch to picking visual", time.perf_counter() - start) - - start = time.perf_counter() - picking_render = canvas.render(bgcolor=(0, 0, 0, 0), alpha=True) - col, row = event.pos - # TODO: handle hidpi screens properly - # col *= 2 - # row *= 2 - # print(col, row, picking_render.shape) - id = picking_render.view(np.uint32) - 1 - # print("do picking", time.perf_counter() - start) - - start = time.perf_counter() - if readd_mesh: - mesh.visible = True - picking_mesh.visible = False - # picking_mesh.parent = None - # view.add(mesh) - - meshdata = mesh.mesh_data - if id[row, col] > 0 and id[row, col] < len(face_colors): - face_colors[id[row, col], :] = (0, 1, 0, 1) - # meshdata.set_face_colors(face_colors) - # mesh.set_data(meshdata=meshdata) - # this is unsafe but faster - meshdata._face_colors_indexed_by_faces[id[row, col]] = (0, 1, 0, 1) - mesh.mesh_data_changed() - # print("switch back", time.perf_counter() - start) - - @canvas.events.key_press.connect def on_key_press(event): global shading_state_index @@ -188,23 +104,12 @@ def on_key_press(event): elif event.key == 'w': wireframe_filter.enabled = not wireframe_filter.enabled mesh.update() - elif event.key == 'p': - if mesh.visible: - picking_mesh.visible = True - mesh.visible = False - view.bgcolor = (0, 0, 0, 1) - elif picking_mesh.visible: - picking_mesh.visible = False - mesh.visible = True - view.bgcolor = (1, 1, 1, 1) - # if picking_mesh.parent is None: - # mesh.parent = None - # view.bgcolor = (0, 0, 0, 1) - # view.add(picking_mesh) - # else: - # picking_mesh.parent = None - # view.bgcolor = (1, 1, 1, 1) - # view.add(mesh) + elif event.key == 'f': + state, wireframe_state_index = cycle_state(wireframe_states, + wireframe_state_index) + for attr, value in state.items(): + setattr(wireframe_filter, attr, value) + mesh.update() canvas.show() diff --git a/vispy/visuals/filters/__init__.py b/vispy/visuals/filters/__init__.py index a22f5e62f5..47635dc18d 100644 --- a/vispy/visuals/filters/__init__.py +++ b/vispy/visuals/filters/__init__.py @@ -6,4 +6,4 @@ from .clipper import Clipper # noqa from .color import Alpha, ColorFilter, IsolineFilter, ZColormapFilter # noqa from .picking import PickingFilter # noqa -from .mesh import TextureFilter, ShadingFilter, InstancedShadingFilter, WireframeFilter # noqa +from .mesh import TextureFilter, ShadingFilter, InstancedShadingFilter, WireframeFilter, FacePickingFilter # noqa diff --git a/vispy/visuals/filters/mesh.py b/vispy/visuals/filters/mesh.py index 5fa13d88b4..123715e450 100644 --- a/vispy/visuals/filters/mesh.py +++ b/vispy/visuals/filters/mesh.py @@ -766,3 +766,85 @@ def _attach(self, visual): def _detach(self, visual): visual.events.data_updated.disconnect(self.on_mesh_data_updated) super()._detach(visual) + + +class FacePickingFilter(Filter): + """Filter used to color mesh faces by a picking ID. + + Note that the ID color uses the alpha channel, so this may not be used + with blending enabled. + + Examples + -------- + See + `examples/scene/face_picking.py + `_ + example script. + """ + + def __init__(self): + vfunc = Function("""\ + varying vec4 v_face_picking_color; + void prepare_face_picking() { + v_face_picking_color = $ids; + } + """) + ffunc = Function("""\ + varying vec4 v_face_picking_color; + void draw_face_picking() { + if ($enabled != 1) { + return; + } + gl_FragColor = v_face_picking_color; + } + """) + + self._ids = VertexBuffer(np.zeros((0, 4), dtype=np.float32)) + vfunc['ids'] = self._ids + super().__init__(vcode=vfunc, fcode=ffunc) + self._n_faces = 0 + self.enabled = False + + @property + def enabled(self): + return self._enabled + + @enabled.setter + def enabled(self, e): + self._enabled = bool(e) + self.fshader['enabled'] = int(self._enabled) + self._update_data() + + def _update_data(self): + if not self.attached: + return + if self._visual.mesh_data.is_empty(): + n_faces = 0 + else: + n_faces = len(self._visual.mesh_data.get_faces()) + + # we only care about the number of faces changing + if self._n_faces == n_faces: + return + self._n_faces = n_faces + + # pack the face ids into a color buffer + # TODO: consider doing the bit-packing in the shader + ids = np.arange( + 1, n_faces + 1, + dtype=np.uint32 + ).view(np.uint8).reshape(n_faces, 4) + ids = np.divide(ids, 255, dtype=np.float32) + self._ids.set_data(np.repeat(ids, 3, axis=0), convert=True) + + def on_mesh_data_updated(self, event): + self._update_data() + + def _attach(self, visual): + super()._attach(visual) + visual.events.data_updated.connect(self.on_mesh_data_updated) + self._update_data() + + def _detach(self, visual): + visual.events.data_updated.disconnect(self.on_mesh_data_updated) + super()._detach(visual) From 07e3f8d979c100a43c455c8255b2b776213d211a Mon Sep 17 00:00:00 2001 From: Ashley Anderson Date: Wed, 14 Jun 2023 10:41:19 -0400 Subject: [PATCH 03/14] Add test for face picking --- .../filters/tests/test_face_picking_filter.py | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 vispy/visuals/filters/tests/test_face_picking_filter.py diff --git a/vispy/visuals/filters/tests/test_face_picking_filter.py b/vispy/visuals/filters/tests/test_face_picking_filter.py new file mode 100644 index 0000000000..35e16bc688 --- /dev/null +++ b/vispy/visuals/filters/tests/test_face_picking_filter.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Vispy Development Team. All Rights Reserved. +# Distributed under the (new) BSD License. See LICENSE.txt for more info. +import numpy as np + +from vispy.geometry import create_plane +from vispy.scene.visuals import Mesh +from vispy.testing import TestingCanvas +from vispy.visuals.filters import FacePickingFilter + + +def test_empty_mesh_face_picking(): + mesh = Mesh() + filter = FacePickingFilter() + mesh.attach(filter) + filter.enabled = True + + +def test_face_picking(): + vertices, faces, _ = create_plane(125, 125) + vertices = vertices["position"] + vertices[:, :2] += 125 / 2 + mesh = Mesh(vertices=vertices, faces=faces) + filter = FacePickingFilter() + mesh.attach(filter) + + with TestingCanvas(size=(125, 125)) as c: + view = c.central_widget.add_view() + view.add(mesh) + filter.enabled = True + mesh.update_gl_state(blend=False) + picking_render = c.render(bgcolor=(0, 0, 0, 0), alpha=True) + + # the plane is made up of two triangles and nearly fills the view + # pick one point on each triangle + assert np.allclose( + picking_render[125 // 2, 125 // 4], + (1, 0, 0, 0), + ) + assert np.allclose( + picking_render[125 // 2, 3 * 125 // 4], + (2, 0, 0, 0), + ) From 29b0f5dca923bfa049d252e0fda7433d681bcf03 Mon Sep 17 00:00:00 2001 From: Ashley Anderson Date: Wed, 14 Jun 2023 10:53:16 -0400 Subject: [PATCH 04/14] Clean up example --- examples/scene/face_picking.py | 96 +++++++++------------------------- vispy/visuals/filters/mesh.py | 4 +- 2 files changed, 28 insertions(+), 72 deletions(-) diff --git a/examples/scene/face_picking.py b/examples/scene/face_picking.py index 22a2ca7f5b..da148a8481 100644 --- a/examples/scene/face_picking.py +++ b/examples/scene/face_picking.py @@ -8,10 +8,18 @@ Picking Faces from a Mesh ========================= -Demonstrate +Demonstrates how to identify (pick) individual faces on a mesh. + +Arguments: +* --mesh - Path to a mesh file (OBJ/OBJ.GZ) [optional] + +Controls: +* s - Cycle shading modes (None, 'flat', 'smooth') +* w - Toggle wireframe +* p - Toggle face picking view """ import argparse -import time +import itertools import numpy as np @@ -25,8 +33,6 @@ parser = argparse.ArgumentParser() default_mesh = load_data_file('orig/triceratops.obj.gz') parser.add_argument('--mesh', default=default_mesh) -parser.add_argument('--shininess', default=100) -parser.add_argument('--wireframe-width', default=1) args, _ = parser.parse_known_args() vertices, faces, _normals, _texcoords = read_mesh(args.mesh) @@ -50,16 +56,9 @@ view.add(mesh) # Use filters to affect the rendering of the mesh. -wireframe_filter = WireframeFilter(width=args.wireframe_width) -# Note: For convenience, this `ShadingFilter` would be created automatically by -# the `MeshVisual with, e.g. `mesh = MeshVisual(..., shading='smooth')`. It is -# created manually here for demonstration purposes. -shading_filter = ShadingFilter(shininess=args.shininess) -# The wireframe filter is attached before the shading filter otherwise the -# wireframe is not shaded. - +wireframe_filter = WireframeFilter() +shading_filter = ShadingFilter() face_picking_filter = FacePickingFilter() - mesh.attach(wireframe_filter) mesh.attach(shading_filter) mesh.attach(face_picking_filter) @@ -78,43 +77,8 @@ def on_transform_change(event): attach_headlight(view) -shading_states = ( - dict(shading=None), - dict(shading='flat'), - dict(shading='smooth'), -) -shading_state_index = shading_states.index( - dict(shading=shading_filter.shading)) - -wireframe_states = ( - dict(wireframe_only=False, faces_only=False,), - dict(wireframe_only=True, faces_only=False,), - dict(wireframe_only=False, faces_only=True,), -) -wireframe_state_index = wireframe_states.index(dict( - wireframe_only=wireframe_filter.wireframe_only, - faces_only=wireframe_filter.faces_only, -)) - - -def cycle_state(states, index): - new_index = (index + 1) % len(states) - return states[new_index], new_index - - -camera_moving = False - - -@canvas.events.mouse_press.connect -def on_mouse_press(event): - global camera_moving - camera_moving = True - - -@canvas.events.mouse_release.connect -def on_mouse_release(event): - global camera_moving - camera_moving = False +shading = itertools.cycle(("flat", "smooth", None)) +shading_filter.shading = next(shading) @canvas.events.mouse_move.connect @@ -122,16 +86,16 @@ def on_mouse_move(event): restore_state = not face_picking_filter.enabled face_picking_filter.enabled = True - mesh.update_gl_state(depth_test=True, blend=False) + mesh.update_gl_state(blend=False) picking_render = canvas.render(bgcolor=(0, 0, 0, 0), alpha=True) if restore_state: face_picking_filter.enabled = False - mesh.update_gl_state(depth_test=True, blend=True) - id = picking_render.view(np.uint32) - 1 + mesh.update_gl_state(blend=not face_picking_filter.enabled) - # modify for hidpi screens + id = picking_render.view(np.uint32) - 1 + # account for hidpi screens - canvas and render may have different size cols, rows = canvas.size col, row = event.pos col = int(col / cols * id.shape[1]) @@ -139,41 +103,33 @@ def on_mouse_move(event): # color the hovered face on the mesh meshdata = mesh.mesh_data - if id[row, col] > 0 and id[row, col] < len(face_colors): + if id[row, col] in range(1, len(face_colors)): face_colors[id[row, col], :] = (0, 1, 0, 1) # meshdata.set_face_colors(face_colors) # mesh.set_data(meshdata=meshdata) - # this less safe, but faster + # this may be less safe, but it's faster meshdata._face_colors_indexed_by_faces[id[row, col]] = (0, 1, 0, 1) mesh.mesh_data_changed() @canvas.events.key_press.connect def on_key_press(event): - global shading_state_index - global wireframe_state_index if event.key == 's': - state, shading_state_index = cycle_state(shading_states, - shading_state_index) - for attr, value in state.items(): - setattr(shading_filter, attr, value) + shading_filter.shading = next(shading) mesh.update() elif event.key == 'w': wireframe_filter.enabled = not wireframe_filter.enabled mesh.update() elif event.key == 'p': - if face_picking_filter.enabled: - face_picking_filter.enabled = False - mesh.update_gl_state(depth_test=True, blend=True) - view.update() - else: - face_picking_filter.enabled = True - mesh.update_gl_state(depth_test=True, blend=False) - view.update() + # toggle face picking view + face_picking_filter.enabled = not face_picking_filter.enabled + mesh.update_gl_state(blend=not face_picking_filter.enabled) + mesh.update() canvas.show() if __name__ == "__main__": + print(__doc__) app.run() diff --git a/vispy/visuals/filters/mesh.py b/vispy/visuals/filters/mesh.py index 123715e450..ffbcf195bd 100644 --- a/vispy/visuals/filters/mesh.py +++ b/vispy/visuals/filters/mesh.py @@ -791,7 +791,7 @@ def __init__(self): """) ffunc = Function("""\ varying vec4 v_face_picking_color; - void draw_face_picking() { + void face_picking_filter() { if ($enabled != 1) { return; } @@ -835,7 +835,7 @@ def _update_data(self): dtype=np.uint32 ).view(np.uint8).reshape(n_faces, 4) ids = np.divide(ids, 255, dtype=np.float32) - self._ids.set_data(np.repeat(ids, 3, axis=0), convert=True) + self._ids.set_data(np.repeat(ids, 3, axis=0)) def on_mesh_data_updated(self, event): self._update_data() From b19c22997c931d8fc3da34f3aa2fb203b7c2e87d Mon Sep 17 00:00:00 2001 From: Ashley Anderson Date: Wed, 14 Jun 2023 11:07:32 -0400 Subject: [PATCH 05/14] Add clearing control, more cleanup --- examples/scene/face_picking.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/examples/scene/face_picking.py b/examples/scene/face_picking.py index da148a8481..351fb4aa1f 100644 --- a/examples/scene/face_picking.py +++ b/examples/scene/face_picking.py @@ -16,7 +16,8 @@ Controls: * s - Cycle shading modes (None, 'flat', 'smooth') * w - Toggle wireframe -* p - Toggle face picking view +* p - Toggle face picking view - shows the colors encoding face ID +* c - Clear painted faces """ import argparse import itertools @@ -48,7 +49,7 @@ mesh = Mesh( vertices, faces, - face_colors=face_colors + face_colors=face_colors.copy() ) mesh.transform = transforms.MatrixTransform() mesh.transform.rotate(90, (1, 0, 0)) @@ -94,26 +95,24 @@ def on_mouse_move(event): mesh.update_gl_state(blend=not face_picking_filter.enabled) - id = picking_render.view(np.uint32) - 1 + ids = picking_render.view(np.uint32) - 1 # account for hidpi screens - canvas and render may have different size cols, rows = canvas.size col, row = event.pos - col = int(col / cols * id.shape[1]) - row = int(row / rows * id.shape[0]) + col = int(col / cols * (ids.shape[1] - 1)) + row = int(row / rows * (ids.shape[0] - 1)) # color the hovered face on the mesh - meshdata = mesh.mesh_data - if id[row, col] in range(1, len(face_colors)): - face_colors[id[row, col], :] = (0, 1, 0, 1) - # meshdata.set_face_colors(face_colors) - # mesh.set_data(meshdata=meshdata) + if ids[row, col] in range(1, len(face_colors)): # this may be less safe, but it's faster - meshdata._face_colors_indexed_by_faces[id[row, col]] = (0, 1, 0, 1) + mesh.mesh_data._face_colors_indexed_by_faces[ids[row, col]] = (0, 1, 0, 1) mesh.mesh_data_changed() @canvas.events.key_press.connect def on_key_press(event): + if event.key == 'c': + mesh.set_data(vertices, faces, face_colors=face_colors) if event.key == 's': shading_filter.shading = next(shading) mesh.update() From 7d07618e46c8de2df4da79eaeab46637ecf43396 Mon Sep 17 00:00:00 2001 From: Ashley Anderson Date: Wed, 14 Jun 2023 11:41:08 -0400 Subject: [PATCH 06/14] Mark test as requires_application --- vispy/visuals/filters/tests/test_face_picking_filter.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/vispy/visuals/filters/tests/test_face_picking_filter.py b/vispy/visuals/filters/tests/test_face_picking_filter.py index 35e16bc688..7b5a81752e 100644 --- a/vispy/visuals/filters/tests/test_face_picking_filter.py +++ b/vispy/visuals/filters/tests/test_face_picking_filter.py @@ -1,11 +1,11 @@ -# -*- coding: utf-8 -*- +# -*- coding: utf-8 -*- # Copyright (c) Vispy Development Team. All Rights Reserved. # Distributed under the (new) BSD License. See LICENSE.txt for more info. import numpy as np from vispy.geometry import create_plane from vispy.scene.visuals import Mesh -from vispy.testing import TestingCanvas +from vispy.testing import requires_application, TestingCanvas from vispy.visuals.filters import FacePickingFilter @@ -16,6 +16,7 @@ def test_empty_mesh_face_picking(): filter.enabled = True +@requires_application() def test_face_picking(): vertices, faces, _ = create_plane(125, 125) vertices = vertices["position"] From 0f66ab8d25c7f28823a6bb7c0cef129a79438557 Mon Sep 17 00:00:00 2001 From: Ashley Anderson Date: Wed, 14 Jun 2023 15:06:33 -0400 Subject: [PATCH 07/14] Render into a region for face picking example, also throttle mouse events --- examples/scene/face_picking.py | 43 ++++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/examples/scene/face_picking.py b/examples/scene/face_picking.py index 351fb4aa1f..59586f1c77 100644 --- a/examples/scene/face_picking.py +++ b/examples/scene/face_picking.py @@ -21,6 +21,7 @@ """ import argparse import itertools +import time import numpy as np @@ -82,30 +83,42 @@ def on_transform_change(event): shading_filter.shading = next(shading) +throttle = time.monotonic() + + @canvas.events.mouse_move.connect def on_mouse_move(event): + global throttle + # throttle mouse events to 50ms + if time.monotonic() - throttle < 0.05: + return + throttle = time.monotonic() + + # adjust the event position for hidpi screens + render_size = tuple(d * canvas.pixel_scale for d in canvas.size) + x_pos = event.pos[0] * canvas.pixel_scale + y_pos = render_size[1] - (event.pos[1] * canvas.pixel_scale) + + # render a small patch around the mouse cursor restore_state = not face_picking_filter.enabled - face_picking_filter.enabled = True mesh.update_gl_state(blend=False) - picking_render = canvas.render(bgcolor=(0, 0, 0, 0), alpha=True) - + picking_render = canvas.render( + region=(x_pos - 1, y_pos - 1, 3, 3), + size=(3, 3), + bgcolor=(0, 0, 0, 0), + alpha=True, + ) if restore_state: face_picking_filter.enabled = False - mesh.update_gl_state(blend=not face_picking_filter.enabled) - ids = picking_render.view(np.uint32) - 1 - # account for hidpi screens - canvas and render may have different size - cols, rows = canvas.size - col, row = event.pos - col = int(col / cols * (ids.shape[1] - 1)) - row = int(row / rows * (ids.shape[0] - 1)) - - # color the hovered face on the mesh - if ids[row, col] in range(1, len(face_colors)): - # this may be less safe, but it's faster - mesh.mesh_data._face_colors_indexed_by_faces[ids[row, col]] = (0, 1, 0, 1) + # unpack the face index from the color in the center pixel + face_idx = (picking_render.view(np.uint32) - 1)[1, 1] + + if face_idx > 0 and face_idx < len(face_colors): + # this may be less safe, but it's faster than set_data + mesh.mesh_data._face_colors_indexed_by_faces[face_idx] = (0, 1, 0, 1) mesh.mesh_data_changed() From 2dd3374e3463ccf0ed6585140410873d10f6c94a Mon Sep 17 00:00:00 2001 From: Ashley Anderson Date: Thu, 15 Jun 2023 12:08:12 -0400 Subject: [PATCH 08/14] Marker picking with working example --- examples/scene/marker_picking.py | 128 ++++++++++++++++++++++++++++++ vispy/visuals/filters/__init__.py | 1 + vispy/visuals/filters/markers.py | 87 ++++++++++++++++++++ vispy/visuals/markers.py | 4 + 4 files changed, 220 insertions(+) create mode 100644 examples/scene/marker_picking.py create mode 100644 vispy/visuals/filters/markers.py diff --git a/examples/scene/marker_picking.py b/examples/scene/marker_picking.py new file mode 100644 index 0000000000..465c7671a5 --- /dev/null +++ b/examples/scene/marker_picking.py @@ -0,0 +1,128 @@ +# -*- coding: utf-8 -*- +# vispy: gallery 30 +# ----------------------------------------------------------------------------- +# Copyright (c) Vispy Development Team. All Rights Reserved. +# Distributed under the (new) BSD License. See LICENSE.txt for more info. +# ----------------------------------------------------------------------------- +""" +Picking Markers +=============== + +Demonstrates how to identify (pick) markers. Click markers to change their edge +color. + +Controls: +* p - Toggle picking view - shows the colors encoding marker ID +* s - Cycle marker symbols +* c - Clear picked markers +""" +import itertools + +import numpy as np +from scipy.constants import golden as GOLDEN + +from vispy import app, scene +from vispy.scene.visuals import Markers +from vispy.visuals.filters import MarkerPickingFilter + +symbols = itertools.cycle(Markers._symbol_shader_values.keys()) + + +MARKER_SIZE = 0.0125 +EDGE_WDITH = MARKER_SIZE / 10 + +canvas = scene.SceneCanvas(keys='interactive', bgcolor='black') +view = canvas.central_widget.add_view(camera="panzoom") +view.camera.rect = (-1, -1, 2, 2) +n = 10_000 +radius = np.linspace(0, 0.9, n)**0.6 +theta = np.arange(n) * GOLDEN * np.pi +pos = np.column_stack([radius * np.cos(theta), radius * np.sin(theta)]) +edge_color = np.ones((len(pos), 4), dtype=np.float32) + +markers = Markers( + pos=pos, + edge_color=edge_color, + face_color="red", + size=MARKER_SIZE, + edge_width=EDGE_WDITH, + scaling="scene", + symbol=next(symbols), +) +markers.update_gl_state(depth_test=True) +view.add(markers) + +# Use filters to affect the rendering of the mesh. +picking_filter = MarkerPickingFilter() +markers.attach(picking_filter) + + +@view.events.connect +def on_viewbox_change(event): + # workaround for vispy/#2501 + markers.update_gl_state(blend=not picking_filter.enabled) + + +@canvas.events.mouse_press.connect +def on_mouse_press(event): + # adjust the event position for hidpi screens + render_size = tuple(d * canvas.pixel_scale for d in canvas.size) + x_pos = event.pos[0] * canvas.pixel_scale + y_pos = render_size[1] - (event.pos[1] * canvas.pixel_scale) + + # render a small patch around the mouse cursor + restore_state = not picking_filter.enabled + picking_filter.enabled = True + markers.update_gl_state(blend=False) + picking_render = canvas.render( + crop=(x_pos - 2, y_pos - 2, 5, 5), + bgcolor=(0, 0, 0, 0), + alpha=True, + ) + if restore_state: + picking_filter.enabled = False + markers.update_gl_state(blend=not picking_filter.enabled) + + # unpack the face index from the color in the center pixel + face_idx = (picking_render.view(np.uint32) - 1)[2, 2] + + if face_idx >= 0 and face_idx < len(pos): + edge_color[face_idx] = (0, 1, 0, 1) + markers.set_data( + pos=pos, + edge_color=edge_color, + face_color="red", + size=MARKER_SIZE, + edge_width=EDGE_WDITH, + symbol=markers.symbol, + ) + + +@canvas.events.key_press.connect +def on_key_press(event): + if event.key == 'c': + edge_color = np.ones((len(pos), 4), dtype=np.float32) + markers.set_data( + pos=pos, + edge_color=edge_color, + face_color="red", + size=MARKER_SIZE, + edge_width=EDGE_WDITH, + symbol=markers.symbol, + ) + if event.key == 'p': + # toggle face picking view + picking_filter.enabled = not picking_filter.enabled + markers.update_gl_state(blend=not picking_filter.enabled) + markers.update() + if event.key == 's': + markers.symbol = next(symbols) + markers.update() + + +canvas.show() + + +if __name__ == "__main__": + print(__doc__) + app.run() diff --git a/vispy/visuals/filters/__init__.py b/vispy/visuals/filters/__init__.py index 47635dc18d..16920e0492 100644 --- a/vispy/visuals/filters/__init__.py +++ b/vispy/visuals/filters/__init__.py @@ -6,4 +6,5 @@ from .clipper import Clipper # noqa from .color import Alpha, ColorFilter, IsolineFilter, ZColormapFilter # noqa from .picking import PickingFilter # noqa +from .markers import MarkerPickingFilter # noqa from .mesh import TextureFilter, ShadingFilter, InstancedShadingFilter, WireframeFilter, FacePickingFilter # noqa diff --git a/vispy/visuals/filters/markers.py b/vispy/visuals/filters/markers.py new file mode 100644 index 0000000000..249bc7acae --- /dev/null +++ b/vispy/visuals/filters/markers.py @@ -0,0 +1,87 @@ +import numpy as np + +from vispy.gloo import VertexBuffer +from vispy.visuals.shaders import Function +from vispy.visuals.filters import Filter + + +class MarkerPickingFilter(Filter): + """Filter used to color markers by a picking ID. + + Note that the ID color uses the alpha channel, so this may not be used + with blending enabled. + + Examples + -------- + See + `examples/scene/marker_picking.py + `_ + example script. + """ + + def __init__(self): + vfunc = Function("""\ + varying vec4 v_marker_picking_color; + void prepare_marker_picking() { + v_marker_picking_color = $ids; + } + """) + ffunc = Function("""\ + varying vec4 v_marker_picking_color; + void marker_picking_filter() { + if ($enabled != 1) { + return; + } + gl_FragColor = v_marker_picking_color; + } + """) + + self._ids = VertexBuffer(np.zeros((0, 4), dtype=np.float32)) + vfunc['ids'] = self._ids + super().__init__(vcode=vfunc, fcode=ffunc) + self._n_markers = 0 + self.enabled = False + + @property + def enabled(self): + return self._enabled + + @enabled.setter + def enabled(self, e): + self._enabled = bool(e) + self.fshader['enabled'] = int(self._enabled) + self._update_data() + + def _update_data(self): + if not self.attached: + return + + if self._visual._data is None: + n = 0 + else: + n = len(self._visual._data['a_position']) + + # we only care about the number of markers changing + if self._n_markers == n: + return + self._n_markers = n + + # pack the marker ids into a color buffer + ids = np.arange( + 1, n + 1, + dtype=np.uint32 + ).view(np.uint8).reshape(n, 4) + ids = np.divide(ids, 255, dtype=np.float32) + self._ids.set_data(ids) + + def on_data_updated(self, event): + self._update_data() + + def _attach(self, visual): + super()._attach(visual) + visual.events.data_updated.connect(self.on_data_updated) + self._update_data() + + def _detach(self, visual): + visual.events.data_updated.disconnect(self.on_data_updated) + super()._detach(visual) diff --git a/vispy/visuals/markers.py b/vispy/visuals/markers.py index fab069232b..c3f80a1592 100644 --- a/vispy/visuals/markers.py +++ b/vispy/visuals/markers.py @@ -11,6 +11,7 @@ from ..gloo import VertexBuffer from .shaders import Function, Variable from .visual import Visual +from ..util.event import Event _VERTEX_SHADER = """ @@ -573,6 +574,8 @@ def __init__(self, scaling="fixed", alpha=1, antialias=1, spherical=False, blend_func=('src_alpha', 'one_minus_src_alpha')) self._draw_mode = 'points' + self.events.add(data_updated=Event) + if len(kwargs) > 0: self.set_data(**kwargs) @@ -662,6 +665,7 @@ def set_data(self, pos=None, size=10., edge_width=1., edge_width_rel=None, self._vbo.set_data(data) self.shared_program.bind(self._vbo) + self.events.data_updated() self.update() @property From 1dd20942f5afb1473d5ab16ab548088ac23a4257 Mon Sep 17 00:00:00 2001 From: Ashley Anderson Date: Thu, 15 Jun 2023 20:45:45 -0400 Subject: [PATCH 09/14] Refactor primitive picking filter base class --- examples/scene/face_picking.py | 22 ++-- examples/scene/marker_picking.py | 105 +++++++++++------- vispy/visuals/filters/__init__.py | 2 +- vispy/visuals/filters/base_filter.py | 100 +++++++++++++++++ vispy/visuals/filters/markers.py | 70 ++---------- vispy/visuals/filters/mesh.py | 65 ++--------- .../filters/tests/test_face_picking_filter.py | 44 -------- .../tests/test_primitive_picking_filters.py | 70 ++++++++++++ 8 files changed, 268 insertions(+), 210 deletions(-) delete mode 100644 vispy/visuals/filters/tests/test_face_picking_filter.py create mode 100644 vispy/visuals/filters/tests/test_primitive_picking_filters.py diff --git a/examples/scene/face_picking.py b/examples/scene/face_picking.py index 59586f1c77..953320c33b 100644 --- a/examples/scene/face_picking.py +++ b/examples/scene/face_picking.py @@ -14,10 +14,10 @@ * --mesh - Path to a mesh file (OBJ/OBJ.GZ) [optional] Controls: +* p - Toggle face picking view - shows the colors encoding face ID +* r - Clear painted faces * s - Cycle shading modes (None, 'flat', 'smooth') * w - Toggle wireframe -* p - Toggle face picking view - shows the colors encoding face ID -* c - Clear painted faces """ import argparse import itertools @@ -114,7 +114,7 @@ def on_mouse_move(event): mesh.update_gl_state(blend=not face_picking_filter.enabled) # unpack the face index from the color in the center pixel - face_idx = (picking_render.view(np.uint32) - 1)[1, 1] + face_idx = (picking_render.view(np.uint32) - 1)[1, 1, 0] if face_idx > 0 and face_idx < len(face_colors): # this may be less safe, but it's faster than set_data @@ -124,19 +124,21 @@ def on_mouse_move(event): @canvas.events.key_press.connect def on_key_press(event): - if event.key == 'c': + if event.key == 'p': + face_picking_filter.enabled = not face_picking_filter.enabled + mesh.update_gl_state(blend=not face_picking_filter.enabled) + mesh.update() + + if event.key == 'r': mesh.set_data(vertices, faces, face_colors=face_colors) + if event.key == 's': shading_filter.shading = next(shading) mesh.update() - elif event.key == 'w': + + if event.key == 'w': wireframe_filter.enabled = not wireframe_filter.enabled mesh.update() - elif event.key == 'p': - # toggle face picking view - face_picking_filter.enabled = not face_picking_filter.enabled - mesh.update_gl_state(blend=not face_picking_filter.enabled) - mesh.update() canvas.show() diff --git a/examples/scene/marker_picking.py b/examples/scene/marker_picking.py index 465c7671a5..fe6b71c82f 100644 --- a/examples/scene/marker_picking.py +++ b/examples/scene/marker_picking.py @@ -8,16 +8,15 @@ Picking Markers =============== -Demonstrates how to identify (pick) markers. Click markers to change their edge -color. +Demonstrates how to identify (pick) markers. Hover markers to change their +symbol and color. Controls: * p - Toggle picking view - shows the colors encoding marker ID -* s - Cycle marker symbols -* c - Clear picked markers +* r - Reset marker symbols and colors """ -import itertools - +import random +import time import numpy as np from scipy.constants import golden as GOLDEN @@ -25,29 +24,46 @@ from vispy.scene.visuals import Markers from vispy.visuals.filters import MarkerPickingFilter -symbols = itertools.cycle(Markers._symbol_shader_values.keys()) - - -MARKER_SIZE = 0.0125 -EDGE_WDITH = MARKER_SIZE / 10 - canvas = scene.SceneCanvas(keys='interactive', bgcolor='black') view = canvas.central_widget.add_view(camera="panzoom") view.camera.rect = (-1, -1, 2, 2) + +# floret pattern n = 10_000 -radius = np.linspace(0, 0.9, n)**0.6 -theta = np.arange(n) * GOLDEN * np.pi +radius = np.linspace(0, 0.9, n)**0.6 # prevent extreme density at center +theta = np.arange(n) * GOLDEN pos = np.column_stack([radius * np.cos(theta), radius * np.sin(theta)]) -edge_color = np.ones((len(pos), 4), dtype=np.float32) + +COLORS = [ + (1, 0, 0, 1), # red + (1, 0.5, 0, 1), # orange + (1, 1, 0, 1), # yellow + (0, 1, 0, 1), # green + (0, 0, 1, 1), # blue + (0.29, 0, 0.51, 1), # indigo + (0.93, 0.51, 0.93, 1), # violet +] + +colors = np.zeros((n, 4), dtype=np.float32) +colors[:, 0] = 1 # red +colors[:, -1] = 1 # alpha +_colors = colors.copy() + +symbols = list(Markers._symbol_shader_values.keys()) +symbols_ring = dict(zip(symbols, symbols[1:])) +symbols_ring[symbols[-1]] = symbols[0] + +EDGE_COLOR = "white" +MARKER_SIZE = 0.0125 +EDGE_WDITH = MARKER_SIZE / 10 markers = Markers( pos=pos, - edge_color=edge_color, - face_color="red", + edge_color=EDGE_COLOR, + face_color=colors, size=MARKER_SIZE, edge_width=EDGE_WDITH, scaling="scene", - symbol=next(symbols), ) markers.update_gl_state(depth_test=True) view.add(markers) @@ -63,8 +79,17 @@ def on_viewbox_change(event): markers.update_gl_state(blend=not picking_filter.enabled) -@canvas.events.mouse_press.connect -def on_mouse_press(event): +throttle = time.monotonic() + + +@canvas.events.mouse_move.connect +def on_mouse_move(event): + global throttle + # throttle mouse events to 50ms + if time.monotonic() - throttle < 0.05: + return + throttle = time.monotonic() + # adjust the event position for hidpi screens render_size = tuple(d * canvas.pixel_scale for d in canvas.size) x_pos = event.pos[0] * canvas.pixel_scale @@ -84,40 +109,42 @@ def on_mouse_press(event): markers.update_gl_state(blend=not picking_filter.enabled) # unpack the face index from the color in the center pixel - face_idx = (picking_render.view(np.uint32) - 1)[2, 2] + marker_idx = (picking_render.view(np.uint32) - 1)[2, 2, 0] - if face_idx >= 0 and face_idx < len(pos): - edge_color[face_idx] = (0, 1, 0, 1) + if marker_idx >= 0 and marker_idx < len(pos): + new_symbols = list(markers.symbol) + new_symbol = symbols_ring[new_symbols[marker_idx]] + new_symbols[marker_idx] = new_symbol + + colors[marker_idx] = random.choice(COLORS) + colors[marker_idx, -1] = 1 # alpha markers.set_data( pos=pos, - edge_color=edge_color, - face_color="red", + edge_color=EDGE_COLOR, + face_color=colors, size=MARKER_SIZE, edge_width=EDGE_WDITH, - symbol=markers.symbol, + symbol=new_symbols, ) @canvas.events.key_press.connect def on_key_press(event): - if event.key == 'c': - edge_color = np.ones((len(pos), 4), dtype=np.float32) - markers.set_data( - pos=pos, - edge_color=edge_color, - face_color="red", - size=MARKER_SIZE, - edge_width=EDGE_WDITH, - symbol=markers.symbol, - ) if event.key == 'p': # toggle face picking view picking_filter.enabled = not picking_filter.enabled markers.update_gl_state(blend=not picking_filter.enabled) markers.update() - if event.key == 's': - markers.symbol = next(symbols) - markers.update() + if event.key == 'r': + # reset marker symbols + colors = _colors + markers.set_data( + pos=pos, + edge_color=EDGE_COLOR, + face_color=colors, + size=MARKER_SIZE, + edge_width=EDGE_WDITH, + ) canvas.show() diff --git a/vispy/visuals/filters/__init__.py b/vispy/visuals/filters/__init__.py index 16920e0492..b283832038 100644 --- a/vispy/visuals/filters/__init__.py +++ b/vispy/visuals/filters/__init__.py @@ -2,7 +2,7 @@ # Copyright (c) Vispy Development Team. All Rights Reserved. # Distributed under the (new) BSD License. See LICENSE.txt for more info. -from .base_filter import Filter # noqa +from .base_filter import Filter, PrimitivePickingFilter # noqa from .clipper import Clipper # noqa from .color import Alpha, ColorFilter, IsolineFilter, ZColormapFilter # noqa from .picking import PickingFilter # noqa diff --git a/vispy/visuals/filters/base_filter.py b/vispy/visuals/filters/base_filter.py index 333d39c0dd..dc7812c34e 100644 --- a/vispy/visuals/filters/base_filter.py +++ b/vispy/visuals/filters/base_filter.py @@ -2,6 +2,11 @@ # Copyright (c) Vispy Development Team. All Rights Reserved. # Distributed under the (new) BSD License. See LICENSE.txt for more info. +from abc import ABCMeta, abstractmethod + +import numpy as np + +from vispy.gloo import VertexBuffer from ..shaders import Function @@ -120,3 +125,98 @@ def _detach(self, visual): self._attached = False self._visual = None + + +class PrimitivePickingFilter(Filter, metaclass=ABCMeta): + """Abstract base class for Visual-specific filters to implement a + primitive-picking mode. + + Subclasses must (and usually only need to) implement + :py:meth:`_update_id_colors`. + """ + + def __init__(self): + vfunc = Function("""\ + varying vec4 v_marker_picking_color; + void prepare_marker_picking() { + v_marker_picking_color = $ids; + } + """) + ffunc = Function("""\ + varying vec4 v_marker_picking_color; + void marker_picking_filter() { + if ($enabled != 1) { + return; + } + gl_FragColor = v_marker_picking_color; + } + """) + + self._id_colors = VertexBuffer(np.zeros((0, 4), dtype=np.float32)) + vfunc['ids'] = self._id_colors + self._n_primitives = 0 + super().__init__(vcode=vfunc, fcode=ffunc) + self.enabled = False + + @abstractmethod + def _update_id_colors(self): + """Calculate the colors encoding the picking IDs for the visual. + + Generally, this method should be implemented to: + 1. Calculate the number of primitives in the visual, stored in + `self._n_primitives`. + 2. Pack the picking IDs into a 4-component float array (RGBA + VertexBuffer), stored in `self._id_colors`. + + As an example of packing IDs into a VertexBuffer, consider the following + implementation for the Mesh visual: + ``` + # calculate the number of primitives + n_faces = len(self._visual.mesh_data.get_faces()) + self._n_primitives = n_faces + + # assign 32 bit IDs to each primitive + # starting from 1 reserves (0, 0, 0, 0) for the background + ids = np.arange(1, n_faces + 1,dtype=np.uint32) + + # reinterpret as 8-bit RGBA and normalize colors into floats + id_colors = np.divide( + ids.view(np.uint8).reshape(n_faces, 4), + 255, + dtype=np.float32 + ) + + # store the colors in a VertexBuffer, repeating each color 3 times + # for each vertex in each triangle + self._id_colors.set_data(np.repeat(idid_colors, 3, axis=0)) + ``` + + For performance, you may want to optimize this method to only update + the IDs when the data meaningfully changes - for example when the + number of primitives changes. + """ + raise NotImplementedError(self) + + def _on_data_updated(self, event=None): + if not self.attached: + return + self._update_id_colors() + + @property + def enabled(self): + return self._enabled + + @enabled.setter + def enabled(self, e): + self._enabled = bool(e) + self.fshader['enabled'] = int(self._enabled) + self._on_data_updated() + + def _attach(self, visual): + super()._attach(visual) + visual.events.data_updated.connect(self._on_data_updated) + self._on_data_updated() + + def _detach(self, visual): + visual.events.data_updated.disconnect(self._on_data_updated) + super()._detach(visual) diff --git a/vispy/visuals/filters/markers.py b/vispy/visuals/filters/markers.py index 249bc7acae..ec040adfdb 100644 --- a/vispy/visuals/filters/markers.py +++ b/vispy/visuals/filters/markers.py @@ -1,11 +1,9 @@ import numpy as np -from vispy.gloo import VertexBuffer -from vispy.visuals.shaders import Function -from vispy.visuals.filters import Filter +from vispy.visuals.filters import PrimitivePickingFilter -class MarkerPickingFilter(Filter): +class MarkerPickingFilter(PrimitivePickingFilter): """Filter used to color markers by a picking ID. Note that the ID color uses the alpha channel, so this may not be used @@ -19,69 +17,21 @@ class MarkerPickingFilter(Filter): example script. """ - def __init__(self): - vfunc = Function("""\ - varying vec4 v_marker_picking_color; - void prepare_marker_picking() { - v_marker_picking_color = $ids; - } - """) - ffunc = Function("""\ - varying vec4 v_marker_picking_color; - void marker_picking_filter() { - if ($enabled != 1) { - return; - } - gl_FragColor = v_marker_picking_color; - } - """) - - self._ids = VertexBuffer(np.zeros((0, 4), dtype=np.float32)) - vfunc['ids'] = self._ids - super().__init__(vcode=vfunc, fcode=ffunc) - self._n_markers = 0 - self.enabled = False - - @property - def enabled(self): - return self._enabled - - @enabled.setter - def enabled(self, e): - self._enabled = bool(e) - self.fshader['enabled'] = int(self._enabled) - self._update_data() - - def _update_data(self): - if not self.attached: - return - + def _update_id_colors(self): if self._visual._data is None: - n = 0 + n_markers = 0 else: - n = len(self._visual._data['a_position']) + n_markers = len(self._visual._data['a_position']) # we only care about the number of markers changing - if self._n_markers == n: + if self._n_primitives == n_markers: return - self._n_markers = n + self._n_primitives = n_markers # pack the marker ids into a color buffer ids = np.arange( - 1, n + 1, + 1, n_markers + 1, dtype=np.uint32 - ).view(np.uint8).reshape(n, 4) + ).view(np.uint8).reshape(n_markers, 4) ids = np.divide(ids, 255, dtype=np.float32) - self._ids.set_data(ids) - - def on_data_updated(self, event): - self._update_data() - - def _attach(self, visual): - super()._attach(visual) - visual.events.data_updated.connect(self.on_data_updated) - self._update_data() - - def _detach(self, visual): - visual.events.data_updated.disconnect(self.on_data_updated) - super()._detach(visual) + self._id_colors.set_data(ids) diff --git a/vispy/visuals/filters/mesh.py b/vispy/visuals/filters/mesh.py index ffbcf195bd..8c319fb1d5 100644 --- a/vispy/visuals/filters/mesh.py +++ b/vispy/visuals/filters/mesh.py @@ -7,7 +7,7 @@ from vispy.gloo import Texture2D, VertexBuffer from vispy.visuals.shaders import Function, Varying -from vispy.visuals.filters import Filter +from vispy.visuals.filters import Filter, PrimitivePickingFilter from ...color import Color @@ -756,19 +756,19 @@ def _update_data(self): bc = np.tile(bc[None, ...], (n_faces, 1, 1)) self._bc.set_data(bc, convert=True) - def on_mesh_data_updated(self, event): + def on_data_updated(self, event): self._update_data() def _attach(self, visual): super()._attach(visual) - visual.events.data_updated.connect(self.on_mesh_data_updated) + visual.events.data_updated.connect(self.on_data_updated) def _detach(self, visual): - visual.events.data_updated.disconnect(self.on_mesh_data_updated) + visual.events.data_updated.disconnect(self.on_data_updated) super()._detach(visual) -class FacePickingFilter(Filter): +class FacePickingFilter(PrimitivePickingFilter): """Filter used to color mesh faces by a picking ID. Note that the ID color uses the alpha channel, so this may not be used @@ -782,51 +782,16 @@ class FacePickingFilter(Filter): example script. """ - def __init__(self): - vfunc = Function("""\ - varying vec4 v_face_picking_color; - void prepare_face_picking() { - v_face_picking_color = $ids; - } - """) - ffunc = Function("""\ - varying vec4 v_face_picking_color; - void face_picking_filter() { - if ($enabled != 1) { - return; - } - gl_FragColor = v_face_picking_color; - } - """) - - self._ids = VertexBuffer(np.zeros((0, 4), dtype=np.float32)) - vfunc['ids'] = self._ids - super().__init__(vcode=vfunc, fcode=ffunc) - self._n_faces = 0 - self.enabled = False - - @property - def enabled(self): - return self._enabled - - @enabled.setter - def enabled(self, e): - self._enabled = bool(e) - self.fshader['enabled'] = int(self._enabled) - self._update_data() - - def _update_data(self): - if not self.attached: - return + def _update_id_colors(self): if self._visual.mesh_data.is_empty(): n_faces = 0 else: n_faces = len(self._visual.mesh_data.get_faces()) # we only care about the number of faces changing - if self._n_faces == n_faces: + if self._n_primitives == n_faces: return - self._n_faces = n_faces + self._n_primitives = n_faces # pack the face ids into a color buffer # TODO: consider doing the bit-packing in the shader @@ -835,16 +800,4 @@ def _update_data(self): dtype=np.uint32 ).view(np.uint8).reshape(n_faces, 4) ids = np.divide(ids, 255, dtype=np.float32) - self._ids.set_data(np.repeat(ids, 3, axis=0)) - - def on_mesh_data_updated(self, event): - self._update_data() - - def _attach(self, visual): - super()._attach(visual) - visual.events.data_updated.connect(self.on_mesh_data_updated) - self._update_data() - - def _detach(self, visual): - visual.events.data_updated.disconnect(self.on_mesh_data_updated) - super()._detach(visual) + self._id_colors.set_data(np.repeat(ids, 3, axis=0)) diff --git a/vispy/visuals/filters/tests/test_face_picking_filter.py b/vispy/visuals/filters/tests/test_face_picking_filter.py deleted file mode 100644 index 7b5a81752e..0000000000 --- a/vispy/visuals/filters/tests/test_face_picking_filter.py +++ /dev/null @@ -1,44 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) Vispy Development Team. All Rights Reserved. -# Distributed under the (new) BSD License. See LICENSE.txt for more info. -import numpy as np - -from vispy.geometry import create_plane -from vispy.scene.visuals import Mesh -from vispy.testing import requires_application, TestingCanvas -from vispy.visuals.filters import FacePickingFilter - - -def test_empty_mesh_face_picking(): - mesh = Mesh() - filter = FacePickingFilter() - mesh.attach(filter) - filter.enabled = True - - -@requires_application() -def test_face_picking(): - vertices, faces, _ = create_plane(125, 125) - vertices = vertices["position"] - vertices[:, :2] += 125 / 2 - mesh = Mesh(vertices=vertices, faces=faces) - filter = FacePickingFilter() - mesh.attach(filter) - - with TestingCanvas(size=(125, 125)) as c: - view = c.central_widget.add_view() - view.add(mesh) - filter.enabled = True - mesh.update_gl_state(blend=False) - picking_render = c.render(bgcolor=(0, 0, 0, 0), alpha=True) - - # the plane is made up of two triangles and nearly fills the view - # pick one point on each triangle - assert np.allclose( - picking_render[125 // 2, 125 // 4], - (1, 0, 0, 0), - ) - assert np.allclose( - picking_render[125 // 2, 3 * 125 // 4], - (2, 0, 0, 0), - ) diff --git a/vispy/visuals/filters/tests/test_primitive_picking_filters.py b/vispy/visuals/filters/tests/test_primitive_picking_filters.py new file mode 100644 index 0000000000..36e64bb54d --- /dev/null +++ b/vispy/visuals/filters/tests/test_primitive_picking_filters.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Vispy Development Team. All Rights Reserved. +# Distributed under the (new) BSD License. See LICENSE.txt for more info. +import numpy as np + +from vispy.geometry import create_plane +from vispy.scene.visuals import Markers, Mesh +from vispy.testing import requires_application, TestingCanvas +from vispy.visuals.filters import FacePickingFilter, MarkerPickingFilter + + +def test_empty_mesh_face_picking(): + mesh = Mesh() + filter = FacePickingFilter() + mesh.attach(filter) + filter.enabled = True + + +@requires_application() +def test_mesh_face_picking(): + vertices, faces, _ = create_plane(125, 125) + vertices = vertices["position"] + vertices[:, :2] += 125 / 2 + mesh = Mesh(vertices=vertices, faces=faces) + filter = FacePickingFilter() + mesh.attach(filter) + + with TestingCanvas(size=(125, 125)) as c: + view = c.central_widget.add_view() + view.add(mesh) + filter.enabled = True + mesh.update_gl_state(blend=False) + picking_render = c.render(bgcolor=(0, 0, 0, 0), alpha=True) + + # unpack the IDs + ids = picking_render.view(np.uint32) + # the plane is made up of two triangles and nearly fills the view + # pick one point on each triangle + assert ids[125 // 2, 125 // 4] == 1 + assert ids[125 // 2, 3 * 125 // 4] == 2 + + +def test_empty_markers_picking(): + markers = Markers() + filter = MarkerPickingFilter() + markers.attach(filter) + filter.enabled = True + + +@requires_application() +def test_markers_picking(): + markers = Markers( + pos=np.array([[-0.5, -0.5], [0.5, 0.5]]), + size=5, + ) + filter = MarkerPickingFilter() + markers.attach(filter) + + with TestingCanvas(size=(125, 125)) as c: + view = c.central_widget.add_view(camera="panzoom") + view.camera.rect = (-1, -1, 2, 2) + view.add(markers) + + filter.enabled = True + markers.update_gl_state(blend=False) + picking_render = c.render(bgcolor=(0, 0, 0, 0), alpha=True) + ids = picking_render.view(np.uint32) + + assert ids[3 * 125 // 4, 125 // 4] == 1 + assert ids[125 // 4, 3 * 125 // 4] == 2 From 6eadcb80e84a17e8fd48ed0d7553660e32ab3756 Mon Sep 17 00:00:00 2001 From: Ashley Anderson Date: Fri, 16 Jun 2023 09:34:04 -0400 Subject: [PATCH 10/14] Fix marker picking example --- examples/scene/marker_picking.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/scene/marker_picking.py b/examples/scene/marker_picking.py index fe6b71c82f..6a2a320069 100644 --- a/examples/scene/marker_picking.py +++ b/examples/scene/marker_picking.py @@ -117,7 +117,6 @@ def on_mouse_move(event): new_symbols[marker_idx] = new_symbol colors[marker_idx] = random.choice(COLORS) - colors[marker_idx, -1] = 1 # alpha markers.set_data( pos=pos, edge_color=EDGE_COLOR, @@ -130,6 +129,7 @@ def on_mouse_move(event): @canvas.events.key_press.connect def on_key_press(event): + global colors if event.key == 'p': # toggle face picking view picking_filter.enabled = not picking_filter.enabled @@ -137,7 +137,7 @@ def on_key_press(event): markers.update() if event.key == 'r': # reset marker symbols - colors = _colors + colors = _colors.copy() markers.set_data( pos=pos, edge_color=EDGE_COLOR, From 6034cbaba5174598ddeda2b72bc529f8a19748a7 Mon Sep 17 00:00:00 2001 From: Ashley Anderson Date: Wed, 28 Jun 2023 14:27:10 -0400 Subject: [PATCH 11/14] Set default fpos so primitive picking is just before visual picking --- vispy/visuals/filters/base_filter.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/vispy/visuals/filters/base_filter.py b/vispy/visuals/filters/base_filter.py index dc7812c34e..78f6c4e25c 100644 --- a/vispy/visuals/filters/base_filter.py +++ b/vispy/visuals/filters/base_filter.py @@ -135,7 +135,9 @@ class PrimitivePickingFilter(Filter, metaclass=ABCMeta): :py:meth:`_update_id_colors`. """ - def __init__(self): + def __init__(self, fpos=9): + # fpos is set to 9 by default to put it near the end, but before the + # default PickingFilter vfunc = Function("""\ varying vec4 v_marker_picking_color; void prepare_marker_picking() { @@ -155,7 +157,8 @@ def __init__(self): self._id_colors = VertexBuffer(np.zeros((0, 4), dtype=np.float32)) vfunc['ids'] = self._id_colors self._n_primitives = 0 - super().__init__(vcode=vfunc, fcode=ffunc) + # set fpos to a very big number to make sure this is applied last + super().__init__(vcode=vfunc, fcode=ffunc, fpos=1e9) self.enabled = False @abstractmethod From 36db4fc96dbffd8293f6583e43595292542e3079 Mon Sep 17 00:00:00 2001 From: Ashley Anderson Date: Wed, 28 Jun 2023 16:48:30 -0400 Subject: [PATCH 12/14] Add a convenience method for packing IDs into RGBA colors --- vispy/visuals/filters/base_filter.py | 18 ++++++++++++++++-- vispy/visuals/filters/markers.py | 9 +++------ vispy/visuals/filters/mesh.py | 12 ++++-------- 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/vispy/visuals/filters/base_filter.py b/vispy/visuals/filters/base_filter.py index 78f6c4e25c..e81e4c0c72 100644 --- a/vispy/visuals/filters/base_filter.py +++ b/vispy/visuals/filters/base_filter.py @@ -150,6 +150,9 @@ def __init__(self, fpos=9): if ($enabled != 1) { return; } + if( gl_FragColor.a == 0.0 ) { + discard; + } gl_FragColor = v_marker_picking_color; } """) @@ -157,8 +160,7 @@ def __init__(self, fpos=9): self._id_colors = VertexBuffer(np.zeros((0, 4), dtype=np.float32)) vfunc['ids'] = self._id_colors self._n_primitives = 0 - # set fpos to a very big number to make sure this is applied last - super().__init__(vcode=vfunc, fcode=ffunc, fpos=1e9) + super().__init__(vcode=vfunc, fcode=ffunc, fpos=fpos) self.enabled = False @abstractmethod @@ -200,6 +202,18 @@ def _update_id_colors(self): """ raise NotImplementedError(self) + @staticmethod + def _pack_ids_into_rgba(ids): + """Pack an array of uint32 primitive ids into float32 RGBA colors.""" + if ids.dtype != np.uint32: + raise ValueError(f"ids must be uint32, got {ids.dtype}") + + return np.divide( + ids.view(np.uint8).reshape(-1, 4), + 255, + dtype=np.float32 + ) + def _on_data_updated(self, event=None): if not self.attached: return diff --git a/vispy/visuals/filters/markers.py b/vispy/visuals/filters/markers.py index ec040adfdb..8f7f914b67 100644 --- a/vispy/visuals/filters/markers.py +++ b/vispy/visuals/filters/markers.py @@ -29,9 +29,6 @@ def _update_id_colors(self): self._n_primitives = n_markers # pack the marker ids into a color buffer - ids = np.arange( - 1, n_markers + 1, - dtype=np.uint32 - ).view(np.uint8).reshape(n_markers, 4) - ids = np.divide(ids, 255, dtype=np.float32) - self._id_colors.set_data(ids) + ids = np.arange(1, n_markers + 1, dtype=np.uint32) + id_colors = self._pack_ids_into_rgba(ids) + self._id_colors.set_data(id_colors) diff --git a/vispy/visuals/filters/mesh.py b/vispy/visuals/filters/mesh.py index 8c319fb1d5..a032aec19e 100644 --- a/vispy/visuals/filters/mesh.py +++ b/vispy/visuals/filters/mesh.py @@ -793,11 +793,7 @@ def _update_id_colors(self): return self._n_primitives = n_faces - # pack the face ids into a color buffer - # TODO: consider doing the bit-packing in the shader - ids = np.arange( - 1, n_faces + 1, - dtype=np.uint32 - ).view(np.uint8).reshape(n_faces, 4) - ids = np.divide(ids, 255, dtype=np.float32) - self._id_colors.set_data(np.repeat(ids, 3, axis=0)) + ids = np.arange(1, n_faces + 1, dtype=np.uint32) + ids = np.repeat(ids, 3, axis=0) # repeat id for each vertex + id_colors = self._pack_ids_into_rgba(ids) + self._id_colors.set_data(id_colors) From 6022b9831f84e15236230ffd89690f2d9b48d66d Mon Sep 17 00:00:00 2001 From: Ashley Anderson Date: Thu, 29 Jun 2023 10:10:54 -0400 Subject: [PATCH 13/14] Refactor primitive picking ABC, add 'discard_transparent' option --- vispy/visuals/filters/base_filter.py | 77 +++++++++++++++------------- vispy/visuals/filters/markers.py | 7 +-- vispy/visuals/filters/mesh.py | 7 ++- 3 files changed, 45 insertions(+), 46 deletions(-) diff --git a/vispy/visuals/filters/base_filter.py b/vispy/visuals/filters/base_filter.py index e81e4c0c72..d0cf38277e 100644 --- a/vispy/visuals/filters/base_filter.py +++ b/vispy/visuals/filters/base_filter.py @@ -132,10 +132,10 @@ class PrimitivePickingFilter(Filter, metaclass=ABCMeta): primitive-picking mode. Subclasses must (and usually only need to) implement - :py:meth:`_update_id_colors`. + :py:meth:`_get_picking_ids`. """ - def __init__(self, fpos=9): + def __init__(self, fpos=9, *, discard_transparent=False): # fpos is set to 9 by default to put it near the end, but before the # default PickingFilter vfunc = Function("""\ @@ -147,10 +147,10 @@ def __init__(self, fpos=9): ffunc = Function("""\ varying vec4 v_marker_picking_color; void marker_picking_filter() { - if ($enabled != 1) { + if ( $enabled != 1 ) { return; } - if( gl_FragColor.a == 0.0 ) { + if ( $discard_transparent == 1 && gl_FragColor.a == 0.0 ) { discard; } gl_FragColor = v_marker_picking_color; @@ -162,46 +162,40 @@ def __init__(self, fpos=9): self._n_primitives = 0 super().__init__(vcode=vfunc, fcode=ffunc, fpos=fpos) self.enabled = False + self.discard_transparent = discard_transparent @abstractmethod - def _update_id_colors(self): - """Calculate the colors encoding the picking IDs for the visual. + def _get_picking_ids(self): + """Return a 1D array of picking IDs for the vertices in the visual. Generally, this method should be implemented to: - 1. Calculate the number of primitives in the visual, stored in - `self._n_primitives`. - 2. Pack the picking IDs into a 4-component float array (RGBA - VertexBuffer), stored in `self._id_colors`. - - As an example of packing IDs into a VertexBuffer, consider the following - implementation for the Mesh visual: - ``` - # calculate the number of primitives - n_faces = len(self._visual.mesh_data.get_faces()) - self._n_primitives = n_faces - - # assign 32 bit IDs to each primitive - # starting from 1 reserves (0, 0, 0, 0) for the background - ids = np.arange(1, n_faces + 1,dtype=np.uint32) - - # reinterpret as 8-bit RGBA and normalize colors into floats - id_colors = np.divide( - ids.view(np.uint8).reshape(n_faces, 4), - 255, - dtype=np.float32 - ) - - # store the colors in a VertexBuffer, repeating each color 3 times - # for each vertex in each triangle - self._id_colors.set_data(np.repeat(idid_colors, 3, axis=0)) - ``` - - For performance, you may want to optimize this method to only update - the IDs when the data meaningfully changes - for example when the - number of primitives changes. + 1. Calculate the number of primitives in the visual (may be + persisted in `self._n_primitives`). + 2. Calculate a range of picking ids for each primitive in the + visual. IDs should start from 1, reserving 0 for the background. If + primitives comprise multiple vertices (triangles), ids may need to + be repeated. + + The return value should be an array of uint32 with shape + (num_vertices,). + + If no change to the picking IDs is needed (for example, the number of + primitives has not changed), this method should return `None`. """ raise NotImplementedError(self) + def _update_id_colors(self): + """Calculate the colors encoding the picking IDs for the visual. + + For performance, this method will not update the id colors VertexBuffer + if :py:meth:`_get_picking_ids` returns `None`. + """ + # this should remain untouched + ids = self._get_picking_ids() + if ids is not None: + id_colors = self._pack_ids_into_rgba(ids) + self._id_colors.set_data(id_colors) + @staticmethod def _pack_ids_into_rgba(ids): """Pack an array of uint32 primitive ids into float32 RGBA colors.""" @@ -229,6 +223,15 @@ def enabled(self, e): self.fshader['enabled'] = int(self._enabled) self._on_data_updated() + @property + def discard_transparent(self): + return self._discard_transparent + + @discard_transparent.setter + def discard_transparent(self, d): + self._discard_transparent = bool(d) + self.fshader['discard_transparent'] = int(self._discard_transparent) + def _attach(self, visual): super()._attach(visual) visual.events.data_updated.connect(self._on_data_updated) diff --git a/vispy/visuals/filters/markers.py b/vispy/visuals/filters/markers.py index 8f7f914b67..3fbf102b16 100644 --- a/vispy/visuals/filters/markers.py +++ b/vispy/visuals/filters/markers.py @@ -17,7 +17,7 @@ class MarkerPickingFilter(PrimitivePickingFilter): example script. """ - def _update_id_colors(self): + def _get_picking_ids(self): if self._visual._data is None: n_markers = 0 else: @@ -28,7 +28,4 @@ def _update_id_colors(self): return self._n_primitives = n_markers - # pack the marker ids into a color buffer - ids = np.arange(1, n_markers + 1, dtype=np.uint32) - id_colors = self._pack_ids_into_rgba(ids) - self._id_colors.set_data(id_colors) + return np.arange(1, n_markers + 1, dtype=np.uint32) diff --git a/vispy/visuals/filters/mesh.py b/vispy/visuals/filters/mesh.py index a032aec19e..dab740a70d 100644 --- a/vispy/visuals/filters/mesh.py +++ b/vispy/visuals/filters/mesh.py @@ -782,7 +782,7 @@ class FacePickingFilter(PrimitivePickingFilter): example script. """ - def _update_id_colors(self): + def _get_picking_ids(self): if self._visual.mesh_data.is_empty(): n_faces = 0 else: @@ -790,10 +790,9 @@ def _update_id_colors(self): # we only care about the number of faces changing if self._n_primitives == n_faces: - return + return None self._n_primitives = n_faces ids = np.arange(1, n_faces + 1, dtype=np.uint32) ids = np.repeat(ids, 3, axis=0) # repeat id for each vertex - id_colors = self._pack_ids_into_rgba(ids) - self._id_colors.set_data(id_colors) + return ids From 5a1da934326531fd8ba7eefdeb59a7d6c94f2649 Mon Sep 17 00:00:00 2001 From: Ashley Anderson Date: Tue, 11 Jul 2023 15:17:21 -0400 Subject: [PATCH 14/14] Use sphinx gallery references instead of GitHub links for examples --- vispy/visuals/filters/markers.py | 5 +---- vispy/visuals/filters/mesh.py | 5 +---- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/vispy/visuals/filters/markers.py b/vispy/visuals/filters/markers.py index 3fbf102b16..34b8b53824 100644 --- a/vispy/visuals/filters/markers.py +++ b/vispy/visuals/filters/markers.py @@ -11,10 +11,7 @@ class MarkerPickingFilter(PrimitivePickingFilter): Examples -------- - See - `examples/scene/marker_picking.py - `_ - example script. + :ref:`sphx_glr_gallery_scene_marker_picking.py` """ def _get_picking_ids(self): diff --git a/vispy/visuals/filters/mesh.py b/vispy/visuals/filters/mesh.py index dab740a70d..711d09e54e 100644 --- a/vispy/visuals/filters/mesh.py +++ b/vispy/visuals/filters/mesh.py @@ -776,10 +776,7 @@ class FacePickingFilter(PrimitivePickingFilter): Examples -------- - See - `examples/scene/face_picking.py - `_ - example script. + :ref:`sphx_glr_gallery_scene_face_picking.py` """ def _get_picking_ids(self):