|
|
@@ -19,7 +19,14 @@
|
|
|
#include "streamReader.h"
|
|
|
#include "virtualFileSystem.h"
|
|
|
#include "eggPolygon.h"
|
|
|
+#include "nodePath.h"
|
|
|
+#include "geomTriangles.h"
|
|
|
+#include "geomPoints.h"
|
|
|
+#include "colorAttrib.h"
|
|
|
+#include "shadeModelAttrib.h"
|
|
|
#include "dcast.h"
|
|
|
+#include "triangulator3.h"
|
|
|
+#include "config_egg2pg.h"
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
|
// Function: ObjToEggConverter::Constructor
|
|
|
@@ -95,6 +102,22 @@ supports_compressed() const {
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
+////////////////////////////////////////////////////////////////////
|
|
|
+// Function: ObjToEggConverter::supports_convert_to_node
|
|
|
+// Access: Published, Virtual
|
|
|
+// Description: Returns true if this converter can directly convert
|
|
|
+// the model type to internal Panda memory structures,
|
|
|
+// given the indicated options, or false otherwise. If
|
|
|
+// this returns true, then convert_to_node() may be
|
|
|
+// called to perform the conversion, which may be faster
|
|
|
+// than calling convert_file() if the ultimate goal is a
|
|
|
+// PandaNode anyway.
|
|
|
+////////////////////////////////////////////////////////////////////
|
|
|
+bool ObjToEggConverter::
|
|
|
+supports_convert_to_node(const LoaderOptions &options) const {
|
|
|
+ return true;
|
|
|
+}
|
|
|
+
|
|
|
////////////////////////////////////////////////////////////////////
|
|
|
// Function: ObjToEggConverter::convert_file
|
|
|
// Access: Public, Virtual
|
|
|
@@ -116,10 +139,40 @@ convert_file(const Filename &filename) {
|
|
|
return !had_error();
|
|
|
}
|
|
|
|
|
|
+////////////////////////////////////////////////////////////////////
|
|
|
+// Function: ObjToEggConverter::convert_to_node
|
|
|
+// Access: Public, Virtual
|
|
|
+// Description: Reads the input file and directly produces a
|
|
|
+// ready-to-render model file as a PandaNode. Returns
|
|
|
+// NULL on failure, or if it is not supported. (This
|
|
|
+// functionality is not supported by all converter
|
|
|
+// types; see supports_convert_to_node()).
|
|
|
+////////////////////////////////////////////////////////////////////
|
|
|
+PT(PandaNode) ObjToEggConverter::
|
|
|
+convert_to_node(const LoaderOptions &options, const Filename &filename) {
|
|
|
+ clear_error();
|
|
|
+
|
|
|
+ _root_node = new PandaNode("");
|
|
|
+ _current_vertex_data = new VertexData(_root_node, "root");
|
|
|
+
|
|
|
+ if (!process_node(filename)) {
|
|
|
+ _error = true;
|
|
|
+ }
|
|
|
+
|
|
|
+ _current_vertex_data->close_geom(this);
|
|
|
+ delete _current_vertex_data;
|
|
|
+
|
|
|
+ if (had_error()) {
|
|
|
+ return NULL;
|
|
|
+ }
|
|
|
+
|
|
|
+ return _root_node;
|
|
|
+}
|
|
|
+
|
|
|
////////////////////////////////////////////////////////////////////
|
|
|
// Function: ObjToEggConverter::process
|
|
|
// Access: Protected
|
|
|
-// Description:
|
|
|
+// Description: Reads the file and converts it to egg structures.
|
|
|
////////////////////////////////////////////////////////////////////
|
|
|
bool ObjToEggConverter::
|
|
|
process(const Filename &filename) {
|
|
|
@@ -131,11 +184,14 @@ process(const Filename &filename) {
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
- _vi = 1;
|
|
|
- _vti = 1;
|
|
|
- _xvti = 1;
|
|
|
- _vni = 1;
|
|
|
+ _v_table.clear();
|
|
|
+ _vn_table.clear();
|
|
|
+ _rgb_table.clear();
|
|
|
+ _vt_table.clear();
|
|
|
_ref_plane_res.set(1.0, 1.0);
|
|
|
+ _v4_given = false;
|
|
|
+ _vt3_given = false;
|
|
|
+ _f_given = false;
|
|
|
|
|
|
_vpool = new EggVertexPool("vpool");
|
|
|
_egg_data->add_child(_vpool);
|
|
|
@@ -192,6 +248,8 @@ process_line(const string &line) {
|
|
|
return process_vt(words);
|
|
|
} else if (tag == "xvt") {
|
|
|
return process_xvt(words);
|
|
|
+ } else if (tag == "xvc") {
|
|
|
+ return process_xvc(words);
|
|
|
} else if (tag == "vn") {
|
|
|
return process_vn(words);
|
|
|
} else if (tag == "f") {
|
|
|
@@ -231,9 +289,9 @@ process_ref_plane_res(const string &line) {
|
|
|
}
|
|
|
|
|
|
bool okflag = true;
|
|
|
- LPoint3d pos;
|
|
|
- okflag &= string_to_double(words[1], _ref_plane_res[0]);
|
|
|
- okflag &= string_to_double(words[2], _ref_plane_res[1]);
|
|
|
+ LPoint3 pos;
|
|
|
+ okflag &= string_to_stdfloat(words[1], _ref_plane_res[0]);
|
|
|
+ okflag &= string_to_stdfloat(words[2], _ref_plane_res[1]);
|
|
|
|
|
|
if (!okflag) {
|
|
|
objegg_cat.error()
|
|
|
@@ -251,17 +309,24 @@ process_ref_plane_res(const string &line) {
|
|
|
////////////////////////////////////////////////////////////////////
|
|
|
bool ObjToEggConverter::
|
|
|
process_v(vector_string &words) {
|
|
|
- if (words.size() != 4 && words.size() != 7) {
|
|
|
+ if (words.size() != 4 && words.size() != 5 &&
|
|
|
+ words.size() != 7 && words.size() != 8) {
|
|
|
objegg_cat.error()
|
|
|
<< "Wrong number of tokens at line " << _line_number << "\n";
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
bool okflag = true;
|
|
|
- LPoint3d pos;
|
|
|
- okflag &= string_to_double(words[1], pos[0]);
|
|
|
- okflag &= string_to_double(words[2], pos[1]);
|
|
|
- okflag &= string_to_double(words[3], pos[2]);
|
|
|
+ LPoint4 pos;
|
|
|
+ okflag &= string_to_stdfloat(words[1], pos[0]);
|
|
|
+ okflag &= string_to_stdfloat(words[2], pos[1]);
|
|
|
+ okflag &= string_to_stdfloat(words[3], pos[2]);
|
|
|
+ if (words.size() == 5 || words.size() == 8) {
|
|
|
+ okflag &= string_to_stdfloat(words[4], pos[3]);
|
|
|
+ _v4_given = true;
|
|
|
+ } else {
|
|
|
+ pos[3] = 1.0;
|
|
|
+ }
|
|
|
|
|
|
if (!okflag) {
|
|
|
objegg_cat.error()
|
|
|
@@ -269,27 +334,28 @@ process_v(vector_string &words) {
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
- EggVertex *vertex = get_vertex(_vi);
|
|
|
- vertex->set_pos(pos);
|
|
|
+ _v_table.push_back(pos);
|
|
|
|
|
|
// Meshlab format might include an RGB color following the vertex
|
|
|
// position.
|
|
|
- if (words.size() == 7) {
|
|
|
- double r, g, b;
|
|
|
- okflag &= string_to_double(words[4], r);
|
|
|
- okflag &= string_to_double(words[5], g);
|
|
|
- okflag &= string_to_double(words[6], b);
|
|
|
+ if (words.size() == 7 && words.size() == 8) {
|
|
|
+ size_t si = words.size();
|
|
|
+ LVecBase3 rgb;
|
|
|
+ okflag &= string_to_stdfloat(words[si - 3], rgb[0]);
|
|
|
+ okflag &= string_to_stdfloat(words[si - 2], rgb[1]);
|
|
|
+ okflag &= string_to_stdfloat(words[si - 1], rgb[2]);
|
|
|
|
|
|
if (!okflag) {
|
|
|
objegg_cat.error()
|
|
|
<< "Invalid number at line " << _line_number << "\n";
|
|
|
return false;
|
|
|
}
|
|
|
- vertex->set_color(LColor(r, g, b, 1.0));
|
|
|
+ while (_rgb_table.size() + 1 < _v_table.size()) {
|
|
|
+ _rgb_table.push_back(LVecBase3(1.0, 1.0, 1.0));
|
|
|
+ }
|
|
|
+ _rgb_table.push_back(rgb);
|
|
|
}
|
|
|
|
|
|
- ++_vi;
|
|
|
-
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
@@ -307,11 +373,14 @@ process_vt(vector_string &words) {
|
|
|
}
|
|
|
|
|
|
bool okflag = true;
|
|
|
- LTexCoord3d uvw;
|
|
|
- okflag &= string_to_double(words[1], uvw[0]);
|
|
|
- okflag &= string_to_double(words[2], uvw[1]);
|
|
|
+ LTexCoord3 uvw;
|
|
|
+ okflag &= string_to_stdfloat(words[1], uvw[0]);
|
|
|
+ okflag &= string_to_stdfloat(words[2], uvw[1]);
|
|
|
if (words.size() == 4) {
|
|
|
- okflag &= string_to_double(words[3], uvw[2]);
|
|
|
+ okflag &= string_to_stdfloat(words[3], uvw[2]);
|
|
|
+ _vt3_given = true;
|
|
|
+ } else {
|
|
|
+ uvw[2] = 0.0;
|
|
|
}
|
|
|
|
|
|
if (!okflag) {
|
|
|
@@ -320,13 +389,7 @@ process_vt(vector_string &words) {
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
- EggVertex *vertex = get_vertex(_vti);
|
|
|
- if (words.size() == 4) {
|
|
|
- vertex->set_uvw("", uvw);
|
|
|
- } else {
|
|
|
- vertex->set_uv("", LTexCoordd(uvw[0], uvw[1]));
|
|
|
- }
|
|
|
- ++_vti;
|
|
|
+ _vt_table.push_back(uvw);
|
|
|
|
|
|
return true;
|
|
|
}
|
|
|
@@ -348,9 +411,9 @@ process_xvt(vector_string &words) {
|
|
|
}
|
|
|
|
|
|
bool okflag = true;
|
|
|
- LTexCoordd uv;
|
|
|
- okflag &= string_to_double(words[1], uv[0]);
|
|
|
- okflag &= string_to_double(words[2], uv[1]);
|
|
|
+ LTexCoord uv;
|
|
|
+ okflag &= string_to_stdfloat(words[1], uv[0]);
|
|
|
+ okflag &= string_to_stdfloat(words[2], uv[1]);
|
|
|
|
|
|
if (!okflag) {
|
|
|
objegg_cat.error()
|
|
|
@@ -361,13 +424,22 @@ process_xvt(vector_string &words) {
|
|
|
uv[0] /= _ref_plane_res[0];
|
|
|
uv[1] = 1.0 - uv[1] / _ref_plane_res[1];
|
|
|
|
|
|
- EggVertex *vertex = get_vertex(_xvti);
|
|
|
- vertex->set_uv("", uv);
|
|
|
- ++_xvti;
|
|
|
+ _xvt_table.push_back(uv);
|
|
|
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
+////////////////////////////////////////////////////////////////////
|
|
|
+// Function: ObjToEggConverter::process_xvc
|
|
|
+// Access: Protected
|
|
|
+// Description: "xvc" is another extended column invented by DRZ. We
|
|
|
+// quietly ignore it.
|
|
|
+////////////////////////////////////////////////////////////////////
|
|
|
+bool ObjToEggConverter::
|
|
|
+process_xvc(vector_string &words) {
|
|
|
+ return true;
|
|
|
+}
|
|
|
+
|
|
|
////////////////////////////////////////////////////////////////////
|
|
|
// Function: ObjToEggConverter::process_vn
|
|
|
// Access: Protected
|
|
|
@@ -382,10 +454,10 @@ process_vn(vector_string &words) {
|
|
|
}
|
|
|
|
|
|
bool okflag = true;
|
|
|
- LVector3d normal;
|
|
|
- okflag &= string_to_double(words[1], normal[0]);
|
|
|
- okflag &= string_to_double(words[2], normal[1]);
|
|
|
- okflag &= string_to_double(words[3], normal[2]);
|
|
|
+ LVector3 normal;
|
|
|
+ okflag &= string_to_stdfloat(words[1], normal[0]);
|
|
|
+ okflag &= string_to_stdfloat(words[2], normal[1]);
|
|
|
+ okflag &= string_to_stdfloat(words[3], normal[2]);
|
|
|
|
|
|
if (!okflag) {
|
|
|
objegg_cat.error()
|
|
|
@@ -394,9 +466,7 @@ process_vn(vector_string &words) {
|
|
|
}
|
|
|
normal.normalize();
|
|
|
|
|
|
- EggVertex *vertex = get_vertex(_vni);
|
|
|
- vertex->set_normal(normal);
|
|
|
- ++_vni;
|
|
|
+ _vn_table.push_back(normal);
|
|
|
|
|
|
return true;
|
|
|
}
|
|
|
@@ -408,6 +478,8 @@ process_vn(vector_string &words) {
|
|
|
////////////////////////////////////////////////////////////////////
|
|
|
bool ObjToEggConverter::
|
|
|
process_f(vector_string &words) {
|
|
|
+ _f_given = true;
|
|
|
+
|
|
|
PT(EggPolygon) poly = new EggPolygon;
|
|
|
for (size_t i = 1; i < words.size(); ++i) {
|
|
|
EggVertex *vertex = get_face_vertex(words[i]);
|
|
|
@@ -451,107 +523,643 @@ process_g(vector_string &words) {
|
|
|
}
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
|
-// Function: ObjToEggConverter::get_vertex
|
|
|
+// Function: ObjToEggConverter::get_face_vertex
|
|
|
// Access: Protected
|
|
|
-// Description: Returns or creates a vertex in the vpool with the
|
|
|
-// given index.
|
|
|
+// Description: Returns or creates a vertex in the vpool according to
|
|
|
+// the indicated face reference.
|
|
|
////////////////////////////////////////////////////////////////////
|
|
|
EggVertex *ObjToEggConverter::
|
|
|
-get_vertex(int n) {
|
|
|
- if (n < 0) {
|
|
|
- // A negative index means to count backward from the end.
|
|
|
- n = _vi + n;
|
|
|
+get_face_vertex(const string &reference) {
|
|
|
+ VertexEntry entry(this, reference);
|
|
|
+
|
|
|
+ // Synthesize a vertex.
|
|
|
+ EggVertex synth;
|
|
|
+
|
|
|
+ if (entry._vi != 0) {
|
|
|
+ if (_v4_given) {
|
|
|
+ synth.set_pos(LCAST(double, _v_table[entry._vi - 1]));
|
|
|
+ } else {
|
|
|
+ LPoint4 pos = _v_table[entry._vi - 1];
|
|
|
+ synth.set_pos(LPoint3d(pos[0], pos[1], pos[2]));
|
|
|
+ }
|
|
|
+
|
|
|
+ if (entry._vi - 1 < (int)_rgb_table.size()) {
|
|
|
+ // We have a per-vertex color too.
|
|
|
+ LRGBColor rgb = _rgb_table[entry._vi - 1];
|
|
|
+ LColorf rgba(rgb[0], rgb[1], rgb[2], 1.0);
|
|
|
+ synth.set_color(rgba);
|
|
|
+ }
|
|
|
}
|
|
|
- EggVertex *vertex = _vpool->get_vertex(n);
|
|
|
- if (vertex == NULL) {
|
|
|
- vertex = new EggVertex;
|
|
|
- _vpool->add_vertex(vertex, n);
|
|
|
+
|
|
|
+ if (entry._vti != 0) {
|
|
|
+ // We have a texture coordinate; apply it.
|
|
|
+ if (_vt3_given) {
|
|
|
+ synth.set_uvw("", LCAST(double, _vt_table[entry._vti - 1]));
|
|
|
+ } else {
|
|
|
+ LTexCoord3 uvw = _vt_table[entry._vti - 1];
|
|
|
+ synth.set_uv("", LTexCoordd(uvw[0], uvw[1]));
|
|
|
+ }
|
|
|
+ } else if (entry._vi - 1 < (int)_xvt_table.size()) {
|
|
|
+ // We have an xvt texture coordinate.
|
|
|
+ synth.set_uv("", LCAST(double, _xvt_table[entry._vi - 1]));
|
|
|
+ }
|
|
|
+
|
|
|
+ if (entry._vni != 0) {
|
|
|
+ // We have a normal; apply it.
|
|
|
+ synth.set_normal(LCAST(double, _vn_table[entry._vni - 1]));
|
|
|
}
|
|
|
|
|
|
- return vertex;
|
|
|
+ return _vpool->create_unique_vertex(synth);
|
|
|
}
|
|
|
|
|
|
+
|
|
|
////////////////////////////////////////////////////////////////////
|
|
|
-// Function: ObjToEggConverter::get_face_vertex
|
|
|
+// Function: ObjToEggConverter::process_node
|
|
|
// Access: Protected
|
|
|
-// Description: Returns or creates a vertex in the vpool according to
|
|
|
-// the indicated face reference.
|
|
|
+// Description: Reads the file and converts it to PandaNode structures.
|
|
|
////////////////////////////////////////////////////////////////////
|
|
|
-EggVertex *ObjToEggConverter::
|
|
|
-get_face_vertex(const string &reference) {
|
|
|
+bool ObjToEggConverter::
|
|
|
+process_node(const Filename &filename) {
|
|
|
+ VirtualFileSystem *vfs = VirtualFileSystem::get_global_ptr();
|
|
|
+ istream *strm = vfs->open_read_file(filename, true);
|
|
|
+ if (strm == NULL) {
|
|
|
+ objegg_cat.error()
|
|
|
+ << "Couldn't read " << filename << "\n";
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ _v_table.clear();
|
|
|
+ _vn_table.clear();
|
|
|
+ _rgb_table.clear();
|
|
|
+ _vt_table.clear();
|
|
|
+ _ref_plane_res.set(1.0, 1.0);
|
|
|
+ _v4_given = false;
|
|
|
+ _vt3_given = false;
|
|
|
+ _f_given = false;
|
|
|
+
|
|
|
+ StreamReader sr(strm, true);
|
|
|
+ string line = sr.readline();
|
|
|
+ _line_number = 1;
|
|
|
+ while (!line.empty()) {
|
|
|
+ line = trim(line);
|
|
|
+ if (line.empty()) {
|
|
|
+ line = sr.readline();
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (line.substr(0, 15) == "#_ref_plane_res") {
|
|
|
+ process_ref_plane_res(line);
|
|
|
+ line = sr.readline();
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (line[0] == '#') {
|
|
|
+ line = sr.readline();
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!process_line_node(line)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ line = sr.readline();
|
|
|
+ ++_line_number;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!_f_given) {
|
|
|
+ generate_points();
|
|
|
+ }
|
|
|
+
|
|
|
+ return true;
|
|
|
+}
|
|
|
+
|
|
|
+////////////////////////////////////////////////////////////////////
|
|
|
+// Function: ObjToEggConverter::process_line_node
|
|
|
+// Access: Protected
|
|
|
+// Description:
|
|
|
+////////////////////////////////////////////////////////////////////
|
|
|
+bool ObjToEggConverter::
|
|
|
+process_line_node(const string &line) {
|
|
|
vector_string words;
|
|
|
- tokenize(reference, words, "/", false);
|
|
|
- nassertr(!words.empty(), NULL);
|
|
|
+ tokenize(line, words, " \t", true);
|
|
|
+ nassertr(!words.empty(), false);
|
|
|
|
|
|
- vector<EggVertex *> vertices;
|
|
|
- vertices.reserve(words.size());
|
|
|
- for (size_t i = 0; i < words.size(); ++i) {
|
|
|
- if (trim(words[i]).empty()) {
|
|
|
- if (i == 0) {
|
|
|
- objegg_cat.error()
|
|
|
- << "Invalid null vertex at line " << _line_number << "\n";
|
|
|
- return NULL;
|
|
|
- } else {
|
|
|
- vertices.push_back(vertices[0]);
|
|
|
+ string tag = words[0];
|
|
|
+ if (tag == "v") {
|
|
|
+ return process_v(words);
|
|
|
+ } else if (tag == "vt") {
|
|
|
+ return process_vt(words);
|
|
|
+ } else if (tag == "xvt") {
|
|
|
+ return process_xvt(words);
|
|
|
+ } else if (tag == "xvc") {
|
|
|
+ return process_xvc(words);
|
|
|
+ } else if (tag == "vn") {
|
|
|
+ return process_vn(words);
|
|
|
+ } else if (tag == "f") {
|
|
|
+ return process_f_node(words);
|
|
|
+ } else if (tag == "g") {
|
|
|
+ return process_g_node(words);
|
|
|
+ } else {
|
|
|
+ bool inserted = _ignored_tags.insert(tag).second;
|
|
|
+ if (inserted) {
|
|
|
+ objegg_cat.info()
|
|
|
+ << "Ignoring tag " << tag << "\n";
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return true;
|
|
|
+}
|
|
|
+
|
|
|
+////////////////////////////////////////////////////////////////////
|
|
|
+// Function: ObjToEggConverter::process_f_node
|
|
|
+// Access: Protected
|
|
|
+// Description: Defines a face in the obj file.
|
|
|
+////////////////////////////////////////////////////////////////////
|
|
|
+bool ObjToEggConverter::
|
|
|
+process_f_node(vector_string &words) {
|
|
|
+ _f_given = true;
|
|
|
+
|
|
|
+ bool all_vn = true;
|
|
|
+ int non_vn_index = -1;
|
|
|
+
|
|
|
+ pvector<VertexEntry> verts;
|
|
|
+ verts.reserve(words.size() - 1);
|
|
|
+ for (size_t i = 1; i < words.size(); ++i) {
|
|
|
+ VertexEntry entry(this, words[i]);
|
|
|
+ verts.push_back(entry);
|
|
|
+ if (entry._vni == 0) {
|
|
|
+ all_vn = false;
|
|
|
+ non_vn_index = i;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (verts.size() < 3) {
|
|
|
+ // Not enough vertices.
|
|
|
+ objegg_cat.error()
|
|
|
+ << "Degenerate face at " << _line_number << "\n";
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!all_vn) {
|
|
|
+ // Synthesize a normal if we need it.
|
|
|
+ LNormal normal = LNormal::zero();
|
|
|
+ for (size_t i = 0; i < verts.size(); ++i) {
|
|
|
+ int vi0 = verts[i]._vi;
|
|
|
+ int vi1 = verts[(i + 1) % verts.size()]._vi;
|
|
|
+ if (vi0 == 0 || vi1 == 0) {
|
|
|
continue;
|
|
|
}
|
|
|
+ const LVecBase4f &p0 = _v_table[vi0 - 1];
|
|
|
+ const LVecBase4f &p1 = _v_table[vi1 - 1];
|
|
|
+
|
|
|
+ normal[0] += p0[1] * p1[2] - p0[2] * p1[1];
|
|
|
+ normal[1] += p0[2] * p1[0] - p0[0] * p1[2];
|
|
|
+ normal[2] += p0[0] * p1[1] - p0[1] * p1[0];
|
|
|
+ }
|
|
|
+ normal.normalize();
|
|
|
+ int synth_vni = add_synth_normal(normal);
|
|
|
+
|
|
|
+ // Roll the polygon around to put the "non-vn" vertex at the
|
|
|
+ // end.
|
|
|
+ while (non_vn_index + 1 < verts.size()) {
|
|
|
+ VertexEntry entry = verts.back();
|
|
|
+ verts.pop_back();
|
|
|
+ verts.insert(verts.begin(), entry);
|
|
|
+ ++non_vn_index;
|
|
|
+ }
|
|
|
+
|
|
|
+ verts.back()._synth_vni = synth_vni;
|
|
|
+ }
|
|
|
+
|
|
|
+ Triangulator3 tri;
|
|
|
+ int num_tris = 1;
|
|
|
+
|
|
|
+ if (verts.size() != 3) {
|
|
|
+ // We have to triangulate a higher-order polygon.
|
|
|
+ for (size_t i = 0; i < verts.size(); ++i) {
|
|
|
+ const LVecBase4 &p = _v_table[verts[i]._vi - 1];
|
|
|
+ tri.add_vertex(p[0], p[1], p[2]);
|
|
|
+ tri.add_polygon_vertex(i);
|
|
|
}
|
|
|
|
|
|
+ tri.triangulate();
|
|
|
+ num_tris = tri.get_num_triangles();
|
|
|
+ }
|
|
|
+
|
|
|
+ if (_current_vertex_data->_prim->get_num_vertices() + 3 * num_tris > egg_max_indices ||
|
|
|
+ _current_vertex_data->_entries.size() + verts.size() > egg_max_vertices) {
|
|
|
+ // We'll exceed our specified limit with these triangles; start a new Geom.
|
|
|
+ _current_vertex_data->close_geom(this);
|
|
|
+ }
|
|
|
+
|
|
|
+ pvector<int> indices;
|
|
|
+ indices.reserve(verts.size());
|
|
|
+ for (size_t i = 0; i < verts.size(); ++i) {
|
|
|
+ int index = _current_vertex_data->add_vertex(this, verts[i]);
|
|
|
+ indices.push_back(index);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (indices.size() == 3) {
|
|
|
+ // It's already a triangle; add it directly.
|
|
|
+ _current_vertex_data->add_triangle(indices[0], indices[1], indices[2]);
|
|
|
+ } else {
|
|
|
+ // We have to triangulate a higher-order polygon.
|
|
|
+ for (size_t i = 0; i < verts.size(); ++i) {
|
|
|
+ const LVecBase4 &p = _v_table[verts[i]._vi - 1];
|
|
|
+ tri.add_vertex(p[0], p[1], p[2]);
|
|
|
+ tri.add_polygon_vertex(i);
|
|
|
+ }
|
|
|
+
|
|
|
+ tri.triangulate();
|
|
|
+ int num_tris = tri.get_num_triangles();
|
|
|
+ for (int ti = 0; ti < num_tris; ++ti) {
|
|
|
+ int i0 = tri.get_triangle_v0(ti);
|
|
|
+ int i1 = tri.get_triangle_v1(ti);
|
|
|
+ int i2 = tri.get_triangle_v2(ti);
|
|
|
+ _current_vertex_data->add_triangle(indices[i0], indices[i1], indices[i2]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return true;
|
|
|
+}
|
|
|
+
|
|
|
+////////////////////////////////////////////////////////////////////
|
|
|
+// Function: ObjToEggConverter::process_g_node
|
|
|
+// Access: Protected
|
|
|
+// Description: Defines a group in the obj file.
|
|
|
+////////////////////////////////////////////////////////////////////
|
|
|
+bool ObjToEggConverter::
|
|
|
+process_g_node(vector_string &words) {
|
|
|
+ _current_vertex_data->close_geom(this);
|
|
|
+ delete _current_vertex_data;
|
|
|
+ _current_vertex_data = NULL;
|
|
|
+
|
|
|
+ NodePath np(_root_node);
|
|
|
+
|
|
|
+ // We assume the group names define a hierarchy of more-specific to
|
|
|
+ // less-specific group names, so that the first group name is the
|
|
|
+ // bottommost node, and the last group name is the topmost node.
|
|
|
+
|
|
|
+ // Thus, iterate from the back to the front.
|
|
|
+ size_t i = words.size();
|
|
|
+ string name;
|
|
|
+ while (i > 2) {
|
|
|
+ --i;
|
|
|
+ name = words[i];
|
|
|
+ NodePath child = np.find(name);
|
|
|
+ if (!child) {
|
|
|
+ child = np.attach_new_node(name);
|
|
|
+ }
|
|
|
+ np = child;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (i > 1) {
|
|
|
+ --i;
|
|
|
+ name = words[i];
|
|
|
+ }
|
|
|
+
|
|
|
+ _current_vertex_data = new VertexData(np.node(), name);
|
|
|
+
|
|
|
+ return true;
|
|
|
+}
|
|
|
+
|
|
|
+////////////////////////////////////////////////////////////////////
|
|
|
+// Function: ObjToEggConverter::generate_points
|
|
|
+// Access: Protected
|
|
|
+// Description: If an obj file defines no faces, create a bunch of
|
|
|
+// GeomPoints to illustrate the vertex positions at
|
|
|
+// least.
|
|
|
+////////////////////////////////////////////////////////////////////
|
|
|
+void ObjToEggConverter::
|
|
|
+generate_points() {
|
|
|
+ CPT(GeomVertexFormat) format = GeomVertexFormat::get_v3();
|
|
|
+ PT(GeomVertexData) vdata = new GeomVertexData("points", format, GeomEnums::UH_static);
|
|
|
+ vdata->set_num_rows(_v_table.size());
|
|
|
+ GeomVertexWriter vertex_writer(vdata, InternalName::get_vertex());
|
|
|
+
|
|
|
+ for (size_t vi = 0; vi < _v_table.size(); ++vi) {
|
|
|
+ const LVecBase4 &p = _v_table[vi];
|
|
|
+ vertex_writer.add_data3(p[0], p[1], p[2]);
|
|
|
+ }
|
|
|
+
|
|
|
+ PT(GeomPrimitive) prim = new GeomPoints(GeomEnums::UH_static);
|
|
|
+ prim->add_next_vertices(_v_table.size());
|
|
|
+ prim->close_primitive();
|
|
|
+
|
|
|
+ PT(Geom) geom = new Geom(vdata);
|
|
|
+ geom->add_primitive(prim);
|
|
|
+
|
|
|
+ PT(GeomNode) geom_node = new GeomNode("points");
|
|
|
+ geom_node->add_geom(geom);
|
|
|
+ _root_node->add_child(geom_node);
|
|
|
+}
|
|
|
+
|
|
|
+////////////////////////////////////////////////////////////////////
|
|
|
+// Function: ObjToEggConverter::add_synth_normal
|
|
|
+// Access: Private
|
|
|
+// Description: Adds a new normal to the synth_vn table, or returns
|
|
|
+// an existing normal. In either case returns the
|
|
|
+// 1-based index number to the normal.
|
|
|
+////////////////////////////////////////////////////////////////////
|
|
|
+int ObjToEggConverter::
|
|
|
+add_synth_normal(const LVecBase3 &normal) {
|
|
|
+ pair<UniqueVec3Table::iterator, bool> result = _unique_synth_vn_table.insert(UniqueVec3Table::value_type(normal, _unique_synth_vn_table.size()));
|
|
|
+ UniqueVec3Table::iterator ni = result.first;
|
|
|
+ int index = (*ni).second;
|
|
|
+
|
|
|
+ if (result.second) {
|
|
|
+ // If the normal was added to the table, it's a unique normal, and
|
|
|
+ // now we have to add it to the table too.
|
|
|
+ _synth_vn_table.push_back(normal);
|
|
|
+ }
|
|
|
+
|
|
|
+ return index + 1;
|
|
|
+}
|
|
|
+
|
|
|
+////////////////////////////////////////////////////////////////////
|
|
|
+// Function: ObjToEggConverter::VertexEntry::Constructor
|
|
|
+// Access: Public
|
|
|
+// Description: Creates a VertexEntry from the n/n/n string format in
|
|
|
+// the obj file face reference.
|
|
|
+////////////////////////////////////////////////////////////////////
|
|
|
+ObjToEggConverter::VertexEntry::
|
|
|
+VertexEntry(const ObjToEggConverter *converter, const string &obj_vertex) {
|
|
|
+ _vi = 0;
|
|
|
+ _vti = 0;
|
|
|
+ _vni = 0;
|
|
|
+ _synth_vni = 0;
|
|
|
+
|
|
|
+ vector_string words;
|
|
|
+ tokenize(obj_vertex, words, "/", false);
|
|
|
+ nassertv(!words.empty());
|
|
|
+
|
|
|
+ for (size_t i = 0; i < words.size(); ++i) {
|
|
|
int index;
|
|
|
- if (!string_to_int(words[i], index)) {
|
|
|
- objegg_cat.error()
|
|
|
- << "Invalid integer " << words[i] << " at line " << _line_number << "\n";
|
|
|
- return NULL;
|
|
|
+ if (trim(words[i]).empty()) {
|
|
|
+ index = 0;
|
|
|
+ } else {
|
|
|
+ if (!string_to_int(words[i], index)) {
|
|
|
+ index = 0;
|
|
|
+ }
|
|
|
}
|
|
|
- EggVertex *vertex = get_vertex(index);
|
|
|
- if (vertex == NULL){
|
|
|
- objegg_cat.error()
|
|
|
- << "Invalid vertex " << index << " at line " << _line_number << "\n";
|
|
|
- return NULL;
|
|
|
+
|
|
|
+ switch (i) {
|
|
|
+ case 0:
|
|
|
+ _vi = index;
|
|
|
+ if (_vi < 0) {
|
|
|
+ _vi = (int)converter->_v_table.size() + _vi;
|
|
|
+ }
|
|
|
+ if (_vi < 0 || _vi - 1 >= (int)converter->_v_table.size()) {
|
|
|
+ _vi = 0;
|
|
|
+ }
|
|
|
+ break;
|
|
|
+
|
|
|
+ case 1:
|
|
|
+ _vti = index;
|
|
|
+ if (_vti < 0) {
|
|
|
+ _vti = (int)converter->_vt_table.size() + _vti;
|
|
|
+ }
|
|
|
+ if (_vti < 0 || _vti - 1 >= (int)converter->_vt_table.size()) {
|
|
|
+ _vti = 0;
|
|
|
+ }
|
|
|
+ break;
|
|
|
+
|
|
|
+ case 2:
|
|
|
+ _vni = index;
|
|
|
+ if (_vni < 0) {
|
|
|
+ _vni = (int)converter->_vn_table.size() + _vni;
|
|
|
+ }
|
|
|
+ if (_vni < 0 || _vni - 1 >= (int)converter->_vn_table.size()) {
|
|
|
+ _vni = 0;
|
|
|
+ }
|
|
|
+ break;
|
|
|
}
|
|
|
- vertices.push_back(vertex);
|
|
|
}
|
|
|
- nassertr(!vertices.empty(), NULL);
|
|
|
- nassertr(vertices.size() == words.size(), NULL);
|
|
|
+}
|
|
|
|
|
|
- if (vertices.size() == 1) {
|
|
|
- // Just a pos reference.
|
|
|
- return vertices[0];
|
|
|
+////////////////////////////////////////////////////////////////////
|
|
|
+// Function: ObjToEggConverter::VertexData::Constructor
|
|
|
+// Access: Public
|
|
|
+// Description:
|
|
|
+////////////////////////////////////////////////////////////////////
|
|
|
+ObjToEggConverter::VertexData::
|
|
|
+VertexData(PandaNode *parent, const string &name) :
|
|
|
+ _parent(parent), _name(name)
|
|
|
+{
|
|
|
+ _geom_node = NULL;
|
|
|
+
|
|
|
+ _v4_given = false;
|
|
|
+ _vt3_given = false;
|
|
|
+ _vt_given = false;
|
|
|
+ _rgb_given = false;
|
|
|
+ _vn_given = false;
|
|
|
+
|
|
|
+ _prim = new GeomTriangles(GeomEnums::UH_static);
|
|
|
+}
|
|
|
|
|
|
- } else if (vertices.size() == 2) {
|
|
|
- // Pos + uv.
|
|
|
- if (vertices[0] == vertices[1]) {
|
|
|
- return vertices[0];
|
|
|
+////////////////////////////////////////////////////////////////////
|
|
|
+// Function: ObjToEggConverter::VertexData::add_vertex
|
|
|
+// Access: Public
|
|
|
+// Description: Adds a new entry to the vertex data for the indicated
|
|
|
+// VertexEntry, or returns an equivalent vertex already
|
|
|
+// present.
|
|
|
+////////////////////////////////////////////////////////////////////
|
|
|
+int ObjToEggConverter::VertexData::
|
|
|
+add_vertex(const ObjToEggConverter *converter, const VertexEntry &entry) {
|
|
|
+ pair<UniqueVertexEntries::iterator, bool> result;
|
|
|
+ UniqueVertexEntries::iterator ni;
|
|
|
+ int index;
|
|
|
+
|
|
|
+ if (entry._vni != 0 || entry._synth_vni != 0) {
|
|
|
+ // If we are storing a vertex with a normal, see if we
|
|
|
+ // have already stored a vertex without a normal first.
|
|
|
+ VertexEntry no_normal(entry);
|
|
|
+ no_normal._vni = 0;
|
|
|
+ no_normal._synth_vni = 0;
|
|
|
+ ni = _unique_entries.find(no_normal);
|
|
|
+ if (ni != _unique_entries.end()) {
|
|
|
+ // We did have such a vertex! In this case, repurpose this
|
|
|
+ // vertex, resetting it to contain this normal.
|
|
|
+ index = (*ni).second;
|
|
|
+ _unique_entries.erase(ni);
|
|
|
+ result = _unique_entries.insert(UniqueVertexEntries::value_type(entry, index));
|
|
|
+ nassertr(result.second, index);
|
|
|
+ nassertr(_entries[index] == no_normal, index);
|
|
|
+ _entries[index]._vni = entry._vni;
|
|
|
+ _entries[index]._synth_vni = entry._synth_vni;
|
|
|
+ return index;
|
|
|
+ }
|
|
|
+ } else if (entry._vni == 0 && entry._synth_vni == 0) {
|
|
|
+ // If we are storing a vertex *without* any normal, see if we have
|
|
|
+ // already stored a vertex with a normal first.
|
|
|
+ ni = _unique_entries.lower_bound(entry);
|
|
|
+ if (ni != _unique_entries.end() && (*ni).first.matches_except_normal(entry)) {
|
|
|
+ // We had such a vertex, so use it.
|
|
|
+ index = (*ni).second;
|
|
|
+ return index;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // We didn't already have a vertex we could repurpose, so try to add
|
|
|
+ // exactly the desired vertex.
|
|
|
+ result = _unique_entries.insert(UniqueVertexEntries::value_type(entry, _entries.size()));
|
|
|
+ ni = result.first;
|
|
|
+ index = (*ni).second;
|
|
|
+
|
|
|
+ if (result.second) {
|
|
|
+ // If the vertex was added to the table, it's a unique vertex, and
|
|
|
+ // now we have to add it to the vertex data too.
|
|
|
+ _entries.push_back(entry);
|
|
|
+
|
|
|
+ if (converter->_v4_given) {
|
|
|
+ _v4_given = true;
|
|
|
+ }
|
|
|
+ if (converter->_vt3_given) {
|
|
|
+ _vt3_given = true;
|
|
|
+ }
|
|
|
+ if (entry._vti != 0) {
|
|
|
+ _vt_given = true;
|
|
|
+ } else if (entry._vi - 1 < (int)converter->_xvt_table.size()) {
|
|
|
+ // We have an xvt texture coordinate.
|
|
|
+ _vt_given = true;
|
|
|
+ }
|
|
|
+ if (entry._vi - 1 < (int)converter->_rgb_table.size()) {
|
|
|
+ // We have a per-vertex color too.
|
|
|
+ _rgb_given = true;
|
|
|
}
|
|
|
- // Synthesize a vertex.
|
|
|
- EggVertex synth(*vertices[0]);
|
|
|
- if (vertices[1]->has_uv("")) {
|
|
|
- synth.set_uv("", vertices[1]->get_uv(""));
|
|
|
- } else if (vertices[1]->has_uvw("")) {
|
|
|
- synth.set_uvw("", vertices[1]->get_uvw(""));
|
|
|
+ if (entry._vni != 0) {
|
|
|
+ _vn_given = true;
|
|
|
}
|
|
|
+ }
|
|
|
+
|
|
|
+ return index;
|
|
|
+}
|
|
|
|
|
|
- return _vpool->create_unique_vertex(synth);
|
|
|
+////////////////////////////////////////////////////////////////////
|
|
|
+// Function: ObjToEggConverter::VertexData::add_triangle
|
|
|
+// Access: Public
|
|
|
+// Description: Adds a triangle to the primitive, as a triple of
|
|
|
+// three vertex index numbers (as returned by
|
|
|
+// add_vertex()).
|
|
|
+////////////////////////////////////////////////////////////////////
|
|
|
+void ObjToEggConverter::VertexData::
|
|
|
+add_triangle(int v1, int v2, int v3) {
|
|
|
+ _prim->add_vertices(v1, v2, v3);
|
|
|
+ _prim->close_primitive();
|
|
|
+}
|
|
|
|
|
|
- } else if (vertices.size() >= 3) {
|
|
|
- // pos + uv + normal.
|
|
|
- if (vertices[0] == vertices[1] && vertices[0] == vertices[2]) {
|
|
|
- return vertices[0];
|
|
|
+////////////////////////////////////////////////////////////////////
|
|
|
+// Function: ObjToEggConverter::VertexData::close_geom
|
|
|
+// Access: Public
|
|
|
+// Description: Finishes the current geom and stores it as a child
|
|
|
+// in the root. Prepares for new geoms.
|
|
|
+////////////////////////////////////////////////////////////////////
|
|
|
+void ObjToEggConverter::VertexData::
|
|
|
+close_geom(const ObjToEggConverter *converter) {
|
|
|
+ if (_prim->get_num_vertices() != 0) {
|
|
|
+ // Create a new format that includes only the columns we actually
|
|
|
+ // used.
|
|
|
+ PT(GeomVertexArrayFormat) aformat = new GeomVertexArrayFormat;
|
|
|
+ if (_v4_given) {
|
|
|
+ aformat->add_column(InternalName::get_vertex(), 4,
|
|
|
+ GeomEnums::NT_stdfloat, GeomEnums::C_point);
|
|
|
+ } else {
|
|
|
+ aformat->add_column(InternalName::get_vertex(), 3,
|
|
|
+ GeomEnums::NT_stdfloat, GeomEnums::C_point);
|
|
|
+ }
|
|
|
+
|
|
|
+ // We always add normals--if no normals appeared in the file, we
|
|
|
+ // synthesize them.
|
|
|
+ aformat->add_column(InternalName::get_normal(), 3,
|
|
|
+ GeomEnums::NT_stdfloat, GeomEnums::C_vector);
|
|
|
+
|
|
|
+ if (_vt_given) {
|
|
|
+ if (_vt3_given) {
|
|
|
+ aformat->add_column(InternalName::get_texcoord(), 3,
|
|
|
+ GeomEnums::NT_stdfloat, GeomEnums::C_texcoord);
|
|
|
+ } else {
|
|
|
+ aformat->add_column(InternalName::get_texcoord(), 2,
|
|
|
+ GeomEnums::NT_stdfloat, GeomEnums::C_texcoord);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (_rgb_given) {
|
|
|
+ aformat->add_column(InternalName::get_color(), 4,
|
|
|
+ GeomEnums::NT_uint8, GeomEnums::C_color);
|
|
|
+ }
|
|
|
+
|
|
|
+ CPT(GeomVertexFormat) format = GeomVertexFormat::register_format(aformat);
|
|
|
+
|
|
|
+ // Create and populate the vertex data.
|
|
|
+ PT(GeomVertexData) vdata = new GeomVertexData(_name, format, GeomEnums::UH_static);
|
|
|
+ GeomVertexWriter vertex_writer(vdata, InternalName::get_vertex());
|
|
|
+ GeomVertexWriter normal_writer(vdata, InternalName::get_normal());
|
|
|
+ GeomVertexWriter texcoord_writer(vdata, InternalName::get_texcoord());
|
|
|
+ GeomVertexWriter color_writer(vdata, InternalName::get_color());
|
|
|
+
|
|
|
+ for (size_t i = 0; i < _entries.size(); ++i) {
|
|
|
+ const VertexEntry &entry = _entries[i];
|
|
|
+
|
|
|
+ if (entry._vi != 0) {
|
|
|
+ vertex_writer.set_row(i);
|
|
|
+ vertex_writer.add_data4(converter->_v_table[entry._vi - 1]);
|
|
|
+ }
|
|
|
+ if (entry._vti != 0) {
|
|
|
+ texcoord_writer.set_row(i);
|
|
|
+ texcoord_writer.add_data3(converter->_vt_table[entry._vti - 1]);
|
|
|
+ } else if (entry._vi - 1 < (int)converter->_xvt_table.size()) {
|
|
|
+ // We have an xvt texture coordinate.
|
|
|
+ texcoord_writer.set_row(i);
|
|
|
+ texcoord_writer.add_data2(converter->_xvt_table[entry._vi - 1]);
|
|
|
+ }
|
|
|
+ if (entry._vni != 0) {
|
|
|
+ normal_writer.set_row(i);
|
|
|
+ normal_writer.add_data3(converter->_vn_table[entry._vni - 1]);
|
|
|
+ } else if (entry._synth_vni != 0) {
|
|
|
+ normal_writer.set_row(i);
|
|
|
+ normal_writer.add_data3(converter->_synth_vn_table[entry._synth_vni - 1]);
|
|
|
+ } else {
|
|
|
+ // In this case, the normal isn't used and doesn't matter; we
|
|
|
+ // fill it in a unit vector just for neatness.
|
|
|
+ normal_writer.set_row(i);
|
|
|
+ normal_writer.add_data3(0, 0, 1);
|
|
|
+ }
|
|
|
+ if (_rgb_given) {
|
|
|
+ if (entry._vi - 1 < (int)converter->_rgb_table.size()) {
|
|
|
+ color_writer.set_row(i);
|
|
|
+ color_writer.add_data3(converter->_rgb_table[entry._vi - 1]);
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- // Synthesize a vertex.
|
|
|
- EggVertex synth(*vertices[0]);
|
|
|
- if (vertices[1]->has_uv("")) {
|
|
|
- synth.set_uv("", vertices[1]->get_uv(""));
|
|
|
- } else if (vertices[1]->has_uvw("")) {
|
|
|
- synth.set_uvw("", vertices[1]->get_uvw(""));
|
|
|
+ // Transform to zup-right.
|
|
|
+ vdata->transform_vertices(LMatrix4::convert_mat(CS_zup_right, CS_default));
|
|
|
+
|
|
|
+ // Now create a Geom with this data.
|
|
|
+ CPT(RenderState) state = RenderState::make_empty();
|
|
|
+ if (_rgb_given) {
|
|
|
+ state = state->add_attrib(ColorAttrib::make_vertex());
|
|
|
+ } else {
|
|
|
+ state = state->add_attrib(ColorAttrib::make_flat(LColor(1, 1, 1, 1)));
|
|
|
}
|
|
|
- if (vertices[2]->has_normal()) {
|
|
|
- synth.set_normal(vertices[2]->get_normal());
|
|
|
+ if (!_vn_given) {
|
|
|
+ // We have synthesized these normals; specify the flat-shading
|
|
|
+ // attrib.
|
|
|
+ state = state->add_attrib(ShadeModelAttrib::make(ShadeModelAttrib::M_flat));
|
|
|
+ _prim->set_shade_model(GeomEnums::SM_flat_last_vertex);
|
|
|
}
|
|
|
|
|
|
- return _vpool->create_unique_vertex(synth);
|
|
|
- }
|
|
|
+ PT(Geom) geom = new Geom(vdata);
|
|
|
+ geom->add_primitive(_prim);
|
|
|
|
|
|
- return NULL;
|
|
|
+ if (_geom_node == NULL) {
|
|
|
+ _geom_node = new GeomNode(_name);
|
|
|
+ _parent->add_child(_geom_node);
|
|
|
+ }
|
|
|
+
|
|
|
+ _geom_node->add_geom(geom, state);
|
|
|
+ }
|
|
|
+
|
|
|
+ _prim = new GeomTriangles(GeomEnums::UH_static);
|
|
|
+ _entries.clear();
|
|
|
+ _unique_entries.clear();
|
|
|
}
|