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.