Posted: 3rd Nov 2013 17:29
While pondering my cascaded shadow mapping issues as of late, I happened to realize a way that works for implementing the same functionality as provided by the built-in DBPro function pick object.
There are some advantages to this;
It supports any kind of camera projection, such as orthographic. (The built-in function does not).
It is written in pure DBPro (or well, it uses IanM's M1U, but what doesn't these days?). As such you can extend / change it to fit your requirements. One of the most important things this allows you to do is pick objects from a list instead of having to use a contiguous range of object id's (ie. 100 - 499) like with the built-in function.
Potential to be more efficient than the built-in function. Using the built-in intersect object function, as is done in the demonstration below, this runs about 20% slower than the standard pick object function. However, using a more efficient raycasting function, like can be found in most third party physics / collision dll's, you can make this run significantly faster than the standard implementation, which is obviously a big plus when picking a large amount of objects in realtime. For example, using the raycasting function from Paul Johnston's ("Sparky"'s) free collision library, this function only requires ~19.7% of the executing time of the built-in version.


The actual function:
+ Code Snippet
rem *********************************************************
rem * Works similar to the built-in PICK OBJECT function.   *
rem *                                                       *
rem * Works by casting a ray from the specified screen      *
rem * position, unprojected into world space from the near  *
rem * to the far plane of the camera and checking against   *
rem * intersections with objects along the way.             *
rem * Unlike the built-in function, this will work with     *
rem * orthographic (and presumably any other kind of)       *
rem * camera projections.                                   *
rem * It is also easy to modify this function to select     *
rem * object id's from a custom list instead of having to   *
rem * provide a contiguous stream of id's to check against. *
rem * Replacing the INTERSECT OBJECT call with a third      *
rem * party function of the same persuasion can also make   *
rem * this function several times faster than the built-in  *
rem * version.                                              *
rem *                                                       *
rem * The final parameter «vecPick» is a VECTOR4 id.        *
rem * This vector will be filled out with the «world»       *
rem * coordinates of the collision between the projected    *
rem * ray and the closest object if there was any such      *
rem * collision. This differs from the built-in PICK OBJECT *
rem * function which returns «view space»                   *
rem * (relative-to-camera) coordinates. through the         *
rem * GET PICK VECTOR X/Y/Z functions.                      *
rem * The W component of «vecPick» will be set to the       *
rem * distance from the near plane to the collision point.  *
rem * This parameter can be set to 0 if this information is *
rem * not required to be returned.                          *
rem * The function will furthermore return the ID of the    *
rem * picked object, or 0 if no object was found; just like *
rem * the built-in function works.                          *
rem *                                                       *
rem * Written by Joel Sjöqvist ("Rudolpho"), 2013-11-03.    *
rem ********************************************************* 
function PickObject(x as dword, y as dword, objStart as dword, objEnd as dword, vecPick as dword)
    matView         = new matrix4()
    matProj         = new matrix4()
    matInvViewProj  = new matrix4()
    
    view matrix4 matView
    projection matrix4 matProj
    multiply matrix4 matInvViewProj, matView, matProj
    determinant# = inverse matrix4(matInvViewProj, matInvViewProj)
    
    rem Determine clip space position from the input screen space coordinates
    clipX# = (2 * (x / (1.0 * screen width()))) - 1.0
    clipY# = (2 * (1.0 - (y / (1.0 * screen height())))) - 1.0
    
    rem Convert clip coords to world space
    vecFromPos  = new vector3(clipX#, clipY#, 0.0)      ` Pick from the near plane
    vecToPos    = new vector3(clipX#, clipY#, 1.0)      ` And to the far plane; further distances are irrelevant
    transform coords vector3 vecFromPos, vecFromPos, matInvViewProj
    transform coords vector3 vecToPos, vecToPos, matInvViewProj
    
    closestDist# = 999999.9
    closestObjId = 0
    for obj = objStart to objEnd
        dist# = intersect object(obj, x vector3(vecFromPos), y vector3(vecFromPos), z vector3(vecFromPos), x vector3(vecToPos), y vector3(vecToPos), z vector3(vecToPos))
        rem Hit?
        if dist# > 0.0
            if dist# < closestDist#
                closestObjId = obj
                closestDist# = dist#
            endif
        endif
    next obj
    
    rem Did we find any target object under the mouse? If we provided a pick vector we can fill that one out then.
    if closestObjId > 0 and vecPick > 0
        rem Get pick direction vector
        vecTmp = new vector3()
        subtract vector3 vecTmp, vecToPos, vecFromPos
        normalize vector3 vecTmp, vecTmp
        rem The hit coordinate is the from vector + (distance * direction vector)
        multiply vector3 vecTmp, closestDist#
        add vector3 vecTmp, vecFromPos, vecTmp
        
        rem We may also want to return the pick distance; we can achieve this by having vecPick be a vector4 and putting it in the W component.
        rem NOTE: This means that we should not treat the pickVector as a position and use it in 3d math right away. Extract the X, Y and Z components 
        rem       first and put them in a new vector3 / vector4 / what-have-you for that.
        set vector4 vecPick, x vector3(vecTmp), y vector3(vecTmp), z vector3(vecTmp), closestDist#
        delete vector3 vecTmp
    endif
    
    delete vector3 vecToPos
    delete vector3 vecFromPos
    delete matrix4 matInvViewProj
    delete matrix4 matProj
    delete matrix4 matView
endfunction closestObjId



A small, ugly demonstration program (no external media required, just copy, paste and compile):
+ Code Snippet
rem #############################################
rem # Demonstration of custom object picking    #
rem # functionality, supporting custom camera   #
rem # projection types and also readily         #
rem # extendible to pick object id's from a     #
rem # custom list instead of a fixed id range   #
rem # like the built-in function uses.          #
rem # Using third party raycasting plugins it   #
rem # is also possible to make this             #
rem # implementation run significantly faster   #
rem # than the built-in one.                    #
rem #                                           #
rem # By Joel Sjöqvist ("Rudolpho"), 2013-11-03 #
rem #############################################


rem Constants
#constant true                      1
#constant false                     0

rem Setup engine
set display mode 1600, 900, 32
set window size 1600, 900
set window position (desktop width() - screen width()) / 2, (desktop height() - screen height()) / 2
sync on
sync rate 0
backdrop on
color backdrop 0xff808080
autocam off
rem Ensure equal illumination from all directions (except from below since we'll never see the scene from there anyway)
make light 1
make light 2
make light 3
make light 4
set directional light 0, 1, 0, 0
set directional light 1, -1, 0, 0
set directional light 2, 0, 0, -1
set directional light 3, 0, 0, 1
set directional light 4, 0, -1, 0
ink 0xffffc000, 0

rem Setup orthographic and perspective projection matrices
matProjOrtho        = new matrix4()
matProjPerspective  = new matrix4()
build ortho lhmatrix4 matProjOrtho, camera aspect() * 48, 48, 1, 72
projection matrix4    matProjPerspective  ` Just use the default one here


rem Create simple scene made up of stacked cubes
dim Block(32 * 32 * 6) as dword
nextObjId = 1
for x = 0 to 31
    for y = 0 to 31
        for z = 0 to 5
            rem Should we spawn a block here?
            if rnd(z) + 1 >= z and IsValidBlockPosition(x, y, z)
                Block((z * 1024) + (y * 32) + x) = nextObjId
                make object cube nextObjId, 1
                position object nextObjId, x - 16, z, y - 16
                rem Colour block based on it's height
                lum = 63 + (25.6 * z)
                color object nextObjId, 0xff << 24 || lum << 16 || lum << 8 || lum
                inc nextObjId
            endif
        next z
    next y
next x

rem Create a pick position indicator object to demonstrate that we can return the point of collision between the object and the ray from the screen position
pickIndicator = nextObjId
make object sphere pickIndicator, 0.4
color object pickIndicator, 0xffc0ff00
rem This will work like the built in GET PICK VECTOR X/Y/Z commands. The pick distance is stored in the W component.
pickVector = new vector4()


rem Allow rotating the camera between even 90 degree points of view
cameraCurrentAngle# = 0.0
cameraTargetAngle#  = 0.0

rem Store last state of the space key in order to use it as a toggle button
bIsSpacekeyDown     = false
bIsOrthographicMode = false

tick = timer() - 1
while not escapekey()
    rem Update timer
    lastTick    = tick
    tick        = timer()
    timeForce#  = (tick - lastTick) * 0.001
    
    rem Pick objects by putting the mouse cursor over them and clicking the left mouse button
    if mouseClick() && 1
        obj = PickObject(mouseX(), mouseY(), 1, nextObjId - 1, pickVector)
        if obj > 0
            color object obj, 0xffff0000
            position object pickIndicator, x vector4(pickVector), y vector4(pickVector), z vector4(pickVector)
        endif
    endif
    
    rem Toggle projection type with the space key
    bSpacekey = spacekey()
    if bSpacekey <> bIsSpacekeyDown
        if bSpacekey
            if bIsOrthoGraphicMode
                apply projection matrix4 matProjPerspective
            else
                apply projection matrix4 matProjOrtho
            endif
            bIsOrthographicMode = 1 - bIsOrthographicMode
        endif
        bIsSpaceKeyDown     = bSpacekey
    endif
    
    rem Rotate the camera in 90-degree steps around the scene
    cameraCurrentAngle# = curveAngle(cameraTargetAngle#, cameraCurrentAngle#, 10000 * timeForce#)
    position camera cos(cameraCurrentAngle#) * 48, 10, sin(cameraCurrentAngle#) * 48
    point camera 0, 0, 0
    
    rem Can rotate by pressing the left / right arrow keys when we're close enough to an angle divisable by 90 degrees
    angleDif# = abs(cameraTargetAngle# - cameraCurrentAngle#)
    if angleDif# < 10.0 or angleDif# > 350.0
        cameraTargetAngle# = cameraTargetAngle# + (90 * (rightkey() - leftkey()))
    endif
    rem Ensure that we keep within the 0 .. 360° range
    cameraTargetAngle#  = wrapvalue(cameraTargetAngle#)
    cameraCurrentAngle# = wrapvalue(cameraCurrentAngle#)
    
    rem Text output
    if bIsOrthographicMode
        text 0, 0, "Orthographic projection"
    else
        text 0, 0, "Perspective projection"
    endif
    center text screen width() / 2, screen height() - 60, "Click on a block to pick it and colour it red."
    center text screen width() / 2, screen height() - 40, "Use the left / right arrow keys to rotate the camera."
    center text screen width() / 2, screen height() - 20, "Press [space] to toggle between perspective and orthographic projection."
    sync
endwhile


rem Returns true if the given block position is valid for spawning a new block, or false otherwise
function IsValidBlockPosition(x as dword, y as dword, z as dword)
    rem Already a block here?
    if Block((z * 1024) + (y * 32) + x) > 0 then exitfunction false
    rem In the bottom row and no block here?
    if z = 0 then exitfunction true
    rem Is there a block in the row beneath this one?
    if Block(((z - 1) * 1024) + (y * 32) + x) > 0 then exitfunction true
    rem Otherwise the position isn't available
endfunction false


rem Works similar to the built-in PICK OBJECT function.
rem The main difference is that this one works with any projection matrix, including orthographic ones.
rem Since it is a pure DBP function it can also easily be tweaked to select object ID's to pick from a custom 
rem list instead of using a continuous range.
rem Comparing this with the built-in PICK OBJECT function, this implementation is ~21% slower.
rem It might be possible to speed it up by excluding impossible object early (bounding box / sphere checks etc.).
rem Also, using another implementation of the INTERSECT OBJECT call can increase speeds significantly. For example 
rem using sc_intersectObject instead (Paul Johnston's ("Sparky") DBP collision DLL v2.05) makes this implementation run 
rem 5x faster(!) than the built-in PICK OBJECT function (more precisely at 19.7% of its speed). Note that I only tested it 
rem using box collisions however; in theory it should be even more efficient for complex objects.
function PickObject(x as dword, y as dword, objStart as dword, objEnd as dword, vecPick as dword)
    matView         = new matrix4()
    matProj         = new matrix4()
    matInvViewProj  = new matrix4()
    
    view matrix4 matView
    projection matrix4 matProj
    multiply matrix4 matInvViewProj, matView, matProj
    determinant# = inverse matrix4(matInvViewProj, matInvViewProj)
    
    rem Determine clip space position from the input screen space coordinates
    clipX# = (2 * (x / (1.0 * screen width()))) - 1.0
    clipY# = (2 * (1.0 - (y / (1.0 * screen height())))) - 1.0
    
    rem Convert clip coords to world space
    vecFromPos  = new vector3(clipX#, clipY#, 0.0)      ` Pick from the near plane
    vecToPos    = new vector3(clipX#, clipY#, 1.0)      ` And to the far plane; further distances are irrelevant
    transform coords vector3 vecFromPos, vecFromPos, matInvViewProj
    transform coords vector3 vecToPos, vecToPos, matInvViewProj
    
    closestDist# = 999999.9
    closestObjId = 0
    for obj = objStart to objEnd
        dist# = intersect object(obj, x vector3(vecFromPos), y vector3(vecFromPos), z vector3(vecFromPos), x vector3(vecToPos), y vector3(vecToPos), z vector3(vecToPos))
        rem Hit?
        if dist# > 0.0
            if dist# < closestDist#
                closestObjId = obj
                closestDist# = dist#
            endif
        endif
    next obj
    
    rem Did we find any target object under the mouse? If we provided a pick vector we can fill that one out then.
    if closestObjId > 0 and vecPick > 0
        rem Get pick direction vector
        vecTmp = new vector3()
        subtract vector3 vecTmp, vecToPos, vecFromPos
        normalize vector3 vecTmp, vecTmp
        rem The hit coordinate is the from vector + (distance * direction vector)
        multiply vector3 vecTmp, closestDist#
        add vector3 vecTmp, vecFromPos, vecTmp
        
        rem We may also want to return the pick distance; we can achieve this by having vecPick be a vector4 and putting it in the W component.
        rem NOTE: This means that we should not treat the pickVector as a position and use it in 3d math right away. Extract the X, Y and Z components 
        rem       first and put them in a new vector3 / vector4 / what-have-you for that.
        set vector4 vecPick, x vector3(vecTmp), y vector3(vecTmp), z vector3(vecTmp), closestDist#
        delete vector3 vecTmp
    endif
    
    delete vector3 vecToPos
    delete vector3 vecFromPos
    delete matrix4 matInvViewProj
    delete matrix4 matProj
    delete matrix4 matView
endfunction closestObjId
Posted: 4th Nov 2013 5:32
Whoa, awesome work Rudolpho!

I'm looking forward to simplifying your example so that I can understand it. Good stuff!
Posted: 4th Nov 2013 6:36

Unlike the built-in function, this will work with orthographic (and presumably any other kind of) camera projections


Does the integrated pick object command not work with different camera projections? I would assume it works in very much the same way as your function does, by referencing the current camera projection and view matrix.
Posted: 4th Nov 2013 9:59
@Burning Feet Man: thanks, it's pretty well commented so hopefully that shouldn't be a problem

@Phaelax: you would think so, but try it - it really doesn't for whatever reason.