Hey guys, I’m struggling to implement a rectangular selection of nodes on my app, I found this solution but I think this is too much code nowadays. Anyone does know a simpler solution?
Thanks in advance
Hey guys, I’m struggling to implement a rectangular selection of nodes on my app, I found this solution but I think this is too much code nowadays. Anyone does know a simpler solution?
Thanks in advance
… What makes you think that it’s too much code? (I would say that it’s a fairly small amount.)
Looking over it, you might be able to reduce the calculations of the largest- and smallest- points down to a few lines via the “max” and “min” methods, but it doesn’t seem all that worth the bother, I would say.
I thought since is an old post that would exist a leaner solution.
Not necessarily at all.
In any case, it’s a fairly short solution (I’d say), so I’d suggest going with it, unless someone else posts to provide another.
In fact it could be argued that the referenced code is too simple.
What I mean is that region-selection of objects is not an easy problem to solve, especially when you want it to be fairly precise.
Consider a window frame model that has its origin (i.e. the position of its node) at its centre. If you drag a rectangle over a corner, that code will not select the frame, since the origin is outside of the rectangle. Conversely, if you had a selected window frame and wanted to deselect it by region-selecting another object that you see through the window, your new selection might still include that frame (because its centre would be included in the rectangle).
It can be very frustrating to draw a big rectangle around a model, only to find that you can’t select it, simply because its origin is currently outside of the camera view.
If you’re interested, below is some code that allows pixel-perfect region-selection. Yes, it’s very complicated indeed, but you don’t need to worry about that; all you have to do is import RegionSelector
from the region_selection
module and add all of your selectable objects to it. You can then draw a rectangle using the RegionSelector.start_region_draw
and RegionSelector.end_region_draw
methods. The latter will then call the functions you provided to handle the logic for selection and deselection. That’s all there’s to it.
You do need support for GLSL version 4.20, though (the code uses a shader).
region_selection.py
from panda3d.core import *
VERT_SHADER = """
#version 420
uniform mat4 p3d_ModelViewProjectionMatrix;
uniform int region_sel_index;
in vec4 p3d_Vertex;
flat out int oindex;
void main() {
gl_Position = p3d_ModelViewProjectionMatrix * p3d_Vertex;
oindex = region_sel_index;
}
"""
FRAG_SHADER = """
#version 420
layout(r32i) uniform iimageBuffer selections;
flat in int oindex;
void main() {
// Write 1 to the location corresponding to the custom index
imageAtomicOr(selections, (oindex >> 5), 1 << (oindex & 31));
}
"""
def create_rectangle():
vertex_format = GeomVertexFormat.get_v3()
vertex_data = GeomVertexData("rectangle_data", vertex_format, Geom.UH_static)
vertex_data.set_num_rows(4)
prim = GeomLines(Geom.UH_static)
prim.add_vertices(0, 1)
prim.add_vertices(1, 2)
prim.add_vertices(2, 3)
prim.add_vertices(3, 0)
geom = Geom(vertex_data)
geom.add_primitive(prim)
node = GeomNode("rectangle_geom_node")
node.add_geom(geom)
return node
class RegionSelectables:
used_indices = BitArray()
obj_indices = {}
objs = {}
@classmethod
def add(cls, obj):
if obj in cls.objs.values():
return
index = cls.used_indices.get_lowest_off_bit()
cls.used_indices.set_bit(index)
cls.obj_indices[obj] = index
cls.objs[index] = obj
obj.set_shader_input("region_sel_index", index)
@classmethod
def remove(cls, obj):
if obj not in cls.objs.values():
return
index = cls.obj_indices[obj]
del cls.obj_indices[obj]
del cls.objs[index]
cls.used_indices.clear_bit(index)
obj.clear_shader_input("region_sel_index")
@classmethod
def clear(cls):
for obj in cls.objs.values():
obj.clear_shader_input("region_sel_index")
cls.used_indices.clear()
cls.obj_indices.clear()
cls.objs.clear()
class RegionSelector:
def __init__(self, showbase, deselect_func, select_func):
self.showbase = showbase
self.deselect_func = deselect_func
self.select_func = select_func
cam = Camera("region_selection_cam")
cam.active = False
self.region_sel_cam = showbase.camera.attach_new_node(cam)
self.selection_cam_mask = BitMask32.bit(10)
self.selection_rectangle = showbase.render2d.attach_new_node(create_rectangle())
self.selection_rectangle.set_color((1., 1., 0., 1.))
self.selection_rectangle.hide()
self.mouse_start_pos = (0., 0.)
self.mouse_end_pos = (0., 0.)
def add(self, obj):
RegionSelectables.add(obj)
def remove(self, obj):
RegionSelectables.remove(obj)
def clear(self):
RegionSelectables.clear()
def start_region_draw(self):
if not self.showbase.mouseWatcherNode.has_mouse():
return
screen_pos = self.showbase.mouseWatcherNode.get_mouse()
self.mouse_start_pos = (screen_pos.x, screen_pos.y)
self.selection_rectangle.show()
self.showbase.task_mgr.add(self.__draw_region, "draw_region")
def __draw_region(self, task):
if not self.showbase.mouseWatcherNode.has_mouse():
return task.cont
screen_pos = self.showbase.mouseWatcherNode.get_mouse()
x1, z1 = self.mouse_start_pos
x2, z2 = self.mouse_end_pos = (screen_pos.x, screen_pos.y)
geom = self.selection_rectangle.node().modify_geom(0)
vertex_data = geom.modify_vertex_data()
pos_writer = GeomVertexWriter(vertex_data, "vertex")
pos_writer.set_row(0)
pos_writer.set_data3(x1, 0., z1)
pos_writer.set_row(1)
pos_writer.set_data3(x1, 0., z2)
pos_writer.set_row(2)
pos_writer.set_data3(x2, 0., z2)
pos_writer.set_row(3)
pos_writer.set_data3(x2, 0., z1)
return task.cont
def end_region_draw(self):
self.showbase.task_mgr.remove("draw_region")
self.selection_rectangle.hide()
x1, y1 = self.mouse_start_pos
x2, y2 = self.mouse_end_pos
x1 = max(0., min(1., .5 + x1 * .5))
y1 = max(0., min(1., .5 + y1 * .5))
x2 = max(0., min(1., .5 + x2 * .5))
y2 = max(0., min(1., .5 + y2 * .5))
l, r = min(x1, x2), max(x1, x2)
b, t = min(y1, y2), max(y1, y2)
self.__region_select((l, r, b, t))
def __update_selection(self, obj_indices):
selected_objs = {RegionSelectables.objs[i] for i in obj_indices}
deselected_objs = set(RegionSelectables.objs.values()) - selected_objs
self.deselect_func(deselected_objs)
self.select_func(selected_objs)
def __region_select(self, frame):
lens = self.showbase.camLens
w, h = lens.film_size
l, r, b, t = frame
# compute film size and offset
w_f = (r - l) * w
h_f = (t - b) * h
x_f = ((r + l) * .5 - .5) * w
y_f = ((t + b) * .5 - .5) * h
win_props = self.showbase.win.properties
w, h = win_props.size # window resolution in pixels
# compute buffer size
w_b = int(round((r - l) * w))
h_b = int(round((t - b) * h))
bfr_size = (w_b, h_b)
if min(bfr_size) < 2:
self.__update_selection([])
return
def get_off_axis_lens(film_size):
lens = self.showbase.camLens
focal_len = lens.focal_length
lens = lens.make_copy()
lens.film_size = film_size
lens.film_offset = (x_f, y_f)
lens.focal_length = focal_len
return lens
lens = get_off_axis_lens((w_f, h_f))
cam_np = self.region_sel_cam
cam = cam_np.node()
cam.set_lens(lens)
cam.camera_mask = self.selection_cam_mask
tex_buffer = self.showbase.win.make_texture_buffer("tex_buffer", w_b, h_b)
cam.active = True
self.showbase.make_camera(tex_buffer, useCamera=cam_np)
obj_count = len(RegionSelectables.objs)
tex = Texture()
tex.setup_1d_texture(obj_count, Texture.T_int, Texture.F_r32i)
tex.clear_color = (0., 0., 0., 0.)
shader = Shader.make(Shader.SL_GLSL, VERT_SHADER, FRAG_SHADER)
state_np = NodePath("state_np")
state_np.set_shader(shader, 1)
state_np.set_shader_input("selections", tex, read=False, write=True)
state = state_np.get_state()
self.region_sel_cam.node().initial_state = state
self.showbase.graphics_engine.render_frame()
gsg = self.showbase.win.get_gsg()
if self.showbase.graphics_engine.extract_texture_data(tex, gsg):
texels = memoryview(tex.get_ram_image()).cast("I")
visible_obj_indices = []
for i, mask in enumerate(texels):
for j in range(32):
if mask & (1 << j):
index = 32 * i + j
visible_obj_indices.append(index)
# print("\nVisible objects:", visible_obj_indices)
self.__update_selection(visible_obj_indices)
else:
# print("\nNo objects are in view.")
self.__update_selection([])
state_np.clear_attrib(ShaderAttrib)
self.showbase.graphics_engine.remove_window(tex_buffer)
cam.active = False
Main script example:
from panda3d.core import *
from direct.showbase.ShowBase import ShowBase
from region_selection import RegionSelector
class MyApp(ShowBase):
def __init__(self):
ShowBase.__init__(self)
self.disable_mouse()
self.camera.set_pos(0., -50., 2.)
self.camera.look_at(0., 0., 0.)
self.selection_color = (1., 0., 0., 1.)
self.selector = RegionSelector(self, self.deselect_objects, self.select_objects)
for i in range(10):
smiley = self.loader.load_model("smiley")
smiley.reparent_to(self.render)
smiley.set_x(2. * i - 10.)
self.selector.add(smiley)
self.accept("mouse1", self.selector.start_region_draw)
self.accept("mouse1-up", self.selector.end_region_draw)
def deselect_objects(self, objs):
# remove tint from deselected objects
for obj in objs:
obj.clear_color_scale()
def select_objects(self, objs):
# give the selected objects a specific tint
for obj in objs:
obj.set_color_scale(self.selection_color)
app = MyApp()
app.run()
This code could actually be extended to allow circle/ellipse-selection, fence-selection, lasso-selection, paint-selection and even include the option to limit selection to objects completely enclosed by the selection shape. You can already try these things out in my Panda3D Studio project if you’re interested. Perhaps I will add the relevant code to this example and create a GitHub repository for it – if and when I find the time, that is.
In case you’re interested, I’ve extracted the needed code from my project into a stand-alone module, which you can find out more about here.
Thank you, I’ll take a look.