How does Angelica 3D handle character customizations?

Newbie Spellweaver
Joined
Mar 20, 2021
Messages
50
Reaction score
8
So I've been digging into the 3D model files a lot, especially for player characters. I can read the .bon and .ski and use all the bone matrices and translation/rotation tracks from the .stck files to generate 3D models of in any frame of the animations (after a lot of reading source code, tutorials about skeletal animations and trial and error lol) but got that much all to work (see attached). Eventual goal is to try to enable editing the animations. But that's not my question.

One thing I'm puzzled on is, in all the A3D source code I'd been digging through, and in all the file layouts I've been reading to get my renderer to work thus far, there's nothing about character customizations. For example say in your character's INI file for their appearance you have waistScale = 200 so they should look pretty fat - how does the mesh for the character get modified to account for this?

The only thing I've seen that's related is in elements.data there are several sections like FACE_ but these are only for faces (clearly!) and only map to DDS textures, not actual changes in the mesh, so the same question is still there for the face - how does the mesh get altered for settings like: offsetForeheadH = 106 ?

Many thanks.
 

Attachments

  • madcleric - How does Angelica 3D handle character customizations? - RaGEZONE Forums
    madcleric.webp
    69.7 KB · Views: 153
wanmei created a complete toolkit for model customizations, there are plugins to export models created in 3dsmax, tool to add animations to .smd, tool to edit gfx, tool to edit attack animations and gfx, tools to work with .ecm, there is tool for complete customization of face( animations, voice, size).
You can find remnants of the tools used in the sources of the various wanmei games.

1683248367210 - How does Angelica 3D handle character customizations? - RaGEZONE Forums

imagine the models as layers, body, clothes, armor, faces, what you edit is the layer, in your case you need to discover the layer responsible for editing the body.

in my example:

what you edit is the models inside /models/face.
this layer overlays the head model.
 
Last edited:
wanmei created a complete toolkit for model customizations, there are plugins to export models created in 3dsmax, tool to add animations to .smd, tool to edit gfx, tool to edit attack animations and gfx, tools to work with .ecm, there is tool for complete customization of face( animations, voice, size).
You can find remnants of the tools used in the sources of the various wanmei games.

View attachment 197637
imagine the models as layers, body, clothes, armor, faces, what you edit is the layer, in your case you need to discover the layer responsible for editing the body.

in my example:

what you edit is the models inside /models/face.
this layer overlays the head model.
Could you please share the tools you use? I am currently learning
 
not banned, exiled
Why did he get exiled?

Something you can do is scan entire source code of client with Notepad++ only including results from *.h, *.cpp, *.hpp, and then match word and find in files 'waistScale' for example. Below I do this and follow the logic as you can do with the rest if you want to follow the pipeline for it. Anyway a TLDR and then we get into it:

TLDR: .eface is variances in face part shapes which is referenced to keep facial expression animations working with the differences in say ear shapes etc. Different bone groups are scaled with vecScale and then assigned labels. ElementClient handles a lot of this but angelica too. pBoneHead for example.

waistScale in .ini for for example, EC_Player.cpp has:

C++:
void CECPlayer::SetBodyWaistScale(unsigned char vScale)
{
    m_CustomizeData.waistScale = vScale;
    UpdateBodyScales();
}

which leads us to:

C++:
void CECPlayer::UpdateBodyScales()
{
    if (!GetPlayerModel())
        return;

    A3DSkinModel * pSkinModel = GetPlayerModel()->GetA3DSkinModel();

    if( pSkinModel )
    {
        A3DVECTOR3 vecScale;
        A3DSkeleton * pSkeleton = pSkinModel->GetSkeleton();

        // head adjust
        vecScale.x = 1.0f;
        float fHeadScaleYZ = 1.0f;
        if (GetProfession() == PROF_YEYING || GetProfession() == PROF_YUEXIAN){
            //    玩家使用大头时,Neck 不随头部缩放更易接受。老职业维持原状
            fHeadScaleYZ = TransformScaleFromIntToFloat( m_CustomizeData.upScale, m_CustomizeFactor.fScaleUpFactor, SCALE_UP_FACTOR);
        }else{
            fHeadScaleYZ = TransformScaleFromIntToFloat(m_CustomizeData.headScale, m_CustomizeFactor.fScaleHeadFactor, SCALE_HEAD_FACTOR);
        }
        vecScale.y = vecScale.z = fHeadScaleYZ;

        A3DBone * pBoneNeck = pSkeleton->GetBone("Bip01 Neck", NULL);
        if( pBoneNeck )
        {
            pBoneNeck->SetScaleFactor(vecScale);
            pBoneNeck->SetScaleType(A3DBone::SCALE_LOCAL_NOOFFSET);
        }

        vecScale.x = vecScale.y = vecScale.z = TransformScaleFromIntToFloat( m_CustomizeData.headScale, m_CustomizeFactor.fScaleHeadFactor, SCALE_HEAD_FACTOR);

        A3DBone * pBoneHead = pSkeleton->GetBone("Bip01 Head", NULL);
        if( pBoneHead )
        {
            pBoneHead->SetScaleFactor(vecScale);
            pBoneHead->SetScaleType(A3DBone::SCALE_WHOLE);
        }

        if( m_iGender == GENDER_FEMALE )
        {
            // female's breast adjust
            vecScale.x = TransformScaleFromIntToFloat( m_CustomizeData.breastScale, m_CustomizeFactor.fScaleBreastFactor, SCALE_BREAST_FACTOR);
            vecScale.y = vecScale.z = TransformScaleFromIntToFloat( m_CustomizeData.breastScale,m_CustomizeFactor.fScaleBreastFactor, SCALE_BREAST_FACTOR);

            A3DBone * pBoneFBreast = pSkeleton->GetBone("Bone01", NULL);
            if( pBoneFBreast )
            {
                pBoneFBreast->SetScaleFactor(vecScale);
                pBoneFBreast->SetScaleType(A3DBone::SCALE_LOCAL);
            }
        }

        // first adjust clavicles
        vecScale.x = TransformScaleFromIntToFloat( m_CustomizeData.upScale, m_CustomizeFactor.fScaleUpFactor, SCALE_UP_FACTOR);
        vecScale.y = vecScale.z =TransformScaleFromIntToFloat( m_CustomizeData.upScale, m_CustomizeFactor.fScaleUpFactor, SCALE_UP_FACTOR);

        A3DBone * pBoneLClavicle = pSkeleton->GetBone("Bip01 L Clavicle", NULL);
        if( pBoneLClavicle )
        {
            pBoneLClavicle->SetScaleFactor(vecScale);
            pBoneLClavicle->SetScaleType(A3DBone::SCALE_LOCAL);
        }
        A3DBone * pBoneRClavicle = pSkeleton->GetBone("Bip01 R Clavicle", NULL);
        if( pBoneRClavicle )
        {
            pBoneRClavicle->SetScaleFactor(vecScale);
            pBoneRClavicle->SetScaleType(A3DBone::SCALE_LOCAL);
        }
        
        // then breast
        vecScale.x = 1.0f;
        vecScale.y = vecScale.z = TransformScaleFromIntToFloat( m_CustomizeData.upScale,m_CustomizeFactor.fScaleUpFactor, SCALE_UP_FACTOR);
        
        A3DBone * pBoneBreast = pSkeleton->GetBone("Bip01 Spine2", NULL);
        if( pBoneBreast )
        {
            pBoneBreast->SetScaleFactor(vecScale);
            pBoneBreast->SetScaleType(A3DBone::SCALE_LOCAL);
        }

        // chest
        vecScale.x = 1.0f;
        vecScale.y = vecScale.z = TransformScaleFromIntToFloat( m_CustomizeData.upScale,m_CustomizeFactor.fScaleUpFactor, SCALE_UP_FACTOR);

        A3DBone * pBoneChest = pSkeleton->GetBone("Bip01 Spine1", NULL);
        if( pBoneChest )
        {
            pBoneChest->SetScaleFactor(vecScale);
            pBoneChest->SetScaleType(A3DBone::SCALE_LOCAL);
        }

        // waist
        vecScale.x = 1.0f;
        vecScale.y = vecScale.z = TransformScaleFromIntToFloat( m_CustomizeData.waistScale,m_CustomizeFactor.fScaleWaistFactor,  SCALE_WAIST_FACTOR);

        A3DBone * pBoneWaist = pSkeleton->GetBone("Bip01 Spine", NULL);
        if( pBoneWaist )
        {
            pBoneWaist->SetScaleFactor(vecScale);
            pBoneWaist->SetScaleType(A3DBone::SCALE_LOCAL_NOOFFSET);
        }

        // pelvis
        vecScale.x = 1.0f;
        vecScale.y = vecScale.z = TransformScaleFromIntToFloat( m_CustomizeData.waistScale,m_CustomizeFactor.fScaleWaistFactor, SCALE_WAIST_FACTOR);

        A3DBone * pBonePelvis = pSkeleton->GetBone("Bip01 Pelvis", NULL);
        if( pBonePelvis )
        {
            pBonePelvis->SetScaleFactor(vecScale);
            pBonePelvis->SetScaleType(A3DBone::SCALE_LOCAL);
        }

        // arm
        vecScale.x = 1.0f;
        vecScale.y = vecScale.z = TransformScaleFromIntToFloat( m_CustomizeData.armWidth,m_CustomizeFactor.fWidthArmFactor, WIDTH_ARM_FACTOR);
        
        A3DBone * pBoneLUpperArm = pSkeleton->GetBone("Bip01 L UpperArm", NULL);
        if( pBoneLUpperArm )
        {
            pBoneLUpperArm->SetScaleFactor(vecScale);
            pBoneLUpperArm->SetScaleType(A3DBone::SCALE_LOCAL);
        }
        A3DBone * pBoneRUpperArm = pSkeleton->GetBone("Bip01 R UpperArm", NULL);
        if( pBoneRUpperArm )
        {
            pBoneRUpperArm->SetScaleFactor(vecScale);
            pBoneRUpperArm->SetScaleType(A3DBone::SCALE_LOCAL);
        }
        A3DBone * pBoneLForeArm = pSkeleton->GetBone("Bip01 L Forearm", NULL);
        if( pBoneLForeArm )
        {
            pBoneLForeArm->SetScaleFactor(vecScale);
            pBoneLForeArm->SetScaleType(A3DBone::SCALE_LOCAL);
        }
        A3DBone * pBoneRForeArm = pSkeleton->GetBone("Bip01 R Forearm", NULL);
        if( pBoneRForeArm )
        {
            pBoneRForeArm->SetScaleFactor(vecScale);
            pBoneRForeArm->SetScaleType(A3DBone::SCALE_LOCAL);
        }

        // leg
        vecScale.x = 1.0f;
        vecScale.y = vecScale.z = TransformScaleFromIntToFloat( m_CustomizeData.legWidth, m_CustomizeFactor.fWidthLegFactor, WIDTH_ARM_FACTOR);

        A3DBone * pBoneLThigh = pSkeleton->GetBone("Bip01 L Thigh", NULL);
        if( pBoneLThigh )
        {
            pBoneLThigh->SetScaleFactor(vecScale);
            pBoneLThigh->SetScaleType(A3DBone::SCALE_LOCAL);
        }
        A3DBone * pBoneRThigh = pSkeleton->GetBone("Bip01 R Thigh", NULL);
        if( pBoneRThigh )
        {
            pBoneRThigh->SetScaleFactor(vecScale);
            pBoneRThigh->SetScaleType(A3DBone::SCALE_LOCAL);
        }

        vecScale.x = 1.0f;
        vecScale.y = vecScale.z = TransformScaleFromIntToFloat( m_CustomizeData.legWidth, m_CustomizeFactor.fWidthLegFactor, WIDTH_LEG_FACTOR);

        A3DBone * pBoneLCalf = pSkeleton->GetBone("Bip01 L Calf", NULL);
        if( pBoneLCalf )
        {
            pBoneLCalf->SetScaleFactor(vecScale);
            pBoneLCalf->SetScaleType(A3DBone::SCALE_LOCAL);
        }
        A3DBone * pBoneRCalf = pSkeleton->GetBone("Bip01 R Calf", NULL);
        if( pBoneRCalf )
        {
            pBoneRCalf->SetScaleFactor(vecScale);
            pBoneRCalf->SetScaleType(A3DBone::SCALE_LOCAL);
        }
        ScaleBody(m_fScaleBySkill);
    }
}

We can see vecScale being used in tandem with bone groups with labels such as Bip01, Spine, Spine1, etc. These refer to different bone groups on the player skeleton for scaling I would assume. Let us follow SetScaleFactor and SetScaleType to be fully clear on how this is done:

C++:
//    Set / Get scale type
void A3DBone::SetScaleType(int iType)
{
    ASSERT(!g_pA3DConfig->GetFlagNewBoneScale());
    m_iScaleType = iType;
}

//    Set / Get scale factor
void A3DBone::SetScaleFactor(const A3DVECTOR3& vScale)
{
    ASSERT(!g_pA3DConfig->GetFlagNewBoneScale());
    m_vScaleFactor = vScale;
}
then just to be sure we go:
C++:
        bool bNewBoneScaleMethod = g_pA3DConfig->GetFlagNewBoneScale();
        sprintf(szLine, _format_new_scale, bNewBoneScaleMethod);
        file.WriteLine(szLine);

        if (bNewBoneScaleMethod)
        {
            sprintf(szLine, _format_bone_num, m_BoneScaleExArr.size());
            file.WriteLine(szLine);

            for (i = 0; i < m_BoneScaleExArr.size(); i++)
            {
                BoneScaleEx* pScale = m_BoneScaleExArr[i];

                sprintf(szLine, _format_bone_index, pScale->m_nIndex);
                file.WriteLine(szLine);

                sprintf(szLine, _format_bone_scale, pScale->m_fLenFactor, pScale->m_fThickFactor, pScale->m_fWholeFactor);
                file.WriteLine(szLine);
            }
        }
        else
        {
            sprintf(szLine, _format_bone_num, m_BoneScales.size());
            file.WriteLine(szLine);

            for (i = 0; i < m_BoneScales.size(); i++)
            {
                BoneScale* pScale = m_BoneScales[i];

                sprintf(szLine, _format_bone_index, pScale->m_nIndex);
                file.WriteLine(szLine);

                sprintf(szLine, _format_bone_scl_type, pScale->m_nType);
                file.WriteLine(szLine);

                sprintf(szLine, _format_bone_scale, VECTOR_XYZ(pScale->m_vScale));
                file.WriteLine(szLine);
            }
        }

        file.WriteLine(m_strScaleBaseBone);

        sprintf(szLine, _format_def_speed, m_fDefPlaySpeed);
        file.WriteLine(szLine);

        sprintf(szLine, _format_can_castshadow, (int)m_bCanCastShadow);
        file.WriteLine(szLine);

        sprintf(szLine, _format_render_model, (int)m_bRenderSkinModel);
        file.WriteLine(szLine);

        sprintf(szLine, _format_render_edge, (int)m_bRenderEdge);
        file.WriteLine(szLine);

        g_GfxSavePixelShaderConsts(&file, m_strPixelShader, m_strShaderTex, m_vecPSConsts);
        int channel_count = 0;

        for (i = 0; i < A3DSkinModel::ACTCHA_MAX; i++)
        {
            ActChannelInfo* pChannel = m_ChannelInfoArr[i];

            if (pChannel && pChannel->bone_names.size())
                channel_count++;
        }

        sprintf(szLine, _format_channel_count, channel_count);
        file.WriteLine(szLine);

        for (i = 0; i < A3DSkinModel::ACTCHA_MAX; i++)
        {
            ActChannelInfo* pChannel = m_ChannelInfoArr[i];

            if (pChannel && pChannel->bone_names.size())
            {
                sprintf(szLine, _format_channel, i);
                file.WriteLine(szLine);

                sprintf(szLine, _format_bone_num, (int)pChannel->bone_names.size());
                file.WriteLine(szLine);

                for (size_t j = 0; j < pChannel->bone_names.size(); j++)
                {
                    file.WriteLine(pChannel->bone_names[j]);
                }
            }
        }

        sprintf(szLine, _format_channel_count, A3DSkinModel::ACTCHA_MAX);
        file.WriteLine(szLine);

        for (i = 0; i < A3DSkinModel::ACTCHA_MAX; i++)
        {
            sprintf(szLine, _format_channel_mask, m_EventMasks[i]);
            file.WriteLine(szLine);
        }

        sprintf(szLine, _format_cogfx_num, m_CoGfxMap.size());
        file.WriteLine(szLine);

        sprintf(szLine, _format_comact_count, m_ActionMap.size());
        file.WriteLine(szLine);

        for (CoGfxMap::iterator it = m_CoGfxMap.begin(); it != m_CoGfxMap.end(); ++it)
            it->second->Save(&file);

        abase::vector<A3DCombinedAction*> act_sort;

        for (CombinedActMap::iterator it_act = m_ActionMap.begin(); it_act != m_ActionMap.end(); ++it_act)
            act_sort.push_back(it_act->second);

        qsort(act_sort.begin(), act_sort.size(), sizeof(int), _str_compare);

        for (i = 0; i < act_sort.size(); i++)
            act_sort[i]->Save(&file);

        int nScriptCount = 0;

        for (i = 0; i < enumECMScriptCount; i++)
            if (!m_Scripts[i].IsEmpty())
                nScriptCount++;

        sprintf(szLine, _format_script_count, nScriptCount);
        file.WriteLine(szLine);

        for (i = 0; i < enumECMScriptCount; i++)
        {
            if (!m_Scripts[i].IsEmpty())
            {
                sprintf(szLine, _format_id, i);
                file.WriteLine(szLine);

                AString& str = m_Scripts[i];
                int nBufLen = (str.GetLength() + 3) / 3 * 4 + 32;
                char* pBuf = new char[nBufLen];
                int len = base64_encode((unsigned char*)(const char*)str, str.GetLength()+1, pBuf);
                int nLines = len / 1500;

                if (len > nLines * 1500)
                    nLines++;

                sprintf(szLine, _format_script_lines, nLines);
                file.WriteLine(szLine);

                const char* pWrite = pBuf;

                while (len)
                {
                    int nWrite = len > 1500 ? 1500 : len;
                    len -= nWrite;

                    AString s(pWrite, nWrite);
                    file.WriteLine(s);
                    pWrite += nWrite;
                }

                delete[] pBuf;
            }
        }

        SaveAdditionalSkin(&file);

        if (pModel)
        {
            sprintf(szLine, _format_child_count, pModel->m_ChildModels.size());
            file.WriteLine(szLine);

            for (ECModelMap::iterator it_child = pModel->m_ChildModels.begin(); it_child != pModel->m_ChildModels.end(); ++it_child)
            {
                sprintf(szLine, _format_child_name, it_child->first);
                file.WriteLine(szLine);

                sprintf(szLine, _format_child_path, it_child->second->m_pMapModel->m_strFilePath);
                file.WriteLine(szLine);

                sprintf(szLine, _format_hh_name, it_child->second->m_strHookName);
                file.WriteLine(szLine);

                sprintf(szLine, _format_cc_name, it_child->second->m_strCC);
                file.WriteLine(szLine);
            }
        }
        else
        {
            sprintf(szLine, _format_child_count, m_ChildInfoArray.size());
            file.WriteLine(szLine);

            for (i = 0; i < m_ChildInfoArray.size(); i++)
            {
                const ChildInfo* pChildInfo = m_ChildInfoArray[i];
                sprintf(szLine, _format_child_name, (const char*)pChildInfo->m_strName);
                file.WriteLine(szLine);

                sprintf(szLine, _format_child_path, (const char*)pChildInfo->m_strPath);
                file.WriteLine(szLine);

                sprintf(szLine, _format_hh_name, (const char*)pChildInfo->m_strHHName);
                file.WriteLine(szLine);

                sprintf(szLine, _format_cc_name, (const char*)pChildInfo->m_strCCName);
                file.WriteLine(szLine);
            }
        }

        sprintf(szLine, _format_phys_file, m_pPhysSyncData ? m_strPhysFileName : "");
        file.WriteLine(szLine);

        _snprintf(szLine, AFILE_LINEMAXLEN, _format_ecmhook_count, m_ECModelHookMap.size());
        file.WriteLine(szLine);

        for (ECModelHookMap::iterator it_ecmhook = m_ECModelHookMap.begin(); it_ecmhook != m_ECModelHookMap.end(); ++it_ecmhook)
        {
            CECModelHook* pHook = it_ecmhook->second;
            if (!pHook)
                continue;

            pHook->Save(&file);
        }

    }

    file.Close();


I don't think we need to follow any deeper to get the gist of how this works. If you can read C++ there should be no confusion here.
 
Forgot to also show vScale referenced in first function, here is where and how it is used in the A3DBone::Update(int nDeltaTime):
C++:
    // let the bone controller take effects.
    A3DMATRIX4 matControlled = IdentityMatrix();
    if (m_pFirstController)
        m_pFirstController->Update(nDeltaTime, this, matControlled);
   
    m_matUpToParent = m_matRelativeTM * matControlled;
    A3DBone* pParent = m_iParent >= 0 ? m_pSkeleton->GetBone(m_iParent) : NULL;

    if (g_pA3DConfig->GetFlagNewBoneScale())
    {
        //    Update the transform matrix for using.
        if (!pParent)
        {
            m_matUpToRoot = m_matUpToParent;
        }
        else if (!m_bAnimDriven)
        {
            //    If this bone is driven by physical system and it's linked with a joint,
            //    it's offset should have been calculated outside.
            m_matUpToRoot = m_matUpToParent * pParent->GetUpToRootTM();
        }
        else
        {
            float fLocalLenSF = m_fLocalLenSF * pParent->GetAccuWholeScale();

            if (fLocalLenSF != 1.0f)
            {
                //    Only scale child bone's offset
                m_matUpToParent._41 *= fLocalLenSF;
                m_matUpToParent._42 *= fLocalLenSF;
                m_matUpToParent._43 *= fLocalLenSF;
            }

            m_matUpToRoot = m_matUpToParent * pParent->GetUpToRootTM();
        }

        m_matAbsoluteTM = m_matScale * m_matUpToRoot * m_pSkeleton->GetAbsoluteTM();
    }
    else    //    Bypast bone scale
    {
        //    Update the transform matrix for using.
        if (pParent)
        {
            if (pParent->GetScaleType() == SCALE_LOCAL)
            {
                //    Only scale child bone's offset
                const A3DVECTOR3& vScale = pParent->GetScaleFactor();
                m_matUpToParent._41 *= vScale.x;
                m_matUpToParent._42 *= vScale.y;
                m_matUpToParent._43 *= vScale.z;
                m_matUpToRoot = m_matUpToParent * pParent->m_matLocalAbs;
            }
            else if (pParent->GetScaleType() == SCALE_LOCAL_NOOFFSET)
            {
                //    Don't scale child bone's offset
                m_matUpToRoot = m_matUpToParent * pParent->m_matLocalAbs;
            }
            else
                m_matUpToRoot = m_matUpToParent * pParent->GetUpToRootTM();
        }
        else
            m_matUpToRoot = m_matUpToParent;

        if (m_iScaleType != SCALE_NONE)
        {
            //    Save the matrix before scaling
            m_matLocalAbs = m_matUpToRoot;

            A3DMATRIX4 mat;
            mat.Scale(m_vScaleFactor.x, m_vScaleFactor.y, m_vScaleFactor.z);
            m_matUpToRoot = mat * m_matUpToRoot;
            m_matUpToParent = mat * m_matUpToParent;
        }

        m_matAbsoluteTM = m_matUpToRoot * m_pSkeleton->GetAbsoluteTM();
    }

    if (m_bAnimDriven)
        m_matAbsoluteTM._42 -= m_pSkeleton->GetFootOffset();

    //    Make no sacle absolute matrix
    NoScaleAbsoluteTM();

Hope this gives you almost every answer you needed to make your code work for what you needed to do. you can see the vScale A3D Vector being defined here midway through this snippet or so.
 
Back