Panda3D
nonlinearImager.cxx
Go to the documentation of this file.
1 /**
2  * PANDA 3D SOFTWARE
3  * Copyright (c) Carnegie Mellon University. All rights reserved.
4  *
5  * All use of this software is subject to the terms of the revised BSD
6  * license. You should have received a copy of this license along
7  * with this source code in a file named "LICENSE."
8  *
9  * @file nonlinearImager.cxx
10  * @author drose
11  * @date 2001-12-12
12  */
13 
14 #include "nonlinearImager.h"
15 #include "config_distort.h"
16 
17 #include "graphicsStateGuardian.h"
18 #include "matrixLens.h"
19 #include "graphicsOutput.h"
20 #include "graphicsEngine.h"
21 #include "dcast.h"
22 #include "cPointerCallbackObject.h"
23 #include "asyncTaskManager.h"
24 #include "genericAsyncTask.h"
25 
26 /**
27  *
28  */
29 NonlinearImager::
30 NonlinearImager() {
31  _engine = nullptr;
32  _stale = true;
33 }
34 
35 /**
36  *
37  */
38 NonlinearImager::
39 ~NonlinearImager() {
42 
43  if (_recompute_task != nullptr) {
45  task_mgr->remove(_recompute_task);
46  }
47 }
48 
49 /**
50  * This version of this method is deprecated and will soon be removed. Use
51  * the version that takes two parameters instead.
52  *
53  * @deprecated Use the version that takes two parameters instead.
54  */
57  return add_screen(NodePath(screen), screen->get_name());
58 }
59 
60 /**
61  * Adds a new ProjectionScreen to the list of screens that will be processed
62  * by the NonlinearImager. Each ProjectionScreen represents a view into the
63  * world. It must be based on a linear camera (or whatever kind of camera is
64  * respected by the graphics engine).
65  *
66  * Each ProjectionScreen object should already have some screen geometry
67  * created.
68  *
69  * As each frame is rendered, an offscreen image will be rendered from the
70  * source camera associated with each ProjectionScreen, and the resulting
71  * image will be applied to the screen geometry.
72  *
73  * The return value is the index number of the new screen.
74  */
76 add_screen(const NodePath &screen, const std::string &name) {
77  nassertr(!screen.is_empty() &&
78  screen.node()->is_of_type(ProjectionScreen::get_class_type()), -1);
79 
80  ProjectionScreen *screen_node = DCAST(ProjectionScreen, screen.node());
81 
82  _screens.push_back(Screen());
83  Screen &new_screen = _screens.back();
84  new_screen._screen = screen;
85  new_screen._screen_node = screen_node;
86  new_screen._name = name;
87  new_screen._buffer = nullptr;
88  new_screen._tex_width = 256;
89  new_screen._tex_height = 256;
90  new_screen._active = true;
91 
92  // Slot a mesh for each viewer.
93  size_t vi;
94  for (vi = 0; vi < _viewers.size(); ++vi) {
95  new_screen._meshes.push_back(Mesh());
96  new_screen._meshes[vi]._last_screen = screen_node->get_last_screen();
97  }
98 
99  _stale = true;
100 
101  if (_dark_room.is_empty()) {
102  _dark_room = screen.get_top();
103  } else {
104  nassertr(_dark_room.is_same_graph(screen), _screens.size() - 1);
105  }
106 
107  return _screens.size() - 1;
108 }
109 
110 /**
111  * Returns the index number of the first appearance of the indicated screen
112  * within the imager's list, or -1 if it does not appear.
113  */
115 find_screen(const NodePath &screen) const {
116  for (size_t i = 0; i < _screens.size(); i++) {
117  if (_screens[i]._screen == screen) {
118  return i;
119  }
120  }
121 
122  return -1;
123 }
124 
125 /**
126  * Removes the screen with the indicated index number from the imager.
127  */
129 remove_screen(int index) {
130  nassertv_always(index >= 0 && index < (int)_screens.size());
131  Screen &screen = _screens[index];
132  for (size_t vi = 0; vi < screen._meshes.size(); vi++) {
133  screen._meshes[vi]._mesh.remove_node();
134  }
135  _screens.erase(_screens.begin() + index);
136 }
137 
138 /**
139  * Removes all screens from the imager.
140  */
143  while (!_screens.empty()) {
144  remove_screen(_screens.size() - 1);
145  }
146 }
147 
148 /**
149  * Returns the number of screens that have been added to the imager.
150  */
152 get_num_screens() const {
153  return _screens.size();
154 }
155 
156 /**
157  * Returns the nth screen that has been added to the imager.
158  */
160 get_screen(int index) const {
161  nassertr(index >= 0 && index < (int)_screens.size(), NodePath());
162  return _screens[index]._screen;
163 }
164 
165 /**
166  * Returns the offscreen buffer that is automatically created for the nth
167  * projection screen. This may return NULL if the screen is inactive or if it
168  * has not been rendered yet.
169  */
171 get_buffer(int index) const {
172  nassertr(index >= 0 && index < (int)_screens.size(), nullptr);
173  return _screens[index]._buffer;
174 }
175 
176 /**
177  * Sets the width and height of the texture used to render the scene for the
178  * indicated screen. This must be less than or equal to the window size, and
179  * it should be a power of two.
180  *
181  * In general, the larger the texture, the greater the detail of the rendered
182  * scene.
183  */
185 set_texture_size(int index, int width, int height) {
186  nassertv(index >= 0 && index < (int)_screens.size());
187 
188  Screen &screen = _screens[index];
189 
190  screen._tex_width = width;
191  screen._tex_height = height;
192 
193  if (screen._buffer != nullptr) {
194  bool removed = _engine->remove_window(screen._buffer);
195  screen._buffer = nullptr;
196  nassertv(removed);
197  }
198 
199  _stale = true;
200 }
201 
202 /**
203  * Specifies the camera that will be used to render the image for this
204  * particular screen.
205  *
206  * The parameter must be a NodePath whose node is a Camera. The camera itself
207  * indicates the scene that is to be rendered.
208  */
210 set_source_camera(int index, const NodePath &source_camera) {
211  nassertv(index >= 0 && index < (int)_screens.size());
212  nassertv(!source_camera.is_empty() &&
213  source_camera.node()->is_of_type(Camera::get_class_type()));
214  _screens[index]._source_camera = source_camera;
215 }
216 
217 /**
218  * Sets the active flag on the indicated screen. If the active flag is true,
219  * the screen will be used; otherwise, it will not appear.
220  */
222 set_screen_active(int index, bool active) {
223  nassertv(index >= 0 && index < (int)_screens.size());
224 
225  Screen &screen = _screens[index];
226  screen._active = active;
227 
228  if (!active) {
229  // If we've just made this screen inactive, remove its meshes.
230  for (size_t vi = 0; vi < screen._meshes.size(); vi++) {
231  screen._meshes[vi]._mesh.remove_node();
232  }
233 
234  // Also remove its buffer.
235  if (screen._buffer != nullptr) {
236  bool removed = _engine->remove_window(screen._buffer);
237  screen._buffer = nullptr;
238  nassertv(removed);
239  }
240 
241  // Hide the screen in the dark room. This doesn't really matter, since
242  // the dark room isn't normally rendered, but hide it anyway in case the
243  // user stuck a camera in there for fun.
244  screen._screen.hide();
245 
246  } else {
247  // If we've just made it active, it needs to be recomputed.
248  _stale = true;
249 
250  screen._screen.show();
251  }
252 }
253 
254 /**
255  * Returns the active flag on the indicated screen.
256  */
258 get_screen_active(int index) const {
259  nassertr(index >= 0 && index < (int)_screens.size(), false);
260  return _screens[index]._active;
261 }
262 
263 
264 /**
265  * Adds the indicated DisplayRegion as a viewer into the NonlinearImager room.
266  * The camera associated with the DisplayRegion at the time add_viewer() is
267  * called is used as the initial viewer camera; it may have a nonlinear lens,
268  * like a fisheye or cylindrical lens.
269  *
270  * This sets up a special scene graph for this DisplayRegion alone and sets up
271  * the DisplayRegion with a specialty camera. If future changes to the camera
272  * are desired, you should use the set_viewer_camera() interface.
273  *
274  * All viewers must share the same GraphicsEngine.
275  *
276  * The return value is the index of the new viewer.
277  */
280  GraphicsOutput *window = dr->get_window();
281  nassertr(window != nullptr, -1);
282 
283  GraphicsStateGuardian *gsg = window->get_gsg();
284  nassertr(gsg != nullptr, -1);
285 
286  GraphicsEngine *engine = gsg->get_engine();
287  nassertr(engine != nullptr, -1);
288 
289  nassertr(_viewers.empty() || (engine == _engine), -1);
290  if (_engine == nullptr) {
291  _engine = engine;
292  }
293 
294  if (_recompute_task == nullptr) {
295  _recompute_task =
296  new GenericAsyncTask("nli_recompute", recompute_callback, (void *)this);
298  task_mgr->add(_recompute_task);
299  }
300 
301  int previous_vi = find_viewer(dr);
302  if (previous_vi >= 0) {
303  return previous_vi;
304  }
305 
306  size_t vi = _viewers.size();
307  _viewers.push_back(Viewer());
308  Viewer &viewer = _viewers[vi];
309 
310  viewer._dr = dr;
311 
312  // Get the current camera off of the DisplayRegion, if any.
313  viewer._viewer = dr->get_camera();
314  if (viewer._viewer.is_empty()) {
315  viewer._viewer_node = nullptr;
316  } else {
317  viewer._viewer_node = DCAST(LensNode, viewer._viewer.node());
318  }
319 
320  // The internal camera is an identity-matrix camera that simply views the
321  // meshes that represent the user's specified camera.
322  viewer._internal_camera = new Camera("internal_camera");
323  viewer._internal_camera->set_lens(new MatrixLens);
324  viewer._internal_scene = NodePath("internal_screens");
325  viewer._internal_camera->set_scene(viewer._internal_scene);
326 
327  NodePath camera_np = viewer._internal_scene.attach_new_node(viewer._internal_camera);
328  viewer._dr->set_camera(camera_np);
329 
330  // Enable face culling on the wireframe mesh. This will help us to cull out
331  // invalid polygons that result from vertices crossing a singularity (for
332  // instance, at the back of a fisheye lens).
333  viewer._internal_scene.set_two_sided(0);
334 
335  // Finally, slot a new mesh for each screen.
336  Screens::iterator si;
337  for (si = _screens.begin(); si != _screens.end(); ++si) {
338  Screen &screen = (*si);
339  screen._meshes.push_back(Mesh());
340  nassertr(screen._meshes.size() == _viewers.size(), -1);
341  }
342 
343  _stale = true;
344 
345  if (_dark_room.is_empty()) {
346  _dark_room = viewer._viewer.get_top();
347  } else {
348  nassertr(_dark_room.is_same_graph(viewer._viewer), vi);
349  }
350 
351  return vi;
352 }
353 
354 /**
355  * Returns the index number of the indicated DisplayRegion within the list of
356  * viewers, or -1 if it is not found.
357  */
359 find_viewer(DisplayRegion *dr) const {
360  for (size_t vi = 0; vi < _viewers.size(); vi++) {
361  if (_viewers[vi]._dr == dr) {
362  return vi;
363  }
364  }
365 
366  return -1;
367 }
368 
369 /**
370  * Removes the viewer with the indicated index number from the imager.
371  */
373 remove_viewer(int index) {
374  nassertv_always(index >= 0 && index < (int)_viewers.size());
375  Viewer &viewer = _viewers[index];
376  viewer._internal_camera->set_scene(NodePath());
377  viewer._dr->set_camera(viewer._viewer);
378 
379  // Also remove the corresponding mesh from each screen.
380  Screens::iterator si;
381  for (si = _screens.begin(); si != _screens.end(); ++si) {
382  Screen &screen = (*si);
383  nassertv(index < (int)screen._meshes.size());
384  screen._meshes[index]._mesh.remove_node();
385  screen._meshes.erase(screen._meshes.begin() + index);
386  }
387 
388  _viewers.erase(_viewers.begin() + index);
389 }
390 
391 /**
392  * Removes all viewers from the imager.
393  */
396  while (!_viewers.empty()) {
397  remove_viewer(_viewers.size() - 1);
398  }
399 }
400 
401 /**
402  * Specifies the LensNode that is to serve as the viewer for this screen. The
403  * relative position of the LensNode to the NonlinearImager, as well as the
404  * properties of the lens associated with the LensNode, determines the UV's
405  * that will be assigned to the geometry within the NonlinearImager.
406  *
407  * It is not necessary to call this except to change the camera after a viewer
408  * has been added, since the default is to use whatever camera is associated
409  * with the DisplayRegion at the time the viewer is added.
410  *
411  * The NodePath must refer to a LensNode (or a Camera).
412  */
414 set_viewer_camera(int index, const NodePath &viewer_camera) {
415  nassertv(index >= 0 && index < (int)_viewers.size());
416  nassertv(!viewer_camera.is_empty() &&
417  viewer_camera.node()->is_of_type(LensNode::get_class_type()));
418  Viewer &viewer = _viewers[index];
419  viewer._viewer = viewer_camera;
420  viewer._viewer_node = DCAST(LensNode, viewer_camera.node());
421  _stale = true;
422 
423  if (_dark_room.is_empty()) {
424  _dark_room = viewer._viewer.get_top();
425  } else {
426  nassertv(_dark_room.is_same_graph(viewer._viewer));
427  }
428 }
429 
430 /**
431  * Returns the NodePath to the LensNode that is to serve as nth viewer for
432  * this screen.
433  */
435 get_viewer_camera(int index) const {
436  nassertr(index >= 0 && index < (int)_viewers.size(), NodePath());
437  return _viewers[index]._viewer;
438 }
439 
440 /**
441  * Returns a pointer to the root node of the internal scene graph for the nth
442  * viewer, which is used to render all of the screen meshes for this viewer.
443  *
444  * This is the scene graph in which the screen meshes within the dark room
445  * have been flattened into the appropriate transformation according to the
446  * viewer's lens properties (and position relative to the screens). It is
447  * this scene graph that is finally rendered to the window.
448  */
450 get_viewer_scene(int index) const {
451  nassertr(index >= 0 && index < (int)_viewers.size(), NodePath());
452  return _viewers[index]._internal_scene;
453 }
454 
455 /**
456  * Returns the number of viewers that have been added to the imager.
457  */
459 get_num_viewers() const {
460  return _viewers.size();
461 }
462 
463 /**
464  * Returns the nth viewer's DisplayRegion that has been added to the imager.
465  */
467 get_viewer(int index) const {
468  nassertr(index >= 0 && index < (int)_viewers.size(), nullptr);
469  return _viewers[index]._dr;
470 }
471 
472 /**
473  * Returns the NodePath to the root of the dark room scene. This is the scene
474  * in which all of the ProjectionScreens and the viewer cameras reside. It's
475  * a standalone scene with a few projection screens arranged artfully around
476  * one or more viewers; it's so named because it's a little virtual theater.
477  *
478  * Normally this scene is not rendered directly; it only exists as an abstract
479  * concept, and to define the relation between the ProjectionScreens and the
480  * viewers. But it may be rendered to help visualize the NonlinearImager's
481  * behavior.
482  */
484 get_dark_room() const {
485  return _dark_room;
486 }
487 
488 /**
489  * Returns the GraphicsEngine that all of the viewers added to the
490  * NonlinearImager have in common.
491  */
493 get_graphics_engine() const {
494  return _engine;
495 }
496 
497 /**
498  * Forces a regeneration of all the mesh objects, etc.
499  */
501 recompute() {
502  size_t vi;
503  for (vi = 0; vi < _viewers.size(); ++vi) {
504  Viewer &viewer = _viewers[vi];
505 
506  Screens::iterator si;
507  for (si = _screens.begin(); si != _screens.end(); ++si) {
508  Screen &screen = (*si);
509  if (screen._active) {
510  recompute_screen(screen, vi);
511  }
512  }
513 
514  if (viewer._viewer_node != nullptr &&
515  viewer._viewer_node->get_lens() != nullptr) {
516  viewer._viewer_lens_change =
517  viewer._viewer_node->get_lens()->get_last_change();
518  }
519  }
520 
521  _stale = false;
522 }
523 
524 /**
525  * This function is added as a task, to ensure that all frames are up-to-date.
526  */
527 AsyncTask::DoneStatus NonlinearImager::
528 recompute_callback(GenericAsyncTask *, void *data) {
529  NonlinearImager *self = (NonlinearImager *)data;
530  self->recompute_if_stale();
531  return AsyncTask::DS_cont;
532 }
533 
534 /**
535  * Calls recompute() if it needs to be called.
536  */
539  if (_stale) {
540  recompute();
541  } else {
542  size_t vi;
543  for (vi = 0; vi < _viewers.size(); ++vi) {
544  Viewer &viewer = _viewers[vi];
545  if (viewer._viewer_node != nullptr) {
546  UpdateSeq lens_change =
547  viewer._viewer_node->get_lens()->get_last_change();
548  if (lens_change != viewer._viewer_lens_change) {
549  // The viewer has changed, so we need to recompute all screens on
550  // this viewer.
551  Screens::iterator si;
552  for (si = _screens.begin(); si != _screens.end(); ++si) {
553  Screen &screen = (*si);
554  if (screen._active) {
555  recompute_screen(screen, vi);
556  }
557  }
558 
559  } else {
560  // We may not need to recompute all screens, but maybe some of them.
561  Screens::iterator si;
562  for (si = _screens.begin(); si != _screens.end(); ++si) {
563  Screen &screen = (*si);
564  if (screen._active &&
565  screen._meshes[vi]._last_screen != screen._screen_node->get_last_screen()) {
566  recompute_screen(screen, vi);
567  } else {
568  screen._screen_node->recompute_if_stale(screen._screen);
569  }
570  }
571  }
572  }
573  }
574  }
575 }
576 
577 /**
578  * Regenerates the mesh objects just for the indicated screen.
579  */
580 void NonlinearImager::
581 recompute_screen(NonlinearImager::Screen &screen, size_t vi) {
582  nassertv(vi < screen._meshes.size());
583  screen._meshes[vi]._mesh.remove_node();
584  if (!screen._active) {
585  return;
586  }
587 
588  screen._screen_node->recompute_if_stale(screen._screen);
589 
590  Viewer &viewer = _viewers[vi];
591  PT(PandaNode) mesh =
592  screen._screen_node->make_flat_mesh(screen._screen, viewer._viewer);
593  if (mesh != nullptr) {
594  screen._meshes[vi]._mesh = viewer._internal_scene.attach_new_node(mesh);
595  }
596 
597  if (screen._buffer == nullptr) {
598  GraphicsOutput *win = viewer._dr->get_window();
599  GraphicsOutput *buffer = win->make_texture_buffer
600  (screen._name, screen._tex_width, screen._tex_height, nullptr, false);
601 
602  if (buffer != nullptr) {
603  screen._buffer = buffer;
604  DisplayRegion *dr = buffer->make_display_region();
605  dr->set_camera(screen._source_camera);
606 
607  } else {
608  screen._meshes[vi]._mesh.clear_texture();
609  }
610  }
611 
612  if (screen._buffer != nullptr) {
613  screen._meshes[vi]._mesh.set_texture(screen._buffer->get_texture());
614 
615  // We don't really need to set the texture on the dark room screen, since
616  // that's normally not rendered, but we do anyway just for debugging
617  // purposes (in case the user does try to render it, to see what's going
618  // on).
619  screen._screen.set_texture(screen._buffer->get_texture());
620  }
621 
622  screen._meshes[vi]._last_screen = screen._screen_node->get_last_screen();
623 }
PANDA 3D SOFTWARE Copyright (c) Carnegie Mellon University.
PANDA 3D SOFTWARE Copyright (c) Carnegie Mellon University.
A class to manage a loose queue of isolated tasks, which can be performed either synchronously (in th...
bool remove(AsyncTask *task)
Removes the indicated task from the active queue.
void add(AsyncTask *task)
Adds the indicated task to the active queue.
static AsyncTaskManager * get_global_ptr()
Returns a pointer to the global AsyncTaskManager.
A node that can be positioned around in the scene graph to represent a point of view for rendering a ...
Definition: camera.h:35
A rectangular subregion within a window for rendering into.
Definition: displayRegion.h:57
set_camera
Sets the camera that is associated with this DisplayRegion.
Definition: displayRegion.h:94
get_camera
Returns the camera associated with this DisplayRegion, or an empty NodePath if no camera is associate...
Definition: displayRegion.h:94
get_window
Returns the GraphicsOutput that this DisplayRegion is ultimately associated with, or NULL if no windo...
Definition: displayRegion.h:88
Associates a generic C-style function pointer with an AsyncTask object.
This class is the main interface to controlling the render process.
This is a base class for the various different classes that represent the result of a frame of render...
get_gsg
Returns the GSG that is associated with this window.
DisplayRegion * make_display_region()
Creates a new DisplayRegion that covers the entire window.
Encapsulates all the communication with a particular instance of a given rendering backend.
GraphicsEngine * get_engine() const
Returns the graphics engine that created this GSG.
A node that contains a Lens.
Definition: lensNode.h:29
A completely generic linear lens.
Definition: matrixLens.h:28
NodePath is the fundamental system for disambiguating instances, and also provides a higher-level int...
Definition: nodePath.h:159
bool is_same_graph(const NodePath &other, Thread *current_thread=Thread::get_current_thread()) const
Returns true if the node represented by this NodePath is parented within the same graph as that of th...
Definition: nodePath.I:275
bool is_empty() const
Returns true if the NodePath contains no nodes.
Definition: nodePath.I:188
PandaNode * node() const
Returns the referenced node of the path.
Definition: nodePath.I:227
NodePath get_top(Thread *current_thread=Thread::get_current_thread()) const
Returns a singleton NodePath that represents the top of the path, or empty NodePath if this path is e...
Definition: nodePath.cxx:209
NodePath attach_new_node(PandaNode *node, int sort=0, Thread *current_thread=Thread::get_current_thread()) const
Attaches a new node, with or without existing parents, to the scene graph below the referenced node o...
Definition: nodePath.cxx:600
void set_two_sided(bool two_sided, int priority=0)
Specifically sets or disables two-sided rendering mode on this particular node.
Definition: nodePath.cxx:4514
This class object combines the rendered output of a 3-d from one or more linear (e....
get_screen
Returns the nth screen that has been added to the imager.
void remove_all_viewers()
Removes all viewers from the imager.
get_num_screens
Returns the number of screens that have been added to the imager.
void recompute()
Forces a regeneration of all the mesh objects, etc.
static AsyncTask::DoneStatus recompute_callback(GenericAsyncTask *task, void *data)
This function is added as a task, to ensure that all frames are up-to-date.
void set_viewer_camera(int index, const NodePath &viewer_camera)
Specifies the LensNode that is to serve as the viewer for this screen.
int find_screen(const NodePath &screen) const
Returns the index number of the first appearance of the indicated screen within the imager's list,...
get_viewer
Returns the nth viewer's DisplayRegion that has been added to the imager.
void recompute_if_stale()
Calls recompute() if it needs to be called.
GraphicsEngine * get_graphics_engine() const
Returns the GraphicsEngine that all of the viewers added to the NonlinearImager have in common.
void set_screen_active(int index, bool active)
Sets the active flag on the indicated screen.
int add_screen(ProjectionScreen *screen)
This version of this method is deprecated and will soon be removed.
void set_texture_size(int index, int width, int height)
Sets the width and height of the texture used to render the scene for the indicated screen.
void remove_all_screens()
Removes all screens from the imager.
int add_viewer(DisplayRegion *dr)
Adds the indicated DisplayRegion as a viewer into the NonlinearImager room.
int find_viewer(DisplayRegion *dr) const
Returns the index number of the indicated DisplayRegion within the list of viewers,...
NodePath get_viewer_scene(int index) const
Returns a pointer to the root node of the internal scene graph for the nth viewer,...
void set_source_camera(int index, const NodePath &source_camera)
Specifies the camera that will be used to render the image for this particular screen.
bool get_screen_active(int index) const
Returns the active flag on the indicated screen.
void remove_viewer(int index)
Removes the viewer with the indicated index number from the imager.
get_num_viewers
Returns the number of viewers that have been added to the imager.
void remove_screen(int index)
Removes the screen with the indicated index number from the imager.
NodePath get_viewer_camera(int index) const
Returns the NodePath to the LensNode that is to serve as nth viewer for this screen.
get_buffer
Returns the offscreen buffer that is automatically created for the nth projection screen.
NodePath get_dark_room() const
Returns the NodePath to the root of the dark room scene.
A basic node of the scene graph or data graph.
Definition: pandaNode.h:65
A ProjectionScreen implements a simple system for projective texturing.
const UpdateSeq & get_last_screen() const
Returns an UpdateSeq corresponding to the last time a screen mesh was generated for the ProjectionScr...
bool is_of_type(TypeHandle handle) const
Returns true if the current object is or derives from the indicated type.
Definition: typedObject.I:28
This is a sequence number that increments monotonically.
Definition: updateSeq.h:37
PANDA 3D SOFTWARE Copyright (c) Carnegie Mellon University.
PANDA 3D SOFTWARE Copyright (c) Carnegie Mellon University.
PANDA 3D SOFTWARE Copyright (c) Carnegie Mellon University.
PANDA 3D SOFTWARE Copyright (c) Carnegie Mellon University.
PANDA 3D SOFTWARE Copyright (c) Carnegie Mellon University.
PANDA 3D SOFTWARE Copyright (c) Carnegie Mellon University.
PANDA 3D SOFTWARE Copyright (c) Carnegie Mellon University.
PANDA 3D SOFTWARE Copyright (c) Carnegie Mellon University.