Making it work at any angle.

Hey everyone!
I am working on finishing up my RectTracker class. A rect tracker is a Python class that draws a rectangle on the screen. It then tracks the position of that rectangle and has a function for you to test if anything is inside of it. An example of this is when you drag your mouse across your desktop and the rectangle is formed.

Here is my problem, if you go into the attached blend, and play it. It will work fine, it will select objects, it will draw the rectangle as expected, and do everything you would think it should do. This is only in the top view.

Now try rotating the view. If you play it now, it probably won’t work right anymore. Any ideas?

I really just need help making it work at any angle. You probably just have to edit the draw function. The hitTest function should work at any angle.

EDIT: A clearer description of exactly what I want:
I want to be able to rotate the camera to any orientation at any point, and still be able to draw a rectangle perpendicular to the current camera angle.

Here are some example images at different angles so you can see what I mean:




-Sunjay03

Attachments

RectTracker.blend (521 KB)

This might be what you’re looking for. I got this code from a version of blender meant for TUIO Messages. Here’s the link.

Here’s the code. It pretty much transfers your mouse coordinates into 3d coordinates from any angle. I’m not sure how your script works(I didn’t look) but this might be what you want? I hope it helps.

Oh yeah, it’s made for Blender 2.5


import Rasterizer

import Mathutils
from Mathutils import *

cont = GameLogic.getCurrentController()
obj = cont.owner

def screentoscene(scene, pos, cursor):  
    """
    Convert screen (x,y) coordinates to a 3d coordinate in a given z-plane
    input: scene  (the blender scene from which we can get the projection camera)
           pos    (the 3d of a point from which you want to use the z-value)
           cursor (the 2d cursor position in blender 2d normalized coordinates: [-1,1])
    """
    # Get the current camera
    cam = scene.active_camera
    camtoclip = cam.projection_matrix
    camtoclip = Matrix(camtoclip[0], camtoclip[1], camtoclip[2], camtoclip[3])

    cliptocam = camtoclip.copy().invert()

    # Get the modeview matrix. 
    camtoworld = cam.getCameraToWorld()    
    camtoworld = Matrix( camtoworld[0], camtoworld[1], camtoworld[2], camtoworld[3])

    # Get its inverse
    worldtocam = cam.getWorldToCamera()    
    worldtocam = Matrix( worldtocam[0], worldtocam[1], worldtocam[2], worldtocam[3])

    pos = Vector([pos[0], pos[1], pos[2], 1.0])
    pos = worldtocam*pos
    pos = camtoclip*pos
    transpos =pos/pos[3]
    
    
    # The cursor is constraint to be on the zplane given in parameter
    pos = Vector([cursor[0], cursor[1], transpos[2], 1.0])
    worldpos = camtoworld*(cliptocam*pos)
    worldpos /= worldpos[3]
    return worldpos    

Thanks for your reply!
Unfortunately, that isn’t exactly what I wanted.
I reconfirmed exactly what I wanted up top in the first post.

You need to account for the orientation of the active camera, obviously.

This should work:


def drawScreenRect(self, active_cam, start_point, end_point, color=[0, 255, 0]): 
        """ start_point and end_point need to be Mathutils Vectors """ 
         
        from Mathutils import Matrix # Assuming you didn't do this already
        import Rasterizer as RS 
         
        # Build transform matrices 
        cam_ori = Matrix(*active_cam.worldOrientation) 
        cam_ori_inv = cam_ori.copy() 
        cam_ori_inv.invert() 
         
        # Transform starting points to identity (where the algorithm works properly) 
        start_point = cam_ori_inv * start_point 
        end_point = cam_ori_inv * end_point 
         
        # Do the work 
        delta = end_point - start_point 
         
        draw_start = start_point.copy() 
        draw_start.x += delta.x 
         
        draw_end = end_point.copy() 
        draw_end.x -= delta.x 
         
        # Transform points back to original rotation 
        start_point = cam_ori * start_point 
        end_point = cam_ori * end_point 
        draw_start = cam_ori * draw_start 
        draw_end = cam_ori * draw_end 
         
        # Draw the box as described by the points 
        RS.drawLine(start_point, draw_start, color) 
        RS.drawLine(draw_start, end_point, color) 
        RS.drawLine(end_point, draw_end, color) 
        RS.drawLine(draw_end, start_point, color)

Also, there are many, many problems with your code - I’ll list a few:

  • You’re re-creating that rect class over and over. Don’t do that. Instead set a property once: own[“rect”] = RectTracker(). Then, just call the draw function from that: own[“rect”].draw(blah, blah).
  • You’re mixing responsibilities: for a function that draws the box, there should be no other job - Don’t check mouse/key state, or do any kind of overall game flow control in utility functions like these. - Look at how I did it above; that’s the right way.
  • You’re abusing the list comprehension facilities that python offers: these were designed for short and sweet one liners, not to do every kind of filtering you can imagine (it just makes the code look ugly).
  • You’re using try/except for things that could be done with “if this in that: …”. Really, the try/except block is something that you should avoid as much as possible; it’s only to be used in extreme situations where you don’t know how to avoid the error in question.

You need to account for the orientation of the active camera, obviously.
It may be for you. :wink:

About your other suggestions, I understand what you are saying completely, and I will do many of the things you say, but just a few things:

  • I tested it both ways, and found that it really didn’t make a huge difference. There is a bug in Blender 2.4x, where the namespace is cleared after running the script because some precaution about accessing data that should not hang around. So to avoid that, I simply make the class each tick.
  • The reason I am checking for clicks in the draw function is so that the user doesn’t need to. After thinking about it a bit, I suppose you are right, because the user should also have the control of deciding where the start and end points are, but it was originally intended for convenience. :yes:
  • There is nothing wrong with how I used list comprehensions, its better this way than it would be to go:

objects = []
for object in GameLogic.getCurrentScene().objects:
    if (not prop or prop in obj) and (not dynamicOnly or obj.mass > 0) and (not visibleOnly or obj.visible != 0) and (obj not in ignore):
        objects.append(object)

The filtering is a minuscule detail, and really wasn’t important enough to get its own lines. The real thing
I wanted people to concentrate on is the code underneath that, which does the real work;
the actual hit test stuff that people would really need. Also, it isn’t actually written anywhere that you must use list comprehensions for simple one liners. In fact, the filtering could be done all on one line, it is just easier to read and decipher when it is not. (Honestly, I think this whole detail is very silly to argue about, but oh well. :wink: )

A lot of this is really minuscule details. I understand your points though, and I am glad you came out. :slight_smile:

-Sunjay03

  • In 2.49b, the state of the class instance should be preserved just fine when set to a game object; the whole marshalling step should only happen on scene load/re-load. Could you make a small example .blend that triggers the error?
  • It’s not just about exposing more functionality. It’s also about good software engineering practices, holding to common sense, and making your functions as context agnostic as possible. When you have a function named draw, that function should just draw based on some input parameters, because that’s what people would expect it to do (although, I think drawScreenRect is a more descriptive name).

As for convenience: you should call draw from within hitTest, in which case you can just run the mouse check code there. Then the user would only need to call hitTest to do everything.

The color should just be a list on the class instance itself; hitTest can pass it to draw.

  • Well, I guess we can say it’s a matter of taste; I just find it irritating to scroll sideways because someone wanted to make everything fit into a single list comprehension. I’m not saying you absolutely have to do this:

objects = []
for obj in GameLogic.getCurrentScene().objects:
    if (not prop or prop in obj) and 
       (not dynamicOnly or obj.mass > 0) and 
       (not visibleOnly or obj.visible != 0) and 
       (obj not in ignore):
       objects.append(obj)

But even this would help:


scene = GameLogic.getCurrentScene()
objects = [obj for obj in scene.objects if (not prop or prop in obj) and 
                                           (not dynamicOnly or obj.mass > 0) and 
                                           (not visibleOnly or obj.visible != 0) and 
                                           (obj not in ignore)]

#3 … probably.

1, 2 and 4 -> definitely not.

PS:

Don’t worry yourself with standards. I’m just trying to help. :wink:

The main benefit of python is readability, so keep it readable I think. If you have an over complex list comprehension I think that throws a fair bit of readability out the window, which is an issue if you intend to distribute your script to others.

It may be worth reading over the style guide:

…code is read much more often than it is written. The guidelines provided here are intended to improve the readability of code and make it consistent across the wide spectrum of Python code

While you are helping, would you mind taking a look at the hitTest function.
After I applied your script to my blend, it stopped working while the camera is upside down. I tried applying your camera orientation stuff, but it still didn’t work.

-Sunjay03

Attachments

RectTracker.blend (527 KB)

I realized that I forgot to give you the correct script!

Sorry:


'''
Draws a rectangle on the screen and then tracks to see if anything is in it.
Sunjay Varma - [www.sunjay-varma.com](http://www.sunjay-varma.com)
'''
import GameLogic
from Mathutils import Vector, Matrix
from Rasterizer import drawLine
class RectTracker:
 def __init__(self):
  from Rasterizer import showMouse as s; s(1)
 # Draws the rectangle
 def draw(self,active_cam, start, end, color=[255,255,255]):
  from Mathutils import Matrix, Vector
  import Rasterizer as RS
  # Build transform matrices
  cam_ori = Matrix(*active_cam.worldOrientation)
  cam_ori_inv = cam_ori.copy()
  cam_ori_inv.invert()
  # Transform starting points to identity (where the algorithm works properly)
  start_point = cam_ori_inv * Vector(start)
  end_point = cam_ori_inv * Vector(end)
  # Do the work
  delta = end_point - start_point
  draw_start = start_point.copy()
  draw_start.x += delta.x
  draw_end = end_point.copy()
  draw_end.x -= delta.x
  # Transform points back to original rotation
  start_point = cam_ori * start_point
  end_point = cam_ori * end_point
  draw_start = cam_ori * draw_start
  draw_end = cam_ori * draw_end
  # Draw the box as described by the points
  RS.drawLine(start_point, draw_start, color)
  RS.drawLine(draw_start, end_point, color)
  RS.drawLine(end_point, draw_end, color)
  RS.drawLine(draw_end, start_point, color)
 #Returns a list of objects within the recttracker.
 def hitTest(self, start, end, prop=None,dynamicOnly=0,visibleOnly=0,ignore=set()):
  if bool(ignore):
   for ob in ignore:
    if isinstance(ob,str):
     try: ob = GameLogic.getCurrentScene().objects['OB'+ob]
     except: pass
  ignore = set(ignore)
  ignore.update([GameLogic.getCurrentScene().active_camera])
  # the objects to search
  objects = []
  for obj in GameLogic.getCurrentScene().objects:
   if (not prop or prop in obj) and 
             (not dynamicOnly or obj.mass > 0) and 
      (not visibleOnly or obj.visible != 0) and 
      (obj not in ignore):
          objects.append(obj)
  camera = GameLogic.getCurrentScene().active_camera
  
  orig = camera.getScreenPosition(start)
  msource = camera.getScreenPosition(end)
  xlow = min(orig[0], msource[0])
  xhigh = max(orig[0], msource[0])
  ylow = min(orig[1], msource[1])
  yhigh = max(orig[1], msource[1])
  hitlist = []
  for obj in objects:
   x, y = camera.getScreenPosition(obj)
   if (xlow < x < xhigh) and (ylow < y < yhigh):
    hitlist.append(obj)
  return hitlist or False
# example code
def main():
 cont = GameLogic.getCurrentController()
 own = cont.owner
 mouseover = cont.sensors['over']
 click = cont.sensors['click']
 camera = GameLogic.getCurrentScene().active_camera
 
 if not 'rect' in own: rect = own['rect'] = RectTracker()
 else: rect = own['rect']
 
 if click.positive:
  if not 'last' in own or not own['last']: own['last'] = mouseover.raySource
  
  rect.draw(camera, own['last'], mouseover.raySource)
  
  hitlist = rect.hitTest(own['last'], mouseover.raySource, prop='select',visibleOnly=1,ignore=[own])
  for obj in GameLogic.getCurrentScene().objects:
   if hitlist != False:
    if obj in hitlist:
     if obj.has_key('active'): obj['active'] = 1
 
    else:
     if obj.has_key('active'): obj['active'] = 0
 
   else:
    if obj.has_key('active'): obj['active'] = 0
     
 else: own['last'] = None


 

Thanks for the link!

  • The script in your bug report .blend is not in the frame of a module. Here, you’re using a module, and you are now doing exactly what I instructed in my initial post (creating the class instance once - it works).

I’m glad you did things properly now (there was really no valid excuse to recreate a class instance on every tick).

Regarding your other problem: Maybe you’re confused by the fact that those little hands are not being rendered when observed from behind? You need to enable “Twoside” for them, in the Texture Face panel, otherwise they will only be visible from the front.

However, the algorithm seems to work fine - I mean, just look at the debug properties - objects are still being selected.

@ Andrew

+1 for style. :smiley:

Thanks! That was silly of me.

-Sunjay03