Browse Source

Merge branch 'master' into shaderpipeline

rdb 5 years ago
parent
commit
5410d1cd01
62 changed files with 1995 additions and 198 deletions
  1. 52 5
      .github/workflows/ci.yml
  2. 1 1
      makepanda/installer.nsi
  3. 2 0
      makepanda/makepanda.py
  4. 6 1
      panda/src/collide/CMakeLists.txt
  5. 5 1
      panda/src/collide/collisionPolygon.h
  6. 86 0
      panda/src/collide/collisionPolygon_ext.cxx
  7. 43 0
      panda/src/collide/collisionPolygon_ext.h
  8. 1 0
      panda/src/collide/p3collide_ext_composite.cxx
  9. 2 18
      panda/src/cull/cullBinBackToFront.cxx
  10. 2 18
      panda/src/cull/cullBinFixed.cxx
  11. 2 18
      panda/src/cull/cullBinFrontToBack.cxx
  12. 2 18
      panda/src/cull/cullBinStateSorted.cxx
  13. 2 18
      panda/src/cull/cullBinUnsorted.cxx
  14. 5 1
      panda/src/display/graphicsStateGuardian.cxx
  15. 1 1
      panda/src/display/graphicsStateGuardian.h
  16. 2 2
      panda/src/dxgsg9/dxGraphicsStateGuardian9.cxx
  17. 1 1
      panda/src/dxgsg9/dxGraphicsStateGuardian9.h
  18. 2 0
      panda/src/express/config_express.cxx
  19. 51 35
      panda/src/glstuff/glGraphicsStateGuardian_src.cxx
  20. 2 1
      panda/src/glstuff/glGraphicsStateGuardian_src.h
  21. 12 0
      panda/src/glstuff/glShaderContext_src.cxx
  22. 7 4
      panda/src/gobj/geom.cxx
  23. 2 2
      panda/src/gobj/geom.h
  24. 21 0
      panda/src/gobj/geomVertexArrayFormat.cxx
  25. 2 0
      panda/src/gobj/geomVertexArrayFormat.h
  26. 23 0
      panda/src/gobj/geomVertexData.I
  27. 46 0
      panda/src/gobj/geomVertexData.cxx
  28. 5 1
      panda/src/gobj/geomVertexData.h
  29. 31 0
      panda/src/gobj/geomVertexFormat.cxx
  30. 3 1
      panda/src/gobj/geomVertexFormat.h
  31. 11 0
      panda/src/gobj/internalName.I
  32. 1 0
      panda/src/gobj/internalName.cxx
  33. 2 0
      panda/src/gobj/internalName.h
  34. 8 2
      panda/src/gobj/shader.cxx
  35. 1 1
      panda/src/gsgbase/graphicsStateGuardianBase.h
  36. 4 0
      panda/src/pgraph/CMakeLists.txt
  37. 6 0
      panda/src/pgraph/config_pgraph.cxx
  38. 18 2
      panda/src/pgraph/cullTraverser.cxx
  39. 3 1
      panda/src/pgraph/cullTraverserData.I
  40. 9 0
      panda/src/pgraph/cullTraverserData.cxx
  41. 2 0
      panda/src/pgraph/cullTraverserData.h
  42. 18 3
      panda/src/pgraph/cullableObject.I
  43. 34 0
      panda/src/pgraph/cullableObject.cxx
  44. 5 0
      panda/src/pgraph/cullableObject.h
  45. 1 2
      panda/src/pgraph/geomDrawCallbackData.cxx
  46. 11 0
      panda/src/pgraph/geomNode.cxx
  47. 280 0
      panda/src/pgraph/instanceList.I
  48. 213 0
      panda/src/pgraph/instanceList.cxx
  49. 159 0
      panda/src/pgraph/instanceList.h
  50. 39 0
      panda/src/pgraph/instancedNode.I
  51. 492 0
      panda/src/pgraph/instancedNode.cxx
  52. 136 0
      panda/src/pgraph/instancedNode.h
  53. 2 0
      panda/src/pgraph/p3pgraph_composite2.cxx
  54. 57 35
      panda/src/pgraph/pandaNode.cxx
  55. 6 0
      panda/src/pgraph/pandaNode.h
  56. 1 1
      panda/src/pgraph/shaderAttrib.I
  57. 2 0
      panda/src/pgraph/shaderAttrib.cxx
  58. 1 0
      panda/src/pgraph/shaderAttrib.h
  59. 3 1
      panda/src/pgraphnodes/shaderGenerator.cxx
  60. 2 2
      panda/src/tinydisplay/tinyGraphicsStateGuardian.cxx
  61. 1 1
      panda/src/tinydisplay/tinyGraphicsStateGuardian.h
  62. 45 0
      tests/collide/test_collision_polygon.py

+ 52 - 5
.github/workflows/ci.yml

@@ -198,7 +198,7 @@ jobs:
       shell: bash
       run: >
         cmake -DWANT_PYTHON_VERSION=3.6
-        -DPython_FIND_REGISTRY=NEVER -DPython_ROOT=$pythonLocation .
+        -DPython_FIND_REGISTRY=NEVER -DPython_ROOT="$pythonLocation" .
     - name: Build (Python 3.6)
       if: contains(matrix.python, 'YES')
       # BEGIN A
@@ -230,7 +230,7 @@ jobs:
       shell: bash
       run: >
         cmake -DWANT_PYTHON_VERSION=3.7
-        -DPython_FIND_REGISTRY=NEVER -DPython_ROOT=$pythonLocation .
+        -DPython_FIND_REGISTRY=NEVER -DPython_ROOT="$pythonLocation" .
     - name: Build (Python 3.7)
       if: contains(matrix.python, 'YES')
       # BEGIN A
@@ -262,7 +262,7 @@ jobs:
       shell: bash
       run: >
         cmake -DWANT_PYTHON_VERSION=3.8
-        -DPython_FIND_REGISTRY=NEVER -DPython_ROOT=$pythonLocation .
+        -DPython_FIND_REGISTRY=NEVER -DPython_ROOT="$pythonLocation" .
     - name: Build (Python 3.8)
       if: contains(matrix.python, 'YES')
       # BEGIN A
@@ -283,6 +283,38 @@ jobs:
         $PYTHON_EXECUTABLE -m pytest ../tests --cov=.
       # END B
 
+    - name: Setup Python (Python 3.9)
+      if: contains(matrix.python, 'YES')
+      uses: actions/setup-python@v1
+      with:
+        python-version: 3.9
+    - name: Configure (Python 3.9)
+      if: contains(matrix.python, 'YES')
+      working-directory: build
+      shell: bash
+      run: >
+        cmake -DWANT_PYTHON_VERSION=3.9
+        -DPython_FIND_REGISTRY=NEVER -DPython_ROOT="$pythonLocation" .
+    - name: Build (Python 3.9)
+      if: contains(matrix.python, 'YES')
+      # BEGIN A
+      working-directory: build
+      run: cmake --build . --config ${{ matrix.config }} --parallel 4
+      # END A
+    - name: Test (Python 3.9)
+      # BEGIN B
+      if: contains(matrix.python, 'YES')
+      working-directory: build
+      shell: bash
+      env:
+        PYTHONPATH: ${{ matrix.config }}
+      run: |
+        PYTHON_EXECUTABLE=$(grep 'Python_EXECUTABLE:' CMakeCache.txt | sed 's/.*=//')
+        $PYTHON_EXECUTABLE -m pip install pytest pytest-cov
+        export COVERAGE_FILE=.coverage.$RANDOM LLVM_PROFILE_FILE=$PWD/pid-%p.profraw
+        $PYTHON_EXECUTABLE -m pytest ../tests --cov=.
+      # END B
+
     - name: Upload coverage reports
       if: always() && matrix.config == 'Coverage'
       working-directory: build
@@ -329,13 +361,27 @@ jobs:
         mv panda3d-1.10.7/thirdparty thirdparty
         rmdir panda3d-1.10.7
         (cd thirdparty/darwin-libs-a && rm -rf rocket)
+    - name: Set up Python 3.9
+      uses: actions/setup-python@v1
+      with:
+        python-version: 3.9
+    - name: Build Python 3.9
+      shell: bash
+      run: |
+        python makepanda/makepanda.py --git-commit=${{github.sha}} --outputdir=built --everything --no-eigen --python-incdir="$pythonLocation/include" --python-libdir="$pythonLocation/lib" --verbose --threads=4
+    - name: Test Python 3.9
+      shell: bash
+      run: |
+        python -m pip install pytest
+        PYTHONPATH=built LD_LIBRARY_PATH=built/lib DYLD_LIBRARY_PATH=built/lib python -m pytest
     - name: Set up Python 3.8
       uses: actions/setup-python@v1
       with:
         python-version: 3.8
     - name: Build Python 3.8
+      shell: bash
       run: |
-        python makepanda/makepanda.py --git-commit=${{github.sha}} --outputdir=built --everything --no-eigen --python-incdir=$pythonLocation/include --python-libdir=$pythonLocation/lib --verbose --threads=4
+        python makepanda/makepanda.py --git-commit=${{github.sha}} --outputdir=built --everything --no-eigen --python-incdir="$pythonLocation/include" --python-libdir="$pythonLocation/lib" --verbose --threads=4
     - name: Test Python 3.8
       shell: bash
       run: |
@@ -346,8 +392,9 @@ jobs:
       with:
         python-version: 3.7
     - name: Build Python 3.7
+      shell: bash
       run: |
-        python makepanda/makepanda.py --git-commit=${{github.sha}} --outputdir=built --everything --no-eigen --python-incdir=$pythonLocation/include --python-libdir=$pythonLocation/lib --verbose --threads=4
+        python makepanda/makepanda.py --git-commit=${{github.sha}} --outputdir=built --everything --no-eigen --python-incdir="$pythonLocation/include" --python-libdir="$pythonLocation/lib" --verbose --threads=4
     - name: Test Python 3.7
       shell: bash
       run: |

+ 1 - 1
makepanda/installer.nsi

@@ -229,7 +229,7 @@ SectionGroup "Panda3D Libraries"
         SetDetailsPrint listonly
 
         SetOutPath $INSTDIR\models
-        File /r /x CVS "${BUILT}\models\*"
+        File /nonfatal /r /x CVS "${BUILT}\models\*"
 
         SetDetailsPrint both
         DetailPrint "Installing optional components..."

+ 2 - 0
makepanda/makepanda.py

@@ -3872,6 +3872,7 @@ OPTS=['DIR:panda/src/collide']
 IGATEFILES=GetDirectoryContents('panda/src/collide', ["*.h", "*_composite*.cxx"])
 TargetAdd('libp3collide.in', opts=OPTS, input=IGATEFILES)
 TargetAdd('libp3collide.in', opts=['IMOD:panda3d.core', 'ILIB:libp3collide', 'SRCDIR:panda/src/collide'])
+PyTargetAdd('p3collide_ext_composite.obj', opts=OPTS, input='p3collide_ext_composite.cxx')
 
 #
 # DIRECTORY: panda/src/parametrics/
@@ -4120,6 +4121,7 @@ PyTargetAdd('core.pyd', input='p3event_pythonTask.obj')
 PyTargetAdd('core.pyd', input='p3gobj_ext_composite.obj')
 PyTargetAdd('core.pyd', input='p3pgraph_ext_composite.obj')
 PyTargetAdd('core.pyd', input='p3display_ext_composite.obj')
+PyTargetAdd('core.pyd', input='p3collide_ext_composite.obj')
 
 PyTargetAdd('core.pyd', input='core_module.obj')
 if not GetLinkAllStatic() and GetTarget() != 'emscripten':

+ 6 - 1
panda/src/collide/CMakeLists.txt

@@ -65,11 +65,16 @@ set(P3COLLIDE_SOURCES
   config_collide.cxx
 )
 
+set(P3COLLIDE_IGATEEXT
+  collisionPolygon_ext.cxx
+  collisionPolygon_ext.h
+)
+
 composite_sources(p3collide P3COLLIDE_SOURCES)
 add_component_library(p3collide SYMBOL BUILDING_PANDA_COLLIDE
   ${P3COLLIDE_HEADERS} ${P3COLLIDE_SOURCES})
 target_link_libraries(p3collide p3tform)
-target_interrogate(p3collide ALL)
+target_interrogate(p3collide ALL EXTENSIONS ${P3COLLIDE_IGATEEXT})
 
 if(NOT BUILD_METALIBS)
   install(TARGETS p3collide

+ 5 - 1
panda/src/collide/collisionPolygon.h

@@ -59,6 +59,9 @@ PUBLISHED:
   bool is_valid() const;
   bool is_concave() const;
 
+  EXTENSION(static bool verify_points(PyObject *points));
+  EXTENSION(void setup_points(PyObject *points));
+
 PUBLISHED:
   MAKE_SEQ_PROPERTY(points, get_num_points, get_point);
   MAKE_PROPERTY(valid, is_valid);
@@ -71,6 +74,8 @@ public:
                                 const CullTraverserData &data,
                                 bool bounds_only) const;
 
+  void setup_points(const LPoint3 *begin, const LPoint3 *end);
+
   virtual PStatCollector &get_volume_pcollector();
   virtual PStatCollector &get_test_pcollector();
 
@@ -128,7 +133,6 @@ private:
   PN_stdfloat dist_to_polygon(const LPoint2 &p, LPoint2 &edge_p, const Points &points) const;
   void project(const LVector3 &axis, PN_stdfloat &center, PN_stdfloat &extent) const;
 
-  void setup_points(const LPoint3 *begin, const LPoint3 *end);
   INLINE LPoint2 to_2d(const LVecBase3 &point3d) const;
   INLINE void calc_to_3d_mat(LMatrix4 &to_3d_mat) const;
   INLINE void rederive_to_3d_mat(LMatrix4 &to_3d_mat) const;

+ 86 - 0
panda/src/collide/collisionPolygon_ext.cxx

@@ -0,0 +1,86 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file collisionPolygon_ext.cxx
+ * @author Derzsi Daniel
+ * @date 2020-10-13
+ */
+
+#include "collisionPolygon_ext.h"
+
+#ifdef HAVE_PYTHON
+
+#include "collisionPolygon.h"
+
+#ifdef STDFLOAT_DOUBLE
+extern struct Dtool_PyTypedObject Dtool_LPoint3d;
+#else
+extern struct Dtool_PyTypedObject Dtool_LPoint3f;
+#endif
+
+/**
+ * Verifies that the indicated Python list of points will define a
+ * CollisionPolygon.
+ */
+bool Extension<CollisionPolygon>::
+verify_points(PyObject *points) {
+  const pvector<LPoint3> vec = convert_points(points);
+  const LPoint3 *verts_begin = &vec[0];
+  const LPoint3 *verts_end = verts_begin + vec.size();
+
+  return CollisionPolygon::verify_points(verts_begin, verts_end);
+}
+
+/**
+ * Initializes this CollisionPolygon with the given Python list of
+ * points.
+ */
+void Extension<CollisionPolygon>::
+setup_points(PyObject *points) {
+  const pvector<LPoint3> vec = convert_points(points);
+  const LPoint3 *verts_begin = &vec[0];
+  const LPoint3 *verts_end = verts_begin + vec.size();
+
+  _this->setup_points(verts_begin, verts_end);
+}
+
+/**
+ * Converts a Python sequence to a list of LPoint3 objects.
+ */
+pvector<LPoint3> Extension<CollisionPolygon>::
+convert_points(PyObject *points) {
+  pvector<LPoint3> vec;
+  PyObject *seq = PySequence_Fast(points, "function expects a sequence");
+
+  if (!seq) {
+    return vec;
+  }
+
+  PyObject **items = PySequence_Fast_ITEMS(seq);
+  Py_ssize_t len = PySequence_Fast_GET_SIZE(seq);
+  void *ptr;
+
+  vec.reserve(len);
+
+  for (Py_ssize_t i = 0; i < len; ++i) {
+#ifdef STDFLOAT_DOUBLE
+    if (ptr = DtoolInstance_UPCAST(items[i], Dtool_LPoint3d)) {
+#else
+    if (ptr = DtoolInstance_UPCAST(items[i], Dtool_LPoint3f)) {
+#endif
+      vec.push_back(*(LPoint3 *)ptr);
+    } else {
+      collide_cat.warning() << "Argument must be of LPoint3 type.\n";
+    }
+  }
+
+  Py_DECREF(seq);
+  return vec;
+}
+
+#endif

+ 43 - 0
panda/src/collide/collisionPolygon_ext.h

@@ -0,0 +1,43 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file collisionPolygon_ext.h
+ * @author Derzsi Daniel
+ * @date 2020-10-13
+ */
+
+#ifndef COLLISIONPOLYGON_EXT_H
+#define COLLISIONPOLYGON_EXT_H
+
+#include "pandabase.h"
+
+#ifdef HAVE_PYTHON
+
+#include "extension.h"
+#include "collisionPolygon.h"
+#include "py_panda.h"
+
+/**
+ * This class defines the extension methods for CollisionPolygon, which are called
+ * instead of any C++ methods with the same prototype.
+ *
+ * @since 1.11.0
+ */
+template<>
+class Extension<CollisionPolygon> : public ExtensionBase<CollisionPolygon> {
+public:
+  static bool verify_points(PyObject *points);
+  void setup_points(PyObject *points);
+
+private:
+  static pvector<LPoint3> convert_points(PyObject *points);
+};
+
+#endif  // HAVE_PYTHON
+
+#endif

+ 1 - 0
panda/src/collide/p3collide_ext_composite.cxx

@@ -0,0 +1 @@
+#include "collisionPolygon_ext.cxx"

+ 2 - 18
panda/src/cull/cullBinBackToFront.cxx

@@ -85,24 +85,8 @@ void CullBinBackToFront::
 draw(bool force, Thread *current_thread) {
   PStatTimer timer(_draw_this_pcollector, current_thread);
 
-  Objects::const_iterator oi;
-  for (oi = _objects.begin(); oi != _objects.end(); ++oi) {
-    CullableObject *object = (*oi)._object;
-
-    if (object->_draw_callback == nullptr) {
-      nassertd(object->_geom != nullptr) continue;
-
-      _gsg->set_state_and_transform(object->_state, object->_internal_transform);
-
-      GeomPipelineReader geom_reader(object->_geom, current_thread);
-      GeomVertexDataPipelineReader data_reader(object->_munged_data, current_thread);
-      data_reader.check_array_readers();
-      geom_reader.draw(_gsg, &data_reader, force);
-    } else {
-      // It has a callback associated.
-      object->draw_callback(_gsg, force, current_thread);
-      // Now the callback has taken care of drawing.
-    }
+  for (const ObjectData &data : _objects) {
+    data._object->draw(_gsg, force, current_thread);
   }
 }
 

+ 2 - 18
panda/src/cull/cullBinFixed.cxx

@@ -71,24 +71,8 @@ void CullBinFixed::
 draw(bool force, Thread *current_thread) {
   PStatTimer timer(_draw_this_pcollector, current_thread);
 
-  Objects::const_iterator oi;
-  for (oi = _objects.begin(); oi != _objects.end(); ++oi) {
-    CullableObject *object = (*oi)._object;
-
-    if (object->_draw_callback == nullptr) {
-      nassertd(object->_geom != nullptr) continue;
-
-      _gsg->set_state_and_transform(object->_state, object->_internal_transform);
-
-      GeomPipelineReader geom_reader(object->_geom, current_thread);
-      GeomVertexDataPipelineReader data_reader(object->_munged_data, current_thread);
-      data_reader.check_array_readers();
-      geom_reader.draw(_gsg, &data_reader, force);
-    } else {
-      // It has a callback associated.
-      object->draw_callback(_gsg, force, current_thread);
-      // Now the callback has taken care of drawing.
-    }
+  for (const ObjectData &data : _objects) {
+    data._object->draw(_gsg, force, current_thread);
   }
 }
 

+ 2 - 18
panda/src/cull/cullBinFrontToBack.cxx

@@ -85,24 +85,8 @@ void CullBinFrontToBack::
 draw(bool force, Thread *current_thread) {
   PStatTimer timer(_draw_this_pcollector, current_thread);
 
-  Objects::const_iterator oi;
-  for (oi = _objects.begin(); oi != _objects.end(); ++oi) {
-    CullableObject *object = (*oi)._object;
-
-    if (object->_draw_callback == nullptr) {
-      nassertd(object->_geom != nullptr) continue;
-
-      _gsg->set_state_and_transform(object->_state, object->_internal_transform);
-
-      GeomPipelineReader geom_reader(object->_geom, current_thread);
-      GeomVertexDataPipelineReader data_reader(object->_munged_data, current_thread);
-      data_reader.check_array_readers();
-      geom_reader.draw(_gsg, &data_reader, force);
-    } else {
-      // It has a callback associated.
-      object->draw_callback(_gsg, force, current_thread);
-      // Now the callback has taken care of drawing.
-    }
+  for (const ObjectData &data : _objects) {
+    data._object->draw(_gsg, force, current_thread);
   }
 }
 

+ 2 - 18
panda/src/cull/cullBinStateSorted.cxx

@@ -70,24 +70,8 @@ void CullBinStateSorted::
 draw(bool force, Thread *current_thread) {
   PStatTimer timer(_draw_this_pcollector, current_thread);
 
-  Objects::const_iterator oi;
-  for (oi = _objects.begin(); oi != _objects.end(); ++oi) {
-    CullableObject *object = (*oi)._object;
-
-    if (object->_draw_callback == nullptr) {
-      nassertd(object->_geom != nullptr) continue;
-
-      _gsg->set_state_and_transform(object->_state, object->_internal_transform);
-
-      GeomPipelineReader geom_reader(object->_geom, current_thread);
-      GeomVertexDataPipelineReader data_reader(object->_munged_data, current_thread);
-      data_reader.check_array_readers();
-      geom_reader.draw(_gsg, &data_reader, force);
-    } else {
-      // It has a callback associated.
-      object->draw_callback(_gsg, force, current_thread);
-      // Now the callback has taken care of drawing.
-    }
+  for (const ObjectData &data : _objects) {
+    data._object->draw(_gsg, force, current_thread);
   }
 }
 

+ 2 - 18
panda/src/cull/cullBinUnsorted.cxx

@@ -55,24 +55,8 @@ void CullBinUnsorted::
 draw(bool force, Thread *current_thread) {
   PStatTimer timer(_draw_this_pcollector, current_thread);
 
-  Objects::iterator oi;
-  for (oi = _objects.begin(); oi != _objects.end(); ++oi) {
-    CullableObject *object = (*oi);
-
-    if (object->_draw_callback == nullptr) {
-      nassertd(object->_geom != nullptr) continue;
-
-      _gsg->set_state_and_transform(object->_state, object->_internal_transform);
-
-      GeomPipelineReader geom_reader(object->_geom, current_thread);
-      GeomVertexDataPipelineReader data_reader(object->_munged_data, current_thread);
-      data_reader.check_array_readers();
-      geom_reader.draw(_gsg, &data_reader, force);
-    } else {
-      // It has a callback associated.
-      object->draw_callback(_gsg, force, current_thread);
-      // Now the callback has taken care of drawing.
-    }
+  for (CullableObject *object : _objects) {
+    object->draw(_gsg, force, current_thread);
   }
 }
 

+ 5 - 1
panda/src/display/graphicsStateGuardian.cxx

@@ -2486,9 +2486,13 @@ finish_decal() {
 bool GraphicsStateGuardian::
 begin_draw_primitives(const GeomPipelineReader *geom_reader,
                       const GeomVertexDataPipelineReader *data_reader,
-                      bool force) {
+                      size_t num_instances, bool force) {
   _data_reader = data_reader;
 
+  if (num_instances == 0) {
+    return false;
+  }
+
   // Always draw if we have a shader, since the shader might use a different
   // mechanism for fetching vertex data.
   return _data_reader->has_vertex() || (_target_shader && _target_shader->has_shader());

+ 1 - 1
panda/src/display/graphicsStateGuardian.h

@@ -377,7 +377,7 @@ public:
 
   virtual bool begin_draw_primitives(const GeomPipelineReader *geom_reader,
                                      const GeomVertexDataPipelineReader *data_reader,
-                                     bool force);
+                                     size_t num_instances, bool force);
   virtual bool draw_triangles(const GeomPrimitivePipelineReader *reader,
                               bool force);
   virtual bool draw_triangles_adj(const GeomPrimitivePipelineReader *reader,

+ 2 - 2
panda/src/dxgsg9/dxGraphicsStateGuardian9.cxx

@@ -1141,8 +1141,8 @@ end_frame(Thread *current_thread) {
 bool DXGraphicsStateGuardian9::
 begin_draw_primitives(const GeomPipelineReader *geom_reader,
                       const GeomVertexDataPipelineReader *data_reader,
-                      bool force) {
-  if (!GraphicsStateGuardian::begin_draw_primitives(geom_reader, data_reader, force)) {
+                      size_t num_instances, bool force) {
+  if (!GraphicsStateGuardian::begin_draw_primitives(geom_reader, data_reader, num_instances, force)) {
     return false;
   }
   nassertr(_data_reader != nullptr, false);

+ 1 - 1
panda/src/dxgsg9/dxGraphicsStateGuardian9.h

@@ -107,7 +107,7 @@ public:
 
   virtual bool begin_draw_primitives(const GeomPipelineReader *geom_reader,
                                      const GeomVertexDataPipelineReader *data_reader,
-                                     bool force);
+                                     size_t num_instances, bool force);
   virtual bool draw_triangles(const GeomPrimitivePipelineReader *reader,
                               bool force);
   virtual bool draw_tristrips(const GeomPrimitivePipelineReader *reader,

+ 2 - 0
panda/src/express/config_express.cxx

@@ -26,6 +26,7 @@
 #include "virtualFileMountMultifile.h"
 #include "virtualFileMountRamdisk.h"
 #include "virtualFileMountSystem.h"
+#include "virtualFileMountZip.h"
 #include "virtualFileSimple.h"
 #include "fileReference.h"
 #include "temporaryFile.h"
@@ -116,6 +117,7 @@ init_libexpress() {
   VirtualFileMountMultifile::init_type();
   VirtualFileMountRamdisk::init_type();
   VirtualFileMountSystem::init_type();
+  VirtualFileMountZip::init_type();
   VirtualFileSimple::init_type();
   FileReference::init_type();
   TemporaryFile::init_type();

+ 51 - 35
panda/src/glstuff/glGraphicsStateGuardian_src.cxx

@@ -4393,7 +4393,7 @@ end_frame(Thread *current_thread) {
 bool CLP(GraphicsStateGuardian)::
 begin_draw_primitives(const GeomPipelineReader *geom_reader,
                       const GeomVertexDataPipelineReader *data_reader,
-                      bool force) {
+                      size_t num_instances, bool force) {
 #ifndef NDEBUG
   if (GLCAT.is_spam()) {
     GLCAT.spam() << "begin_draw_primitives: " << *(data_reader->get_object()) << "\n";
@@ -4410,11 +4410,13 @@ begin_draw_primitives(const GeomPipelineReader *geom_reader,
   }
 #endif
 
-  if (!GraphicsStateGuardian::begin_draw_primitives(geom_reader, data_reader, force)) {
+  if (!GraphicsStateGuardian::begin_draw_primitives(geom_reader, data_reader, num_instances, force)) {
     return false;
   }
   nassertr(_data_reader != nullptr, false);
 
+  _instance_count = _supports_geometry_instancing ? num_instances : 1;
+
   _geom_display_list = 0;
 
   if (_auto_antialias_mode) {
@@ -4955,7 +4957,7 @@ draw_triangles(const GeomPrimitivePipelineReader *reader, bool force) {
       }
 
 #ifndef OPENGLES_1
-      if (_supports_geometry_instancing && _instance_count > 0) {
+      if (_instance_count != 1) {
         _glDrawElementsInstanced(GL_TRIANGLES, num_vertices,
                                  get_numeric_type(reader->get_index_type()),
                                  client_pointer, _instance_count);
@@ -4971,7 +4973,7 @@ draw_triangles(const GeomPrimitivePipelineReader *reader, bool force) {
       }
     } else {
 #ifndef OPENGLES_1
-      if (_supports_geometry_instancing && _instance_count > 0) {
+      if (_instance_count != 1) {
         _glDrawArraysInstanced(GL_TRIANGLES,
                                reader->get_first_vertex(),
                                num_vertices, _instance_count);
@@ -5021,7 +5023,7 @@ draw_triangles_adj(const GeomPrimitivePipelineReader *reader, bool force) {
         return false;
       }
 
-      if (_supports_geometry_instancing && _instance_count > 0) {
+      if (_instance_count != 1) {
         _glDrawElementsInstanced(GL_TRIANGLES_ADJACENCY, num_vertices,
                                  get_numeric_type(reader->get_index_type()),
                                  client_pointer, _instance_count);
@@ -5034,7 +5036,7 @@ draw_triangles_adj(const GeomPrimitivePipelineReader *reader, bool force) {
                              client_pointer);
       }
     } else {
-      if (_supports_geometry_instancing && _instance_count > 0) {
+      if (_instance_count != 1) {
         _glDrawArraysInstanced(GL_TRIANGLES_ADJACENCY,
                                reader->get_first_vertex(),
                                num_vertices, _instance_count);
@@ -5086,7 +5088,7 @@ draw_tristrips(const GeomPrimitivePipelineReader *reader, bool force) {
           return false;
         }
 #ifndef OPENGLES_1
-        if (_supports_geometry_instancing && _instance_count > 0) {
+        if (_instance_count != 1) {
           _glDrawElementsInstanced(GL_TRIANGLE_STRIP, num_vertices,
                                    get_numeric_type(reader->get_index_type()),
                                    client_pointer, _instance_count);
@@ -5102,7 +5104,7 @@ draw_tristrips(const GeomPrimitivePipelineReader *reader, bool force) {
         }
       } else {
 #ifndef OPENGLES_1
-        if (_supports_geometry_instancing && _instance_count > 0) {
+        if (_instance_count != 1) {
           _glDrawArraysInstanced(GL_TRIANGLE_STRIP,
                                  reader->get_first_vertex(),
                                  num_vertices, _instance_count);
@@ -5136,7 +5138,7 @@ draw_tristrips(const GeomPrimitivePipelineReader *reader, bool force) {
         for (size_t i = 0; i < ends.size(); i++) {
           _vertices_tristrip_pcollector.add_level(ends[i] - start);
 #ifndef OPENGLES_1
-          if (_supports_geometry_instancing && _instance_count > 0) {
+          if (_instance_count != 1) {
             _glDrawElementsInstanced(GL_TRIANGLE_STRIP, ends[i] - start,
                                      get_numeric_type(reader->get_index_type()),
                                      client_pointer + start * index_stride,
@@ -5158,7 +5160,7 @@ draw_tristrips(const GeomPrimitivePipelineReader *reader, bool force) {
         for (size_t i = 0; i < ends.size(); i++) {
           _vertices_tristrip_pcollector.add_level(ends[i] - start);
 #ifndef OPENGLES_1
-          if (_supports_geometry_instancing && _instance_count > 0) {
+          if (_instance_count != 1) {
             _glDrawArraysInstanced(GL_TRIANGLE_STRIP, first_vertex + start,
                                    ends[i] - start, _instance_count);
           } else
@@ -5216,7 +5218,7 @@ draw_tristrips_adj(const GeomPrimitivePipelineReader *reader, bool force) {
         if (!setup_primitive(client_pointer, reader, force)) {
           return false;
         }
-        if (_supports_geometry_instancing && _instance_count > 0) {
+        if (_instance_count != 1) {
           _glDrawElementsInstanced(GL_TRIANGLE_STRIP_ADJACENCY, num_vertices,
                                    get_numeric_type(reader->get_index_type()),
                                    client_pointer, _instance_count);
@@ -5229,7 +5231,7 @@ draw_tristrips_adj(const GeomPrimitivePipelineReader *reader, bool force) {
                                client_pointer);
         }
       } else {
-        if (_supports_geometry_instancing && _instance_count > 0) {
+        if (_instance_count != 1) {
           _glDrawArraysInstanced(GL_TRIANGLE_STRIP_ADJACENCY,
                                  reader->get_first_vertex(),
                                  num_vertices, _instance_count);
@@ -5262,7 +5264,7 @@ draw_tristrips_adj(const GeomPrimitivePipelineReader *reader, bool force) {
         unsigned int start = 0;
         for (size_t i = 0; i < ends.size(); i++) {
           _vertices_tristrip_pcollector.add_level(ends[i] - start);
-          if (_supports_geometry_instancing && _instance_count > 0) {
+          if (_instance_count != 1) {
             _glDrawElementsInstanced(GL_TRIANGLE_STRIP_ADJACENCY, ends[i] - start,
                                      get_numeric_type(reader->get_index_type()),
                                      client_pointer + start * index_stride,
@@ -5281,7 +5283,7 @@ draw_tristrips_adj(const GeomPrimitivePipelineReader *reader, bool force) {
         int first_vertex = reader->get_first_vertex();
         for (size_t i = 0; i < ends.size(); i++) {
           _vertices_tristrip_pcollector.add_level(ends[i] - start);
-          if (_supports_geometry_instancing && _instance_count > 0) {
+          if (_instance_count != 1) {
             _glDrawArraysInstanced(GL_TRIANGLE_STRIP_ADJACENCY, first_vertex + start,
                                    ends[i] - start, _instance_count);
           } else {
@@ -5339,7 +5341,7 @@ draw_trifans(const GeomPrimitivePipelineReader *reader, bool force) {
       for (size_t i = 0; i < ends.size(); i++) {
         _vertices_trifan_pcollector.add_level(ends[i] - start);
 #ifndef OPENGLES_1
-        if (_supports_geometry_instancing && _instance_count > 0) {
+        if (_instance_count != 1) {
           _glDrawElementsInstanced(GL_TRIANGLE_FAN, ends[i] - start,
                                    get_numeric_type(reader->get_index_type()),
                                    client_pointer + start * index_stride,
@@ -5360,7 +5362,7 @@ draw_trifans(const GeomPrimitivePipelineReader *reader, bool force) {
       for (size_t i = 0; i < ends.size(); i++) {
         _vertices_trifan_pcollector.add_level(ends[i] - start);
 #ifndef OPENGLES_1
-        if (_supports_geometry_instancing && _instance_count > 0) {
+        if (_instance_count != 1) {
           _glDrawArraysInstanced(GL_TRIANGLE_FAN, first_vertex + start,
                                  ends[i] - start, _instance_count);
         } else
@@ -5418,7 +5420,7 @@ draw_patches(const GeomPrimitivePipelineReader *reader, bool force) {
       }
 
 #ifndef OPENGLES_1
-      if (_supports_geometry_instancing && _instance_count > 0) {
+      if (_instance_count != 1) {
         _glDrawElementsInstanced(GL_PATCHES, num_vertices,
                                  get_numeric_type(reader->get_index_type()),
                                  client_pointer, _instance_count);
@@ -5434,7 +5436,7 @@ draw_patches(const GeomPrimitivePipelineReader *reader, bool force) {
       }
     } else {
 #ifndef OPENGLES_1
-      if (_supports_geometry_instancing && _instance_count > 0) {
+      if (_instance_count != 1) {
         _glDrawArraysInstanced(GL_PATCHES,
                                reader->get_first_vertex(),
                                num_vertices, _instance_count);
@@ -5484,7 +5486,7 @@ draw_lines(const GeomPrimitivePipelineReader *reader, bool force) {
         return false;
       }
 #ifndef OPENGLES_1
-      if (_supports_geometry_instancing && _instance_count > 0) {
+      if (_instance_count != 1) {
         _glDrawElementsInstanced(GL_LINES, num_vertices,
                                  get_numeric_type(reader->get_index_type()),
                                  client_pointer, _instance_count);
@@ -5500,7 +5502,7 @@ draw_lines(const GeomPrimitivePipelineReader *reader, bool force) {
       }
     } else {
 #ifndef OPENGLES_1
-      if (_supports_geometry_instancing && _instance_count > 0) {
+      if (_instance_count != 1) {
         _glDrawArraysInstanced(GL_LINES,
                                reader->get_first_vertex(),
                                num_vertices, _instance_count);
@@ -5548,7 +5550,7 @@ draw_lines_adj(const GeomPrimitivePipelineReader *reader, bool force) {
       if (!setup_primitive(client_pointer, reader, force)) {
         return false;
       }
-      if (_supports_geometry_instancing && _instance_count > 0) {
+      if (_instance_count != 1) {
         _glDrawElementsInstanced(GL_LINES_ADJACENCY, num_vertices,
                                  get_numeric_type(reader->get_index_type()),
                                  client_pointer, _instance_count);
@@ -5561,7 +5563,7 @@ draw_lines_adj(const GeomPrimitivePipelineReader *reader, bool force) {
                              client_pointer);
       }
     } else {
-      if (_supports_geometry_instancing && _instance_count > 0) {
+      if (_instance_count != 1) {
         _glDrawArraysInstanced(GL_LINES_ADJACENCY,
                                reader->get_first_vertex(),
                                num_vertices, _instance_count);
@@ -5620,7 +5622,7 @@ draw_linestrips(const GeomPrimitivePipelineReader *reader, bool force) {
         return false;
       }
 #ifndef OPENGLES_1
-      if (_supports_geometry_instancing && _instance_count > 0) {
+      if (_instance_count != 1) {
         _glDrawElementsInstanced(GL_LINE_STRIP, num_vertices,
                                  get_numeric_type(reader->get_index_type()),
                                  client_pointer, _instance_count);
@@ -5660,7 +5662,7 @@ draw_linestrips(const GeomPrimitivePipelineReader *reader, bool force) {
         for (size_t i = 0; i < ends.size(); i++) {
           _vertices_other_pcollector.add_level(ends[i] - start);
 #ifndef OPENGLES_1
-          if (_supports_geometry_instancing && _instance_count > 0) {
+          if (_instance_count != 1) {
             _glDrawElementsInstanced(GL_LINE_STRIP, ends[i] - start,
                                      get_numeric_type(reader->get_index_type()),
                                      client_pointer + start * index_stride,
@@ -5682,7 +5684,7 @@ draw_linestrips(const GeomPrimitivePipelineReader *reader, bool force) {
         for (size_t i = 0; i < ends.size(); i++) {
           _vertices_other_pcollector.add_level(ends[i] - start);
 #ifndef OPENGLES_1
-          if (_supports_geometry_instancing && _instance_count > 0) {
+          if (_instance_count != 1) {
             _glDrawArraysInstanced(GL_LINE_STRIP, first_vertex + start,
                                    ends[i] - start, _instance_count);
           } else
@@ -5740,7 +5742,7 @@ draw_linestrips_adj(const GeomPrimitivePipelineReader *reader, bool force) {
       if (!setup_primitive(client_pointer, reader, force)) {
         return false;
       }
-      if (_supports_geometry_instancing && _instance_count > 0) {
+      if (_instance_count != 1) {
         _glDrawElementsInstanced(GL_LINE_STRIP_ADJACENCY, num_vertices,
                                  get_numeric_type(reader->get_index_type()),
                                  client_pointer, _instance_count);
@@ -5775,7 +5777,7 @@ draw_linestrips_adj(const GeomPrimitivePipelineReader *reader, bool force) {
         unsigned int start = 0;
         for (size_t i = 0; i < ends.size(); i++) {
           _vertices_other_pcollector.add_level(ends[i] - start);
-          if (_supports_geometry_instancing && _instance_count > 0) {
+          if (_instance_count != 1) {
             _glDrawElementsInstanced(GL_LINE_STRIP_ADJACENCY, ends[i] - start,
                                      get_numeric_type(reader->get_index_type()),
                                      client_pointer + start * index_stride,
@@ -5794,7 +5796,7 @@ draw_linestrips_adj(const GeomPrimitivePipelineReader *reader, bool force) {
         int first_vertex = reader->get_first_vertex();
         for (size_t i = 0; i < ends.size(); i++) {
           _vertices_other_pcollector.add_level(ends[i] - start);
-          if (_supports_geometry_instancing && _instance_count > 0) {
+          if (_instance_count != 1) {
             _glDrawArraysInstanced(GL_LINE_STRIP_ADJACENCY, first_vertex + start,
                                    ends[i] - start, _instance_count);
           } else {
@@ -5841,7 +5843,7 @@ draw_points(const GeomPrimitivePipelineReader *reader, bool force) {
         return false;
       }
 #ifndef OPENGLES_1
-      if (_supports_geometry_instancing && _instance_count > 0) {
+      if (_instance_count != 1) {
         _glDrawElementsInstanced(GL_POINTS, num_vertices,
                                  get_numeric_type(reader->get_index_type()),
                                  client_pointer, _instance_count);
@@ -5857,7 +5859,7 @@ draw_points(const GeomPrimitivePipelineReader *reader, bool force) {
       }
     } else {
 #ifndef OPENGLES_1
-      if (_supports_geometry_instancing && _instance_count > 0) {
+      if (_instance_count != 1) {
         _glDrawArraysInstanced(GL_POINTS,
                                reader->get_first_vertex(),
                                num_vertices, _instance_count);
@@ -9824,15 +9826,22 @@ get_external_image_format(Texture *tex) const {
 
   case Texture::F_rgba:
   case Texture::F_rgbm:
-  case Texture::F_rgba4:
-  case Texture::F_rgba5:
   case Texture::F_rgba8:
   case Texture::F_rgba12:
+    return _supports_bgr ? GL_BGRA : GL_RGBA;
+
+  case Texture::F_rgba4:
+  case Texture::F_rgba5:
   case Texture::F_rgba16:
   case Texture::F_rgba32:
   case Texture::F_srgb_alpha:
   case Texture::F_rgb10_a2:
+#ifdef OPENGLES
+    // OpenGL ES doesn't have sized BGRA formats.
+    return GL_RGBA;
+#else
     return _supports_bgr ? GL_BGRA : GL_RGBA;
+#endif
 
   case Texture::F_luminance:
 #ifdef OPENGLES
@@ -10353,9 +10362,9 @@ get_internal_image_format(Texture *tex, bool force_sized) const {
 
 #ifdef OPENGLES
   case Texture::F_rgba8:
-    return GL_RGBA8_OES;
+    return _supports_bgr ? GL_BGRA : GL_RGBA8_OES;
   case Texture::F_rgba12:
-    return force_sized ? GL_RGBA8 : GL_RGBA;
+    return _supports_bgr ? GL_BGRA : (force_sized ? GL_RGBA8 : GL_RGBA);
 #else
   case Texture::F_rgba8:
     if (Texture::is_unsigned(tex->get_component_type())) {
@@ -11497,7 +11506,7 @@ set_state_and_transform(const RenderState *target,
 
 #ifndef OPENGLES_1
   determine_target_shader();
-  _instance_count = _target_shader->get_instance_count();
+  _sattr_instance_count = _target_shader->get_instance_count();
 
   if (_target_shader != _state_shader) {
     do_issue_shader();
@@ -13832,7 +13841,11 @@ upload_simple_texture(CLP(TextureContext) *gtc) {
   Texture *tex = gtc->get_texture();
   nassertr(tex != nullptr, false);
 
+#ifdef OPENGLES
+  GLenum internal_format = GL_BGRA;
+#else
   GLenum internal_format = GL_RGBA;
+#endif
   GLenum external_format = GL_BGRA;
 
   const unsigned char *image_ptr = tex->get_simple_ram_image();
@@ -13846,6 +13859,9 @@ upload_simple_texture(CLP(TextureContext) *gtc) {
     // If the GL doesn't claim to support BGR, we may have to reverse the
     // component ordering of the image.
     external_format = GL_RGBA;
+#ifdef OPENGLES
+    internal_format = GL_RGBA;
+#endif
     image_ptr = fix_component_ordering(bgr_image, image_ptr, image_size,
                                        external_format, tex);
   }

+ 2 - 1
panda/src/glstuff/glGraphicsStateGuardian_src.h

@@ -313,7 +313,7 @@ public:
 
   virtual bool begin_draw_primitives(const GeomPipelineReader *geom_reader,
                                      const GeomVertexDataPipelineReader *data_reader,
-                                     bool force);
+                                     size_t num_instances, bool force);
   virtual bool draw_triangles(const GeomPrimitivePipelineReader *reader,
                               bool force);
 #ifndef OPENGLES
@@ -1107,6 +1107,7 @@ public:
   bool _supports_texture_max_level;
 
 #ifndef OPENGLES_1
+  GLsizei _sattr_instance_count;
   GLsizei _instance_count;
 #endif
 

+ 12 - 0
panda/src/glstuff/glShaderContext_src.cxx

@@ -2773,6 +2773,18 @@ update_shader_vertex_arrays(ShaderContext *prev, bool force) {
                  _glgsg->_glVertexAttribI4ui != nullptr) {
           _glgsg->_glVertexAttribI4ui(p, 0, 1, 2, 3);
         }
+        else if (name == InternalName::get_instance_matrix()) {
+          const LMatrix4 &ident_mat = LMatrix4::ident_mat();
+
+          for (int i = 0; i < bind._elements; ++i) {
+#ifdef STDFLOAT_DOUBLE
+            _glgsg->_glVertexAttrib4dv(p, ident_mat.get_data() + i * 4);
+#else
+            _glgsg->_glVertexAttrib4fv(p, ident_mat.get_data() + i * 4);
+#endif
+            ++p;
+          }
+        }
       }
     }
 

+ 7 - 4
panda/src/gobj/geom.cxx

@@ -1298,18 +1298,20 @@ prepare_now(PreparedGraphicsObjects *prepared_objects,
  * Actually draws the Geom with the indicated GSG, using the indicated vertex
  * data (which might have been pre-munged to support the GSG's needs).
  *
+ * num_instances specifies the number of times to render the geometry.
+ *
  * Returns true if all of the primitives were drawn normally, false if there
  * was a problem (for instance, some of the data was nonresident).  If force
  * is passed true, it will wait for the data to become resident if necessary.
  */
 bool Geom::
 draw(GraphicsStateGuardianBase *gsg, const GeomVertexData *vertex_data,
-     bool force, Thread *current_thread) const {
+     size_t num_instances, bool force, Thread *current_thread) const {
   GeomPipelineReader geom_reader(this, current_thread);
   GeomVertexDataPipelineReader data_reader(vertex_data, current_thread);
   data_reader.check_array_readers();
 
-  return geom_reader.draw(gsg, &data_reader, force);
+  return geom_reader.draw(gsg, &data_reader, num_instances, force);
 }
 
 /**
@@ -1847,11 +1849,12 @@ check_valid(const GeomVertexDataPipelineReader *data_reader) const {
  */
 bool GeomPipelineReader::
 draw(GraphicsStateGuardianBase *gsg,
-     const GeomVertexDataPipelineReader *data_reader, bool force) const {
+     const GeomVertexDataPipelineReader *data_reader,
+     size_t num_instances, bool force) const {
   bool all_ok;
   {
     PStatTimer timer(Geom::_draw_primitive_setup_pcollector);
-    all_ok = gsg->begin_draw_primitives(this, data_reader, force);
+    all_ok = gsg->begin_draw_primitives(this, data_reader, num_instances, force);
   }
   if (all_ok) {
     Geom::Primitives::const_iterator pi;

+ 2 - 2
panda/src/gobj/geom.h

@@ -158,7 +158,7 @@ PUBLISHED:
 
 public:
   bool draw(GraphicsStateGuardianBase *gsg,
-            const GeomVertexData *vertex_data,
+            const GeomVertexData *vertex_data, size_t num_instances,
             bool force, Thread *current_thread) const;
 
   INLINE void calc_tight_bounds(LPoint3 &min_point, LPoint3 &max_point,
@@ -433,7 +433,7 @@ public:
 
   bool draw(GraphicsStateGuardianBase *gsg,
             const GeomVertexDataPipelineReader *data_reader,
-            bool force) const;
+            size_t num_instances, bool force) const;
 
 private:
   const Geom *_object;

+ 21 - 0
panda/src/gobj/geomVertexArrayFormat.cxx

@@ -655,6 +655,27 @@ compare_to(const GeomVertexArrayFormat &other) const {
   return 0;
 }
 
+/**
+ * Returns a suitable format for sending an array of instances to the graphics
+ * backend.
+ *
+ * This may only be called after the format has been registered.  The return
+ * value will have been already registered.
+ */
+const GeomVertexArrayFormat *GeomVertexArrayFormat::
+get_instance_array_format() {
+  static CPT(GeomVertexArrayFormat) inst_array_format;
+
+  if (inst_array_format == nullptr) {
+    GeomVertexArrayFormat *new_array_format = new GeomVertexArrayFormat("instance_matrix", 4, NT_stdfloat, C_matrix);
+    new_array_format->set_divisor(1);
+    inst_array_format = GeomVertexArrayFormat::register_format(new_array_format);
+  }
+
+  nassertr(inst_array_format != nullptr, nullptr);
+  return inst_array_format.p();
+}
+
 /**
  * Resorts the _columns vector so that the columns are listed in the same
  * order they appear in the record.

+ 2 - 0
panda/src/gobj/geomVertexArrayFormat.h

@@ -123,6 +123,8 @@ PUBLISHED:
 public:
   int compare_to(const GeomVertexArrayFormat &other) const;
 
+  static const GeomVertexArrayFormat *get_instance_array_format();
+
 private:
   class Registry;
   INLINE static Registry *get_registry();

+ 23 - 0
panda/src/gobj/geomVertexData.I

@@ -208,6 +208,29 @@ set_array(size_t i, const GeomVertexArrayData *array) {
   writer.set_array(i, array);
 }
 
+/**
+ * Removes the array wit hthe given index from the GeomVertexData.
+ */
+INLINE void GeomVertexData::
+remove_array(size_t i) {
+  GeomVertexDataPipelineWriter writer(this, true, Thread::get_current_thread());
+  writer.remove_array(i);
+}
+
+/**
+ * Inserts the indicated vertex data array into the list of arrays, which also
+ * modifies the format.  You should be careful that the new array has the same
+ * number of rows as the vertex data.
+ *
+ * Don't call this in a downstream thread unless you don't mind it blowing
+ * away other changes you might have recently made in an upstream thread.
+ */
+INLINE void GeomVertexData::
+insert_array(size_t i, const GeomVertexArrayData *array) {
+  GeomVertexDataPipelineWriter writer(this, true, Thread::get_current_thread());
+  writer.insert_array(i, array);
+}
+
 /**
  * Returns a const pointer to the TransformTable assigned to this data.
  * Vertices within the table will index into this table to indicate their

+ 46 - 0
panda/src/gobj/geomVertexData.cxx

@@ -2582,6 +2582,52 @@ set_array(size_t i, const GeomVertexArrayData *array) {
   }
 }
 
+/**
+ *
+ */
+void GeomVertexDataPipelineWriter::
+remove_array(size_t i) {
+  nassertv(i < _cdata->_arrays.size());
+
+  GeomVertexFormat *new_format = new GeomVertexFormat(*_cdata->_format);
+  new_format->remove_array(i);
+  _cdata->_format = GeomVertexFormat::register_format(new_format);
+  _cdata->_arrays.erase(_cdata->_arrays.begin() + i);
+
+  _object->clear_cache_stage();
+  _cdata->_modified = Geom::get_next_modified();
+  _cdata->_animated_vertices.clear();
+
+  if (_got_array_writers) {
+    _array_writers.erase(_array_writers.begin() + i);
+  }
+}
+
+/**
+ *
+ */
+void GeomVertexDataPipelineWriter::
+insert_array(size_t i, const GeomVertexArrayData *array) {
+  const GeomVertexArrayFormat *array_format = array->get_array_format();
+
+  if (i > _cdata->_arrays.size()) {
+    i = _cdata->_arrays.size();
+  }
+
+  GeomVertexFormat *new_format = new GeomVertexFormat(*_cdata->_format);
+  new_format->insert_array(i, array_format);
+  _cdata->_format = GeomVertexFormat::register_format(new_format);
+  _cdata->_arrays.insert(_cdata->_arrays.begin() + i, (GeomVertexArrayData *)array);
+
+  _object->clear_cache_stage();
+  _cdata->_modified = Geom::get_next_modified();
+  _cdata->_animated_vertices.clear();
+
+  if (_got_array_writers) {
+    _array_writers.insert(_array_writers.begin() + i, new GeomVertexArrayDataHandle(_cdata->_arrays[i].get_write_pointer(), _current_thread));
+  }
+}
+
 /**
  * Copies a single row of the data from the other array into the indicated row
  * of this array.  In this case, the source format must exactly match the

+ 5 - 1
panda/src/gobj/geomVertexData.h

@@ -112,7 +112,9 @@ PUBLISHED:
   INLINE PT(GeomVertexArrayData) modify_array(size_t i);
   INLINE PT(GeomVertexArrayDataHandle) modify_array_handle(size_t i);
   INLINE void set_array(size_t i, const GeomVertexArrayData *array);
-  MAKE_SEQ_PROPERTY(arrays, get_num_arrays, get_array, set_array);
+  INLINE void remove_array(size_t i);
+  INLINE void insert_array(size_t i, const GeomVertexArrayData *array);
+  MAKE_SEQ_PROPERTY(arrays, get_num_arrays, get_array, set_array, remove_array, insert_array);
 
   INLINE const TransformTable *get_transform_table() const;
   void set_transform_table(const TransformTable *table);
@@ -520,6 +522,8 @@ public:
 
   PT(GeomVertexArrayData) modify_array(size_t i);
   void set_array(size_t i, const GeomVertexArrayData *array);
+  void remove_array(size_t i);
+  void insert_array(size_t i, const GeomVertexArrayData *array);
 
   int get_num_rows() const;
   bool set_num_rows(int n);

+ 31 - 0
panda/src/gobj/geomVertexFormat.cxx

@@ -134,6 +134,32 @@ get_post_animated_format() const {
   return _post_animated_format;
 }
 
+/**
+ * Returns a suitable vertex format for sending the animated vertices to the
+ * graphics backend.  This is the same format as the source format, with the
+ * instancing columns added.
+ *
+ * This may only be called after the format has been registered.  The return
+ * value will have been already registered.
+ */
+CPT(GeomVertexFormat) GeomVertexFormat::
+get_post_instanced_format() const {
+  nassertr(is_registered(), nullptr);
+
+  if (_post_instanced_format == nullptr) {
+    PT(GeomVertexFormat) new_format = new GeomVertexFormat(*this);
+    new_format->add_array(GeomVertexArrayFormat::register_format(GeomVertexArrayFormat::get_instance_array_format()));
+
+    CPT(GeomVertexFormat) registered =
+      GeomVertexFormat::register_format(new_format);
+    ((GeomVertexFormat *)this)->_post_instanced_format = registered;
+  }
+
+  _post_instanced_format->test_ref_count_integrity();
+
+  return _post_instanced_format;
+}
+
 /**
  * Returns a new GeomVertexFormat that includes all of the columns defined in
  * either this GeomVertexFormat or the other one.  If any column is defined in
@@ -818,6 +844,11 @@ do_unregister() {
     unref_delete(_post_animated_format);
   }
   _post_animated_format = nullptr;
+
+  if (_post_instanced_format != nullptr) {
+    unref_delete(_post_instanced_format);
+    _post_instanced_format = nullptr;
+  }
 }
 
 /**

+ 3 - 1
panda/src/gobj/geomVertexFormat.h

@@ -72,6 +72,7 @@ PUBLISHED:
   MAKE_PROPERTY(animation, get_animation, set_animation);
 
   CPT(GeomVertexFormat) get_post_animated_format() const;
+  CPT(GeomVertexFormat) get_post_instanced_format() const;
   CPT(GeomVertexFormat) get_union_format(const GeomVertexFormat *other) const;
 
   INLINE size_t get_num_arrays() const;
@@ -222,7 +223,8 @@ private:
   typedef pvector<MorphRecord> Morphs;
   Morphs _morphs;
 
-  const GeomVertexFormat *_post_animated_format;
+  const GeomVertexFormat *_post_animated_format = nullptr;
+  const GeomVertexFormat *_post_instanced_format = nullptr;
 
   // This is the global registry of all currently-in-use formats.
   typedef pset<GeomVertexFormat *, IndirectCompareTo<GeomVertexFormat> > Formats;

+ 11 - 0
panda/src/gobj/internalName.I

@@ -366,6 +366,17 @@ get_view() {
   return _view;
 }
 
+/**
+ * Returns the standard InternalName "instance_matrix".
+ */
+INLINE PT(InternalName) InternalName::
+get_instance_matrix() {
+  if (_instance_matrix == nullptr) {
+    _instance_matrix = InternalName::make("instance_matrix");
+  }
+  return _instance_matrix;
+}
+
 /**
  *
  */

+ 1 - 0
panda/src/gobj/internalName.cxx

@@ -40,6 +40,7 @@ PT(InternalName) InternalName::_world;
 PT(InternalName) InternalName::_camera;
 PT(InternalName) InternalName::_model;
 PT(InternalName) InternalName::_view;
+PT(InternalName) InternalName::_instance_matrix;
 
 TypeHandle InternalName::_type_handle;
 TypeHandle InternalName::_texcoord_type_handle;

+ 2 - 0
panda/src/gobj/internalName.h

@@ -92,6 +92,7 @@ PUBLISHED:
   INLINE static PT(InternalName) get_camera();
   INLINE static PT(InternalName) get_model();
   INLINE static PT(InternalName) get_view();
+  INLINE static PT(InternalName) get_instance_matrix();
 
 #ifdef HAVE_PYTHON
   // These versions are exposed to Python, which have additional logic to map
@@ -141,6 +142,7 @@ private:
   static PT(InternalName) _camera;
   static PT(InternalName) _model;
   static PT(InternalName) _view;
+  static PT(InternalName) _instance_matrix;
 
 public:
   // Datagram stuff

+ 8 - 2
panda/src/gobj/shader.cxx

@@ -1018,13 +1018,12 @@ bind_vertex_input(const InternalName *name, const ::ShaderType *type, int locati
   bind._name = nullptr;
   bind._append_uv = -1;
 
-  //FIXME: matrices
   uint32_t dim[3];
   if (!type->as_scalar_type(bind._scalar_type, dim[0], dim[1], dim[2])) {
     shader_cat.error()
       << "Unrecognized type " << *type << " for vertex input " << *name << "\n";
   }
-  bind._elements = dim[0];
+  bind._elements = dim[0] * dim[1];
 
   if (shader_cat.is_debug()) {
     shader_cat.debug()
@@ -1060,6 +1059,13 @@ bind_vertex_input(const InternalName *name, const ::ShaderType *type, int locati
       bind._name = InternalName::get_texcoord();
       bind._append_uv = atoi(name_str.substr(17).c_str());
     }
+    else if (name_str == "p3d_InstanceMatrix") {
+      bind._name = InternalName::get_instance_matrix();
+
+      if (dim[1] != 4 || dim[2] != 3) {
+        return report_parameter_error(name, type, "expected mat4x3");
+      }
+    }
     else {
       shader_cat.error()
         << "Unrecognized built-in vertex input name '" << name_str << "'!\n";

+ 1 - 1
panda/src/gsgbase/graphicsStateGuardianBase.h

@@ -204,7 +204,7 @@ public:
 
   virtual bool begin_draw_primitives(const GeomPipelineReader *geom_reader,
                                      const GeomVertexDataPipelineReader *data_reader,
-                                     bool force)=0;
+                                     size_t num_instances, bool force)=0;
   virtual bool draw_triangles(const GeomPrimitivePipelineReader *reader, bool force)=0;
   virtual bool draw_triangles_adj(const GeomPrimitivePipelineReader *reader, bool force)=0;
   virtual bool draw_tristrips(const GeomPrimitivePipelineReader *reader, bool force)=0;

+ 4 - 0
panda/src/pgraph/CMakeLists.txt

@@ -39,6 +39,8 @@ set(P3PGRAPH_HEADERS
   geomDrawCallbackData.I geomDrawCallbackData.h
   geomNode.I geomNode.h
   geomTransformer.I geomTransformer.h
+  instanceList.I instanceList.h
+  instancedNode.I instancedNode.h
   internalNameCollection.I internalNameCollection.h
   lensNode.I lensNode.h
   light.I light.h
@@ -139,6 +141,8 @@ set(P3PGRAPH_SOURCES
   geomDrawCallbackData.cxx
   geomNode.cxx
   geomTransformer.cxx
+  instanceList.cxx
+  instancedNode.cxx
   internalNameCollection.cxx
   lensNode.cxx
   light.cxx

+ 6 - 0
panda/src/pgraph/config_pgraph.cxx

@@ -42,6 +42,8 @@
 #include "geomDrawCallbackData.h"
 #include "geomNode.h"
 #include "geomTransformer.h"
+#include "instanceList.h"
+#include "instancedNode.h"
 #include "lensNode.h"
 #include "light.h"
 #include "lightAttrib.h"
@@ -416,6 +418,8 @@ init_libpgraph() {
   GeomDrawCallbackData::init_type();
   GeomNode::init_type();
   GeomTransformer::init_type();
+  InstanceList::init_type();
+  InstancedNode::init_type();
   LensNode::init_type();
   Light::init_type();
   LightAttrib::init_type();
@@ -484,6 +488,8 @@ init_libpgraph() {
   Fog::register_with_read_factory();
   FogAttrib::register_with_read_factory();
   GeomNode::register_with_read_factory();
+  InstanceList::register_with_read_factory();
+  InstancedNode::register_with_read_factory();
   LensNode::register_with_read_factory();
   LightAttrib::register_with_read_factory();
   LightRampAttrib::register_with_read_factory();

+ 18 - 2
panda/src/pgraph/cullTraverser.cxx

@@ -271,10 +271,10 @@ show_bounds(CullTraverserData &data, bool tight) {
       CullableObject *outer_viz =
         new CullableObject(std::move(bounds_viz), get_bounds_outer_viz_state(),
                            internal_transform);
+      outer_viz->_instances = data._instances;
       _cull_handler->record_object(outer_viz, this);
     }
-
-  } else {
+  } else if (data._instances == nullptr) {
     draw_bounding_volume(node->get_bounds(), internal_transform);
 
     if (node->is_geom_node()) {
@@ -287,6 +287,22 @@ show_bounds(CullTraverserData &data, bool tight) {
                              internal_transform);
       }
     }
+  } else {
+    // Draw bounds for every instance.
+    for (const InstanceList::Instance &instance : *data._instances) {
+      CPT(TransformState) transform = internal_transform->compose(instance.get_transform());
+      draw_bounding_volume(node->get_bounds(), transform);
+
+      if (node->is_geom_node()) {
+        // Also show the bounding volumes of included Geoms.
+        transform = transform->compose(node->get_transform());
+        GeomNode *gnode = (GeomNode *)node;
+        int num_geoms = gnode->get_num_geoms();
+        for (int i = 0; i < num_geoms; ++i) {
+          draw_bounding_volume(gnode->get_geom(i)->get_bounds(), transform);
+        }
+      }
+    }
   }
 }
 

+ 3 - 1
panda/src/pgraph/cullTraverserData.I

@@ -50,6 +50,7 @@ CullTraverserData(const CullTraverserData &parent, PandaNode *child) :
   _state(parent._state),
   _view_frustum(parent._view_frustum),
   _cull_planes(parent._cull_planes),
+  _instances(parent._instances),
   _draw_mask(parent._draw_mask),
   _portal_depth(parent._portal_depth)
 {
@@ -110,7 +111,8 @@ get_modelview_transform(const CullTraverser *trav) const {
  */
 INLINE CPT(TransformState) CullTraverserData::
 get_internal_transform(const CullTraverser *trav) const {
-  return trav->get_scene()->get_cs_world_transform()->compose(_net_transform);
+  const TransformState *cs_world_transform = trav->get_scene()->get_cs_world_transform();
+  return cs_world_transform->compose(_net_transform);
 }
 
 /**

+ 9 - 0
panda/src/pgraph/cullTraverserData.cxx

@@ -77,6 +77,15 @@ apply_transform_and_state(CullTraverser *trav) {
 void CullTraverserData::
 apply_transform(const TransformState *node_transform) {
   if (!node_transform->is_identity()) {
+    if (_instances != nullptr) {
+      InstanceList *instances = new InstanceList(*_instances);
+      for (InstanceList::Instance &instance : *instances) {
+        instance.set_transform(instance.get_transform()->compose(node_transform));
+      }
+      _instances = std::move(instances);
+      return;
+    }
+
     _net_transform = _net_transform->compose(node_transform);
 
     if ((_view_frustum != nullptr) ||

+ 2 - 0
panda/src/pgraph/cullTraverserData.h

@@ -23,6 +23,7 @@
 #include "pointerTo.h"
 #include "drawMask.h"
 #include "pvector.h"
+#include "instanceList.h"
 
 class PandaNode;
 class CullTraverser;
@@ -81,6 +82,7 @@ public:
   CPT(RenderState) _state;
   PT(GeometricBoundingVolume) _view_frustum;
   CPT(CullPlanes) _cull_planes;
+  CPT(InstanceList) _instances;
   DrawMask _draw_mask;
   int _portal_depth;
 

+ 18 - 3
panda/src/pgraph/cullableObject.I

@@ -70,7 +70,7 @@ operator = (const CullableObject &copy) {
  */
 INLINE void CullableObject::
 draw(GraphicsStateGuardianBase *gsg, bool force, Thread *current_thread) {
-  if (_draw_callback != nullptr) {
+  if (UNLIKELY(_draw_callback != nullptr)) {
     // It has a callback associated.
     gsg->clear_before_callback();
     gsg->set_state_and_transform(_state, _internal_transform);
@@ -81,11 +81,26 @@ draw(GraphicsStateGuardianBase *gsg, bool force, Thread *current_thread) {
       gsg->clear_state_and_transform();
     }
     // Now the callback has taken care of drawing.
-  } else {
+  }
+  else if (LIKELY(_instances == nullptr)) {
     nassertv(_geom != nullptr);
     gsg->set_state_and_transform(_state, _internal_transform);
     draw_inline(gsg, force, current_thread);
   }
+  else {
+    // It has an instance list left over (not munged into vertex data), which
+    // means the shader doesn't implement instancing.  Just render the object
+    // more than once.
+    nassertv(_geom != nullptr);
+    GeomPipelineReader geom_reader(_geom, current_thread);
+    GeomVertexDataPipelineReader data_reader(_munged_data, current_thread);
+    data_reader.check_array_readers();
+
+    for (const InstanceList::Instance &instance : *_instances) {
+      gsg->set_state_and_transform(_state, _internal_transform->compose(instance.get_transform()));
+      geom_reader.draw(gsg, &data_reader, _num_instances, force);
+    }
+  }
 }
 
 /**
@@ -130,7 +145,7 @@ flush_level() {
  */
 INLINE void CullableObject::
 draw_inline(GraphicsStateGuardianBase *gsg, bool force, Thread *current_thread) {
-  _geom->draw(gsg, _munged_data, force, current_thread);
+  _geom->draw(gsg, _munged_data, _num_instances, force, current_thread);
 }
 
 /**

+ 34 - 0
panda/src/pgraph/cullableObject.cxx

@@ -39,6 +39,7 @@ PStatCollector CullableObject::_munge_geom_pcollector("*:Munge:Geom");
 PStatCollector CullableObject::_munge_sprites_pcollector("*:Munge:Sprites");
 PStatCollector CullableObject::_munge_sprites_verts_pcollector("*:Munge:Sprites:Verts");
 PStatCollector CullableObject::_munge_sprites_prims_pcollector("*:Munge:Sprites:Prims");
+PStatCollector CullableObject::_munge_instances_pcollector("*:Munge:Instances");
 PStatCollector CullableObject::_sw_sprites_pcollector("SW Sprites");
 
 TypeHandle CullableObject::_type_handle;
@@ -173,6 +174,23 @@ munge_geom(GraphicsStateGuardianBase *gsg, GeomMunger *munger,
       std::swap(_munged_data, animated_vertices);
     }
 
+    if (sattr != nullptr) {
+      if (_instances != nullptr &&
+          sattr->get_flag(ShaderAttrib::F_hardware_instancing)) {
+        // We are under an InstancedNode, and the shader implements hardware.
+        // Munge the instance list into the vertex data.
+        munge_instances(current_thread);
+        _num_instances = _instances->size();
+        _instances = nullptr;
+      } else {
+        // No, use the instance count from the ShaderAttrib.
+        int count = sattr->get_instance_count();
+        _num_instances = (count > 0) ? (size_t)count : 1;
+      }
+    } else {
+      _num_instances = 1;
+    }
+
 #ifndef NDEBUG
     if (show_vertex_animation) {
       GeomVertexDataPipelineReader data_reader(_munged_data, current_thread);
@@ -204,6 +222,22 @@ output(std::ostream &out) const {
   }
 }
 
+/**
+ * Returns a GeomVertexData that represents the results of computing the
+ * instance arrays for this data.
+ */
+void CullableObject::
+munge_instances(Thread *current_thread) {
+  PStatTimer timer(_munge_instances_pcollector, current_thread);
+
+  PT(GeomVertexData) instanced_data = new GeomVertexData(*_munged_data);
+  const GeomVertexArrayFormat *array_format = GeomVertexArrayFormat::get_instance_array_format();
+
+  CPT(GeomVertexArrayData) new_array = _instances->get_array_data(array_format);
+  instanced_data->insert_array((size_t)-1, new_array);
+  _munged_data = instanced_data;
+}
+
 /**
  * Converts a table of points to quads for rendering on systems that don't
  * support fancy points.

+ 5 - 0
panda/src/pgraph/cullableObject.h

@@ -30,6 +30,7 @@
 #include "lightMutex.h"
 #include "callbackObject.h"
 #include "geomDrawCallbackData.h"
+#include "instanceList.h"
 
 class CullTraverser;
 class GeomMunger;
@@ -73,8 +74,11 @@ public:
   CPT(RenderState) _state;
   CPT(TransformState) _internal_transform;
   PT(CallbackObject) _draw_callback;
+  CPT(InstanceList) _instances;
+  int _num_instances = 1;
 
 private:
+  void munge_instances(Thread *current_thread);
   bool munge_points_to_quads(const CullTraverser *traverser, bool force);
 
   static CPT(RenderState) get_flash_cpu_state();
@@ -113,6 +117,7 @@ private:
   static PStatCollector _munge_sprites_pcollector;
   static PStatCollector _munge_sprites_verts_pcollector;
   static PStatCollector _munge_sprites_prims_pcollector;
+  static PStatCollector _munge_instances_pcollector;
   static PStatCollector _sw_sprites_pcollector;
 
 public:

+ 1 - 2
panda/src/pgraph/geomDrawCallbackData.cxx

@@ -45,7 +45,6 @@ upcall() {
       _gsg->clear_state_and_transform();
     }
 
-    _obj->_geom->draw(_gsg, _obj->_munged_data, _force,
-                      Thread::get_current_thread());
+    _obj->draw_inline(_gsg, _force, Thread::get_current_thread());
   }
 }

+ 11 - 0
panda/src/pgraph/geomNode.cxx

@@ -39,6 +39,7 @@
 #include "boundingSphere.h"
 #include "config_mathutil.h"
 #include "preparedGraphicsObjects.h"
+#include "instanceList.h"
 
 
 bool allow_flatten_color = ConfigVariableBool
@@ -527,6 +528,16 @@ add_for_draw(CullTraverser *trav, CullTraverserData &data) {
       continue;
     }
 
+    if (data._instances != nullptr) {
+      // Draw each individual instance.  We don't bother culling each
+      // individual Geom for each instance; that is probably way too slow.
+      CullableObject *object =
+        new CullableObject(std::move(geom), std::move(state), internal_transform);
+      object->_instances = data._instances;
+      trav->get_cull_handler()->record_object(object, trav);
+      continue;
+    }
+
     // Cull the Geom bounding volume against the view frustum andor the cull
     // planes.  Don't bother unless we've got more than one Geom, since
     // otherwise the bounding volume of the GeomNode is (probably) the same as

+ 280 - 0
panda/src/pgraph/instanceList.I

@@ -0,0 +1,280 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file instanceList.I
+ * @author rdb
+ * @date 2019-03-10
+ */
+
+/**
+ * Initializes an instance with the identity transform.
+ */
+INLINE InstanceList::Instance::
+Instance() : _transform(TransformState::make_identity()) {
+}
+
+/**
+ * Initializes an instance with the given transformation.
+ */
+INLINE InstanceList::Instance::
+Instance(CPT(TransformState) transform) : _transform(std::move(transform)) {
+}
+
+/**
+ *
+ */
+INLINE LPoint3 InstanceList::Instance::
+get_pos() const {
+  return get_transform()->get_pos();
+}
+
+/**
+ *
+ */
+INLINE void InstanceList::Instance::
+set_pos(const LPoint3 &pos) {
+  set_transform(get_transform()->set_pos(pos));
+}
+
+/**
+ *
+ */
+INLINE void InstanceList::Instance::
+set_pos(PN_stdfloat x, PN_stdfloat y, PN_stdfloat z) {
+  set_pos(LPoint3(x, y, z));
+}
+
+/**
+ *
+ */
+INLINE LVecBase3 InstanceList::Instance::
+get_hpr() const {
+  return get_transform()->get_hpr();
+}
+
+/**
+ *
+ */
+INLINE void InstanceList::Instance::
+set_hpr(const LVecBase3 &hpr) {
+  set_transform(get_transform()->set_hpr(hpr));
+}
+
+/**
+ *
+ */
+INLINE void InstanceList::Instance::
+set_hpr(PN_stdfloat h, PN_stdfloat p, PN_stdfloat r) {
+  set_hpr(LVecBase3(h, p, r));
+}
+
+/**
+ *
+ */
+INLINE LQuaternion InstanceList::Instance::
+get_quat() const {
+  return get_transform()->get_quat();
+}
+
+/**
+ *
+ */
+INLINE void InstanceList::Instance::
+set_quat(const LQuaternion &quat) {
+  set_transform(get_transform()->set_quat(quat));
+}
+
+/**
+ *
+ */
+INLINE LVecBase3 InstanceList::Instance::
+get_scale() const {
+  return get_transform()->get_scale();
+}
+
+/**
+ *
+ */
+INLINE void InstanceList::Instance::
+set_scale(const LVecBase3 &scale) {
+  set_transform(get_transform()->set_scale(scale));
+}
+
+/**
+ *
+ */
+INLINE void InstanceList::Instance::
+set_scale(PN_stdfloat sx, PN_stdfloat sy, PN_stdfloat sz) {
+  set_scale(LVecBase3(sx, sy, sz));
+}
+
+/**
+ *
+ */
+INLINE const LMatrix4 &InstanceList::Instance::
+get_mat() const {
+  return get_transform()->get_mat();
+}
+
+/**
+ *
+ */
+INLINE void InstanceList::Instance::
+set_mat(const LMatrix4 &mat) {
+  set_transform(TransformState::make_mat(mat));
+}
+
+/**
+ *
+ */
+INLINE const TransformState *InstanceList::Instance::
+get_transform() const {
+  return _transform.p();
+}
+
+/**
+ *
+ */
+INLINE void InstanceList::Instance::
+set_transform(CPT(TransformState) transform) {
+  _transform = std::move(transform);
+}
+
+/**
+ * Adds a new instance with the indicated transformation to the list.
+ */
+INLINE void InstanceList::
+append(InstanceList::Instance instance) {
+  _instances.push_back(std::move(instance));
+  _cached_array.clear();
+}
+
+/**
+ * Adds a new instance with the indicated transformation to the list.
+ */
+INLINE void InstanceList::
+append(const TransformState *transform) {
+  _instances.push_back(Instance(transform));
+  _cached_array.clear();
+}
+
+/**
+ * Adds a new instance with the indicated transformation to the list.
+ */
+INLINE void InstanceList::
+append(const LPoint3 &pos, const LVecBase3 &hpr, const LVecBase3 &scale) {
+
+  append(TransformState::make_pos_hpr_scale(pos, hpr, scale));
+}
+
+/**
+ * Adds a new instance with the indicated transformation to the list.
+ */
+INLINE void InstanceList::
+append(const LPoint3 &pos, const LQuaternion &quat, const LVecBase3 &scale) {
+
+  append(TransformState::make_pos_quat_scale(pos, quat, scale));
+}
+
+/**
+ * Returns the total number of instances in the list.
+ */
+INLINE size_t InstanceList::
+size() const {
+  return _instances.size();
+}
+
+/**
+ * Returns the nth instance in the list.
+ */
+INLINE const InstanceList::Instance &InstanceList::
+operator [] (size_t n) const {
+  return _instances[n];
+}
+
+/**
+ * Returns the nth instance in the list.
+ */
+INLINE InstanceList::Instance &InstanceList::
+operator [] (size_t n) {
+  _cached_array.clear();
+  return _instances[n];
+}
+
+/**
+ * Empties the instance list.
+ */
+INLINE void InstanceList::
+clear() {
+  _instances.clear();
+  _cached_array.clear();
+}
+
+/**
+ * Reserves space for the given number of instances.
+ */
+INLINE void InstanceList::
+reserve(size_t n) {
+  _instances.reserve(n);
+}
+
+/**
+ * Returns true if the InstanceList is empty.
+ */
+INLINE bool InstanceList::
+empty() const {
+  return _instances.empty();
+}
+
+/**
+ * Returns an iterator to the beginning of the list.
+ */
+INLINE InstanceList::iterator InstanceList::
+begin() {
+  return _instances.begin();
+}
+
+/**
+ * Returns a const_iterator to the beginning of the list.
+ */
+INLINE InstanceList::const_iterator InstanceList::
+begin() const {
+  return _instances.begin();
+}
+
+/**
+ * Returns a const_iterator to the beginning of the list.
+ */
+INLINE InstanceList::const_iterator InstanceList::
+cbegin() const {
+  return _instances.cbegin();
+}
+
+/**
+ * Returns an iterator to the end of the list.
+ */
+INLINE InstanceList::iterator InstanceList::
+end() {
+  return _instances.end();
+}
+
+/**
+ * Returns a const_iterator to the end of the list.
+ */
+INLINE InstanceList::const_iterator InstanceList::
+end() const {
+  return _instances.end();
+}
+
+/**
+ * Returns a const_iterator to the end of the list.
+ */
+INLINE InstanceList::const_iterator InstanceList::
+cend() const {
+  return _instances.cend();
+}

+ 213 - 0
panda/src/pgraph/instanceList.cxx

@@ -0,0 +1,213 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file instanceList.cxx
+ * @author rdb
+ * @date 2019-03-10
+ */
+
+#include "instanceList.h"
+#include "indent.h"
+#include "bamReader.h"
+#include "bamWriter.h"
+#include "bitArray.h"
+#include "geomVertexWriter.h"
+
+TypeHandle InstanceList::_type_handle;
+
+/**
+ * Required to implement CopyOnWriteObject.
+ */
+PT(CopyOnWriteObject) InstanceList::
+make_cow_copy() {
+  return new InstanceList(*this);
+}
+
+/**
+ *
+ */
+InstanceList::
+InstanceList() {
+}
+
+/**
+ *
+ */
+InstanceList::
+InstanceList(const InstanceList &copy) :
+  _instances(copy._instances)
+{
+}
+
+/**
+ *
+ */
+InstanceList::
+~InstanceList() {
+}
+
+/**
+ * Transforms all of the instances in the list by the indicated matrix.
+ */
+void InstanceList::
+xform(const LMatrix4 &mat) {
+
+}
+
+/**
+ * Returns an immutable copy without the bits turned on in the indicated mask.
+ */
+CPT(InstanceList) InstanceList::
+without(const BitArray &mask) const {
+  size_t num_instances = size();
+  size_t num_culled = (size_t)mask.get_num_on_bits();
+  if (num_culled == 0) {
+    return this;
+  }
+  else if (num_culled >= num_instances) {
+    static CPT(InstanceList) empty_list;
+    if (empty_list == nullptr) {
+      empty_list = new InstanceList;
+    }
+
+    nassertr(num_culled <= num_instances, empty_list);
+    return empty_list;
+  }
+
+  InstanceList *new_list = new InstanceList;
+  new_list->_instances.reserve(num_instances - num_culled);
+
+  for (size_t i = (size_t)mask.get_lowest_off_bit(); i < num_instances; ++i) {
+    if (!mask.get_bit(i)) {
+      new_list->_instances.push_back(_instances[i]);
+    }
+  }
+
+  return new_list;
+}
+
+/**
+ * Returns a GeomVertexArrayData containing the matrices.
+ */
+CPT(GeomVertexArrayData) InstanceList::
+get_array_data(const GeomVertexArrayFormat *format) const {
+  CPT(GeomVertexArrayData) array_data = _cached_array;
+  if (array_data != nullptr) {
+    if (array_data->get_array_format() == format) {
+      return array_data;
+    }
+  }
+
+  nassertr(format != nullptr, nullptr);
+
+  size_t num_instances = size();
+  PT(GeomVertexArrayData) new_array = new GeomVertexArrayData(format, GeomEnums::UH_stream);
+  new_array->unclean_set_num_rows(num_instances);
+
+  {
+    GeomVertexWriter writer(new_array, Thread::get_current_thread());
+    writer.set_column(InternalName::get_instance_matrix());
+    for (size_t i = 0; i < num_instances; ++i) {
+      writer.set_matrix4(_instances[i].get_mat());
+    }
+  }
+
+  _cached_array = new_array;
+  return new_array;
+}
+
+/**
+ *
+ */
+void InstanceList::
+output(std::ostream &out) const {
+  out << "InstanceList[" << size() << "]";
+}
+
+/**
+ *
+ */
+void InstanceList::
+write(std::ostream &out, int indent_level) const {
+  indent(out, indent_level) << "InstanceList[" << size() << "]:\n";
+  for (const Instance &instance : *this) {
+    indent(out, indent_level + 2) << *instance.get_transform() << "\n";
+  }
+}
+
+/**
+ * Tells the BamReader how to create objects of type InstanceList.
+ */
+void InstanceList::
+register_with_read_factory() {
+  BamReader::get_factory()->register_factory(get_class_type(), make_from_bam);
+}
+
+/**
+ * Writes the contents of this object to the datagram for shipping out to a
+ * Bam file.
+ */
+void InstanceList::
+write_datagram(BamWriter *manager, Datagram &dg) {
+  CopyOnWriteObject::write_datagram(manager, dg);
+
+  for (const Instance &instance : *(const InstanceList *)this) {
+    manager->write_pointer(dg, instance.get_transform());
+  }
+}
+
+/**
+ * Receives an array of pointers, one for each time manager->read_pointer()
+ * was called in fillin(). Returns the number of pointers processed.
+ */
+int InstanceList::
+complete_pointers(TypedWritable **p_list, BamReader *manager) {
+  int pi = CopyOnWriteObject::complete_pointers(p_list, manager);
+
+  for (Instance &instance : *this) {
+    instance = Instance(DCAST(TransformState, p_list[pi++]));
+  }
+
+  return pi;
+}
+
+/**
+ * This function is called by the BamReader's factory when a new object of
+ * type InstanceList is encountered in the Bam file.  It should create
+ * the InstanceList and extract its information from the file.
+ */
+TypedWritable *InstanceList::
+make_from_bam(const FactoryParams &params) {
+  InstanceList *object = new InstanceList;
+  DatagramIterator scan;
+  BamReader *manager;
+
+  parse_params(params, scan, manager);
+  object->fillin(scan, manager);
+
+  return object;
+}
+
+/**
+ * This internal function is called by make_from_bam to read in all of the
+ * relevant data from the BamFile for the new InstanceList.
+ */
+void InstanceList::
+fillin(DatagramIterator &scan, BamReader *manager) {
+  CopyOnWriteObject::fillin(scan, manager);
+
+  size_t num_instances = scan.get_uint16();
+  _instances.clear();
+  _instances.resize(num_instances);
+
+  for (size_t i = 0; i < num_instances; ++i) {
+    manager->read_pointer(scan);
+  }
+
+  _cached_array.clear();
+}

+ 159 - 0
panda/src/pgraph/instanceList.h

@@ -0,0 +1,159 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file instanceList.h
+ * @author rdb
+ * @date 2019-03-10
+ */
+
+#ifndef INSTANCELIST_H
+#define INSTANCELIST_H
+
+#include "pandabase.h"
+#include "copyOnWriteObject.h"
+#include "transformState.h"
+#include "pvector.h"
+#include "geomVertexArrayData.h"
+
+class BitArray;
+class FactoryParams;
+
+/**
+ * This structure stores a list of per-instance data, used by InstancedNode.
+ *
+ * @since 1.11.0
+ */
+class EXPCL_PANDA_PGRAPH InstanceList : public CopyOnWriteObject {
+protected:
+  virtual PT(CopyOnWriteObject) make_cow_copy() override;
+
+PUBLISHED:
+  InstanceList();
+  InstanceList(const InstanceList &copy);
+  virtual ~InstanceList();
+  ALLOC_DELETED_CHAIN(InstanceList);
+
+  /**
+   * An individual instance in an InstanceList.
+   *
+   * @since 1.11.0
+   */
+  class EXPCL_PANDA_PGRAPH Instance {
+  public:
+    INLINE explicit Instance();
+    INLINE explicit Instance(CPT(TransformState) transform);
+
+  PUBLISHED:
+    INLINE LPoint3 get_pos() const;
+    INLINE void set_pos(const LPoint3 &);
+    INLINE void set_pos(PN_stdfloat x, PN_stdfloat y, PN_stdfloat z);
+
+    INLINE LVecBase3 get_hpr() const;
+    INLINE void set_hpr(const LVecBase3 &);
+    INLINE void set_hpr(PN_stdfloat h, PN_stdfloat p, PN_stdfloat r);
+
+    INLINE LQuaternion get_quat() const;
+    INLINE void set_quat(const LQuaternion &);
+
+    INLINE LVecBase3 get_scale() const;
+    INLINE void set_scale(const LVecBase3 &);
+    INLINE void set_scale(PN_stdfloat sx, PN_stdfloat sy, PN_stdfloat sz);
+
+    INLINE const LMatrix4 &get_mat() const;
+    INLINE void set_mat(const LMatrix4 &mat);
+
+    INLINE const TransformState *get_transform() const;
+    INLINE void set_transform(CPT(TransformState));
+    MAKE_PROPERTY(transform, get_transform);
+
+  private:
+    CPT(TransformState) _transform;
+  };
+
+  void append(Instance instance);
+  void append(const TransformState *transform);
+  void append(const LPoint3 &pos,
+              const LVecBase3 &hpr = LVecBase3(0),
+              const LVecBase3 &scale = LVecBase3(1));
+  void append(const LPoint3 &pos,
+              const LQuaternion &quat,
+              const LVecBase3 &scale = LVecBase3(1));
+
+  INLINE size_t size() const;
+  INLINE const Instance &operator [] (size_t n) const;
+  INLINE Instance &operator [] (size_t n);
+  INLINE void clear();
+  INLINE void reserve(size_t);
+
+  void xform(const LMatrix4 &mat);
+
+public:
+  typedef pvector<Instance> Instances;
+  typedef Instances::iterator iterator;
+  typedef Instances::const_iterator const_iterator;
+
+  INLINE bool empty() const;
+
+  INLINE iterator begin();
+  INLINE const_iterator begin() const;
+  INLINE const_iterator cbegin() const;
+
+  INLINE iterator end();
+  INLINE const_iterator end() const;
+  INLINE const_iterator cend() const;
+
+  CPT(InstanceList) without(const BitArray &mask) const;
+
+  CPT(GeomVertexArrayData) get_array_data(const GeomVertexArrayFormat *format) const;
+
+  virtual void output(std::ostream &out) const;
+  virtual void write(std::ostream &out, int indent_level) const;
+
+private:
+  Instances _instances;
+
+  mutable CPT(GeomVertexArrayData) _cached_array;
+
+public:
+  static void register_with_read_factory();
+  virtual void write_datagram(BamWriter *manager, Datagram &dg) override;
+  virtual int complete_pointers(TypedWritable **plist, BamReader *manager) override;
+
+protected:
+  static TypedWritable *make_from_bam(const FactoryParams &params);
+  void fillin(DatagramIterator &scan, BamReader *manager) override;
+
+public:
+  static TypeHandle get_class_type() {
+    return _type_handle;
+  }
+  static void init_type() {
+    CopyOnWriteObject::init_type();
+    register_type(_type_handle, "InstanceList",
+                  CopyOnWriteObject::get_class_type());
+  }
+  virtual TypeHandle get_type() const override {
+    return get_class_type();
+  }
+  virtual TypeHandle force_init_type() override {
+    init_type();
+    return get_class_type();
+  }
+
+private:
+  static TypeHandle _type_handle;
+};
+
+inline std::ostream &operator <<(std::ostream &out, const InstanceList &list) {
+  list.output(out);
+  return out;
+}
+
+#include "instanceList.I"
+
+#endif

+ 39 - 0
panda/src/pgraph/instancedNode.I

@@ -0,0 +1,39 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file instancedNode.I
+ * @author rdb
+ * @date 2019-03-10
+ */
+
+/**
+ * Returns the number of instances.
+ */
+INLINE size_t InstancedNode::
+get_num_instances() const {
+  Thread *current_thread = Thread::get_current_thread();
+  CDReader cdata(_cycler, current_thread);
+  nassertr_always(cdata->_instances != nullptr, 0);
+  return cdata->_instances.get_read_pointer(current_thread)->size();
+}
+
+/**
+ * Returns the list of instances.
+ */
+INLINE CPT(InstanceList) InstancedNode::
+get_instances(Thread *current_thread) const {
+  CDReader cdata(_cycler, current_thread);
+  return cdata->_instances.get_read_pointer(current_thread);
+}
+
+/**
+ *
+ */
+INLINE InstancedNode::CData::
+CData() : _instances(new InstanceList) {
+}

+ 492 - 0
panda/src/pgraph/instancedNode.cxx

@@ -0,0 +1,492 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file instancedNode.cxx
+ * @author rdb
+ * @date 2019-03-10
+ */
+
+#include "instancedNode.h"
+#include "boundingBox.h"
+#include "boundingSphere.h"
+#include "cullTraverserData.h"
+#include "cullPlanes.h"
+
+TypeHandle InstancedNode::_type_handle;
+
+/**
+ *
+ */
+InstancedNode::
+InstancedNode(const std::string &name) :
+  PandaNode(name)
+{
+  set_cull_callback();
+}
+
+/**
+ *
+ */
+InstancedNode::
+InstancedNode(const InstancedNode &copy) :
+  PandaNode(copy),
+  _cycler(copy._cycler)
+{
+  set_cull_callback();
+}
+
+/**
+ *
+ */
+InstancedNode::
+~InstancedNode() {
+}
+
+/**
+ * Returns a newly-allocated PandaNode that is a shallow copy of this one.  It
+ * will be a different pointer, but its internal data may or may not be shared
+ * with that of the original PandaNode.  No children will be copied.
+ */
+PandaNode *InstancedNode::
+make_copy() const {
+  return new InstancedNode(*this);
+}
+
+/**
+ * Returns the list of instances.
+ *
+ * Don't call this in a downstream thread unless you don't mind it blowing
+ * away other changes you might have recently made in an upstream thread.
+ */
+PT(InstanceList) InstancedNode::
+modify_instances() {
+  Thread *current_thread = Thread::get_current_thread();
+  CDWriter cdata(_cycler, true, current_thread);
+  PT(InstanceList) instances = cdata->_instances.get_write_pointer();
+  mark_bounds_stale(current_thread->get_pipeline_stage(), current_thread);
+  mark_bam_modified();
+  return instances;
+}
+
+/**
+ * Entirely replaces the list of instances with the given list.
+ *
+ * Don't call this in a downstream thread unless you don't mind it blowing
+ * away other changes you might have recently made in an upstream thread.
+ */
+void InstancedNode::
+set_instances(PT(InstanceList) instances) {
+  Thread *current_thread = Thread::get_current_thread();
+  CDWriter cdata(_cycler, true);
+  cdata->_instances = std::move(instances);
+  mark_bounds_stale(current_thread->get_pipeline_stage(), current_thread);
+  mark_bam_modified();
+}
+
+/**
+ * Returns true if it is generally safe to flatten out this particular kind of
+ * PandaNode by duplicating instances (by calling dupe_for_flatten()), false
+ * otherwise (for instance, a Camera cannot be safely flattened, because the
+ * Camera pointer itself is meaningful).
+ */
+bool InstancedNode::
+safe_to_flatten() const {
+  return false;
+}
+
+/**
+ * Returns true if it is generally safe to combine this particular kind of
+ * PandaNode with other kinds of PandaNodes of compatible type, adding
+ * children or whatever.  For instance, an LODNode should not be combined with
+ * any other PandaNode, because its set of children is meaningful.
+ */
+bool InstancedNode::
+safe_to_combine() const {
+  // This can happen iff the instance list is identical; see combine_with().
+  return true;
+}
+
+/**
+ * Transforms the contents of this node by the indicated matrix, if it means
+ * anything to do so.  For most kinds of nodes, this does nothing.
+ */
+void InstancedNode::
+xform(const LMatrix4 &mat) {
+}
+
+/**
+ * Collapses this node with the other node, if possible, and returns a pointer
+ * to the combined node, or NULL if the two nodes cannot safely be combined.
+ *
+ * The return value may be this, other, or a new node altogether.
+ *
+ * This function is called from GraphReducer::flatten(), and need not deal
+ * with children; its job is just to decide whether to collapse the two nodes
+ * and what the collapsed node should look like.
+ */
+PandaNode *InstancedNode::
+combine_with(PandaNode *other) {
+  if (is_exact_type(get_class_type()) && other->is_exact_type(get_class_type())) {
+    InstancedNode *iother = DCAST(InstancedNode, other);
+
+    // Only combine them if the instance lists for both are identical.
+    Thread *current_thread = Thread::get_current_thread();
+    CDReader this_cdata(_cycler, current_thread);
+    CDReader other_cdata(iother->_cycler, current_thread);
+    CPT(InstanceList) this_instances = this_cdata->_instances.get_read_pointer(current_thread);
+    CPT(InstanceList) other_instances = other_cdata->_instances.get_read_pointer(current_thread);
+    if (this_instances == other_instances) {
+      return this;
+    }
+  }
+
+  return nullptr;
+}
+
+/**
+ * This is used to support NodePath::calc_tight_bounds().  It is not intended
+ * to be called directly, and it has nothing to do with the normal Panda
+ * bounding-volume computation.
+ *
+ * If the node contains any geometry, this updates min_point and max_point to
+ * enclose its bounding box.  found_any is to be set true if the node has any
+ * geometry at all, or left alone if it has none.  This method may be called
+ * over several nodes, so it may enter with min_point, max_point, and
+ * found_any already set.
+ */
+CPT(TransformState) InstancedNode::
+calc_tight_bounds(LPoint3 &min_point, LPoint3 &max_point, bool &found_any,
+                  const TransformState *transform, Thread *current_thread) const {
+
+  CPT(InstanceList) instances = get_instances(current_thread);
+  CPT(TransformState) next_transform = transform->compose(get_transform(current_thread));
+
+  for (size_t ii = 0; ii < instances->size(); ++ii) {
+    CPT(TransformState) instance_transform = next_transform->compose((*instances)[ii].get_transform());
+
+    Children cr = get_children(current_thread);
+    size_t num_children = cr.get_num_children();
+    for (size_t ci = 0; ci < num_children; ++ci) {
+      cr.get_child(ci)->calc_tight_bounds(min_point, max_point,
+                                          found_any, instance_transform,
+                                          current_thread);
+    }
+  }
+
+  return next_transform;
+}
+
+/**
+ * This function will be called during the cull traversal to perform any
+ * additional operations that should be performed at cull time.  This may
+ * include additional manipulation of render state or additional
+ * visible/invisible decisions, or any other arbitrary operation.
+ *
+ * Note that this function will *not* be called unless set_cull_callback() is
+ * called in the constructor of the derived class.  It is necessary to call
+ * set_cull_callback() to indicated that we require cull_callback() to be
+ * called.
+ *
+ * By the time this function is called, the node has already passed the
+ * bounding-volume test for the viewing frustum, and the node's transform and
+ * state have already been applied to the indicated CullTraverserData object.
+ *
+ * The return value is true if this node should be visible, or false if it
+ * should be culled.
+ */
+bool InstancedNode::
+cull_callback(CullTraverser *trav, CullTraverserData &data) {
+  Thread *current_thread = trav->get_current_thread();
+
+  CPT(InstanceList) instances = get_instances(current_thread);
+
+  if (data._instances != nullptr) {
+    // We are already under an instanced node.  Create a new combined list.
+    InstanceList *new_list = new InstanceList();
+    new_list->reserve(data._instances->size() * instances->size());
+    for (const InstanceList::Instance &parent_instance : *data._instances) {
+      for (const InstanceList::Instance &this_instance : *instances) {
+        new_list->append(parent_instance.get_transform()->compose(this_instance.get_transform()));
+      }
+    }
+    instances = new_list;
+  }
+
+  if (data._view_frustum != nullptr || !data._cull_planes->is_empty()) {
+    // Culling is on, so we need to figure out which instances are visible.
+    Children children = data.node_reader()->get_children();
+    data.node_reader()->release();
+
+    // Keep track of which instances should be culled away.
+    BitArray culled_instances;
+    culled_instances.set_range(0, instances->size());
+
+    for (size_t ii = 0; ii < instances->size(); ++ii) {
+      CullTraverserData instance_data(data);
+      instance_data.apply_transform((*instances)[ii].get_transform());
+
+      for (size_t ci = 0; ci < children.size(); ++ci) {
+        CullTraverserData child_data(instance_data, children.get_child(ci));
+        if (child_data.is_in_view(trav->get_camera_mask())) {
+          // Yep, the instance is in view.
+          culled_instances.clear_bit(ii);
+          break;
+        }
+      }
+    }
+
+    instances = instances->without(culled_instances);
+  } else {
+    data.node_reader()->release();
+  }
+
+  if (instances->empty()) {
+    // There are no instances, or they are all culled away.
+    return false;
+  }
+
+  data._instances = std::move(instances);
+
+  // Disable culling from this point on, for now.  It's probably not worth it
+  // to keep lists of transformed bounding volumes for each instance.
+  data._view_frustum = nullptr;
+  data._cull_planes = CullPlanes::make_empty();
+
+  return true;
+
+  /*
+  for (const InstanceList::Instance &instance : *instances) {
+    CullTraverserData instance_data(data);
+    instance_data.apply_transform(instance.get_transform());
+    trav->traverse_below(instance_data);
+  }
+  return false;
+  */
+}
+
+/**
+ *
+ */
+void InstancedNode::
+output(std::ostream &out) const {
+  PandaNode::output(out);
+  out << " (" << get_num_instances() << " instances)";
+}
+
+/**
+ * Returns a BoundingVolume that represents the external contents of the node.
+ * This should encompass the internal bounds, but also the bounding volumes of
+ * of all this node's children, which are passed in.
+ */
+void InstancedNode::
+compute_external_bounds(CPT(BoundingVolume) &external_bounds,
+                        BoundingVolume::BoundsType btype,
+                        const BoundingVolume **volumes, size_t num_volumes,
+                        int pipeline_stage, Thread *current_thread) const {
+
+  CPT(InstanceList) instances = get_instances(current_thread);
+
+  PT(GeometricBoundingVolume) gbv;
+  if (btype == BoundingVolume::BT_sphere) {
+    gbv = new BoundingSphere;
+  } else {
+    gbv = new BoundingBox;
+  }
+
+  if (num_volumes == 0 || instances->empty()) {
+    external_bounds = gbv;
+    return;
+  }
+
+  // Compute a sphere at the origin, encompassing the children.  This may not
+  // be the most optimal shape, but it allows us to easily estimate a bounding
+  // volume without having to take each instance transform into account.
+  PN_stdfloat max_radius = 0;
+  LVector3 max_abs_box(0);
+
+  for (size_t i = 0; i < num_volumes; ++i) {
+    const BoundingVolume *child_volume = volumes[i];
+    if (child_volume->is_empty()) {
+      continue;
+    }
+    if (child_volume->is_infinite()) {
+      gbv->set_infinite();
+      break;
+    }
+    if (const BoundingSphere *child_sphere = child_volume->as_bounding_sphere()) {
+      max_radius = child_sphere->get_center().length() + child_sphere->get_radius();
+    }
+    else if (const FiniteBoundingVolume *child_finite = child_volume->as_finite_bounding_volume()) {
+      LPoint3 min1 = child_finite->get_min();
+      LPoint3 max1 = child_finite->get_max();
+      max_abs_box.set(
+        std::max(max_abs_box[0], std::max(std::fabs(min1[0]), std::fabs(max1[0]))),
+        std::max(max_abs_box[1], std::max(std::fabs(min1[1]), std::fabs(max1[1]))),
+        std::max(max_abs_box[2], std::max(std::fabs(min1[2]), std::fabs(max1[2]))));
+    }
+    else {
+      gbv->set_infinite();
+      break;
+    }
+  }
+
+  max_radius = std::max(max_radius, max_abs_box.length());
+  if (max_radius == 0 || gbv->is_infinite()) {
+    external_bounds = gbv;
+    return;
+  }
+
+  // Now that we have a sphere encompassing the children, we will make a box
+  // surrounding all the instances, extended by the computed radius.
+  LPoint3 min_point = (*instances)[0].get_pos();
+  LPoint3 max_point(min_point);
+
+  for (const InstanceList::Instance &instance : *instances) {
+    // To make the math easier and not have to take rotations into account, we
+    // take the highest scale component and multiply it by the radius of the
+    // bounding sphere on the origin we just calculated.
+    LVecBase3 scale = instance.get_scale();
+    PN_stdfloat max_scale = std::max(std::fabs(scale[0]), std::max(std::fabs(scale[1]), std::fabs(scale[2])));
+    PN_stdfloat inst_radius = max_scale * max_radius;
+    LVector3 extends_by(inst_radius);
+    LPoint3 pos = instance.get_pos();
+    min_point = min_point.fmin(pos - extends_by);
+    max_point = max_point.fmax(pos + extends_by);
+  }
+
+  if (min_point == max_point) {
+    external_bounds = gbv;
+    return;
+  }
+
+  // If we really need to make a sphere, we use the center of the bounding box
+  // as our sphere center, and iterate again to find the furthest instance.
+  if (btype == BoundingVolume::BT_sphere) {
+    LPoint3 center = (min_point + max_point) * 0.5;
+
+    PN_stdfloat max_distance = 0;
+    for (const InstanceList::Instance &instance : *instances) {
+      LVecBase3 scale = instance.get_scale();
+      PN_stdfloat max_scale = std::max(std::fabs(scale[0]), std::max(std::fabs(scale[1]), std::fabs(scale[2])));
+      PN_stdfloat inst_radius = max_scale * max_radius;
+      PN_stdfloat distance = (instance.get_pos() - center).length() + inst_radius;
+      max_distance = std::max(max_distance, distance);
+    }
+
+    if (max_distance == 0) {
+      external_bounds = gbv;
+      return;
+    }
+    ((BoundingSphere *)gbv.p())->set_center(center);
+    ((BoundingSphere *)gbv.p())->set_radius(max_distance);
+  } else {
+    ((BoundingBox *)gbv.p())->set_min_max(min_point, max_point);
+  }
+
+  // If we have a transform, apply it to the bounding volume we just
+  // computed.
+  CPT(TransformState) transform = get_transform(current_thread);
+  if (!transform->is_identity()) {
+    gbv->xform(transform->get_mat());
+  }
+
+  external_bounds = gbv;
+}
+
+/**
+ * Tells the BamReader how to create objects of type GeomNode.
+ */
+void InstancedNode::
+register_with_read_factory() {
+  BamReader::get_factory()->register_factory(get_class_type(), make_from_bam);
+}
+
+/**
+ * Writes the contents of this object to the datagram for shipping out to a
+ * Bam file.
+ */
+void InstancedNode::
+write_datagram(BamWriter *manager, Datagram &dg) {
+  PandaNode::write_datagram(manager, dg);
+  manager->write_cdata(dg, _cycler);
+}
+
+/**
+ * This function is called by the BamReader's factory when a new object of
+ * type InstancedNode is encountered in the Bam file.  It should create the
+ * InstancedNode and extract its information from the file.
+ */
+TypedWritable *InstancedNode::
+make_from_bam(const FactoryParams &params) {
+  InstancedNode *node = new InstancedNode("");
+  DatagramIterator scan;
+  BamReader *manager;
+
+  parse_params(params, scan, manager);
+  node->fillin(scan, manager);
+
+  return node;
+}
+
+/**
+ * This internal function is called by make_from_bam to read in all of the
+ * relevant data from the BamFile for the new InstancedNode.
+ */
+void InstancedNode::
+fillin(DatagramIterator &scan, BamReader *manager) {
+  PandaNode::fillin(scan, manager);
+  manager->read_cdata(scan, _cycler);
+}
+
+/**
+ *
+ */
+InstancedNode::CData::
+CData(const InstancedNode::CData &copy) :
+  _instances(copy._instances)
+{
+}
+
+/**
+ *
+ */
+CycleData *InstancedNode::CData::
+make_copy() const {
+  return new CData(*this);
+}
+
+/**
+ * Writes the contents of this object to the datagram for shipping out to a
+ * Bam file.
+ */
+void InstancedNode::CData::
+write_datagram(BamWriter *manager, Datagram &dg) const {
+  CPT(InstanceList) instances = _instances.get_read_pointer();
+  manager->write_pointer(dg, instances.p());
+}
+
+/**
+ * Receives an array of pointers, one for each time manager->read_pointer()
+ * was called in fillin(). Returns the number of pointers processed.
+ */
+int InstancedNode::CData::
+complete_pointers(TypedWritable **p_list, BamReader *manager) {
+  int pi = CycleData::complete_pointers(p_list, manager);
+
+  _instances = DCAST(InstanceList, p_list[pi++]);
+  return pi;
+}
+
+/**
+ * This internal function is called by make_from_bam to read in all of the
+ * relevant data from the BamFile for the new GeomNode.
+ */
+void InstancedNode::CData::
+fillin(DatagramIterator &scan, BamReader *manager) {
+  manager->read_pointer(scan);
+}

+ 136 - 0
panda/src/pgraph/instancedNode.h

@@ -0,0 +1,136 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file instancedNode.h
+ * @author rdb
+ * @date 2019-03-09
+ */
+
+#ifndef INSTANCEDNODE_H
+#define INSTANCEDNODE_H
+
+#include "pandabase.h"
+#include "pandaNode.h"
+#include "copyOnWritePointer.h"
+#include "instanceList.h"
+
+/**
+ * This is a special node that instances its contents using a list of
+ * transforms that get applied on top of the node's own transform.  This is a
+ * bit more limited than the regular instance_to mechanism, but it is a better
+ * choice for hardware instancing.
+ *
+ * For best performance, it is highly recommended to flatten the nodes under
+ * this (by calling flatten_strong()), since culling will not be performed for
+ * individual sub-nodes under each instance.
+ *
+ * @since 1.11.0
+ */
+class EXPCL_PANDA_PGRAPH InstancedNode : public PandaNode {
+PUBLISHED:
+  explicit InstancedNode(const std::string &name);
+
+protected:
+  InstancedNode(const InstancedNode &copy);
+
+public:
+  virtual ~InstancedNode();
+  virtual PandaNode *make_copy() const override;
+
+  INLINE size_t get_num_instances() const;
+  INLINE CPT(InstanceList) get_instances(Thread *current_thread = Thread::get_current_thread()) const;
+  PT(InstanceList) modify_instances();
+  void set_instances(PT(InstanceList) instances);
+
+PUBLISHED:
+  MAKE_PROPERTY(instances, modify_instances, set_instances);
+
+public:
+  virtual bool safe_to_flatten() const override;
+  virtual bool safe_to_combine() const override;
+  virtual void xform(const LMatrix4 &mat) override;
+  virtual PandaNode *combine_with(PandaNode *other) override;
+
+  virtual CPT(TransformState)
+    calc_tight_bounds(LPoint3 &min_point, LPoint3 &max_point,
+                      bool &found_any,
+                      const TransformState *transform,
+                      Thread *current_thread) const override;
+
+  virtual bool cull_callback(CullTraverser *trav, CullTraverserData &data) override;
+
+  virtual void output(std::ostream &out) const override;
+
+protected:
+  virtual void compute_external_bounds(CPT(BoundingVolume) &external_bounds,
+                                       BoundingVolume::BoundsType btype,
+                                       const BoundingVolume **volumes,
+                                       size_t num_volumes,
+                                       int pipeline_stage,
+                                       Thread *current_thread) const override;
+
+private:
+  // This is the data that must be cycled between pipeline stages.
+  class EXPCL_PANDA_PGRAPH CData final : public CycleData {
+  public:
+    INLINE CData();
+    CData(const CData &copy);
+    virtual CycleData *make_copy() const override;
+    virtual void write_datagram(BamWriter *manager, Datagram &dg) const override;
+    virtual int complete_pointers(TypedWritable **plist, BamReader *manager) override;
+    virtual void fillin(DatagramIterator &scan, BamReader *manager) override;
+    virtual TypeHandle get_parent_type() const override {
+      return InstancedNode::get_class_type();
+    }
+
+  private:
+    COWPT(InstanceList) _instances;
+
+    friend class InstancedNode;
+  };
+
+  PipelineCycler<CData> _cycler;
+  typedef CycleDataReader<CData> CDReader;
+  typedef CycleDataWriter<CData> CDWriter;
+  typedef CycleDataStageReader<CData> CDStageReader;
+  typedef CycleDataLockedStageReader<CData> CDLockedStageReader;
+  typedef CycleDataStageWriter<CData> CDStageWriter;
+
+public:
+  static void register_with_read_factory();
+  virtual void write_datagram(BamWriter *manager, Datagram &dg) override;
+
+protected:
+  static TypedWritable *make_from_bam(const FactoryParams &params);
+  void fillin(DatagramIterator &scan, BamReader *manager) override;
+
+public:
+  static TypeHandle get_class_type() {
+    return _type_handle;
+  }
+  static void init_type() {
+    PandaNode::init_type();
+    register_type(_type_handle, "InstancedNode",
+                  PandaNode::get_class_type());
+    CData::init_type();
+  }
+  virtual TypeHandle get_type() const override {
+    return get_class_type();
+  }
+  virtual TypeHandle force_init_type() override {
+    init_type();
+    return get_class_type();
+  }
+
+private:
+  static TypeHandle _type_handle;
+};
+
+#include "instancedNode.I"
+
+#endif

+ 2 - 0
panda/src/pgraph/p3pgraph_composite2.cxx

@@ -20,3 +20,5 @@
 #include "geomDrawCallbackData.cxx"
 #include "geomNode.cxx"
 #include "geomTransformer.cxx"
+#include "instanceList.cxx"
+#include "instancedNode.cxx"

+ 57 - 35
panda/src/pgraph/pandaNode.cxx

@@ -2315,6 +2315,59 @@ compute_internal_bounds(CPT(BoundingVolume) &internal_bounds,
   internal_vertices = 0;
 }
 
+/**
+ * Returns a BoundingVolume that represents the external contents of the node.
+ * This should encompass the internal bounds, but also the bounding volumes of
+ * of all this node's children, which are passed in.
+ */
+void PandaNode::
+compute_external_bounds(CPT(BoundingVolume) &external_bounds,
+                        BoundingVolume::BoundsType btype,
+                        const BoundingVolume **volumes, size_t num_volumes,
+                        int pipeline_stage, Thread *current_thread) const {
+
+  CPT(TransformState) transform = get_transform(current_thread);
+  PT(GeometricBoundingVolume) gbv;
+
+  if (btype == BoundingVolume::BT_box) {
+    gbv = new BoundingBox;
+  }
+  else if (btype == BoundingVolume::BT_sphere || !transform->is_identity()) {
+    gbv = new BoundingSphere;
+  }
+  else {
+    // If all of the child volumes are a BoundingBox, and we have no
+    // transform, then our volume is also a BoundingBox.
+    bool all_box = true;
+
+    for (size_t i = 0; i < num_volumes; ++i) {
+      if (volumes[i]->as_bounding_box() == nullptr) {
+        all_box = false;
+      }
+    }
+
+    if (all_box) {
+      gbv = new BoundingBox;
+    } else {
+      gbv = new BoundingSphere;
+    }
+  }
+
+  if (num_volumes > 0) {
+    const BoundingVolume **child_begin = &volumes[0];
+    const BoundingVolume **child_end = child_begin + num_volumes;
+    ((BoundingVolume *)gbv)->around(child_begin, child_end);
+
+    // If we have a transform, apply it to the bounding volume we just
+    // computed.
+    if (!transform->is_identity()) {
+      gbv->xform(transform->get_mat());
+    }
+  }
+
+  external_bounds = gbv;
+}
+
 /**
  * Called after a scene graph update that either adds or remove parents from
  * this node, this just provides a hook for derived PandaNode objects that
@@ -3263,7 +3316,6 @@ update_cached(bool update_bounds, int pipeline_stage, PandaNode::CDLockedStageRe
 #endif
     int child_volumes_i = 0;
 
-    bool all_box = true;
     CPT(BoundingVolume) internal_bounds = nullptr;
 
     if (update_bounds) {
@@ -3276,9 +3328,6 @@ update_cached(bool update_bounds, int pipeline_stage, PandaNode::CDLockedStageRe
 #endif
         nassertr(child_volumes_i < num_children + 1, CDStageWriter(_cycler, pipeline_stage, cdata));
         child_volumes[child_volumes_i++] = internal_bounds;
-        if (internal_bounds->as_bounding_box() == nullptr) {
-          all_box = false;
-        }
       }
     }
 
@@ -3374,9 +3423,6 @@ update_cached(bool update_bounds, int pipeline_stage, PandaNode::CDLockedStageRe
 #endif
             nassertr(child_volumes_i < num_children + 1, CDStageWriter(_cycler, pipeline_stage, cdata));
             child_volumes[child_volumes_i++] = child_cdataw->_external_bounds;
-            if (child_cdataw->_external_bounds->as_bounding_box() == nullptr) {
-              all_box = false;
-            }
           }
           num_vertices += child_cdataw->_nested_vertices;
         }
@@ -3429,9 +3475,6 @@ update_cached(bool update_bounds, int pipeline_stage, PandaNode::CDLockedStageRe
 #endif
             nassertr(child_volumes_i < num_children + 1, CDStageWriter(_cycler, pipeline_stage, cdata));
             child_volumes[child_volumes_i++] = child_cdata->_external_bounds;
-            if (child_cdata->_external_bounds->as_bounding_box() == nullptr) {
-              all_box = false;
-            }
           }
           num_vertices += child_cdata->_nested_vertices;
         }
@@ -3485,38 +3528,17 @@ update_cached(bool update_bounds, int pipeline_stage, PandaNode::CDLockedStageRe
         if (update_bounds) {
           cdataw->_nested_vertices = num_vertices;
 
-          CPT(TransformState) transform = get_transform(current_thread);
-          PT(GeometricBoundingVolume) gbv;
-
           BoundingVolume::BoundsType btype = cdataw->_bounds_type;
           if (btype == BoundingVolume::BT_default) {
             btype = bounds_type;
           }
 
-          if (btype == BoundingVolume::BT_box ||
-              (btype != BoundingVolume::BT_sphere && all_box && transform->is_identity())) {
-            // If all of the child volumes are a BoundingBox, and we have no
-            // transform, then our volume is also a BoundingBox.
+          compute_external_bounds(cdataw->_external_bounds, btype,
+                                  child_volumes, child_volumes_i,
+                                  pipeline_stage, current_thread);
 
-            gbv = new BoundingBox;
-          } else {
-            // Otherwise, it's a sphere.
-            gbv = new BoundingSphere;
-          }
-
-          if (child_volumes_i > 0) {
-            const BoundingVolume **child_begin = &child_volumes[0];
-            const BoundingVolume **child_end = child_begin + child_volumes_i;
-            ((BoundingVolume *)gbv)->around(child_begin, child_end);
-
-            // If we have a transform, apply it to the bounding volume we just
-            // computed.
-            if (!transform->is_identity()) {
-              gbv->xform(transform->get_mat());
-            }
-          }
+          nassertr(cdataw->_external_bounds != nullptr, cdataw);
 
-          cdataw->_external_bounds = gbv;
           cdataw->_last_bounds_update = next_update;
         }
 

+ 6 - 0
panda/src/pgraph/pandaNode.h

@@ -351,6 +351,12 @@ protected:
                                        int &internal_vertices,
                                        int pipeline_stage,
                                        Thread *current_thread) const;
+  virtual void compute_external_bounds(CPT(BoundingVolume) &external_bounds,
+                                       BoundingVolume::BoundsType btype,
+                                       const BoundingVolume **volumes,
+                                       size_t num_volumes,
+                                       int pipeline_stage,
+                                       Thread *current_thread) const;
   virtual void parents_changed();
   virtual void children_changed();
   virtual void transform_changed();

+ 1 - 1
panda/src/pgraph/shaderAttrib.I

@@ -82,7 +82,7 @@ get_shader_priority() const {
 
 /**
  * Returns the number of geometry instances.  A value of 0 means not to use
- * instancing at all.
+ * instancing at all.  This value is ignored if F_hardware_instancing is set.
  */
 INLINE int ShaderAttrib::
 get_instance_count() const {

+ 2 - 0
panda/src/pgraph/shaderAttrib.cxx

@@ -249,6 +249,8 @@ set_shader_inputs(const pvector<ShaderInput> &inputs) const {
  * Sets the geometry instance count.  Do not confuse this with instanceTo,
  * which is used for animation instancing, and has nothing to do with this.  A
  * value of 0 means not to use instancing at all.
+ *
+ * This value should not be set if F_hardware_instancing is also set.
  */
 CPT(RenderAttrib) ShaderAttrib::
 set_instance_count(int instance_count) const {

+ 1 - 0
panda/src/pgraph/shaderAttrib.h

@@ -51,6 +51,7 @@ PUBLISHED:
     F_subsume_alpha_test  = 1 << 1,  // Shader promises to subsume the alpha test using TEXKILL
     F_hardware_skinning   = 1 << 2,  // Shader needs pre-animated vertices
     F_shader_point_size   = 1 << 3,  // Shader provides point size, not RenderModeAttrib
+    F_hardware_instancing = 1 << 4,  // Shader needs instance list
   };
 
   INLINE bool               has_shader() const;

+ 3 - 1
panda/src/pgraphnodes/shaderGenerator.cxx

@@ -1014,7 +1014,9 @@ synthesize_shader(const RenderState *rs, const GeomVertexAnimationSpec &anim) {
     text << "\t uniform float4 clipplane_" << i << ",\n";
   }
 
-  text << "\t uniform float4 attr_ambient,\n";
+  if (key._lighting) {
+    text << "\t uniform float4 attr_ambient,\n";
+  }
   text << "\t uniform float4 attr_colorscale\n";
   text << ") {\n";
 

+ 2 - 2
panda/src/tinydisplay/tinyGraphicsStateGuardian.cxx

@@ -472,14 +472,14 @@ end_frame(Thread *current_thread) {
 bool TinyGraphicsStateGuardian::
 begin_draw_primitives(const GeomPipelineReader *geom_reader,
                       const GeomVertexDataPipelineReader *data_reader,
-                      bool force) {
+                      size_t num_instances, bool force) {
 #ifndef NDEBUG
   if (tinydisplay_cat.is_spam()) {
     tinydisplay_cat.spam() << "begin_draw_primitives: " << *(data_reader->get_object()) << "\n";
   }
 #endif  // NDEBUG
 
-  if (!GraphicsStateGuardian::begin_draw_primitives(geom_reader, data_reader, force)) {
+  if (!GraphicsStateGuardian::begin_draw_primitives(geom_reader, data_reader, num_instances, force)) {
     return false;
   }
   nassertr(_data_reader != nullptr, false);

+ 1 - 1
panda/src/tinydisplay/tinyGraphicsStateGuardian.h

@@ -64,7 +64,7 @@ public:
 
   virtual bool begin_draw_primitives(const GeomPipelineReader *geom_reader,
                                      const GeomVertexDataPipelineReader *data_reader,
-                                     bool force);
+                                     size_t num_instances, bool force);
   virtual bool draw_triangles(const GeomPrimitivePipelineReader *reader,
                               bool force);
   virtual bool draw_tristrips(const GeomPrimitivePipelineReader *reader,

+ 45 - 0
tests/collide/test_collision_polygon.py

@@ -0,0 +1,45 @@
+from panda3d import core
+
+
+def test_collision_polygon_verify_not_enough_points():
+    # Less than 3 points cannot create a polygon
+    assert not core.CollisionPolygon.verify_points([])
+    assert not core.CollisionPolygon.verify_points([core.LPoint3(1, 0, 0)])
+    assert not core.CollisionPolygon.verify_points([core.LPoint3(1, 0, 0), core.LPoint3(0, 0, 1)])
+
+
+def test_collision_polygon_verify_repeating_points():
+    # Repeating points cannot create a polygon
+    assert not core.CollisionPolygon.verify_points([core.LPoint3(1, 0, 0), core.LPoint3(1, 0, 0), core.LPoint3(0, 0, 1)])
+    assert not core.CollisionPolygon.verify_points([core.LPoint3(3, 6, 1), core.LPoint3(1, 3, 5), core.LPoint3(9, 1, 2), core.LPoint3(1, 3, 5)])
+
+
+def test_collision_polygon_verify_colinear_points():
+    # Colinear points cannot create a polygon
+    assert not core.CollisionPolygon.verify_points([core.LPoint3(1, 2, 3), core.LPoint3(2, 3, 4), core.LPoint3(3, 4, 5)])
+    assert not core.CollisionPolygon.verify_points([core.LPoint3(2, 1, 1), core.LPoint3(3, 2, 1), core.LPoint3(4, 3, 1)])
+
+
+def test_collision_polygon_verify_points():
+    # Those should be regular, non-colinear points
+    assert core.CollisionPolygon.verify_points([core.LPoint3(1, 0, 0), core.LPoint3(0, 1, 0), core.LPoint3(0, 0, 1)])
+    assert core.CollisionPolygon.verify_points([core.LPoint3(10, 2, 8), core.LPoint3(7, 1, 3), core.LPoint3(5, 9, 6)])
+    assert core.CollisionPolygon.verify_points([core.LPoint3(3, -8, -7), core.LPoint3(9, 10, 8), core.LPoint3(7, 0, 10), core.LPoint3(-6, -2, 3)])
+    assert core.CollisionPolygon.verify_points([core.LPoint3(-1, -3, -5), core.LPoint3(10, 3, -10), core.LPoint3(-10, 10, -4), core.LPoint3(0, 1, -4), core.LPoint3(-9, -2, 0)])
+
+
+def test_collision_polygon_setup_points():
+    # Create empty collision polygon
+    polygon = core.CollisionPolygon(core.LVecBase3(0, 0, 0), core.LVecBase3(0, 0, 0), core.LVecBase3(0, 0, 0))
+    assert not polygon.is_valid()
+
+    # Test our setup method against a few test cases
+    for points in [
+        [core.LPoint3(-1, -3, -5), core.LPoint3(10, 3, -10), core.LPoint3(-10, 10, -4), core.LPoint3(0, 1, -4), core.LPoint3(-9, -2, 0)],
+        [core.LPoint3(3, -8, -7), core.LPoint3(9, 10, 8), core.LPoint3(7, 0, 10), core.LPoint3(-6, -2, 3)],
+        [core.LPoint3(1, 0, 0), core.LPoint3(0, 1, 0), core.LPoint3(0, 0, 1)],
+        [core.LPoint3(10, 2, 8), core.LPoint3(7, 1, 3), core.LPoint3(5, 9, 6)]
+    ]:
+        polygon.setup_points(points)
+        assert polygon.is_valid()
+        assert polygon.get_num_points() == len(points)