Hardware instancing problem with Panda 1.8

Here is my instancing code. I think it would make a good starting point for an easier interface to include in Panda but I really don’t have the time to bring it to completion.
This actually allows you to have a varying number of instances, so you can add or remove instances as you go. This is done in the vertex shader I posted earlier by modifying one of the lines like this:

const int num_instances = %i;

The code creates many shaders with different numbers substituted for %i and applies the right shader as needed (default max 1024, but you can increase that in the code).
You could put in a new module, say, instancemanager.py:

from panda3d.core import *


class InstanceManager(object):
    def __init__(self, parent=render, max_instances=1024):
        self.parent = parent
        self.no_render = NodePath('no_render')
        self.categories = {}
        v = open('shaders/instance_v.glsl').read()
        f = open('shaders/instance_f.glsl').read()
        self.instance_shader = [Shader.make(Shader.SLGLSL, v % i, f) for i in range(max_instances + 1)]
        taskMgr.add(self.update_category_shaders, 'instance manager')

    def add(self, model):
        try:
            category = self.categories[model]
        except KeyError:
            category = InstanceCategory(self, model)
        return Instance(category)

    def update_category_shaders(self, task):
        for category in self.categories:
            self.categories[category].update_shader()
        return task.cont


class InstanceCategory(object):
    def __init__(self, manager, model):
        self.manager = manager
        self.model = loader.loadModel(model)
        bs = OmniBoundingVolume()
        self.model.node().setBounds(bs)
        self.model.node().setFinal(1)
        self.model.reparentTo(manager.no_render)
        self.instances = []
        self.shader_data = PTA_LVecBase4f()
        self.need_update = True
        self.manager.categories[model] = self

    def add(self):
        count = len(self.instances)
        if count == 1:
            self.model.reparentTo(self.manager.parent)
        self.shader_data.pushBack(UnalignedLVecBase4f())
        self.shader_data.pushBack(UnalignedLVecBase4f())
        self.shader_data.pushBack(UnalignedLVecBase4f())
        self.shader_data.pushBack(UnalignedLVecBase4f())
        self.model.setShader(self.manager.instance_shader[count])
        self.model.setInstanceCount(count)
        self.update_shader()

    def remove(self, index):
        if len(self.instances) > 1:
            self.shader_data[index * 4] = self.shader_data[-4]
            self.shader_data[index * 4 + 1] = self.shader_data[-3]
            self.shader_data[index * 4 + 2] = self.shader_data[-2]
            self.shader_data[index * 4 + 3] = self.shader_data[-1]
            self.instances[index] = self.instances[-1]
            self.instances[index].id = index
        else:
            self.model.reparentTo(self.manager.no_render)
        for i in range(4):
            self.shader_data.popBack()
        self.instances.pop()
        count = len(self.instances)
        self.model.setShader(self.manager.instance_shader[count])
        self.model.setInstanceCount(count)

    def update_shader(self):
        if self.need_update:
            self.model.setShaderInput('shader_data[0]', self.shader_data)
            self.model.setShaderInput('shader_data', self.shader_data)
            self.need_update = False


class Instance(object):
    def __init__(self, category):
        self.category = category
        self.id = len(category.instances)
        category.instances.append(self)
        category.add()

    def destroy(self):
        self.category.remove(self.id)

    def update_transform_inputs(self, mat):
        for i in range(4):
            col = mat.getCol(i)
            self.category.shader_data[self.id * 4 + i] = UnalignedLVecBase4f(col[0], col[1], col[2], col[3])
        self.category.need_update = True

You would use it in a program by doing something like this:

from instancemanager import InstanceManager
instances = InstanceManager()

class FunBox(object):
  def __init__(self, pos):
    self.instance = instances.add('models/box')
    self.np = NodePath('box')
    self.np.setPos(pos)
    self.instance.update_transform_inputs(self.np.getMat())

  def destroy(self):
    self.instance.destroy()
    self.np.removeNode()


# make some instances
box_a = FunBox(Point3(0, 10, 0))
box_b = FunBox(Point3(0, 15, 0))
box_c = FunBox(Point3(0, 20, 0))
box_d = FunBox(Point3(0, 25, 0))

box_c.destroy() # get rid of one of them

The way it is set up right now it relies on you to call update_transform_inputs on your Instance each time you need to move it, but you could automate this with a bit of work to the InstanceManager. I leave it as an exercise to the reader.
I did not actually test this version with my game-specific stuff stripped out, but feel free to reply with any questions. I’ve been meaning to share this with those strugging with Panda’s recently added instancing functionality but have been super busy as of late.

Now that I look through the code again, what you might be missing is the setShaderInput call which I have two of in this code:

self.model.setShaderInput('shader_data[0]', self.shader_data)
self.model.setShaderInput('shader_data', self.shader_data)

Some video card drivers want the input name as usual, and some want it with [0] put on the end. Both are valid, but each driver could accept one or the other or both, so best to be safe and have both lines.