Blender V2.61 - r43446

shrinkwrap.c

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