Point and Click in 3d space

Advanced game related topics
citystate
Enthusiast
Enthusiast
Posts: 638
Joined: Sun Feb 12, 2006 10:06 pm

Point and Click in 3d space

Post by citystate »

Here's something I've been working on for a while.

The code works by translating the mouse's screen position to a virtual screen in 3d space. It uses the vector between the mouse's 3d location and the camera to determine the distance of each bounding sphere centre to the vector. The spheres which have a radius less than their distance to the vector are in the line of selection, the sphere that is closest to the camera is the current object.

I figured this could be used with any 3d game (or app) where entities need to be selected with the mouse.

Code: Select all

;cheating a bit with this structure - I use it for both 3D points and vectors
Structure vector
  x.d
  y.d
  z.d
EndStructure

Structure mouse
  x.l
  y.l
  v.vector
  button.l
  over.l
EndStructure

Structure bounded
  object.l
  centre.vector
  radius.d
  distance.d
EndStructure

Global height.l, width.l, mousedepth.d, camera.vector, lookat.vector, origin.vector
Global NewList spheres.bounded()

width  = 1024
height = 768
mousedepth = -665

;width  = 800       ;These numbers seem to work for a 
;height = 600       ;800 by 600 screen but the graphic
;mousedepth = -519  ;detail is better at 1024x768

Procedure copy(*a.vector,*b.vector)
  *a\x = *b\x
  *a\y = *b\y
  *a\z = *b\z
EndProcedure

Procedure new(*v.vector,x.d,y.d,z.d)
  *v\x = x
  *v\y = y
  *v\z = z
EndProcedure

Procedure vectorise(*v.vector,*from.vector,*toPoint.vector)
  new(*v, *toPoint\x - *from\x,*toPoint\y - *from\y,*toPoint\z - *from\z)
EndProcedure

Procedure equal(*a.vector,*b.vector)
  If *a\x <> *b\x:ProcedureReturn 0:EndIf
  If *a\y <> *b\y:ProcedureReturn 0:EndIf
  If *a\z <> *b\z:ProcedureReturn 0:EndIf
  ProcedureReturn 1
EndProcedure

Procedure translate(*v.vector, *t.vector)
  *v\x+*t\x
  *v\y+*t\y
  *v\z+*t\z
EndProcedure

Procedure rotate(*v.vector, *r.vector)
  copy(temp.vector,*v) ;rotate x axis
  *v\y = temp\y*Cos(-*r\x)-temp\z*Sin(-*r\x)
  *v\z = temp\y*Sin(-*r\x)+temp\z*Cos(-*r\x)
  copy(temp.vector,*v) ;rotate y axis
  *v\z = temp\z*Cos(-*r\y)-temp\x*Sin(-*r\y)
  *v\x = temp\z*Sin(-*r\y)+temp\x*Cos(-*r\y)
  copy(temp.vector,*v) ;rotate z axis
  *v\x = temp\x*Cos(-*r\z)-temp\y*Sin(-*r\z)
  *v\y = temp\x*Sin(-*r\z)+temp\y*Cos(-*r\z)
EndProcedure

Procedure.d dot(*a.vector,*b.vector)
  ProcedureReturn (*a\x * *b\x)+(*a\y * *b\y)+(*a\z * *b\z)
EndProcedure

Procedure.d angle(*a.vector, *b.vector)
  ProcedureReturn ACos(dot(*a,*b)/(Pow(dot(*a,*a),0.5)*Pow(dot(*b,*b),0.5)))
EndProcedure

Procedure map(*v.vector)
  a.d:b.d:c.d
  vectorise(LA.vector,camera,lookat)
  new(screen.vector,0,0,mousedepth)
  
  new(Lxy.vector,LA\x,LA\y,0)
  new(Lyz.vector,0,LA\y,LA\z)
  new(Lxz.vector,LA\x,0,LA\z)
  new(Sxy.vector,screen\x,screen\y,0)
  new(Syz.vector,0,screen\y,screen\z)
  new(Sxz.vector,screen\x,0,screen\z)
  a = angle(Sxy,Lxy)
  b = angle(Syz,Lyz)
  c = angle(Sxz,Lxz)
  If equal(Lxy,origin)+equal(Sxy,origin):a=0:EndIf
  If equal(Lyz,origin)+equal(Syz,origin):b=0:EndIf
  If equal(Lxz,origin)+equal(Sxz,origin):c=0:EndIf
  new(angles.vector,b,c,a)
  rotate(*v,angles)
  translate(*v,camera)
EndProcedure

Procedure.d distance(*a.vector,*b.vector,*c.vector)
  ;distance between line AB and point C
  new(AB.vector,*b\x-*a\x,*b\y-*a\y,*b\z-*a\z)
  new(AC.vector,*c\x-*a\x,*c\y-*a\y,*c\z-*a\z)
  angle.d = angle(AB,AC)
  ProcedureReturn Pow(dot(AC,AC),0.5)*Sin(angle)
EndProcedure

Procedure over(*v.vector)
  ForEach spheres()
    Delay(0)
    copy(centre.vector,spheres()\centre)
    spheres()\distance=Pow(Pow(camera\x-centre\x,2)+Pow(camera\y-centre\y,2)+Pow(camera\z-centre\z,2),0.5)
    basicangle.l = (180*angle(lookat,centre)/#PI)
    If basicangle>90:spheres()\distance = -spheres()\distance:EndIf
  Next
  SortStructuredList(spheres(),0,OffsetOf(bounded\distance),#PB_Sort_Double)
  ForEach spheres()
    Delay(0)
    If (spheres()\distance>=0) And (distance(camera,*v,spheres()\centre)<= spheres()\radius): ProcedureReturn spheres()\object:EndIf
  Next
  ProcedureReturn -1
EndProcedure

Procedure getMouse(*m.mouse)
  ExamineMouse()
  If MouseButton(#PB_MouseButton_Left)
    *m\button=1
  ElseIf MouseButton(#PB_MouseButton_Right)
    *m\button=2
  ElseIf MouseButton(#PB_MouseButton_Middle)
    *m\button=3
  Else
    *m\button=0
  EndIf
  x = MouseX()-width/2
  y = (height/2-MouseY())
  z = mousedepth
  new(v.vector,x,y,z)
  map(v)
  *m\x = MouseX()
  *m\y = MouseY()
  copy(*m\v,v)
  *m\over = over(v)
EndProcedure

Procedure.s displayMouse(*m.mouse)
  ProcedureReturn "("+Str(*m\x)+","+Str(*m\y)+") - ["+StrD(*m\v\x,2)+","+StrD(*m\v\y,2)+","+StrD(*m\v\z,2)+"] - distance "+StrD(Pow(Pow(*m\v\x - camera\x,2)+Pow(*m\v\y - camera\y,2)+Pow(*m\v\z - camera\z,2),0.5),2)
EndProcedure

Procedure setcamera(cameraID.l,*v.vector)
  If *v <> #Null:copy(camera,*v):EndIf
  CameraLocate(cameraID,*v\x,*v\y,*v\z)
  If lookat <> #Null:new(lookat,0,0,-1000):EndIf
  CameraLookAt(cameraID,lookat\x,lookat\y,lookat\z)
EndProcedure

Procedure setlookat(cameraID.l,*v.vector)
  If *v <> #Null:copy(lookat,*v):EndIf
  CameraLookAt(cameraID,lookat\x,lookat\y,lookat\z)
EndProcedure

Procedure addSphere(object.l,*centre.vector,radius.d)
  AddElement(spheres())
  spheres()\object = object
  spheres()\centre\x = *centre\x
  spheres()\centre\y = *centre\y
  spheres()\centre\z = *centre\z
  spheres()\radius = radius
EndProcedure

Procedure moveObject(object.l,*v.vector)
  MoveEntity(object,*v\x,*v\y,*v\z)
  SortStructuredList(spheres(),0,OffsetOf(bounded\object),#PB_Sort_Long)
  ForEach spheres()
    If spheres()\object = object:translate(spheres()\centre,*v):EndIf
    If spheres()\object > object:ProcedureReturn:EndIf
  Next
EndProcedure

;-Initialise
ExamineDesktops()
InitEngine3D()
InitSprite()
InitKeyboard()
InitMouse()
OpenScreen(width,height,32,"")
UseJPEGImageDecoder()
UsePNGImageDecoder() 
Add3DArchive("Data\", #PB_3DArchive_FileSystem) 
Parse3DScripts()
LoadSprite(0,"Data\mouse.PNG") 

;-Setup Movement Vectors
new(up.vector,0,0,-1):map(up)
new(down.vector,0,0,1):map(down)
new(left.vector,-1,0,0):map(left)
new(right.vector,1,0,0):map(right)

;-Set named Vectors
new(origin,0,0,0)
new(camera,0,0,0)
new(lookat,0,0,mousedepth)

;-Setup Camera
CreateCamera(0, 0, 0, 100, 100)
setcamera(0,camera) 
setlookat(0,lookat)
CameraFOV(0, #PI / 3) 

;-Setup Cubes
LoadMesh   (1, "cube.mesh") 
LoadTexture(1, "stone.jpg") 
CreateMaterial(1,TextureID(1)) 

For i=1 To 1000
  new(r.vector,2*Random(width)-width,2*Random(height)-height,-Random(-mousedepth))
  CreateEntity(i,MeshID(1),MaterialID(1),r\x,r\y,r\z)
  ScaleEntity(i,0.1,0.1,0.1)
  addSphere(i,r,6.5)
Next i

;-Main loop
Repeat
  ClearScreen(0)
  RenderWorld()
  getmouse(mouse.mouse)
  StartDrawing(ScreenOutput())
    DrawText(0,0, "Mouse location: "+displayMouse(mouse),$FFFF,0)
    ovr = over(mouse\v)
    If ovr<0
      DrawText(0,16, "Mouse is over: nothing ",$FF,0)
    Else
      DrawText(0,16, "Mouse is over: object "+Str(ovr),$FF00,0)
      If ExamineKeyboard()
        If KeyboardPushed(#PB_Key_Up)
          moveObject(ovr,up)
        ElseIf KeyboardPushed(#PB_Key_Down)
          moveObject(ovr,down)
        ElseIf KeyboardPushed(#PB_Key_Left)
          moveObject(ovr,left)
        ElseIf KeyboardPushed(#PB_Key_Right)
          moveObject(ovr,right)
        EndIf
      EndIf
    EndIf
  StopDrawing()
  DisplayTransparentSprite(0,MouseX(),MouseY())    
  FlipBuffers()
  ExamineKeyboard()
Until KeyboardPushed(#PB_Key_Escape)
I found that it runs fairly smoothly on my machine provided there are fewer than 2000 bounding spheres (an issue with linkedlists). Any tips on how to optimise it are welcome.

Cheers,
Matt

PS the code needs three files in the Data\ directory
  • mouse.png
    wall.jpg
    cube.mesh
I used Comtois' cube.mesh from some of his earlier forays into this area but any mesh will do.