goat3d
changeset 82:70b7c41a4f17
foo
author | John Tsiombikas <nuclear@member.fsf.org> |
---|---|
date | Sun, 11 May 2014 22:04:54 +0300 |
parents | 7458b8568463 |
children | 57e745dd13c2 |
files | goatview/Makefile goatview/src/goatview.cc goatview/src/goatview.h goatview/src/main.cc goatview/src/opengl.h src/aabox.cc src/aabox.h src/goat3d.cc src/goat3d.h src/goat3d_writexml.cc |
diffstat | 10 files changed, 208 insertions(+), 80 deletions(-) [+] |
line diff
1.1 --- a/goatview/Makefile Thu May 08 23:05:29 2014 +0300 1.2 +++ b/goatview/Makefile Sun May 11 22:04:54 2014 +0300 1.3 @@ -10,7 +10,7 @@ 1.4 1.5 goat_root = .. 1.6 1.7 -CFLAGS = -pedantic -Wall -g $(pic) -I$(goat_root)/src $(qtinc) 1.8 +CFLAGS = -Wall -Wno-cpp -g $(pic) -I$(goat_root)/src $(qtinc) 1.9 CXXFLAGS = -std=c++11 $(CFLAGS) 1.10 LDFLAGS = $(libgoat) $(libgl) $(qtlib) -lvmath 1.11 MOC = moc
2.1 --- a/goatview/src/goatview.cc Thu May 08 23:05:29 2014 +0300 2.2 +++ b/goatview/src/goatview.cc Sun May 11 22:04:54 2014 +0300 2.3 @@ -1,20 +1,47 @@ 2.4 #include <stdio.h> 2.5 +#include "opengl.h" 2.6 #include <QtOpenGL/QtOpenGL> 2.7 -#include "opengl.h" 2.8 #include <vmath/vmath.h> 2.9 #include "goatview.h" 2.10 #include "goat3d.h" 2.11 2.12 +static void update_tree(QTreeWidget *tree); 2.13 +static void add_tree(QTreeWidget *tree, goat3d_node *node, QTreeWidgetItem *parent); 2.14 + 2.15 static void draw_node(goat3d_node *node); 2.16 +static void draw_mesh(goat3d_mesh *mesh); 2.17 2.18 goat3d *scene; 2.19 -QSettings *settings; 2.20 2.21 static long anim_time; 2.22 static float cam_theta, cam_phi, cam_dist = 8; 2.23 static float fov = 60.0; 2.24 +static bool use_nodes = true; 2.25 2.26 -bool load_scene(const char *fname) 2.27 + 2.28 +GoatView::GoatView() 2.29 +{ 2.30 + glview = 0; 2.31 + 2.32 + make_menu(); 2.33 + make_dock(); 2.34 + make_center(); 2.35 + 2.36 + statusBar(); 2.37 + 2.38 + setWindowTitle("GoatView"); 2.39 + 2.40 + QSettings *settings = new QSettings; 2.41 + resize(settings->value("main/size", QSize(1024, 768)).toSize()); 2.42 + move(settings->value("main/pos", QPoint(100, 100)).toPoint()); 2.43 + delete settings; 2.44 +} 2.45 + 2.46 +GoatView::~GoatView() 2.47 +{ 2.48 +} 2.49 + 2.50 +bool GoatView::load_scene(const char *fname) 2.51 { 2.52 if(scene) { 2.53 goat3d_free(scene); 2.54 @@ -24,39 +51,27 @@ 2.55 } 2.56 2.57 float bmin[3], bmax[3]; 2.58 - goat3d_get_bounds(scene, bmin, bmax); 2.59 - float bsize = (Vector3(bmax[0], bmax[1], bmax[2]) - Vector3(bmin[0], bmin[1], bmin[2])).length(); 2.60 - cam_dist = bsize / tan(DEG_TO_RAD(fov) / 2.0) + bsize; 2.61 + if(goat3d_get_bounds(scene, bmin, bmax) != -1) { 2.62 + float bsize = (Vector3(bmax[0], bmax[1], bmax[2]) - Vector3(bmin[0], bmin[1], bmin[2])).length(); 2.63 + cam_dist = bsize / tan(DEG_TO_RAD(fov) / 2.0) + bsize; 2.64 + printf("bounds size: %f, cam_dist: %f\n", bsize, cam_dist); 2.65 + } 2.66 2.67 - printf("bounds size: %f, cam_dist: %f\n", bsize, cam_dist); 2.68 + update_tree(scntree); 2.69 return true; 2.70 } 2.71 2.72 -GoatView::GoatView() 2.73 -{ 2.74 - make_menu(); 2.75 - make_dock(); 2.76 - make_center(); 2.77 - 2.78 - statusBar(); 2.79 - 2.80 - setWindowTitle("GoatView"); 2.81 - resize(settings->value("main/size", QSize(1024, 768)).toSize()); 2.82 - move(settings->value("main/pos", QPoint(100, 100)).toPoint()); 2.83 -} 2.84 - 2.85 -GoatView::~GoatView() 2.86 -{ 2.87 -} 2.88 - 2.89 void GoatView::closeEvent(QCloseEvent *ev) 2.90 { 2.91 + QSettings *settings = new QSettings; 2.92 settings->setValue("main/size", size()); 2.93 settings->setValue("main/pos", pos()); 2.94 + delete settings; 2.95 } 2.96 2.97 bool GoatView::make_menu() 2.98 { 2.99 + // file menu 2.100 QMenu *menu_file = menuBar()->addMenu("&File"); 2.101 2.102 QAction *act_open_sce = new QAction("&Open Scene", this); 2.103 @@ -72,6 +87,15 @@ 2.104 act_quit->setShortcuts(QKeySequence::Quit); 2.105 connect(act_quit, &QAction::triggered, [&](){qApp->quit();}); 2.106 menu_file->addAction(act_quit); 2.107 + 2.108 + // view menu 2.109 + QMenu *menu_view = menuBar()->addMenu("&View"); 2.110 + 2.111 + QAction *act_use_nodes = new QAction("use nodes", this); 2.112 + act_use_nodes->setCheckable(true); 2.113 + act_use_nodes->setChecked(use_nodes); 2.114 + connect(act_use_nodes, &QAction::triggered, this, [&](){use_nodes = !use_nodes; glview->updateGL();}); 2.115 + menu_view->addAction(act_use_nodes); 2.116 return true; 2.117 } 2.118 2.119 @@ -82,15 +106,27 @@ 2.120 QVBoxLayout *dock_vbox = new QVBoxLayout; 2.121 dock_cont->setLayout(dock_vbox); 2.122 2.123 - QPushButton *bn_quit = new QPushButton("quit"); 2.124 - dock_vbox->addWidget(bn_quit); 2.125 - connect(bn_quit, &QPushButton::clicked, [&](){qApp->quit();}); 2.126 - 2.127 QDockWidget *dock = new QDockWidget("Scene graph", this); 2.128 dock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); 2.129 dock->setWidget(dock_cont); 2.130 addDockWidget(Qt::LeftDockWidgetArea, dock); 2.131 2.132 + // make the tree view widget 2.133 + scntree = new QTreeWidget; 2.134 + scntree->setColumnCount(1); 2.135 + QStringList hdrstr; 2.136 + hdrstr << "Node";// << "Type"; 2.137 + scntree->setHeaderItem(new QTreeWidgetItem((QTreeWidget*)0, hdrstr)); 2.138 + scntree->setAlternatingRowColors(true); 2.139 + dock_vbox->addWidget(scntree); 2.140 + 2.141 + update_tree(scntree); 2.142 + 2.143 + // misc 2.144 + QPushButton *bn_quit = new QPushButton("quit"); 2.145 + dock_vbox->addWidget(bn_quit); 2.146 + connect(bn_quit, &QPushButton::clicked, [&](){qApp->quit();}); 2.147 + 2.148 // ---- bottom dock ---- 2.149 dock_cont = new QWidget; 2.150 QHBoxLayout *dock_hbox = new QHBoxLayout; 2.151 @@ -110,8 +146,8 @@ 2.152 2.153 bool GoatView::make_center() 2.154 { 2.155 - GoatViewport *vport = new GoatViewport; 2.156 - setCentralWidget(vport); 2.157 + glview = new GoatViewport(this); 2.158 + setCentralWidget(glview); 2.159 return true; 2.160 } 2.161 2.162 @@ -135,11 +171,48 @@ 2.163 statusBar()->showMessage("opening animation..."); 2.164 } 2.165 2.166 +static void update_tree(QTreeWidget *tree) 2.167 +{ 2.168 + if(!scene) return; 2.169 + 2.170 + int num_nodes = goat3d_get_node_count(scene); 2.171 + for(int i=0; i<num_nodes; i++) { 2.172 + goat3d_node *node = goat3d_get_node(scene, i); 2.173 + if(goat3d_get_node_parent(node)) { 2.174 + continue; 2.175 + } 2.176 + 2.177 + // only add the root nodes, the rest will be added recursively by them 2.178 + add_tree(tree, node, 0); 2.179 + } 2.180 + tree->expandAll(); 2.181 +} 2.182 + 2.183 +static void add_tree(QTreeWidget *tree, goat3d_node *node, QTreeWidgetItem *parent) 2.184 +{ 2.185 + //char icon_name[64]; 2.186 + //sprintf(icon_name, ":/icons/icons/icon_%s.png", node->get_type()); 2.187 + 2.188 + QStringList row; 2.189 + row << goat3d_get_node_name(node) << "M"; 2.190 + QTreeWidgetItem *item = new QTreeWidgetItem(parent, row); 2.191 + //item->setIcon(0, QIcon(icon_name)); 2.192 + tree->addTopLevelItem(item); 2.193 + 2.194 + int num_children = goat3d_get_node_child_count(node); 2.195 + for(int i=0; i<num_children; i++) { 2.196 + add_tree(tree, goat3d_get_node_child(node, i), item); 2.197 + } 2.198 +} 2.199 + 2.200 + 2.201 2.202 // ---- OpenGL viewport ---- 2.203 -GoatViewport::GoatViewport() 2.204 +GoatViewport::GoatViewport(QWidget *main_win) 2.205 : QGLWidget(QGLFormat(QGL::DepthBuffer)) 2.206 { 2.207 + this->main_win = main_win; 2.208 + initialized = false; 2.209 } 2.210 2.211 GoatViewport::~GoatViewport() 2.212 @@ -151,10 +224,24 @@ 2.213 return QSize(800, 600); 2.214 } 2.215 2.216 +#define CRITICAL(error, detail) \ 2.217 + do { \ 2.218 + fprintf(stderr, "%s: %s\n", error, detail); \ 2.219 + QMessageBox::critical(main_win, error, detail); \ 2.220 + abort(); \ 2.221 + } while(0) 2.222 + 2.223 void GoatViewport::initializeGL() 2.224 { 2.225 + if(initialized) return; 2.226 + initialized = true; 2.227 + 2.228 init_opengl(); 2.229 2.230 + if(!GLEW_ARB_transpose_matrix) { 2.231 + CRITICAL("OpenGL initialization failed", "ARB_transpose_matrix extension not found!"); 2.232 + } 2.233 + 2.234 glClearColor(0.1f, 0.1f, 0.1f, 1.0f); 2.235 2.236 glEnable(GL_DEPTH_TEST); 2.237 @@ -179,61 +266,39 @@ 2.238 glRotatef(cam_phi, 1, 0, 0); 2.239 glRotatef(cam_theta, 0, 1, 0); 2.240 2.241 - if(scene) { 2.242 + if(!scene) return; 2.243 + 2.244 + if(use_nodes) { 2.245 int node_count = goat3d_get_node_count(scene); 2.246 for(int i=0; i<node_count; i++) { 2.247 goat3d_node *node = goat3d_get_node(scene, i); 2.248 - //if(!goat3d_get_node_parent(node)) { 2.249 - draw_node(node); // only draw root nodes, the rest will be drawn recursively 2.250 - //} 2.251 + draw_node(node); // only draw root nodes, the rest will be drawn recursively 2.252 + } 2.253 + } else { 2.254 + int mesh_count = goat3d_get_mesh_count(scene); 2.255 + for(int i=0; i<mesh_count; i++) { 2.256 + goat3d_mesh *mesh = goat3d_get_mesh(scene, i); 2.257 + draw_mesh(mesh); 2.258 } 2.259 } 2.260 } 2.261 2.262 +#ifndef GLEW_ARB_transpose_matrix 2.263 +#error "GLEW_ARB_transpose_matrix undefined?" 2.264 +#endif 2.265 + 2.266 static void draw_node(goat3d_node *node) 2.267 { 2.268 float xform[16]; 2.269 goat3d_get_node_matrix(node, xform, anim_time); 2.270 - for(int i=0; i<4; i++) { 2.271 - for(int j=0; j<i; j++) { 2.272 - float tmp = xform[i * 4 + j]; 2.273 - xform[i * 4 + j] = xform[j * 4 + i]; 2.274 - xform[j * 4 + i] = tmp; 2.275 - } 2.276 - } 2.277 2.278 glPushMatrix(); 2.279 - glMultMatrixf(xform); 2.280 + glMultTransposeMatrixf(xform); 2.281 2.282 if(goat3d_get_node_type(node) == GOAT3D_NODE_MESH) { 2.283 goat3d_mesh *mesh = (goat3d_mesh*)goat3d_get_node_object(node); 2.284 2.285 - int num_faces = goat3d_get_mesh_face_count(mesh); 2.286 - int num_verts = goat3d_get_mesh_attrib_count(mesh, GOAT3D_MESH_ATTR_VERTEX); 2.287 - 2.288 - glEnableClientState(GL_VERTEX_ARRAY); 2.289 - glVertexPointer(3, GL_FLOAT, 0, goat3d_get_mesh_attribs(mesh, GOAT3D_MESH_ATTR_VERTEX)); 2.290 - 2.291 - float *data; 2.292 - if((data = (float*)goat3d_get_mesh_attribs(mesh, GOAT3D_MESH_ATTR_NORMAL))) { 2.293 - glEnableClientState(GL_NORMAL_ARRAY); 2.294 - glNormalPointer(GL_FLOAT, 0, data); 2.295 - } 2.296 - if((data = (float*)goat3d_get_mesh_attribs(mesh, GOAT3D_MESH_ATTR_TEXCOORD))) { 2.297 - glEnableClientState(GL_TEXTURE_COORD_ARRAY); 2.298 - glTexCoordPointer(2, GL_FLOAT, 0, data); 2.299 - } 2.300 - 2.301 - int *indices; 2.302 - if((indices = goat3d_get_mesh_faces(mesh))) { 2.303 - glDrawElements(GL_TRIANGLES, num_faces * 3, GL_UNSIGNED_INT, indices); 2.304 - } else { 2.305 - glDrawArrays(GL_TRIANGLES, 0, num_verts * 3); 2.306 - } 2.307 - 2.308 - glDisableClientState(GL_VERTEX_ARRAY); 2.309 - glDisableClientState(GL_NORMAL_ARRAY); 2.310 - glDisableClientState(GL_TEXTURE_COORD_ARRAY); 2.311 + draw_mesh(mesh); 2.312 } 2.313 2.314 /*int num_child = goat3d_get_node_child_count(node); 2.315 @@ -244,6 +309,36 @@ 2.316 glPopMatrix(); 2.317 } 2.318 2.319 +static void draw_mesh(goat3d_mesh *mesh) 2.320 +{ 2.321 + int num_faces = goat3d_get_mesh_face_count(mesh); 2.322 + int num_verts = goat3d_get_mesh_attrib_count(mesh, GOAT3D_MESH_ATTR_VERTEX); 2.323 + 2.324 + glEnableClientState(GL_VERTEX_ARRAY); 2.325 + glVertexPointer(3, GL_FLOAT, 0, goat3d_get_mesh_attribs(mesh, GOAT3D_MESH_ATTR_VERTEX)); 2.326 + 2.327 + float *data; 2.328 + if((data = (float*)goat3d_get_mesh_attribs(mesh, GOAT3D_MESH_ATTR_NORMAL))) { 2.329 + glEnableClientState(GL_NORMAL_ARRAY); 2.330 + glNormalPointer(GL_FLOAT, 0, data); 2.331 + } 2.332 + if((data = (float*)goat3d_get_mesh_attribs(mesh, GOAT3D_MESH_ATTR_TEXCOORD))) { 2.333 + glEnableClientState(GL_TEXTURE_COORD_ARRAY); 2.334 + glTexCoordPointer(2, GL_FLOAT, 0, data); 2.335 + } 2.336 + 2.337 + int *indices; 2.338 + if((indices = goat3d_get_mesh_faces(mesh))) { 2.339 + glDrawElements(GL_TRIANGLES, num_faces * 3, GL_UNSIGNED_INT, indices); 2.340 + } else { 2.341 + glDrawArrays(GL_TRIANGLES, 0, num_verts * 3); 2.342 + } 2.343 + 2.344 + glDisableClientState(GL_VERTEX_ARRAY); 2.345 + glDisableClientState(GL_NORMAL_ARRAY); 2.346 + glDisableClientState(GL_TEXTURE_COORD_ARRAY); 2.347 +} 2.348 + 2.349 2.350 static float prev_x, prev_y; 2.351 void GoatViewport::mousePressEvent(QMouseEvent *ev)
3.1 --- a/goatview/src/goatview.h Thu May 08 23:05:29 2014 +0300 3.2 +++ b/goatview/src/goatview.h Sun May 11 22:04:54 2014 +0300 3.3 @@ -7,13 +7,16 @@ 3.4 #include "goat3d.h" 3.5 3.6 extern goat3d *scene; 3.7 -extern QSettings *settings; 3.8 3.9 -bool load_scene(const char *fname); 3.10 +class GoatViewport; 3.11 3.12 class GoatView : public QMainWindow { 3.13 Q_OBJECT 3.14 private: 3.15 + GoatViewport *glview; 3.16 + QStandardItemModel *sgmodel; // scene graph model 3.17 + QTreeWidget *scntree; 3.18 + 3.19 void closeEvent(QCloseEvent *ev); 3.20 bool make_menu(); 3.21 bool make_dock(); 3.22 @@ -26,12 +29,19 @@ 3.23 public: 3.24 GoatView(); 3.25 ~GoatView(); 3.26 + 3.27 + bool load_scene(const char *fname); 3.28 }; 3.29 3.30 class GoatViewport : public QGLWidget { 3.31 +private: 3.32 Q_OBJECT 3.33 + 3.34 + QWidget *main_win; 3.35 + bool initialized; 3.36 + 3.37 public: 3.38 - GoatViewport(); 3.39 + GoatViewport(QWidget *main_win); 3.40 ~GoatViewport(); 3.41 3.42 QSize sizeHint() const;
4.1 --- a/goatview/src/main.cc Thu May 08 23:05:29 2014 +0300 4.2 +++ b/goatview/src/main.cc Sun May 11 22:04:54 2014 +0300 4.3 @@ -8,7 +8,8 @@ 4.4 app.setOrganizationName("Mutant Stargoat"); 4.5 app.setOrganizationDomain("mutantstargoat.com"); 4.6 app.setApplicationName("GoatView"); 4.7 - settings = new QSettings; 4.8 + 4.9 + GoatView gview; 4.10 4.11 QCommandLineParser argparse; 4.12 argparse.addHelpOption(); 4.13 @@ -25,7 +26,7 @@ 4.14 } 4.15 std::string fname = args.at(0).toStdString(); 4.16 printf("loading scene file: %s ...\n", fname.c_str()); 4.17 - if(!load_scene(fname.c_str())) { 4.18 + if(!gview.load_scene(fname.c_str())) { 4.19 fprintf(stderr, "failed to load scene: %s\n", fname.c_str()); 4.20 return 1; 4.21 } 4.22 @@ -43,7 +44,6 @@ 4.23 } 4.24 } 4.25 4.26 - GoatView gview; 4.27 gview.show(); 4.28 4.29 return app.exec();
5.1 --- a/goatview/src/opengl.h Thu May 08 23:05:29 2014 +0300 5.2 +++ b/goatview/src/opengl.h Sun May 11 22:04:54 2014 +0300 5.3 @@ -2,6 +2,7 @@ 5.4 #define OPENGL_H_ 5.5 5.6 #include <GL/glew.h> 5.7 +#define QT_NO_OPENGL_ES_2 5.8 5.9 #ifdef WIN32 5.10 #include <windows.h> 5.11 @@ -25,4 +26,4 @@ 5.12 } 5.13 #endif 5.14 5.15 -#endif /* OPENGL_G_ */ 5.16 +#endif /* OPENGL_H_ */
6.1 --- a/src/aabox.cc Thu May 08 23:05:29 2014 +0300 6.2 +++ b/src/aabox.cc Sun May 11 22:04:54 2014 +0300 6.3 @@ -14,6 +14,16 @@ 6.4 { 6.5 } 6.6 6.7 +bool AABox::operator ==(const AABox &rhs) const 6.8 +{ 6.9 + return bmin == rhs.bmin && bmax == rhs.bmax; 6.10 +} 6.11 + 6.12 +bool AABox::operator !=(const AABox &rhs) const 6.13 +{ 6.14 + return !(*this == rhs); 6.15 +} 6.16 + 6.17 AABox g3dimpl::aabox_union(const AABox &a, const AABox &b) 6.18 { 6.19 Vector3 bmin, bmax;
7.1 --- a/src/aabox.h Thu May 08 23:05:29 2014 +0300 7.2 +++ b/src/aabox.h Sun May 11 22:04:54 2014 +0300 7.3 @@ -11,6 +11,9 @@ 7.4 7.5 AABox(); 7.6 AABox(const Vector3 &b0, const Vector3 &b1); 7.7 + 7.8 + bool operator ==(const AABox &rhs) const; 7.9 + bool operator !=(const AABox &rhs) const; 7.10 }; 7.11 7.12 AABox aabox_union(const AABox &a, const AABox &b);
8.1 --- a/src/goat3d.cc Thu May 08 23:05:29 2014 +0300 8.2 +++ b/src/goat3d.cc Sun May 11 22:04:54 2014 +0300 8.3 @@ -253,13 +253,18 @@ 8.4 return &g->scn->get_ambient().x; 8.5 } 8.6 8.7 -GOAT3DAPI void goat3d_get_bounds(const struct goat3d *g, float *bmin, float *bmax) 8.8 +GOAT3DAPI int goat3d_get_bounds(const struct goat3d *g, float *bmin, float *bmax) 8.9 { 8.10 AABox bbox = g->scn->get_bounds(); 8.11 + if(bbox == AABox()) { 8.12 + return -1; 8.13 + } 8.14 + 8.15 for(int i=0; i<3; i++) { 8.16 bmin[i] = bbox.bmin[i]; 8.17 bmax[i] = bbox.bmax[i]; 8.18 } 8.19 + return 0; 8.20 } 8.21 8.22 // ---- materials ---- 8.23 @@ -926,7 +931,7 @@ 8.24 8.25 GOAT3DAPI void goat3d_get_node_matrix(const struct goat3d_node *node, float *matrix, long tmsec) 8.26 { 8.27 - node->get_xform(tmsec, (Matrix4x4*)matrix); 8.28 + node->get_node_xform(tmsec, (Matrix4x4*)matrix); 8.29 } 8.30 8.31
9.1 --- a/src/goat3d.h Thu May 08 23:05:29 2014 +0300 9.2 +++ b/src/goat3d.h Sun May 11 22:04:54 2014 +0300 9.3 @@ -123,7 +123,7 @@ 9.4 GOAT3DAPI void goat3d_set_ambient3f(struct goat3d *g, float ar, float ag, float ab); 9.5 GOAT3DAPI const float *goat3d_get_ambient(const struct goat3d *g); 9.6 9.7 -GOAT3DAPI void goat3d_get_bounds(const struct goat3d *g, float *bmin, float *bmax); 9.8 +GOAT3DAPI int goat3d_get_bounds(const struct goat3d *g, float *bmin, float *bmax); 9.9 9.10 /* materials */ 9.11 GOAT3DAPI void goat3d_add_mtl(struct goat3d *g, struct goat3d_material *mtl);
10.1 --- a/src/goat3d_writexml.cc Thu May 08 23:05:29 2014 +0300 10.2 +++ b/src/goat3d_writexml.cc Sun May 11 22:04:54 2014 +0300 10.3 @@ -33,6 +33,8 @@ 10.4 10.5 bool Scene::savexml(goat3d_io *io) const 10.6 { 10.7 + xmlout(io, 0, "<!-- vi:set filetype=xml: -->\n"); 10.8 + xmlout(io, 0, "<!-- Goat3D XML scene description -->\n"); 10.9 xmlout(io, 0, "<scene>\n"); 10.10 10.11 // write environment stuff 10.12 @@ -62,6 +64,8 @@ 10.13 10.14 bool Scene::save_anim_xml(goat3d_io *io) const 10.15 { 10.16 + xmlout(io, 0, "<!-- vi:set filetype=xml: -->\n"); 10.17 + xmlout(io, 0, "<!-- Goat3D XML animation -->\n"); 10.18 xmlout(io, 0, "<anim>\n"); 10.19 10.20 if(!nodes.empty()) {