qpgeom animation

This commit is contained in:
David Rose 2005-03-25 21:59:20 +00:00
parent 68a147ef62
commit 3716a4b470
17 changed files with 632 additions and 204 deletions

View File

@ -197,6 +197,7 @@ unify_attributes(EggPrimitive::Shading shading) {
EggVertexPool *vertex_pool = orig_vertex->get_pool();
nassertv(vertex_pool != (EggVertexPool *)NULL);
vertex = vertex_pool->create_unique_vertex(*vertex);
vertex->copy_grefs_from(*orig_vertex);
replace(pi, vertex);
}
clear_normal();
@ -225,6 +226,7 @@ unify_attributes(EggPrimitive::Shading shading) {
EggVertexPool *vertex_pool = orig_vertex->get_pool();
nassertv(vertex_pool != (EggVertexPool *)NULL);
vertex = vertex_pool->create_unique_vertex(*vertex);
vertex->copy_grefs_from(*orig_vertex);
replace(pi, vertex);
}
}
@ -272,6 +274,7 @@ unify_attributes(EggPrimitive::Shading shading) {
EggVertexPool *vertex_pool = orig_vertex->get_pool();
nassertv(vertex_pool != (EggVertexPool *)NULL);
vertex = vertex_pool->create_unique_vertex(*vertex);
vertex->copy_grefs_from(*orig_vertex);
replace(pi, vertex);
}
Components::iterator ci;

View File

@ -944,7 +944,9 @@ rebuild_vertex_pool(EggVertexPool *vertex_pool, bool recurse) {
Vertices::const_iterator vi;
for (vi = vertices.begin(); vi != vertices.end(); ++vi) {
EggVertex *vertex = (*vi);
prim->add_vertex(vertex_pool->create_unique_vertex(*vertex));
EggVertex *new_vertex = vertex_pool->create_unique_vertex(*vertex);
new_vertex->copy_grefs_from(*vertex);
prim->add_vertex(new_vertex);
}
for (i = 0; i < num_components; i++) {
prim->set_component(i, &attributes[i]);
@ -965,7 +967,9 @@ rebuild_vertex_pool(EggVertexPool *vertex_pool, bool recurse) {
Vertices::const_iterator vi;
for (vi = vertices.begin(); vi != vertices.end(); ++vi) {
EggVertex *vertex = (*vi);
prim->add_vertex(vertex_pool->create_unique_vertex(*vertex));
EggVertex *new_vertex = vertex_pool->create_unique_vertex(*vertex);
new_vertex->copy_grefs_from(*vertex);
prim->add_vertex(new_vertex);
}
} else if (child->is_of_type(EggGroupNode::get_class_type())) {

View File

@ -375,6 +375,7 @@ unify_attributes(EggPrimitive::Shading shading) {
EggVertexPool *vertex_pool = orig_vertex->get_pool();
nassertv(vertex_pool != (EggVertexPool *)NULL);
vertex = vertex_pool->create_unique_vertex(*vertex);
vertex->copy_grefs_from(*orig_vertex);
replace(pi, vertex);
}
clear_normal();
@ -406,6 +407,7 @@ unify_attributes(EggPrimitive::Shading shading) {
EggVertexPool *vertex_pool = orig_vertex->get_pool();
nassertv(vertex_pool != (EggVertexPool *)NULL);
vertex = vertex_pool->create_unique_vertex(*vertex);
vertex->copy_grefs_from(*orig_vertex);
replace(pi, vertex);
}
}
@ -1123,6 +1125,7 @@ do_apply_flat_attribute(int vertex_index, EggAttributes *attrib) {
if (significant_change) {
new_vertex = get_pool()->create_unique_vertex(*new_vertex);
new_vertex->copy_grefs_from(*orig_vertex);
set_vertex(vertex_index, new_vertex);
} else {
// Just copy the new attributes back into the pool.

View File

@ -19,10 +19,11 @@
#include "characterMaker.h"
#include "eggLoader.h"
#include "config_egg2pg.h"
#include "eggBinner.h"
#include "computedVertices.h"
#include "eggGroup.h"
#include "eggPrimitive.h"
#include "eggBin.h"
#include "partGroup.h"
#include "characterJoint.h"
#include "characterJointBundle.h"
@ -32,6 +33,8 @@
#include "eggSurface.h"
#include "eggCurve.h"
#include "modelNode.h"
#include "jointVertexTransform.h"
#include "userVertexTransform.h"
////////////////////////////////////////////////////////////////////
// Function: CharacterMaker::Construtor
@ -61,6 +64,15 @@ make_node() {
return _character_node;
}
////////////////////////////////////////////////////////////////////
// Function: CharacterMaker::get_name
// Access: Public
// Description: Returns the name of the character.
////////////////////////////////////////////////////////////////////
string CharacterMaker::
get_name() const {
return _egg_root->get_name();
}
////////////////////////////////////////////////////////////////////
// Function: CharacterMaker::egg_to_part
@ -82,6 +94,38 @@ egg_to_part(EggNode *egg_node) const {
return _parts[index];
}
////////////////////////////////////////////////////////////////////
// Function: CharacterMaker::egg_to_transform
// Access: Public
// Description: Returns a JointVertexTransform suitable for
// applying the animation associated with the given
// egg node (which should be a joint). Returns an
// identity transform if the egg node is not a joint in
// the character's hierarchy.
////////////////////////////////////////////////////////////////////
VertexTransform *CharacterMaker::
egg_to_transform(EggNode *egg_node) {
int index = egg_to_index(egg_node);
if (index < 0) {
// Not a joint in the hierarchy.
return get_identity_transform();
}
VertexTransforms::iterator vi = _vertex_transforms.find(index);
if (vi != _vertex_transforms.end()) {
return (*vi).second;
}
PartGroup *part = _parts[index];
CharacterJoint *joint;
DCAST_INTO_R(joint, part, get_identity_transform());
PT(VertexTransform) vt = new JointVertexTransform(joint);
_vertex_transforms[index] = vt;
return vt;
}
////////////////////////////////////////////////////////////////////
// Function: CharacterMaker::egg_to_index
// Access: Public
@ -148,12 +192,19 @@ make_bundle() {
build_joint_hierarchy(_egg_root, _skeleton_root);
_bundle->sort_descendants();
if (use_qpgeom) {
// The new, experimental Geom system.
make_qpgeometry(_egg_root);
} else {
// The old Geom system.
make_geometry(_egg_root);
_character_node->_computed_vertices =
_comp_verts_maker.make_computed_vertices(_character_node, *this);
}
parent_joint_nodes(_skeleton_root);
make_geometry(_egg_root);
_character_node->_computed_vertices =
_comp_verts_maker.make_computed_vertices(_character_node, *this);
return _bundle;
}
@ -276,6 +327,57 @@ make_geometry(EggNode *egg_node) {
}
}
////////////////////////////////////////////////////////////////////
// Function: CharacterMaker::make_qpgeometry
// Access: Private
// Description: Walks the hierarchy, looking for bins that represent
// polysets, which are to be animated with the
// character. Invokes the egg loader to create the
// animated geometry.
//
// This is part of the experimental Geom rewrite.
////////////////////////////////////////////////////////////////////
void CharacterMaker::
make_qpgeometry(EggNode *egg_node) {
if (egg_node->is_of_type(EggBin::get_class_type())) {
EggBin *egg_bin = DCAST(EggBin, egg_node);
if (!egg_bin->empty() &&
egg_bin->get_bin_number() == EggBinner::BN_polyset) {
EggGroupNode *bin_home = determine_bin_home(egg_bin);
bool is_dynamic;
if (bin_home == (EggGroupNode *)NULL) {
// This is a dynamic polyset that lives under the character's
// root node.
bin_home = _egg_root;
is_dynamic = true;
} else {
// This is a totally static polyset that is parented under
// some animated joint node.
is_dynamic = false;
}
PandaNode *parent = part_to_node(egg_to_part(bin_home));
LMatrix4d transform =
egg_bin->get_vertex_frame() *
bin_home->get_node_frame_inv();
_loader.make_polyset(egg_bin, parent, &transform, is_dynamic,
this);
}
}
if (egg_node->is_of_type(EggGroupNode::get_class_type())) {
EggGroupNode *egg_group = DCAST(EggGroupNode, egg_node);
EggGroupNode::const_iterator ci;
for (ci = egg_group->begin(); ci != egg_group->end(); ++ci) {
make_qpgeometry(*ci);
}
}
}
////////////////////////////////////////////////////////////////////
// Function: CharacterMaker::make_static_primitive
// Access: Private
@ -419,3 +521,142 @@ determine_primitive_home(EggPrimitive *egg_primitive) {
// explicit joint assignment.
return home;
}
////////////////////////////////////////////////////////////////////
// Function: CharacterMaker::determine_bin_home
// Access: Private
// Description: Examines the joint assignment of the vertices of all
// of the primitives within this bin to determine which
// parent node the bin's polyset should be created
// under.
////////////////////////////////////////////////////////////////////
EggGroupNode *CharacterMaker::
determine_bin_home(EggBin *egg_bin) {
// A primitive's vertices may be referenced by any joint in the
// character. Or, the primitive itself may be explicitly placed
// under a joint.
// If any of the vertices, in any primitive, are referenced by
// multiple joints, or if any two vertices are referenced by
// different joints, then the entire bin must be considered dynamic.
// (We'll indicate a dynamic bin by returning NULL.)
// We need to keep track of the one joint we've encountered so far,
// to see if all the vertices are referenced by the same joint.
EggGroupNode *home = NULL;
EggGroupNode::const_iterator ci;
for (ci = egg_bin->begin(); ci != egg_bin->end(); ++ci) {
CPT(EggPrimitive) egg_primitive = DCAST(EggPrimitive, (*ci));
EggPrimitive::const_iterator vi;
for (vi = egg_primitive->begin();
vi != egg_primitive->end();
++vi) {
EggVertex *vertex = (*vi);
if (vertex->gref_size() > 1) {
// This vertex is referenced by multiple joints; the primitive
// is dynamic.
return NULL;
}
if (!vertex->_dxyzs.empty() ||
!vertex->_dnormals.empty() ||
!vertex->_drgbas.empty()) {
// This vertex has some morph slider definitions; therefore, the
// primitive is dynamic.
return NULL;
}
EggVertex::const_uv_iterator uvi;
for (uvi = vertex->uv_begin(); uvi != vertex->uv_end(); ++uvi) {
if (!(*uvi)->_duvs.empty()) {
// Ditto: the vertex has some UV morphs; therefore the
// primitive is dynamic.
return NULL;
}
}
EggGroupNode *vertex_home;
if (vertex->gref_size() == 0) {
// This vertex is not referenced at all, which means it belongs
// right where it is.
vertex_home = egg_primitive->get_parent();
} else {
nassertr(vertex->gref_size() == 1, NULL);
// This vertex is referenced exactly once.
vertex_home = *vertex->gref_begin();
}
if (home != NULL && home != vertex_home) {
// Oops, two vertices are referenced by different joints! The
// primitive is dynamic.
return NULL;
}
home = vertex_home;
}
}
// This shouldn't be possible, unless there are no vertices--but we
// eliminate invalid primitives before we begin, so all primitives
// should have vertices, and all bins should have primitives.
nassertr(home != NULL, NULL);
// So, all the vertices are assigned to the same group. This means
// all the primitives in the bin belong entirely to one joint.
// If the group is not, in fact, a joint then we return the first
// joint above the group.
EggGroup *egg_group = (EggGroup *)NULL;
if (home->is_of_type(EggGroup::get_class_type())) {
egg_group = DCAST(EggGroup, home);
}
while (egg_group != (EggGroup *)NULL &&
egg_group->get_group_type() != EggGroup::GT_joint &&
egg_group->get_dart_type() == EggGroup::DT_none) {
nassertr(egg_group->get_parent() != (EggGroupNode *)NULL, NULL);
home = egg_group->get_parent();
egg_group = (EggGroup *)NULL;
if (home->is_of_type(EggGroup::get_class_type())) {
egg_group = DCAST(EggGroup, home);
}
}
if (egg_group != (EggGroup *)NULL &&
egg_group->get_group_type() == EggGroup::GT_joint &&
egg_group->get_dcs_type() == EggGroup::DC_none) {
// If we have rigid geometry that is assigned to a joint without a
// <DCS> flag, which means the joint didn't get created as its own
// node, go ahead and make an implicit <DCS> flag for the joint.
// The alternative is to return NULL to treat the geometry as
// dynamic (and animate it by animating its vertices), but display
// lists and vertex buffers will perform better if as much
// geometry as possible is rigid.
egg_group->set_dcs_type(EggGroup::DC_default);
PT(ModelNode) geom_node = new ModelNode(egg_group->get_name());
geom_node->set_preserve_transform(ModelNode::PT_local);
CharacterJoint *joint;
DCAST_INTO_R(joint, egg_to_part(egg_group), home);
joint->_geom_node = geom_node.p();
}
return home;
}
////////////////////////////////////////////////////////////////////
// Function: CharacterMaker::get_identity_transform
// Access: Private
// Description: Returns a VertexTransform that represents the root of
// the character--it never animates.
////////////////////////////////////////////////////////////////////
VertexTransform *CharacterMaker::
get_identity_transform() {
if (_identity_transform == (VertexTransform *)NULL) {
_identity_transform = new UserVertexTransform("root");
}
return _identity_transform;
}

View File

@ -22,7 +22,7 @@
#include "pandabase.h"
#include "computedVerticesMaker.h"
#include "vertexTransform.h"
#include "vector_PartGroupStar.h"
#include "typedef.h"
#include "pmap.h"
@ -31,6 +31,7 @@ class EggNode;
class EggGroup;
class EggGroupNode;
class EggPrimitive;
class EggBin;
class PartGroup;
class CharacterJointBundle;
class Character;
@ -51,7 +52,9 @@ public:
Character *make_node();
string get_name() const;
PartGroup *egg_to_part(EggNode *egg_node) const;
VertexTransform *egg_to_transform(EggNode *egg_node);
int egg_to_index(EggNode *egg_node) const;
PandaNode *part_to_node(PartGroup *part) const;
@ -63,12 +66,15 @@ private:
void parent_joint_nodes(PartGroup *part);
void make_geometry(EggNode *egg_node);
void make_qpgeometry(EggNode *egg_node);
void make_static_primitive(EggPrimitive *egg_primitive,
EggGroupNode *prim_home);
void make_dynamic_primitive(EggPrimitive *egg_primitive,
EggGroupNode *prim_home);
EggGroupNode *determine_primitive_home(EggPrimitive *egg_primitive);
EggGroupNode *determine_bin_home(EggBin *egg_bin);
VertexTransform *get_identity_transform();
typedef pmap<EggNode *, int> NodeMap;
NodeMap _node_map;
@ -76,6 +82,10 @@ private:
typedef vector_PartGroupStar Parts;
Parts _parts;
typedef pmap<int, PT(VertexTransform) > VertexTransforms;
VertexTransforms _vertex_transforms;
PT(VertexTransform) _identity_transform;
EggLoader &_loader;
EggGroup *_egg_root;
Character *_character_node;

View File

@ -89,6 +89,8 @@
#include "sheetNode.h"
#include "look_at.h"
#include "configVariableString.h"
#include "transformBlendPalette.h"
#include "transformBlend.h"
#include <ctype.h>
#include <algorithm>
@ -527,6 +529,135 @@ make_indexed_primitive(EggPrimitive *egg_prim, PandaNode *parent,
_builder.add_prim(bucket, bprim);
}
////////////////////////////////////////////////////////////////////
// Function: EggLoader::make_polyset
// Access: Public
// Description: Creates a polyset--that is, a Geom--from the
// primitives that have already been grouped into a bin.
// If transform is non-NULL, it represents the transform
// to apply to the vertices (instead of the default
// transform based on the bin's position within the
// hierarchy).
////////////////////////////////////////////////////////////////////
void EggLoader::
make_polyset(EggBin *egg_bin, PandaNode *parent, const LMatrix4d *transform,
bool is_dynamic, CharacterMaker *character_maker) {
if (egg_bin->empty()) {
// If there are no children--no primitives--never mind.
return;
}
// We know that all of the primitives in the bin have the same
// render state, so we can get that information from the first
// primitive.
EggGroupNode::const_iterator ci = egg_bin->begin();
nassertv(ci != egg_bin->end());
CPT(EggPrimitive) first_prim = DCAST(EggPrimitive, (*ci));
nassertv(first_prim != (EggPrimitive *)NULL);
const EggRenderState *render_state;
DCAST_INTO_V(render_state, first_prim->get_user_data(EggRenderState::get_class_type()));
if (render_state->_hidden && egg_suppress_hidden) {
// Eat this polyset.
return;
}
if (!use_qpgeom) {
// In the old Geom system, just send each primitive to the
// Builder.
for (ci = egg_bin->begin(); ci != egg_bin->end(); ++ci) {
EggPrimitive *egg_prim;
DCAST_INTO_V(egg_prim, (*ci));
make_nonindexed_primitive(egg_prim, parent, transform, _comp_verts_maker);
}
return;
}
// Generate an optimal vertex pool for the polygons within just the
// bin (which translates directly to an optimal GeomVertexData
// structure).
PT(EggVertexPool) vertex_pool = new EggVertexPool("bin");
egg_bin->rebuild_vertex_pool(vertex_pool, false);
if (egg_mesh) {
// If we're using the mesher, mesh now.
egg_bin->mesh_triangles(0);
} else {
// If we're not using the mesher, at least triangulate any
// higher-order polygons we might have.
egg_bin->triangulate_polygons(EggGroupNode::T_polygon | EggGroupNode::T_convex);
}
// Now that we've meshed, apply the per-prim attributes onto the
// vertices, so we can copy them to the GeomVertexData.
egg_bin->apply_last_attribute(false);
egg_bin->post_apply_flat_attribute(false);
vertex_pool->remove_unused_vertices();
// vertex_pool->write(cerr, 0);
// egg_bin->write(cerr, 0);
// Now create a handful of GeomPrimitives corresponding to the
// various types of primitives we have.
Primitives primitives;
for (ci = egg_bin->begin(); ci != egg_bin->end(); ++ci) {
EggPrimitive *egg_prim;
DCAST_INTO_V(egg_prim, (*ci));
make_primitive(render_state, egg_prim, primitives);
}
if (!primitives.empty()) {
LMatrix4d mat;
if (transform != NULL) {
mat = (*transform);
} else {
mat = egg_bin->get_vertex_to_node();
}
// Now convert the vertex pool to a GeomVertexData.
nassertv(vertex_pool != (EggVertexPool *)NULL);
PT(qpGeomVertexData) vertex_data =
make_vertex_data(render_state, vertex_pool, mat,
is_dynamic, character_maker);
nassertv(vertex_data != (qpGeomVertexData *)NULL);
// And create a Geom to hold the primitives.
PT(qpGeom) geom = new qpGeom;
geom->set_vertex_data(vertex_data);
// Add each new primitive to the Geom.
Primitives::const_iterator pi;
for (pi = primitives.begin(); pi != primitives.end(); ++pi) {
qpGeomPrimitive *primitive = (*pi).second;
geom->add_primitive(primitive);
}
// vertex_data->write(cerr);
// geom->write(cerr);
// render_state->_state->write(cerr, 0);
// Now, is our parent node a GeomNode, or just an ordinary
// PandaNode? If it's a GeomNode, we can add the new Geom directly
// to our parent; otherwise, we need to create a new node.
if (parent->is_geom_node() && !render_state->_hidden) {
DCAST(GeomNode, parent)->add_geom(geom, render_state->_state);
} else {
PT(GeomNode) geom_node = new GeomNode(egg_bin->get_name());
if (render_state->_hidden) {
parent->add_stashed(geom_node);
} else {
parent->add_child(geom_node);
}
geom_node->add_geom(geom, render_state->_state);
}
}
return;
}
////////////////////////////////////////////////////////////////////
// Function: EggLoader::make_nurbs_curve
// Access: Private
@ -1404,7 +1535,8 @@ make_node(EggBin *egg_bin, PandaNode *parent) {
// node (a parent of one or more similar EggPrimitives).
switch (egg_bin->get_bin_number()) {
case EggBinner::BN_polyset:
return make_polyset(egg_bin, parent);
make_polyset(egg_bin, parent, NULL, false, NULL);
return NULL;
case EggBinner::BN_lod:
return make_lod(egg_bin, parent);
@ -1417,122 +1549,6 @@ make_node(EggBin *egg_bin, PandaNode *parent) {
return (PandaNode *)NULL;
}
////////////////////////////////////////////////////////////////////
// Function: EggLoader::make_polyset
// Access: Private
// Description:
////////////////////////////////////////////////////////////////////
PandaNode *EggLoader::
make_polyset(EggBin *egg_bin, PandaNode *parent) {
if (egg_bin->empty()) {
// If there are no children--no primitives--never mind.
return NULL;
}
// We know that all of the primitives in the bin have the same
// render state, so we can get that information from the first
// primitive.
EggGroup::const_iterator ci = egg_bin->begin();
nassertr(ci != egg_bin->end(), NULL);
CPT(EggPrimitive) first_prim = DCAST(EggPrimitive, (*ci));
nassertr(first_prim != (EggPrimitive *)NULL, NULL);
const EggRenderState *render_state;
DCAST_INTO_R(render_state, first_prim->get_user_data(EggRenderState::get_class_type()), NULL);
if (render_state->_hidden && egg_suppress_hidden) {
// Eat this polyset.
return NULL;
}
if (!use_qpgeom) {
// In the old Geom system, just send each primitive to the
// Builder.
for (ci = egg_bin->begin(); ci != egg_bin->end(); ++ci) {
EggPrimitive *egg_prim;
DCAST_INTO_R(egg_prim, (*ci), NULL);
make_nonindexed_primitive(egg_prim, parent, NULL, _comp_verts_maker);
}
return NULL;
}
// Generate an optimal vertex pool for the polygons within just the
// bin (which translates directly to an optimal GeomVertexData
// structure).
PT(EggVertexPool) vertex_pool = new EggVertexPool("bin");
egg_bin->rebuild_vertex_pool(vertex_pool, false);
if (egg_mesh) {
// If we're using the mesher, mesh now.
egg_bin->mesh_triangles(0);
} else {
// If we're not using the mesher, at least triangulate any
// higher-order polygons we might have.
egg_bin->triangulate_polygons(EggGroupNode::T_polygon | EggGroupNode::T_convex);
}
// Now that we've meshed, apply the per-prim attributes onto the
// vertices, so we can copy them to the GeomVertexData.
egg_bin->apply_last_attribute(false);
egg_bin->post_apply_flat_attribute(false);
vertex_pool->remove_unused_vertices();
// vertex_pool->write(cerr, 0);
// egg_bin->write(cerr, 0);
// Now create a handful of GeomPrimitives corresponding to the
// various types of primitives we have.
Primitives primitives;
for (ci = egg_bin->begin(); ci != egg_bin->end(); ++ci) {
EggPrimitive *egg_prim;
DCAST_INTO_R(egg_prim, (*ci), NULL);
make_primitive(render_state, egg_prim, primitives);
}
if (!primitives.empty()) {
// Now convert the vertex pool to a GeomVertexData.
nassertr(vertex_pool != (EggVertexPool *)NULL, NULL);
PT(qpGeomVertexData) vertex_data =
make_vertex_data(render_state, vertex_pool,
egg_bin->get_vertex_to_node());
nassertr(vertex_data != (qpGeomVertexData *)NULL, NULL);
// And create a Geom to hold the primitives.
PT(qpGeom) geom = new qpGeom;
geom->set_vertex_data(vertex_data);
// Add each new primitive to the Geom.
Primitives::const_iterator pi;
for (pi = primitives.begin(); pi != primitives.end(); ++pi) {
qpGeomPrimitive *primitive = (*pi).second;
geom->add_primitive(primitive);
}
// vertex_data->write(cerr);
// geom->write(cerr);
// render_state->_state->write(cerr, 0);
// Now, is our parent node a GeomNode, or just an ordinary
// PandaNode? If it's a GeomNode, we can add the new Geom directly
// to our parent; otherwise, we need to create a new node.
if (parent->is_geom_node() && !render_state->_hidden) {
DCAST(GeomNode, parent)->add_geom(geom, render_state->_state);
} else {
PT(GeomNode) geom_node = new GeomNode(egg_bin->get_name());
if (render_state->_hidden) {
parent->add_stashed(geom_node);
} else {
parent->add_child(geom_node);
}
geom_node->add_geom(geom, render_state->_state);
}
}
return NULL;
}
////////////////////////////////////////////////////////////////////
// Function: EggLoader::make_lod
// Access: Private
@ -1888,7 +1904,8 @@ check_for_polysets(EggGroup *egg_group, bool &all_polysets, bool &any_hidden) {
////////////////////////////////////////////////////////////////////
PT(qpGeomVertexData) EggLoader::
make_vertex_data(const EggRenderState *render_state,
EggVertexPool *vertex_pool, const LMatrix4d &transform) {
EggVertexPool *vertex_pool, const LMatrix4d &transform,
bool is_dynamic, CharacterMaker *character_maker) {
VertexPoolTransform vpt;
vpt._vertex_pool = vertex_pool;
vpt._bake_in_uvs = render_state->_bake_in_uvs;
@ -1928,12 +1945,39 @@ make_vertex_data(const EggRenderState *render_state,
array_format->add_data_type(iname, 2, qpGeomVertexDataType::NT_float32);
}
CPT(qpGeomVertexFormat) format =
qpGeomVertexFormat::register_format(new qpGeomVertexFormat(array_format));
PT(qpGeomVertexFormat) temp_format = new qpGeomVertexFormat(array_format);
// Now create a new GeomVertexData using the indicated format.
PT(qpGeomVertexData) vertex_data =
new qpGeomVertexData(format, qpGeomUsageHint::UH_static);
PT(TransformBlendPalette) blend_palette;
string name;
if (is_dynamic) {
// If it's a dynamic object, we need a TransformBlendPalette, and
// another array that indexes into the palette per vertex.
blend_palette = new TransformBlendPalette;
PT(qpGeomVertexArrayFormat) blend_array_format = new qpGeomVertexArrayFormat;
blend_array_format->add_data_type
(InternalName::get_transform_blend(), 1, qpGeomVertexDataType::NT_uint16);
temp_format->add_array(blend_array_format);
// We'll also assign the character name to the vertex data, so it
// will show up in PStats.
name = character_maker->get_name();
}
CPT(qpGeomVertexFormat) format =
qpGeomVertexFormat::register_format(temp_format);
// Now create a new GeomVertexData using the indicated format. It
// is actually correct to create it with UH_static even it
// represents a dynamic object, because the vertex data itself won't
// be changing--just the result of applying the animation is
// dynamic.
PT(qpGeomVertexData) vertex_data =
new qpGeomVertexData(name, format, qpGeomUsageHint::UH_static);
if (is_dynamic) {
vertex_data->set_transform_blend_palette(blend_palette);
}
// And fill the data from the vertex pool.
EggVertexPool::const_iterator vi;
@ -1975,6 +2019,25 @@ make_vertex_data(const EggRenderState *render_state,
gvi.set_data2f(LCAST(float, uv));
}
if (is_dynamic) {
// Figure out the transforms affecting this particular vertex.
TransformBlend blend;
EggVertex::GroupRef::const_iterator gri;
for (gri = vertex->gref_begin(); gri != vertex->gref_end(); ++gri) {
EggGroup *egg_joint = (*gri);
double membership = egg_joint->get_vertex_membership(vertex);
PT(VertexTransform) vt = character_maker->egg_to_transform(egg_joint);
nassertr(vt != (VertexTransform *)NULL, vertex_data);
blend.add_transform(vt, membership);
}
blend.normalize_weights();
int palette_index = blend_palette->add_blend(blend);
gvi.set_data_type(InternalName::get_transform_blend());
gvi.set_data1i(palette_index);
}
}
bool inserted = _vertex_pool_data.insert

View File

@ -58,6 +58,7 @@ class CollisionPolygon;
class PortalNode;
class PolylightNode;
class EggRenderState;
class CharacterMaker;
///////////////////////////////////////////////////////////////////
// Class : EggLoader
@ -83,6 +84,10 @@ public:
const LMatrix4d *transform,
ComputedVerticesMaker &comp_verts_maker);
void make_polyset(EggBin *egg_bin, PandaNode *parent,
const LMatrix4d *transform, bool is_dynamic,
CharacterMaker *character_maker);
private:
class TextureDef {
public:
@ -137,9 +142,10 @@ private:
void check_for_polysets(EggGroup *egg_group, bool &all_polysets,
bool &any_hidden);
PT(qpGeomVertexData) make_vertex_data(const EggRenderState *render_state,
EggVertexPool *vertex_pool,
const LMatrix4d &transform);
PT(qpGeomVertexData) make_vertex_data
(const EggRenderState *render_state, EggVertexPool *vertex_pool,
const LMatrix4d &transform, bool is_dynamic,
CharacterMaker *character_maker);
void make_primitive(const EggRenderState *render_state,
EggPrimitive *egg_prim, Primitives &primitives);

View File

@ -633,7 +633,7 @@ load_default_model(const NodePath &parent) {
if (use_qpgeom) {
// New, experimental Geom code.
PT(qpGeomVertexData) vdata = new qpGeomVertexData
(qpGeomVertexFormat::get_v3n3cpt2(),
(string(), qpGeomVertexFormat::get_v3n3cpt2(),
qpGeomUsageHint::UH_static);
qpGeomVertexIterator vertex(vdata, InternalName::get_vertex());
qpGeomVertexIterator normal(vdata, InternalName::get_normal());
@ -1060,7 +1060,7 @@ load_image_as_model(const Filename &filename) {
if (use_qpgeom) {
PT(qpGeomVertexData) vdata = new qpGeomVertexData
(qpGeomVertexFormat::get_v3t2(),
(string(), qpGeomVertexFormat::get_v3t2(),
qpGeomUsageHint::UH_static);
qpGeomVertexIterator vertex(vdata, InternalName::get_vertex());
qpGeomVertexIterator texcoord(vdata, InternalName::get_texcoord());

View File

@ -17,6 +17,30 @@
////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////
// Function: qpGeomVertexData::get_name
// Access: Published
// Description: Returns the name passed to the constructor, if any.
// This name is reported on the PStats graph for vertex
// computations.
////////////////////////////////////////////////////////////////////
INLINE const string &qpGeomVertexData::
get_name() const {
return _name;
}
////////////////////////////////////////////////////////////////////
// Function: qpGeomVertexData::set_name
// Access: Published
// Description: Changes the name of the vertex data. This name is
// reported on the PStats graph for vertex computations.
////////////////////////////////////////////////////////////////////
INLINE void qpGeomVertexData::
set_name(const string &name) {
_name = name;
_this_animate_vertices_pcollector = PStatCollector(_animate_vertices_pcollector, name);
}
////////////////////////////////////////////////////////////////////
// Function: qpGeomVertexData::get_format
// Access: Published
@ -154,8 +178,8 @@ INLINE qpGeomVertexData::CData::
CData(const qpGeomVertexData::CData &copy) :
_arrays(copy._arrays),
_transform_blend_palette(copy._transform_blend_palette),
_computed_vertices(copy._computed_vertices),
_computed_vertices_modified(copy._computed_vertices_modified),
_animated_vertices(copy._animated_vertices),
_animated_vertices_modified(copy._animated_vertices_modified),
_modified(copy._modified)
{
}

View File

@ -22,13 +22,14 @@
#include "bamReader.h"
#include "bamWriter.h"
#include "pset.h"
#include "indent.h"
TypeHandle qpGeomVertexData::_type_handle;
PStatCollector qpGeomVertexData::_convert_pcollector("Cull:Munge:Convert");
PStatCollector qpGeomVertexData::_scale_color_pcollector("Cull:Munge:Scale color");
PStatCollector qpGeomVertexData::_set_color_pcollector("Cull:Munge:Set color");
PStatCollector qpGeomVertexData::_compute_vertices_pcollector("Cull:Compute vertices");
PStatCollector qpGeomVertexData::_animate_vertices_pcollector("Cull:Animate vertices");
////////////////////////////////////////////////////////////////////
// Function: qpGeomVertexData::Default Constructor
@ -37,7 +38,9 @@ PStatCollector qpGeomVertexData::_compute_vertices_pcollector("Cull:Compute vert
// reading from the bam file.
////////////////////////////////////////////////////////////////////
qpGeomVertexData::
qpGeomVertexData() {
qpGeomVertexData() :
_this_animate_vertices_pcollector(_animate_vertices_pcollector)
{
}
////////////////////////////////////////////////////////////////////
@ -46,10 +49,13 @@ qpGeomVertexData() {
// Description:
////////////////////////////////////////////////////////////////////
qpGeomVertexData::
qpGeomVertexData(const qpGeomVertexFormat *format,
qpGeomVertexData(const string &name,
const qpGeomVertexFormat *format,
qpGeomUsageHint::UsageHint usage_hint) :
_name(name),
_format(format),
_usage_hint(usage_hint)
_usage_hint(usage_hint),
_this_animate_vertices_pcollector(_animate_vertices_pcollector, name)
{
nassertv(_format->is_registered());
@ -72,8 +78,10 @@ qpGeomVertexData(const qpGeomVertexFormat *format,
qpGeomVertexData::
qpGeomVertexData(const qpGeomVertexData &copy) :
TypedWritableReferenceCount(copy),
_name(copy._name),
_format(copy._format),
_cycler(copy._cycler)
_cycler(copy._cycler),
_this_animate_vertices_pcollector(copy._this_animate_vertices_pcollector)
{
}
@ -85,8 +93,10 @@ qpGeomVertexData(const qpGeomVertexData &copy) :
void qpGeomVertexData::
operator = (const qpGeomVertexData &copy) {
TypedWritableReferenceCount::operator = (copy);
_name = copy._name;
_format = copy._format;
_cycler = copy._cycler;
_this_animate_vertices_pcollector = copy._this_animate_vertices_pcollector;
}
////////////////////////////////////////////////////////////////////
@ -138,7 +148,7 @@ clear_vertices() {
(*ai)->clear_vertices();
}
cdata->_modified = qpGeom::get_next_modified();
cdata->_computed_vertices.clear();
cdata->_animated_vertices.clear();
}
////////////////////////////////////////////////////////////////////
@ -162,7 +172,7 @@ modify_array(int i) {
cdata->_arrays[i] = new qpGeomVertexArrayData(*cdata->_arrays[i]);
}
cdata->_modified = qpGeom::get_next_modified();
cdata->_computed_vertices_modified = UpdateSeq();
cdata->_animated_vertices_modified = UpdateSeq();
return cdata->_arrays[i];
}
@ -181,7 +191,7 @@ set_array(int i, const qpGeomVertexArrayData *array) {
nassertv(i >= 0 && i < (int)cdata->_arrays.size());
cdata->_arrays[i] = (qpGeomVertexArrayData *)array;
cdata->_modified = qpGeom::get_next_modified();
cdata->_computed_vertices_modified = UpdateSeq();
cdata->_animated_vertices_modified = UpdateSeq();
}
////////////////////////////////////////////////////////////////////
@ -203,7 +213,7 @@ modify_transform_blend_palette() {
cdata->_transform_blend_palette = new TransformBlendPalette(*cdata->_transform_blend_palette);
}
cdata->_modified = qpGeom::get_next_modified();
cdata->_computed_vertices_modified = UpdateSeq();
cdata->_animated_vertices_modified = UpdateSeq();
return cdata->_transform_blend_palette;
}
@ -222,7 +232,7 @@ set_transform_blend_palette(const TransformBlendPalette *palette) {
CDWriter cdata(_cycler);
cdata->_transform_blend_palette = (TransformBlendPalette *)palette;
cdata->_modified = qpGeom::get_next_modified();
cdata->_computed_vertices_modified = UpdateSeq();
cdata->_animated_vertices_modified = UpdateSeq();
}
////////////////////////////////////////////////////////////////////
@ -270,7 +280,7 @@ convert_to(const qpGeomVertexFormat *new_format) const {
PStatTimer timer(_convert_pcollector);
PT(qpGeomVertexData) new_data =
new qpGeomVertexData(new_format, get_usage_hint());
new qpGeomVertexData(get_name(), new_format, get_usage_hint());
new_data->set_transform_blend_palette(get_transform_blend_palette());
pset<int> done_arrays;
@ -420,7 +430,7 @@ set_color(const Colorf &color, int num_components,
}
////////////////////////////////////////////////////////////////////
// Function: qpGeomVertexData::compute_vertices
// Function: qpGeomVertexData::animate_vertices
// Access: Published
// Description: Returns a GeomVertexData that represents the results
// of computing the vertex animation on the CPU for this
@ -438,27 +448,27 @@ set_color(const Colorf &color, int num_components,
// graphics backend to update vertex buffers optimally).
////////////////////////////////////////////////////////////////////
CPT(qpGeomVertexData) qpGeomVertexData::
compute_vertices() const {
animate_vertices() const {
CDReader cdata(_cycler);
if (cdata->_transform_blend_palette == (TransformBlendPalette *)NULL) {
// No vertex animation.
return this;
}
if (cdata->_computed_vertices == (qpGeomVertexData *)NULL) {
if (cdata->_animated_vertices == (qpGeomVertexData *)NULL) {
CDWriter cdataw(((qpGeomVertexData *)this)->_cycler, cdata);
((qpGeomVertexData *)this)->make_computed_vertices(cdataw);
return cdataw->_computed_vertices;
((qpGeomVertexData *)this)->make_animated_vertices(cdataw);
return cdataw->_animated_vertices;
} else {
UpdateSeq blend_modified = cdata->_transform_blend_palette->get_modified();
if (cdata->_computed_vertices_modified == blend_modified) {
if (cdata->_animated_vertices_modified == blend_modified) {
// No changes.
return cdata->_computed_vertices;
return cdata->_animated_vertices;
}
CDWriter cdataw(((qpGeomVertexData *)this)->_cycler, cdata);
cdataw->_computed_vertices_modified = blend_modified;
((qpGeomVertexData *)this)->update_computed_vertices(cdataw);
return cdataw->_computed_vertices;
cdataw->_animated_vertices_modified = blend_modified;
((qpGeomVertexData *)this)->update_animated_vertices(cdataw);
return cdataw->_animated_vertices;
}
}
@ -510,7 +520,8 @@ replace_data_type(const InternalName *name, int num_components,
}
PT(qpGeomVertexData) new_data =
new qpGeomVertexData(qpGeomVertexFormat::register_format(new_format),
new qpGeomVertexData(get_name(),
qpGeomVertexFormat::register_format(new_format),
usage_hint);
if (keep_animation) {
new_data->set_transform_blend_palette(get_transform_blend_palette());
@ -567,6 +578,11 @@ output(ostream &out) const {
void qpGeomVertexData::
write(ostream &out, int indent_level) const {
_format->write_with_data(out, indent_level, this);
if (get_transform_blend_palette() != (TransformBlendPalette *)NULL) {
indent(out, indent_level)
<< "Transform blend palette:\n";
get_transform_blend_palette()->write(out, indent_level + 2);
}
}
////////////////////////////////////////////////////////////////////
@ -1155,63 +1171,80 @@ do_set_num_vertices(int n, qpGeomVertexData::CDWriter &cdata) {
if (any_changed) {
cdata->_modified = qpGeom::get_next_modified();
cdata->_computed_vertices.clear();
cdata->_animated_vertices.clear();
}
return any_changed;
}
////////////////////////////////////////////////////////////////////
// Function: qpGeomVertexData::make_computed_vertices
// Function: qpGeomVertexData::make_animated_vertices
// Access: Private
// Description: Creates the GeomVertexData that represents the
// results of computing the vertex animation on the CPU.
////////////////////////////////////////////////////////////////////
void qpGeomVertexData::
make_computed_vertices(qpGeomVertexData::CDWriter &cdata) {
make_animated_vertices(qpGeomVertexData::CDWriter &cdata) {
// First, make a new format that doesn't have the transform_blend
// array.
cdata->_computed_vertices = replace_data_type
cdata->_animated_vertices = replace_data_type
(InternalName::get_transform_blend(), 0, qpGeomVertexDataType::NT_uint16,
min(get_usage_hint(), qpGeomUsageHint::UH_dynamic), false);
// Now fill it up with the appropriate data.
update_computed_vertices(cdata);
update_animated_vertices(cdata);
}
////////////////////////////////////////////////////////////////////
// Function: qpGeomVertexData::update_computed_vertices
// Function: qpGeomVertexData::update_animated_vertices
// Access: Private
// Description: Recomputes the results of computing the vertex
// animation on the CPU, and applies them to the
// existing computed_vertices object.
// existing animated_vertices object.
////////////////////////////////////////////////////////////////////
void qpGeomVertexData::
update_computed_vertices(qpGeomVertexData::CDWriter &cdata) {
update_animated_vertices(qpGeomVertexData::CDWriter &cdata) {
int num_vertices = get_num_vertices();
if (gobj_cat.is_debug()) {
gobj_cat.debug()
<< "Computing " << num_vertices << " vertices.\n";
<< "Animating " << num_vertices << " vertices for " << get_name()
<< "\n";
}
PStatTimer timer(_compute_vertices_pcollector);
PStatTimer timer(_this_animate_vertices_pcollector);
CPT(TransformBlendPalette) palette = cdata->_transform_blend_palette;
nassertv(palette != (TransformBlendPalette *)NULL);
PT(qpGeomVertexData) new_data = cdata->_computed_vertices;
// Recompute all the blends up front.
int num_blends = palette->get_num_blends();
int bi;
for (bi = 0; bi < num_blends; bi++) {
palette->get_blend(bi).update_blend();
}
PT(qpGeomVertexData) new_data = cdata->_animated_vertices;
// Now go through and apply the scale, copying it to the new data.
qpGeomVertexIterator from(this, InternalName::get_vertex());
qpGeomVertexIterator blendi(this, InternalName::get_transform_blend());
qpGeomVertexIterator to(new_data, InternalName::get_vertex());
for (int i = 0; i < num_vertices; i++) {
LPoint4f vertex = from.get_data4f();
int bi = blendi.get_data1i();
const TransformBlend &blend = palette->get_blend(bi);
blend.transform_point(vertex);
to.set_data4f(vertex);
if (from.get_data_type()->get_num_values() == 4) {
for (int i = 0; i < num_vertices; i++) {
LPoint4f vertex = from.get_data4f();
int bi = blendi.get_data1i();
palette->get_blend(bi).transform_point(vertex);
to.set_data4f(vertex);
}
} else {
for (int i = 0; i < num_vertices; i++) {
LPoint3f vertex = from.get_data3f();
int bi = blendi.get_data1i();
palette->get_blend(bi).transform_point(vertex);
to.set_data3f(vertex);
}
}
}

View File

@ -64,12 +64,16 @@ class EXPCL_PANDA qpGeomVertexData : public TypedWritableReferenceCount {
private:
qpGeomVertexData();
PUBLISHED:
qpGeomVertexData(const qpGeomVertexFormat *format,
qpGeomVertexData(const string &name,
const qpGeomVertexFormat *format,
qpGeomUsageHint::UsageHint usage_hint);
qpGeomVertexData(const qpGeomVertexData &copy);
void operator = (const qpGeomVertexData &copy);
virtual ~qpGeomVertexData();
INLINE const string &get_name() const;
INLINE void set_name(const string &name);
INLINE const qpGeomVertexFormat *get_format() const;
INLINE qpGeomUsageHint::UsageHint get_usage_hint() const;
@ -98,7 +102,7 @@ PUBLISHED:
set_color(const Colorf &color, int num_components,
qpGeomVertexDataType::NumericType numeric_type) const;
CPT(qpGeomVertexData) compute_vertices() const;
CPT(qpGeomVertexData) animate_vertices() const;
PT(qpGeomVertexData)
replace_data_type(const InternalName *name, int num_components,
@ -135,6 +139,7 @@ public:
static void unpack_argb(int data[4], unsigned int packed_argb);
private:
string _name;
CPT(qpGeomVertexFormat) _format;
qpGeomUsageHint::UsageHint _usage_hint;
@ -152,8 +157,8 @@ private:
Arrays _arrays;
PT(TransformBlendPalette) _transform_blend_palette;
PT(qpGeomVertexData) _computed_vertices;
UpdateSeq _computed_vertices_modified;
PT(qpGeomVertexData) _animated_vertices;
UpdateSeq _animated_vertices_modified;
UpdateSeq _modified;
};
@ -163,13 +168,15 @@ private:
private:
bool do_set_num_vertices(int n, CDWriter &cdata);
void make_computed_vertices(CDWriter &cdata);
void update_computed_vertices(CDWriter &cdata);
void make_animated_vertices(CDWriter &cdata);
void update_animated_vertices(CDWriter &cdata);
static PStatCollector _convert_pcollector;
static PStatCollector _scale_color_pcollector;
static PStatCollector _set_color_pcollector;
static PStatCollector _compute_vertices_pcollector;
static PStatCollector _animate_vertices_pcollector;
PStatCollector _this_animate_vertices_pcollector;
public:
static void register_with_read_factory();

View File

@ -177,22 +177,53 @@ get_weight(int n) const {
return _entries[n]._weight;
}
////////////////////////////////////////////////////////////////////
// Function: TransformBlend::update_blend
// Access: Published
// Description: Recomputes the internal representation of the blend
// value, if necessary. You should call this before
// calling get_blend() or transform_point().
////////////////////////////////////////////////////////////////////
INLINE void TransformBlend::
update_blend() const {
CDReader cdata(_cycler);
if (cdata->_global_modified != VertexTransform::get_global_modified()) {
CDWriter cdataw(((TransformBlend *)this)->_cycler, cdata);
((TransformBlend *)this)->recompute_result(cdataw);
}
}
////////////////////////////////////////////////////////////////////
// Function: TransformBlend::get_blend
// Access: Published
// Description: Returns the current value of the blend, based on the
// current value of all of the nested transform objects
// and their associated weights.
//
// You should call update_blend() to ensure that the
// cache is up-to-date before calling this.
////////////////////////////////////////////////////////////////////
INLINE void TransformBlend::
get_blend(LMatrix4f &result) const {
CDReader cdata(_cycler);
if (cdata->_global_modified != VertexTransform::get_global_modified()) {
CDWriter cdataw(((TransformBlend *)this)->_cycler, cdata);
((TransformBlend *)this)->recompute_result(cdataw);
result = cdataw->_result;
} else {
result = cdata->_result;
nassertv(cdata->_global_modified == VertexTransform::get_global_modified());
result = cdata->_result;
}
////////////////////////////////////////////////////////////////////
// Function: TransformBlend::transform_point
// Access: Published
// Description: Transforms the indicated point by the blend matrix.
//
// You should call update_blend() to ensure that the
// cache is up-to-date before calling this.
////////////////////////////////////////////////////////////////////
INLINE void TransformBlend::
transform_point(LPoint4f &point) const {
if (!_entries.empty()) {
CDReader cdata(_cycler);
nassertv(cdata->_global_modified == VertexTransform::get_global_modified());
point = point * cdata->_result;
}
}
@ -200,18 +231,16 @@ get_blend(LMatrix4f &result) const {
// Function: TransformBlend::transform_point
// Access: Published
// Description: Transforms the indicated point by the blend matrix.
//
// You should call update_blend() to ensure that the
// cache is up-to-date before calling this.
////////////////////////////////////////////////////////////////////
INLINE void TransformBlend::
transform_point(LPoint4f &point) const {
transform_point(LPoint3f &point) const {
if (!_entries.empty()) {
CDReader cdata(_cycler);
if (cdata->_global_modified != VertexTransform::get_global_modified()) {
CDWriter cdataw(((TransformBlend *)this)->_cycler, cdata);
((TransformBlend *)this)->recompute_result(cdataw);
point = point * cdataw->_result;
} else {
point = point * cdata->_result;
}
nassertv(cdata->_global_modified == VertexTransform::get_global_modified());
point = point * cdata->_result;
}
}

View File

@ -70,8 +70,11 @@ PUBLISHED:
INLINE const VertexTransform *get_transform(int n) const;
INLINE float get_weight(int n) const;
INLINE void update_blend() const;
INLINE void get_blend(LMatrix4f &result) const;
INLINE void transform_point(LPoint4f &point) const;
INLINE void transform_point(LPoint3f &point) const;
INLINE UpdateSeq get_modified() const;
void output(ostream &out) const;

View File

@ -119,9 +119,10 @@ add_blend(const TransformBlend &blend) {
// Description:
////////////////////////////////////////////////////////////////////
void TransformBlendPalette::
write(ostream &out) const {
write(ostream &out, int indent_level) const {
for (int i = 0; i < (int)_blends.size(); i++) {
out << i << ". " << _blends[i] << "\n";
indent(out, indent_level)
<< i << ". " << _blends[i] << "\n";
}
}

View File

@ -64,7 +64,7 @@ PUBLISHED:
void remove_blend(int n);
int add_blend(const TransformBlend &blend);
void write(ostream &out) const;
void write(ostream &out, int indent_level) const;
private:
void clear_index();

View File

@ -38,7 +38,7 @@ munge_geom(const qpGeomMunger *munger) {
_munger = munger;
CPT(qpGeom) qpgeom = DCAST(qpGeom, _geom);
qpgeom->munge_geom(munger, qpgeom, _munged_data);
_munged_data = _munged_data->compute_vertices();
_munged_data = _munged_data->animate_vertices();
_geom = qpgeom;
}
}

View File

@ -123,6 +123,7 @@ static TimeCollectorProperties time_properties[] = {
{ 0, "App:Show code:Nametags:3d:Contents", { 0.0, 0.5, 0.0 } },
{ 0, "App:Show code:Nametags:3d:Adjust", { 0.5, 0.0, 0.5 } },
{ 1, "Cull", { 0.0, 1.0, 0.0 }, 1.0 / 30.0 },
{ 1, "Cull:Animate vertices", { 1.0, 0.5, 0.3 }, 1.0 / 30.0 },
{ 1, "Cull:Show fps", { 0.5, 0.8, 1.0 } },
{ 1, "Cull:Bins", { 0.3, 0.6, 0.3 } },
{ 1, "Cull:Munge", { 0.3, 0.3, 0.9 } },