00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015 #include "geoMipTerrain.h"
00016
00017 #include "geomVertexFormat.h"
00018 #include "geomVertexArrayFormat.h"
00019 #include "internalName.h"
00020 #include "geomVertexData.h"
00021 #include "geomVertexWriter.h"
00022 #include "geomTristrips.h"
00023 #include "geomTriangles.h"
00024 #include "geom.h"
00025 #include "geomNode.h"
00026 #include "boundingBox.h"
00027 #include "config_grutil.h"
00028
00029 #include "sceneGraphReducer.h"
00030
00031 #include "collideMask.h"
00032
00033 TypeHandle GeoMipTerrain::_type_handle;
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044 PT(GeomNode) GeoMipTerrain::
00045 generate_block(unsigned short mx,
00046 unsigned short my,
00047 unsigned short level) {
00048
00049 nassertr(mx < (_xsize - 1) / _block_size, NULL);
00050 nassertr(my < (_ysize - 1) / _block_size, NULL);
00051
00052 unsigned short center = _block_size / 2;
00053 unsigned int vcounter = 0;
00054
00055
00056 PT(GeomVertexArrayFormat) array = new GeomVertexArrayFormat();
00057 if (_has_color_map) {
00058 array->add_column(InternalName::make("color"), 4,
00059 Geom::NT_stdfloat, Geom::C_color);
00060 }
00061 array->add_column(InternalName::make("vertex"), 3,
00062 Geom::NT_stdfloat, Geom::C_point);
00063 array->add_column(InternalName::make("texcoord"), 2,
00064 Geom::NT_stdfloat, Geom::C_texcoord);
00065 array->add_column(InternalName::make("normal"), 3,
00066 Geom::NT_stdfloat, Geom::C_vector);
00067 PT(GeomVertexFormat) format = new GeomVertexFormat();
00068 format->add_array(array);
00069
00070
00071 PT(GeomVertexData) vdata = new GeomVertexData(_root.get_name(),
00072 GeomVertexFormat::register_format(format), Geom::UH_stream);
00073 vdata->unclean_set_num_rows((_block_size + 1) * (_block_size + 1));
00074 GeomVertexWriter cwriter;
00075 if (_has_color_map) {
00076 cwriter = GeomVertexWriter(vdata, "color");
00077 }
00078 GeomVertexWriter vwriter (vdata, "vertex" );
00079 GeomVertexWriter twriter (vdata, "texcoord");
00080 GeomVertexWriter nwriter (vdata, "normal" );
00081 PT(GeomTriangles) prim = new GeomTriangles(Geom::UH_stream);
00082
00083 if (_bruteforce) {
00084
00085
00086 level = 0;
00087 }
00088
00089
00090 level = min(max(_min_level, level), _max_level);
00091 unsigned short reallevel = level;
00092 level = int(pow(2.0, int(level)));
00093
00094
00095 unsigned short lnlevel = get_neighbor_level(mx, my, -1, 0);
00096 unsigned short rnlevel = get_neighbor_level(mx, my, 1, 0);
00097 unsigned short bnlevel = get_neighbor_level(mx, my, 0, -1);
00098 unsigned short tnlevel = get_neighbor_level(mx, my, 0, 1);
00099 bool ljunction = (lnlevel != reallevel);
00100 bool rjunction = (rnlevel != reallevel);
00101 bool bjunction = (bnlevel != reallevel);
00102 bool tjunction = (tnlevel != reallevel);
00103
00104
00105
00106
00107
00108
00109
00110 unsigned short lowblocksize = _block_size / level + 1;
00111
00112 for (int x = 0; x <= _block_size; x++) {
00113 for (int y = 0; y <= _block_size; y++) {
00114 if ((x % level) == 0 && (y % level) == 0) {
00115 if (_has_color_map) {
00116 LVecBase4d color = _color_map.get_xel_a(int((mx * _block_size + x)
00117 / double(_xsize) * _color_map.get_x_size()),
00118 int((my * _block_size + y)
00119 / double(_ysize) * _color_map.get_y_size()));
00120 cwriter.add_data4(LCAST(PN_stdfloat, color));
00121 }
00122 vwriter.add_data3(x - 0.5 * _block_size, y - 0.5 * _block_size, get_pixel_value(mx, my, x, y));
00123 twriter.add_data2((mx * _block_size + x) / double(_xsize - 1),
00124 (my * _block_size + y) / double(_ysize - 1));
00125 nwriter.add_data3(get_normal(mx, my, x, y));
00126 if (x > 0 && y > 0) {
00127
00128 if (x == level && ljunction) {
00129 if (y > level && y < _block_size) {
00130 prim->add_vertex(min(max(sfav(y / level, lnlevel, reallevel), 0), lowblocksize - 1));
00131 prim->add_vertex(vcounter - 1);
00132 prim->add_vertex(vcounter);
00133 prim->close_primitive();
00134 }
00135 if (f_part((y / level) / PN_stdfloat(pow(2.0, int(lnlevel - reallevel)))) == 0.5) {
00136 prim->add_vertex(min(max(sfav(y / level + 1, lnlevel, reallevel), 0), lowblocksize - 1));
00137 prim->add_vertex(min(max(sfav(y / level - 1, lnlevel, reallevel), 0), lowblocksize - 1));
00138 prim->add_vertex(vcounter);
00139 prim->close_primitive();
00140 }
00141 } else if (
00142 (!(bjunction && y == level && x > level && x < _block_size) &&
00143 !(rjunction && x == _block_size) &&
00144 !(tjunction && y == _block_size && x > level && x < _block_size))) {
00145 if ((x <= center && y <= center) || (x > center && y > center)) {
00146 if (x > center) {
00147 prim->add_vertex(vcounter - lowblocksize - 1);
00148 prim->add_vertex(vcounter - 1);
00149 prim->add_vertex(vcounter);
00150 } else {
00151 prim->add_vertex(vcounter);
00152 prim->add_vertex(vcounter - lowblocksize);
00153 prim->add_vertex(vcounter - lowblocksize - 1);
00154 }
00155 } else {
00156 if (x > center) {
00157 prim->add_vertex(vcounter);
00158 prim->add_vertex(vcounter - lowblocksize);
00159 prim->add_vertex(vcounter - 1);
00160 } else {
00161 prim->add_vertex(vcounter - 1);
00162 prim->add_vertex(vcounter - lowblocksize);
00163 prim->add_vertex(vcounter - lowblocksize - 1);
00164 }
00165 }
00166 prim->close_primitive();
00167 }
00168
00169 if (x == _block_size - level && rjunction) {
00170 if (y > level && y < _block_size) {
00171 prim->add_vertex(lowblocksize * (lowblocksize - 1) + min(max(sfav(y / level, rnlevel, reallevel), 0), lowblocksize - 1));
00172 prim->add_vertex(vcounter);
00173 prim->add_vertex(vcounter - 1);
00174 prim->close_primitive();
00175 }
00176 if (f_part((y / level) / PN_stdfloat(pow(2.0, int(rnlevel - reallevel)))) == 0.5) {
00177 prim->add_vertex(lowblocksize * (lowblocksize - 1) + min(max(sfav(y / level - 1, rnlevel, reallevel), 0), lowblocksize - 1));
00178 prim->add_vertex(lowblocksize * (lowblocksize - 1) + min(max(sfav(y / level + 1, rnlevel, reallevel), 0), lowblocksize - 1));
00179 prim->add_vertex(vcounter);
00180 prim->close_primitive();
00181 }
00182 }
00183
00184 if (y == level && bjunction) {
00185 if (x > level && x < _block_size) {
00186 prim->add_vertex(vcounter);
00187 prim->add_vertex(vcounter - lowblocksize);
00188 prim->add_vertex(min(max(sfav(x / level, bnlevel, reallevel), 0), lowblocksize - 1) * lowblocksize);
00189 prim->close_primitive();
00190 }
00191 if (f_part((x / level) / PN_stdfloat(pow(2.0, int(bnlevel - reallevel)))) == 0.5) {
00192 prim->add_vertex(min(max(sfav(x / level - 1, bnlevel, reallevel), 0), lowblocksize - 1) * lowblocksize);
00193 prim->add_vertex(min(max(sfav(x / level + 1, bnlevel, reallevel), 0), lowblocksize - 1) * lowblocksize);
00194 prim->add_vertex(vcounter);
00195 prim->close_primitive();
00196 }
00197 } else if (
00198 (!(ljunction && x == level && y > level && y < _block_size) &&
00199 !(tjunction && y == _block_size) &&
00200 !(rjunction && x == _block_size && y > level && y < _block_size))) {
00201 if ((x <= center && y <= center) || (x > center && y > center)) {
00202 if (y > center) {
00203 prim->add_vertex(vcounter);
00204 prim->add_vertex(vcounter - lowblocksize);
00205 prim->add_vertex(vcounter - lowblocksize - 1);
00206 } else {
00207 prim->add_vertex(vcounter - lowblocksize - 1);
00208 prim->add_vertex(vcounter - 1);
00209 prim->add_vertex(vcounter);
00210 }
00211 } else {
00212 if (y > center) {
00213 prim->add_vertex(vcounter);
00214 prim->add_vertex(vcounter - lowblocksize);
00215 prim->add_vertex(vcounter - 1);
00216 } else {
00217 prim->add_vertex(vcounter - 1);
00218 prim->add_vertex(vcounter - lowblocksize);
00219 prim->add_vertex(vcounter - lowblocksize - 1);
00220 }
00221 }
00222 prim->close_primitive();
00223 }
00224
00225 if (y == _block_size - level && tjunction) {
00226 if (x > level && x < _block_size) {
00227 prim->add_vertex(min(max(sfav(x / level, tnlevel, reallevel), 0), lowblocksize - 1) * lowblocksize + lowblocksize - 1);
00228 prim->add_vertex(vcounter - lowblocksize);
00229 prim->add_vertex(vcounter);
00230 prim->close_primitive();
00231 }
00232 if (f_part((x / level) / PN_stdfloat(pow(2.0, int(tnlevel - reallevel)))) == 0.5) {
00233 prim->add_vertex(min(max(sfav(x / level + 1, tnlevel, reallevel), 0), lowblocksize - 1) * lowblocksize + lowblocksize - 1);
00234 prim->add_vertex(min(max(sfav(x / level - 1, tnlevel, reallevel), 0), lowblocksize - 1) * lowblocksize + lowblocksize - 1);
00235 prim->add_vertex(vcounter);
00236 prim->close_primitive();
00237 }
00238 }
00239 }
00240 vcounter++;
00241 }
00242 }
00243 }
00244
00245 PT(Geom) geom = new Geom(vdata);
00246 geom->add_primitive(prim);
00247 geom->set_bounds_type(BoundingVolume::BT_box);
00248
00249 ostringstream sname;
00250 sname << "gmm" << mx << "x" << my;
00251 PT(GeomNode) node = new GeomNode(sname.str());
00252 node->add_geom(geom);
00253 node->set_bounds_type(BoundingVolume::BT_box);
00254 _old_levels.at(mx).at(my) = reallevel;
00255
00256 return node;
00257 }
00258
00259
00260
00261
00262
00263
00264
00265
00266
00267
00268
00269
00270
00271
00272
00273 double GeoMipTerrain::
00274 get_elevation(double x, double y) {
00275 y = (_ysize - 1) - y;
00276 unsigned int xlo = (unsigned int) x;
00277 unsigned int ylo = (unsigned int) y;
00278 if (xlo < 0) xlo = 0;
00279 if (ylo < 0) ylo = 0;
00280 if (xlo > _xsize - 2)
00281 xlo = _xsize - 2;
00282 if (ylo > _ysize - 2)
00283 ylo = _ysize - 2;
00284 unsigned int xhi = xlo + 1;
00285 unsigned int yhi = ylo + 1;
00286 double xoffs = x - xlo;
00287 double yoffs = y - ylo;
00288 double grayxlyl = get_pixel_value(xlo, ylo);
00289 double grayxhyl = get_pixel_value(xhi, ylo);
00290 double grayxlyh = get_pixel_value(xlo, yhi);
00291 double grayxhyh = get_pixel_value(xhi, yhi);
00292 double lerpyl = grayxhyl * xoffs + grayxlyl * (1.0 - xoffs);
00293 double lerpyh = grayxhyh * xoffs + grayxlyh * (1.0 - xoffs);
00294 return lerpyh * yoffs + lerpyl * (1.0 - yoffs);
00295 }
00296
00297
00298
00299
00300
00301
00302
00303
00304
00305
00306
00307
00308
00309
00310
00311
00312
00313
00314 LVector3 GeoMipTerrain::
00315 get_normal(int x, int y) {
00316 int nx = x - 1;
00317 int px = x + 1;
00318 int ny = y - 1;
00319 int py = y + 1;
00320 if (nx < 0) nx++;
00321 if (ny < 0) ny++;
00322 if (px >= int(_xsize)) px--;
00323 if (py >= int(_ysize)) py--;
00324 double drx = get_pixel_value(px, y) - get_pixel_value(nx, y);
00325 double dry = get_pixel_value(x, py) - get_pixel_value(x, ny);
00326 LVector3 normal(drx * 0.5, dry * 0.5, 1);
00327 normal.normalize();
00328
00329 return normal;
00330 }
00331
00332
00333
00334
00335
00336
00337
00338
00339
00340
00341
00342
00343
00344
00345
00346
00347 PNMImage GeoMipTerrain::
00348 make_slope_image() {
00349 PNMImage result (_xsize, _ysize);
00350 result.make_grayscale();
00351 for (unsigned int x = 0; x < _xsize; ++x) {
00352 for (unsigned int y = 0; y < _ysize; ++y) {
00353 LVector3 normal (get_normal(x, y));
00354 normal.set(normal.get_x() / _root.get_sx(),
00355 normal.get_y() / _root.get_sy(),
00356 normal.get_z() / _root.get_sz());
00357 normal.normalize();
00358 result.set_xel(x, y, normal.angle_deg(LVector3::up()) / 90.0);
00359 }
00360 }
00361 return result;
00362 }
00363
00364
00365
00366
00367
00368
00369
00370
00371
00372
00373 void GeoMipTerrain::
00374 calc_ambient_occlusion(PN_stdfloat radius, PN_stdfloat contrast, PN_stdfloat brightness) {
00375 _color_map = PNMImage(_xsize, _ysize);
00376 _color_map.make_grayscale();
00377 _color_map.set_maxval(_heightfield.get_maxval());
00378
00379 for (unsigned int x = 0; x < _xsize; ++x) {
00380 for (unsigned int y = 0; y < _ysize; ++y) {
00381 _color_map.set_xel(x, _ysize - y - 1, get_pixel_value(x, y));
00382 }
00383 }
00384
00385
00386
00387 _color_map.gaussian_filter(radius);
00388
00389 for (unsigned int x = 0; x < _xsize; ++x) {
00390 for (unsigned int y = 0; y < _ysize; ++y) {
00391 _color_map.set_xel(x, y, (get_pixel_value(x, _ysize - y - 1) - _color_map.get_gray(x, y)) * contrast + brightness);
00392 }
00393 }
00394
00395 _has_color_map = true;
00396 }
00397
00398
00399
00400
00401
00402
00403
00404
00405
00406
00407 void GeoMipTerrain::
00408 generate() {
00409 if (_xsize < 3 || _ysize < 3) {
00410 grutil_cat.error() << "No valid heightfield image has been set!\n";
00411 return;
00412 }
00413 calc_levels();
00414 _root.node()->remove_all_children();
00415 _blocks.clear();
00416 _old_levels.clear();
00417 _old_levels.resize(int((_xsize - 1) / _block_size));
00418 _root_flattened = false;
00419 for (unsigned int mx = 0; mx < (_xsize - 1) / _block_size; mx++) {
00420 _old_levels[mx].resize(int((_ysize - 1) / _block_size));
00421 pvector<NodePath> tvector;
00422 for (unsigned int my = 0; my < (_ysize - 1) / _block_size; my++) {
00423 if (_bruteforce) {
00424 tvector.push_back(_root.attach_new_node(generate_block(mx, my, 0)));
00425 } else {
00426 tvector.push_back(_root.attach_new_node(generate_block(mx, my, _levels[mx][my])));
00427 }
00428 tvector[my].set_pos((mx + 0.5) * _block_size, (my + 0.5) * _block_size, 0);
00429 }
00430 _blocks.push_back(tvector);
00431 tvector.clear();
00432 }
00433 auto_flatten();
00434 _is_dirty = false;
00435 }
00436
00437
00438
00439
00440
00441
00442
00443
00444
00445
00446
00447
00448
00449
00450
00451 bool GeoMipTerrain::
00452 update() {
00453 if (_xsize < 3 || _ysize < 3) {
00454 grutil_cat.error() << "No valid heightfield image has been set!\n";
00455 return false;
00456 }
00457 if (_is_dirty) {
00458 generate();
00459 return true;
00460 } else if (!_bruteforce) {
00461 calc_levels();
00462 if (root_flattened()) {
00463 _root.node()->remove_all_children();
00464 unsigned int xsize = _blocks.size();
00465 for (unsigned int tx = 0; tx < xsize; tx++) {
00466 unsigned int ysize = _blocks[tx].size();
00467 for (unsigned int ty = 0;ty < ysize; ty++) {
00468 _blocks[tx][ty].reparent_to(_root);
00469 }
00470 }
00471 _root_flattened = false;
00472 }
00473 bool returnVal = false;
00474 for (unsigned int mx = 0; mx < (_xsize - 1) / _block_size; mx++) {
00475 for (unsigned int my = 0; my < (_ysize - 1) / _block_size; my++) {
00476 bool isUpd (update_block(mx, my));
00477 if (isUpd && mx > 0 && _old_levels[mx - 1][my] == _levels[mx - 1][my]) {
00478 if (update_block(mx - 1, my, -1, true)) {
00479 returnVal = true;
00480 }
00481 }
00482 if (isUpd && mx < (_ysize - 1)/_block_size - 1
00483 && _old_levels[mx + 1][my] == _levels[mx + 1][my]) {
00484 if (update_block(mx + 1, my, -1, true)) {
00485 returnVal = true;
00486 }
00487 }
00488 if (isUpd && my > 0 && _old_levels[mx][my - 1] == _levels[mx][my - 1]) {
00489 if (update_block(mx, my - 1, -1, true)) {
00490 returnVal = true;
00491 }
00492 }
00493 if (isUpd && my < (_ysize - 1)/_block_size - 1
00494 && _old_levels[mx][my + 1] == _levels[mx][my + 1]) {
00495 if (update_block(mx, my + 1, -1, true)) {
00496 returnVal = true;
00497 }
00498 }
00499 if (isUpd) {
00500 returnVal = true;
00501 }
00502 }
00503 }
00504 auto_flatten();
00505 return returnVal;
00506 }
00507 return false;
00508 }
00509
00510
00511
00512
00513
00514
00515
00516
00517
00518
00519
00520 bool GeoMipTerrain::
00521 root_flattened() {
00522 if (_root_flattened) {
00523 return true;
00524 }
00525
00526
00527
00528
00529
00530
00531 int total = 0;
00532 unsigned int xsize = _blocks.size();
00533 for (unsigned int tx = 0; tx < xsize; tx++) {
00534 unsigned int ysize = _blocks[tx].size();
00535 for (unsigned int ty = 0;ty < ysize; ty++) {
00536 if (_blocks[tx][ty].get_node(1) != _root.node()) {
00537 grutil_cat.error() << "GeoMipTerrain: root node unexpectedly mangled!\n";
00538 return true;
00539 }
00540 total += 1;
00541 }
00542 }
00543 if (total != _root.node()->get_num_children()) {
00544 grutil_cat.error() << "GeoMipTerrain: root node unexpectedly mangled: " << total << " vs " << (_root.node()->get_num_children()) << "\n";
00545 return true;
00546 }
00547
00548
00549 return false;
00550 }
00551
00552
00553
00554
00555
00556
00557 void GeoMipTerrain::
00558 auto_flatten() {
00559 if (_auto_flatten == AFM_off) {
00560 return;
00561 }
00562
00563
00564
00565
00566
00567 NodePath np("Backup Node");
00568 np.node()->copy_children(_root.node());
00569
00570
00571 switch(_auto_flatten) {
00572 case AFM_light: _root.flatten_light(); break;
00573 case AFM_medium: _root.flatten_medium(); break;
00574 case AFM_strong: _root.flatten_strong(); break;
00575 }
00576
00577 _root_flattened = true;
00578 }
00579
00580
00581
00582
00583
00584
00585
00586 void GeoMipTerrain::
00587 calc_levels() {
00588 nassertv(_xsize >= 3 && _ysize >= 3);
00589 _levels.clear();
00590 for (unsigned int mx = 0; mx < (_xsize - 1) / _block_size; mx++) {
00591 pvector<unsigned short> tvector;
00592 for (unsigned int my = 0; my < (_ysize - 1) / _block_size; my++) {
00593 if(_bruteforce) {
00594 tvector.push_back(0);
00595 } else {
00596 tvector.push_back(min(short(max(_min_level, lod_decide(mx, my))),
00597 short(_max_level)));
00598 }
00599 }
00600 _levels.push_back(tvector);
00601 tvector.clear();
00602 }
00603 }
00604
00605
00606
00607
00608
00609
00610
00611
00612
00613
00614
00615
00616
00617
00618
00619
00620
00621
00622 bool GeoMipTerrain::
00623 update_block(unsigned short mx, unsigned short my,
00624 signed short level, bool forced) {
00625 nassertr_always(!_is_dirty, false);
00626 nassertr_always(mx < (_xsize - 1) / _block_size, false);
00627 nassertr_always(my < (_ysize - 1) / _block_size, false);
00628 if (level < 0) {
00629 level = _levels[mx][my];
00630 }
00631 level = min(max(_min_level, (unsigned short) level), _max_level);
00632 if (forced || _old_levels[mx][my] != level) {
00633
00634 generate_block(mx, my, level)->replace_node(_blocks[mx][my].node());
00635 return true;
00636 }
00637 return false;
00638 }
00639
00640
00641
00642
00643
00644
00645
00646
00647
00648
00649 bool GeoMipTerrain::
00650 set_heightfield(const Filename &filename, PNMFileType *ftype) {
00651
00652 PNMImageHeader imgheader;
00653 if (imgheader.read_header(filename, ftype)) {
00654
00655 _heightfield.copy_header_from(imgheader);
00656
00657 if(!is_power_of_two(imgheader.get_x_size() - 1) || !is_power_of_two(imgheader.get_y_size() - 1)) {
00658
00659 unsigned int reqx, reqy;
00660 reqx = max(3, (int) pow(2.0, ceil(log((double) max(2, imgheader.get_x_size() - 1)) / log(2.0))) + 1);
00661 reqy = max(3, (int) pow(2.0, ceil(log((double) max(2, imgheader.get_y_size() - 1)) / log(2.0))) + 1);
00662
00663
00664 if (reqx != (unsigned int)imgheader.get_x_size() || reqy != (unsigned int)imgheader.get_y_size()) {
00665 grutil_cat.warning()
00666 << "Rescaling heightfield image " << filename
00667 << " from " << imgheader.get_x_size() << "x" << imgheader.get_y_size()
00668 << " to " << reqx << "x" << reqy << " pixels.\n";
00669 _heightfield.set_read_size(reqx, reqy);
00670 }
00671 }
00672
00673
00674 if (!_heightfield.read(filename, ftype)) {
00675 _heightfield.clear_read_size();
00676 grutil_cat.error() << "Failed to read heightfield image " << filename << "!\n";
00677 return false;
00678 }
00679
00680 _is_dirty = true;
00681 _xsize = _heightfield.get_x_size();
00682 _ysize = _heightfield.get_y_size();
00683 return true;
00684 } else {
00685 grutil_cat.error() << "Failed to load heightfield image " << filename << "!\n";
00686 }
00687 return false;
00688 }
00689
00690
00691
00692
00693
00694
00695 unsigned short GeoMipTerrain::
00696 get_neighbor_level(unsigned short mx, unsigned short my, short dmx, short dmy) {
00697
00698
00699 if ((int)mx + (int)dmx < 0 || (int)mx + (int)dmx >= ((int)_xsize - 1) / (int)_block_size ||
00700 (int)my + (int)dmy < 0 || (int)my + (int)dmy >= ((int)_ysize - 1) / (int)_block_size) {
00701 return (_stitching) ? _max_level : min(max(_min_level, _levels[mx][my]), _max_level);
00702 }
00703
00704 if (_bruteforce) {
00705 return min(max(_min_level, _levels[mx][my]), _max_level);
00706 }
00707
00708
00709 if (_levels[mx + dmx][my + dmy] > _levels[mx][my]) {
00710 return min(max(_min_level, _levels[mx + dmx][my + dmy]), _max_level);
00711 } else {
00712 return min(max(_min_level, _levels[mx][my]), _max_level);
00713 }
00714 }
00715