#!/usr/bin/env python # Author: Shao Zhang and Phil Saltzman # Models: Eddie Canaan # Last Updated: 2015-03-13 # # This tutorial shows how to determine what objects the mouse is pointing to # We do this using a collision ray that extends from the mouse position # and points straight into the scene, and see what it collides with. We pick # the object with the closest collision from direct.showbase.ShowBase import ShowBase from panda3d.core import CollisionTraverser, CollisionNode from panda3d.core import CollisionHandlerQueue, CollisionRay from panda3d.core import AmbientLight, DirectionalLight, LightAttrib from panda3d.core import TextNode from panda3d.core import LPoint3, LVector3, BitMask32 from direct.gui.OnscreenText import OnscreenText from direct.showbase.DirectObject import DirectObject from direct.task.Task import Task import sys # First we define some constants for the colors BLACK = (0, 0, 0, 1) WHITE = (1, 1, 1, 1) HIGHLIGHT = (0, 1, 1, 1) PIECEBLACK = (.15, .15, .15, 1) # Now we define some helper functions that we will need later # This function, given a line (vector plus origin point) and a desired z value, # will give us the point on the line where the desired z value is what we want. # This is how we know where to position an object in 3D space based on a 2D mouse # position. It also assumes that we are dragging in the XY plane. # # This is derived from the mathematical of a plane, solved for a given point def PointAtZ(z, point, vec): return point + vec * ((z - point.getZ()) / vec.getZ()) # A handy little function for getting the proper position for a given square1 def SquarePos(i): return LPoint3((i % 8) - 3.5, int(i // 8) - 3.5, 0) # Helper function for determining whether a square should be white or black # The modulo operations (%) generate the every-other pattern of a chess-board def SquareColor(i): if (i + ((i // 8) % 2)) % 2: return BLACK else: return WHITE class ChessboardDemo(ShowBase): def __init__(self): # Initialize the ShowBase class from which we inherit, which will # create a window and set up everything we need for rendering into it. ShowBase.__init__(self) # This code puts the standard title and instruction text on screen self.title = OnscreenText(text="Panda3D: Tutorial - Mouse Picking", style=1, fg=(1, 1, 1, 1), shadow=(0, 0, 0, 1), pos=(0.8, -0.95), scale = .07) self.escapeEvent = OnscreenText( text="ESC: Quit", parent=base.a2dTopLeft, style=1, fg=(1, 1, 1, 1), pos=(0.06, -0.1), align=TextNode.ALeft, scale = .05) self.mouse1Event = OnscreenText( text="Left-click and drag: Pick up and drag piece", parent=base.a2dTopLeft, align=TextNode.ALeft, style=1, fg=(1, 1, 1, 1), pos=(0.06, -0.16), scale=.05) self.accept('escape', sys.exit) # Escape quits self.disableMouse() # Disble mouse camera control camera.setPosHpr(0, -12, 8, 0, -35, 0) # Set the camera self.setupLights() # Setup default lighting # Since we are using collision detection to do picking, we set it up like # any other collision detection system with a traverser and a handler self.picker = CollisionTraverser() # Make a traverser self.pq = CollisionHandlerQueue() # Make a handler # Make a collision node for our picker ray self.pickerNode = CollisionNode('mouseRay') # Attach that node to the camera since the ray will need to be positioned # relative to it self.pickerNP = camera.attachNewNode(self.pickerNode) # Everything to be picked will use bit 1. This way if we were doing other # collision we could separate it self.pickerNode.setFromCollideMask(BitMask32.bit(1)) self.pickerRay = CollisionRay() # Make our ray # Add it to the collision node self.pickerNode.addSolid(self.pickerRay) # Register the ray as something that can cause collisions self.picker.addCollider(self.pickerNP, self.pq) # self.picker.showCollisions(render) # Now we create the chess board and its pieces # We will attach all of the squares to their own root. This way we can do the # collision pass just on the squares and save the time of checking the rest # of the scene self.squareRoot = render.attachNewNode("squareRoot") # For each square self.squares = [None for i in range(64)] self.pieces = [None for i in range(64)] for i in range(64): # Load, parent, color, and position the model (a single square # polygon) self.squares[i] = loader.loadModel("models/square") self.squares[i].reparentTo(self.squareRoot) self.squares[i].setPos(SquarePos(i)) self.squares[i].setColor(SquareColor(i)) # Set the model itself to be collideable with the ray. If this model was # any more complex than a single polygon, you should set up a collision # sphere around it instead. But for single polygons this works # fine. self.squares[i].find("**/polygon").node().setIntoCollideMask( BitMask32.bit(1)) # Set a tag on the square's node so we can look up what square this is # later during the collision pass self.squares[i].find("**/polygon").node().setTag('square', str(i)) # We will use this variable as a pointer to whatever piece is currently # in this square # The order of pieces on a chessboard from white's perspective. This list # contains the constructor functions for the piece classes defined # below pieceOrder = (Rook, Knight, Bishop, Queen, King, Bishop, Knight, Rook) for i in range(8, 16): # Load the white pawns self.pieces[i] = Pawn(i, WHITE) for i in range(48, 56): # load the black pawns self.pieces[i] = Pawn(i, PIECEBLACK) for i in range(8): # Load the special pieces for the front row and color them white self.pieces[i] = pieceOrder[i](i, WHITE) # Load the special pieces for the back row and color them black self.pieces[i + 56] = pieceOrder[i](i + 56, PIECEBLACK) # This will represent the index of the currently highlited square self.hiSq = False # This wil represent the index of the square where currently dragged piece # was grabbed from self.dragging = False # Start the task that handles the picking self.mouseTask = taskMgr.add(self.mouseTask, 'mouseTask') self.accept("mouse1", self.grabPiece) # left-click grabs a piece self.accept("mouse1-up", self.releasePiece) # releasing places it # This function swaps the positions of two pieces def swapPieces(self, fr, to): temp = self.pieces[fr] self.pieces[fr] = self.pieces[to] self.pieces[to] = temp if self.pieces[fr]: self.pieces[fr].square = fr self.pieces[fr].obj.setPos(SquarePos(fr)) if self.pieces[to]: self.pieces[to].square = to self.pieces[to].obj.setPos(SquarePos(to)) def mouseTask(self, task): # This task deals with the highlighting and dragging based on the mouse # First, clear the current highlight if self.hiSq is not False: self.squares[self.hiSq].setColor(SquareColor(self.hiSq)) self.hiSq = False # Check to see if we can access the mouse. We need it to do anything # else if self.mouseWatcherNode.hasMouse(): # get the mouse position mpos = self.mouseWatcherNode.getMouse() # Set the position of the ray based on the mouse position self.pickerRay.setFromLens(self.camNode, mpos.getX(), mpos.getY()) # If we are dragging something, set the position of the object # to be at the appropriate point over the plane of the board if self.dragging is not False: # Gets the point described by pickerRay.getOrigin(), which is relative to # camera, relative instead to render nearPoint = render.getRelativePoint( camera, self.pickerRay.getOrigin()) # Same thing with the direction of the ray nearVec = render.getRelativeVector( camera, self.pickerRay.getDirection()) self.pieces[self.dragging].obj.setPos( PointAtZ(.5, nearPoint, nearVec)) # Do the actual collision pass (Do it only on the squares for # efficiency purposes) self.picker.traverse(self.squareRoot) if self.pq.getNumEntries() > 0: # if we have hit something, sort the hits so that the closest # is first, and highlight that node self.pq.sortEntries() i = int(self.pq.getEntry(0).getIntoNode().getTag('square')) # Set the highlight on the picked square self.squares[i].setColor(HIGHLIGHT) self.hiSq = i return Task.cont def grabPiece(self): # If a square is highlighted and it has a piece, set it to dragging # mode if self.hiSq is not False and self.pieces[self.hiSq]: self.dragging = self.hiSq self.hiSq = False def releasePiece(self): # Letting go of a piece. If we are not on a square, return it to its original # position. Otherwise, swap it with the piece in the new square # Make sure we really are dragging something if self.dragging is not False: # We have let go of the piece, but we are not on a square if self.hiSq is False: self.pieces[self.dragging].obj.setPos( SquarePos(self.dragging)) else: # Otherwise, swap the pieces self.swapPieces(self.dragging, self.hiSq) # We are no longer dragging anything self.dragging = False def setupLights(self): # This function sets up some default lighting ambientLight = AmbientLight("ambientLight") ambientLight.setColor((.8, .8, .8, 1)) directionalLight = DirectionalLight("directionalLight") directionalLight.setDirection(LVector3(0, 45, -45)) directionalLight.setColor((0.2, 0.2, 0.2, 1)) render.setLight(render.attachNewNode(directionalLight)) render.setLight(render.attachNewNode(ambientLight)) # Class for a piece. This just handles loading the model and setting initial # position and color class Piece(object): def __init__(self, square, color): self.obj = loader.loadModel(self.model) self.obj.reparentTo(render) self.obj.setColor(color) self.obj.setPos(SquarePos(square)) # Classes for each type of chess piece # Obviously, we could have done this by just passing a string to Piece's init. # But if you wanted to make rules for how the pieces move, a good place to start # would be to make an isValidMove(toSquare) method for each piece type # and then check if the destination square is acceptible during ReleasePiece class Pawn(Piece): model = "models/pawn" class King(Piece): model = "models/king" class Queen(Piece): model = "models/queen" class Bishop(Piece): model = "models/bishop" class Knight(Piece): model = "models/knight" class Rook(Piece): model = "models/rook" # Do the main initialization and start 3D rendering demo = ChessboardDemo() demo.run()