mirror of
https://github.com/panda3d/panda3d.git
synced 2025-09-30 16:58:40 -04:00
begin animation support for xfile
This commit is contained in:
parent
99d7222df4
commit
9d564e2470
@ -20,12 +20,16 @@
|
||||
#define COMBINED_SOURCES $[TARGET]_composite1.cxx
|
||||
|
||||
#define SOURCES \
|
||||
config_xfile.h xFileFace.h xFileMaker.h xFileMaterial.h \
|
||||
config_xfile.h \
|
||||
xFileAnimationSet.h \
|
||||
xFileFace.h xFileMaker.h xFileMaterial.h \
|
||||
xFileMesh.h xFileNormal.h xFileTemplates.h \
|
||||
xFileToEggConverter.h xFileVertex.h
|
||||
|
||||
#define INCLUDED_SOURCES \
|
||||
config_xfile.cxx xFileFace.cxx xFileMaker.cxx xFileMaterial.cxx \
|
||||
config_xfile.cxx \
|
||||
xFileAnimationSet.cxx \
|
||||
xFileFace.cxx xFileMaker.cxx xFileMaterial.cxx \
|
||||
xFileMesh.cxx xFileNormal.cxx xFileTemplates.cxx \
|
||||
xFileToEggConverter.cxx xFileVertex.cxx
|
||||
|
||||
|
167
pandatool/src/xfile/xFileAnimationSet.cxx
Normal file
167
pandatool/src/xfile/xFileAnimationSet.cxx
Normal file
@ -0,0 +1,167 @@
|
||||
// Filename: xFileAnimationSet.cxx
|
||||
// Created by: drose (02Oct04)
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// PANDA 3D SOFTWARE
|
||||
// Copyright (c) 2001 - 2004, Disney Enterprises, Inc. All rights reserved
|
||||
//
|
||||
// All use of this software is subject to the terms of the Panda 3d
|
||||
// Software license. You should have received a copy of this license
|
||||
// along with this source code; you will also find a current copy of
|
||||
// the license at http://etc.cmu.edu/panda3d/docs/license/ .
|
||||
//
|
||||
// To contact the maintainers of this program write to
|
||||
// panda3d-general@lists.sourceforge.net .
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include "xFileAnimationSet.h"
|
||||
#include "xFileToEggConverter.h"
|
||||
|
||||
#include "eggGroup.h"
|
||||
#include "eggTable.h"
|
||||
#include "eggData.h"
|
||||
#include "eggXfmSAnim.h"
|
||||
#include "dcast.h"
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// Function: XFileAnimationSet::Constructor
|
||||
// Access: Public
|
||||
// Description:
|
||||
////////////////////////////////////////////////////////////////////
|
||||
XFileAnimationSet::
|
||||
XFileAnimationSet() {
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// Function: XFileAnimationSet::Destructor
|
||||
// Access: Public
|
||||
// Description:
|
||||
////////////////////////////////////////////////////////////////////
|
||||
XFileAnimationSet::
|
||||
~XFileAnimationSet() {
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// Function: XFileAnimationSet::create_hierarchy
|
||||
// Access: Public
|
||||
// Description: Sets up the hierarchy of EggTables corresponding to
|
||||
// this AnimationSet.
|
||||
////////////////////////////////////////////////////////////////////
|
||||
bool XFileAnimationSet::
|
||||
create_hierarchy(XFileToEggConverter *converter) {
|
||||
// Egg animation tables start off with one Table entry, enclosing a
|
||||
// Bundle entry.
|
||||
EggTable *table = new EggTable(get_name());
|
||||
converter->get_egg_data().add_child(table);
|
||||
EggTable *bundle = new EggTable(converter->_char_name);
|
||||
table->add_child(bundle);
|
||||
bundle->set_table_type(EggTable::TT_bundle);
|
||||
|
||||
// Then the Bundle contains a "<skeleton>" entry, which begins the
|
||||
// animation table hierarchy.
|
||||
EggTable *skeleton = new EggTable("<skeleton>");
|
||||
bundle->add_child(skeleton);
|
||||
|
||||
// Fill in the rest of the hierarchy with empty tables.
|
||||
mirror_table(converter->get_dart_node(), skeleton);
|
||||
|
||||
// Now populate those empty tables with the frame data.
|
||||
JointData::const_iterator ji;
|
||||
for (ji = _joint_data.begin(); ji != _joint_data.end(); ++ji) {
|
||||
const string &joint_name = (*ji).first;
|
||||
const FrameData &table = (*ji).second;
|
||||
|
||||
EggXfmSAnim *anim_table = get_table(joint_name);
|
||||
if (anim_table == (EggXfmSAnim *)NULL) {
|
||||
xfile_cat.warning()
|
||||
<< "Frame " << joint_name << ", named by animation data, not defined.\n";
|
||||
} else {
|
||||
// If we have animation data, apply it.
|
||||
FrameData::const_iterator fi;
|
||||
for (fi = table.begin(); fi != table.end(); ++fi) {
|
||||
anim_table->add_data(*fi);
|
||||
}
|
||||
anim_table->optimize();
|
||||
}
|
||||
}
|
||||
|
||||
// Put some data in the empty tables also.
|
||||
Tables::iterator ti;
|
||||
for (ti = _tables.begin(); ti != _tables.end(); ++ti) {
|
||||
const string &joint_name = (*ti).first;
|
||||
EggXfmSAnim *anim_table = (*ti).second._table;
|
||||
EggGroup *joint = (*ti).second._joint;
|
||||
if (anim_table->empty() && joint != (EggGroup *)NULL) {
|
||||
// If there's no animation data, assign the rest transform.
|
||||
anim_table->add_data(joint->get_transform());
|
||||
}
|
||||
anim_table->optimize();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// Function: XFileAnimationSet::get_table
|
||||
// Access: Public
|
||||
// Description: Returns the table associated with the indicated joint
|
||||
// name.
|
||||
////////////////////////////////////////////////////////////////////
|
||||
EggXfmSAnim *XFileAnimationSet::
|
||||
get_table(const string &joint_name) const {
|
||||
Tables::const_iterator ti;
|
||||
ti = _tables.find(joint_name);
|
||||
if (ti != _tables.end()) {
|
||||
return (*ti).second._table;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// Function: XFileAnimationSet::create_frame_data
|
||||
// Access: Public
|
||||
// Description: Returns a reference to a new FrameData table
|
||||
// corresponding to the indicated joint.
|
||||
////////////////////////////////////////////////////////////////////
|
||||
XFileAnimationSet::FrameData &XFileAnimationSet::
|
||||
create_frame_data(const string &joint_name) {
|
||||
return _joint_data[joint_name];
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// Function: XFileAnimationSet::mirror_table
|
||||
// Access: Private
|
||||
// Description: Builds up a new set of EggTable nodes, as a
|
||||
// mirror of the existing set of EggGroup (joint)
|
||||
// nodes, and saves each new table in the _tables
|
||||
// record.
|
||||
////////////////////////////////////////////////////////////////////
|
||||
void XFileAnimationSet::
|
||||
mirror_table(EggGroup *model_node, EggTable *anim_node) {
|
||||
EggGroupNode::iterator gi;
|
||||
for (gi = model_node->begin(); gi != model_node->end(); ++gi) {
|
||||
EggNode *child = (*gi);
|
||||
if (child->is_of_type(EggGroup::get_class_type())) {
|
||||
EggGroup *group = DCAST(EggGroup, child);
|
||||
if (group->get_group_type() == EggGroup::GT_joint) {
|
||||
// When we come to a <Joint>, create a new Table for it.
|
||||
EggTable *new_table = new EggTable(group->get_name());
|
||||
anim_node->add_child(new_table);
|
||||
EggXfmSAnim *xform = new EggXfmSAnim("xform");
|
||||
new_table->add_child(xform);
|
||||
TablePair &table_pair = _tables[group->get_name()];
|
||||
table_pair._table = xform;
|
||||
table_pair._joint = group;
|
||||
|
||||
// Now recurse.
|
||||
mirror_table(group, new_table);
|
||||
|
||||
} else {
|
||||
// If we come to an ordinary <Group>, skip past it.
|
||||
mirror_table(group, anim_node);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
69
pandatool/src/xfile/xFileAnimationSet.h
Normal file
69
pandatool/src/xfile/xFileAnimationSet.h
Normal file
@ -0,0 +1,69 @@
|
||||
// Filename: xFileAnimationSet.h
|
||||
// Created by: drose (02Oct04)
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// PANDA 3D SOFTWARE
|
||||
// Copyright (c) 2001 - 2004, Disney Enterprises, Inc. All rights reserved
|
||||
//
|
||||
// All use of this software is subject to the terms of the Panda 3d
|
||||
// Software license. You should have received a copy of this license
|
||||
// along with this source code; you will also find a current copy of
|
||||
// the license at http://etc.cmu.edu/panda3d/docs/license/ .
|
||||
//
|
||||
// To contact the maintainers of this program write to
|
||||
// panda3d-general@lists.sourceforge.net .
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////
|
||||
|
||||
#ifndef XFILEANIMATIONSET_H
|
||||
#define XFILEANIMATIONSET_H
|
||||
|
||||
#include "pandatoolbase.h"
|
||||
#include "pmap.h"
|
||||
#include "pvector.h"
|
||||
#include "luse.h"
|
||||
#include "namable.h"
|
||||
|
||||
class XFileToEggConverter;
|
||||
class EggGroup;
|
||||
class EggTable;
|
||||
class EggXfmSAnim;
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// Class : XFileAnimationSet
|
||||
// Description : This represents a tree of EggTables, corresponding to
|
||||
// Animation entries in the X file. There is one
|
||||
// EggTable for each joint in the character's joint
|
||||
// set, and the whole tree is structured as a
|
||||
// mirror of the joint set.
|
||||
////////////////////////////////////////////////////////////////////
|
||||
class XFileAnimationSet : public Namable {
|
||||
public:
|
||||
XFileAnimationSet();
|
||||
~XFileAnimationSet();
|
||||
|
||||
bool create_hierarchy(XFileToEggConverter *converter);
|
||||
EggXfmSAnim *get_table(const string &joint_name) const;
|
||||
|
||||
typedef pvector<LMatrix4d> FrameData;
|
||||
FrameData &create_frame_data(const string &joint_name);
|
||||
|
||||
private:
|
||||
void mirror_table(EggGroup *model_node, EggTable *anim_node);
|
||||
|
||||
typedef pmap<string, FrameData> JointData;
|
||||
JointData _joint_data;
|
||||
|
||||
class TablePair {
|
||||
public:
|
||||
EggGroup *_joint;
|
||||
EggXfmSAnim *_table;
|
||||
};
|
||||
|
||||
typedef pmap<string, TablePair> Tables;
|
||||
Tables _tables;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
@ -21,6 +21,7 @@
|
||||
#include "xFileVertex.h"
|
||||
#include "xFileNormal.h"
|
||||
#include "xFileMaterial.h"
|
||||
#include "config_xfile.h"
|
||||
|
||||
#include "eggVertexPool.h"
|
||||
#include "eggVertex.h"
|
||||
@ -38,6 +39,7 @@ XFileMesh(CoordinateSystem cs) : _cs(cs) {
|
||||
_has_colors = false;
|
||||
_has_uvs = false;
|
||||
_has_materials = false;
|
||||
_egg_parent = NULL;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
@ -251,30 +253,41 @@ add_material(XFileMaterial *material) {
|
||||
return next_index;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// Function: XFileMesh::set_egg_parent
|
||||
// Access: Public
|
||||
// Description: Specifies the egg node that will eventually be the
|
||||
// parent of this mesh, when create_polygons() is later
|
||||
// called.
|
||||
////////////////////////////////////////////////////////////////////
|
||||
void XFileMesh::
|
||||
set_egg_parent(EggGroupNode *egg_parent) {
|
||||
// We actually put the mesh under its own group.
|
||||
EggGroup *egg_group = new EggGroup(get_name());
|
||||
egg_parent->add_child(egg_group);
|
||||
|
||||
_egg_parent = egg_group;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// Function: XFileMesh::create_polygons
|
||||
// Access: Public
|
||||
// Description: Creates a slew of EggPolygons according to the faces
|
||||
// in the mesh, and adds them to the indicated parent
|
||||
// node.
|
||||
// in the mesh, and adds them to the
|
||||
// previously-indicated parent node.
|
||||
////////////////////////////////////////////////////////////////////
|
||||
bool XFileMesh::
|
||||
create_polygons(EggGroupNode *egg_parent, XFileToEggConverter *converter) {
|
||||
if (has_name()) {
|
||||
// Put a named mesh within its own group.
|
||||
EggGroup *egg_group = new EggGroup(get_name());
|
||||
egg_parent->add_child(egg_group);
|
||||
egg_parent = egg_group;
|
||||
}
|
||||
create_polygons(XFileToEggConverter *converter) {
|
||||
nassertr(_egg_parent != (EggGroupNode *)NULL, false);
|
||||
|
||||
EggVertexPool *vpool = new EggVertexPool(get_name());
|
||||
egg_parent->add_child(vpool);
|
||||
_egg_parent->add_child(vpool);
|
||||
Faces::const_iterator fi;
|
||||
for (fi = _faces.begin(); fi != _faces.end(); ++fi) {
|
||||
XFileFace *face = (*fi);
|
||||
|
||||
EggPolygon *egg_poly = new EggPolygon;
|
||||
egg_parent->add_child(egg_poly);
|
||||
_egg_parent->add_child(egg_poly);
|
||||
|
||||
// Set up the vertices for the polygon.
|
||||
XFileFace::Vertices::reverse_iterator vi;
|
||||
@ -282,7 +295,8 @@ create_polygons(EggGroupNode *egg_parent, XFileToEggConverter *converter) {
|
||||
int vertex_index = (*vi)._vertex_index;
|
||||
int normal_index = (*vi)._normal_index;
|
||||
if (vertex_index < 0 || vertex_index >= (int)_vertices.size()) {
|
||||
nout << "Vertex index out of range in Mesh.\n";
|
||||
xfile_cat.error()
|
||||
<< "Vertex index out of range in Mesh.\n";
|
||||
return false;
|
||||
}
|
||||
XFileVertex *vertex = _vertices[vertex_index];
|
||||
@ -294,6 +308,7 @@ create_polygons(EggGroupNode *egg_parent, XFileToEggConverter *converter) {
|
||||
|
||||
// Create a temporary EggVertex before adding it to the pool.
|
||||
EggVertex temp_vtx;
|
||||
temp_vtx.set_external_index(vertex_index);
|
||||
temp_vtx.set_pos(LCAST(double, vertex->_point));
|
||||
if (vertex->_has_color) {
|
||||
temp_vtx.set_color(vertex->_color);
|
||||
@ -311,7 +326,7 @@ create_polygons(EggGroupNode *egg_parent, XFileToEggConverter *converter) {
|
||||
|
||||
// Transform the vertex into the appropriate (global) coordinate
|
||||
// space.
|
||||
temp_vtx.transform(egg_parent->get_node_to_vertex());
|
||||
temp_vtx.transform(_egg_parent->get_node_to_vertex());
|
||||
|
||||
// Now get a real EggVertex matching our template.
|
||||
EggVertex *egg_vtx = vpool->create_unique_vertex(temp_vtx);
|
||||
@ -326,11 +341,32 @@ create_polygons(EggGroupNode *egg_parent, XFileToEggConverter *converter) {
|
||||
}
|
||||
}
|
||||
|
||||
// Now go through all of the vertices and skin them up.
|
||||
EggVertexPool::iterator vi;
|
||||
for (vi = vpool->begin(); vi != vpool->end(); ++vi) {
|
||||
EggVertex *egg_vtx = (*vi);
|
||||
int vertex_index = egg_vtx->get_external_index();
|
||||
|
||||
SkinWeights::const_iterator swi;
|
||||
for (swi = _skin_weights.begin(); swi != _skin_weights.end(); ++swi) {
|
||||
const SkinWeightsData &data = (*swi);
|
||||
WeightMap::const_iterator wmi = data._weight_map.find(vertex_index);
|
||||
if (wmi != data._weight_map.end()) {
|
||||
EggGroup *joint = converter->find_joint(data._joint_name,
|
||||
data._matrix_offset);
|
||||
if (joint != (EggGroup *)NULL) {
|
||||
double weight = (*wmi).second;
|
||||
joint->ref_vertex(egg_vtx, weight);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!has_normals()) {
|
||||
// If we don't have explicit normals, make some up, per the DX
|
||||
// spec. Since the DX spec doesn't mention anything about a
|
||||
// crease angle, we should be as generous as possible.
|
||||
egg_parent->recompute_vertex_normals(180.0, _cs);
|
||||
_egg_parent->recompute_vertex_normals(180.0, _cs);
|
||||
}
|
||||
|
||||
return true;
|
||||
@ -574,7 +610,8 @@ read_mesh_data(const Datagram &raw_data) {
|
||||
}
|
||||
|
||||
if (di.get_remaining_size() != 0) {
|
||||
nout << "Ignoring " << di.get_remaining_size() << " trailing Mesh.\n";
|
||||
xfile_cat.warning()
|
||||
<< "Ignoring " << di.get_remaining_size() << " trailing Mesh.\n";
|
||||
}
|
||||
|
||||
return true;
|
||||
@ -604,7 +641,8 @@ read_normal_data(const Datagram &raw_data) {
|
||||
int num_faces = di.get_int32();
|
||||
|
||||
if (num_faces != _faces.size()) {
|
||||
nout << "Incorrect number of faces in MeshNormals.\n";
|
||||
xfile_cat.error()
|
||||
<< "Incorrect number of faces in MeshNormals.\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -612,7 +650,8 @@ read_normal_data(const Datagram &raw_data) {
|
||||
XFileFace *face = _faces[i];
|
||||
int num_vertices = di.get_int32();
|
||||
if (num_vertices != face->_vertices.size()) {
|
||||
nout << "Incorrect number of vertices for face in MeshNormals.\n";
|
||||
xfile_cat.error()
|
||||
<< "Incorrect number of vertices for face in MeshNormals.\n";
|
||||
return false;
|
||||
}
|
||||
for (int j = 0; j < num_vertices; j++) {
|
||||
@ -621,8 +660,9 @@ read_normal_data(const Datagram &raw_data) {
|
||||
}
|
||||
|
||||
if (di.get_remaining_size() != 0) {
|
||||
nout << "Ignoring " << di.get_remaining_size()
|
||||
<< " trailing MeshNormals.\n";
|
||||
xfile_cat.warning()
|
||||
<< "Ignoring " << di.get_remaining_size()
|
||||
<< " trailing MeshNormals.\n";
|
||||
}
|
||||
|
||||
return true;
|
||||
@ -643,7 +683,8 @@ read_color_data(const Datagram &raw_data) {
|
||||
for (i = 0; i < num_colors; i++) {
|
||||
unsigned int vertex_index = di.get_int32();
|
||||
if (vertex_index < 0 || vertex_index >= _vertices.size()) {
|
||||
nout << "Vertex index out of range in MeshVertexColors.\n";
|
||||
xfile_cat.error()
|
||||
<< "Vertex index out of range in MeshVertexColors.\n";
|
||||
return false;
|
||||
}
|
||||
XFileVertex *vertex = _vertices[vertex_index];
|
||||
@ -655,8 +696,9 @@ read_color_data(const Datagram &raw_data) {
|
||||
}
|
||||
|
||||
if (di.get_remaining_size() != 0) {
|
||||
nout << "Ignoring " << di.get_remaining_size()
|
||||
<< " trailing MeshVertexColors.\n";
|
||||
xfile_cat.warning()
|
||||
<< "Ignoring " << di.get_remaining_size()
|
||||
<< " trailing MeshVertexColors.\n";
|
||||
}
|
||||
|
||||
return true;
|
||||
@ -674,7 +716,8 @@ read_uv_data(const Datagram &raw_data) {
|
||||
|
||||
int num_vertices = di.get_int32();
|
||||
if (num_vertices != _vertices.size()) {
|
||||
nout << "Wrong number of vertices in MeshTextureCoords.\n";
|
||||
xfile_cat.error()
|
||||
<< "Wrong number of vertices in MeshTextureCoords.\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -687,13 +730,62 @@ read_uv_data(const Datagram &raw_data) {
|
||||
}
|
||||
|
||||
if (di.get_remaining_size() != 0) {
|
||||
nout << "Ignoring " << di.get_remaining_size()
|
||||
<< " trailing MeshTextureCoords.\n";
|
||||
xfile_cat.warning()
|
||||
<< "Ignoring " << di.get_remaining_size()
|
||||
<< " trailing MeshTextureCoords.\n";
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// Function: XFileMesh::read_skin_weights_data
|
||||
// Access: Public
|
||||
// Description: Fills the structure based on the raw data from the
|
||||
// SkinWeights template.
|
||||
////////////////////////////////////////////////////////////////////
|
||||
bool XFileMesh::
|
||||
read_skin_weights_data(const Datagram &raw_data) {
|
||||
DatagramIterator di(raw_data);
|
||||
|
||||
// Create a new SkinWeightsData record for the table. We'll need
|
||||
// this data later when we create the vertices.
|
||||
_skin_weights.push_back(SkinWeightsData());
|
||||
SkinWeightsData &data = _skin_weights.back();
|
||||
|
||||
// The DX system encodes a pointer to a character string in four
|
||||
// bytes within the stream. Weird, in a Microsofty sort of way.
|
||||
data._joint_name = (const char *)di.get_uint32();
|
||||
|
||||
int num_weights = di.get_int32();
|
||||
|
||||
vector_int vindices;
|
||||
vindices.reserve(num_weights);
|
||||
|
||||
// Unpack the list of vertices first
|
||||
int i;
|
||||
for (i = 0; i < num_weights; i++) {
|
||||
int vindex = di.get_int32();
|
||||
if (vindex < 0 || vindex > (int)_vertices.size()) {
|
||||
xfile_cat.error()
|
||||
<< "Illegal vertex index " << vindex << " in SkinWeights.\n";
|
||||
return false;
|
||||
}
|
||||
vindices.push_back(vindex);
|
||||
}
|
||||
|
||||
// Then unpack the weight for each vertex.
|
||||
for (i = 0; i < num_weights; i++) {
|
||||
float weight = di.get_float32();
|
||||
data._weight_map[vindices[i]] = weight;
|
||||
}
|
||||
|
||||
// Finally, read the matrix offset.
|
||||
data._matrix_offset.read_datagram(di);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// Function: XFileMesh::read_material_list_data
|
||||
// Access: Public
|
||||
@ -708,7 +800,8 @@ read_material_list_data(const Datagram &raw_data) {
|
||||
unsigned int num_faces = di.get_int32();
|
||||
|
||||
if (num_faces > _faces.size()) {
|
||||
nout << "Too many faces in MaterialList.\n";
|
||||
xfile_cat.error()
|
||||
<< "Too many faces in MaterialList.\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -730,8 +823,9 @@ read_material_list_data(const Datagram &raw_data) {
|
||||
}
|
||||
|
||||
if (di.get_remaining_size() != 0) {
|
||||
nout << "Ignoring " << di.get_remaining_size()
|
||||
<< " trailing MeshMaterialList.\n";
|
||||
xfile_cat.warning()
|
||||
<< "Ignoring " << di.get_remaining_size()
|
||||
<< " trailing MeshMaterialList.\n";
|
||||
}
|
||||
|
||||
return true;
|
||||
|
@ -58,8 +58,9 @@ public:
|
||||
int add_normal(XFileNormal *normal);
|
||||
int add_material(XFileMaterial *material);
|
||||
|
||||
bool create_polygons(EggGroupNode *egg_parent,
|
||||
XFileToEggConverter *converter);
|
||||
void set_egg_parent(EggGroupNode *egg_parent);
|
||||
|
||||
bool create_polygons(XFileToEggConverter *converter);
|
||||
|
||||
bool has_normals() const;
|
||||
bool has_colors() const;
|
||||
@ -79,6 +80,7 @@ public:
|
||||
bool read_normal_data(const Datagram &raw_data);
|
||||
bool read_color_data(const Datagram &raw_data);
|
||||
bool read_uv_data(const Datagram &raw_data);
|
||||
bool read_skin_weights_data(const Datagram &raw_data);
|
||||
bool read_material_list_data(const Datagram &raw_data);
|
||||
|
||||
private:
|
||||
@ -94,6 +96,17 @@ private:
|
||||
Materials _materials;
|
||||
Faces _faces;
|
||||
|
||||
typedef pmap<int, float> WeightMap;
|
||||
|
||||
class SkinWeightsData {
|
||||
public:
|
||||
string _joint_name;
|
||||
WeightMap _weight_map;
|
||||
LMatrix4f _matrix_offset;
|
||||
};
|
||||
typedef pvector<SkinWeightsData> SkinWeights;
|
||||
SkinWeights _skin_weights;
|
||||
|
||||
typedef pmap<XFileVertex *, int, IndirectCompareTo<XFileVertex> > UniqueVertices;
|
||||
typedef pmap<XFileNormal *, int, IndirectCompareTo<XFileNormal> > UniqueNormals;
|
||||
typedef pmap<XFileMaterial *, int, IndirectCompareTo<XFileMaterial> > UniqueMaterials;
|
||||
@ -105,6 +118,8 @@ private:
|
||||
bool _has_colors;
|
||||
bool _has_uvs;
|
||||
bool _has_materials;
|
||||
|
||||
EggGroupNode *_egg_parent;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
@ -20,15 +20,38 @@
|
||||
#include "xFileMesh.h"
|
||||
#include "xFileMaterial.h"
|
||||
#include "xFileTemplates.h"
|
||||
#include "xFileAnimationSet.h"
|
||||
#include "config_xfile.h"
|
||||
|
||||
#include "eggData.h"
|
||||
#include "eggGroup.h"
|
||||
#include "eggXfmSAnim.h"
|
||||
#include "eggGroupUniquifier.h"
|
||||
#include "datagram.h"
|
||||
#include "eggMaterialCollection.h"
|
||||
#include "eggTextureCollection.h"
|
||||
#include "dcast.h"
|
||||
|
||||
#define MY_DEFINE_GUID(name, l, w1, w2, b1, b2, b3, b4, b5, b6, b7, b8) \
|
||||
EXTERN_C const GUID DECLSPEC_SELECTANY name \
|
||||
= { l, w1, w2, { b1, b2, b3, b4, b5, b6, b7, b8 } }
|
||||
|
||||
// These are defined in d3dx9mesh.h, which we may not have available
|
||||
// (so far, Panda is only dependent on dx8 API's).
|
||||
#ifndef DXFILEOBJ_XSkinMeshHeader
|
||||
// {3CF169CE-FF7C-44ab-93C0-F78F62D172E2}
|
||||
MY_DEFINE_GUID(DXFILEOBJ_XSkinMeshHeader,
|
||||
0x3cf169ce, 0xff7c, 0x44ab, 0x93, 0xc0, 0xf7, 0x8f, 0x62, 0xd1, 0x72, 0xe2);
|
||||
#endif
|
||||
|
||||
#ifndef DXFILEOBJ_SkinWeights
|
||||
// {6F0D123B-BAD2-4167-A0D0-80224F25FABB}
|
||||
MY_DEFINE_GUID(DXFILEOBJ_SkinWeights,
|
||||
0x6f0d123b, 0xbad2, 0x4167, 0xa0, 0xd0, 0x80, 0x22, 0x4f, 0x25, 0xfa, 0xbb);
|
||||
#endif
|
||||
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// Function: XFileToEggConverter::Constructor
|
||||
// Access: Public
|
||||
@ -36,8 +59,10 @@
|
||||
////////////////////////////////////////////////////////////////////
|
||||
XFileToEggConverter::
|
||||
XFileToEggConverter() {
|
||||
_make_char = false;
|
||||
_dx_file = NULL;
|
||||
_dx_file_enum = NULL;
|
||||
_dart_node = NULL;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
@ -47,10 +72,12 @@ XFileToEggConverter() {
|
||||
////////////////////////////////////////////////////////////////////
|
||||
XFileToEggConverter::
|
||||
XFileToEggConverter(const XFileToEggConverter ©) :
|
||||
SomethingToEggConverter(copy)
|
||||
SomethingToEggConverter(copy),
|
||||
_make_char(copy._make_char)
|
||||
{
|
||||
_dx_file = NULL;
|
||||
_dx_file_enum = NULL;
|
||||
_dart_node = NULL;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
@ -138,7 +165,25 @@ convert_file(const Filename &filename) {
|
||||
_egg_data->set_coordinate_system(CS_yup_left);
|
||||
}
|
||||
|
||||
return get_toplevel();
|
||||
if (!get_toplevel()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!create_polygons()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_make_char) {
|
||||
// Now make sure that each joint has a unique name.
|
||||
EggGroupUniquifier uniquifier;
|
||||
uniquifier.uniquify(_dart_node);
|
||||
}
|
||||
|
||||
if (!create_hierarchy()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
@ -157,6 +202,37 @@ close() {
|
||||
_dx_file->Release();
|
||||
_dx_file = NULL;
|
||||
}
|
||||
|
||||
// Clean up all the other stuff.
|
||||
Meshes::const_iterator mi;
|
||||
for (mi = _meshes.begin(); mi != _meshes.end(); ++mi) {
|
||||
delete (*mi);
|
||||
}
|
||||
_meshes.clear();
|
||||
|
||||
for (mi = _toplevel_meshes.begin(); mi != _toplevel_meshes.end(); ++mi) {
|
||||
delete (*mi);
|
||||
}
|
||||
_toplevel_meshes.clear();
|
||||
|
||||
AnimationSets::const_iterator asi;
|
||||
for (asi = _animation_sets.begin(); asi != _animation_sets.end(); ++asi) {
|
||||
delete (*asi);
|
||||
}
|
||||
_animation_sets.clear();
|
||||
|
||||
_joints.clear();
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// Function: XFileToEggConverter::get_dart_node
|
||||
// Access: Public
|
||||
// Description: Returns the root of the joint hierarchy, if
|
||||
// _make_char is true, or NULL otherwise.
|
||||
////////////////////////////////////////////////////////////////////
|
||||
EggGroup *XFileToEggConverter::
|
||||
get_dart_node() const {
|
||||
return _dart_node;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
@ -183,6 +259,98 @@ create_unique_material(const EggMaterial ©) {
|
||||
return _materials.create_unique_material(copy, ~EggMaterial::E_mref_name);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// Function: XFileToEggConverter::find_joint (one parameter)
|
||||
// Access: Public
|
||||
// Description: This is called by set_animation_frame, for
|
||||
// the purposes of building the frame data for the
|
||||
// animation--it needs to know the original rest frame
|
||||
// transform.
|
||||
////////////////////////////////////////////////////////////////////
|
||||
EggGroup *XFileToEggConverter::
|
||||
find_joint(const string &joint_name) {
|
||||
Joints::iterator ji;
|
||||
ji = _joints.find(joint_name);
|
||||
if (ji != _joints.end()) {
|
||||
JointDef &joint_def = (*ji).second;
|
||||
if (joint_def._node == (EggGroup *)NULL) {
|
||||
// An invalid joint detected earlier.
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return joint_def._node;
|
||||
}
|
||||
|
||||
// Joint name is unknown. Issue a warning, then insert NULL into
|
||||
// the table so we don't get the same warning again with the next
|
||||
// polygon.
|
||||
if (_make_char) {
|
||||
xfile_cat.warning()
|
||||
<< "Joint name " << joint_name << " in animation data is undefined.\n";
|
||||
}
|
||||
_joints[joint_name]._node = NULL;
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// Function: XFileToEggConverter::find_joint (two parameters)
|
||||
// Access: Public
|
||||
// Description: This is called by XFileMesh::create_polygons(), for
|
||||
// the purposes of applying skinning to vertices. It
|
||||
// searches for the joint matching the indicated name,
|
||||
// and returns it, possibly creating a new joint if the
|
||||
// requested matrix_offset demands it. Returns NULL if
|
||||
// the joint name is unknown.
|
||||
////////////////////////////////////////////////////////////////////
|
||||
EggGroup *XFileToEggConverter::
|
||||
find_joint(const string &joint_name, const LMatrix4f &matrix_offset) {
|
||||
return find_joint(joint_name);
|
||||
/*
|
||||
Joints::iterator ji;
|
||||
ji = _joints.find(joint_name);
|
||||
if (ji != _joints.end()) {
|
||||
JointDef &joint_def = (*ji).second;
|
||||
if (joint_def._node == (EggGroup *)NULL) {
|
||||
// An invalid joint detected earlier.
|
||||
return NULL;
|
||||
}
|
||||
|
||||
OffsetJoints::iterator oji = joint_def._offsets.find(matrix_offset);
|
||||
if (oji != joint_def._offsets.end()) {
|
||||
// We've previously created a joint for this matrix, so just
|
||||
// reuse it.
|
||||
return (*oji).second;
|
||||
}
|
||||
|
||||
if (!joint_def._offsets.empty()) {
|
||||
const LMatrix4f &mat = (*joint_def._offsets.begin()).first;
|
||||
}
|
||||
|
||||
// We need to create a new joint for this matrix.
|
||||
EggGroup *new_joint = new EggGroup("synth");
|
||||
joint_def._node->add_child(new_joint);
|
||||
|
||||
new_joint->set_group_type(EggGroup::GT_joint);
|
||||
new_joint->set_transform(LCAST(double, matrix_offset));
|
||||
joint_def._offsets[matrix_offset] = new_joint;
|
||||
|
||||
return new_joint;
|
||||
}
|
||||
|
||||
// Joint name is unknown. Issue a warning, then insert NULL into
|
||||
// the table so we don't get the same warning again with the next
|
||||
// polygon.
|
||||
if (_make_char) {
|
||||
xfile_cat.warning()
|
||||
<< "Joint name " << joint_name << " in animation data is undefined.\n";
|
||||
}
|
||||
_joints[joint_name]._node = NULL;
|
||||
|
||||
return NULL;
|
||||
*/
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// Function: XFileToEggConverter::get_toplevel
|
||||
// Access: Private
|
||||
@ -195,13 +363,22 @@ get_toplevel() {
|
||||
HRESULT hr;
|
||||
LPDIRECTXFILEDATA obj;
|
||||
|
||||
PT(EggGroup) egg_toplevel = new EggGroup;
|
||||
bool any_frames = false;
|
||||
EggGroupNode *egg_parent = _egg_data;
|
||||
|
||||
// If we are converting an animatable model, make an extra node to
|
||||
// represent the root of the hierarchy.
|
||||
if (_make_char) {
|
||||
_dart_node = new EggGroup(_char_name);
|
||||
egg_parent->add_child(_dart_node);
|
||||
_dart_node->set_dart_type(EggGroup::DT_default);
|
||||
egg_parent = _dart_node;
|
||||
}
|
||||
|
||||
_any_frames = false;
|
||||
|
||||
hr = _dx_file_enum->GetNextDataObject(&obj);
|
||||
while (hr == DXFILE_OK) {
|
||||
if (!convert_toplevel_object(obj, _egg_data,
|
||||
egg_toplevel, any_frames)) {
|
||||
if (!convert_toplevel_object(obj, egg_parent)) {
|
||||
return false;
|
||||
}
|
||||
hr = _dx_file_enum->GetNextDataObject(&obj);
|
||||
@ -213,13 +390,6 @@ get_toplevel() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!any_frames) {
|
||||
// If the file contained no frames at all, then all of the meshes
|
||||
// that appeared at the toplevel were meant to be directly
|
||||
// included.
|
||||
_egg_data->steal_children(*egg_toplevel);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -230,8 +400,7 @@ get_toplevel() {
|
||||
// any Frames, to the appropriate egg structures.
|
||||
////////////////////////////////////////////////////////////////////
|
||||
bool XFileToEggConverter::
|
||||
convert_toplevel_object(LPDIRECTXFILEDATA obj, EggGroupNode *egg_parent,
|
||||
EggGroupNode *egg_toplevel, bool &any_frames) {
|
||||
convert_toplevel_object(LPDIRECTXFILEDATA obj, EggGroupNode *egg_parent) {
|
||||
HRESULT hr;
|
||||
|
||||
// Determine what type of data object we have.
|
||||
@ -251,7 +420,7 @@ convert_toplevel_object(LPDIRECTXFILEDATA obj, EggGroupNode *egg_parent,
|
||||
// referenced below.
|
||||
|
||||
} else if (*type == TID_D3DRMFrame) {
|
||||
any_frames = true;
|
||||
_any_frames = true;
|
||||
if (!convert_frame(obj, egg_parent)) {
|
||||
return false;
|
||||
}
|
||||
@ -261,12 +430,17 @@ convert_toplevel_object(LPDIRECTXFILEDATA obj, EggGroupNode *egg_parent,
|
||||
return false;
|
||||
}
|
||||
|
||||
} else if (*type == TID_D3DRMAnimationSet) {
|
||||
if (!convert_animation_set(obj)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
} else if (*type == TID_D3DRMMesh) {
|
||||
// Assume a Mesh at the toplevel is just present to define a
|
||||
// reference that will be included below. Convert it into the
|
||||
// egg_toplevel group, where it will be ignored unless there are
|
||||
// _toplevel_meshes set, where it will be ignored unless there are
|
||||
// no frames at all in the file.
|
||||
if (!convert_mesh(obj, egg_toplevel)) {
|
||||
if (!convert_mesh(obj, egg_parent, true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -352,7 +526,7 @@ convert_data_object(LPDIRECTXFILEDATA obj, EggGroupNode *egg_parent) {
|
||||
}
|
||||
|
||||
} else if (*type == TID_D3DRMMesh) {
|
||||
if (!convert_mesh(obj, egg_parent)) {
|
||||
if (!convert_mesh(obj, egg_parent, false)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -381,6 +555,24 @@ convert_frame(LPDIRECTXFILEDATA obj, EggGroupNode *egg_parent) {
|
||||
EggGroup *group = new EggGroup(name);
|
||||
egg_parent->add_child(group);
|
||||
|
||||
if (_make_char) {
|
||||
group->set_group_type(EggGroup::GT_joint);
|
||||
if (name.empty()) {
|
||||
// Make up a name for this unnamed joint.
|
||||
group->set_name("unnamed");
|
||||
|
||||
} else {
|
||||
JointDef joint_def;
|
||||
joint_def._node = group;
|
||||
bool inserted = _joints.insert(Joints::value_type(name, joint_def)).second;
|
||||
if (!inserted) {
|
||||
xfile_cat.warning()
|
||||
<< "Nonunique Frame name " << name
|
||||
<< " encountered; animation will be ambiguous.\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now walk through the children of the frame.
|
||||
LPDIRECTXFILEOBJECT child_obj;
|
||||
|
||||
@ -434,6 +626,398 @@ convert_transform(LPDIRECTXFILEDATA obj, EggGroupNode *egg_parent) {
|
||||
return true;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// Function: XFileToEggConverter::convert_animation_set
|
||||
// Access: Private
|
||||
// Description: Begins an AnimationSet. This is the root of one
|
||||
// particular animation (table of frames per joint) to
|
||||
// be applied to the model within this file.
|
||||
////////////////////////////////////////////////////////////////////
|
||||
bool XFileToEggConverter::
|
||||
convert_animation_set(LPDIRECTXFILEDATA obj) {
|
||||
HRESULT hr;
|
||||
|
||||
XFileAnimationSet *animation_set = new XFileAnimationSet();
|
||||
animation_set->set_name(get_object_name(obj));
|
||||
|
||||
// Now walk through the children of the set; each one animates a
|
||||
// different joint.
|
||||
LPDIRECTXFILEOBJECT child_obj;
|
||||
|
||||
hr = obj->GetNextObject(&child_obj);
|
||||
while (hr == DXFILE_OK) {
|
||||
if (!convert_animation_set_object(child_obj, *animation_set)) {
|
||||
return false;
|
||||
}
|
||||
hr = obj->GetNextObject(&child_obj);
|
||||
}
|
||||
|
||||
if (hr != DXFILEERR_NOMOREOBJECTS) {
|
||||
xfile_cat.error()
|
||||
<< "Error extracting children of AnimationSet "
|
||||
<< get_object_name(obj) << ".\n";
|
||||
delete animation_set;
|
||||
return false;
|
||||
}
|
||||
|
||||
_animation_sets.push_back(animation_set);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// Function: XFileToEggConverter::convert_animation_set_object
|
||||
// Access: Private
|
||||
// Description: Converts the indicated object, a child of a
|
||||
// AnimationSet.
|
||||
////////////////////////////////////////////////////////////////////
|
||||
bool XFileToEggConverter::
|
||||
convert_animation_set_object(LPDIRECTXFILEOBJECT obj,
|
||||
XFileAnimationSet &animation_set) {
|
||||
HRESULT hr;
|
||||
LPDIRECTXFILEDATA data_obj;
|
||||
LPDIRECTXFILEDATAREFERENCE ref_obj;
|
||||
|
||||
// See if the object is a data object.
|
||||
hr = obj->QueryInterface(IID_IDirectXFileData, (void **)&data_obj);
|
||||
if (hr == DD_OK) {
|
||||
// It is.
|
||||
return convert_animation_set_data_object(data_obj, animation_set);
|
||||
}
|
||||
|
||||
// Or maybe it's a reference to a previous object.
|
||||
hr = obj->QueryInterface(IID_IDirectXFileDataReference, (void **)&ref_obj);
|
||||
if (hr == DD_OK) {
|
||||
// It is.
|
||||
if (ref_obj->Resolve(&data_obj) == DXFILE_OK) {
|
||||
return convert_animation_set_data_object(data_obj, animation_set);
|
||||
}
|
||||
}
|
||||
|
||||
// It isn't.
|
||||
if (xfile_cat.is_debug()) {
|
||||
xfile_cat.debug()
|
||||
<< "Ignoring animation set object of unknown type: "
|
||||
<< get_object_name(obj) << "\n";
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// Function: XFileToEggConverter::convert_animation_set_data_object
|
||||
// Access: Private
|
||||
// Description: Converts the indicated data object, a child of a
|
||||
// AnimationSet.
|
||||
////////////////////////////////////////////////////////////////////
|
||||
bool XFileToEggConverter::
|
||||
convert_animation_set_data_object(LPDIRECTXFILEDATA obj, XFileAnimationSet &animation_set) {
|
||||
HRESULT hr;
|
||||
|
||||
// Determine what type of data object we have.
|
||||
const GUID *type;
|
||||
hr = obj->GetType(&type);
|
||||
if (hr != DXFILE_OK) {
|
||||
xfile_cat.error()
|
||||
<< "Unable to get type of template\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (*type == TID_D3DRMAnimation) {
|
||||
if (!convert_animation(obj, animation_set)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (xfile_cat.is_debug()) {
|
||||
xfile_cat.debug()
|
||||
<< "Ignoring animation set data object of unknown type: "
|
||||
<< get_object_name(obj) << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// Function: XFileToEggConverter::convert_animation
|
||||
// Access: Private
|
||||
// Description: Converts the indicated Animation template object.
|
||||
////////////////////////////////////////////////////////////////////
|
||||
bool XFileToEggConverter::
|
||||
convert_animation(LPDIRECTXFILEDATA obj, XFileAnimationSet &animation_set) {
|
||||
HRESULT hr;
|
||||
|
||||
// Within an Animation template, we expect to find a reference to a
|
||||
// frame, possibly an AnimationOptions object, and one or more
|
||||
// AnimationKey objects.
|
||||
LPDIRECTXFILEOBJECT child_obj;
|
||||
|
||||
// First, walk through the list of children, to find the one that is
|
||||
// the frame reference. We need to know this up front so we know
|
||||
// which table we should be building up.
|
||||
string frame_name;
|
||||
bool got_frame_name = false;
|
||||
|
||||
pvector<LPDIRECTXFILEOBJECT> children;
|
||||
|
||||
hr = obj->GetNextObject(&child_obj);
|
||||
while (hr == DXFILE_OK) {
|
||||
LPDIRECTXFILEDATAREFERENCE ref_obj;
|
||||
if (child_obj->QueryInterface(IID_IDirectXFileDataReference, (void **)&ref_obj) == DD_OK) {
|
||||
// Here's a reference!
|
||||
LPDIRECTXFILEDATA data_obj;
|
||||
if (ref_obj->Resolve(&data_obj) == DXFILE_OK) {
|
||||
const GUID *type;
|
||||
if (data_obj->GetType(&type) == DXFILE_OK) {
|
||||
if (*type == TID_D3DRMFrame) {
|
||||
// Ok, this one is a reference to a frame. Save the name.
|
||||
frame_name = get_object_name(data_obj);
|
||||
got_frame_name = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
children.push_back(child_obj);
|
||||
}
|
||||
|
||||
hr = obj->GetNextObject(&child_obj);
|
||||
}
|
||||
|
||||
if (hr != DXFILEERR_NOMOREOBJECTS) {
|
||||
xfile_cat.error()
|
||||
<< "Error extracting children of Animation "
|
||||
<< get_object_name(obj) << ".\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!got_frame_name) {
|
||||
xfile_cat.error()
|
||||
<< "Animation " << get_object_name(obj)
|
||||
<< " includes no reference to a frame.\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
FrameData &table = animation_set.create_frame_data(frame_name);
|
||||
|
||||
// Now go back again and get the actual data.
|
||||
pvector<LPDIRECTXFILEOBJECT>::iterator ci;
|
||||
for (ci = children.begin(); ci != children.end(); ++ci) {
|
||||
if (!convert_animation_object((*ci), frame_name, table)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// Function: XFileToEggConverter::convert_animation_object
|
||||
// Access: Private
|
||||
// Description: Converts the indicated object, a child of a
|
||||
// Animation.
|
||||
////////////////////////////////////////////////////////////////////
|
||||
bool XFileToEggConverter::
|
||||
convert_animation_object(LPDIRECTXFILEOBJECT obj, const string &joint_name,
|
||||
XFileToEggConverter::FrameData &table) {
|
||||
HRESULT hr;
|
||||
LPDIRECTXFILEDATA data_obj;
|
||||
LPDIRECTXFILEDATAREFERENCE ref_obj;
|
||||
|
||||
// See if the object is a data object.
|
||||
hr = obj->QueryInterface(IID_IDirectXFileData, (void **)&data_obj);
|
||||
if (hr == DD_OK) {
|
||||
// It is.
|
||||
return convert_animation_data_object(data_obj, joint_name, table);
|
||||
}
|
||||
|
||||
// Or maybe it's a reference to a previous object.
|
||||
hr = obj->QueryInterface(IID_IDirectXFileDataReference, (void **)&ref_obj);
|
||||
if (hr == DD_OK) {
|
||||
// It is.
|
||||
if (ref_obj->Resolve(&data_obj) == DXFILE_OK) {
|
||||
return convert_animation_data_object(data_obj, joint_name, table);
|
||||
}
|
||||
}
|
||||
|
||||
// It isn't.
|
||||
if (xfile_cat.is_debug()) {
|
||||
xfile_cat.debug()
|
||||
<< "Ignoring animation set object of unknown type: "
|
||||
<< get_object_name(obj) << "\n";
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// Function: XFileToEggConverter::convert_animation_data_object
|
||||
// Access: Private
|
||||
// Description: Converts the indicated data object, a child of a
|
||||
// Animation.
|
||||
////////////////////////////////////////////////////////////////////
|
||||
bool XFileToEggConverter::
|
||||
convert_animation_data_object(LPDIRECTXFILEDATA obj, const string &joint_name,
|
||||
XFileToEggConverter::FrameData &table) {
|
||||
HRESULT hr;
|
||||
|
||||
// Determine what type of data object we have.
|
||||
const GUID *type;
|
||||
hr = obj->GetType(&type);
|
||||
if (hr != DXFILE_OK) {
|
||||
xfile_cat.error()
|
||||
<< "Unable to get type of template\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (*type == TID_D3DRMAnimationOptions) {
|
||||
// Quietly ignore AnimationOptions.
|
||||
|
||||
} else if (*type == TID_D3DRMAnimationKey) {
|
||||
if (!convert_animation_key(obj, joint_name, table)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
} else {
|
||||
if (xfile_cat.is_debug()) {
|
||||
xfile_cat.debug()
|
||||
<< "Ignoring animation set data object of unknown type: "
|
||||
<< get_object_name(obj) << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// Function: XFileToEggConverter::convert_animation_key
|
||||
// Access: Private
|
||||
// Description: Converts the indicated AnimationKey template object.
|
||||
////////////////////////////////////////////////////////////////////
|
||||
bool XFileToEggConverter::
|
||||
convert_animation_key(LPDIRECTXFILEDATA obj, const string &joint_name,
|
||||
XFileToEggConverter::FrameData &table) {
|
||||
Datagram raw_data;
|
||||
if (!get_data(obj, raw_data)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
DatagramIterator di(raw_data);
|
||||
int key_type = di.get_uint32();
|
||||
int nkeys = di.get_uint32();
|
||||
|
||||
int last_time = 0;
|
||||
|
||||
for (int i = 0; i < nkeys; i++) {
|
||||
int time = di.get_uint32();
|
||||
|
||||
int nvalues = di.get_uint32();
|
||||
pvector<float> values;
|
||||
values.reserve(nvalues);
|
||||
for (int j = 0; j < nvalues; j++) {
|
||||
float value = di.get_float32();
|
||||
values.push_back(value);
|
||||
}
|
||||
|
||||
while (last_time <= time) {
|
||||
if (!set_animation_frame(joint_name, table, last_time, key_type,
|
||||
&values[0], nvalues)) {
|
||||
return false;
|
||||
}
|
||||
last_time++;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// Function: XFileToEggConverter::set_animation_frame
|
||||
// Access: Private
|
||||
// Description: Sets a single frame of the animation data.
|
||||
////////////////////////////////////////////////////////////////////
|
||||
bool XFileToEggConverter::
|
||||
set_animation_frame(const string &joint_name,
|
||||
XFileToEggConverter::FrameData &table, int frame,
|
||||
int key_type, const float *values, int nvalues) {
|
||||
LMatrix4d mat;
|
||||
|
||||
// Pad out the table by duplicating the last row as necessary.
|
||||
if ((int)table.size() <= frame) {
|
||||
if (table.empty()) {
|
||||
// Get the initial transform from the joint's rest transform.
|
||||
EggGroup *joint = find_joint(joint_name);
|
||||
if (joint != (EggGroup *)NULL) {
|
||||
mat = joint->get_transform();
|
||||
} else {
|
||||
mat = LMatrix4d::ident_mat();
|
||||
}
|
||||
} else {
|
||||
// Get the initial transform from the last frame of animation.
|
||||
mat = table.back();
|
||||
}
|
||||
table.push_back(mat);
|
||||
while ((int)table.size() <= frame) {
|
||||
table.push_back(mat);
|
||||
}
|
||||
|
||||
} else {
|
||||
mat = table.back();
|
||||
}
|
||||
|
||||
// Now modify the last row in the table.
|
||||
switch (key_type) {
|
||||
/*
|
||||
case 0:
|
||||
// Key type 0: rotation
|
||||
break;
|
||||
*/
|
||||
|
||||
/*
|
||||
case 1:
|
||||
// Key type 1: scale
|
||||
break;
|
||||
*/
|
||||
|
||||
case 2:
|
||||
// Key type 2: position
|
||||
if (nvalues != 3) {
|
||||
xfile_cat.error()
|
||||
<< "Incorrect number of values in animation table: "
|
||||
<< nvalues << " for position data.\n";
|
||||
return false;
|
||||
}
|
||||
mat.set_row(3, LVecBase3d(values[0], values[1], values[2]));
|
||||
break;
|
||||
|
||||
/*
|
||||
case 3:
|
||||
// Key type 3: ????
|
||||
break;
|
||||
*/
|
||||
|
||||
case 4:
|
||||
// Key type 4: full matrix
|
||||
if (nvalues != 16) {
|
||||
xfile_cat.error()
|
||||
<< "Incorrect number of values in animation table: "
|
||||
<< nvalues << " for matrix data.\n";
|
||||
return false;
|
||||
}
|
||||
mat.set(values[0], values[1], values[2], values[3],
|
||||
values[4], values[5], values[6], values[7],
|
||||
values[8], values[9], values[10], values[11],
|
||||
values[12], values[13], values[14], values[15]);
|
||||
break;
|
||||
|
||||
default:
|
||||
xfile_cat.error()
|
||||
<< "Unsupported key type " << key_type << " in animation table.\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
table.back() = mat;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// Function: XFileToEggConverter::convert_mesh
|
||||
// Access: Private
|
||||
@ -441,7 +1025,8 @@ convert_transform(LPDIRECTXFILEDATA obj, EggGroupNode *egg_parent) {
|
||||
// structures.
|
||||
////////////////////////////////////////////////////////////////////
|
||||
bool XFileToEggConverter::
|
||||
convert_mesh(LPDIRECTXFILEDATA obj, EggGroupNode *egg_parent) {
|
||||
convert_mesh(LPDIRECTXFILEDATA obj, EggGroupNode *egg_parent,
|
||||
bool is_toplevel) {
|
||||
HRESULT hr;
|
||||
|
||||
Datagram raw_data;
|
||||
@ -449,9 +1034,12 @@ convert_mesh(LPDIRECTXFILEDATA obj, EggGroupNode *egg_parent) {
|
||||
return false;
|
||||
}
|
||||
|
||||
XFileMesh mesh(_egg_data->get_coordinate_system());
|
||||
mesh.set_name(get_object_name(obj));
|
||||
if (!mesh.read_mesh_data(raw_data)) {
|
||||
XFileMesh *mesh = new XFileMesh(_egg_data->get_coordinate_system());
|
||||
mesh->set_name(get_object_name(obj));
|
||||
mesh->set_egg_parent(egg_parent);
|
||||
|
||||
if (!mesh->read_mesh_data(raw_data)) {
|
||||
delete mesh;
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -460,7 +1048,7 @@ convert_mesh(LPDIRECTXFILEDATA obj, EggGroupNode *egg_parent) {
|
||||
|
||||
hr = obj->GetNextObject(&child_obj);
|
||||
while (hr == DXFILE_OK) {
|
||||
if (!convert_mesh_object(child_obj, mesh)) {
|
||||
if (!convert_mesh_object(child_obj, *mesh)) {
|
||||
return false;
|
||||
}
|
||||
hr = obj->GetNextObject(&child_obj);
|
||||
@ -470,11 +1058,14 @@ convert_mesh(LPDIRECTXFILEDATA obj, EggGroupNode *egg_parent) {
|
||||
xfile_cat.error()
|
||||
<< "Error extracting children of mesh " << get_object_name(obj)
|
||||
<< ".\n";
|
||||
delete mesh;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!mesh.create_polygons(egg_parent, this)) {
|
||||
return false;
|
||||
if (is_toplevel) {
|
||||
_toplevel_meshes.push_back(mesh);
|
||||
} else {
|
||||
_meshes.push_back(mesh);
|
||||
}
|
||||
|
||||
return true;
|
||||
@ -556,6 +1147,14 @@ convert_mesh_data_object(LPDIRECTXFILEDATA obj, XFileMesh &mesh) {
|
||||
return false;
|
||||
}
|
||||
|
||||
} else if (*type == DXFILEOBJ_XSkinMeshHeader) {
|
||||
// Quietly ignore a skin mesh header.
|
||||
|
||||
} else if (*type == DXFILEOBJ_SkinWeights) {
|
||||
if (!convert_skin_weights(obj, mesh)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
} else {
|
||||
if (xfile_cat.is_debug()) {
|
||||
xfile_cat.debug()
|
||||
@ -627,6 +1226,26 @@ convert_mesh_uvs(LPDIRECTXFILEDATA obj, XFileMesh &mesh) {
|
||||
return true;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// Function: XFileToEggConverter::convert_skin_weights
|
||||
// Access: Private
|
||||
// Description: Converts the indicated SkinWeights template
|
||||
// object.
|
||||
////////////////////////////////////////////////////////////////////
|
||||
bool XFileToEggConverter::
|
||||
convert_skin_weights(LPDIRECTXFILEDATA obj, XFileMesh &mesh) {
|
||||
Datagram raw_data;
|
||||
if (!get_data(obj, raw_data)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!mesh.read_skin_weights_data(raw_data)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// Function: XFileToEggConverter::convert_mesh_material_list
|
||||
// Access: Private
|
||||
@ -874,6 +1493,67 @@ convert_texture(LPDIRECTXFILEDATA obj, XFileMaterial &material) {
|
||||
return true;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// Function: XFileToEggConverter::create_polygons
|
||||
// Access: Private
|
||||
// Description: Creates all the polygons associated with
|
||||
// previously-saved meshes.
|
||||
////////////////////////////////////////////////////////////////////
|
||||
bool XFileToEggConverter::
|
||||
create_polygons() {
|
||||
bool okflag = true;
|
||||
|
||||
Meshes::const_iterator mi;
|
||||
for (mi = _meshes.begin(); mi != _meshes.end(); ++mi) {
|
||||
if (!(*mi)->create_polygons(this)) {
|
||||
okflag = false;
|
||||
}
|
||||
delete (*mi);
|
||||
}
|
||||
_meshes.clear();
|
||||
|
||||
for (mi = _toplevel_meshes.begin(); mi != _toplevel_meshes.end(); ++mi) {
|
||||
if (!_any_frames) {
|
||||
if (!(*mi)->create_polygons(this)) {
|
||||
okflag = false;
|
||||
}
|
||||
}
|
||||
delete (*mi);
|
||||
}
|
||||
_toplevel_meshes.clear();
|
||||
|
||||
return okflag;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// Function: XFileToEggConverter::create_hierarchy
|
||||
// Access: Private
|
||||
// Description: Creates the animation table hierarchies for the
|
||||
// previously-saved animation sets.
|
||||
////////////////////////////////////////////////////////////////////
|
||||
bool XFileToEggConverter::
|
||||
create_hierarchy() {
|
||||
bool okflag = true;
|
||||
|
||||
if (!_make_char && !_animation_sets.empty()) {
|
||||
xfile_cat.warning()
|
||||
<< "Ignoring animation data without -a.\n";
|
||||
}
|
||||
|
||||
AnimationSets::const_iterator asi;
|
||||
for (asi = _animation_sets.begin(); asi != _animation_sets.end(); ++asi) {
|
||||
if (_make_char) {
|
||||
if (!(*asi)->create_hierarchy(this)) {
|
||||
okflag = false;
|
||||
}
|
||||
}
|
||||
delete (*asi);
|
||||
}
|
||||
_animation_sets.clear();
|
||||
|
||||
return okflag;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// Function: XFileToEggConverter::get_object_name
|
||||
// Access: Private
|
||||
|
@ -20,9 +20,13 @@
|
||||
#define XFILETOEGGCONVERTER_H
|
||||
|
||||
#include "pandatoolbase.h"
|
||||
#include "xFileAnimationSet.h"
|
||||
#include "somethingToEggConverter.h"
|
||||
#include "eggTextureCollection.h"
|
||||
#include "eggMaterialCollection.h"
|
||||
#include "pvector.h"
|
||||
#include "pmap.h"
|
||||
#include "luse.h"
|
||||
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
@ -31,10 +35,11 @@
|
||||
#include <rmxfguid.h>
|
||||
#undef WIN32_LEAN_AND_MEAN
|
||||
|
||||
class EggGroupNode;
|
||||
class Datagram;
|
||||
class XFileMesh;
|
||||
class XFileMaterial;
|
||||
class EggGroup;
|
||||
class EggGroupNode;
|
||||
class EggTexture;
|
||||
class EggMaterial;
|
||||
|
||||
@ -56,24 +61,53 @@ public:
|
||||
virtual bool convert_file(const Filename &filename);
|
||||
void close();
|
||||
|
||||
EggGroup *get_dart_node() const;
|
||||
|
||||
EggTexture *create_unique_texture(const EggTexture ©);
|
||||
EggMaterial *create_unique_material(const EggMaterial ©);
|
||||
EggGroup *find_joint(const string &joint_name);
|
||||
EggGroup *find_joint(const string &joint_name,
|
||||
const LMatrix4f &matrix_offset);
|
||||
|
||||
public:
|
||||
bool _make_char;
|
||||
string _char_name;
|
||||
|
||||
private:
|
||||
typedef XFileAnimationSet::FrameData FrameData;
|
||||
|
||||
bool get_toplevel();
|
||||
bool convert_toplevel_object(LPDIRECTXFILEDATA obj, EggGroupNode *egg_parent,
|
||||
EggGroupNode *egg_toplevel, bool &any_frames);
|
||||
bool convert_toplevel_object(LPDIRECTXFILEDATA obj, EggGroupNode *egg_parent);
|
||||
bool convert_object(LPDIRECTXFILEOBJECT obj, EggGroupNode *egg_parent);
|
||||
bool convert_data_object(LPDIRECTXFILEDATA obj, EggGroupNode *egg_parent);
|
||||
bool convert_frame(LPDIRECTXFILEDATA obj, EggGroupNode *egg_parent);
|
||||
bool convert_transform(LPDIRECTXFILEDATA obj, EggGroupNode *egg_parent);
|
||||
bool convert_mesh(LPDIRECTXFILEDATA obj, EggGroupNode *egg_parent);
|
||||
bool convert_animation_set(LPDIRECTXFILEDATA obj);
|
||||
bool convert_animation_set_object(LPDIRECTXFILEOBJECT obj,
|
||||
XFileAnimationSet &animation_set);
|
||||
bool convert_animation_set_data_object(LPDIRECTXFILEDATA obj,
|
||||
XFileAnimationSet &animation_set);
|
||||
bool convert_animation(LPDIRECTXFILEDATA obj,
|
||||
XFileAnimationSet &animation_set);
|
||||
bool convert_animation_object(LPDIRECTXFILEOBJECT obj,
|
||||
const string &joint_name, FrameData &table);
|
||||
bool convert_animation_data_object(LPDIRECTXFILEDATA obj,
|
||||
const string &joint_name,
|
||||
FrameData &table);
|
||||
bool convert_animation_key(LPDIRECTXFILEDATA obj, const string &joint_name,
|
||||
FrameData &table);
|
||||
bool set_animation_frame(const string &joint_name, FrameData &table,
|
||||
int frame, int key_type,
|
||||
const float *values, int nvalues);
|
||||
bool convert_mesh(LPDIRECTXFILEDATA obj, EggGroupNode *egg_parent,
|
||||
bool is_toplevel);
|
||||
|
||||
bool convert_mesh_object(LPDIRECTXFILEOBJECT obj, XFileMesh &mesh);
|
||||
bool convert_mesh_data_object(LPDIRECTXFILEDATA obj, XFileMesh &mesh);
|
||||
bool convert_mesh_normals(LPDIRECTXFILEDATA obj, XFileMesh &mesh);
|
||||
bool convert_mesh_colors(LPDIRECTXFILEDATA obj, XFileMesh &mesh);
|
||||
bool convert_mesh_uvs(LPDIRECTXFILEDATA obj, XFileMesh &mesh);
|
||||
bool convert_skin_weights(LPDIRECTXFILEDATA obj, XFileMesh &mesh);
|
||||
bool convert_mesh_material_list(LPDIRECTXFILEDATA obj, XFileMesh &mesh);
|
||||
bool convert_material_list_object(LPDIRECTXFILEOBJECT obj, XFileMesh &mesh);
|
||||
bool convert_material_list_data_object(LPDIRECTXFILEDATA obj, XFileMesh &mesh);
|
||||
@ -82,12 +116,42 @@ private:
|
||||
bool convert_material_data_object(LPDIRECTXFILEDATA obj, XFileMaterial &material);
|
||||
bool convert_texture(LPDIRECTXFILEDATA obj, XFileMaterial &material);
|
||||
|
||||
bool create_polygons();
|
||||
bool create_hierarchy();
|
||||
|
||||
string get_object_name(LPDIRECTXFILEOBJECT obj);
|
||||
bool get_data(LPDIRECTXFILEDATA obj, Datagram &raw_data);
|
||||
|
||||
LPDIRECTXFILE _dx_file;
|
||||
LPDIRECTXFILEENUMOBJECT _dx_file_enum;
|
||||
|
||||
bool _any_frames;
|
||||
|
||||
typedef pvector<XFileMesh *> Meshes;
|
||||
Meshes _meshes;
|
||||
Meshes _toplevel_meshes;
|
||||
|
||||
typedef pvector<XFileAnimationSet *> AnimationSets;
|
||||
AnimationSets _animation_sets;
|
||||
|
||||
typedef pmap<LMatrix4f, EggGroup *> OffsetJoints;
|
||||
|
||||
// A joint definition consists of the pointer to the EggGroup that
|
||||
// represents the actual joint, plus a table of synthetic joints
|
||||
// that were created for each animation set's offset matrix (we need
|
||||
// to create a different joint to apply each unique offset matrix in
|
||||
// an animation set).
|
||||
class JointDef {
|
||||
public:
|
||||
EggGroup *_node;
|
||||
OffsetJoints _offsets;
|
||||
};
|
||||
|
||||
typedef pmap<string, JointDef> Joints;
|
||||
Joints _joints;
|
||||
|
||||
EggGroup *_dart_node;
|
||||
|
||||
EggTextureCollection _textures;
|
||||
EggMaterialCollection _materials;
|
||||
};
|
||||
|
@ -1,5 +1,6 @@
|
||||
|
||||
#include "config_xfile.cxx"
|
||||
#include "xFileAnimationSet.cxx"
|
||||
#include "xFileFace.cxx"
|
||||
#include "xFileMaker.cxx"
|
||||
#include "xFileMaterial.cxx"
|
||||
|
@ -33,7 +33,7 @@ EggToX() : EggToSomething("DirectX", ".x", true, false) {
|
||||
("This program reads an Egg file and outputs an equivalent, "
|
||||
"or nearly equivalent, DirectX-style .x file. Only simple "
|
||||
"hierarchy and polygon meshes are supported; advanced features "
|
||||
"like LOD's, decals, and characters cannot be supported.");
|
||||
"like LOD's, decals, and animation or skinning are not supported.");
|
||||
|
||||
add_option
|
||||
("m", "", 0,
|
||||
|
@ -36,9 +36,19 @@ XFileToEgg() :
|
||||
add_transform_options();
|
||||
|
||||
set_program_description
|
||||
("This program converts DirectX retained-mode (.x) files to egg. This "
|
||||
"is a simple converter that only supports basic polygons, materials, "
|
||||
"and textures, in a hierarchy; animation is not supported at this time.");
|
||||
("This program converts DirectX retained-mode (.x) files to egg. "
|
||||
"Polygon meshes, materials, and textures, as well as skeleton "
|
||||
"animation and skinning data, are supported. All animations "
|
||||
"found in the source .x file are written together into the same "
|
||||
"egg file.");
|
||||
|
||||
add_option
|
||||
("a", "name", 0,
|
||||
"Convert as an animatable model, converting Frames into Joints. This "
|
||||
"should be specified for a model which is intended to be animated. The "
|
||||
"default is to convert the model as a normal static model, which is "
|
||||
"usually more optimal if animation is not required.",
|
||||
&XFileToEgg::dispatch_string, &_make_char, &_char_name);
|
||||
|
||||
redescribe_option
|
||||
("cs",
|
||||
@ -60,6 +70,9 @@ run() {
|
||||
XFileToEggConverter converter;
|
||||
converter.set_egg_data(&_data, false);
|
||||
|
||||
converter._make_char = _make_char;
|
||||
converter._char_name = _char_name;
|
||||
|
||||
// Copy in the path and animation parameters.
|
||||
apply_parameters(converter);
|
||||
|
||||
|
@ -35,6 +35,10 @@ public:
|
||||
XFileToEgg();
|
||||
|
||||
void run();
|
||||
|
||||
public:
|
||||
bool _make_char;
|
||||
string _char_name;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
Loading…
x
Reference in New Issue
Block a user