"""Contains the BufferViewer class, which is used as a debugging aid
when debugging render-to-texture effects. It shows different views at
the bottom of the screen showing the various render targets.
When using ShowBase, the normal way to enable the BufferViewer is using the
following code::
base.bufferViewer.toggleEnable()
Or, you can enable the following variable in your Config.prc::
show-buffers true
"""
__all__ = ['BufferViewer']
from panda3d.core import *
from direct.task import Task
from direct.task.TaskManagerGlobal import taskMgr
from direct.directnotify.DirectNotifyGlobal import *
from direct.showbase.DirectObject import DirectObject
import math
[docs]class BufferViewer(DirectObject):
notify = directNotify.newCategory('BufferViewer')
[docs] def __init__(self, win, parent):
"""Access: private. Constructor."""
self.enabled = 0
size = ConfigVariableDouble('buffer-viewer-size', '0 0')
self.sizex = size[0]
self.sizey = size[1]
self.position = ConfigVariableString('buffer-viewer-position', "lrcorner").getValue()
self.layout = ConfigVariableString('buffer-viewer-layout', "hline").getValue()
self.include = "all"
self.exclude = "none"
self.cullbin = "fixed"
self.cullsort = 10000
self.win = win
self.engine = GraphicsEngine.getGlobalPtr()
self.renderParent = parent
self.cards = []
self.cardindex = 0
self.cardmaker = CardMaker("cubemaker")
self.cardmaker.setFrame(-1,1,-1,1)
self.task = 0
self.dirty = 1
self.accept("render-texture-targets-changed", self.refreshReadout)
if (ConfigVariableBool("show-buffers", 0).getValue()):
self.enable(1)
[docs] def refreshReadout(self):
"""Force the readout to be refreshed. This is usually invoked
by GraphicsOutput::add_render_texture (via an event handler).
However, it is also possible to invoke it manually. Currently,
the only time I know of that this is necessary is after a
window resize (and I ought to fix that)."""
self.dirty = 1
# Call enabled again, mainly to ensure that the task has been
# started.
self.enable(self.enabled)
[docs] def isValidTextureSet(self, x):
"""Access: private. Returns true if the parameter is a
list of GraphicsOutput and Texture, or the keyword 'all'."""
if (isinstance(x, list)):
for elt in x:
if (self.isValidTextureSet(elt)==0):
return 0
else:
return (x=="all") or (isinstance(x, Texture)) or (isinstance(x, GraphicsOutput))
[docs] def isEnabled(self):
"""Returns true if the buffer viewer is currently enabled."""
return self.enabled
[docs] def enable(self, x):
"""Turn the buffer viewer on or off. The initial state of the
buffer viewer depends on the Config variable 'show-buffers'."""
if (x != 0) and (x != 1):
BufferViewer.notify.error('invalid parameter to BufferViewer.enable')
return
self.enabled = x
self.dirty = 1
if (x and self.task == 0):
self.task = taskMgr.add(self.maintainReadout, "buffer-viewer-maintain-readout",
priority=1)
[docs] def toggleEnable(self):
"""Toggle the buffer viewer on or off. The initial state of the
enable flag depends on the Config variable 'show-buffers'."""
self.enable(1-self.enabled)
[docs] def setCardSize(self, x, y):
"""Set the size of each card. The units are relative to
render2d (ie, 1x1 card is not square). If one of the
dimensions is zero, then the viewer will choose a value
for that dimension so as to ensure that the aspect ratio
of the card matches the aspect ratio of the source-window.
If both dimensions are zero, the viewer uses a heuristic
to choose a reasonable size for the card. The initial
value is (0, 0)."""
if (x < 0) or (y < 0):
BufferViewer.notify.error('invalid parameter to BufferViewer.setCardSize')
return
self.sizex = x
self.sizey = y
self.dirty = 1
[docs] def setPosition(self, pos):
"""Set the position of the cards. The valid values are:
- *llcorner* - put them in the lower-left corner of the window
- *lrcorner* - put them in the lower-right corner of the window
- *ulcorner* - put them in the upper-left corner of the window
- *urcorner* - put them in the upper-right corner of the window
- *window* - put them in a separate window
The initial value is 'lrcorner'."""
valid=["llcorner","lrcorner","ulcorner","urcorner","window"]
if (valid.count(pos)==0):
BufferViewer.notify.error('invalid parameter to BufferViewer.setPosition')
BufferViewer.notify.error('valid parameters are: llcorner, lrcorner, ulcorner, urcorner, window')
return
if (pos == "window"):
BufferViewer.notify.error('BufferViewer.setPosition - "window" mode not implemented yet.')
return
self.position = pos
self.dirty = 1
[docs] def setLayout(self, lay):
"""Set the layout of the cards. The valid values are:
- *vline* - display them in a vertical line
- *hline* - display them in a horizontal line
- *vgrid* - display them in a vertical grid
- *hgrid* - display them in a horizontal grid
- *cycle* - display one card at a time, using selectCard/advanceCard
The default value is 'hline'."""
valid=["vline","hline","vgrid","hgrid","cycle"]
if (valid.count(lay)==0):
BufferViewer.notify.error('invalid parameter to BufferViewer.setLayout')
BufferViewer.notify.error('valid parameters are: vline, hline, vgrid, hgrid, cycle')
return
self.layout = lay
self.dirty = 1
[docs] def selectCard(self, i):
"""Only useful when using setLayout('cycle'). Sets the index
that selects which card to display. The index is taken modulo
the actual number of cards."""
self.cardindex = i
self.dirty = 1
[docs] def advanceCard(self):
"""Only useful when using setLayout('cycle'). Increments the index
that selects which card to display. The index is taken modulo
the actual number of cards."""
self.cardindex += 1
self.dirty = 1
[docs] def setInclude(self, x):
"""Set the include-set for the buffer viewer. The include-set
specifies which of the render-to-texture targets to display.
Valid inputs are the string 'all' (display every render-to-texture
target), or a list of GraphicsOutputs or Textures. The initial
value is 'all'."""
if (self.isValidTextureSet(x)==0):
BufferViewer.notify.error('setInclude: must be list of textures and buffers, or "all"')
return
self.include = x
self.dirty = 1
[docs] def setExclude(self, x):
"""Set the exclude-set for the buffer viewer. The exclude-set
should be a list of GraphicsOutputs and Textures to ignore.
The exclude-set is subtracted from the include-set (so the excludes
effectively override the includes.) The initial value is the
empty list."""
if (self.isValidTextureSet(x)==0):
BufferViewer.notify.error('setExclude: must be list of textures and buffers')
return
self.exclude = x
self.dirty = 1
[docs] def setSort(self, bin, sort):
"""Set the cull-bin and sort-order for the output cards. The
default value is 'fixed', 10000."""
self.cullbin = bin
self.cullsort = sort
self.dirty = 1
[docs] def setRenderParent(self, renderParent):
"""Set the scene graph root to which the output cards should
be parented. The default is render2d. """
self.renderParent = renderParent
self.dirty = 1
[docs] def analyzeTextureSet(self, x, set):
"""Access: private. Converts a list of GraphicsObject,
GraphicsEngine, and Texture into a table of Textures."""
if (isinstance(x, list)):
for elt in x:
self.analyzeTextureSet(elt, set)
elif (isinstance(x, Texture)):
set[x] = 1
elif (isinstance(x, GraphicsOutput)):
for itex in range(x.countTextures()):
tex = x.getTexture(itex)
set[tex] = 1
elif (isinstance(x, GraphicsEngine)):
for iwin in range(x.getNumWindows()):
win = x.getWindow(iwin)
self.analyzeTextureSet(win, set)
elif (x=="all"):
self.analyzeTextureSet(self.engine, set)
else: return
[docs] def makeFrame(self, sizex, sizey):
"""Access: private. Each texture card is displayed with
a two-pixel wide frame (a ring of black and a ring of white).
This routine builds the frame geometry. It is necessary to
be precise so that the frame exactly aligns to pixel
boundaries, and so that it doesn't overlap the card at all."""
format=GeomVertexFormat.getV3cp()
vdata=GeomVertexData('card-frame', format, Geom.UHDynamic)
vwriter=GeomVertexWriter(vdata, 'vertex')
cwriter=GeomVertexWriter(vdata, 'color')
ringoffset = [0, 1, 1, 2]
ringbright = [0, 0, 1, 1]
for ring in range(4):
offsetx = (ringoffset[ring]*2.0) / float(sizex)
offsety = (ringoffset[ring]*2.0) / float(sizey)
bright = ringbright[ring]
vwriter.addData3f(Vec3F.rfu(-1 - offsetx, 0, -1 - offsety))
vwriter.addData3f(Vec3F.rfu( 1 + offsetx, 0, -1 - offsety))
vwriter.addData3f(Vec3F.rfu( 1 + offsetx, 0, 1 + offsety))
vwriter.addData3f(Vec3F.rfu(-1 - offsetx, 0, 1 + offsety))
cwriter.addData3f(bright, bright, bright)
cwriter.addData3f(bright, bright, bright)
cwriter.addData3f(bright, bright, bright)
cwriter.addData3f(bright, bright, bright)
triangles=GeomTriangles(Geom.UHStatic)
for i in range(2):
delta = i*8
triangles.addVertices(0+delta, 4+delta, 1+delta)
triangles.addVertices(1+delta, 4+delta, 5+delta)
triangles.addVertices(1+delta, 5+delta, 2+delta)
triangles.addVertices(2+delta, 5+delta, 6+delta)
triangles.addVertices(2+delta, 6+delta, 3+delta)
triangles.addVertices(3+delta, 6+delta, 7+delta)
triangles.addVertices(3+delta, 7+delta, 0+delta)
triangles.addVertices(0+delta, 7+delta, 4+delta)
triangles.closePrimitive()
geom=Geom(vdata)
geom.addPrimitive(triangles)
geomnode=GeomNode("card-frame")
geomnode.addGeom(geom)
return NodePath(geomnode)
[docs] def maintainReadout(self, task):
"""Access: private. Whenever necessary, rebuilds the entire
display from scratch. This is only done when the configuration
parameters have changed."""
# If nothing has changed, don't update.
if (self.dirty==0):
return Task.cont
self.dirty = 0
# Delete the old set of cards.
for card in self.cards:
card.removeNode()
self.cards = []
# If not enabled, return.
if (self.enabled == 0):
self.task = 0
return Task.done
# Generate the include and exclude sets.
exclude = {}
include = {}
self.analyzeTextureSet(self.exclude, exclude)
self.analyzeTextureSet(self.include, include)
# Use a custom sampler when applying the textures. This fixes
# wrap issues and prevents depth compare on shadow maps.
sampler = SamplerState()
sampler.setWrapU(SamplerState.WM_clamp)
sampler.setWrapV(SamplerState.WM_clamp)
sampler.setWrapW(SamplerState.WM_clamp)
sampler.setMinfilter(SamplerState.FT_linear)
sampler.setMagfilter(SamplerState.FT_nearest)
# Generate a list of cards and the corresponding windows.
cards = []
wins = []
for iwin in range(self.engine.getNumWindows()):
win = self.engine.getWindow(iwin)
for itex in range(win.countTextures()):
tex = win.getTexture(itex)
if (tex in include) and (tex not in exclude):
if (tex.getTextureType() == Texture.TTCubeMap):
for face in range(6):
self.cardmaker.setUvRangeCube(face)
card = NodePath(self.cardmaker.generate())
card.setTexture(tex, sampler)
cards.append(card)
elif (tex.getTextureType() == Texture.TT2dTextureArray):
for layer in range(tex.getZSize()):
self.cardmaker.setUvRange((0, 1, 1, 0), (0, 0, 1, 1),\
(layer, layer, layer, layer))
card = NodePath(self.cardmaker.generate())
# 2D texture arrays are not supported by
# the fixed-function pipeline, so we need to
# enable the shader generator to view them.
card.setShaderAuto()
card.setTexture(tex, sampler)
cards.append(card)
else:
card = win.getTextureCard()
card.setTexture(tex, sampler)
cards.append(card)
wins.append(win)
exclude[tex] = 1
self.cards = cards
if (len(cards)==0):
self.task = 0
return Task.done
ncards = len(cards)
# Decide how many rows and columns to use for the layout.
if (self.layout == "hline"):
rows = 1
cols = ncards
elif (self.layout == "vline"):
rows = ncards
cols = 1
elif (self.layout == "hgrid"):
rows = int(math.sqrt(ncards))
cols = rows
if (rows * cols < ncards): cols += 1
if (rows * cols < ncards): rows += 1
elif (self.layout == "vgrid"):
rows = int(math.sqrt(ncards))
cols = rows
if (rows * cols < ncards): rows += 1
if (rows * cols < ncards): cols += 1
elif (self.layout == "cycle"):
rows = 1
cols = 1
else:
BufferViewer.notify.error('shouldnt ever get here in BufferViewer.maintainReadout')
# Choose an aspect ratio for the cards. All card size
# calculations are done in pixel-units, using integers,
# in order to ensure that everything ends up neatly on
# a pixel boundary.
aspectx = wins[0].getXSize()
aspecty = wins[0].getYSize()
for win in wins:
if (win.getXSize()*aspecty) != (win.getYSize()*aspectx):
aspectx = 1
aspecty = 1
# Choose a card size. If the user didn't specify a size,
# use a heuristic, otherwise, just follow orders. The
# heuristic uses an initial card size of 42.66666667% of
# the screen vertically, which comes to 256 pixels on
# an 800x600 display. Then, it double checks that the
# readout will fit on the screen, and if not, it shrinks it.
bordersize = 4.0
if (float(self.sizex)==0.0) and (float(self.sizey)==0.0):
sizey = int(0.4266666667 * self.win.getYSize())
sizex = (sizey * aspectx) // aspecty
v_sizey = (self.win.getYSize() - (rows-1) - (rows*2)) // rows
v_sizex = (v_sizey * aspectx) // aspecty
if (v_sizey < sizey) or (v_sizex < sizex):
sizey = v_sizey
sizex = v_sizex
adjustment = 2
h_sizex = float (self.win.getXSize() - adjustment) / float (cols)
h_sizex -= bordersize
if (h_sizex < 1.0):
h_sizex = 1.0
h_sizey = (h_sizex * aspecty) // aspectx
if (h_sizey < sizey) or (h_sizex < sizex):
sizey = h_sizey
sizex = h_sizex
else:
sizex = int(self.sizex * 0.5 * self.win.getXSize())
sizey = int(self.sizey * 0.5 * self.win.getYSize())
if (sizex == 0): sizex = (sizey*aspectx) // aspecty
if (sizey == 0): sizey = (sizex*aspecty) // aspectx
# Convert from pixels to render2d-units.
fsizex = (2.0 * sizex) / float(self.win.getXSize())
fsizey = (2.0 * sizey) / float(self.win.getYSize())
fpixelx = 2.0 / float(self.win.getXSize())
fpixely = 2.0 / float(self.win.getYSize())
# Choose directional offsets
if (self.position == "llcorner"):
dirx = -1.0
diry = -1.0
elif (self.position == "lrcorner"):
dirx = 1.0
diry = -1.0
elif (self.position == "ulcorner"):
dirx = -1.0
diry = 1.0
elif (self.position == "urcorner"):
dirx = 1.0
diry = 1.0
else:
BufferViewer.notify.error('window mode not implemented yet')
# Create the frame
frame = self.makeFrame(sizex, sizey)
# Now, position the cards on the screen.
# For each card, create a frame consisting of eight quads.
for r in range(rows):
for c in range(cols):
index = c + r*cols
if (index < ncards):
index = (index + self.cardindex) % len(cards)
posx = dirx * (1.0 - ((c + 0.5) * (fsizex + fpixelx * bordersize))) - (fpixelx * dirx)
posy = diry * (1.0 - ((r + 0.5) * (fsizey + fpixely * bordersize))) - (fpixely * diry)
placer = NodePath("card-structure")
placer.setPos(Point3.rfu(posx, 0, posy))
placer.setScale(Vec3.rfu(fsizex*0.5, 1.0, fsizey*0.5))
placer.setBin(self.cullbin, self.cullsort)
placer.reparentTo(self.renderParent)
frame.instanceTo(placer)
cards[index].reparentTo(placer)
cards[index] = placer
return Task.cont