Browse Source

Merge branch 'master' into shaderpipeline

rdb 5 years ago
parent
commit
ed6d89bbb9

+ 26 - 21
.github/workflows/ci.yml

@@ -92,12 +92,18 @@ jobs:
     - name: Install dependencies (macOS)
       if: runner.os == 'macOS'
       run: |
-        curl -O https://www.panda3d.org/download/panda3d-1.10.5/panda3d-1.10.5-tools-mac.tar.gz
-        tar -xf panda3d-1.10.5-tools-mac.tar.gz
+        curl -O https://www.panda3d.org/download/panda3d-1.10.7/panda3d-1.10.7-tools-mac.tar.gz
+        tar -xf panda3d-1.10.7-tools-mac.tar.gz
+        mv panda3d-1.10.7/thirdparty thirdparty
+        rmdir panda3d-1.10.7
 
-        brew install ccache
+        mkdir -p build/Frameworks
+        cp -R thirdparty/darwin-libs-a/nvidiacg/Cg.framework build/Frameworks/Cg.framework
+
+        mkdir -p "build/${{ matrix.config }}/Frameworks"
+        cp -R thirdparty/darwin-libs-a/nvidiacg/Cg.framework "build/${{ matrix.config }}/Frameworks/Cg.framework"
 
-        echo "##[set-env name=thirdpartyOption;]-D THIRDPARTY_DIRECTORY=../panda3d-1.10.5/thirdparty" -DHAVE_CG=OFF
+        brew install ccache
 
     - name: Install dependencies (Ubuntu)
       if: startsWith(matrix.os, 'ubuntu')
@@ -120,20 +126,19 @@ jobs:
       if: runner.os == 'Windows'
       uses: actions/cache@v1
       with:
-        path: thirdparty-tools
-        key: ci-cmake-${{ runner.OS }}-thirdparty-v1.10.5-r1
+        path: thirdparty
+        key: ci-cmake-${{ runner.OS }}-thirdparty-v1.10.7-r1
     - name: Install dependencies (Windows)
       if: runner.os == 'Windows'
       shell: powershell
       run: |
-        if (!(Test-Path thirdparty-tools/panda3d-1.10.5)) {
+        if (!(Test-Path thirdparty/win-libs-vc14-x64)) {
           $wc = New-Object System.Net.WebClient
-          $wc.DownloadFile("https://www.panda3d.org/download/panda3d-1.10.5/panda3d-1.10.5-tools-win64.zip", "thirdparty-tools.zip")
+          $wc.DownloadFile("https://www.panda3d.org/download/panda3d-1.10.7/panda3d-1.10.7-tools-win64.zip", "thirdparty-tools.zip")
           Expand-Archive -Path thirdparty-tools.zip
+          Move-Item -Path thirdparty-tools/panda3d-1.10.7/thirdparty -Destination .
         }
 
-        echo "##[set-env name=thirdpartyOption;]-D THIRDPARTY_DIRECTORY=../thirdparty-tools/panda3d-1.10.5/thirdparty"
-
     - name: ccache (non-Windows)
       if: runner.os != 'Windows'
       uses: actions/cache@v1
@@ -146,15 +151,15 @@ jobs:
       env:
         CMAKE_GENERATOR: "${{ matrix.generator }}"
       run: >
-        mkdir build
+        mkdir -p build
 
         cd build
 
         if ${{ matrix.compiler == 'Clang' }}; then
           if [[ "$CMAKE_GENERATOR" == *Studio*2019* ]]; then
-            export CMAKE_GENERATOR_TOOLSET=ClangCL thirdpartyOption="$thirdpartyOption -DHAVE_HARFBUZZ=NO"
+            export CMAKE_GENERATOR_TOOLSET=ClangCL
           elif [[ "$CMAKE_GENERATOR" == *Studio* ]]; then
-            export CMAKE_GENERATOR_TOOLSET=LLVM thirdpartyOption="$thirdpartyOption -DHAVE_HARFBUZZ=NO"
+            export CMAKE_GENERATOR_TOOLSET=LLVM
           else
             export CC=clang CXX=clang++
           fi
@@ -162,7 +167,8 @@ jobs:
 
         if ${{ runner.os != 'Windows' }}; then
           compilerLauncher=$(echo -DCMAKE_C{,XX}_COMPILER_LAUNCHER=ccache)
-          echo "##[set-env name=CCACHE_DIR;]$(dirname $PWD)/ccache"
+          export CCACHE_DIR="$(dirname $PWD)/ccache"
+          echo "CCACHE_DIR=$(dirname $PWD)/ccache" >> $GITHUB_ENV
         fi
 
         cmake
@@ -172,7 +178,6 @@ jobs:
         -D BUILD_METALIBS=${{ matrix.metalibs }}
         -D HAVE_PYTHON=${{ matrix.python }}
         -D HAVE_EIGEN=${{ matrix.eigen }}
-        ${thirdpartyOption:-}
         ..
 
     - name: Build (no Python)
@@ -313,16 +318,16 @@ jobs:
       shell: powershell
       run: |
         $wc = New-Object System.Net.WebClient
-        $wc.DownloadFile("https://www.panda3d.org/download/panda3d-1.10.5/panda3d-1.10.5-tools-win64.zip", "thirdparty-tools.zip")
+        $wc.DownloadFile("https://www.panda3d.org/download/panda3d-1.10.7/panda3d-1.10.7-tools-win64.zip", "thirdparty-tools.zip")
         Expand-Archive -Path thirdparty-tools.zip
-        Move-Item -Path thirdparty-tools/panda3d-1.10.5/thirdparty -Destination .
+        Move-Item -Path thirdparty-tools/panda3d-1.10.7/thirdparty -Destination .
     - name: Get thirdparty packages (macOS)
       if: runner.os == 'macOS'
       run: |
-        curl -O https://www.panda3d.org/download/panda3d-1.10.5/panda3d-1.10.5-tools-mac.tar.gz
-        tar -xf panda3d-1.10.5-tools-mac.tar.gz
-        mv panda3d-1.10.5/thirdparty thirdparty
-        rmdir panda3d-1.10.5
+        curl -O https://www.panda3d.org/download/panda3d-1.10.7/panda3d-1.10.7-tools-mac.tar.gz
+        tar -xf panda3d-1.10.7-tools-mac.tar.gz
+        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.8
       uses: actions/setup-python@v1

+ 2 - 3
BACKERS.md

@@ -15,7 +15,6 @@ This is a list of all the people who are contributing financially to Panda3D.  I
 * [Mitchell Stokes](https://opencollective.com/mitchell-stokes)
 * [Daniel Stokes](https://opencollective.com/daniel-stokes)
 * [David Rose](https://opencollective.com/david-rose)
-* [Carnetsoft](https://cs-driving-simulator.com/)
 
 ## Benefactors
 
@@ -23,14 +22,14 @@ This is a list of all the people who are contributing financially to Panda3D.  I
 
 * Sam Edwards
 * Max Voss
-* Will Nielsen
 
 ## Enthusiasts
 
-![Benefactors](https://opencollective.com/panda3d/tiers/enthusiast.svg?avatarHeight=48&width=600)
+![Enthusiasts](https://opencollective.com/panda3d/tiers/enthusiast.svg?avatarHeight=48&width=600)
 
 * Eric Thomson
 * Kyle Roach
+* Brian Lach
 
 ## Backers
 

+ 1 - 1
README.md

@@ -1,4 +1,4 @@
-[![Build Status](https://travis-ci.org/panda3d/panda3d.svg?branch=master)](https://travis-ci.org/panda3d/panda3d)
+[![Build Status](https://github.com/panda3d/panda3d/workflows/Continuous%20Integration/badge.svg?branch=master)](https://github.com/panda3d/panda3d/actions?query=branch%3Amaster+workflow%3A%22Continuous+Integration%22)
 [![OpenCollective](https://opencollective.com/panda3d/backers/badge.svg)](https://opencollective.com/panda3d)
 [![OpenCollective](https://opencollective.com/panda3d/sponsors/badge.svg)](https://opencollective.com/panda3d)
 

+ 3 - 3
cmake/macros/PackageConfig.cmake

@@ -145,6 +145,8 @@ function(package_option name)
     endif()
   endif()
 
+  option("HAVE_${name}" "${cache_string}" "${default}")
+
   # If it was set by the user but not found, display an error.
   string(TOUPPER "${found_as}" FOUND_AS)
   if(HAVE_${name} AND NOT ${found_as}_FOUND AND NOT ${FOUND_AS}_FOUND)
@@ -180,9 +182,7 @@ function(package_option name)
       "$<TARGET_PROPERTY:PKG::${name},INTERFACE_INCLUDE_DIRECTORIES>")
   endif()
 
-  # Create the option, and if it actually is enabled, populate the INTERFACE
-  # library created above
-  option("HAVE_${name}" "${cache_string}" "${default}")
+  # If the option actually is enabled, populate the INTERFACE library created above
   if(HAVE_${name})
     set(use_variables ON)
 

+ 9 - 9
direct/src/directtools/DirectSession.py

@@ -68,24 +68,24 @@ class DirectSession(DirectObject):
         self.activeParent = None
 
         self.selectedNPReadout = OnscreenText.OnscreenText(
-            pos = (-1.0, -0.9), bg=Vec4(1, 1, 1, 1),
-            scale = 0.05, align = TextNode.ALeft,
+            pos = (0.1, 0.1), bg=Vec4(0, 0, 0, .2), style=3,
+            fg = (1, 1, 1, 1), scale = 0.05, align = TextNode.ALeft,
             mayChange = 1, font = self.font)
         # Make sure readout is never lit or drawn in wireframe
         useDirectRenderStyle(self.selectedNPReadout)
         self.selectedNPReadout.reparentTo(hidden)
 
         self.activeParentReadout = OnscreenText.OnscreenText(
-            pos = (-1.0, -0.975), bg=Vec4(1, 1, 1, 1),
-            scale = 0.05, align = TextNode.ALeft,
+            pos = (0.1, 0.16), bg=Vec4(0, 0, 0, .2), style = 3,
+            fg = (1, 1, 1, 1), scale = 0.05, align = TextNode.ALeft,
             mayChange = 1, font = self.font)
         # Make sure readout is never lit or drawn in wireframe
         useDirectRenderStyle(self.activeParentReadout)
         self.activeParentReadout.reparentTo(hidden)
 
         self.directMessageReadout = OnscreenText.OnscreenText(
-            pos = (-1.0, 0.9), bg=Vec4(1, 1, 1, 1),
-            scale = 0.05, align = TextNode.ALeft,
+            pos = (0.1, -0.1), bg=Vec4(0, 0, 0, .2), style = 3,
+            fg = (1, 1, 1, 1), scale = 0.05, align = TextNode.ALeft,
             mayChange = 1, font = self.font)
         # Make sure readout is never lit or drawn in wireframe
         useDirectRenderStyle(self.directMessageReadout)
@@ -698,7 +698,7 @@ class DirectSession(DirectObject):
                 self.ancestry = dnp.getAncestors()
                 self.ancestryIndex = 0
             # Update the selectedNPReadout
-            self.selectedNPReadout.reparentTo(aspect2d)
+            self.selectedNPReadout.reparentTo(base.a2dBottomLeft)
             self.selectedNPReadout.setText(
                 'Selected:' + dnp.getName())
             # Show the manipulation widget
@@ -782,7 +782,7 @@ class DirectSession(DirectObject):
         # Record new parent
         self.activeParent = nodePath
         # Update the activeParentReadout
-        self.activeParentReadout.reparentTo(aspect2d)
+        self.activeParentReadout.reparentTo(base.a2dBottomLeft)
         self.activeParentReadout.setText(
             'Active Reparent Target:' + nodePath.getName())
         # Alert everyone else
@@ -1031,7 +1031,7 @@ class DirectSession(DirectObject):
     def message(self, text):
         taskMgr.remove('hideDirectMessage')
         taskMgr.remove('hideDirectMessageLater')
-        self.directMessageReadout.reparentTo(aspect2d)
+        self.directMessageReadout.reparentTo(base.a2dTopLeft)
         self.directMessageReadout.setText(text)
         self.hideDirectMessageLater()
 

+ 1 - 2
direct/src/dist/commands.py

@@ -1311,7 +1311,6 @@ class bdist_apps(setuptools.Command):
             for i in apps
         ]
 
-        fullname = self.distribution.get_fullname()
         shortname = self.distribution.get_name()
 
         # Create the .nsi installer script
@@ -1321,7 +1320,7 @@ class bdist_apps(setuptools.Command):
 
         # Some global info
         nsi.write('Name "%s"\n' % shortname)
-        nsi.write('OutFile "%s"\n' % (fullname+'.exe'))
+        nsi.write('OutFile "%s"\n' % os.path.join(self.dist_dir, basename+'.exe'))
         if is_64bit:
             nsi.write('InstallDir "$PROGRAMFILES64\\%s"\n' % shortname)
         else:

+ 5 - 1
direct/src/dist/pefile.py

@@ -242,7 +242,11 @@ class VersionInfoResource(object):
         length, value_length = unpack('<HH', data[0:4])
         offset = 40 + value_length + (value_length & 1)
         dwords = array('I')
-        dwords.fromstring(bytes(data[40:offset]))
+        if sys.version_info >= (3, 2):
+            dwords.frombytes(bytes(data[40:offset]))
+        else:
+            dwords.fromstring(bytes(data[40:offset]))
+
         if len(dwords) > 0:
             self.signature = dwords[0]
         if len(dwords) > 1:

+ 30 - 29
direct/src/showbase/Loader.py

@@ -192,21 +192,21 @@ class Loader(DirectObject):
         pathname), the return value will be a NodePath to the model
         loaded if the load was successful, or None otherwise.  If the
         input modelPath is a list of pathnames, the return value will
-        be a list of NodePaths and/or Nones.
+        be a list of `.NodePath` objects and/or Nones.
 
         loaderOptions may optionally be passed in to control details
         about the way the model is searched and loaded.  See the
-        LoaderOptions class for more.
+        `.LoaderOptions` class for more.
 
-        The default is to look in the ModelPool (RAM) cache first, and
-        return a copy from that if the model can be found there.  If
-        the bam cache is enabled (via the model-cache-dir config
+        The default is to look in the `.ModelPool` (RAM) cache first,
+        and return a copy from that if the model can be found there.
+        If the bam cache is enabled (via the `model-cache-dir` config
         variable), then that will be consulted next, and if both
         caches fail, the file will be loaded from disk.  If noCache is
         True, then neither cache will be consulted or updated.
 
         If allowInstance is True, a shared instance may be returned
-        from the ModelPool.  This is dangerous, since it is easy to
+        from the `.ModelPool`.  This is dangerous, since it is easy to
         accidentally modify the shared instance, and invalidate future
         load attempts of the same model.  Normally, you should leave
         allowInstance set to False, which will always return a unique
@@ -214,10 +214,10 @@ class Loader(DirectObject):
 
         If okMissing is True, None is returned if the model is not
         found or cannot be read, and no error message is printed.
-        Otherwise, an IOError is raised if the model is not found or
+        Otherwise, an `IOError` is raised if the model is not found or
         cannot be read (similar to attempting to open a nonexistent
-        file).  (If modelPath is a list of filenames, then IOError is
-        raised if *any* of the models could not be loaded.)
+        file).  (If modelPath is a list of filenames, then `IOError`
+        is raised if *any* of the models could not be loaded.)
 
         If callback is not None, then the model load will be performed
         asynchronously.  In this case, loadModel() will initiate a
@@ -235,7 +235,7 @@ class Loader(DirectObject):
 
         True asynchronous model loading requires Panda to have been
         compiled with threading support enabled (you can test
-        Thread.isThreadingSupported()).  In the absence of threading
+        `.Thread.isThreadingSupported()`).  In the absence of threading
         support, the asynchronous interface still exists and still
         behaves exactly as described, except that loadModel() might
         not return immediately.
@@ -420,7 +420,7 @@ class Loader(DirectObject):
     def saveModel(self, modelPath, node, loaderOptions = None,
                   callback = None, extraArgs = [], priority = None,
                   blocking = None):
-        """ Saves the model (a NodePath or PandaNode) to the indicated
+        """ Saves the model (a `NodePath` or `PandaNode`) to the indicated
         filename path.  Returns true on success, false on failure.  If
         a callback is used, the model is saved asynchronously, and the
         true/false status is passed to the callback function. """
@@ -508,8 +508,8 @@ class Loader(DirectObject):
         """
         modelPath is a string.
 
-        This loads a special model as a TextFont object, for rendering
-        text with a TextNode.  A font file must be either a special
+        This loads a special model as a `TextFont` object, for rendering
+        text with a `TextNode`.  A font file must be either a special
         egg file (or bam file) generated with egg-mkfont, which is
         considered a static font, or a standard font file (like a TTF
         file) that is supported by FreeType, which is considered a
@@ -573,7 +573,7 @@ class Loader(DirectObject):
 
         If color is not None, it should be a VBase4 specifying the
         foreground color of the font.  Specifying this option breaks
-        TextNode.setColor(), so you almost never want to use this
+        `TextNode.setColor()`, so you almost never want to use this
         option; the default (white) is the most appropriate for a
         font, as it allows text to have any arbitrary color assigned
         at generation time.  However, if you want to use a colored
@@ -695,7 +695,8 @@ class Loader(DirectObject):
         texturePath is a string.
 
         Attempt to load a texture from the given file path using
-        TexturePool class.
+        `TexturePool` class.  Returns a `Texture` object, or raises
+        `IOError` if the file could not be loaded.
 
         okMissing should be True to indicate the method should return
         None if the texture file is not found.  If it is False, the
@@ -713,17 +714,17 @@ class Loader(DirectObject):
         the texture and the number of expected mipmap images.
 
         If minfilter or magfilter is not None, they should be a symbol
-        like SamplerState.FTLinear or SamplerState.FTNearest.  (minfilter
-        may be further one of the Mipmap filter type symbols.)  These
-        specify the filter mode that will automatically be applied to
-        the texture when it is loaded.  Note that this setting may
+        like `SamplerState.FTLinear` or `SamplerState.FTNearest`.
+        (minfilter may be further one of the Mipmap filter type symbols.)
+        These specify the filter mode that will automatically be applied
+        to the texture when it is loaded.  Note that this setting may
         override the texture's existing settings, even if it has
-        already been loaded.  See egg-texture-cards for a more robust
+        already been loaded.  See `egg-texture-cards` for a more robust
         way to apply per-texture filter types and settings.
 
         If anisotropicDegree is not None, it specifies the anisotropic degree
         to apply to the texture when it is loaded.  Like minfilter and
-        magfilter, egg-texture-cards may be a more robust way to apply
+        magfilter, `egg-texture-cards` may be a more robust way to apply
         this setting.
 
         If multiview is true, it indicates to load a multiview or
@@ -769,7 +770,7 @@ class Loader(DirectObject):
         """
         texturePattern is a string that contains a sequence of one or
         more hash characters ('#'), which will be filled in with the
-        z-height number.  Returns a 3-D Texture object, suitable for
+        z-height number.  Returns a 3-D `Texture` object, suitable for
         rendering volumetric textures.
 
         okMissing should be True to indicate the method should return
@@ -826,7 +827,7 @@ class Loader(DirectObject):
         """
         texturePattern is a string that contains a sequence of one or
         more hash characters ('#'), which will be filled in with the
-        z-height number.  Returns a 2-D Texture array object, suitable
+        z-height number.  Returns a 2-D `Texture` array object, suitable
         for rendering array of textures.
 
         okMissing should be True to indicate the method should return
@@ -884,7 +885,7 @@ class Loader(DirectObject):
         texturePattern is a string that contains a sequence of one or
         more hash characters ('#'), which will be filled in with the
         face index number (0 through 6).  Returns a six-face cube map
-        Texture object.
+        `Texture` object.
 
         okMissing should be True to indicate the method should return
         None if the texture file is not found.  If it is False, the
@@ -951,8 +952,8 @@ class Loader(DirectObject):
         """Loads one or more sound files, specifically designated as a
         "sound effect" file (that is, uses the sfxManager to load the
         sound).  There is no distinction between sound effect files
-        and music files other than the particular AudioManager used to
-        load the sound file, but this distinction allows the sound
+        and music files other than the particular `AudioManager` used
+        to load the sound file, but this distinction allows the sound
         effects and/or the music files to be adjusted as a group,
         independently of the other group."""
 
@@ -965,8 +966,8 @@ class Loader(DirectObject):
         """Loads one or more sound files, specifically designated as a
         "music" file (that is, uses the musicManager to load the
         sound).  There is no distinction between sound effect files
-        and music files other than the particular AudioManager used to
-        load the sound file, but this distinction allows the sound
+        and music files other than the particular `AudioManager` used
+        to load the sound file, but this distinction allows the sound
         effects and/or the music files to be adjusted as a group,
         independently of the other group."""
         if(self.base.musicManager):
@@ -1052,7 +1053,7 @@ class Loader(DirectObject):
                            callback = None, extraArgs = []):
         """ Performs a model.flattenStrong() operation in a sub-thread
         (if threading is compiled into Panda).  The model may be a
-        single NodePath, or it may be a list of NodePaths.
+        single `.NodePath`, or it may be a list of NodePaths.
 
         Each model is duplicated and flattened in the sub-thread.
 

+ 6 - 0
direct/src/showbase/ShowBase.py

@@ -152,6 +152,7 @@ class ShowBase(DirectObject.DirectObject):
         self.wantStats = self.config.GetBool('want-pstats', 0)
         self.wantTk = False
         self.wantWx = False
+        self.wantDirect = False
 
         #: Fill this in with a function to invoke when the user "exits"
         #: the program by closing the main window.
@@ -3264,7 +3265,12 @@ class ShowBase(DirectObject.DirectObject):
     def startDirect(self, fWantDirect = 1, fWantTk = 1, fWantWx = 0):
         self.startTk(fWantTk)
         self.startWx(fWantWx)
+
+        if self.wantDirect == fWantDirect:
+            return
+
         self.wantDirect = fWantDirect
+
         if self.wantDirect:
             # Use importlib to prevent this import from being picked up
             # by modulefinder when packaging an application.

+ 1 - 1
panda/src/collide/collisionTraverser.h

@@ -62,7 +62,7 @@ PUBLISHED:
   void clear_colliders();
   MAKE_SEQ_PROPERTY(colliders, get_num_colliders, get_collider);
 
-  void traverse(const NodePath &root);
+  BLOCKING void traverse(const NodePath &root);
 
 #if defined(DO_COLLISION_RECORDING) || !defined(CPPPARSER)
   void set_recorder(CollisionRecorder *recorder);

+ 3 - 2
panda/src/display/standardMunger.cxx

@@ -121,8 +121,9 @@ munge_data_impl(const GeomVertexData *data) {
   }
 
   GeomVertexAnimationSpec animation = new_data->get_format()->get_animation();
-  if (_shader_skinning || (_auto_shader && hardware_animated_vertices &&
-      animation.get_animation_type() == AT_panda)) {
+  if ((_shader_skinning && animation.get_animation_type() != AT_none) ||
+      (_auto_shader && hardware_animated_vertices &&
+       animation.get_animation_type() == AT_panda)) {
     animation.set_hardware(4, true);
 
   } else if (hardware_animated_vertices &&

+ 11 - 0
panda/src/glstuff/glGraphicsStateGuardian_src.cxx

@@ -1992,6 +1992,8 @@ reset() {
          get_extension_func("glUniform3uiv");
       _glUniform4uiv = (PFNGLUNIFORM4UIVPROC)
          get_extension_func("glUniform4uiv");
+      _glVertexAttribI4ui = (PFNGLVERTEXATTRIBI4UIPROC)
+         get_extension_func("glVertexAttribI4ui");
 
     } else if (has_extension("GL_EXT_gpu_shader4")) {
       _glBindFragDataLocation = (PFNGLBINDFRAGDATALOCATIONPROC)
@@ -2006,10 +2008,13 @@ reset() {
          get_extension_func("glUniform3uivEXT");
       _glUniform4uiv = (PFNGLUNIFORM4UIVPROC)
          get_extension_func("glUniform4uivEXT");
+      _glVertexAttribI4ui = (PFNGLVERTEXATTRIBI4UIPROC)
+         get_extension_func("glVertexAttribI4uiEXT");
 
     } else {
       _glBindFragDataLocation = nullptr;
       _glVertexAttribIPointer = nullptr;
+      _glVertexAttribI4ui = nullptr;
     }
 
     if (is_at_least_gl_version(4, 0)) {
@@ -2046,6 +2051,9 @@ reset() {
          get_extension_func("glPatchParameteri");
     }
   }
+  else {
+    _glVertexAttribI4ui = nullptr;
+  }
 #endif
 
 #ifdef OPENGLES_2
@@ -2108,8 +2116,11 @@ reset() {
   if (is_at_least_gles_version(3, 0)) {
     _glVertexAttribIPointer = (PFNGLVERTEXATTRIBIPOINTERPROC)
       get_extension_func("glVertexAttribIPointer");
+    _glVertexAttribI4ui = (PFNGLVERTEXATTRIBI4UIPROC)
+      get_extension_func("glVertexAttribI4ui");
   } else {
     _glVertexAttribIPointer = nullptr;
+    _glVertexAttribI4ui = nullptr;
   }
 
   if (has_extension("GL_EXT_blend_func_extended")) {

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

@@ -198,6 +198,7 @@ typedef void (APIENTRYP PFNGLUNIFORMMATRIX4X3DVPROC) (GLint location, GLsizei co
 typedef void (APIENTRYP PFNGLVALIDATEPROGRAMPROC) (GLuint program);
 typedef void (APIENTRYP PFNGLVERTEXATTRIB4FVPROC) (GLuint index, const GLfloat *v);
 typedef void (APIENTRYP PFNGLVERTEXATTRIB4DVPROC) (GLuint index, const GLdouble *v);
+typedef void (APIENTRYP PFNGLVERTEXATTRIBI4UIPROC) (GLuint index, GLuint x, GLuint y, GLuint z, GLuint w);
 typedef void (APIENTRYP PFNGLVERTEXATTRIBPOINTERPROC) (GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const GLvoid *pointer);
 typedef void (APIENTRYP PFNGLVERTEXATTRIBIPOINTERPROC) (GLuint index, GLint size, GLenum type, GLsizei stride, const GLvoid *pointer);
 typedef void (APIENTRYP PFNGLVERTEXATTRIBLPOINTERPROC) (GLuint index, GLint size, GLenum type, GLsizei stride, const GLvoid *pointer);
@@ -1029,6 +1030,7 @@ public:
   PFNGLVALIDATEPROGRAMPROC _glValidateProgram;
   PFNGLVERTEXATTRIB4FVPROC _glVertexAttrib4fv;
   PFNGLVERTEXATTRIB4DVPROC _glVertexAttrib4dv;
+  PFNGLVERTEXATTRIBI4UIPROC _glVertexAttribI4ui;
   PFNGLVERTEXATTRIBPOINTERPROC _glVertexAttribPointer;
   PFNGLVERTEXATTRIBIPOINTERPROC _glVertexAttribIPointer;
   PFNGLVERTEXATTRIBLPOINTERPROC _glVertexAttribLPointer;

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

@@ -2769,6 +2769,10 @@ update_shader_vertex_arrays(ShaderContext *prev, bool force) {
           _glgsg->_glVertexAttrib4fv(p, _glgsg->_scene_graph_color.get_data());
 #endif
         }
+        else if (name == InternalName::get_transform_index() &&
+                 _glgsg->_glVertexAttribI4ui != nullptr) {
+          _glgsg->_glVertexAttribI4ui(p, 0, 1, 2, 3);
+        }
       }
     }
 

+ 1 - 1
panda/src/gobj/geomVertexData.cxx

@@ -659,7 +659,7 @@ copy_from(const GeomVertexData *source, bool keep_data_objects,
             for (size_t i = 0; i < blend.get_num_transforms(); i++) {
               int index = add_transform(transform_table, blend.get_transform(i),
                                         already_added);
-              nassertv(index <= 4);
+              nassertv(index < 4);
               weights[index] = blend.get_weight(i);
             }
             if (weight.has_column()) {

+ 1 - 1
panda/src/gobj/matrixLens.I

@@ -54,7 +54,7 @@ operator = (const MatrixLens &copy) {
  * Explicitly specifies the projection matrix.  This matrix should convert X
  * and Y to the range [-film_size/2, film_size/2], where (-fs/2,-fs/2) is the
  * lower left corner of the screen and (fs/2, fs/2) is the upper right.  Z
- * should go to the range [-1, 1], where -1 is the far plane and 1 is the near
+ * should go to the range [-1, 1], where -1 is the near plane and 1 is the far
  * plane.  Note that this is a left-handed Y-up coordinate system.
  *
  * The default film_size for a MatrixLens is 2, so the default range is [-1,

+ 1 - 0
setup.cfg

@@ -17,6 +17,7 @@ classifiers =
     Programming Language :: Python :: 3.6
     Programming Language :: Python :: 3.7
     Programming Language :: Python :: 3.8
+    Programming Language :: Python :: 3.9
     Programming Language :: Python :: Implementation :: CPython
     Topic :: Games/Entertainment
     Topic :: Multimedia