Loader.py 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970
  1. """Loader module: contains the Loader class"""
  2. __all__ = ['Loader']
  3. from panda3d.core import *
  4. from panda3d.core import Loader as PandaLoader
  5. from direct.directnotify.DirectNotifyGlobal import *
  6. from direct.showbase.DirectObject import DirectObject
  7. # You can specify a phaseChecker callback to check
  8. # a modelPath to see if it is being loaded in the correct
  9. # phase
  10. phaseChecker = None
  11. class Loader(DirectObject):
  12. """
  13. Load models, textures, sounds, and code.
  14. """
  15. notify = directNotify.newCategory("Loader")
  16. loaderIndex = 0
  17. class Callback:
  18. def __init__(self, numObjects, gotList, callback, extraArgs):
  19. self.objects = [None] * numObjects
  20. self.gotList = gotList
  21. self.callback = callback
  22. self.extraArgs = extraArgs
  23. self.numRemaining = numObjects
  24. self.cancelled = False
  25. self.requests = set()
  26. def gotObject(self, index, object):
  27. self.objects[index] = object
  28. self.numRemaining -= 1
  29. if self.numRemaining == 0:
  30. if self.gotList:
  31. self.callback(self.objects, *self.extraArgs)
  32. else:
  33. self.callback(*(self.objects + self.extraArgs))
  34. # special methods
  35. def __init__(self, base):
  36. self.base = base
  37. self.loader = PandaLoader.getGlobalPtr()
  38. self.__requests = {}
  39. self.hook = "async_loader_%s" % (Loader.loaderIndex)
  40. Loader.loaderIndex += 1
  41. self.accept(self.hook, self.__gotAsyncObject)
  42. def destroy(self):
  43. self.ignore(self.hook)
  44. self.loader.stopThreads()
  45. del self.base
  46. del self.loader
  47. # model loading funcs
  48. def loadModel(self, modelPath, loaderOptions = None, noCache = None,
  49. allowInstance = False, okMissing = None,
  50. callback = None, extraArgs = [], priority = None):
  51. """
  52. Attempts to load a model or models from one or more relative
  53. pathnames. If the input modelPath is a string (a single model
  54. pathname), the return value will be a NodePath to the model
  55. loaded if the load was successful, or None otherwise. If the
  56. input modelPath is a list of pathnames, the return value will
  57. be a list of NodePaths and/or Nones.
  58. loaderOptions may optionally be passed in to control details
  59. about the way the model is searched and loaded. See the
  60. LoaderOptions class for more.
  61. The default is to look in the ModelPool (RAM) cache first, and
  62. return a copy from that if the model can be found there. If
  63. the bam cache is enabled (via the model-cache-dir config
  64. variable), then that will be consulted next, and if both
  65. caches fail, the file will be loaded from disk. If noCache is
  66. True, then neither cache will be consulted or updated.
  67. If allowInstance is True, a shared instance may be returned
  68. from the ModelPool. This is dangerous, since it is easy to
  69. accidentally modify the shared instance, and invalidate future
  70. load attempts of the same model. Normally, you should leave
  71. allowInstance set to False, which will always return a unique
  72. copy.
  73. If okMissing is True, None is returned if the model is not
  74. found or cannot be read, and no error message is printed.
  75. Otherwise, an IOError is raised if the model is not found or
  76. cannot be read (similar to attempting to open a nonexistent
  77. file). (If modelPath is a list of filenames, then IOError is
  78. raised if *any* of the models could not be loaded.)
  79. If callback is not None, then the model load will be performed
  80. asynchronously. In this case, loadModel() will initiate a
  81. background load and return immediately. The return value will
  82. be an object that may later be passed to
  83. loader.cancelRequest() to cancel the asynchronous request. At
  84. some later point, when the requested model(s) have finished
  85. loading, the callback function will be invoked with the n
  86. loaded models passed as its parameter list. It is possible
  87. that the callback will be invoked immediately, even before
  88. loadModel() returns. If you use callback, you may also
  89. specify a priority, which specifies the relative importance
  90. over this model over all of the other asynchronous load
  91. requests (higher numbers are loaded first).
  92. True asynchronous model loading requires Panda to have been
  93. compiled with threading support enabled (you can test
  94. Thread.isThreadingSupported()). In the absence of threading
  95. support, the asynchronous interface still exists and still
  96. behaves exactly as described, except that loadModel() might
  97. not return immediately.
  98. """
  99. assert Loader.notify.debug("Loading model: %s" % (modelPath))
  100. if loaderOptions is None:
  101. loaderOptions = LoaderOptions()
  102. else:
  103. loaderOptions = LoaderOptions(loaderOptions)
  104. if okMissing is not None:
  105. if okMissing:
  106. loaderOptions.setFlags(loaderOptions.getFlags() & ~LoaderOptions.LFReportErrors)
  107. else:
  108. loaderOptions.setFlags(loaderOptions.getFlags() | LoaderOptions.LFReportErrors)
  109. else:
  110. okMissing = ((loaderOptions.getFlags() & LoaderOptions.LFReportErrors) == 0)
  111. if noCache is not None:
  112. if noCache:
  113. loaderOptions.setFlags(loaderOptions.getFlags() | LoaderOptions.LFNoCache)
  114. else:
  115. loaderOptions.setFlags(loaderOptions.getFlags() & ~LoaderOptions.LFNoCache)
  116. if allowInstance:
  117. loaderOptions.setFlags(loaderOptions.getFlags() | LoaderOptions.LFAllowInstance)
  118. if not isinstance(modelPath, (tuple, list, set)):
  119. # We were given a single model pathname.
  120. modelList = [modelPath]
  121. if phaseChecker:
  122. phaseChecker(modelPath, loaderOptions)
  123. gotList = False
  124. else:
  125. # Assume we were given a list of model pathnames.
  126. modelList = modelPath
  127. gotList = True
  128. if callback is None:
  129. # We got no callback, so it's a synchronous load.
  130. result = []
  131. for modelPath in modelList:
  132. node = self.loader.loadSync(Filename(modelPath), loaderOptions)
  133. if node is not None:
  134. nodePath = NodePath(node)
  135. else:
  136. nodePath = None
  137. result.append(nodePath)
  138. if not okMissing and None in result:
  139. message = 'Could not load model file(s): %s' % (modelList,)
  140. raise IOError(message)
  141. if gotList:
  142. return result
  143. else:
  144. return result[0]
  145. else:
  146. # We got a callback, so we want an asynchronous (threaded)
  147. # load. We'll return immediately, but when all of the
  148. # requested models have been loaded, we'll invoke the
  149. # callback (passing it the models on the parameter list).
  150. cb = Loader.Callback(len(modelList), gotList, callback, extraArgs)
  151. i = 0
  152. for modelPath in modelList:
  153. request = self.loader.makeAsyncRequest(Filename(modelPath), loaderOptions)
  154. if priority is not None:
  155. request.setPriority(priority)
  156. request.setDoneEvent(self.hook)
  157. self.loader.loadAsync(request)
  158. cb.requests.add(request)
  159. self.__requests[request] = (cb, i)
  160. i += 1
  161. return cb
  162. def cancelRequest(self, cb):
  163. """Cancels an aysynchronous loading or flatten request issued
  164. earlier. The callback associated with the request will not be
  165. called after cancelRequest() has been performed. """
  166. if not cb.cancelled:
  167. cb.cancelled = True
  168. for request in cb.requests:
  169. self.loader.remove(request)
  170. del self.__requests[request]
  171. cb.requests = None
  172. def isRequestPending(self, cb):
  173. """ Returns true if an asynchronous loading or flatten request
  174. issued earlier is still pending, or false if it has completed or
  175. been cancelled. """
  176. return bool(cb.requests)
  177. def loadModelOnce(self, modelPath):
  178. """
  179. modelPath is a string.
  180. Attempt to load a model from modelPool, if not present
  181. then attempt to load it from disk. Return a nodepath to
  182. the model if successful or None otherwise
  183. """
  184. Loader.notify.info("loader.loadModelOnce() is deprecated; use loader.loadModel() instead.")
  185. return self.loadModel(modelPath, noCache = False)
  186. def loadModelCopy(self, modelPath, loaderOptions = None):
  187. """loadModelCopy(self, string)
  188. NOTE: This method is deprecated and should not be used.
  189. Attempt to load a model from modelPool, if not present
  190. then attempt to load it from disk. Return a nodepath to
  191. a copy of the model if successful or None otherwise
  192. """
  193. Loader.notify.info("loader.loadModelCopy() is deprecated; use loader.loadModel() instead.")
  194. return self.loadModel(modelPath, loaderOptions = loaderOptions, noCache = False)
  195. def loadModelNode(self, modelPath):
  196. """
  197. modelPath is a string.
  198. This is like loadModelOnce in that it loads a model from the
  199. modelPool, but it does not then instance it to hidden and it
  200. returns a Node instead of a NodePath. This is particularly
  201. useful for special models like fonts that you don't care about
  202. where they're parented to, and you don't want a NodePath
  203. anyway--it prevents accumulation of instances of the font
  204. model under hidden.
  205. However, if you're loading a font, see loadFont(), below.
  206. """
  207. Loader.notify.info("loader.loadModelNode() is deprecated; use loader.loadModel() instead.")
  208. model = self.loadModel(modelPath, noCache = False)
  209. if model is not None:
  210. model = model.node()
  211. return model
  212. def unloadModel(self, model):
  213. """
  214. model is the return value of loadModel(). For backward
  215. compatibility, it may also be the filename that was passed to
  216. loadModel(), though this requires a disk search.
  217. """
  218. if isinstance(model, NodePath):
  219. # Maybe we were given a NodePath
  220. modelNode = model.node()
  221. elif isinstance(model, ModelNode):
  222. # Maybe we were given a node
  223. modelNode = model
  224. elif isinstance(model, (str, Filename)):
  225. # If we were given a filename, we have to ask the loader
  226. # to resolve it for us.
  227. options = LoaderOptions(LoaderOptions.LFSearch | LoaderOptions.LFNoDiskCache | LoaderOptions.LFCacheOnly)
  228. modelNode = self.loader.loadSync(Filename(model), options)
  229. if modelNode is None:
  230. # Model not found.
  231. assert Loader.notify.debug("Unloading model not loaded: %s" % (model))
  232. return
  233. assert Loader.notify.debug("%s resolves to %s" % (model, modelNode.getFullpath()))
  234. else:
  235. raise TypeError('Invalid parameter to unloadModel: %s' % (model))
  236. assert Loader.notify.debug("Unloading model: %s" % (modelNode.getFullpath()))
  237. ModelPool.releaseModel(modelNode)
  238. def saveModel(self, modelPath, node, loaderOptions = None,
  239. callback = None, extraArgs = [], priority = None):
  240. """ Saves the model (a NodePath or PandaNode) to the indicated
  241. filename path. Returns true on success, false on failure. If
  242. a callback is used, the model is saved asynchronously, and the
  243. true/false status is passed to the callback function. """
  244. if loaderOptions is None:
  245. loaderOptions = LoaderOptions()
  246. else:
  247. loaderOptions = LoaderOptions(loaderOptions)
  248. if not isinstance(modelPath, (tuple, list, set)):
  249. # We were given a single model pathname.
  250. modelList = [modelPath]
  251. nodeList = [node]
  252. if phaseChecker:
  253. phaseChecker(modelPath, loaderOptions)
  254. gotList = False
  255. else:
  256. # Assume we were given a list of model pathnames.
  257. modelList = modelPath
  258. nodeList = node
  259. gotList = True
  260. assert(len(modelList) == len(nodeList))
  261. # Make sure we have PandaNodes, not NodePaths.
  262. for i in range(len(nodeList)):
  263. if isinstance(nodeList[i], NodePath):
  264. nodeList[i] = nodeList[i].node()
  265. # From here on, we deal with a list of (filename, node) pairs.
  266. modelList = list(zip(modelList, nodeList))
  267. if callback is None:
  268. # We got no callback, so it's a synchronous save.
  269. result = []
  270. for modelPath, node in modelList:
  271. thisResult = self.loader.saveSync(Filename(modelPath), loaderOptions, node)
  272. result.append(thisResult)
  273. if gotList:
  274. return result
  275. else:
  276. return result[0]
  277. else:
  278. # We got a callback, so we want an asynchronous (threaded)
  279. # save. We'll return immediately, but when all of the
  280. # requested models have been saved, we'll invoke the
  281. # callback (passing it the models on the parameter list).
  282. cb = Loader.Callback(len(modelList), gotList, callback, extraArgs)
  283. i = 0
  284. for modelPath, node in modelList:
  285. request = self.loader.makeAsyncSaveRequest(Filename(modelPath), loaderOptions, node)
  286. if priority is not None:
  287. request.setPriority(priority)
  288. request.setDoneEvent(self.hook)
  289. self.loader.saveAsync(request)
  290. cb.requests.add(request)
  291. self.__requests[request] = (cb, i)
  292. i += 1
  293. return cb
  294. # font loading funcs
  295. def loadFont(self, modelPath,
  296. spaceAdvance = None, lineHeight = None,
  297. pointSize = None,
  298. pixelsPerUnit = None, scaleFactor = None,
  299. textureMargin = None, polyMargin = None,
  300. minFilter = None, magFilter = None,
  301. anisotropicDegree = None,
  302. color = None,
  303. outlineWidth = None,
  304. outlineFeather = 0.1,
  305. outlineColor = VBase4(0, 0, 0, 1),
  306. renderMode = None,
  307. okMissing = False):
  308. """
  309. modelPath is a string.
  310. This loads a special model as a TextFont object, for rendering
  311. text with a TextNode. A font file must be either a special
  312. egg file (or bam file) generated with egg-mkfont, which is
  313. considered a static font, or a standard font file (like a TTF
  314. file) that is supported by FreeType, which is considered a
  315. dynamic font.
  316. okMissing should be True to indicate the method should return
  317. None if the font file is not found. If it is False, the
  318. method will raise an exception if the font file is not found
  319. or cannot be loaded.
  320. Most font-customization parameters accepted by this method
  321. (except lineHeight and spaceAdvance) may only be specified for
  322. dynamic font files like TTF files, not for static egg files.
  323. lineHeight specifies the vertical distance between consecutive
  324. lines, in Panda units. If unspecified, it is taken from the
  325. font information. This parameter may be specified for static
  326. as well as dynamic fonts.
  327. spaceAdvance specifies the width of a space character (ascii
  328. 32), in Panda units. If unspecified, it is taken from the
  329. font information. This may be specified for static as well as
  330. dynamic fonts.
  331. The remaining parameters may only be specified for dynamic
  332. fonts.
  333. pixelsPerUnit controls the visual quality of the rendered text
  334. characters. It specifies the number of texture pixels per
  335. each Panda unit of character height. Increasing this number
  336. increases the amount of detail that can be represented in the
  337. characters, at the expense of texture memory.
  338. scaleFactor also controls the visual quality of the rendered
  339. text characters. It is the amount by which the characters are
  340. rendered bigger out of Freetype, and then downscaled to fit
  341. within the texture. Increasing this number may reduce some
  342. artifacts of very small font characters, at a small cost of
  343. processing time to generate the characters initially.
  344. textureMargin specifies the number of pixels of the texture to
  345. leave between adjacent characters. It may be a floating-point
  346. number. This helps reduce bleed-through from nearby
  347. characters within the texture space. Increasing this number
  348. reduces artifacts at the edges of the character cells
  349. (especially for very small text scales), at the expense of
  350. texture memory.
  351. polyMargin specifies the amount of additional buffer to create
  352. in the polygon that represents each character, in Panda units.
  353. It is similar to textureMargin, but it controls the polygon
  354. buffer, not the texture buffer. Increasing this number
  355. reduces artifacts from letters getting chopped off at the
  356. edges (especially for very small text scales), with some
  357. increasing risk of adjacent letters overlapping and obscuring
  358. each other.
  359. minFilter, magFilter, and anisotropicDegree specify the
  360. texture filter modes that should be applied to the textures
  361. that are created to hold the font characters.
  362. If color is not None, it should be a VBase4 specifying the
  363. foreground color of the font. Specifying this option breaks
  364. TextNode.setColor(), so you almost never want to use this
  365. option; the default (white) is the most appropriate for a
  366. font, as it allows text to have any arbitrary color assigned
  367. at generation time. However, if you want to use a colored
  368. outline (below) with a different color for the interior, for
  369. instance a yellow letter with a blue outline, then you need
  370. this option, and then *all* text generated with this font will
  371. have to be yellow and blue.
  372. If outlineWidth is nonzero, an outline will be created at
  373. runtime for the letters, and outlineWidth will be the desired
  374. width of the outline, in points (most fonts are 10 points
  375. high, so 0.5 is often a good choice). If you specify
  376. outlineWidth, you can also specify outlineFeather (0.0 .. 1.0)
  377. and outlineColor. You may need to increase pixelsPerUnit to
  378. get the best results.
  379. if renderMode is not None, it may be one of the following
  380. symbols to specify a geometry-based font:
  381. TextFont.RMTexture - this is the default. Font characters
  382. are rendered into a texture and applied to a polygon.
  383. This gives the best general-purpose results.
  384. TextFont.RMWireframe - Font characters are rendered as a
  385. sequence of one-pixel lines. Consider enabling line or
  386. multisample antialiasing for best results.
  387. TextFont.RMPolygon - Font characters are rendered as a
  388. flat polygon. This works best for very large
  389. characters, and generally requires polygon or
  390. multisample antialiasing to be enabled for best results.
  391. TextFont.RMExtruded - Font characters are rendered with a
  392. 3-D outline made of polygons, like a cookie cutter.
  393. This is appropriate for a 3-D scene, but may be
  394. completely invisible when assigned to a 2-D scene and
  395. viewed normally from the front, since polygons are
  396. infinitely thin.
  397. TextFont.RMSolid - A combination of RMPolygon and
  398. RMExtruded: a flat polygon in front with a solid
  399. three-dimensional edge. This is best for letters that
  400. will be tumbling in 3-D space.
  401. If the texture mode is other than RMTexture, most of the above
  402. parameters do not apply, though pixelsPerUnit still does apply
  403. and roughly controls the tightness of the curve approximation
  404. (and the number of vertices generated).
  405. """
  406. assert Loader.notify.debug("Loading font: %s" % (modelPath))
  407. if phaseChecker:
  408. loaderOptions = LoaderOptions()
  409. if(okMissing):
  410. loaderOptions.setFlags(loaderOptions.getFlags() & ~LoaderOptions.LFReportErrors)
  411. phaseChecker(modelPath, loaderOptions)
  412. font = FontPool.loadFont(modelPath)
  413. if font is None:
  414. if not okMissing:
  415. message = 'Could not load font file: %s' % (modelPath)
  416. raise IOError(message)
  417. # If we couldn't load the model, at least return an
  418. # empty font.
  419. font = StaticTextFont(PandaNode("empty"))
  420. # The following properties may only be set for dynamic fonts.
  421. if hasattr(font, "setPointSize"):
  422. if pointSize is not None:
  423. font.setPointSize(pointSize)
  424. if pixelsPerUnit is not None:
  425. font.setPixelsPerUnit(pixelsPerUnit)
  426. if scaleFactor is not None:
  427. font.setScaleFactor(scaleFactor)
  428. if textureMargin is not None:
  429. font.setTextureMargin(textureMargin)
  430. if polyMargin is not None:
  431. font.setPolyMargin(polyMargin)
  432. if minFilter is not None:
  433. font.setMinfilter(minFilter)
  434. if magFilter is not None:
  435. font.setMagfilter(magFilter)
  436. if anisotropicDegree is not None:
  437. font.setAnisotropicDegree(anisotropicDegree)
  438. if color:
  439. font.setFg(color)
  440. # This means we want the background to match the
  441. # foreground color, but transparent.
  442. font.setBg(VBase4(color[0], color[1], color[2], 0.0))
  443. if outlineWidth:
  444. font.setOutline(outlineColor, outlineWidth, outlineFeather)
  445. # This means we want the background to match the
  446. # outline color, but transparent.
  447. font.setBg(VBase4(outlineColor[0], outlineColor[1], outlineColor[2], 0.0))
  448. if renderMode:
  449. font.setRenderMode(renderMode)
  450. if lineHeight is not None:
  451. # If the line height is specified, it overrides whatever
  452. # the font itself thinks the line height should be. This
  453. # and spaceAdvance should be set last, since some of the
  454. # other parameters can cause these to be reset to their
  455. # default.
  456. font.setLineHeight(lineHeight)
  457. if spaceAdvance is not None:
  458. font.setSpaceAdvance(spaceAdvance)
  459. return font
  460. # texture loading funcs
  461. def loadTexture(self, texturePath, alphaPath = None,
  462. readMipmaps = False, okMissing = False,
  463. minfilter = None, magfilter = None,
  464. anisotropicDegree = None, loaderOptions = None,
  465. multiview = None):
  466. """
  467. texturePath is a string.
  468. Attempt to load a texture from the given file path using
  469. TexturePool class.
  470. okMissing should be True to indicate the method should return
  471. None if the texture file is not found. If it is False, the
  472. method will raise an exception if the texture file is not
  473. found or cannot be loaded.
  474. If alphaPath is not None, it is the name of a grayscale image
  475. that is applied as the texture's alpha channel.
  476. If readMipmaps is True, then the filename string must contain
  477. a sequence of hash characters ('#') that are filled in with
  478. the mipmap index number, and n images will be loaded
  479. individually which define the n mipmap levels of the texture.
  480. The base level is mipmap level 0, and this defines the size of
  481. the texture and the number of expected mipmap images.
  482. If minfilter or magfilter is not None, they should be a symbol
  483. like SamplerState.FTLinear or SamplerState.FTNearest. (minfilter
  484. may be further one of the Mipmap filter type symbols.) These
  485. specify the filter mode that will automatically be applied to
  486. the texture when it is loaded. Note that this setting may
  487. override the texture's existing settings, even if it has
  488. already been loaded. See egg-texture-cards for a more robust
  489. way to apply per-texture filter types and settings.
  490. If anisotropicDegree is not None, it specifies the anisotropic degree
  491. to apply to the texture when it is loaded. Like minfilter and
  492. magfilter, egg-texture-cards may be a more robust way to apply
  493. this setting.
  494. If multiview is true, it indicates to load a multiview or
  495. stereo texture. In this case, the filename should contain a
  496. hash character ('#') that will be replaced with '0' for the
  497. left image and '1' for the right image. Larger numbers are
  498. also allowed if you need more than two views.
  499. """
  500. if loaderOptions is None:
  501. loaderOptions = LoaderOptions()
  502. else:
  503. loaderOptions = LoaderOptions(loaderOptions)
  504. if multiview is not None:
  505. flags = loaderOptions.getTextureFlags()
  506. if multiview:
  507. flags |= LoaderOptions.TFMultiview
  508. else:
  509. flags &= ~LoaderOptions.TFMultiview
  510. loaderOptions.setTextureFlags(flags)
  511. if alphaPath is None:
  512. assert Loader.notify.debug("Loading texture: %s" % (texturePath))
  513. texture = TexturePool.loadTexture(texturePath, 0, readMipmaps, loaderOptions)
  514. else:
  515. assert Loader.notify.debug("Loading texture: %s %s" % (texturePath, alphaPath))
  516. texture = TexturePool.loadTexture(texturePath, alphaPath, 0, 0, readMipmaps, loaderOptions)
  517. if not texture and not okMissing:
  518. message = 'Could not load texture: %s' % (texturePath)
  519. raise IOError(message)
  520. if minfilter is not None:
  521. texture.setMinfilter(minfilter)
  522. if magfilter is not None:
  523. texture.setMagfilter(magfilter)
  524. if anisotropicDegree is not None:
  525. texture.setAnisotropicDegree(anisotropicDegree)
  526. return texture
  527. def load3DTexture(self, texturePattern, readMipmaps = False, okMissing = False,
  528. minfilter = None, magfilter = None, anisotropicDegree = None,
  529. loaderOptions = None, multiview = None, numViews = 2):
  530. """
  531. texturePattern is a string that contains a sequence of one or
  532. more hash characters ('#'), which will be filled in with the
  533. z-height number. Returns a 3-D Texture object, suitable for
  534. rendering volumetric textures.
  535. okMissing should be True to indicate the method should return
  536. None if the texture file is not found. If it is False, the
  537. method will raise an exception if the texture file is not
  538. found or cannot be loaded.
  539. If readMipmaps is True, then the filename string must contain
  540. two sequences of hash characters; the first group is filled in
  541. with the z-height number, and the second group with the mipmap
  542. index number.
  543. If multiview is true, it indicates to load a multiview or
  544. stereo texture. In this case, numViews should also be
  545. specified (the default is 2), and the sequence of texture
  546. images will be divided into numViews views. The total
  547. z-height will be (numImages / numViews). For instance, if you
  548. read 16 images with numViews = 2, then you have created a
  549. stereo multiview image, with z = 8. In this example, images
  550. numbered 0 - 7 will be part of the left eye view, and images
  551. numbered 8 - 15 will be part of the right eye view.
  552. """
  553. assert Loader.notify.debug("Loading 3-D texture: %s" % (texturePattern))
  554. if loaderOptions is None:
  555. loaderOptions = LoaderOptions()
  556. else:
  557. loaderOptions = LoaderOptions(loaderOptions)
  558. if multiview is not None:
  559. flags = loaderOptions.getTextureFlags()
  560. if multiview:
  561. flags |= LoaderOptions.TFMultiview
  562. else:
  563. flags &= ~LoaderOptions.TFMultiview
  564. loaderOptions.setTextureFlags(flags)
  565. loaderOptions.setTextureNumViews(numViews)
  566. texture = TexturePool.load3dTexture(texturePattern, readMipmaps, loaderOptions)
  567. if not texture and not okMissing:
  568. message = 'Could not load 3-D texture: %s' % (texturePattern)
  569. raise IOError(message)
  570. if minfilter is not None:
  571. texture.setMinfilter(minfilter)
  572. if magfilter is not None:
  573. texture.setMagfilter(magfilter)
  574. if anisotropicDegree is not None:
  575. texture.setAnisotropicDegree(anisotropicDegree)
  576. return texture
  577. def loadCubeMap(self, texturePattern, readMipmaps = False, okMissing = False,
  578. minfilter = None, magfilter = None, anisotropicDegree = None,
  579. loaderOptions = None, multiview = None):
  580. """
  581. texturePattern is a string that contains a sequence of one or
  582. more hash characters ('#'), which will be filled in with the
  583. face index number (0 through 6). Returns a six-face cube map
  584. Texture object.
  585. okMissing should be True to indicate the method should return
  586. None if the texture file is not found. If it is False, the
  587. method will raise an exception if the texture file is not
  588. found or cannot be loaded.
  589. If readMipmaps is True, then the filename string must contain
  590. two sequences of hash characters; the first group is filled in
  591. with the face index number, and the second group with the
  592. mipmap index number.
  593. If multiview is true, it indicates to load a multiview or
  594. stereo cube map. For a stereo cube map, 12 images will be
  595. loaded--images numbered 0 - 5 will become the left eye view,
  596. and images 6 - 11 will become the right eye view. In general,
  597. the number of images found on disk must be a multiple of six,
  598. and each six images will define a new view.
  599. """
  600. assert Loader.notify.debug("Loading cube map: %s" % (texturePattern))
  601. if loaderOptions is None:
  602. loaderOptions = LoaderOptions()
  603. else:
  604. loaderOptions = LoaderOptions(loaderOptions)
  605. if multiview is not None:
  606. flags = loaderOptions.getTextureFlags()
  607. if multiview:
  608. flags |= LoaderOptions.TFMultiview
  609. else:
  610. flags &= ~LoaderOptions.TFMultiview
  611. loaderOptions.setTextureFlags(flags)
  612. texture = TexturePool.loadCubeMap(texturePattern, readMipmaps, loaderOptions)
  613. if not texture and not okMissing:
  614. message = 'Could not load cube map: %s' % (texturePattern)
  615. raise IOError(message)
  616. if minfilter is not None:
  617. texture.setMinfilter(minfilter)
  618. if magfilter is not None:
  619. texture.setMagfilter(magfilter)
  620. if anisotropicDegree is not None:
  621. texture.setAnisotropicDegree(anisotropicDegree)
  622. return texture
  623. def unloadTexture(self, texture):
  624. """
  625. Removes the previously-loaded texture from the cache, so
  626. that when the last reference to it is gone, it will be
  627. released. This also means that the next time the same texture
  628. is loaded, it will be re-read from disk (and duplicated in
  629. texture memory if there are still outstanding references to
  630. it).
  631. The texture parameter may be the return value of any previous
  632. call to loadTexture(), load3DTexture(), or loadCubeMap().
  633. """
  634. assert Loader.notify.debug("Unloading texture: %s" % (texture))
  635. TexturePool.releaseTexture(texture)
  636. # sound loading funcs
  637. def loadSfx(self, *args, **kw):
  638. """Loads one or more sound files, specifically designated as a
  639. "sound effect" file (that is, uses the sfxManager to load the
  640. sound). There is no distinction between sound effect files
  641. and music files other than the particular AudioManager used to
  642. load the sound file, but this distinction allows the sound
  643. effects and/or the music files to be adjusted as a group,
  644. independently of the other group."""
  645. # showbase-created sfxManager should always be at front of list
  646. if(self.base.sfxManagerList):
  647. return self.loadSound(self.base.sfxManagerList[0], *args, **kw)
  648. return None
  649. def loadMusic(self, *args, **kw):
  650. """Loads one or more sound files, specifically designated as a
  651. "music" file (that is, uses the musicManager to load the
  652. sound). There is no distinction between sound effect files
  653. and music files other than the particular AudioManager used to
  654. load the sound file, but this distinction allows the sound
  655. effects and/or the music files to be adjusted as a group,
  656. independently of the other group."""
  657. if(self.base.musicManager):
  658. return self.loadSound(self.base.musicManager, *args, **kw)
  659. else:
  660. return None
  661. def loadSound(self, manager, soundPath, positional = False,
  662. callback = None, extraArgs = []):
  663. """Loads one or more sound files, specifying the particular
  664. AudioManager that should be used to load them. The soundPath
  665. may be either a single filename, or a list of filenames. If a
  666. callback is specified, the loading happens in the background,
  667. just as in loadModel(); otherwise, the loading happens before
  668. loadSound() returns."""
  669. if not isinstance(soundPath, (MovieAudio, tuple, list, set)):
  670. # We were given a single sound pathname.
  671. soundList = [soundPath]
  672. gotList = False
  673. else:
  674. # Assume we were given a list of sound pathnames.
  675. soundList = soundPath
  676. gotList = True
  677. if callback is None:
  678. # We got no callback, so it's a synchronous load.
  679. result = []
  680. for soundPath in soundList:
  681. # should return a valid sound obj even if musicMgr is invalid
  682. sound = manager.getSound(soundPath, positional)
  683. result.append(sound)
  684. if gotList:
  685. return result
  686. else:
  687. return result[0]
  688. else:
  689. # We got a callback, so we want an asynchronous (threaded)
  690. # load. We'll return immediately, but when all of the
  691. # requested sounds have been loaded, we'll invoke the
  692. # callback (passing it the sounds on the parameter list).
  693. cb = Loader.Callback(len(soundList), gotList, callback, extraArgs)
  694. for i, soundPath in enumerate(soundList):
  695. request = AudioLoadRequest(manager, soundPath, positional)
  696. request.setDoneEvent(self.hook)
  697. self.loader.loadAsync(request)
  698. cb.requests.add(request)
  699. self.__requests[request] = (cb, i)
  700. return cb
  701. def unloadSfx(self, sfx):
  702. if (sfx):
  703. if(self.base.sfxManagerList):
  704. self.base.sfxManagerList[0].uncacheSound (sfx.getName())
  705. ## def makeNodeNamesUnique(self, nodePath, nodeCount):
  706. ## if nodeCount == 0:
  707. ## Loader.modelCount += 1
  708. ## nodePath.setName(nodePath.getName() +
  709. ## ('_%d_%d' % (Loader.modelCount, nodeCount)))
  710. ## for i in range(nodePath.getNumChildren()):
  711. ## nodeCount += 1
  712. ## self.makeNodeNamesUnique(nodePath.getChild(i), nodeCount)
  713. def loadShader(self, shaderPath, okMissing = False):
  714. shader = ShaderPool.loadShader (shaderPath)
  715. if not shader and not okMissing:
  716. message = 'Could not load shader file: %s' % (shaderPath)
  717. raise IOError(message)
  718. return shader
  719. def unloadShader(self, shaderPath):
  720. if shaderPath is not None:
  721. ShaderPool.releaseShader(shaderPath)
  722. def asyncFlattenStrong(self, model, inPlace = True,
  723. callback = None, extraArgs = []):
  724. """ Performs a model.flattenStrong() operation in a sub-thread
  725. (if threading is compiled into Panda). The model may be a
  726. single NodePath, or it may be a list of NodePaths.
  727. Each model is duplicated and flattened in the sub-thread.
  728. If inPlace is True, then when the flatten operation completes,
  729. the newly flattened copies are automatically dropped into the
  730. scene graph, in place the original models.
  731. If a callback is specified, then it is called after the
  732. operation is finished, receiving the flattened model (or a
  733. list of flattened models)."""
  734. if isinstance(model, NodePath):
  735. # We were given a single model.
  736. modelList = [model]
  737. gotList = False
  738. else:
  739. # Assume we were given a list of models.
  740. modelList = model
  741. gotList = True
  742. if inPlace:
  743. extraArgs = [gotList, callback, modelList, extraArgs]
  744. callback = self.__asyncFlattenDone
  745. gotList = True
  746. cb = Loader.Callback(len(modelList), gotList, callback, extraArgs)
  747. i = 0
  748. for model in modelList:
  749. request = ModelFlattenRequest(model.node())
  750. request.setDoneEvent(self.hook)
  751. request.setPythonObject((cb, i))
  752. self.loader.loadAsync(request)
  753. cb.requests[request] = True
  754. self.__requests[request] = (cb, i)
  755. i += 1
  756. return cb
  757. def __asyncFlattenDone(self, models,
  758. gotList, callback, origModelList, extraArgs):
  759. """ The asynchronous flatten operation has completed; quietly
  760. drop in the new models. """
  761. self.notify.debug("asyncFlattenDone: %s" % (models,))
  762. assert(len(models) == len(origModelList))
  763. for i in range(len(models)):
  764. origModelList[i].getChildren().detach()
  765. orig = origModelList[i].node()
  766. flat = models[i].node()
  767. orig.copyAllProperties(flat)
  768. orig.replaceNode(flat)
  769. if callback:
  770. if gotList:
  771. callback(origModelList, *extraArgs)
  772. else:
  773. callback(*(origModelList + extraArgs))
  774. def __gotAsyncObject(self, request):
  775. """A model or sound file or some such thing has just been
  776. loaded asynchronously by the sub-thread. Add it to the list
  777. of loaded objects, and call the appropriate callback when it's
  778. time."""
  779. if request not in self.__requests:
  780. return
  781. cb, i = self.__requests[request]
  782. if cb.cancelled:
  783. # Shouldn't be here.
  784. del self.__requests[request]
  785. return
  786. cb.requests.discard(request)
  787. if not cb.requests:
  788. del self.__requests[request]
  789. object = None
  790. if hasattr(request, "getModel"):
  791. node = request.getModel()
  792. if node is not None:
  793. object = NodePath(node)
  794. elif hasattr(request, "getSound"):
  795. object = request.getSound()
  796. elif hasattr(request, "getSuccess"):
  797. object = request.getSuccess()
  798. cb.gotObject(i, object)
  799. load_model = loadModel
  800. cancel_request = cancelRequest
  801. is_request_pending = isRequestPending
  802. unload_model = unloadModel
  803. save_model = saveModel
  804. load_font = loadFont
  805. load_texture = loadTexture
  806. load_3d_texture = load3DTexture
  807. load_cube_map = loadCubeMap
  808. unload_texture = unloadTexture
  809. load_sfx = loadSfx
  810. load_music = loadMusic
  811. load_sound = loadSound
  812. unload_sfx = unloadSfx
  813. load_shader = loadShader
  814. unload_shader = unloadShader
  815. async_flatten_strong = asyncFlattenStrong