Panda3D
Loading...
Searching...
No Matches
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
18#include "matrixLens.h"
19#include "graphicsOutput.h"
20#include "graphicsEngine.h"
21#include "dcast.h"
23#include "asyncTaskManager.h"
24#include "genericAsyncTask.h"
25
26/**
27 *
28 */
29NonlinearImager::
30NonlinearImager() {
31 _engine = nullptr;
32 _stale = true;
33}
34
35/**
36 *
37 */
38NonlinearImager::
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 */
76add_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 */
115find_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 */
129remove_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 */
152get_num_screens() const {
153 return _screens.size();
154}
155
156/**
157 * Returns the nth screen that has been added to the imager.
158 */
160get_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 */
171get_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 */
185set_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 */
210set_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 */
222set_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 */
258get_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 */
359find_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 */
373remove_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 */
414set_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 */
435get_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 */
450get_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 */
459get_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 */
467get_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 */
484get_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 */
493get_graphics_engine() const {
494 return _engine;
495}
496
497/**
498 * Forces a regeneration of all the mesh objects, etc.
499 */
501recompute() {
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 */
527AsyncTask::DoneStatus NonlinearImager::
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 */
580void NonlinearImager::
581recompute_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.
set_camera
Sets the camera that is associated with this DisplayRegion.
get_camera
Returns the camera associated with this DisplayRegion, or an empty NodePath if no camera is associate...
get_window
Returns the GraphicsOutput that this DisplayRegion is ultimately associated with, or NULL if no windo...
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
void show()
Undoes the effect of a previous hide() on this node: makes the referenced node (and the entire subgra...
Definition nodePath.I:1796
void hide()
Makes the referenced node (and the entire subgraph below this node) invisible to all cameras.
Definition nodePath.I:1853
void set_texture(Texture *tex, int priority=0)
Adds the indicated texture to the list of textures that will be rendered on the default texture stage...
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:208
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:599
void set_two_sided(bool two_sided, int priority=0)
Specifically sets or disables two-sided rendering mode on this particular node.
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.