NoUserOrg

Reto Spoerri : Game Design : Zürich

3d-textbubble

Filed in: Resources.ProgrammingCodeSnipplets3d-textbubble · Modified on : Fri, 21 Aug 09

makes a billboarded (allways facing the camera) text window, with borders and background.

changes:

06.07.2009: cleaned up a bit, now 2d and 3d(billboarded) classes, complex and simple versions.

Screenshot

{{:codesnipplets:panda3d:codesnipplets:panda3d:3d-textbubble:textbubble.png|}}

Sourcecode Simple Version

without alignment and window callbacks

from pandac.PandaModules import TextNode, CardMaker, Vec3, Vec4, NodePath

# size and border of the textnode
BORDER_SIZE          = 1.5
CARD_BACKGROUND_SIZE = Vec4(0.1, 0.2, 0.5, 0.1)
CARD_FRAME_SIZE      = Vec4(0.1, 0.1, 0.1, 0.1)
# colors of the border, background and text
BORDER_COLOR         = Vec4(0.3, 0.3, 0.3, 1.0)
BACKGROUND_COLOR     = Vec4(1.0, 1.0, 1.0, 0.7)
FONT_COLOR           = Vec4(0.0, 0.0, 0.0, 1.0)

class InfoText(NodePath):
  def __init__(self, parent):
    self.parent = parent

    NodePath.__init__(self, 'InfoTextNodePath')
    self.reparentTo(self.parent)

    # create the textnode
    self.textNode = TextNode('InfoTextNode')
    self.textNode.setTextColor(FONT_COLOR)
    self.textNodePath = NodePath.attachNewNode(self, self.textNode)

    # background of the textnode
    self.textNode.setCardColor(BACKGROUND_COLOR)
    self.textNode.setCardDecal(1) # required if in 3d space (z-fighting)
    self.textNode.setCardAsMargin(*CARD_BACKGROUND_SIZE) # left, right, bottom, top

    # frame of the textnode
    self.textNode.setFrameAsMargin(*(CARD_BACKGROUND_SIZE+CARD_FRAME_SIZE))
    self.textNode.setFrameLineWidth(BORDER_SIZE)
    self.textNode.setFrameColor(BORDER_COLOR)

    self.setText('default\ntext')

  def setText(self, text):
    self.textNode.setText(text)

class InfoTextBillaboarded(InfoText):
  def __init__(self, parent):
    # make the text viewing the camera
    self.billboardNodePath = parent.attachNewNode('InfoTextBillaboardedNode')
    self.billboardNodePath.setBillboardPointEye()
    #self.billboardNodePath.setZ(0.1)
    #self.billboardNodePath.setScale(.025)

    InfoText.__init__(self, self.billboardNodePath)

  def setText(self, text):
    InfoText.setText(self, text)

    # move the center of rotation to the lower left corner
    # this is not entirely correct, the x-coordinate is a bit too small
    frameSize = self.textNode.getFrameActual()
    pos = Vec3(frameSize.getX(),0,-frameSize.getZ()) # * NodePath.getScale(self, self.parent)
    self.textNodePath.setPos(self, pos)

if __name__ == '__main__':
  print "I: infoText2.__main__: Example Usage, zoom out to see everything"
  from direct.directbase import DirectStart
  mdl = loader.loadModel('misc/sphere.egg')
  mdl.reparentTo(render)
  mdl.setScale(0.02)

  a = InfoText(aspect2d)
  a.setScale(0.05)
  a.setText('2d attached\nInfoText')

  b = InfoTextBillaboarded(render)
  b.setScale(0.05)
  b.setText('3d billaboarded\nInfoTextBillaboarded')
  run()

Sourcecode Complex Version

with alignment and window callbacks

from pandac.PandaModules import TextNode, CardMaker, Vec3, Vec4, NodePath
from direct.showbase.DirectObject import DirectObject

# size and border of the textnode
BORDER_SIZE          = 1.5
CARD_BACKGROUND_SIZE = Vec4(0.1, 0.2, 0.5, 0.1)
CARD_FRAME_SIZE      = Vec4(0.1, 0.1, 0.1, 0.1)
# colors of the border, background and text
BORDER_COLOR         = Vec4(0.3, 0.3, 0.3, 1.0)
BACKGROUND_COLOR     = Vec4(1.0, 1.0, 1.0, 0.7)
FONT_COLOR           = Vec4(0.0, 0.0, 0.0, 1.0)

# vertical alignment
INFOTEXT_FRAME_VALIGN_TOP = 1
INFOTEXT_FRAME_VALIGN_CENTER = .5
INFOTEXT_FRAME_VALIGN_BOTTOM = 0
INFOTEXT_FRAME_HALIGN_LEFT = 0
INFOTEXT_FRAME_HALIGN_CENTER = 0.5
INFOTEXT_FRAME_HALIGN_RIGHT = 1

INFOTEXT_WINDOW_VALIGN_TOP = 1
INFOTEXT_WINDOW_VALIGN_CENTER = 0
INFOTEXT_WINDOW_VALIGN_BOTTOM = -1
INFOTEXT_WINDOW_HALIGN_LEFT = -1
INFOTEXT_WINDOW_HALIGN_CENTER = 0
INFOTEXT_WINDOW_HALIGN_RIGHT = 1

class InfoText(NodePath, DirectObject):
  def __init__(self, parent):
    self.parent = parent

    self.verticalFrameAlign = INFOTEXT_FRAME_VALIGN_TOP
    self.horizontalFrameAlign = INFOTEXT_FRAME_HALIGN_LEFT

    NodePath.__init__(self, 'InfoTextNodePath')
    self.reparentTo(self.parent)

    # create the textnode
    self.textNode = TextNode('InfoTextNode')
    self.textNode.setTextColor(FONT_COLOR)
    self.textNodePath = NodePath.attachNewNode(self, self.textNode)

    # background of the textnode
    self.textNode.setCardColor(BACKGROUND_COLOR)
    self.textNode.setCardDecal(1) # required if in 3d space (z-fighting)
    self.textNode.setCardAsMargin(*CARD_BACKGROUND_SIZE) # left, right, bottom, top

    # frame of the textnode
    self.textNode.setFrameAsMargin(*(CARD_BACKGROUND_SIZE+CARD_FRAME_SIZE))
    self.textNode.setFrameLineWidth(BORDER_SIZE)
    self.textNode.setFrameColor(BORDER_COLOR)

    self.verticalWindowAlign = INFOTEXT_WINDOW_VALIGN_BOTTOM
    self.horizontalWindowAlign = INFOTEXT_WINDOW_HALIGN_LEFT
    DirectObject.__init__(self)
    self.accept('window-event', self.repositionText)

    self.setText('default\ntext')

  def setText(self, text):
    self.textNode.setText(text)
    self.repositionText()

  def repositionText(self, window=None):
    if window is None:
      # requires panda3d to be started
      window = base.win
    # this calculates the frame position relative to the window
    wp = window.getProperties()
    aspect = wp.getXSize() / float(wp.getYSize())
    if aspect > 1.0:
      windowWidth = wp.getXSize() / float(wp.getYSize()) * self.horizontalWindowAlign
      windowHeight = 1.0 * self.verticalWindowAlign
    else:
      windowWidth = 1.0 * self.horizontalWindowAlign
      windowHeight = wp.getYSize() / float(wp.getXSize()) * self.verticalWindowAlign
    windowPos = self.textNodePath.getRelativeVector(self.parent, Vec3(windowWidth, 0, windowHeight))

    # this calculates the frame position relative to itself's size
    frameSize = self.textNode.getFrameActual()
    pos = Vec3(-frameSize.getX(),0,-frameSize.getZ())
    framePos = Vec3( (frameSize.getY()-frameSize.getX()) * self.horizontalFrameAlign,
                 0,
                 (frameSize.getW()-frameSize.getZ()) * self.verticalFrameAlign)
    self.textNodePath.setPos(self, pos-framePos+windowPos)

  def setFrameVAlign(self, alignment):
    self.verticalFrameAlign = alignment
    self.repositionText()
  def setFrameHAlign(self, alignment):
    self.horizontalFrameAlign = alignment
    self.repositionText()

  def setWindowVAlign(self, alignment):
    self.verticalWindowAlign = alignment
    self.repositionText()
  def setWindowHAlign(self, alignment):
    self.horizontalWindowAlign = alignment
    self.repositionText()

class InfoTextBillboarded(InfoText):
  def __init__(self, parent):
    # make the text viewing the camera
    self.billboardNodePath = parent.attachNewNode('InfoTextBillboardedNode')
    self.billboardNodePath.setBillboardPointEye()

    InfoText.__init__(self, self.billboardNodePath)

  def setText(self, text):
    InfoText.setText(self, text)

if __name__ == '__main__':
  print "I: infoText2.__main__: Example Usage, zoom out to see everything"
  from direct.directbase import DirectStart
  mdl = loader.loadModel('misc/sphere.egg')
  mdl.reparentTo(render)
  mdl.setScale(0.02)

  a = InfoText(aspect2d)
  a.setScale(0.05)
  a.setFrameHAlign(INFOTEXT_FRAME_HALIGN_LEFT)
  a.setFrameVAlign(INFOTEXT_FRAME_VALIGN_TOP)
  a.setWindowHAlign(INFOTEXT_WINDOW_HALIGN_LEFT)
  a.setWindowVAlign(INFOTEXT_WINDOW_VALIGN_TOP)
  a.setText('2d attached\nInfoText\nframe aligned LT\nwindow aligned LT')
  b = InfoText(aspect2d)
  b.setScale(0.05)
  b.setFrameHAlign(INFOTEXT_FRAME_HALIGN_RIGHT)
  b.setFrameVAlign(INFOTEXT_FRAME_VALIGN_TOP)
  b.setWindowHAlign(INFOTEXT_WINDOW_HALIGN_RIGHT)
  b.setWindowVAlign(INFOTEXT_WINDOW_VALIGN_TOP)
  b.setText('2d attached\nInfoText\naligned RT\nwindow aligned RT')
  c = InfoText(aspect2d)
  c.setScale(0.05)
  c.setFrameHAlign(INFOTEXT_FRAME_HALIGN_RIGHT)
  c.setFrameVAlign(INFOTEXT_FRAME_VALIGN_BOTTOM)
  c.setWindowHAlign(INFOTEXT_WINDOW_HALIGN_RIGHT)
  c.setWindowVAlign(INFOTEXT_WINDOW_VALIGN_BOTTOM)
  c.setText('2d attached\nInfoText\naligned RB\nwindow aligned RB')
  d = InfoText(aspect2d)
  d.setScale(0.05)
  d.setFrameHAlign(INFOTEXT_FRAME_HALIGN_LEFT)
  d.setFrameVAlign(INFOTEXT_FRAME_VALIGN_BOTTOM)
  d.setWindowHAlign(INFOTEXT_WINDOW_HALIGN_LEFT)
  d.setWindowVAlign(INFOTEXT_WINDOW_VALIGN_BOTTOM)
  d.setText('2d attached\nInfoText\naligned LB\nwindow aligned LB')

  e = InfoTextBillboarded(render)
  e.setScale(.5)
  e.setFrameHAlign(INFOTEXT_FRAME_HALIGN_RIGHT)
  e.setFrameVAlign(INFOTEXT_FRAME_VALIGN_BOTTOM)
  e.setText('3d billboarded\nInfoTextBillboarded')
  run()