-Added the console command prvm_globalset
authorblack <black@d7cf8633-e32d-0410-b094-e92efae38249>
Sat, 4 Dec 2004 16:15:17 +0000 (16:15 +0000)
committerblack <black@d7cf8633-e32d-0410-b094-e92efae38249>
Sat, 4 Dec 2004 16:15:17 +0000 (16:15 +0000)
-Rewritten CL_Video to support multiple video streams
-Fixed a bug in the new VM's epair parser
The only problem CL_Video now has, is that it doesnt use TEXF_FRAGMENT
right now. Ive added a more general description of this problem to the
todo list.

git-svn-id: svn://svn.icculus.org/twilight/trunk/darkplaces@4827 d7cf8633-e32d-0410-b094-e92efae38249

cl_main.c
cl_video.c
cl_video.h
gl_draw.c
host.c
prvm_edict.c
todo

index c5caa79..37ad080 100644 (file)
--- a/cl_main.c
+++ b/cl_main.c
@@ -1381,8 +1381,6 @@ void CL_Init (void)
        CL_Particles_Init();
        CL_Screen_Init();
        CL_CGVM_Init();
-
-       CL_Video_Init();
 }
 
 
index 130a58f..d3804da 100644 (file)
 #include "cl_video.h"
 #include "dpvsimpledecode.h"
 
-mempool_t *clvideomempool;
+// constants (and semi-constants)
+static int  cl_videormask;
+static int  cl_videobmask;
+static int  cl_videogmask;
+static int     cl_videobytesperpixel;
 
-int cl_videoplaying = false;
-void *cl_videostream;
+static clvideo_t videoarray[ MAXCLVIDEOS ];
+static mempool_t *cl_videomempool;
+static rtexturepool_t *cl_videotexturepool;
 
-double cl_videostarttime;
-int cl_videoframenum;
-double cl_videoframerate;
-
-int cl_videoimagewidth;
-int cl_videoimageheight;
-int cl_videoimagedata_rmask;
-int cl_videoimagedata_gmask;
-int cl_videoimagedata_bmask;
-int cl_videoimagedata_bytesperpixel;
-void *cl_videoimagedata;
+static clvideo_t *FindUnusedVid( void )
+{
+       int i;
+       for( i = 1 ; i < MAXCLVIDEOS ; i++ )
+               if( videoarray[ i ].state == CLVIDEO_UNUSED )
+                       return &videoarray[ i ];
+       return NULL;
+}
 
-rtexture_t *cl_videotexture;
-rtexturepool_t *cl_videotexturepool;
+static qboolean OpenStream( clvideo_t * video )
+{
+       char *errorstring;
+       video->stream = dpvsimpledecode_open( video->filename, &errorstring);
+       if (!video->stream )
+       {
+               Con_Printf("unable to open \"%s\", error: %s\n", video->filename, errorstring);
+               return false;
+       }
+       return true;
+}
 
-void CL_VideoFrame(void)
+static void SuspendVideo( clvideo_t * video )
 {
-       int frames, framenum;
-       if (!cl_videoplaying)
+       if( video->suspended )
                return;
-       framenum = (realtime - cl_videostarttime) * cl_videoframerate;
-       //Con_Printf("frame %i\n", framenum);
-       if (framenum < 0)
-               framenum = 0;
-       frames = 0;
-       while (cl_videoframenum < framenum)
-       {
-               frames++;
-               cl_videoframenum++;
-               if (dpvsimpledecode_video(cl_videostream, cl_videoimagedata, cl_videoimagedata_rmask, cl_videoimagedata_gmask, cl_videoimagedata_bmask, cl_videoimagedata_bytesperpixel, cl_videoimagewidth * cl_videoimagedata_bytesperpixel))
-               {
-                       CL_VideoStop();
-                       return;
+       video->suspended = true;
+       // free the texture
+       R_FreeTexture( video->cpif.tex );
+       // free the image data
+       Mem_Free( video->imagedata );
+       // if we are in firstframe mode, also close the stream
+       if( video->state == CLVIDEO_FIRSTFRAME ) 
+               dpvsimpledecode_close( video->stream ); 
+}
+
+static qboolean WakeVideo( clvideo_t * video )
+{
+       if( !video->suspended )
+               return true;
+       video->suspended = false;
+       
+       if( video->state == CLVIDEO_FIRSTFRAME )
+               if( !OpenStream( video ) ) {
+                       video->state = CLVIDEO_UNUSED;
+                       return false;
                }
+               
+       video->imagedata = Mem_Alloc( cl_videomempool, video->cpif.width * video->cpif.height * cl_videobytesperpixel );
+       video->cpif.tex = R_LoadTexture2D( cl_videotexturepool, video->cpif.name, 
+               video->cpif.width, video->cpif.height, NULL, TEXTYPE_RGBA, 0, NULL );    
+
+       // update starttime
+       video->starttime += realtime - video->lasttime;
+       return true;
+}
+
+static clvideo_t* OpenVideo( clvideo_t *video, char *filename, char *name, int owner )
+{
+       strncpy( video->filename, filename, MAX_QPATH );
+       video->ownertag = owner;
+       strncpy( video->cpif.name, CLVIDEOPREFIX, MAX_QPATH );
+       strncat( video->cpif.name, name, MAX_QPATH - sizeof( CLVIDEOPREFIX ) );
+
+       if( !OpenStream( video ) )
+               return NULL;
+
+       video->state = CLVIDEO_FIRSTFRAME;
+       video->framenum = -1;
+       video->framerate = dpvsimpledecode_getframerate( video->stream );
+       video->lasttime = realtime;
+
+       video->cpif.width = dpvsimpledecode_getwidth( video->stream );
+       video->cpif.height = dpvsimpledecode_getheight( video->stream );
+       video->cpif.tex = R_LoadTexture2D( cl_videotexturepool, video->cpif.name, 
+               video->cpif.width, video->cpif.height, NULL, TEXTYPE_RGBA, 0, NULL );
+
+    video->imagedata = Mem_Alloc( cl_videomempool, video->cpif.width * video->cpif.height * cl_videobytesperpixel );
+
+       return video;
+}
+
+clvideo_t* CL_OpenVideo( char *filename, char *name, int owner )
+{
+       clvideo_t *video;
+
+       video = FindUnusedVid();
+       if( !video ) {
+               Con_Printf( "unable to open video \"%s\" - video limit reached\n", filename );
+               return NULL;
        }
-       if (frames)
-       {
-               R_UpdateTexture(cl_videotexture, cl_videoimagedata);
-               //Draw_NewPic("engine_videoframe", cl_videoimagewidth, cl_videoimageheight, false, cl_videoimagedata);
+       return OpenVideo( video, filename, name, owner );
+}
+
+clvideo_t* CL_GetVideo( char *name )
+{
+       int i;
+       clvideo_t *video;
+
+       for( i = 0 ; i < MAXCLVIDEOS ; i++ )
+               if( videoarray[ i ].state != CLVIDEO_UNUSED 
+                       &&      !strcmp( videoarray[ i ].cpif.name , name ) )
+                       break;
+       if( i == MAXCLVIDEOS )
+               return NULL;
+       video = &videoarray[ i ];
+
+       if( video->suspended )
+               if( !WakeVideo( video ) )
+                       return NULL;
+       video->lasttime = realtime;
+
+       return video;
+}
+
+void CL_StartVideo( clvideo_t * video )
+{
+       if( !video )
+               return;
+
+       video->starttime = video->lasttime = realtime;
+       video->framenum = -1;
+       video->state = CLVIDEO_PLAY;
+}
+
+void CL_LoopVideo( clvideo_t * video )
+{
+       if( !video )
+               return;
+
+       video->starttime = video->lasttime = realtime;
+       video->framenum = -1;
+       video->state = CLVIDEO_LOOP;
+}
+
+void CL_PauseVideo( clvideo_t * video )
+{
+       if( !video )
+               return;
+
+       video->state = CLVIDEO_PAUSE;
+       video->lasttime = realtime;
+}
+
+void CL_RestartVideo( clvideo_t *video )
+{
+       if( !video )
+               return;
+    
+       video->starttime = video->lasttime = realtime;
+       video->framenum = -1;
+}
+
+void CL_StopVideo( clvideo_t * video )
+{
+       if( !video )
+               return;
+
+       video->lasttime = realtime;
+       video->framenum = -1;
+       video->state = CLVIDEO_FIRSTFRAME;
+}
+
+void CL_CloseVideo( clvideo_t * video )
+{
+       if( !video || video->state == CLVIDEO_UNUSED )
+               return;
+
+       video->state = CLVIDEO_UNUSED;
+       
+       if( !video->suspended || video->state != CLVIDEO_FIRSTFRAME )
+               dpvsimpledecode_close( video->stream );
+       if( !video->suspended ) {
+               Mem_Free( video->imagedata );
+               R_FreeTexture( video->cpif.tex );
        }
 }
 
+static void VideoFrame( clvideo_t *video )
+{
+       int destframe;
+
+       if( video->state == CLVIDEO_FIRSTFRAME )
+               destframe = 0;
+       else
+               destframe = (realtime - video->starttime) * video->framerate;
+       if( destframe < 0 )
+               destframe = 0;
+       if( video->framenum < destframe ) {
+               do {
+                       video->framenum++;
+                       if( dpvsimpledecode_video( video->stream, video->imagedata, cl_videormask, 
+                               cl_videogmask, cl_videobmask, cl_videobytesperpixel, 
+                               cl_videobytesperpixel * video->cpif.width ) 
+                               ) { // finished?
+                               video->framenum = -1;
+                               if( video->state == CLVIDEO_LOOP )
+                                               video->starttime = realtime;
+                               else if( video->state == CLVIDEO_PLAY )
+                                               video->state = CLVIDEO_FIRSTFRAME;
+                               return;
+                       }
+               } while( video->framenum < destframe );
+               R_UpdateTexture( video->cpif.tex, video->imagedata );
+       }                                       
+}
+
+void CL_VideoFrame( void ) // update all videos
+{
+       int i;
+       clvideo_t *video;
+
+       for( video = videoarray, i = 0 ; i < MAXCLVIDEOS ; video++, i++ )
+               if( video->state != CLVIDEO_UNUSED && !video->suspended )
+                       if( realtime - video->lasttime > CLTHRESHOLD )
+                               SuspendVideo( video );
+                       else if( video->state == CLVIDEO_PAUSE )
+                               video->starttime = realtime + video->framenum * video->framerate;
+                       else 
+                               VideoFrame( video );
+
+       if( videoarray->state == CLVIDEO_FIRSTFRAME )
+               CL_VideoStop();
+}
+
+void CL_Video_Shutdown( void )
+{
+       int i;
+       for( i = 0 ; i < MAXCLVIDEOS ; i++ )
+               CL_CloseVideo( &videoarray[ i ] );
+
+       R_FreeTexturePool( &cl_videotexturepool );
+       Mem_FreePool( &cl_videomempool );
+}
+
+void CL_PurgeOwner( int owner )
+{
+       int i;
+       for( i = 0 ; i < MAXCLVIDEOS ; i++ )
+               if( videoarray[ i ].ownertag == owner )
+                       CL_CloseVideo( &videoarray[ i ] );
+}
+
+int cl_videoplaying = false; // old, but still supported
+
 void CL_DrawVideo(void)
 {
        if (cl_videoplaying)
-       {
-               drawqueuemesh_t mesh;
-               float vertex3f[12];
-               float texcoord2f[8];
-               float color4f[16];
-               float s1, t1, s2, t2, x1, y1, x2, y2;
-               x1 = 0;
-               y1 = 0;
-               x2 = vid.conwidth;
-               y2 = vid.conheight;
-               R_FragmentLocation(cl_videotexture, NULL, NULL, &s1, &t1, &s2, &t2);
-               texcoord2f[0] = s1;texcoord2f[1] = t1;
-               texcoord2f[2] = s2;texcoord2f[3] = t1;
-               texcoord2f[4] = s2;texcoord2f[5] = t2;
-               texcoord2f[6] = s1;texcoord2f[7] = t2;
-               R_FillColors(color4f, 4, 1, 1, 1, 1);
-               vertex3f[ 0] = x1;vertex3f[ 1] = y1;vertex3f[ 2] = 0;
-               vertex3f[ 3] = x2;vertex3f[ 4] = y1;vertex3f[ 5] = 0;
-               vertex3f[ 6] = x2;vertex3f[ 7] = y2;vertex3f[ 8] = 0;
-               vertex3f[ 9] = x1;vertex3f[10] = y2;vertex3f[11] = 0;
-               mesh.texture = cl_videotexture;
-               mesh.num_triangles = 2;
-               mesh.num_vertices = 4;
-               mesh.data_element3i = polygonelements;
-               mesh.data_vertex3f = vertex3f;
-               mesh.data_texcoord2f = texcoord2f;
-               mesh.data_color4f = color4f;
-               DrawQ_Mesh(&mesh, 0);
-               //DrawQ_Pic(0, 0, "engine_videoframe", vid.conwidth, vid.conheight, 1, 1, 1, 1, 0);
-       }
+               DrawQ_Pic(0, 0, videoarray->cpif.name, vid.conwidth, vid.conheight, 1, 1, 1, 1, 0);
 }
 
 void CL_VideoStart(char *filename)
 {
-       char *errorstring;
-       cl_videostream = dpvsimpledecode_open(filename, &errorstring);
-       if (!cl_videostream)
-       {
-               Con_Printf("unable to open \"%s\", error: %s\n", filename, errorstring);
+       if( videoarray->state != CLVIDEO_UNUSED )
+               CL_CloseVideo( videoarray );
+       if( !OpenVideo( videoarray, filename, filename, 0 ) )
                return;
-       }
 
        cl_videoplaying = true;
-       cl_videostarttime = realtime;
-       cl_videoframenum = -1;
-       cl_videoframerate = dpvsimpledecode_getframerate(cl_videostream);
-       cl_videoimagewidth = dpvsimpledecode_getwidth(cl_videostream);
-       cl_videoimageheight = dpvsimpledecode_getheight(cl_videostream);
-
-       // RGBA format
-       cl_videoimagedata_bytesperpixel = 4;
-       cl_videoimagedata_rmask = BigLong(0xFF000000);
-       cl_videoimagedata_gmask = BigLong(0x00FF0000);
-       cl_videoimagedata_bmask = BigLong(0x0000FF00);
-       cl_videoimagedata = Mem_Alloc(clvideomempool, cl_videoimagewidth * cl_videoimageheight * cl_videoimagedata_bytesperpixel);
-       //memset(cl_videoimagedata, 97, cl_videoimagewidth * cl_videoimageheight * cl_videoimagedata_bytesperpixel);
 
-       cl_videotexturepool = R_AllocTexturePool();
-       cl_videotexture = R_LoadTexture2D(cl_videotexturepool, "videotexture", cl_videoimagewidth, cl_videoimageheight, NULL, TEXTYPE_RGBA, TEXF_FRAGMENT, NULL);
+       CL_StartVideo( videoarray );
 }
 
 void CL_VideoStop(void)
 {
        cl_videoplaying = false;
 
-       if (cl_videostream)
-               dpvsimpledecode_close(cl_videostream);
-       cl_videostream = NULL;
-
-       if (cl_videoimagedata)
-               Mem_Free(cl_videoimagedata);
-       cl_videoimagedata = NULL;
-
-       cl_videotexture = NULL;
-       R_FreeTexturePool(&cl_videotexturepool);
-
-       Draw_FreePic("engine_videoframe");
+       CL_CloseVideo( videoarray );
 }
 
 static void CL_PlayVideo_f(void)
@@ -151,10 +301,16 @@ static void CL_StopVideo_f(void)
        CL_VideoStop();
 }
 
-void CL_Video_Init(void)
+void CL_Video_Init( void )
 {
-       Cmd_AddCommand("playvideo", CL_PlayVideo_f);
-       Cmd_AddCommand("stopvideo", CL_StopVideo_f);
+       cl_videobytesperpixel = 4;
+       cl_videormask = BigLong(0xFF000000);
+       cl_videogmask = BigLong(0x00FF0000);
+       cl_videobmask = BigLong(0x0000FF00);
+
+       cl_videomempool = Mem_AllocPool( "CL_Video", 0, NULL );
+       cl_videotexturepool = R_AllocTexturePool();
 
-       clvideomempool = Mem_AllocPool("CL_Video", 0, NULL);
+       Cmd_AddCommand( "playvideo", CL_PlayVideo_f );
+       Cmd_AddCommand( "stopvideo", CL_StopVideo_f );
 }
index 58738af..9a6ad8c 100644 (file)
@@ -2,11 +2,61 @@
 #ifndef CL_VIDEO_H
 #define CL_VIDEO_H
 
+#define MAXCLVIDEOS            64 + 1 // 1 video is reserved for the cinematic mode
+#define CLVIDEOPREFIX  "_"
+#define CLTHRESHOLD            2.0
+
+typedef enum clvideostate_s
+{
+       CLVIDEO_UNUSED,
+       CLVIDEO_PLAY,
+       CLVIDEO_LOOP,
+       CLVIDEO_PAUSE,
+       CLVIDEO_FIRSTFRAME,
+} clvideostate_t;
+
+typedef struct clvideo_s
+{
+       int             ownertag;
+       clvideostate_t state;
+
+       // private stuff
+       void    *stream;
+
+       double  starttime;
+       int             framenum;
+       double  framerate;
+
+       void    *imagedata;
+
+       cachepic_t cpif;
+
+       // if a video is suspended, it is automatically paused (else we'd still have to process the frames)
+    double  lasttime; // used to determine whether the video's resources should be freed or not
+       qboolean suspended; // when lasttime - realtime > THRESHOLD, all but the stream is freed
+
+       char    filename[MAX_QPATH];
+} clvideo_t;
+
+clvideo_t*     CL_OpenVideo( char *filename, char *name, int owner );
+clvideo_t*     CL_GetVideo( char *name );
+void           CL_StartVideo( clvideo_t * video );
+void           CL_LoopVideo( clvideo_t * video );
+void           CL_PauseVideo( clvideo_t * video );
+void           CL_StopVideo( clvideo_t * video );
+void           CL_RestartVideo( clvideo_t *video );
+void           CL_CloseVideo( clvideo_t * video );
+void           CL_PurgeOwner( int owner );
+
+void           CL_VideoFrame( void ); // update all videos
+void           CL_Video_Init( void );
+void           CL_Video_Shutdown( void );
+
+// old interface
 extern int cl_videoplaying;
-void CL_VideoFrame(void);
-void CL_DrawVideo(void);
-void CL_VideoStart(char *filename);
-void CL_VideoStop(void);
-void CL_Video_Init(void);
+
+void CL_DrawVideo( void );
+void CL_VideoStart( char *filename );
+void CL_VideoStop( void );
 
 #endif
index e63b7d4..9c801c4 100644 (file)
--- a/gl_draw.c
+++ b/gl_draw.c
@@ -22,6 +22,8 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 #include "image.h"
 #include "wad.h"
 
+#include "cl_video.h"
+
 
 static rtexture_t *char_texture;
 
@@ -272,6 +274,14 @@ cachepic_t *Draw_CachePic (char *path)
                if (!strcmp (path, pic->name))
                        return pic;
 
+       if (!strncmp(CLVIDEOPREFIX, path, sizeof(CLVIDEOPREFIX) - 1)) {
+               clvideo_t *video;
+
+               video = CL_GetVideo(path);
+               if( video )
+                       return &video->cpif;
+       }
+
        if (numcachepics == MAX_CACHED_PICS)
                Sys_Error ("numcachepics == MAX_CACHED_PICS");
        pic = cachepics + (numcachepics++);
diff --git a/host.c b/host.c
index e6b2084..c7fe6ac 100644 (file)
--- a/host.c
+++ b/host.c
@@ -942,6 +942,7 @@ void Host_Init (void)
        if (cls.state != ca_dedicated)
        {
                VID_Open();
+               CL_Video_Init();
                CL_InitTEnts ();  // We must wait after sound startup to load tent sounds
                SCR_BeginLoadingPlaque();
                MR_Init();
@@ -1009,6 +1010,7 @@ void Host_Shutdown(void)
        // AK shutdown PRVM
        // AK hmm, no PRVM_Shutdown(); yet
 
+       CL_Video_Shutdown();
 
        Host_SaveConfig_f();
 
index d3c5bbd..c7d6b6b 100644 (file)
@@ -915,7 +915,7 @@ qboolean PRVM_ED_ParseEpair(prvm_edict_t *ent, ddef_t *key, const char *s)
        if (ent)
                val = (prvm_eval_t *)((int *)ent->v + key->ofs);
        else
-               val = (prvm_eval_t *)((int *)pr_globals + key->ofs);
+               val = (prvm_eval_t *)((int *)prog->globals + key->ofs);
        switch (key->type & ~DEF_SAVEGLOBAL)
        {
        case ev_string:
@@ -1013,12 +1013,9 @@ void PRVM_ED_EdictSet_f(void)
        ed = PRVM_EDICT_NUM(atoi(Cmd_Argv(2)));
 
        if((key = PRVM_ED_FindField(Cmd_Argv(3))) == 0)
-       {
                Con_Printf("Key %s not found !\n", Cmd_Argv(3));
-               return;
-       }
-
-       PRVM_ED_ParseEpair(ed, key, Cmd_Argv(4));
+       else
+               PRVM_ED_ParseEpair(ed, key, Cmd_Argv(4));
 
        PRVM_End;
 }
@@ -1681,6 +1678,31 @@ void PRVM_Global_f(void)
 
 /*
 ===============
+PRVM_GlobalSet
+===============
+*/
+void PRVM_GlobalSet_f(void)
+{
+       ddef_t *global;
+       if( Cmd_Argc() != 4 ) {
+               Con_Printf( "prvm_globalset <program name> <global name> <value>\n" );
+               return;
+       }
+
+       PRVM_Begin;
+       if( !PRVM_SetProgFromString( Cmd_Argv(1) ) )
+               return;
+
+       global = PRVM_ED_FindGlobal( Cmd_Argv(2) );
+       if( !global )
+               Con_Printf( "No global '%s' in %s!\n", Cmd_Argv(2), Cmd_Argv(1) );
+       else 
+               PRVM_ED_ParseEpair( NULL, global, Cmd_Argv(3) );
+       PRVM_End;
+}
+
+/*
+===============
 PRVM_Init
 ===============
 */
@@ -1693,6 +1715,7 @@ void PRVM_Init (void)
        Cmd_AddCommand ("prvm_fields", PRVM_Fields_f);
        Cmd_AddCommand ("prvm_globals", PRVM_Globals_f);
        Cmd_AddCommand ("prvm_global", PRVM_Global_f);
+       Cmd_AddCommand ("prvm_globalset", PRVM_GlobalSet_f);
        Cmd_AddCommand ("prvm_edictset", PRVM_ED_EdictSet_f);
        // LordHavoc: optional runtime bounds checking (speed drain, but worth it for security, on by default - breaks most QCCX features (used by CRMod and others))
        Cvar_RegisterVariable (&prvm_boundscheck);
diff --git a/todo b/todo
index 739280e..cb8230f 100644 (file)
--- a/todo
+++ b/todo
 1 lhfire: get lhfire_gui build from Tomaz.
 1 lhfire: post lhfire_gui build.
 1 lhfire: prepare example scripts for release.
+2 darkplaces client: make CL_Video use TEXF_FRAGMENT again by adding general, transparent support for it in all drawqueue functions (so you dont need to call FragmentLocation) (Black)
 2 darkplaces cleanup: add cvar callbacks and make net cvars have callbacks
 2 darkplaces cleanup: add fs_datapath and fs_userpath cvars to better support Linux, this can be done by making each gamedir add both the basepath and userpath variants of the gamedir, and making sure the userpath one is last so it is used for writing (Mercury)
 2 darkplaces cleanup: change menu qc key input to using string key names instead of numbers (the bind alias names should be able to do this) (Mercury, Black, Vermeulen)
 4 darkplaces cleanup: use the memory pool nesting feature ! (Black[,Vicious])
 4 darkplaces client: add decals on models (Urre)
 4 darkplaces client: add qw protocol support (making darkplaces work as a qwcl client) (tell Fuh)
-4 darkplaces client: add video playback handles to the cl_video code so that other systems can use streaming video textures, and allow the menu qc to use these (Black)
+d darkplaces client: add video playback handles to the cl_video code so that other systems can use streaming video textures, and allow the menu qc to use these (Black)
 4 darkplaces client: figure out why intermission camera pitch changes after a moment (Tomaz)
 4 darkplaces console: add setlock command which marks a cvar as locked, and sends it over network to connected clients as a setlock command, the clients will not allow the user to modify the cvars while locked (and will only accept setlock commands over the network), and cvars are unlocked when level ends - the server will send the locks again on next level (VorteX)
 4 darkplaces csqc: add builtin to clientside qc for reading triangles of model meshes (useful to orient a ui along a triangle of a model mesh)