I have some code that you can feel free to use. It might be overkill but it should do the job.
It uses an action to build a billboard like behavior. eg. the billboard node looks directly at the camera position rather than matching the camera alignment. It was useful in a cave scenario. It also deals with Z order and depth write to ensure the text is visible.
Otherwise you just call
AttachSpeechBubble to float some text above an object.
Code:
import viz
viz.go()
#########Utility Class and Functions for Speech Bubble############
# all you need to call is AttachSpeechBubble
#code is based upon worldviz avatar lookat tutorial
MAXLINELENGTH = 60
class BillboardToNodeAction(viz.ActionClass):
def begin(self,object):
"""Called once when action starts"""
self.nodeToLookAt = self._actiondata_.data[0]
self.nodeToModify = self._actiondata_.data[1]
self.duration = self._actiondata_.data[2]
self.blendIn = self._actiondata_.data[3]
self.timeElapsed = 0
self.startQuat = self.nodeToModify.getQuat()
def update(self,elapsed,object):
self.timeElapsed += elapsed
p = 1
if self.blendIn <> 0.0:
p = self.timeElapsed / self.blendIn
positionToLookAt = self.nodeToLookAt.getPosition()
for ix in range(3):
positionToLookAt[ix] = self.nodeToModify.getPosition()[ix] + self.nodeToModify.getPosition()[ix] - positionToLookAt[ix]
if self.duration > 0.0 and self.timeElapsed > self.duration:
self.nodeToModify.lookat(positionToLookAt, 0, viz.ABS_GLOBAL)
self.end(object)
elif p < 1.0:
self.nodeToModify.lookat( positionToLookAt, 0, viz.ABS_GLOBAL )
targetQuat = self.nodeToModify.getQuat()
nextQuad = vizmat.slerp(self.startQuat,targetQuat,p)
self.nodeToModify.setQuat(nextQuad)
else:
self.nodeToModify.lookat(positionToLookAt, 0, viz.ABS_GLOBAL)
def end(self,object):
viz.ActionClass.end(self,object)
#Function to construct the action instance
def billboardNode( nodeToLookAt, nodeToModify, duration = viz.FOREVER, blendIn = 0.0):
action = viz.ActionData()
action.data = [ nodeToLookAt, nodeToModify, duration, blendIn ]
action.actionclass = BillboardToNodeAction
return action
def FormatTextbox(msg):
length = len(msg)
lastspace = 0
while lastspace + MAXLINELENGTH < length and lastspace <> -1:
lastspace = msg.rfind(' ',lastspace, lastspace + MAXLINELENGTH)
if lastspace > 0 and lastspace < length:
msg = msg[:lastspace] + '\n' + msg[lastspace+1:]
else:
lastspace = -1
lines = msg.splitlines()
return lines
def AttachSpeechBubble(dest,message, height = 1.8):
lines = FormatTextbox(message)
rootline = 0
parentline = 0
for lineix in range(len(lines),0,-1):
speechline = viz.addText(lines[lineix-1])
speechline.alignment(viz.TEXT_CENTER_BOTTOM)
if not rootline:
rootline = speechline
parentline = speechline
else:
speechline.parent(parentline)
parentline = speechline
speechline.translate([0,1.0,0])
speechline.draworder(13)
speechline.depthFunc(viz.GL_ALWAYS)
speechline.disable(viz.DEPTH_WRITE)
speechtext = rootline
speechbox = speechtext.getBoundingBox()
speechtext.setScale([0.12,0.12,0.12])
viz.startlayer(viz.QUADS)
viz.vertex(speechbox.xmin-0.1,speechbox.ymax+0.1,0)
viz.vertex(speechbox.xmax+0.1,speechbox.ymax+0.1,0)
viz.vertex(speechbox.xmax+0.1,speechbox.ymin-0.1,0)
viz.vertex(speechbox.xmin-0.1,speechbox.ymin-0.1,0)
speechquad = viz.endlayer()
speechquad.color([0,0,0])
speechquad.disable(viz.CULL_FACE)
speechquad.parent(speechtext)
speechquad.draworder(12)
speechquad.alpha(0.5)
speechquad.depthFunc(viz.GL_ALWAYS)
speechquad.disable(viz.DEPTH_WRITE)
if type(dest) == list:
speechtext.translate(dest)
else:
linkposition = viz.link(dest, speechtext)
linkposition.setMask(viz.LINK_POS)
linkposition.preTrans([0, height, 0])
speechtext.addAction(billboardNode(viz.MainView,speechtext))
return speechtext
#################################
viz.clearcolor(0,0,0.5)
box = viz.add("box.wrl")
box.setPosition(0,2,7)
AttachSpeechBubble(dest = box,message = "This text floats above the box",height = 1.0)