Flattenning on Actors

I thought flattening makes sense for static objects only. But in release notes to 1.4.0 it is said that “Multiple different Actors can be flattened into one node.” What does it mean? Can it improve performance?

You can flatten an Actor:

a = Actor('a', ...)
a.flattenStrong()

This reduces the Geom count of the actor as low as possible, and yes, it can improve performance, particularly if your bottleneck is Geom count. Note that it can also impair performance, if your bottleneck is something other than Geom count; the flatten operation may require the replication of vertices in order to achieve maximum Geom reduction, which may mean the cost of vertex animation is increased. As in any performance optimization, the right answer depends on your particular scene and your particular target hardware.

You can also flatten multiple Actors together:

a = Actor('a', ...)
b = Actor('b', ...)
root = NodePath('root')
a.reparentTo(root)
b.reparentTo(root)
root.flattenStrong()

This will reduce a and b into (maybe) one Geom, even though they remain independently controllable actors. Note that the actual nodes that refer to a and b may have been removed, so you can no longer call NodePath operations on either actor–for instance, you can no longer call a.setPos() or b.reparentTo(foo). This is a consequence of the flatten operation and is true whether you’re talking about static geometry or Actors. However, you can still control the animations perfectly well. You can call a.play(‘walk’) and b.pose(‘run’, 10) or whatever.

Once again, whether this results in a performance boost depends on the precise nature of your scene and the target hardware.

David

Sorry for reviving this old thread. I have been testing flattenStrong() on Ralph, with surprisingly great results :wink:
The question is: I have found postFlatten() method. It is said that flattening can break something in actor, and this method is required to restore the functionality. Can you, please, elaborate it? When is it required to postFlatten() actor?

Oh, yeah, I keep forgetting about that dirty little method. :frowning:

At the moment, calling flattenStrong() will break an actor if you have already loaded up and played some animations, especially if the actor involves multiple models (e.g. multiple LOD’s) and you have the mergeLODBundles flag enabled–the already-bound animations will no longer work, until you call postFlatten().

But it does no harm to call postFlatten() in other circumstances, and I can’t promise that the above situation will always be the only situation in which it is necessary. So, it’s a good idea to always call actor.postFlatten() after you have called flatten.

David

In 1.6.0 asyncFlattenStrong() method has been introduced. Is it possible to use it with Actors somehow?

Sure, it should work as advertised. You probably want to call postFlatten() in the callback.

David

actor.asyncFlattenStrong() gives error “AttributeError: ‘Actor’ object has no attribute ‘asyncFlattenStrong’”
:confused:
Can you please give a short snippet? And next, what is “to call postFlatten() in the callback”? I am not sure how to implement this callback thing…

Sorry, it’s a method on loader: https://www.panda3d.org/apiref.php?page=Loader#asyncFlattenStrong

David

Wouldn’t it make sense to add this to NodePath-extensions.py as well?

Well, I’d actually rather get away from the use of the various *_extensions.py files. Using this mechanism to bolt interfaces where they don’t belong structurally makes it difficult to move to a more organized, namespace-aware system in the future.

Since the asynchronous flattening requires some global knowledge (of the taskMgr etc.), I think it’s sensible to have the interface on a global object, instead of on every object.

David

Do I use it correctly? Sorry for such question, but this asynchronous flattening is completely new to me, and I am not even sure how to check whether the model was flattened correctly…

actor = Actor()
model = loader.loadModel("samples/Roaming-Ralph/models/ralph")
loader.asyncFlattenStrong(model, callback=actor.postFlatten())
actor.loadModel(model)
actor.loadAnims({"run":"samples/Roaming-Ralph/models/ralph-run"})
actor.loop("run")

EDIT:
I changed the code into following:

def _onCallback(actor):
    actor.postFlatten()
    actor.analyze()
actor = Actor()
model = loader.loadModel("samples/Roaming-Ralph/models/ralph")
loader.asyncFlattenStrong(model, callback=_onCallback(actor))
actor.loadModel(model)
actor.loadAnims({"run":"samples/Roaming-Ralph/models/ralph-run"})
actor.loop("run")

analyze() method prints nonsense:

1 total nodes (including 0 instances); 0 LODNodes.
0 transforms; 0% of nodes have some render attribute.
0 Geoms, with 0 GeomVertexDatas and 0 GeomVertexFormats, appear on 0 GeomNodes.
0 vertices, 0 normals, 0 colors, 0 texture coordinates.
GeomVertexData arrays occupy 0K memory.
GeomPrimitive arrays occupy 0K memory.
0 triangles:
  0 of these are on 0 tristrips.
  0 of these are independent triangles.
0 textures, estimated minimum 0K texture memory required.

It looks like callback was called before the model was flattened or even loaded…

EDIT 2:
I added a task to print actor.analyze() after few seconds. It shows that the above code doesn’t perform any flattening… :confused: This asynchronous thing is pretty tricky, isn’t it?

Note that this:

loader.asyncFlattenStrong(model, callback=actor.postFlatten()) 

or this:

loader.asyncFlattenStrong(model, callback=_onCallback(actor))

is wrong. This means you are calling the callback already now, and passing the return value (None) to “callback”.
You need to pass the function, and not its return value:

loader.asyncFlattenStrong(model, callback=actor.postFlatten) 

This doesn’t work either… First, postFlatten() method takes only one argument, while this statement sends two. I had to change into following:

def _onCallback(model, actor):
    actor.postFlatten()
actor = Actor()
model = loader.loadModel("samples/Roaming-Ralph/models/ralph")
loader.asyncFlattenStrong(model, callback=_onCallback, extraArgs=[actor])
actor.loadModel(model)
actor.loadAnims({"run":"samples/Roaming-Ralph/models/ralph-run"})
actor.loop("run")

The model doesn’t get flattened. I don’t know what is wrong…
EDIT: I can be wrong in some way though: the actor is not animated after the callback. Since it is correct behavior after flattening, some work was done. Some work, but not flattening: if I then print actor.analyze(), it shows that the model was not flattened.

For simplicity, I made this simple code. Press Enter to print actor.analyze():

import sys
from pandac.PandaModules import *
from direct.actor.Actor import Actor
import direct.directbase.DirectStart

base.cam.setPos(render, 0, -20, 10)
base.cam.lookAt(0, 0, 0)

'''
# 1) Usual flattenStrong:
actor = Actor("samples/Roaming-Ralph/models/ralph",
            {"run":"samples/Roaming-Ralph/models/ralph-run"})
actor.reparentTo(render)
actor.flattenStrong()
actor.postFlatten()
actor.loop("run")
'''

# 2) asyncFlattenStrong:
def _onCallback(fmodel):
    actor.postFlatten()
model = loader.loadModel("samples/Roaming-Ralph/models/ralph")
loader.asyncFlattenStrong(model, callback=_onCallback)
actor = Actor(model,
            {"run":"samples/Roaming-Ralph/models/ralph-run"})
actor.reparentTo(render)
actor.loop("run")

base.accept("enter", actor.analyze)
base.accept("escape", sys.exit)
run()

The first, “traditional” flattened actor prints out:

3 total nodes (including 0 instances); 0 LODNodes.
0 transforms; 0% of nodes have some render attribute.
2 Geoms, with 1 GeomVertexDatas and 1 GeomVertexFormats, appear on 1 GeomNodes.
4748 vertices, 4748 normals, 0 colors, 4748 texture coordinates.
GeomVertexData arrays occupy 158K memory.
GeomPrimitive arrays occupy 21K memory.
2374 vertices are unreferenced by any GeomPrimitives.
3550 triangles:
  112 of these are on 36 tristrips (3.11111 average tris per strip).
  3438 of these are independent triangles.
1 textures, estimated minimum 768K texture memory required.

The second, asynchronous flattened actor, prints out:

3 total nodes (including 0 instances); 0 LODNodes.
0 transforms; 0% of nodes have some render attribute.
2 Geoms, with 2 GeomVertexDatas and 2 GeomVertexFormats, appear on 1 GeomNodes.
4748 vertices, 4748 normals, 0 colors, 4748 texture coordinates.
GeomVertexData arrays occupy 158K memory.
GeomPrimitive arrays occupy 21K memory.
2374 vertices are unreferenced by any GeomPrimitives.
1 GeomVertexArrayDatas are redundant, wasting 5K.
3550 triangles:
  112 of these are on 36 tristrips (3.11111 average tris per strip).
  3438 of these are independent triangles.
1 textures, estimated minimum 768K texture memory required.

Obviously, results are not the same… Where is the problem?

I have found the way that works!!!

def _onCallback(fmodel):
    actor.postFlatten()
actor = Actor("samples/Roaming-Ralph/models/ralph",
            {"run":"samples/Roaming-Ralph/models/ralph-run"})
loader.asyncFlattenStrong(actor.getChild(0),
                        callback=_onCallback)

:smiley:
I tried to use actor.getGeomNode() but it breaks animation. actor.getChild(0) works fine. The problem is solved.

FTR, calling clearModelNodes on the NodePath before flattening would work too, instead of using getChild(0).

Like this?

actor.clearModelNodes()
loader.asyncFlattenStrong(actor,
                        callback=_onCallback)

I tried but it breaks animation (it cannot be played anymore).

Hey, wait. Your Actor has a ModelNode within it? That shouldn’t happen unless you pass flattenable = False to the Actor constructor. Can you show me the result of Actor.ls() before you call flatten?

David

I use Ralph model without any changes, and I don’t pass anything to Actor constructor except model file and animation file.
Before flattening, actor.ls() prints:

PandaNode Ralph
  Character __Actor_modelRoot
    GeomNode  (2 geoms: S:(ColorAttrib CullFaceAttrib TextureAttrib))

The same as after flattening, though :slight_smile:
actor.analyze() before and after flattening are different.

Does it really? Not for me. I think ralph, all by himself, is already as flat as he can be. What is the difference between before and after in your analyze() output?

Maybe you should just experiment with a synchronous actor.flattenStrong() first, and be sure you understand exactly what you expect that to do, before you take the next step into asynchronous flattening.

David