If you played Left4Dead, you might wonder how they manage to show dozens of animated and detailed zombies at the same time. The answer is: instancing.
This is very powerful technique that allows you to display dozens of characters onscreen with much better framerates then if you assign a unique actor for each of them separately. Your CPU will need to animate only some of the actors, while other “clones” just share the same animation (the disadvantage is all instances of an actor will have the same model and the same animation at the same frame).
I have made this small sample in order to demonstrate this technique. For example, on my ancient laptop I can have 100 visible actors onscreen (10 unique actors with 10 instances each) at 60 fps. At the same time, having 100 unique actors onscreen can blow up my laptop.
Please, enjoy and share your feedback.
from pandac.PandaModules import *
from direct.actor.Actor import Actor
from direct.gui.DirectGui import OnscreenText
import random, sys
# General settings
use_multiple_colors = True
use_multiple_textures = False
screenWidth, screenHeight = 500, 500
# Apply settings and run Panda
loadPrcFileData("", "win-size %d %d" % (screenWidth, screenHeight))
loadPrcFileData("", "sync-video #f")
#loadPrcFileData("", "want-pstats #t")
#loadPrcFileData("", "hardware-animated-vertices #t")
import direct.directbase.DirectStart
base.setBackgroundColor(0, 0, 0.2)
base.setFrameRateMeter(True)
base.disableMouse()
# Helper function
def addInstructions(pos, msg):
return OnscreenText(text=msg, style=1, fg=(1,1,1,1), mayChange=1,
pos=(-1, pos), align=TextNode.ALeft, scale = .05,
shadow=(0,0,0,1), shadowOffset=(0.1,0.1))
# Readout and instructions
inst1 = addInstructions(0.95, 'Left/Right Arrow : descrease/increase the number of actors (currently: unknown)')
inst2 = addInstructions(0.90, 'Down/Up Arrow : descrease/increase the number of clones (currently: unknown)')
inst3 = addInstructions(0.85, 'Visible actors onscreen: unknown')
inst4 = addInstructions(0.80, 'Space : pause/resume animation')
inst5 = addInstructions(0.75, 'Enter : print render.ls() info')
inst6 = addInstructions(0.70, 'Escape : quit')
# Setup camera
base.camera.setPos(0, 0, 10)
base.camera.lookAt(0, 0, 0)
lens = OrthographicLens()
lens.setFilmSize(screenWidth/10, screenHeight/10)
base.cam.node().setLens(lens)
# The actual script
actors = []
instances = {}
instances_for_actor = 1
animPlaying = True
class Instance():
def __init__(self, actor, color=None, tex=None):
self.placeholder = render.attachNewNode("placeholder")
x = (random.random()-0.5) * screenWidth/10
y = (random.random()-0.5) * screenHeight/10
self.placeholder.setPos(x, y, 0)
self.instanced_model = actor.instanceTo(self.placeholder)
# To set different color on the instanced actor, apply setColor()
# method on the parent of the instance:
if color:
self.placeholder.setColor(*color)
# To add/replace the actor's texture, apply setTexture() method
# on the parent of the instance:
if tex:
self.placeholder.setTexture(tex, 1)
def destroy(self):
self.instanced_model.detachNode()
self.placeholder.detachNode()
def _newActor():
actor = Actor("samples/Roaming-Ralph/models/ralph",
{"run":"samples/Roaming-Ralph/models/ralph-run"})
actor.flattenStrong()
actor.postFlatten()
if animPlaying:
actor.loop("run")
return actor
def _addInstance(actor):
# In this sample I use random color and texture for each instance.
color = None
if use_multiple_colors:
color = (random.random(), random.random(), random.random())
tex = None
if use_multiple_textures:
textures = ('tex1.jpg', 'tex2.png') # list of available textures
tex = loader.loadTexture(random.choice(textures))
instance = Instance(actor, color=color, tex=tex)
instances[actor].append(instance)
def _removeInstance(instance):
instance.destroy()
def addActor():
global actors, instances, instances_for_actor
actor = _newActor()
actors += [actor]
instances[actor] = []
for i in range(instances_for_actor):
_addInstance(actor)
updateReadout()
def removeActor():
global actors, instances, instances_for_actor
if len(actors) == 1:
return
actor = actors.pop()
for instance in instances[actor]:
_removeInstance(instance)
del instances[actor]
actor.cleanup()
actor.removeNode()
updateReadout()
def addInstances():
global actors, instances, instances_for_actor
instances_for_actor += 1
for actor in actors:
_addInstance(actor)
updateReadout()
def removeInstances():
global actors, instances, instances_for_actor
if instances_for_actor == 1:
return
instances_for_actor -= 1
for actor in actors:
_removeInstance(instances[actor].pop())
updateReadout()
def updateReadout():
global actors, instances_for_actor, inst1, inst2, inst3
inst1.setText('Left/Right Arrow : descrease/increase the number of unique actors (currently %d)' % len(actors))
inst2.setText('Down/Up Arrow : descrease/increase the number of clones (currently %d for each actor)' % instances_for_actor)
inst3.setText('Visible actors onscreen: %d' % (len(actors) * instances_for_actor))
def toggleAnimation():
global animPlaying, actors
if animPlaying:
for actor in actors:
actor.stop()
animPlaying = False
else:
for actor in actors:
actor.loop("run", restart=0)
animPlaying = True
# Add the first actor
addActor()
# Controls
base.accept("enter", render.ls)
base.accept("escape", sys.exit)
base.accept("space", toggleAnimation)
base.accept("arrow_right", addActor)
base.accept("arrow_left", removeActor)
base.accept("arrow_up", addInstances)
base.accept("arrow_down", removeInstances)
run()
EDIT: To make it work, just put 2 any textures in the same folder with this sample (tex1.png and tex2.png). Just make sure they use different colors, to make the difference clearly visible, for example, one is just white, another is black.
Probably, you will want to test texture replacement and color replacement separately, to make their effect clearly visible. To do that, change variables “use_multiple_colors” and “use_multiple_textures” in the beginning.