|
Blender
V2.59
|
00001 /* 00002 * $Id: shrinkwrap.c 36773 2011-05-19 11:24:56Z blendix $ 00003 * 00004 * ***** BEGIN GPL LICENSE BLOCK ***** 00005 * 00006 * This program is free software; you can redistribute it and/or 00007 * modify it under the terms of the GNU General Public License 00008 * as published by the Free Software Foundation; either version 2 00009 * of the License, or (at your option) any later version. 00010 * 00011 * This program is distributed in the hope that it will be useful, 00012 * but WITHOUT ANY WARRANTY; without even the implied warranty of 00013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 00014 * GNU General Public License for more details. 00015 * 00016 * You should have received a copy of the GNU General Public License 00017 * along with this program; if not, write to the Free Software Foundation, 00018 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 00019 * 00020 * The Original Code is Copyright (C) Blender Foundation. 00021 * All rights reserved. 00022 * 00023 * The Original Code is: all of this file. 00024 * 00025 * Contributor(s): Andr Pinto 00026 * 00027 * ***** END GPL LICENSE BLOCK ***** 00028 */ 00029 00034 #include <string.h> 00035 #include <float.h> 00036 #include <math.h> 00037 #include <memory.h> 00038 #include <stdio.h> 00039 #include <time.h> 00040 #include <assert.h> 00041 00042 #include "DNA_object_types.h" 00043 #include "DNA_modifier_types.h" 00044 #include "DNA_meshdata_types.h" 00045 #include "DNA_mesh_types.h" 00046 #include "DNA_scene_types.h" 00047 00048 #include "BLI_editVert.h" 00049 #include "BLI_math.h" 00050 #include "BLI_utildefines.h" 00051 00052 #include "BKE_shrinkwrap.h" 00053 #include "BKE_DerivedMesh.h" 00054 #include "BKE_lattice.h" 00055 00056 #include "BKE_deform.h" 00057 #include "BKE_mesh.h" 00058 #include "BKE_subsurf.h" 00059 00060 /* Util macros */ 00061 #define OUT_OF_MEMORY() ((void)printf("Shrinkwrap: Out of memory\n")) 00062 00063 /* Benchmark macros */ 00064 #if !defined(_WIN32) && 0 00065 00066 #include <sys/time.h> 00067 00068 #define BENCH(a) \ 00069 do { \ 00070 double _t1, _t2; \ 00071 struct timeval _tstart, _tend; \ 00072 clock_t _clock_init = clock(); \ 00073 gettimeofday ( &_tstart, NULL); \ 00074 (a); \ 00075 gettimeofday ( &_tend, NULL); \ 00076 _t1 = ( double ) _tstart.tv_sec + ( double ) _tstart.tv_usec/ ( 1000*1000 ); \ 00077 _t2 = ( double ) _tend.tv_sec + ( double ) _tend.tv_usec/ ( 1000*1000 ); \ 00078 printf("%s: %fs (real) %fs (cpu)\n", #a, _t2-_t1, (float)(clock()-_clock_init)/CLOCKS_PER_SEC);\ 00079 } while(0) 00080 00081 #else 00082 00083 #define BENCH(a) (a) 00084 00085 #endif 00086 00087 typedef void ( *Shrinkwrap_ForeachVertexCallback) (DerivedMesh *target, float *co, float *normal); 00088 00089 /* get derived mesh */ 00090 //TODO is anyfunction that does this? returning the derivedFinal witouth we caring if its in edit mode or not? 00091 DerivedMesh *object_get_derived_final(Object *ob) 00092 { 00093 Mesh *me= ob->data; 00094 EditMesh *em = BKE_mesh_get_editmesh(me); 00095 00096 if(em) { 00097 DerivedMesh *dm = em->derivedFinal; 00098 BKE_mesh_end_editmesh(me, em); 00099 return dm; 00100 } 00101 00102 return ob->derivedFinal; 00103 } 00104 00105 /* Space transform */ 00106 void space_transform_from_matrixs(SpaceTransform *data, float local[4][4], float target[4][4]) 00107 { 00108 float itarget[4][4]; 00109 invert_m4_m4(itarget, target); 00110 mul_serie_m4(data->local2target, itarget, local, NULL, NULL, NULL, NULL, NULL, NULL); 00111 invert_m4_m4(data->target2local, data->local2target); 00112 } 00113 00114 void space_transform_apply(const SpaceTransform *data, float *co) 00115 { 00116 mul_v3_m4v3(co, ((SpaceTransform*)data)->local2target, co); 00117 } 00118 00119 void space_transform_invert(const SpaceTransform *data, float *co) 00120 { 00121 mul_v3_m4v3(co, ((SpaceTransform*)data)->target2local, co); 00122 } 00123 00124 static void space_transform_apply_normal(const SpaceTransform *data, float *no) 00125 { 00126 mul_mat3_m4_v3( ((SpaceTransform*)data)->local2target, no); 00127 normalize_v3(no); // TODO: could we just determine de scale value from the matrix? 00128 } 00129 00130 static void space_transform_invert_normal(const SpaceTransform *data, float *no) 00131 { 00132 mul_mat3_m4_v3(((SpaceTransform*)data)->target2local, no); 00133 normalize_v3(no); // TODO: could we just determine de scale value from the matrix? 00134 } 00135 00136 /* 00137 * Returns the squared distance between two given points 00138 */ 00139 static float squared_dist(const float *a, const float *b) 00140 { 00141 float tmp[3]; 00142 VECSUB(tmp, a, b); 00143 return INPR(tmp, tmp); 00144 } 00145 00146 /* 00147 * Shrinkwrap to the nearest vertex 00148 * 00149 * it builds a kdtree of vertexs we can attach to and then 00150 * for each vertex performs a nearest vertex search on the tree 00151 */ 00152 static void shrinkwrap_calc_nearest_vertex(ShrinkwrapCalcData *calc) 00153 { 00154 int i; 00155 00156 BVHTreeFromMesh treeData = NULL_BVHTreeFromMesh; 00157 BVHTreeNearest nearest = NULL_BVHTreeNearest; 00158 00159 00160 BENCH(bvhtree_from_mesh_verts(&treeData, calc->target, 0.0, 2, 6)); 00161 if(treeData.tree == NULL) 00162 { 00163 OUT_OF_MEMORY(); 00164 return; 00165 } 00166 00167 //Setup nearest 00168 nearest.index = -1; 00169 nearest.dist = FLT_MAX; 00170 #ifndef __APPLE__ 00171 #pragma omp parallel for default(none) private(i) firstprivate(nearest) shared(treeData,calc) schedule(static) 00172 #endif 00173 for(i = 0; i<calc->numVerts; ++i) 00174 { 00175 float *co = calc->vertexCos[i]; 00176 float tmp_co[3]; 00177 float weight = defvert_array_find_weight_safe(calc->dvert, i, calc->vgroup); 00178 if(weight == 0.0f) continue; 00179 00180 00181 //Convert the vertex to tree coordinates 00182 if(calc->vert) 00183 { 00184 VECCOPY(tmp_co, calc->vert[i].co); 00185 } 00186 else 00187 { 00188 VECCOPY(tmp_co, co); 00189 } 00190 space_transform_apply(&calc->local2target, tmp_co); 00191 00192 //Use local proximity heuristics (to reduce the nearest search) 00193 // 00194 //If we already had an hit before.. we assume this vertex is going to have a close hit to that other vertex 00195 //so we can initiate the "nearest.dist" with the expected value to that last hit. 00196 //This will lead in prunning of the search tree. 00197 if(nearest.index != -1) 00198 nearest.dist = squared_dist(tmp_co, nearest.co); 00199 else 00200 nearest.dist = FLT_MAX; 00201 00202 BLI_bvhtree_find_nearest(treeData.tree, tmp_co, &nearest, treeData.nearest_callback, &treeData); 00203 00204 00205 //Found the nearest vertex 00206 if(nearest.index != -1) 00207 { 00208 //Adjusting the vertex weight, so that after interpolating it keeps a certain distance from the nearest position 00209 float dist = sasqrt(nearest.dist); 00210 if(dist > FLT_EPSILON) weight *= (dist - calc->keepDist)/dist; 00211 00212 //Convert the coordinates back to mesh coordinates 00213 VECCOPY(tmp_co, nearest.co); 00214 space_transform_invert(&calc->local2target, tmp_co); 00215 00216 interp_v3_v3v3(co, co, tmp_co, weight); //linear interpolation 00217 } 00218 } 00219 00220 free_bvhtree_from_mesh(&treeData); 00221 } 00222 00223 /* 00224 * This function raycast a single vertex and updates the hit if the "hit" is considered valid. 00225 * Returns TRUE if "hit" was updated. 00226 * Opts control whether an hit is valid or not 00227 * Supported options are: 00228 * MOD_SHRINKWRAP_CULL_TARGET_FRONTFACE (front faces hits are ignored) 00229 * MOD_SHRINKWRAP_CULL_TARGET_BACKFACE (back faces hits are ignored) 00230 */ 00231 int normal_projection_project_vertex(char options, const float *vert, const float *dir, const SpaceTransform *transf, BVHTree *tree, BVHTreeRayHit *hit, BVHTree_RayCastCallback callback, void *userdata) 00232 { 00233 float tmp_co[3], tmp_no[3]; 00234 const float *co, *no; 00235 BVHTreeRayHit hit_tmp; 00236 00237 //Copy from hit (we need to convert hit rays from one space coordinates to the other 00238 memcpy( &hit_tmp, hit, sizeof(hit_tmp) ); 00239 00240 //Apply space transform (TODO readjust dist) 00241 if(transf) 00242 { 00243 VECCOPY( tmp_co, vert ); 00244 space_transform_apply( transf, tmp_co ); 00245 co = tmp_co; 00246 00247 VECCOPY( tmp_no, dir ); 00248 space_transform_apply_normal( transf, tmp_no ); 00249 no = tmp_no; 00250 00251 hit_tmp.dist *= mat4_to_scale( ((SpaceTransform*)transf)->local2target ); 00252 } 00253 else 00254 { 00255 co = vert; 00256 no = dir; 00257 } 00258 00259 hit_tmp.index = -1; 00260 00261 BLI_bvhtree_ray_cast(tree, co, no, 0.0f, &hit_tmp, callback, userdata); 00262 00263 if(hit_tmp.index != -1) { 00264 /* invert the normal first so face culling works on rotated objects */ 00265 if(transf) { 00266 space_transform_invert_normal(transf, hit_tmp.no); 00267 } 00268 00269 if (options & (MOD_SHRINKWRAP_CULL_TARGET_FRONTFACE|MOD_SHRINKWRAP_CULL_TARGET_BACKFACE)) { 00270 /* apply backface */ 00271 const float dot= dot_v3v3(dir, hit_tmp.no); 00272 if( ((options & MOD_SHRINKWRAP_CULL_TARGET_FRONTFACE) && dot <= 0.0f) || 00273 ((options & MOD_SHRINKWRAP_CULL_TARGET_BACKFACE) && dot >= 0.0f) 00274 ) { 00275 return FALSE; /* Ignore hit */ 00276 } 00277 } 00278 00279 if(transf) { 00280 /* Inverting space transform (TODO make coeherent with the initial dist readjust) */ 00281 space_transform_invert(transf, hit_tmp.co); 00282 hit_tmp.dist = len_v3v3((float *)vert, hit_tmp.co); 00283 } 00284 00285 memcpy(hit, &hit_tmp, sizeof(hit_tmp) ); 00286 return TRUE; 00287 } 00288 return FALSE; 00289 } 00290 00291 00292 static void shrinkwrap_calc_normal_projection(ShrinkwrapCalcData *calc) 00293 { 00294 int i; 00295 00296 //Options about projection direction 00297 const char use_normal = calc->smd->shrinkOpts; 00298 float proj_axis[3] = {0.0f, 0.0f, 0.0f}; 00299 00300 //Raycast and tree stuff 00301 BVHTreeRayHit hit; 00302 BVHTreeFromMesh treeData= NULL_BVHTreeFromMesh; 00303 00304 //auxiliar target 00305 DerivedMesh *auxMesh = NULL; 00306 BVHTreeFromMesh auxData = NULL_BVHTreeFromMesh; 00307 SpaceTransform local2aux; 00308 00309 //If the user doesn't allows to project in any direction of projection axis 00310 //then theres nothing todo. 00311 if((use_normal & (MOD_SHRINKWRAP_PROJECT_ALLOW_POS_DIR | MOD_SHRINKWRAP_PROJECT_ALLOW_NEG_DIR)) == 0) 00312 return; 00313 00314 00315 //Prepare data to retrieve the direction in which we should project each vertex 00316 if(calc->smd->projAxis == MOD_SHRINKWRAP_PROJECT_OVER_NORMAL) 00317 { 00318 if(calc->vert == NULL) return; 00319 } 00320 else 00321 { 00322 //The code supports any axis that is a combination of X,Y,Z 00323 //altought currently UI only allows to set the 3 diferent axis 00324 if(calc->smd->projAxis & MOD_SHRINKWRAP_PROJECT_OVER_X_AXIS) proj_axis[0] = 1.0f; 00325 if(calc->smd->projAxis & MOD_SHRINKWRAP_PROJECT_OVER_Y_AXIS) proj_axis[1] = 1.0f; 00326 if(calc->smd->projAxis & MOD_SHRINKWRAP_PROJECT_OVER_Z_AXIS) proj_axis[2] = 1.0f; 00327 00328 normalize_v3(proj_axis); 00329 00330 //Invalid projection direction 00331 if(INPR(proj_axis, proj_axis) < FLT_EPSILON) 00332 return; 00333 } 00334 00335 if(calc->smd->auxTarget) 00336 { 00337 auxMesh = object_get_derived_final(calc->smd->auxTarget); 00338 if(!auxMesh) 00339 return; 00340 space_transform_setup( &local2aux, calc->ob, calc->smd->auxTarget); 00341 } 00342 00343 //After sucessufuly build the trees, start projection vertexs 00344 if( bvhtree_from_mesh_faces(&treeData, calc->target, 0.0, 4, 6) 00345 && (auxMesh == NULL || bvhtree_from_mesh_faces(&auxData, auxMesh, 0.0, 4, 6))) 00346 { 00347 00348 #ifndef __APPLE__ 00349 #pragma omp parallel for private(i,hit) schedule(static) 00350 #endif 00351 for(i = 0; i<calc->numVerts; ++i) 00352 { 00353 float *co = calc->vertexCos[i]; 00354 float tmp_co[3], tmp_no[3]; 00355 float weight = defvert_array_find_weight_safe(calc->dvert, i, calc->vgroup); 00356 00357 if(weight == 0.0f) continue; 00358 00359 if(calc->vert) 00360 { 00361 /* calc->vert contains verts from derivedMesh */ 00362 /* this coordinated are deformed by vertexCos only for normal projection (to get correct normals) */ 00363 /* for other cases calc->varts contains undeformed coordinates and vertexCos should be used */ 00364 if(calc->smd->projAxis == MOD_SHRINKWRAP_PROJECT_OVER_NORMAL) { 00365 VECCOPY(tmp_co, calc->vert[i].co); 00366 normal_short_to_float_v3(tmp_no, calc->vert[i].no); 00367 } else { 00368 VECCOPY(tmp_co, co); 00369 VECCOPY(tmp_no, proj_axis); 00370 } 00371 } 00372 else 00373 { 00374 VECCOPY(tmp_co, co); 00375 VECCOPY(tmp_no, proj_axis); 00376 } 00377 00378 00379 hit.index = -1; 00380 hit.dist = 10000.0f; //TODO: we should use FLT_MAX here, but sweepsphere code isnt prepared for that 00381 00382 //Project over positive direction of axis 00383 if(use_normal & MOD_SHRINKWRAP_PROJECT_ALLOW_POS_DIR) 00384 { 00385 00386 if(auxData.tree) 00387 normal_projection_project_vertex(0, tmp_co, tmp_no, &local2aux, auxData.tree, &hit, auxData.raycast_callback, &auxData); 00388 00389 normal_projection_project_vertex(calc->smd->shrinkOpts, tmp_co, tmp_no, &calc->local2target, treeData.tree, &hit, treeData.raycast_callback, &treeData); 00390 } 00391 00392 //Project over negative direction of axis 00393 if(use_normal & MOD_SHRINKWRAP_PROJECT_ALLOW_NEG_DIR && hit.index == -1) 00394 { 00395 float inv_no[3]; 00396 negate_v3_v3(inv_no, tmp_no); 00397 00398 if(auxData.tree) 00399 normal_projection_project_vertex(0, tmp_co, inv_no, &local2aux, auxData.tree, &hit, auxData.raycast_callback, &auxData); 00400 00401 normal_projection_project_vertex(calc->smd->shrinkOpts, tmp_co, inv_no, &calc->local2target, treeData.tree, &hit, treeData.raycast_callback, &treeData); 00402 } 00403 00404 00405 if(hit.index != -1) 00406 { 00407 madd_v3_v3v3fl(hit.co, hit.co, tmp_no, calc->keepDist); 00408 interp_v3_v3v3(co, co, hit.co, weight); 00409 } 00410 } 00411 } 00412 00413 //free data structures 00414 free_bvhtree_from_mesh(&treeData); 00415 free_bvhtree_from_mesh(&auxData); 00416 } 00417 00418 /* 00419 * Shrinkwrap moving vertexs to the nearest surface point on the target 00420 * 00421 * it builds a BVHTree from the target mesh and then performs a 00422 * NN matchs for each vertex 00423 */ 00424 static void shrinkwrap_calc_nearest_surface_point(ShrinkwrapCalcData *calc) 00425 { 00426 int i; 00427 00428 BVHTreeFromMesh treeData = NULL_BVHTreeFromMesh; 00429 BVHTreeNearest nearest = NULL_BVHTreeNearest; 00430 00431 //Create a bvh-tree of the given target 00432 BENCH(bvhtree_from_mesh_faces( &treeData, calc->target, 0.0, 2, 6)); 00433 if(treeData.tree == NULL) 00434 { 00435 OUT_OF_MEMORY(); 00436 return; 00437 } 00438 00439 //Setup nearest 00440 nearest.index = -1; 00441 nearest.dist = FLT_MAX; 00442 00443 00444 //Find the nearest vertex 00445 #ifndef __APPLE__ 00446 #pragma omp parallel for default(none) private(i) firstprivate(nearest) shared(calc,treeData) schedule(static) 00447 #endif 00448 for(i = 0; i<calc->numVerts; ++i) 00449 { 00450 float *co = calc->vertexCos[i]; 00451 float tmp_co[3]; 00452 float weight = defvert_array_find_weight_safe(calc->dvert, i, calc->vgroup); 00453 if(weight == 0.0f) continue; 00454 00455 //Convert the vertex to tree coordinates 00456 if(calc->vert) 00457 { 00458 VECCOPY(tmp_co, calc->vert[i].co); 00459 } 00460 else 00461 { 00462 VECCOPY(tmp_co, co); 00463 } 00464 space_transform_apply(&calc->local2target, tmp_co); 00465 00466 //Use local proximity heuristics (to reduce the nearest search) 00467 // 00468 //If we already had an hit before.. we assume this vertex is going to have a close hit to that other vertex 00469 //so we can initiate the "nearest.dist" with the expected value to that last hit. 00470 //This will lead in prunning of the search tree. 00471 if(nearest.index != -1) 00472 nearest.dist = squared_dist(tmp_co, nearest.co); 00473 else 00474 nearest.dist = FLT_MAX; 00475 00476 BLI_bvhtree_find_nearest(treeData.tree, tmp_co, &nearest, treeData.nearest_callback, &treeData); 00477 00478 //Found the nearest vertex 00479 if(nearest.index != -1) 00480 { 00481 if(calc->smd->shrinkOpts & MOD_SHRINKWRAP_KEEP_ABOVE_SURFACE) 00482 { 00483 //Make the vertex stay on the front side of the face 00484 VECADDFAC(tmp_co, nearest.co, nearest.no, calc->keepDist); 00485 } 00486 else 00487 { 00488 //Adjusting the vertex weight, so that after interpolating it keeps a certain distance from the nearest position 00489 float dist = sasqrt( nearest.dist ); 00490 if(dist > FLT_EPSILON) 00491 interp_v3_v3v3(tmp_co, tmp_co, nearest.co, (dist - calc->keepDist)/dist); //linear interpolation 00492 else 00493 VECCOPY( tmp_co, nearest.co ); 00494 } 00495 00496 //Convert the coordinates back to mesh coordinates 00497 space_transform_invert(&calc->local2target, tmp_co); 00498 interp_v3_v3v3(co, co, tmp_co, weight); //linear interpolation 00499 } 00500 } 00501 00502 free_bvhtree_from_mesh(&treeData); 00503 } 00504 00505 /* Main shrinkwrap function */ 00506 void shrinkwrapModifier_deform(ShrinkwrapModifierData *smd, Object *ob, DerivedMesh *dm, float (*vertexCos)[3], int numVerts) 00507 { 00508 00509 DerivedMesh *ss_mesh = NULL; 00510 ShrinkwrapCalcData calc = NULL_ShrinkwrapCalcData; 00511 00512 //remove loop dependencies on derived meshs (TODO should this be done elsewhere?) 00513 if(smd->target == ob) smd->target = NULL; 00514 if(smd->auxTarget == ob) smd->auxTarget = NULL; 00515 00516 00517 //Configure Shrinkwrap calc data 00518 calc.smd = smd; 00519 calc.ob = ob; 00520 calc.numVerts = numVerts; 00521 calc.vertexCos = vertexCos; 00522 00523 //DeformVertex 00524 calc.vgroup = defgroup_name_index(calc.ob, calc.smd->vgroup_name); 00525 if(dm) 00526 { 00527 calc.dvert = dm->getVertDataArray(dm, CD_MDEFORMVERT); 00528 } 00529 else if(calc.ob->type == OB_LATTICE) 00530 { 00531 calc.dvert = lattice_get_deform_verts(calc.ob); 00532 } 00533 00534 00535 if(smd->target) 00536 { 00537 calc.target = object_get_derived_final(smd->target); 00538 00539 //TODO there might be several "bugs" on non-uniform scales matrixs 00540 //because it will no longer be nearest surface, not sphere projection 00541 //because space has been deformed 00542 space_transform_setup(&calc.local2target, ob, smd->target); 00543 00544 //TODO: smd->keepDist is in global units.. must change to local 00545 calc.keepDist = smd->keepDist; 00546 } 00547 00548 00549 00550 calc.vgroup = defgroup_name_index(calc.ob, smd->vgroup_name); 00551 00552 if(dm != NULL && smd->shrinkType == MOD_SHRINKWRAP_PROJECT) 00553 { 00554 //Setup arrays to get vertexs positions, normals and deform weights 00555 calc.vert = dm->getVertDataArray(dm, CD_MVERT); 00556 calc.dvert = dm->getVertDataArray(dm, CD_MDEFORMVERT); 00557 00558 //Using vertexs positions/normals as if a subsurface was applied 00559 if(smd->subsurfLevels) 00560 { 00561 SubsurfModifierData ssmd= {{NULL}}; 00562 ssmd.subdivType = ME_CC_SUBSURF; //catmull clark 00563 ssmd.levels = smd->subsurfLevels; //levels 00564 00565 ss_mesh = subsurf_make_derived_from_derived(dm, &ssmd, FALSE, NULL, 0, 0, (ob->mode & OB_MODE_EDIT)); 00566 00567 if(ss_mesh) 00568 { 00569 calc.vert = ss_mesh->getVertDataArray(ss_mesh, CD_MVERT); 00570 if(calc.vert) 00571 { 00572 //TRICKY: this code assumes subsurface will have the transformed original vertices 00573 //in their original order at the end of the vert array. 00574 calc.vert = calc.vert + ss_mesh->getNumVerts(ss_mesh) - dm->getNumVerts(dm); 00575 } 00576 } 00577 00578 //Just to make sure we are not leaving any memory behind 00579 assert(ssmd.emCache == NULL); 00580 assert(ssmd.mCache == NULL); 00581 } 00582 } 00583 00584 //Projecting target defined - lets work! 00585 if(calc.target) 00586 { 00587 switch(smd->shrinkType) 00588 { 00589 case MOD_SHRINKWRAP_NEAREST_SURFACE: 00590 BENCH(shrinkwrap_calc_nearest_surface_point(&calc)); 00591 break; 00592 00593 case MOD_SHRINKWRAP_PROJECT: 00594 BENCH(shrinkwrap_calc_normal_projection(&calc)); 00595 break; 00596 00597 case MOD_SHRINKWRAP_NEAREST_VERTEX: 00598 BENCH(shrinkwrap_calc_nearest_vertex(&calc)); 00599 break; 00600 } 00601 } 00602 00603 //free memory 00604 if(ss_mesh) 00605 ss_mesh->release(ss_mesh); 00606 } 00607