diff --git a/utils/pxstudiomdl/CMakeLists.txt b/utils/pxstudiomdl/CMakeLists.txt index 481e4180..38549f82 100644 --- a/utils/pxstudiomdl/CMakeLists.txt +++ b/utils/pxstudiomdl/CMakeLists.txt @@ -16,10 +16,13 @@ list(APPEND DIR_SOURCES "../common/virtualfs.cpp" "../common/zone.cpp" "${CMAKE_SOURCE_DIR}/public/crclib.cpp" + "gltf_source.cpp" "image_utils.cpp" + "model_source.cpp" "optimize.cpp" "simpilfy.cpp" "skin.cpp" + "smd_source.cpp" "studiomdl.cpp" "write.cpp" ) @@ -89,6 +92,9 @@ target_link_libraries(${PROJECT_NAME} PRIVATE imagelib ) +find_path(CGLTF_INCLUDE_DIRS "cgltf.h") +target_include_directories(${PROJECT_NAME} PRIVATE ${CGLTF_INCLUDE_DIRS}) + # link platform-specific depedency libraries if(NOT XASH_WIN32) target_link_libraries(${PROJECT_NAME} PRIVATE diff --git a/utils/pxstudiomdl/gltf_source.cpp b/utils/pxstudiomdl/gltf_source.cpp new file mode 100644 index 00000000..bfdd1718 --- /dev/null +++ b/utils/pxstudiomdl/gltf_source.cpp @@ -0,0 +1,728 @@ +/* +gltf_source.cpp - glTF reference mesh/animation importer +Copyright (C) 2026 SNMetamorph + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. +*/ + +#include "port.h" + +// compile cgltf implementation exactly once; the implementation section lives +// outside cgltf.h's include guard, so undef the macro before any other include +#define CGLTF_IMPLEMENTATION +#include +#undef CGLTF_IMPLEMENTATION + +#include "cmdlib.h" +#include "mathlib.h" +#include "stringlib.h" +#include "file_system.h" +#include "gltf_source.h" +#include + +#define GLTF_ROOT_BONE_NAME "static_prop" + +namespace +{ + struct GltfNodeChannels + { + const cgltf_animation_sampler *translation; + const cgltf_animation_sampler *rotation; + const cgltf_animation_sampler *scale; + }; + + struct GltfSavedTRS + { + cgltf_bool ht, hr, hs; + float t[3]; + float r[4]; + float s[3]; + }; +} + +//----------------------------------------------------------------------------- +// Purpose: pick a texture name for a primitive: material name, then mesh name, +// then the base color texture image name +//----------------------------------------------------------------------------- +static void GetTextureName( const cgltf_mesh *mesh, const cgltf_primitive *prim, char *out, size_t outSize ) +{ + out[0] = '\0'; + + if( prim->material && prim->material->name && prim->material->name[0] ) + { + Q_strncpy( out, prim->material->name, outSize ); + COM_DefaultExtension( out, ".tga" ); + } + else if( mesh->name && mesh->name[0] ) + { + Q_strncpy( out, mesh->name, outSize ); + COM_DefaultExtension( out, ".tga" ); + } + else if( prim->material && prim->material->has_pbr_metallic_roughness ) + { + const cgltf_texture *tex = prim->material->pbr_metallic_roughness.base_color_texture.texture; + + if( tex && tex->image && tex->image->uri && tex->image->uri[0] ) + { + const char *uri = tex->image->uri; + const char *base = uri; + + // strip directory part + for( const char *p = uri; *p; p++ ) + { + if( *p == '/' || *p == '\\' ) + base = p + 1; + } + + Q_strncpy( out, base, outSize ); + COM_StripExtension( out ); + COM_DefaultExtension( out, ".tga" ); + } + } + + if( out[0] == '\0' ) + { + Q_strncpy( out, "default", outSize ); + COM_DefaultExtension( out, ".tga" ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: attach a base-color image embedded in the source file (bufferView) +// to the given texture so Grab_Skin can decode it without a file +//----------------------------------------------------------------------------- +static void AttachEmbeddedImage( int texIndex, const cgltf_material *material ) +{ + if( texIndex < 0 || texIndex >= g_numtextures || g_texture[texIndex].pembedded != nullptr ) + return; + + if( material == nullptr || !material->has_pbr_metallic_roughness ) + return; + + const cgltf_texture *tex = material->pbr_metallic_roughness.base_color_texture.texture; + + if( tex == nullptr || tex->image == nullptr || tex->image->buffer_view == nullptr ) + return; + + const cgltf_buffer_view *view = tex->image->buffer_view; + const uint8_t *data = cgltf_buffer_view_data( view ); + + if( data == nullptr || view->size == 0 ) + return; + + byte *copy = (byte *)Mem_Alloc( view->size ); + memcpy( copy, data, view->size ); + + g_texture[texIndex].pembedded = copy; + g_texture[texIndex].embeddsize = (int)view->size; + + MsgDev( D_INFO, "embedded texture: %s (%i bytes, %s)\n", g_texture[texIndex].name, (int)view->size, + ( tex->image->mime_type && tex->image->mime_type[0] ) ? tex->image->mime_type : "unknown" ); +} + +//----------------------------------------------------------------------------- +// Purpose: convert glTF node world matrix (Y-up, column-major) to GoldSrc matrix3x4 (Z-up) +//----------------------------------------------------------------------------- +static matrix3x4 GlTFNodeWorldGold( const cgltf_node *node ) +{ + cgltf_float m[16]; + + cgltf_node_transform_world( node, m ); + + // glTF world matrix W is column-major; GoldSrc space is M = C * W with C: (x,y,z)->(x,-z,y) + return matrix3x4( + m[0], -m[2], m[1], + m[4], -m[6], m[5], + m[8], -m[10], m[9], + m[12], -m[14], m[13] ); +} + +//----------------------------------------------------------------------------- +// Purpose: build bind-pose bones (names, hierarchy and local transforms) from a glTF skin +//----------------------------------------------------------------------------- +static bool BuildSkinSkeleton( const cgltf_skin *skin, s_node_t *localBone, s_bone_t *skeleton, int &numbones ) +{ + if( skin->joints_count == 0 || skin->joints_count > MAXSTUDIOSRCBONES ) + return false; + + int nj = (int)skin->joints_count; + + for( int i = 0; i < nj; i++ ) + { + const cgltf_node *n = skin->joints[i]; + const char *nm = ( n->name && n->name[0] ) ? n->name : "bone"; + + Q_strncpy( localBone[i].name, nm, sizeof( localBone[i].name )); + localBone[i].parent = -1; + } + + for( int i = 0; i < nj; i++ ) + { + const cgltf_node *n = skin->joints[i]; + + if( n->parent ) + { + for( int k = 0; k < nj; k++ ) + { + if( skin->joints[k] == n->parent ) + { + localBone[i].parent = k; + break; + } + } + } + } + + std::vector world( nj ); + + for( int i = 0; i < nj; i++ ) + world[i] = GlTFNodeWorldGold( skin->joints[i] ); + + for( int i = 0; i < nj; i++ ) + { + int p = localBone[i].parent; + matrix3x4 local = ( p == -1 ) ? world[i] : world[p].Invert().ConcatTransforms( world[i] ); + local.GetStudioTransform( skeleton[i].pos, skeleton[i].rot ); + } + + numbones = nj; + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: collapse duplicate/small bone weights and normalize them +//----------------------------------------------------------------------------- +static int NormalizeBoneWeights( int count, int bones[MAXSTUDIOBONEWEIGHTS], float weights[MAXSTUDIOBONEWEIGHTS] ) +{ + for( int i = 0; i < count - 1; i++ ) + { + for( int j = i + 1; j < count; j++ ) + { + if( bones[i] == bones[j] ) + { + weights[i] += weights[j]; + weights[j] = 0.0f; + } + } + } + + for( int i = 0; i < count - 1; i++ ) + { + for( int j = i + 1; j < count; j++ ) + { + if( weights[j] > weights[i] ) + { + float w = weights[i]; weights[i] = weights[j]; weights[j] = w; + int b = bones[i]; bones[i] = bones[j]; bones[j] = b; + } + } + } + + while( count > 1 && weights[count-1] < 0.005f ) + count--; + + float sum = 0.0f; + for( int i = 0; i < count; i++ ) + sum += weights[i]; + + if( sum <= 0.0f ) + { + for( int i = 0; i < count; i++ ) + weights[i] = 1.0f / count; + } + else + { + for( int i = 0; i < count; i++ ) + weights[i] /= sum; + } + + return count; +} + +//----------------------------------------------------------------------------- +// Purpose: sample an animation channel at the given time +//----------------------------------------------------------------------------- +static void SampleChannel( const cgltf_animation_sampler *s, float t, float *out, int n ) +{ + if( s == nullptr || s->input == nullptr || s->output == nullptr ) + return; + + cgltf_size count = s->input->count; + + if( count == 0 ) + return; + + if( count == 1 ) + { + cgltf_accessor_read_float( s->output, 0, out, n ); + return; + } + + float t0, tLast; + cgltf_accessor_read_float( s->input, 0, &t0, 1 ); + cgltf_accessor_read_float( s->input, count - 1, &tLast, 1 ); + + if( t <= t0 ) + { + cgltf_accessor_read_float( s->output, 0, out, n ); + return; + } + + if( t >= tLast ) + { + cgltf_accessor_read_float( s->output, count - 1, out, n ); + return; + } + + cgltf_size i = 0; + for( ; i + 1 < count; i++ ) + { + float ta, tb; + cgltf_accessor_read_float( s->input, i, &ta, 1 ); + cgltf_accessor_read_float( s->input, i + 1, &tb, 1 ); + if( t >= ta && t <= tb ) + break; + } + + float va[4] = { 0, 0, 0, 0 }; + float vb[4] = { 0, 0, 0, 0 }; + cgltf_accessor_read_float( s->output, i, va, n ); + cgltf_accessor_read_float( s->output, i + 1, vb, n ); + + if( s->interpolation == cgltf_interpolation_type_step ) + { + for( int k = 0; k < n; k++ ) { + out[k] = va[k]; + } + return; + } + + float ta, tb; + cgltf_accessor_read_float( s->input, i, &ta, 1 ); + cgltf_accessor_read_float( s->input, i + 1, &tb, 1 ); + float u = ( tb > ta ) ? ( t - ta ) / ( tb - ta ) : 0.0f; + + if( n == 4 && s->interpolation == cgltf_interpolation_type_linear ) + { + // shortest-path spherical interpolation of the rotation + Vector4D p( va[0], va[1], va[2], va[3] ); + Vector4D q( vb[0], vb[1], vb[2], vb[3] ); + Vector4D qt; + + QuaternionSlerp( p, q, u, qt ); + + out[0] = qt.x; + out[1] = qt.y; + out[2] = qt.z; + out[3] = qt.w; + return; + } + + for( int k = 0; k < n; k++ ) { + out[k] = va[k] + ( vb[k] - va[k] ) * u; + } +} + +CGltfSource::CGltfSource( const char *name ) + : m_data( nullptr, &cgltf_free ) +{ + char path[1024]; + cgltf_options options = {}; + cgltf_data *data = nullptr; + + Q_snprintf( path, sizeof( path ), "%s/%s", cddir[numdirs], name ); + COM_DefaultExtension( path, ".gltf" ); + m_filename = path; + + cgltf_result result = cgltf_parse_file( &options, path, &data ); + + if( result != cgltf_result_success || data == nullptr ) { + COM_FatalError( "failed to parse glTF file %s (error %d)\n", path, (int)result ); + } + + result = cgltf_load_buffers( &options, data, path ); + + if( result != cgltf_result_success ) + { + cgltf_free( data ); + COM_FatalError( "failed to load glTF buffers %s (error %d)\n", path, (int)result ); + } + + m_data.reset( data ); +} + +void CGltfSource::GrabStudio( s_model_t *pmodel ) +{ + if( !m_data ) + COM_FatalError( "glTF file %s was not loaded\n", m_filename.c_str()); + + cgltf_data *data = m_data.get(); + const cgltf_skin *skin = ( data->skins_count > 0 ) ? &data->skins[0] : nullptr; + + if( !BuildSkinSkeleton( skin, pmodel->localBone, pmodel->skeleton, pmodel->numbones )) + { + // no skeleton: bind everything to a single root bone + pmodel->numbones = 1; + Q_strncpy( pmodel->localBone[0].name, GLTF_ROOT_BONE_NAME, sizeof( pmodel->localBone[0].name )); + pmodel->localBone[0].parent = -1; + pmodel->skeleton[0].pos = Vector( 0, 0, 0 ); + pmodel->skeleton[0].rot = Radian( 0, 0, 0 ); + } + + Build_Reference( pmodel ); + + // glTF is Y-up while GoldSrc is Z-up: rotate all geometry 90 degrees around X + matrix3x4 convert = matrix3x4( g_vecZero, Radian( M_PI / 2.0f, 0.0f, 0.0f )); + + for( cgltf_size mi = 0; mi < data->meshes_count; mi++ ) + { + const cgltf_mesh *mesh = &data->meshes[mi]; + + for( cgltf_size pi = 0; pi < mesh->primitives_count; pi++ ) + { + const cgltf_primitive *prim = &mesh->primitives[pi]; + const cgltf_accessor *posAcc = nullptr; + const cgltf_accessor *nrmAcc = nullptr; + const cgltf_accessor *uvAcc = nullptr; + const cgltf_accessor *jointAcc = nullptr; + const cgltf_accessor *weightAcc = nullptr; + + for( cgltf_size ai = 0; ai < prim->attributes_count; ai++ ) + { + const cgltf_attribute *attr = &prim->attributes[ai]; + + if( attr->type == cgltf_attribute_type_position ) + posAcc = attr->data; + else if( attr->type == cgltf_attribute_type_normal ) + nrmAcc = attr->data; + else if( attr->type == cgltf_attribute_type_texcoord && attr->index == 0 ) + uvAcc = attr->data; + else if( attr->type == cgltf_attribute_type_joints ) + jointAcc = attr->data; + else if( attr->type == cgltf_attribute_type_weights ) + weightAcc = attr->data; + } + + if( posAcc == nullptr ) + continue; + + bool skinned = ( skin != nullptr && jointAcc != nullptr && weightAcc != nullptr ); + + char texturename[64]; + s_mesh_t *pmesh; + + GetTextureName( mesh, prim, texturename, sizeof( texturename )); + pmesh = LookupMesh( pmodel, texturename ); + AttachEmbeddedImage( pmesh->skinref, prim->material ); + + cgltf_size indexCount = prim->indices ? prim->indices->count : posAcc->count; + + for( cgltf_size t = 0; t + 2 < indexCount; t += 3 ) + { + s_trianglevert_t *ptri = LookupTriangle( pmesh, pmesh->numtris ); + cgltf_size idx[3]; + Vector vpos[3]; + Vector vnorm[3]; + float uv[3][2]; + int bones[3][MAXSTUDIOBONEWEIGHTS]; + float weights[3][MAXSTUDIOBONEWEIGHTS]; + int numb[3] = { 1, 1, 1 }; + + for( int j = 0; j < 3; j++ ) + { + idx[j] = prim->indices ? cgltf_accessor_read_index( prim->indices, t + j ) : ( t + j ); + + float p[3] = { 0, 0, 0 }; + float n[3] = { 0, 0, 0 }; + + cgltf_accessor_read_float( posAcc, idx[j], p, 3 ); + if( nrmAcc ) { + cgltf_accessor_read_float( nrmAcc, idx[j], n, 3 ); + } + + if( uvAcc ) + { + cgltf_accessor_read_float( uvAcc, idx[j], uv[j], 2 ); + // glTF V origin is top-left, SMD expects bottom-left + uv[j][1] = 1.0f - uv[j][1]; + } + else + { + uv[j][0] = 0.0f; + uv[j][1] = 0.0f; + } + + vpos[j] = convert.VectorTransform( Vector( p[0], p[1], p[2] )); + vnorm[j] = convert.VectorRotate( Vector( n[0], n[1], n[2] )); + + bones[j][0] = 0; + weights[j][0] = 1.0f; + + if( skinned ) + { + float jf[4] = { 0, 0, 0, 0 }; + float wf[4] = { 0, 0, 0, 0 }; + + cgltf_accessor_read_float( jointAcc, idx[j], jf, 4 ); + cgltf_accessor_read_float( weightAcc, idx[j], wf, 4 ); + + for( int k = 0; k < MAXSTUDIOBONEWEIGHTS; k++ ) + { + bones[j][k] = (int)( jf[k] + 0.5f ); + weights[j][k] = wf[k]; + } + + numb[j] = NormalizeBoneWeights( MAXSTUDIOBONEWEIGHTS, bones[j], weights[j] ); + + // match SMD behavior: without $boneweights keep only the dominant bone + if( !allow_boneweights ) + { + weights[j][0] = 1.0f; + numb[j] = 1; + } + } + } + + // generate a face normal if the mesh doesn't provide one + if( nrmAcc == nullptr ) + { + Vector edge1 = vpos[1] - vpos[0]; + Vector edge2 = vpos[2] - vpos[0]; + Vector facenorm = CrossProduct( edge1, edge2 ).Normalize(); + + vnorm[0] = vnorm[1] = vnorm[2] = facenorm; + } + + for( int j = 0; j < 3; j++ ) + { + s_trianglevert_t *ptriv = pmodel->flip_triangles ? ( ptri + 2 - j ) : ( ptri + j ); + s_srcvertex_t *srcv; + + pmodel->srcvert.AddToTail(); + srcv = &pmodel->srcvert[pmodel->srcvert.Count() - 1]; + memset( srcv, 0, sizeof( *srcv )); + + srcv->vert = vpos[j] * pmodel->scale; + srcv->norm = vnorm[j]; + srcv->skinref = pmesh->skinref; + srcv->localWeight.numbones = numb[j]; + + for( int k = 0; k < numb[j]; k++ ) + { + srcv->localWeight.bone[k] = bones[j][k]; + srcv->localWeight.weight[k] = weights[j][k]; + } + + if( skinned && allow_boneweights ) { + has_boneweights = true; + } + + ptriv->u = uv[j][0]; + ptriv->v = uv[j][1]; + ptriv->vertindex = ptriv->normindex = pmodel->srcvert.Count() - 1; + } + + pmesh->numtris++; + } + } + } +} + +void CGltfSource::GrabAnimation( s_animation_t *panim ) +{ + if( !m_data ) + COM_FatalError( "glTF file %s was not loaded\n", m_filename.c_str()); + + cgltf_data *data = m_data.get(); + const cgltf_skin *skin = ( data->skins_count > 0 ) ? &data->skins[0] : nullptr; + std::vector bindSkel( MAXSTUDIOSRCBONES ); + int numbones = 0; + + panim->source.startframe = 0; + panim->source.endframe = 0; + panim->source.numframes = 1; + + if( !skin || !BuildSkinSkeleton( skin, panim->localBone, bindSkel.data(), numbones )) + { + // static-only: emit a single identity frame for the root bone + panim->numbones = 1; + Q_strncpy( panim->localBone[0].name, GLTF_ROOT_BONE_NAME, sizeof( panim->localBone[0].name )); + panim->localBone[0].parent = -1; + + panim->rawanim[0] = (s_bone_t *)Mem_Alloc( sizeof( s_bone_t )); + panim->rawanim[0][0].pos = Vector( 0, 0, 0 ); + panim->rawanim[0][0].rot = Radian( 0, 0, 0 ); + return; + } + + panim->numbones = numbones; + + // no animation: emit a single bind-pose frame + if( data->animations_count == 0 || data->animations[0].channels_count == 0 ) + { + panim->rawanim[0] = (s_bone_t *)Mem_Alloc( sizeof( s_bone_t ) * numbones ); + + for( int b = 0; b < numbones; b++ ) { + panim->rawanim[0][b] = bindSkel[b]; + } + return; + } + + const cgltf_animation *anim = &data->animations[0]; + int nodeCount = (int)data->nodes_count; + + if( data->animations_count > 1 ) + { + const char *animName = (anim->name && anim->name[0]) ? anim->name : ""; + Msg( "^3Warning:^7 glTF file \"%s\" contains %d animations, only the first one \"%s\" will be imported\n", + m_filename.c_str(), (int)data->animations_count, animName ); + } + + // gather per-node channels + std::vector nodeCh( nodeCount ); + + for( int n = 0; n < nodeCount; n++ ) + nodeCh[n].translation = nodeCh[n].rotation = nodeCh[n].scale = nullptr; + + for( cgltf_size ci = 0; ci < anim->channels_count; ci++ ) + { + const cgltf_animation_channel *c = &anim->channels[ci]; + int n = (int)( c->target_node - data->nodes ); + + if( n < 0 || n >= nodeCount ) + continue; + + if( c->target_path == cgltf_animation_path_type_translation ) + nodeCh[n].translation = c->sampler; + else if( c->target_path == cgltf_animation_path_type_rotation ) + nodeCh[n].rotation = c->sampler; + else if( c->target_path == cgltf_animation_path_type_scale ) + nodeCh[n].scale = c->sampler; + } + + // frame count and duration come from the samplers + cgltf_size numframes = 1; + float maxTime = 0.0f; + const cgltf_animation_sampler *master = nullptr; + + for( cgltf_size si = 0; si < anim->samplers_count; si++ ) + { + const cgltf_animation_sampler *s = &anim->samplers[si]; + + if( s->input->count > numframes ) + { + numframes = s->input->count; + master = s; + } + + if( s->input->count > 0 ) + { + float tLast; + cgltf_accessor_read_float( s->input, s->input->count - 1, &tLast, 1 ); + if( tLast > maxTime ) + maxTime = tLast; + } + } + + // sample using the exact key times of the densest sampler so we stay on-key + std::vector frameTimes( numframes ); + + for( cgltf_size f = 0; f < numframes; f++ ) + { + if( master ) + cgltf_accessor_read_float( master->input, f, &frameTimes[f], 1 ); + else + frameTimes[f] = ( numframes > 1 ) ? ( maxTime * (float)f / (float)( numframes - 1 )) : 0.0f; + } + + for( cgltf_size f = 0; f < numframes; f++ ) + panim->rawanim[f] = (s_bone_t *)Mem_Alloc( sizeof( s_bone_t ) * numbones ); + + panim->source.startframe = 0; + panim->source.endframe = (int)numframes - 1; + panim->source.numframes = (int)numframes; + + // snapshot node transforms so we can drive cgltf's exact matrix math per frame + std::vector saved( nodeCount ); + + for( int n = 0; n < nodeCount; n++ ) + { + const cgltf_node *nd = &data->nodes[n]; + saved[n].ht = nd->has_translation; + saved[n].hr = nd->has_rotation; + saved[n].hs = nd->has_scale; + memcpy( saved[n].t, nd->translation, sizeof( saved[n].t )); + memcpy( saved[n].r, nd->rotation, sizeof( saved[n].r )); + memcpy( saved[n].s, nd->scale, sizeof( saved[n].s )); + } + + std::vector worldGold( numbones ); + + for( cgltf_size f = 0; f < numframes; f++ ) + { + float t = frameTimes[f]; + + // apply sampled transforms onto the nodes so cgltf resolves the hierarchy + for( int n = 0; n < nodeCount; n++ ) + { + cgltf_node *nd = &data->nodes[n]; + const GltfNodeChannels &ch = nodeCh[n]; + + nd->has_translation = saved[n].ht; + memcpy( nd->translation, saved[n].t, sizeof( nd->translation )); + if( ch.translation ) + { + nd->has_translation = 1; + SampleChannel( ch.translation, t, nd->translation, 3 ); + } + + nd->has_rotation = saved[n].hr; + memcpy( nd->rotation, saved[n].r, sizeof( nd->rotation )); + if( ch.rotation ) + { + nd->has_rotation = 1; + SampleChannel( ch.rotation, t, nd->rotation, 4 ); + } + + nd->has_scale = saved[n].hs; + memcpy( nd->scale, saved[n].s, sizeof( nd->scale )); + if( ch.scale ) + { + nd->has_scale = 1; + SampleChannel( ch.scale, t, nd->scale, 3 ); + } + } + + for( int b = 0; b < numbones; b++ ) + worldGold[b] = GlTFNodeWorldGold( skin->joints[b] ); + + // derive parent-relative GoldSrc transforms and decompose + for( int b = 0; b < numbones; b++ ) + { + int pb = panim->localBone[b].parent; + matrix3x4 l = ( pb == -1 ) ? worldGold[b] : worldGold[pb].Invert().ConcatTransforms( worldGold[b] ); + l.GetStudioTransform( panim->rawanim[f][b].pos, panim->rawanim[f][b].rot ); + } + } + + // restore node transforms + for( int n = 0; n < nodeCount; n++ ) + { + cgltf_node *nd = &data->nodes[n]; + nd->has_translation = saved[n].ht; + nd->has_rotation = saved[n].hr; + nd->has_scale = saved[n].hs; + memcpy( nd->translation, saved[n].t, sizeof( nd->translation )); + memcpy( nd->rotation, saved[n].r, sizeof( nd->rotation )); + memcpy( nd->scale, saved[n].s, sizeof( nd->scale )); + } +} diff --git a/utils/pxstudiomdl/gltf_source.h b/utils/pxstudiomdl/gltf_source.h new file mode 100644 index 00000000..0c69df20 --- /dev/null +++ b/utils/pxstudiomdl/gltf_source.h @@ -0,0 +1,37 @@ +/* +gltf_source.h - glTF reference mesh/animation importer +Copyright (C) 2026 SNMetamorph + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. +*/ + +#pragma once + +#include "model_source.h" +#include +#include +#include + +class CGltfSource : public IStudioSource +{ +public: + explicit CGltfSource( const char *name ); + ~CGltfSource( void ) {} + + void GrabStudio( s_model_t *pmodel ) override; + void GrabAnimation( s_animation_t *panim ) override; + +private: + using CGlTFData = std::unique_ptr; + + CGlTFData m_data; + std::string m_filename; +}; diff --git a/utils/pxstudiomdl/image_utils.cpp b/utils/pxstudiomdl/image_utils.cpp index 5664101d..56636dd0 100644 --- a/utils/pxstudiomdl/image_utils.cpp +++ b/utils/pxstudiomdl/image_utils.cpp @@ -92,6 +92,42 @@ rgbdata_t *ImageUtils::LoadImageMemory( const char *filename, const byte *buf, s return pic; } +/* +================ +ImageUtils::LoadImageMemoryAuto + +detect the image format by its magic bytes; required for embedded +textures that carry no meaningful file extension (e.g. from .glb) +================ +*/ +rgbdata_t *ImageUtils::LoadImageMemoryAuto( const char *filename, const byte *buf, size_t fileSize ) +{ + if( !buf || fileSize < 4 ) + return NULL; + + // PNG + if( fileSize >= 8 && buf[0] == 0x89 && buf[1] == 'P' && buf[2] == 'N' && buf[3] == 'G' ) + return Image_LoadPNG( filename, buf, fileSize ); + + // BMP + if( buf[0] == 'B' && buf[1] == 'M' ) + return Image_LoadBMP( filename, buf, fileSize ); + + // DDS + if( buf[0] == 'D' && buf[1] == 'D' && buf[2] == 'S' && buf[3] == ' ' ) + return Image_LoadDDS( filename, buf, fileSize ); + + // JPEG (not supported by imagelib) + if( buf[0] == 0xFF && buf[1] == 0xD8 && buf[2] == 0xFF ) + { + MsgDev( D_ERROR, "LoadImageMemoryAuto: JPEG textures are not supported (%s)\n", filename ); + return NULL; + } + + // fall back to extension-based detection + return LoadImageMemory( filename, buf, fileSize ); +} + /* ================ Image_ApplyPaletteGamma diff --git a/utils/pxstudiomdl/image_utils.h b/utils/pxstudiomdl/image_utils.h index 73e26dcc..0eaa65b7 100644 --- a/utils/pxstudiomdl/image_utils.h +++ b/utils/pxstudiomdl/image_utils.h @@ -23,6 +23,7 @@ namespace ImageUtils { rgbdata_t *LoadImageFile(const char *filename); rgbdata_t *LoadImageMemory(const char *filename, const byte *buf, size_t fileSize); + rgbdata_t *LoadImageMemoryAuto(const char *filename, const byte *buf, size_t fileSize); void ApplyPaletteGamma(rgbdata_t *pic); }; diff --git a/utils/pxstudiomdl/model_source.cpp b/utils/pxstudiomdl/model_source.cpp new file mode 100644 index 00000000..a95f94f5 --- /dev/null +++ b/utils/pxstudiomdl/model_source.cpp @@ -0,0 +1,134 @@ +/* +model_source.cpp - common interface for reference mesh/animation sources +Copyright (C) 2017 Uncle Mike +Copyright (C) 2026 SNMetamorph + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. +*/ + +#include "port.h" +#include "cmdlib.h" +#include "mathlib.h" +#include "stringlib.h" +#include "file_system.h" +#include "model_source.h" +#include "smd_source.h" +#include "gltf_source.h" + +bool IsEnd( char const *pLine ) +{ + if( !Q_strncmp( "end", pLine, 3 )) + return true; + return ( pLine[3] == '\0' ) || ( pLine[3] == '\n' ); +} + +int LookupTexture( const char *texturename ) +{ + int i; + + for( i = 0; i < g_numtextures; i++ ) + { + if( !Q_stricmp( g_texture[i].name, texturename )) + return i; + } + + Q_strncpy( g_texture[i].name, texturename, sizeof( g_texture[0].name )); + + // XDM: allow such names as "tex_chrome_bright" - chrome and full brightness effects + if( Q_stristr( texturename, "chrome" ) != NULL ) + g_texture[i].flags |= STUDIO_NF_FLATSHADE | STUDIO_NF_CHROME; + if( Q_stristr( texturename, "bright" ) != NULL ) + g_texture[i].flags |= STUDIO_NF_FULLBRIGHT; + g_numtextures++; + + return i; +} + +s_mesh_t *LookupMesh( s_model_t *pmodel, char *texturename ) +{ + int i, j; + + j = LookupTexture( texturename ); + + for( i = 0; i < pmodel->nummesh; i++ ) + { + if( pmodel->pmesh[i]->skinref == j ) + return pmodel->pmesh[i]; + } + + if( i >= MAXSTUDIOMESHES ) + COM_FatalError( "too many meshes in model: \"%s\"\n", pmodel->name ); + + pmodel->nummesh = i + 1; + pmodel->pmesh[i] = (s_mesh_t *)Mem_Alloc( sizeof( s_mesh_t )); + pmodel->pmesh[i]->skinref = j; + + return pmodel->pmesh[i]; +} + +s_trianglevert_t *LookupTriangle( s_mesh_t *pmesh, int index ) +{ + if( index >= MAXSTUDIOTRIANGLES ) + COM_FatalError( "max studio triangles exceeds 65536\n" ); + + if( index >= pmesh->alloctris ) + { + int start = pmesh->alloctris; + pmesh->alloctris = index + 256; + + if( pmesh->triangle ) + { + pmesh->triangle = (s_trianglevert_t (*)[3])Mem_Realloc( pmesh->triangle, pmesh->alloctris * sizeof( *pmesh->triangle )); + memset( &pmesh->triangle[start], 0, ( pmesh->alloctris - start ) * sizeof( *pmesh->triangle )); + } + else + { + pmesh->triangle = (s_trianglevert_t (*)[3])Mem_Alloc( pmesh->alloctris * sizeof( *pmesh->triangle )); + } + } + + return pmesh->triangle[index]; +} + +//----------------------------------------------------------------------------- +// Purpose: build compound boneToPose matrices from the reference skeleton +//----------------------------------------------------------------------------- +void Build_Reference( s_model_t *pmodel ) +{ + for( int i = 0; i < pmodel->numbones; i++ ) + { + matrix3x4 bonematrix = matrix3x4( pmodel->skeleton[i].pos, pmodel->skeleton[i].rot ); + int parent = pmodel->localBone[i].parent; + + if( parent == -1 ) + { + // scale the done pos. + // calc rotational matrices + pmodel->boneToPose[i] = bonematrix; + + } + else + { + // calc compound rotational matrices + pmodel->boneToPose[i] = pmodel->boneToPose[parent].ConcatTransforms( bonematrix ); + } + } +} + +CSourcePtr CreateStudioSource( const char *name ) +{ + const char *ext = COM_FileExtension( name ); + + if( !Q_stricmp( ext, "gltf" ) || !Q_stricmp( ext, "glb" )) + return std::make_unique( name ); + + return std::make_unique(); +} diff --git a/utils/pxstudiomdl/model_source.h b/utils/pxstudiomdl/model_source.h new file mode 100644 index 00000000..259e0fd6 --- /dev/null +++ b/utils/pxstudiomdl/model_source.h @@ -0,0 +1,45 @@ +/* +model_source.h - common interface for reference mesh/animation sources +Copyright (C) 2017 Uncle Mike + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. +*/ + +#pragma once + +#include "studio.h" +#include "studiomdl.h" +#include + +//----------------------------------------------------------------------------- +// Common interface for source model formats (SMD, glTF, ...) +//----------------------------------------------------------------------------- +class IStudioSource +{ +public: + virtual ~IStudioSource( void ) {} + virtual void GrabStudio( s_model_t *pmodel ) = 0; + virtual void GrabAnimation( s_animation_t *panim ) = 0; +}; + +using CSourcePtr = std::unique_ptr; + +// create an importer matching the source file format (detected by extension) +CSourcePtr CreateStudioSource( const char *name ); + +// +// shared helpers, used by importers and QC parsing +// +bool IsEnd( char const *pLine ); +int LookupTexture( const char *texturename ); +s_mesh_t *LookupMesh( s_model_t *pmodel, char *texturename ); +s_trianglevert_t *LookupTriangle( s_mesh_t *pmesh, int index ); +void Build_Reference( s_model_t *pmodel ); diff --git a/utils/pxstudiomdl/skin.cpp b/utils/pxstudiomdl/skin.cpp index 30b81af7..e41e5105 100644 --- a/utils/pxstudiomdl/skin.cpp +++ b/utils/pxstudiomdl/skin.cpp @@ -40,6 +40,12 @@ void Grab_Skin( s_texture_t *ptexture ) { pic = ImageUtils::LoadImageMemory( ptexture->name, white_bmp, sizeof( white_bmp )); } + else if( ptexture->pembedded ) + { + // texture data is embedded in the source file (glTF/GLB bufferView) + pic = ImageUtils::LoadImageMemoryAuto( ptexture->name, ptexture->pembedded, ptexture->embeddsize ); + if( !pic ) MsgDev( D_ERROR, "unable to decode embedded texture %s\n", ptexture->name ); + } else { if( cdtextureset ) diff --git a/utils/pxstudiomdl/smd_source.cpp b/utils/pxstudiomdl/smd_source.cpp new file mode 100644 index 00000000..7a40122e --- /dev/null +++ b/utils/pxstudiomdl/smd_source.cpp @@ -0,0 +1,619 @@ +/* +smd_source.cpp - SMD reference mesh/animation importer +Copyright (C) 2017 Uncle Mike +Copyright (C) 2026 SNMetamorph + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. +*/ + +#include "port.h" +#include "cmdlib.h" +#include "mathlib.h" +#include "stringlib.h" +#include "file_system.h" +#include "smd_source.h" +#include + +//----------------------------------------------------------------------------- +// Purpose: collapse duplicate bone weights and keep only the strongest ones +//----------------------------------------------------------------------------- +static int SortAndBalanceBones( int iCount, int iMaxCount, int bones[], float weights[] ) +{ + int i, bShouldSort; + float w, t; + + // collapse duplicate bone weights + for( i = 0; i < iCount-1; i++ ) + { + for( int j = i + 1; j < iCount; j++ ) + { + if( bones[i] == bones[j] ) + { + weights[i] += weights[j]; + weights[j] = 0.0; + } + } + } + + // do sleazy bubble sort + do { + bShouldSort = false; + for( i = 0; i < iCount-1; i++ ) + { + if( weights[i+1] > weights[i] ) + { + int j = bones[i+1]; + bones[i+1] = bones[i]; + bones[i] = j; + w = weights[i+1]; + weights[i+1] = weights[i]; + weights[i] = w; + bShouldSort = true; + } + } + } while( bShouldSort ); + + // throw away all weights less than 1/20th + while( iCount > 1 && weights[iCount-1] < 0.005 ) + iCount--; + + // clip to the top iMaxCount bones + if( iCount > iMaxCount ) + iCount = iMaxCount; + + t = 0.0f; + + for( i = 0; i < iCount; i++ ) + t += weights[i]; + + if( t <= 0.0f ) + { + // missing weights?, go ahead and evenly share? + // FIXME: shouldn't this error out? + t = 1.0 / iCount; + + for( i = 0; i < iCount; i++ ) + weights[i] = t; + } + else + { + // scale to sum to 1.0 + t = 1.0 / t; + + for( i = 0; i < iCount; i++ ) + weights[i] = weights[i] * t; + } + + return iCount; +} + +CSmdSource::CSmdSource( void ) + : m_input( nullptr, &fclose ) + , m_linecount( 0 ) +{ + m_line[0] = '\0'; +} + +bool CSmdSource::GetLine( void ) +{ + while( fgets( m_line.data(), m_line.size(), m_input.get()) != NULL ) + { + m_linecount++; + + // skip comments + if( m_line[0] == '/' && m_line[1] == '/' ) + continue; + return true; + } + + return false; +} + +int CSmdSource::Grab_Nodes( s_node_t *pnodes ) +{ + int index, parent; + int numbones = 0; + char name[1024]; + + for( index = 0; index < MAXSTUDIOSRCBONES; index++ ) + pnodes[index].parent = -1; + + while( GetLine( )) + { + if( sscanf( m_line.data(), "%d \"%[^\"]\" %d", &index, name, &parent ) == 3 ) + { + Q_strncpy( pnodes[index].name, name, sizeof( pnodes[0].name )); + pnodes[index].parent = parent; + numbones = Q_max( numbones, index ); + } + else + { + return numbones + 1; + } + } + + COM_FatalError( "Unexpected EOF at line %d\n", m_linecount ); + + return 0; +} + +void CSmdSource::Grab_Skeleton( s_model_t *pmodel ) +{ + Vector pos; + Radian rot; + char cmd[1024]; + int index; + + while( GetLine( )) + { + if( sscanf( m_line.data(), "%d %f %f %f %f %f %f", &index, &pos.x, &pos.y, &pos.z, &rot.x, &rot.y, &rot.z ) == 7 ) + { + pos *= pmodel->scale; + pmodel->skeleton[index].pos = pos; + pmodel->skeleton[index].rot = rot; + } + else if( sscanf( m_line.data(), "%s %d", cmd, &index ) == 2 ) + { + if( !Q_strcmp( cmd, "time" )) + { + // begin building skeleton + } + else if( !Q_strcmp( cmd, "end" )) + { + Build_Reference( pmodel ); + return; + } + } + } +} + +void CSmdSource::Grab_Triangles( s_model_t *pmodel ) +{ + int i, j, k; + int ncount = 0; + float vmin = 9999.0f; + + // load the base triangles + while( 1 ) + { + if( !GetLine( )) + break; + + // check for end + if( IsEnd( m_line.data())) + break; + + char texturename[64]; + s_mesh_t *pmesh; + s_trianglevert_t *ptriv; + int bone; + Vector vert[3]; + Vector norm[3]; + + Q_strncpy( texturename, m_line.data(), sizeof( texturename )); + + // strip off trailing smag + for( i = Q_strlen( texturename ) - 1; i >= 0 && !isgraph( texturename[i] ); i-- ); + texturename[i + 1] = '\0'; + + // funky texture overrides + for( i = 0; i < numrep; i++ ) + { + if( sourcetexture[i][0] == '\0' ) + { + Q_strncpy( texturename, defaulttexture[i], sizeof( texturename )); + break; + } + + if( !Q_stricmp( texturename, sourcetexture[i] )) + { + Q_strncpy( texturename, defaulttexture[i], sizeof( texturename )); + break; + } + } + + if( texturename[0] == '\0' ) + { + // weird model problem, skip them + GetLine(); + GetLine(); + GetLine(); + continue; + } + + if( Q_stristr( texturename, "null.bmp" ) || Q_stristr( texturename, "null.tga" )) + { + // skip all faces with the null texture on them. + GetLine(); + GetLine(); + GetLine(); + continue; + } + + COM_DefaultExtension( texturename, ".tga" ); // Crowbar decompiler issues + pmesh = LookupMesh( pmodel, texturename ); + + for( j = 0; j < 3; j++ ) + { + if( pmodel->flip_triangles ) + ptriv = LookupTriangle( pmesh, pmesh->numtris ) + 2 - j; // quake wants them in the reverse order + else ptriv = LookupTriangle( pmesh, pmesh->numtris ) + j; + + if( !GetLine( )) + { + COM_FatalError( "%s: error on line %d: %s", m_filename.c_str(), m_linecount, m_line.data()); + } + + pmodel->srcvert.AddToTail(); + s_srcvertex_t *srcv = &pmodel->srcvert[pmodel->srcvert.Count() - 1]; + int iCount = 0, bones[MAXSTUDIOSRCBONES]; + float weights[MAXSTUDIOSRCBONES]; + s_boneweight_t boneWeight; + + // clean memory before use to avoid bug + memset(srcv, 0, sizeof(*srcv)); + + // get support for Source bone weights description + i = sscanf( m_line.data(), "%d %f %f %f %f %f %f %f %f %d %d %f %d %f %d %f %d %f", + &bone, + &srcv->vert[0], &srcv->vert[1], &srcv->vert[2], + &srcv->norm[0], &srcv->norm[1], &srcv->norm[2], + &ptriv->u, &ptriv->v, + &iCount, + &bones[0], &weights[0], &bones[1], &weights[1], &bones[2], &weights[2], &bones[3], &weights[3] ); + + if( i < 9 ) continue; + + if( bone < 0 || bone >= pmodel->numbones ) + { + COM_FatalError( "bogus bone index\n%d %s :\n%s", m_linecount, m_filename.c_str(), m_line.data()); + } + + // continue parsing more bones. + if( iCount > MAXSTUDIOBONEWEIGHTS ) + { + char *token; + int ctr = 0; + + for( k = 0; k < 18; k++ ) + { + while( m_line[ctr] == ' ' ) + { + ctr++; + } + + token = strtok( &m_line[ctr], " " ); + ctr += Q_strlen( token ) + 1; + } + + for( k = 4; k < iCount && k < MAXSTUDIOSRCBONES; k++ ) + { + while( m_line[ctr] == ' ' ) + { + ctr++; + } + + token = strtok( &m_line[ctr], " " ); + ctr += Q_strlen( token ) + 1; + + bones[k] = verify_atoi( token ); + + token = strtok( &m_line[ctr], " " ); + ctr += strlen( token ) + 1; + + weights[k] = verify_atof( token ); + } + } + + vmin = Q_min( srcv->vert.z, vmin ); + srcv->skinref = pmesh->skinref; + srcv->vert *= pmodel->scale; + + vert[j] = srcv->vert; + norm[j] = srcv->norm; + + // initialize boneweigts + for( k = 0; k < MAXSTUDIOBONEWEIGHTS; k++ ) + { + boneWeight.weight[k] = 0.0f; + boneWeight.bone[k] = -1; + } + + if( i == 9 || iCount == 0 ) + { + boneWeight.weight[0] = 1.0f; + boneWeight.bone[0] = bone; + boneWeight.numbones = 1; + } + else + { + iCount = SortAndBalanceBones( iCount, MAXSTUDIOBONEWEIGHTS, bones, weights ); + + if( allow_boneweights ) + { + for( k = 0; k < iCount; k++ ) + { + boneWeight.bone[k] = bound( 0, bones[k], MAXSTUDIOBONES - 1 ); + boneWeight.weight[k] = weights[k]; + } + + boneWeight.numbones = iCount; + has_boneweights = true; + } + else + { + boneWeight.bone[0] = bones[0]; + boneWeight.weight[0] = 1.0f; + boneWeight.numbones = 1; + } + } + + srcv->localWeight = boneWeight; + ptriv->vertindex = ptriv->normindex = pmodel->srcvert.Count() - 1; + + // tag bone as being used + // pmodel->bone[bone].ref = 1; + } + + if( tag_reversed || tag_normals ) + { + // check triangle direction + if( DotProduct( norm[0], norm[1] ) < 0 || DotProduct( norm[1], norm[2] ) < 0 || DotProduct( norm[2], norm[0] ) < 0 ) + { + ncount++; + + if( tag_normals ) + { + // steal the triangle and make it white + s_trianglevert_t *ptriv2; + + pmesh = LookupMesh( pmodel, "#white.bmp" ); + ptriv2 = LookupTriangle( pmesh, pmesh->numtris ); + + ptriv2[0] = ptriv[0]; + ptriv2[1] = ptriv[1]; + ptriv2[2] = ptriv[2]; + } + } + else + { + Vector a1, a2, sn; + float x, y, z; + + a1 = vert[1] - vert[0]; + a2 = vert[2] - vert[0]; + sn = CrossProduct( a1, a2 ).Normalize(); + + x = DotProduct( sn, norm[0] ); + y = DotProduct( sn, norm[1] ); + z = DotProduct( sn, norm[2] ); + + if( x < 0.0 || y < 0.0 || z < 0.0 ) + { + if( tag_reversed ) + { + // steal the triangle and make it white + s_trianglevert_t *ptriv2; + + MsgDev( D_INFO, "triangle reversed (%f %f %f)\n", + DotProduct( norm[0], norm[1] ), + DotProduct( norm[1], norm[2] ), + DotProduct( norm[2], norm[0] )); + + pmesh = LookupMesh( pmodel, "#white.bmp" ); + ptriv2 = LookupTriangle( pmesh, pmesh->numtris ); + + ptriv2[0] = ptriv[0]; + ptriv2[1] = ptriv[1]; + ptriv2[2] = ptriv[2]; + } + } + } + } + + pmesh->numtris++; + } + + if( ncount ) MsgDev( D_WARN, "%d triangles with misdirected normals\n", ncount ); + if( vmin != 0.0 ) MsgDev( D_REPORT, "lowest vector at %f\n", vmin ); +} + +bool CSmdSource::Grab_AnimFrames( s_animation_t *panim ) +{ + Vector pos; + Radian rot; + char cmd[1024]; + int index, size; + int t = -99999999; + + size = panim->numbones * sizeof( s_bone_t ); + panim->source.startframe = -1; + panim->source.endframe = 0; + + while( GetLine( )) + { + if( sscanf( m_line.data(), "%d %f %f %f %f %f %f", &index, &pos[0], &pos[1], &pos[2], &rot[0], &rot[1], &rot[2] ) == 7 ) + { + if( panim->source.startframe < 0 ) + COM_FatalError( "missing frame start(%d) : %s\n", m_linecount, m_line.data()); + + panim->rawanim[t][index].pos = pos; + panim->rawanim[t][index].rot = rot; + continue; + } + + if( sscanf( m_line.data(), "%1023s %d", cmd, &index ) < 1 ) + { + COM_FatalError( "(%d) : %s", m_linecount, m_line.data()); + continue; + } + + if( !Q_stricmp( cmd, "time" )) + { + t = index; + + if( panim->source.startframe == -1 ) + panim->source.startframe = t; + + if( t < panim->source.startframe ) + COM_FatalError( "frame error(%d) : %s\n", m_linecount, m_line.data()); + + if( t > panim->source.endframe ) + panim->source.endframe = t; + + t -= panim->source.startframe; + + if( t > MAXSTUDIOANIMFRAMES ) + { + MsgDev( D_ERROR, "animation %s has too many frames. Cutted at %d\n", panim->name, MAXSTUDIOANIMFRAMES ); + panim->source.numframes = MAXSTUDIOANIMFRAMES - 1; + panim->source.endframe = MAXSTUDIOANIMFRAMES - 1; + return false; + } + + if( panim->rawanim[t] != NULL ) + continue; + + panim->rawanim[t] = (s_bone_t *)Mem_Alloc( size ); + + // duplicate previous frames keys + if( t > 0 && panim->rawanim[t-1] ) + { + for( int j = 0; j < panim->numbones; j++ ) + { + panim->rawanim[t][j].pos = panim->rawanim[t-1][j].pos; + panim->rawanim[t][j].rot = panim->rawanim[t-1][j].rot; + } + } + continue; + } + + if( !Q_stricmp( cmd, "end" )) + { + panim->source.numframes = panim->source.endframe - panim->source.startframe + 1; + + for( t = 0; t < panim->source.numframes; t++ ) + { + if( panim->rawanim[t] == NULL ) + COM_FatalError( "%s is missing frame %d\n", panim->name, t + panim->source.startframe ); + } + return true; + } + + COM_FatalError( "(%d) : %s", m_linecount, m_line.data()); + } + + COM_FatalError( "unexpected EOF: %s\n", panim->name ); + + return true; +} + +void CSmdSource::GrabStudio( s_model_t *pmodel ) +{ + char cmd[1024]; + int option; + char path[1024]; + + Q_snprintf( path, sizeof( path ), "%s/%s", cddir[numdirs], pmodel->name ); + COM_DefaultExtension( path, ".smd" ); + m_filename = path; + + if( !COM_FileExists( path )) + COM_FatalError( "%s doesn't exist\n", path ); + + m_input.reset( fopen( path, "r" )); + if( !m_input ) + COM_FatalError( "%s couldn't be open\n", path ); + + MsgDev( D_INFO, "grabbing: %s.smd\t\t[^1mesh^7]\n", pmodel->name ); + m_linecount = 0; + + while( GetLine( )) + { + int numRead = sscanf( m_line.data(), "%s %d", cmd, &option ); + + // blank line + if(( numRead == EOF ) || ( numRead == 0 )) + continue; + + if( !Q_strcmp( cmd, "version" )) + { + if( option != 1 ) + COM_FatalError( "%s version %i should be 1\n", path, option ); + } + else if( !Q_strcmp( cmd, "nodes" )) + { + pmodel->numbones = Grab_Nodes( pmodel->localBone ); + } + else if( !Q_strcmp( cmd, "skeleton" )) + { + Grab_Skeleton( pmodel ); + } + else if( !Q_strcmp( cmd, "triangles" )) + { + Grab_Triangles( pmodel ); + } + else + { + MsgDev( D_WARN, "unknown studio command\n" ); + } + } +} + +void CSmdSource::GrabAnimation( s_animation_t *panim ) +{ + char cmd[1024]; + int option; + char path[1024]; + + Q_snprintf( path, sizeof( path ), "%s/%s", cddir[numdirs], panim->filename ); + COM_DefaultExtension( path, ".smd" ); + m_filename = path; + + if( !COM_FileExists( path )) + COM_FatalError ("%s doesn't exist\n", path ); + + m_input.reset( fopen( path, "r" )); + if( !m_input ) + COM_FatalError( "%s couldn't be open\n", path ); + m_linecount = 0; + + while( GetLine( )) + { + sscanf( m_line.data(), "%s %d", cmd, &option ); + if( !Q_strcmp( cmd, "version" )) + { + if( option != 1 ) + COM_FatalError( "%s version %i should be 1\n", path, option ); + } + else if( !Q_strcmp( cmd, "nodes" )) + { + panim->numbones = Grab_Nodes( panim->localBone ); + } + else if( !Q_strcmp( cmd, "skeleton" )) + { + if( !Grab_AnimFrames( panim )) + break; // animation was cutted + } + else + { + // some artists use mesh reference as default animation + if( Q_strcmp( cmd, "triangles" )) + MsgDev( D_WARN, "unknown studio command\n" ); + + while( GetLine( )) + { + if( IsEnd( m_line.data())) + break; + } + } + } +} diff --git a/utils/pxstudiomdl/smd_source.h b/utils/pxstudiomdl/smd_source.h new file mode 100644 index 00000000..a525e269 --- /dev/null +++ b/utils/pxstudiomdl/smd_source.h @@ -0,0 +1,46 @@ +/* +smd_source.h - SMD reference mesh/animation importer +Copyright (C) 2017 Uncle Mike +Copyright (C) 2026 SNMetamorph + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. +*/ + +#pragma once + +#include "model_source.h" +#include +#include +#include +#include + +class CSmdSource : public IStudioSource +{ +public: + CSmdSource( void ); + ~CSmdSource( void ) {} + + void GrabStudio( s_model_t *pmodel ) override; + void GrabAnimation( s_animation_t *panim ) override; + +private: + bool GetLine( void ); + + int Grab_Nodes( s_node_t *pnodes ); + void Grab_Skeleton( s_model_t *pmodel ); + void Grab_Triangles( s_model_t *pmodel ); + bool Grab_AnimFrames( s_animation_t *panim ); + + std::unique_ptr m_input; + std::array m_line; + int m_linecount; + std::string m_filename; +}; diff --git a/utils/pxstudiomdl/studiomdl.cpp b/utils/pxstudiomdl/studiomdl.cpp index f250b41e..22f22712 100644 --- a/utils/pxstudiomdl/studiomdl.cpp +++ b/utils/pxstudiomdl/studiomdl.cpp @@ -27,6 +27,7 @@ GNU General Public License for more details. #include "crashhandler.h" #include "app_info.h" #include "build_info.h" +#include "model_source.h" CUtlArray< char > g_KeyValueText; char filename[1024]; @@ -131,14 +132,19 @@ void ClearModel( void ) Mem_Free( g_xnodename[i] ); for( i = 0; i < g_numtextures; i++ ) + { Mem_Free( g_texture[i].pdata ); + Mem_Free( g_texture[i].pembedded ); + } memset( g_xnode, 0, sizeof( g_xnode )); memset( g_xnodename, 0, sizeof( g_xnodename )); memset( g_xnodeskip, 0, sizeof( g_xnodeskip )); g_numxnodes = g_numxnodeskips = 0; - memset( g_texture, 0, sizeof( g_texture )); + for( i = 0; i < MAXSTUDIOSKINS; i++ ) + g_texture[i] = s_texture_t(); + g_numtextures = 0; memset( g_sequencegroup, 0, sizeof( g_sequencegroup )); @@ -199,13 +205,6 @@ bool GetLineInput( void ) return false; } -bool IsEnd( char const *pLine ) -{ - if( !Q_strncmp( "end", pLine, 3 )) - return true; - return ( pLine[3] == '\0' ) || ( pLine[3] == '\n' ); -} - int verify_atoi_dbg( const char *token, const int line ) { if( token[0] != '-' && ( token[0] < '0' || token[0] > '9' )) @@ -336,76 +335,6 @@ int findGlobalBoneXSI( const char *name ) return -1; } -int SortAndBalanceBones( int iCount, int iMaxCount, int bones[], float weights[] ) -{ - int i, bShouldSort; - float w, t; - - // collapse duplicate bone weights - for( i = 0; i < iCount-1; i++ ) - { - for( int j = i + 1; j < iCount; j++ ) - { - if( bones[i] == bones[j] ) - { - weights[i] += weights[j]; - weights[j] = 0.0; - } - } - } - - // do sleazy bubble sort - do { - bShouldSort = false; - for( i = 0; i < iCount-1; i++ ) - { - if( weights[i+1] > weights[i] ) - { - int j = bones[i+1]; - bones[i+1] = bones[i]; - bones[i] = j; - w = weights[i+1]; - weights[i+1] = weights[i]; - weights[i] = w; - bShouldSort = true; - } - } - } while( bShouldSort ); - - // throw away all weights less than 1/20th - while( iCount > 1 && weights[iCount-1] < 0.005 ) - iCount--; - - // clip to the top iMaxCount bones - if( iCount > iMaxCount ) - iCount = iMaxCount; - - t = 0.0f; - - for( i = 0; i < iCount; i++ ) - t += weights[i]; - - if( t <= 0.0f ) - { - // missing weights?, go ahead and evenly share? - // FIXME: shouldn't this error out? - t = 1.0 / iCount; - - for( i = 0; i < iCount; i++ ) - weights[i] = t; - } - else - { - // scale to sum to 1.0 - t = 1.0 / t; - - for( i = 0; i < iCount; i++ ) - weights[i] = weights[i] * t; - } - - return iCount; -} - int LookupControl( const char *string ) { if( !Q_stricmp( string, "X" )) return STUDIO_X; @@ -492,74 +421,6 @@ int LookupAttachment( const char *name ) return -1; } -int LookupTexture( const char *texturename ) -{ - int i; - - for( i = 0; i < g_numtextures; i++ ) - { - if( !Q_stricmp( g_texture[i].name, texturename )) - return i; - } - - Q_strncpy( g_texture[i].name, texturename, sizeof( g_texture[0].name )); - - // XDM: allow such names as "tex_chrome_bright" - chrome and full brightness effects - if( Q_stristr( texturename, "chrome" ) != NULL ) - g_texture[i].flags |= STUDIO_NF_FLATSHADE | STUDIO_NF_CHROME; - if( Q_stristr( texturename, "bright" ) != NULL ) - g_texture[i].flags |= STUDIO_NF_FULLBRIGHT; - g_numtextures++; - - return i; -} - -s_mesh_t *LookupMesh( s_model_t *pmodel, char *texturename ) -{ - int i, j; - - j = LookupTexture( texturename ); - - for( i = 0; i < pmodel->nummesh; i++ ) - { - if( pmodel->pmesh[i]->skinref == j ) - return pmodel->pmesh[i]; - } - - if( i >= MAXSTUDIOMESHES ) - COM_FatalError( "too many meshes in model: \"%s\"\n", pmodel->name ); - - pmodel->nummesh = i + 1; - pmodel->pmesh[i] = (s_mesh_t *)Mem_Alloc( sizeof( s_mesh_t )); - pmodel->pmesh[i]->skinref = j; - - return pmodel->pmesh[i]; -} - -s_trianglevert_t *LookupTriangle( s_mesh_t *pmesh, int index ) -{ - if( index >= MAXSTUDIOTRIANGLES ) - COM_FatalError( "max studio triangles exceeds 65536\n" ); - - if( index >= pmesh->alloctris ) - { - int start = pmesh->alloctris; - pmesh->alloctris = index + 256; - - if( pmesh->triangle ) - { - pmesh->triangle = (s_trianglevert_t (*)[3])Mem_Realloc( pmesh->triangle, pmesh->alloctris * sizeof( *pmesh->triangle )); - memset( &pmesh->triangle[start], 0, ( pmesh->alloctris - start ) * sizeof( *pmesh->triangle )); - } - else - { - pmesh->triangle = (s_trianglevert_t (*)[3])Mem_Alloc( pmesh->alloctris * sizeof( *pmesh->triangle )); - } - } - - return pmesh->triangle[index]; -} - int LookupNormal( s_model_t *pmodel, s_srcvertex_t *srcv ) { for (int i = 0; i < pmodel->norm.Count(); i++) @@ -666,525 +527,22 @@ void scale_animation( s_animation_t *panim ) } } -void Build_Reference( s_model_t *pmodel ) -{ - for( int i = 0; i < pmodel->numbones; i++ ) - { - matrix3x4 bonematrix = matrix3x4( pmodel->skeleton[i].pos, pmodel->skeleton[i].rot ); - int parent = pmodel->localBone[i].parent; - - if( parent == -1 ) - { - // scale the done pos. - // calc rotational matrices - pmodel->boneToPose[i] = bonematrix; - - } - else - { - // calc compound rotational matrices - pmodel->boneToPose[i] = pmodel->boneToPose[parent].ConcatTransforms( bonematrix ); - } - } -} - -void Grab_Triangles( s_model_t *pmodel ) -{ - int i, j, k; - int ncount = 0; - float vmin = 9999.0f; - - // load the base triangles - while( 1 ) - { - if( !GetLineInput( )) - break; - - // check for end - if( IsEnd( line )) - break; - - char texturename[64]; - s_mesh_t *pmesh; - s_trianglevert_t *ptriv; - int bone; - Vector vert[3]; - Vector norm[3]; - - Q_strncpy( texturename, line, sizeof( texturename )); - - // strip off trailing smag - for( i = Q_strlen( texturename ) - 1; i >= 0 && !isgraph( texturename[i] ); i-- ); - texturename[i + 1] = '\0'; - - // funky texture overrides - for( i = 0; i < numrep; i++ ) - { - if( sourcetexture[i][0] == '\0' ) - { - Q_strncpy( texturename, defaulttexture[i], sizeof( texturename )); - break; - } - - if( !Q_stricmp( texturename, sourcetexture[i] )) - { - Q_strncpy( texturename, defaulttexture[i], sizeof( texturename )); - break; - } - } - - if( texturename[0] == '\0' ) - { - // weird model problem, skip them - GetLineInput(); - GetLineInput(); - GetLineInput(); - continue; - } - - if( Q_stristr( texturename, "null.bmp" ) || Q_stristr( texturename, "null.tga" )) - { - // skip all faces with the null texture on them. - GetLineInput(); - GetLineInput(); - GetLineInput(); - continue; - } - - COM_DefaultExtension( texturename, ".tga" ); // Crowbar decompiler issues - pmesh = LookupMesh( pmodel, texturename ); - - for( j = 0; j < 3; j++ ) - { - if( pmodel->flip_triangles ) - ptriv = LookupTriangle( pmesh, pmesh->numtris ) + 2 - j; // quake wants them in the reverse order - else ptriv = LookupTriangle( pmesh, pmesh->numtris ) + j; - - if( !GetLineInput( )) - { - COM_FatalError( "%s: error on line %d: %s", filename, linecount, line ); - } - - pmodel->srcvert.AddToTail(); - s_srcvertex_t *srcv = &pmodel->srcvert[pmodel->srcvert.Count() - 1]; - int iCount = 0, bones[MAXSTUDIOSRCBONES]; - float weights[MAXSTUDIOSRCBONES]; - s_boneweight_t boneWeight; - - // clean memory before use to avoid bug - memset(srcv, 0, sizeof(*srcv)); - - // get support for Source bone weights description - i = sscanf( line, "%d %f %f %f %f %f %f %f %f %d %d %f %d %f %d %f %d %f", - &bone, - &srcv->vert[0], &srcv->vert[1], &srcv->vert[2], - &srcv->norm[0], &srcv->norm[1], &srcv->norm[2], - &ptriv->u, &ptriv->v, - &iCount, - &bones[0], &weights[0], &bones[1], &weights[1], &bones[2], &weights[2], &bones[3], &weights[3] ); - - if( i < 9 ) continue; - - if( bone < 0 || bone >= pmodel->numbones ) - { - COM_FatalError( "bogus bone index\n%d %s :\n%s", linecount, filename, line ); - } - - // continue parsing more bones. - if( iCount > MAXSTUDIOBONEWEIGHTS ) - { - char *token; - int ctr = 0; - - for( k = 0; k < 18; k++ ) - { - while( line[ctr] == ' ' ) - { - ctr++; - } - - token = strtok( &line[ctr], " " ); - ctr += Q_strlen( token ) + 1; - } - - for( k = 4; k < iCount && k < MAXSTUDIOSRCBONES; k++ ) - { - while( line[ctr] == ' ' ) - { - ctr++; - } - - token = strtok( &line[ctr], " " ); - ctr += Q_strlen( token ) + 1; - - bones[k] = verify_atoi( token ); - - token = strtok( &line[ctr], " " ); - ctr += strlen( token ) + 1; - - weights[k] = verify_atof( token ); - } - } - - vmin = Q_min( srcv->vert.z, vmin ); - srcv->skinref = pmesh->skinref; - srcv->vert *= pmodel->scale; - - vert[j] = srcv->vert; - norm[j] = srcv->norm; - - // initialize boneweigts - for( k = 0; k < MAXSTUDIOBONEWEIGHTS; k++ ) - { - boneWeight.weight[k] = 0.0f; - boneWeight.bone[k] = -1; - } - - if( i == 9 || iCount == 0 ) - { - boneWeight.weight[0] = 1.0f; - boneWeight.bone[0] = bone; - boneWeight.numbones = 1; - } - else - { - iCount = SortAndBalanceBones( iCount, MAXSTUDIOBONEWEIGHTS, bones, weights ); - - if( allow_boneweights ) - { - for( k = 0; k < iCount; k++ ) - { - boneWeight.bone[k] = bound( 0, bones[k], MAXSTUDIOBONES - 1 ); - boneWeight.weight[k] = weights[k]; - } - - boneWeight.numbones = iCount; - has_boneweights = true; - } - else - { - boneWeight.bone[0] = bones[0]; - boneWeight.weight[0] = 1.0f; - boneWeight.numbones = 1; - } - } - - srcv->localWeight = boneWeight; - ptriv->vertindex = ptriv->normindex = pmodel->srcvert.Count() - 1; - - // tag bone as being used - // pmodel->bone[bone].ref = 1; - } - - if( tag_reversed || tag_normals ) - { - // check triangle direction - if( DotProduct( norm[0], norm[1] ) < 0 || DotProduct( norm[1], norm[2] ) < 0 || DotProduct( norm[2], norm[0] ) < 0 ) - { - ncount++; - - if( tag_normals ) - { - // steal the triangle and make it white - s_trianglevert_t *ptriv2; - - pmesh = LookupMesh( pmodel, "#white.bmp" ); - ptriv2 = LookupTriangle( pmesh, pmesh->numtris ); - - ptriv2[0] = ptriv[0]; - ptriv2[1] = ptriv[1]; - ptriv2[2] = ptriv[2]; - } - } - else - { - Vector a1, a2, sn; - float x, y, z; - - a1 = vert[1] - vert[0]; - a2 = vert[2] - vert[0]; - sn = CrossProduct( a1, a2 ).Normalize(); - - x = DotProduct( sn, norm[0] ); - y = DotProduct( sn, norm[1] ); - z = DotProduct( sn, norm[2] ); - - if( x < 0.0 || y < 0.0 || z < 0.0 ) - { - if( tag_reversed ) - { - // steal the triangle and make it white - s_trianglevert_t *ptriv2; - - MsgDev( D_INFO, "triangle reversed (%f %f %f)\n", - DotProduct( norm[0], norm[1] ), - DotProduct( norm[1], norm[2] ), - DotProduct( norm[2], norm[0] )); - - pmesh = LookupMesh( pmodel, "#white.bmp" ); - ptriv2 = LookupTriangle( pmesh, pmesh->numtris ); - - ptriv2[0] = ptriv[0]; - ptriv2[1] = ptriv[1]; - ptriv2[2] = ptriv[2]; - } - } - } - } - - pmesh->numtris++; - } - - if( ncount ) MsgDev( D_WARN, "%d triangles with misdirected normals\n", ncount ); - if( vmin != 0.0 ) MsgDev( D_REPORT, "lowest vector at %f\n", vmin ); -} - -bool Grab_AnimFrames( s_animation_t *panim ) -{ - Vector pos; - Radian rot; - char cmd[1024]; - int index, size; - int t = -99999999; - - size = panim->numbones * sizeof( s_bone_t ); - panim->source.startframe = -1; - panim->source.endframe = 0; - - while( GetLineInput( )) - { - if( sscanf( line, "%d %f %f %f %f %f %f", &index, &pos[0], &pos[1], &pos[2], &rot[0], &rot[1], &rot[2] ) == 7 ) - { - if( panim->source.startframe < 0 ) - COM_FatalError( "missing frame start(%d) : %s\n", linecount, line ); - - panim->rawanim[t][index].pos = pos; - panim->rawanim[t][index].rot = rot; - continue; - } - - if( sscanf( line, "%1023s %d", cmd, &index ) == 0 ) - { - COM_FatalError( "(%d) : %s", linecount, line ); - continue; - } - - if( !Q_stricmp( cmd, "time" )) - { - t = index; - - if( panim->source.startframe == -1 ) - panim->source.startframe = t; - - if( t < panim->source.startframe ) - COM_FatalError( "frame error(%d) : %s\n", linecount, line ); - - if( t > panim->source.endframe ) - panim->source.endframe = t; - - t -= panim->source.startframe; - - if( t > MAXSTUDIOANIMFRAMES ) - { - MsgDev( D_ERROR, "animation %s has too many frames. Cutted at %d\n", panim->name, MAXSTUDIOANIMFRAMES ); - panim->source.numframes = MAXSTUDIOANIMFRAMES - 1; - panim->source.endframe = MAXSTUDIOANIMFRAMES - 1; - return false; - } - - if( panim->rawanim[t] != NULL ) - continue; - - panim->rawanim[t] = (s_bone_t *)Mem_Alloc( size ); - - // duplicate previous frames keys - if( t > 0 && panim->rawanim[t-1] ) - { - for( int j = 0; j < panim->numbones; j++ ) - { - panim->rawanim[t][j].pos = panim->rawanim[t-1][j].pos; - panim->rawanim[t][j].rot = panim->rawanim[t-1][j].rot; - } - } - continue; - } - - if( !Q_stricmp( cmd, "end" )) - { - panim->source.numframes = panim->source.endframe - panim->source.startframe + 1; - - for( t = 0; t < panim->source.numframes; t++ ) - { - if( panim->rawanim[t] == NULL ) - COM_FatalError( "%s is missing frame %d\n", panim->name, t + panim->source.startframe ); - } - return true; - } - - COM_FatalError( "(%d) : %s", linecount, line ); - } - - COM_FatalError( "unexpected EOF: %s\n", panim->name ); - - return true; -} - -void Grab_Skeleton( s_model_t *pmodel ) -{ - Vector pos; - Radian rot; - char cmd[1024]; - int index; - - while( GetLineInput( )) - { - if( sscanf( line, "%d %f %f %f %f %f %f", &index, &pos.x, &pos.y, &pos.z, &rot.x, &rot.y, &rot.z ) == 7 ) - { - pos *= pmodel->scale; - pmodel->skeleton[index].pos = pos; - pmodel->skeleton[index].rot = rot; - } - else if( sscanf( line, "%s %d", cmd, &index )) - { - if( !Q_strcmp( cmd, "time" )) - { - // begin building skeleton - } - else if( !Q_strcmp( cmd, "end" )) - { - Build_Reference( pmodel ); - return; - } - } - } -} - -int Grab_Nodes( s_node_t *pnodes ) -{ - int index, parent; - int numbones = 0; - char name[1024]; - - for( index = 0; index < MAXSTUDIOSRCBONES; index++ ) - pnodes[index].parent = -1; - - while( GetLineInput( )) - { - if( sscanf( line, "%d \"%[^\"]\" %d", &index, name, &parent ) == 3 ) - { - Q_strncpy( pnodes[index].name, name, sizeof( pnodes[0].name )); - pnodes[index].parent = parent; - numbones = Q_max( numbones, index ); - } - else - { - return numbones + 1; - } - } - - COM_FatalError( "Unexpected EOF at line %d\n", linecount ); - - return 0; -} - +//----------------------------------------------------------------------------- +// Purpose: import reference mesh through format-agnostic source interface +//----------------------------------------------------------------------------- void Grab_Studio( s_model_t *pmodel ) { - char cmd[1024]; - int option; - - Q_snprintf( filename, sizeof( filename ), "%s/%s", cddir[numdirs], pmodel->name ); - COM_DefaultExtension( filename, ".smd" ); - - if( !COM_FileExists( filename )) - COM_FatalError( "%s doesn't exist\n", filename ); - - if(( input = fopen( filename, "r" )) == 0 ) - COM_FatalError( "%s couldn't be open\n", filename ); - - MsgDev( D_INFO, "grabbing: %s.smd\t\t[^1mesh^7]\n", pmodel->name ); - linecount = 0; - - while( GetLineInput( )) - { - int numRead = sscanf( line, "%s %d", cmd, &option ); - - // blank line - if(( numRead == EOF ) || ( numRead == 0 )) - continue; - - if( !Q_strcmp( cmd, "version" )) - { - if( option != 1 ) - COM_FatalError( "%s version %i should be 1\n", filename, option ); - } - else if( !Q_strcmp( cmd, "nodes" )) - { - pmodel->numbones = Grab_Nodes( pmodel->localBone ); - } - else if( !Q_strcmp( cmd, "skeleton" )) - { - Grab_Skeleton( pmodel ); - } - else if( !Q_strcmp( cmd, "triangles" )) - { - Grab_Triangles( pmodel ); - } - else - { - MsgDev( D_WARN, "unknown studio command\n" ); - } - } - - fclose( input ); + CSourcePtr source = CreateStudioSource( pmodel->name ); + source->GrabStudio( pmodel ); } +//----------------------------------------------------------------------------- +// Purpose: import animation through format-agnostic source interface +//----------------------------------------------------------------------------- void Grab_Animation( const char *name, s_animation_t *panim ) { - char cmd[1024]; - int option; - - Q_snprintf( filename, sizeof( filename ), "%s/%s", cddir[numdirs], name ); - COM_DefaultExtension( filename, ".smd" ); - - if( !COM_FileExists( filename )) - COM_FatalError ("%s doesn't exist\n", filename); - - if(( input = fopen( filename, "r" )) == 0 ) - COM_FatalError( "%s couldn't be open\n", filename ); - linecount = 0; - - while( GetLineInput( )) - { - sscanf( line, "%s %d", cmd, &option ); - if( !Q_strcmp( cmd, "version" )) - { - if( option != 1 ) - COM_FatalError( "%s version %i should be 1\n", filename, option ); - } - else if( !Q_strcmp( cmd, "nodes" )) - { - panim->numbones = Grab_Nodes( panim->localBone ); - } - else if( !Q_strcmp( cmd, "skeleton" )) - { - if( !Grab_AnimFrames( panim )) - break; // animation was cutted - } - else - { - // some artists use mesh reference as default animation - if( Q_strcmp( cmd, "triangles" )) - MsgDev( D_WARN, "unknown studio command\n" ); - - while( GetLineInput( )) - { - if( IsEnd( line )) - break; - } - } - } - - fclose( input ); + CSourcePtr source = CreateStudioSource( name ); + source->GrabAnimation( panim ); } void Cmd_Eyeposition( void ) @@ -5487,7 +4845,7 @@ static void WaitForKey() Sys_WaitForKeyInput(); } -static vfile_t *GenerateModelScript(const char *modelName) +static vfile_t *GenerateModelScript(const char *modelName, const char *extension) { vfile_t *file = VFS_Create( NULL, 0 ); if (file) @@ -5502,8 +4860,8 @@ static vfile_t *GenerateModelScript(const char *modelName) VFS_Printf( file, "\n$freecoords\n" ); VFS_Printf( file, "\n$origin 0 0 0\n\n" ); VFS_Printf( file, "\n//reference mesh(es)" ); - VFS_Printf( file, "\n$body \"body\" \"%s.smd\"", modelName ); - VFS_Printf( file, "\n$sequence \"%s\" \"%s\" fps 30 loop", modelName, modelName ); + VFS_Printf( file, "\n$body \"body\" \"%s.%s\"", modelName, extension ); + VFS_Printf( file, "\n$sequence \"%s\" \"%s.%s\" fps 30 loop", modelName, modelName, extension ); VFS_Printf( file, "\n\n" ); VFS_Printf( file, "// End of QC script.\n" ); } @@ -5622,13 +4980,15 @@ int main( int argc, char **argv ) COM_DefaultExtension( path, ".qc" ); COM_StripExtension( outname ); - if (Q_strcmp(COM_FileExtension(path), "smd") == 0) + const char *extension = COM_FileExtension( path ); + + if( !Q_stricmp( extension, "smd" ) || !Q_stricmp( extension, "gltf" ) || !Q_stricmp( extension, "glb" )) { char modelName[64]; COM_FileBase(path, modelName); - MsgDev(D_INFO, "Single SMD-file mode, autogenerating QC-script\n"); + MsgDev(D_INFO, "Single %s-file mode, autogenerating QC-script\n", extension); - vfile_t *file = GenerateModelScript(modelName); + vfile_t *file = GenerateModelScript(modelName, extension); if (!file) { COM_FatalError("failed to autogenerate QC-script"); } diff --git a/utils/pxstudiomdl/studiomdl.h b/utils/pxstudiomdl/studiomdl.h index 82a28576..960a9aa0 100644 --- a/utils/pxstudiomdl/studiomdl.h +++ b/utils/pxstudiomdl/studiomdl.h @@ -1,3 +1,5 @@ +#pragma once + #include #include "mathlib.h" #include @@ -25,6 +27,10 @@ #define verify_atof( a ) verify_atof_dbg( a, __LINE__ ) #define verify_atoi( a ) verify_atoi_dbg( a, __LINE__ ) +int verify_atoi_dbg( const char *token, const int line ); +float verify_atof_dbg( const char *token, const int line ); +float verify_atof_with_null( const char *token ); + EXTERN char outname[1024]; EXTERN bool cdset; EXTERN char cddir[32][256]; @@ -644,26 +650,28 @@ EXTERN int g_numxnodeskips; // FIXME: what about texture overrides inline with loading models typedef struct { - char name[MAXSRCSTUDIONAME]; - int flags; - int srcwidth; - int srcheight; - struct rgbdata_s *psrc; - float max_s; - float min_s; - float max_t; - float min_t; - int skintop; - int skinleft; - int skinwidth; - int skinheight; - float fskintop; - float fskinleft; - float fskinwidth; - float fskinheight; - void *pdata; - int size; - int parent; + char name[MAXSRCSTUDIONAME] = {}; + int flags = 0; + int srcwidth = 0; + int srcheight = 0; + struct rgbdata_s *psrc = nullptr; + float max_s = 0.0f; + float min_s = 0.0f; + float max_t = 0.0f; + float min_t = 0.0f; + int skintop = 0; + int skinleft = 0; + int skinwidth = 0; + int skinheight = 0; + float fskintop = 0.0f; + float fskinleft = 0.0f; + float fskinwidth = 0.0f; + float fskinheight = 0.0f; + void *pdata = nullptr; + byte *pembedded = nullptr; // raw encoded image embedded in a source file (glTF/GLB bufferView) + int embeddsize = 0; + int size = 0; + int parent = 0; } s_texture_t; EXTERN s_texture_t g_texture[MAXSTUDIOSKINS]; diff --git a/vcpkg.json b/vcpkg.json index 9b881aae..07a48ea4 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -23,6 +23,7 @@ "utils": { "description": "Build utilities", "dependencies": [ + { "name": "cgltf", "version>=": "1.15" }, { "name": "miniz", "version>=": "3.0.2" }, { "name": "rapidjson", "version>=": "2025-02-26#0" } ]