BufferViewer.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501
  1. """Contains the BufferViewer class, which is used as a debugging aid
  2. when debugging render-to-texture effects. It shows different views at
  3. the bottom of the screen showing the various render targets.
  4. When using ShowBase, the normal way to enable the BufferViewer is using the
  5. following code::
  6. base.bufferViewer.toggleEnable()
  7. Or, you can enable the following variable in your Config.prc::
  8. show-buffers true
  9. """
  10. __all__ = ['BufferViewer']
  11. from panda3d.core import (
  12. CardMaker,
  13. ConfigVariableBool,
  14. ConfigVariableDouble,
  15. ConfigVariableString,
  16. Geom,
  17. GeomNode,
  18. GeomTriangles,
  19. GeomVertexData,
  20. GeomVertexFormat,
  21. GeomVertexWriter,
  22. GraphicsEngine,
  23. GraphicsOutput,
  24. NodePath,
  25. Point3,
  26. SamplerState,
  27. Texture,
  28. Vec3,
  29. Vec3F,
  30. )
  31. from direct.task import Task
  32. from direct.task.TaskManagerGlobal import taskMgr
  33. from direct.directnotify.DirectNotifyGlobal import directNotify
  34. from direct.showbase.DirectObject import DirectObject
  35. import math
  36. class BufferViewer(DirectObject):
  37. notify = directNotify.newCategory('BufferViewer')
  38. def __init__(self, win, parent):
  39. """Access: private. Constructor."""
  40. self.enabled = 0
  41. size = ConfigVariableDouble('buffer-viewer-size', '0 0')
  42. self.sizex = size[0]
  43. self.sizey = size[1]
  44. self.position = ConfigVariableString('buffer-viewer-position', "lrcorner").getValue()
  45. self.layout = ConfigVariableString('buffer-viewer-layout', "hline").getValue()
  46. self.include = "all"
  47. self.exclude = "none"
  48. self.cullbin = "fixed"
  49. self.cullsort = 10000
  50. self.win = win
  51. self.engine = GraphicsEngine.getGlobalPtr()
  52. self.renderParent = parent
  53. self.cards = []
  54. self.cardindex = 0
  55. self.cardmaker = CardMaker("cubemaker")
  56. self.cardmaker.setFrame(-1,1,-1,1)
  57. self.task = 0
  58. self.dirty = 1
  59. self.accept("render-texture-targets-changed", self.refreshReadout)
  60. if ConfigVariableBool("show-buffers", 0):
  61. self.enable(1)
  62. def refreshReadout(self):
  63. """Force the readout to be refreshed. This is usually invoked
  64. by GraphicsOutput::add_render_texture (via an event handler).
  65. However, it is also possible to invoke it manually. Currently,
  66. the only time I know of that this is necessary is after a
  67. window resize (and I ought to fix that)."""
  68. self.dirty = 1
  69. # Call enabled again, mainly to ensure that the task has been
  70. # started.
  71. self.enable(self.enabled)
  72. def isValidTextureSet(self, x):
  73. """Access: private. Returns true if the parameter is a
  74. list of GraphicsOutput and Texture, or the keyword 'all'."""
  75. if isinstance(x, list):
  76. for elt in x:
  77. if not self.isValidTextureSet(elt):
  78. return 0
  79. else:
  80. return x == "all" or isinstance(x, Texture) or isinstance(x, GraphicsOutput)
  81. def isEnabled(self):
  82. """Returns true if the buffer viewer is currently enabled."""
  83. return self.enabled
  84. def enable(self, x):
  85. """Turn the buffer viewer on or off. The initial state of the
  86. buffer viewer depends on the Config variable 'show-buffers'."""
  87. if x != 0 and x != 1:
  88. BufferViewer.notify.error('invalid parameter to BufferViewer.enable')
  89. return
  90. self.enabled = x
  91. self.dirty = 1
  92. if (x and self.task == 0):
  93. self.task = taskMgr.add(self.maintainReadout, "buffer-viewer-maintain-readout",
  94. priority=1)
  95. def toggleEnable(self):
  96. """Toggle the buffer viewer on or off. The initial state of the
  97. enable flag depends on the Config variable 'show-buffers'."""
  98. self.enable(1-self.enabled)
  99. def setCardSize(self, x, y):
  100. """Set the size of each card. The units are relative to
  101. render2d (ie, 1x1 card is not square). If one of the
  102. dimensions is zero, then the viewer will choose a value
  103. for that dimension so as to ensure that the aspect ratio
  104. of the card matches the aspect ratio of the source-window.
  105. If both dimensions are zero, the viewer uses a heuristic
  106. to choose a reasonable size for the card. The initial
  107. value is (0, 0)."""
  108. if x < 0 or y < 0:
  109. BufferViewer.notify.error('invalid parameter to BufferViewer.setCardSize')
  110. return
  111. self.sizex = x
  112. self.sizey = y
  113. self.dirty = 1
  114. def setPosition(self, pos):
  115. """Set the position of the cards. The valid values are:
  116. - *llcorner* - put them in the lower-left corner of the window
  117. - *lrcorner* - put them in the lower-right corner of the window
  118. - *ulcorner* - put them in the upper-left corner of the window
  119. - *urcorner* - put them in the upper-right corner of the window
  120. - *window* - put them in a separate window
  121. The initial value is 'lrcorner'."""
  122. valid = ["llcorner", "lrcorner", "ulcorner", "urcorner", "window"]
  123. if valid.count(pos) == 0:
  124. BufferViewer.notify.error('invalid parameter to BufferViewer.setPosition')
  125. BufferViewer.notify.error('valid parameters are: llcorner, lrcorner, ulcorner, urcorner, window')
  126. return
  127. if pos == "window":
  128. BufferViewer.notify.error('BufferViewer.setPosition - "window" mode not implemented yet.')
  129. return
  130. self.position = pos
  131. self.dirty = 1
  132. def setLayout(self, lay):
  133. """Set the layout of the cards. The valid values are:
  134. - *vline* - display them in a vertical line
  135. - *hline* - display them in a horizontal line
  136. - *vgrid* - display them in a vertical grid
  137. - *hgrid* - display them in a horizontal grid
  138. - *cycle* - display one card at a time, using selectCard/advanceCard
  139. The default value is 'hline'."""
  140. valid=["vline", "hline", "vgrid", "hgrid", "cycle"]
  141. if valid.count(lay) == 0:
  142. BufferViewer.notify.error('invalid parameter to BufferViewer.setLayout')
  143. BufferViewer.notify.error('valid parameters are: vline, hline, vgrid, hgrid, cycle')
  144. return
  145. self.layout = lay
  146. self.dirty = 1
  147. def selectCard(self, i):
  148. """Only useful when using setLayout('cycle'). Sets the index
  149. that selects which card to display. The index is taken modulo
  150. the actual number of cards."""
  151. self.cardindex = i
  152. self.dirty = 1
  153. def advanceCard(self):
  154. """Only useful when using setLayout('cycle'). Increments the index
  155. that selects which card to display. The index is taken modulo
  156. the actual number of cards."""
  157. self.cardindex += 1
  158. self.dirty = 1
  159. def setInclude(self, x):
  160. """Set the include-set for the buffer viewer. The include-set
  161. specifies which of the render-to-texture targets to display.
  162. Valid inputs are the string 'all' (display every render-to-texture
  163. target), or a list of GraphicsOutputs or Textures. The initial
  164. value is 'all'."""
  165. if not self.isValidTextureSet(x):
  166. BufferViewer.notify.error('setInclude: must be list of textures and buffers, or "all"')
  167. return
  168. self.include = x
  169. self.dirty = 1
  170. def setExclude(self, x):
  171. """Set the exclude-set for the buffer viewer. The exclude-set
  172. should be a list of GraphicsOutputs and Textures to ignore.
  173. The exclude-set is subtracted from the include-set (so the excludes
  174. effectively override the includes.) The initial value is the
  175. empty list."""
  176. if not self.isValidTextureSet(x):
  177. BufferViewer.notify.error('setExclude: must be list of textures and buffers')
  178. return
  179. self.exclude = x
  180. self.dirty = 1
  181. def setSort(self, bin, sort):
  182. """Set the cull-bin and sort-order for the output cards. The
  183. default value is 'fixed', 10000."""
  184. self.cullbin = bin
  185. self.cullsort = sort
  186. self.dirty = 1
  187. def setRenderParent(self, renderParent):
  188. """Set the scene graph root to which the output cards should
  189. be parented. The default is render2d. """
  190. self.renderParent = renderParent
  191. self.dirty = 1
  192. def analyzeTextureSet(self, x, set):
  193. """Access: private. Converts a list of GraphicsObject,
  194. GraphicsEngine, and Texture into a table of Textures."""
  195. if isinstance(x, list):
  196. for elt in x:
  197. self.analyzeTextureSet(elt, set)
  198. elif isinstance(x, Texture):
  199. set[x] = 1
  200. elif isinstance(x, GraphicsOutput):
  201. for itex in range(x.countTextures()):
  202. tex = x.getTexture(itex)
  203. set[tex] = 1
  204. elif isinstance(x, GraphicsEngine):
  205. for iwin in range(x.getNumWindows()):
  206. win = x.getWindow(iwin)
  207. self.analyzeTextureSet(win, set)
  208. elif x == "all":
  209. self.analyzeTextureSet(self.engine, set)
  210. else:
  211. return
  212. def makeFrame(self, sizex, sizey):
  213. """Access: private. Each texture card is displayed with
  214. a two-pixel wide frame (a ring of black and a ring of white).
  215. This routine builds the frame geometry. It is necessary to
  216. be precise so that the frame exactly aligns to pixel
  217. boundaries, and so that it doesn't overlap the card at all."""
  218. format = GeomVertexFormat.getV3c()
  219. vdata = GeomVertexData('card-frame', format, Geom.UHDynamic)
  220. vwriter = GeomVertexWriter(vdata, 'vertex')
  221. cwriter = GeomVertexWriter(vdata, 'color')
  222. ringoffset = [0, 1, 1, 2]
  223. ringbright = [0, 0, 1, 1]
  224. for ring in range(4):
  225. offsetx = (ringoffset[ring]*2.0) / float(sizex)
  226. offsety = (ringoffset[ring]*2.0) / float(sizey)
  227. bright = ringbright[ring]
  228. vwriter.addData3f(Vec3F.rfu(-1 - offsetx, 0, -1 - offsety))
  229. vwriter.addData3f(Vec3F.rfu( 1 + offsetx, 0, -1 - offsety))
  230. vwriter.addData3f(Vec3F.rfu( 1 + offsetx, 0, 1 + offsety))
  231. vwriter.addData3f(Vec3F.rfu(-1 - offsetx, 0, 1 + offsety))
  232. cwriter.addData3f(bright, bright, bright)
  233. cwriter.addData3f(bright, bright, bright)
  234. cwriter.addData3f(bright, bright, bright)
  235. cwriter.addData3f(bright, bright, bright)
  236. triangles = GeomTriangles(Geom.UHStatic)
  237. for i in range(2):
  238. delta = i*8
  239. triangles.addVertices(0 + delta, 4 + delta, 1 + delta)
  240. triangles.addVertices(1 + delta, 4 + delta, 5 + delta)
  241. triangles.addVertices(1 + delta, 5 + delta, 2 + delta)
  242. triangles.addVertices(2 + delta, 5 + delta, 6 + delta)
  243. triangles.addVertices(2 + delta, 6 + delta, 3 + delta)
  244. triangles.addVertices(3 + delta, 6 + delta, 7 + delta)
  245. triangles.addVertices(3 + delta, 7 + delta, 0 + delta)
  246. triangles.addVertices(0 + delta, 7 + delta, 4 + delta)
  247. triangles.closePrimitive()
  248. geom = Geom(vdata)
  249. geom.addPrimitive(triangles)
  250. geomnode=GeomNode("card-frame")
  251. geomnode.addGeom(geom)
  252. return NodePath(geomnode)
  253. def maintainReadout(self, task):
  254. """Access: private. Whenever necessary, rebuilds the entire
  255. display from scratch. This is only done when the configuration
  256. parameters have changed."""
  257. # If nothing has changed, don't update.
  258. if not self.dirty:
  259. return Task.cont
  260. self.dirty = 0
  261. # Delete the old set of cards.
  262. for card in self.cards:
  263. card.removeNode()
  264. self.cards = []
  265. # If not enabled, return.
  266. if not self.enabled:
  267. self.task = 0
  268. return Task.done
  269. # Generate the include and exclude sets.
  270. exclude = {}
  271. include = {}
  272. self.analyzeTextureSet(self.exclude, exclude)
  273. self.analyzeTextureSet(self.include, include)
  274. # Use a custom sampler when applying the textures. This fixes
  275. # wrap issues and prevents depth compare on shadow maps.
  276. sampler = SamplerState()
  277. sampler.setWrapU(SamplerState.WM_clamp)
  278. sampler.setWrapV(SamplerState.WM_clamp)
  279. sampler.setWrapW(SamplerState.WM_clamp)
  280. sampler.setMinfilter(SamplerState.FT_linear)
  281. sampler.setMagfilter(SamplerState.FT_nearest)
  282. # Generate a list of cards and the corresponding windows.
  283. cards = []
  284. wins = []
  285. for iwin in range(self.engine.getNumWindows()):
  286. win = self.engine.getWindow(iwin)
  287. for itex in range(win.countTextures()):
  288. tex = win.getTexture(itex)
  289. if (tex in include) and (tex not in exclude):
  290. if tex.getTextureType() == Texture.TTCubeMap:
  291. for face in range(6):
  292. self.cardmaker.setUvRangeCube(face)
  293. card = NodePath(self.cardmaker.generate())
  294. card.setTexture(tex, sampler)
  295. cards.append(card)
  296. elif tex.getTextureType() == Texture.TT2dTextureArray:
  297. for layer in range(tex.getZSize()):
  298. self.cardmaker.setUvRange((0, 1, 1, 0), (0, 0, 1, 1),\
  299. (layer, layer, layer, layer))
  300. card = NodePath(self.cardmaker.generate())
  301. # 2D texture arrays are not supported by
  302. # the fixed-function pipeline, so we need to
  303. # enable the shader generator to view them.
  304. card.setShaderAuto()
  305. card.setTexture(tex, sampler)
  306. cards.append(card)
  307. else:
  308. card = win.getTextureCard()
  309. card.setTexture(tex, sampler)
  310. cards.append(card)
  311. wins.append(win)
  312. exclude[tex] = 1
  313. self.cards = cards
  314. if len(cards) == 0:
  315. self.task = 0
  316. return Task.done
  317. ncards = len(cards)
  318. # Decide how many rows and columns to use for the layout.
  319. if self.layout == "hline":
  320. rows = 1
  321. cols = ncards
  322. elif self.layout == "vline":
  323. rows = ncards
  324. cols = 1
  325. elif self.layout == "hgrid":
  326. rows = int(math.sqrt(ncards))
  327. cols = rows
  328. if rows * cols < ncards:
  329. cols += 1
  330. if rows * cols < ncards:
  331. rows += 1
  332. elif self.layout == "vgrid":
  333. rows = int(math.sqrt(ncards))
  334. cols = rows
  335. if rows * cols < ncards:
  336. rows += 1
  337. if rows * cols < ncards:
  338. cols += 1
  339. elif self.layout == "cycle":
  340. rows = 1
  341. cols = 1
  342. else:
  343. BufferViewer.notify.error('shouldnt ever get here in BufferViewer.maintainReadout')
  344. # Choose an aspect ratio for the cards. All card size
  345. # calculations are done in pixel-units, using integers,
  346. # in order to ensure that everything ends up neatly on
  347. # a pixel boundary.
  348. aspectx = wins[0].getXSize()
  349. aspecty = wins[0].getYSize()
  350. for win in wins:
  351. if win.getXSize() * aspecty != win.getYSize() * aspectx:
  352. aspectx = 1
  353. aspecty = 1
  354. # Choose a card size. If the user didn't specify a size,
  355. # use a heuristic, otherwise, just follow orders. The
  356. # heuristic uses an initial card size of 42.66666667% of
  357. # the screen vertically, which comes to 256 pixels on
  358. # an 800x600 display. Then, it double checks that the
  359. # readout will fit on the screen, and if not, it shrinks it.
  360. bordersize = 4.0
  361. if float(self.sizex) == 0.0 and float(self.sizey) == 0.0:
  362. sizey = int(0.4266666667 * self.win.getYSize())
  363. sizex = (sizey * aspectx) // aspecty
  364. v_sizey = (self.win.getYSize() - (rows - 1) - (rows * 2)) // rows
  365. v_sizex = (v_sizey * aspectx) // aspecty
  366. if (v_sizey < sizey) or (v_sizex < sizex):
  367. sizey = v_sizey
  368. sizex = v_sizex
  369. adjustment = 2
  370. h_sizex = float(self.win.getXSize() - adjustment) / float(cols)
  371. h_sizex -= bordersize
  372. if h_sizex < 1.0:
  373. h_sizex = 1.0
  374. h_sizey = (h_sizex * aspecty) // aspectx
  375. if h_sizey < sizey or h_sizex < sizex:
  376. sizey = h_sizey
  377. sizex = h_sizex
  378. else:
  379. sizex = int(self.sizex * 0.5 * self.win.getXSize())
  380. sizey = int(self.sizey * 0.5 * self.win.getYSize())
  381. if sizex == 0:
  382. sizex = (sizey * aspectx) // aspecty
  383. if sizey == 0:
  384. sizey = (sizex * aspecty) // aspectx
  385. # Convert from pixels to render2d-units.
  386. fsizex = (2.0 * sizex) / float(self.win.getXSize())
  387. fsizey = (2.0 * sizey) / float(self.win.getYSize())
  388. fpixelx = 2.0 / float(self.win.getXSize())
  389. fpixely = 2.0 / float(self.win.getYSize())
  390. # Choose directional offsets
  391. if self.position == "llcorner":
  392. dirx = -1.0
  393. diry = -1.0
  394. elif self.position == "lrcorner":
  395. dirx = 1.0
  396. diry = -1.0
  397. elif self.position == "ulcorner":
  398. dirx = -1.0
  399. diry = 1.0
  400. elif self.position == "urcorner":
  401. dirx = 1.0
  402. diry = 1.0
  403. else:
  404. BufferViewer.notify.error('window mode not implemented yet')
  405. # Create the frame
  406. frame = self.makeFrame(sizex, sizey)
  407. # Now, position the cards on the screen.
  408. # For each card, create a frame consisting of eight quads.
  409. for r in range(rows):
  410. for c in range(cols):
  411. index = c + r * cols
  412. if index < ncards:
  413. index = (index + self.cardindex) % len(cards)
  414. posx = dirx * (1.0 - ((c + 0.5) * (fsizex + fpixelx * bordersize))) - (fpixelx * dirx)
  415. posy = diry * (1.0 - ((r + 0.5) * (fsizey + fpixely * bordersize))) - (fpixely * diry)
  416. placer = NodePath("card-structure")
  417. placer.setPos(Point3.rfu(posx, 0, posy))
  418. placer.setScale(Vec3.rfu(fsizex * 0.5, 1.0, fsizey * 0.5))
  419. placer.setBin(self.cullbin, self.cullsort)
  420. placer.reparentTo(self.renderParent)
  421. frame.instanceTo(placer)
  422. cards[index].reparentTo(placer)
  423. cards[index] = placer
  424. return Task.cont
  425. # Snake-case aliases, for people who prefer these.
  426. advance_card = advanceCard
  427. analyze_texture_set = analyzeTextureSet
  428. is_enabled = isEnabled
  429. is_valid_texture_set = isValidTextureSet
  430. maintain_readout = maintainReadout
  431. make_frame = makeFrame
  432. refresh_readout = refreshReadout
  433. select_card = selectCard
  434. set_card_size = setCardSize
  435. set_exclude = setExclude
  436. set_include = setInclude
  437. set_layout = setLayout
  438. set_position = setPosition
  439. set_render_parent = setRenderParent
  440. set_sort = setSort
  441. toggle_enable = toggleEnable