Jump to content

All character models form 2014 client imported into blender, complete with animations


Recommended Posts

  • Active+ Member

Hello,

I've exported all the character models from a 2014 client, including animations and textures. I've created convenient .blend files, which might be useful for someone. You can use these for rendering purposes, or use the skeletons to create your own animations or other interesting projects.

You can also use these files to import all the models into another engine, such as Unity or Unreal.

I have tried to correct all the errors I found. However, it must be taken into account that these models and animations were created using Autodesk software. Consequently, there are certain differences that complicate the import process. One of these differences is the skeleton itself; we can notice that at the ends of fingers, toes, and other bones, there are small blocks often named with "nub." This is one of the differences in how the skeleton is constructed in Maya compared to Blender. These can be removed, and the animations will still work, but I left them just in case. It's always easier to remove something than to add it later.

All textures are in PNG format, and the alpha channel is preserved. Metin encodes information about, I believe, the metallness of the material in the alpha channel. I'm not sure about this. Therefore, most materials are set to ignore the alpha channel.

All textures are packed in the .blend file; however, I am providing these textures separately in a folder for easy access.

There is no reason why I exported these models. I just found a tool here on our forum and had to test it. I'm sharing the result in case someone finds it useful.

I have always wanted to export a character from Metin, but the possibilities were limited. It's great that it is now possible.

This is the hidden content, please

This is the hidden content, please

spacer.png

spacer.png

Quick and dirty render from blender:

spacer.pngspacer.png

spacer.pngspacer.png

 

  • Metin2 Dev 184
  • Eyes 13
  • Flame 9
  • Angry 1
  • Think 1
  • Scream 1
  • Good 69
  • muscle 5
  • Love 28
  • Love 234
  • Active+ Member

just to see if I understood correctly. in the archive, there are only general run animation for each character right? 

  • Active+ Member
14 hours ago, KolenMG said:

just to see if I understood correctly. in the archive, there are only general run animation for each character right? 

Each character has all available animations. Make sure you have the skeleton selected to see them.

  • muscle 1
  • Love 2
  • 3 months later...

I believe there are some issues with the root motion of the animations.
Let me explain better: some animations, like attack combos, are supposed to move the character forward during execution. However, this behavior seems to have been compromised, possibly due to some changes made.

It would be really nice if you could solve this problem ❤️

spacer.png

Edited by Metin2 Dev International
Core X - External 2 Internal
On 4/16/2025 at 5:26 AM, Viciuus said:

I believe there are some issues with the root motion of the animations.
Let me explain better: some animations, like attack combos, are supposed to move the character forward during execution. However, this behavior seems to have been compromised, possibly due to some changes made.

It would be really nice if you could solve this problem ❤️

spacer.png

I solved the issue by creating a new main bone called "root". Here's what I did in Blender:

  1. In Edit Mode, press Shift + A to create a new bone and name it "root".
  2. Select the "Bip01" bone in the armature hierarchy.
  3. Go to the Bone > Relations panel and set "root" as its parent.
  4. Check the keyframes of the Bip01 bone, specifically on the Z Location axis.

If the motion goes forward and then backward, remove the keyframes that reverse the motion.


For those using Unreal Engine, like me:

I used the RMFixToolEditor plugin (available on the Marketplace). (Instead of change all keyframes from Bip01 to root manually)

Under Transfer Animation Between Bones, set:

  • From: Bip01
  • To: root

Enable Clear zero frame offset and select only on Y axis.

For each animation sequence, enable Root Motion, but disable Use Normalized Root Motion Scale.

Result: a clean and acceptable root motion.

Edited by Viciuus
edit
  • muscle 1
  • Love 1
  • 3 weeks later...
  • Active+ Member
On 4/12/2025 at 12:09 AM, Jerry50 said:

Hello, what tool did you use? Could you explain the extraction process?

 

On 4/4/2025 at 1:13 AM, szabsza said:

Much appreciated! Could you share the method you used to convert these models into the correct format? I tried Noesis, but the bones are messed up and rotated.

 

GrannyExporter.exe

 

Blender Import Addon (Better Fbx Importer & Exporter 6.0.5 by Mesh Online) paid

https://superhivemarket.com/products/better-fbx-importer--exporter

 

A few JS scripts that I used for exporting. I do not guarantee that they work as intended and I do not provide support for their use.

make_index_4.js

Spoiler

 

const fs = require("fs");
const path = require("path");

// Tutaj ustaw ścieżkę, którą chcesz przeszukiwać
// Przykład: const inputDir = "D:\\ymir work\\pc2\\sura";
const inputDir = "D:\\ymir work\\zone";

// Ścieżki do pliku wykonywalnego GrannyExporter.exe i katalogu wyjściowego
const exe_path = 'c:\\Users\\XD\\Desktop\\GrannyExporter.exe';
const out_dir = 'c:\\Users\\XD\\Desktop\\index_out';

// Funkcja, która rekurencyjnie zbiera wszystkie pliki .gr2 z danego katalogu i podkatalogów
function getAllGr2FilesRecursively(dir) {
    let results = [];

    // Odczytujemy zawartość bieżącego folderu
    const items = fs.readdirSync(dir, { withFileTypes: true });

    // Iterujemy po każdym elemencie (plik / katalog)
    for (const item of items) {
        const fullPath = path.join(dir, item.name);

        if (item.isDirectory()) {
            // Rekurencyjnie schodzimy w głąb podkatalogu
            results = results.concat(getAllGr2FilesRecursively(fullPath));
        } else if (item.isFile() && item.name.endsWith(".gr2")) {
            // Mamy plik .gr2 – dodajemy jego ścieżkę do listy
            results.push(fullPath);
        }
    }

    results = results.filter(file => !file.includes("_lod_"));
    results = results.filter(file => !file.includes("_lod0"));

    return results;
}

// Funkcja pomocnicza do tworzenia pliku .cmd (lista poleceń dla GrannyExporter.exe)
function createCmdFile(filePaths, outputCmdFileName) {
    // Każdą ścieżkę opakowujemy w -f "..."
    const wrappedPaths = filePaths.map(filePath => `-f "${filePath}"`).join(" ");

    // Budujemy treść pliku .cmd: parametry + wskazanie katalogu wyjściowego
    // (możesz dopisać dodatkowe argumenty, np. -s itp., zależnie od potrzeb)
    const cmdContent = `${exe_path} ${wrappedPaths} -d "${out_dir}"`;

    // Zapisujemy do pliku .cmd
    fs.writeFileSync(outputCmdFileName, cmdContent, "utf8");
    console.log(`File written: ${outputCmdFileName}`);
}

// Główna część skryptu
function main() {
    // Sprawdzamy, czy ścieżka istnieje
    if (!fs.existsSync(inputDir)) {
        console.error(`Podana ścieżka nie istnieje: ${inputDir}`);
        process.exit(1);
    }

    // Zbieramy wszystkie pliki .gr2 z katalogu i podkatalogów
    const gr2_models = getAllGr2FilesRecursively(inputDir);

    // Zapisujemy listę plików do gr2_models.txt (pomocne w debugowaniu)
    const modelsListFileName = "models.txt";
    fs.writeFileSync(modelsListFileName, gr2_models.join("\n"), "utf8");
    console.log(`File written: ${modelsListFileName}`);

    // Generujemy plik .cmd dla wszystkich modeli
    createCmdFile(gr2_models, "models-cmd.txt");
}

// Uruchamiamy funkcję main, aby wykonać powyższe operacje
main();
 

 

make_index_3.js

Spoiler

 

const fs = require("fs");
const path = require("path");

const exe_path = '. c:\\Users\\XD\\Desktop\\GrannyExporter.exe';
const out_dir = 'c:\\Users\\XD\\Desktop\\index_out';

// Teraz directories jest tablicą obiektów, 
// w których każdy obiekt zawiera ścieżkę 'path' oraz nazwę base_model.
const directories = [
    // { path: "d:\\ymir work\\pc\\assassin", base_model: "assassin_novice" },
    // { path: "d:\\ymir work\\pc\\shaman", base_model: "shaman_miyeom" },
    // { path: "d:\\ymir work\\pc\\sura", base_model: "sura_unra" },
    // { path: "d:\\ymir work\\pc\\warrior", base_model: "warrior_nahan" },
    // { path: "d:\\ymir work\\pc2\\assassin", base_model: "assassin_4-1" },
    // { path: "d:\\ymir work\\pc2\\shaman", base_model: "shaman_4-1" },
    { path: "d:\\ymir work\\pc2\\sura", base_model: "sura_4-1" },
    // { path: "d:\\ymir work\\pc2\\warrior", base_model: "warrior_nahan" }
];

function getSubdirectories(dir) {
    return fs.readdirSync(dir, { withFileTypes: true })
        .filter(item => item.isDirectory())
        .map(item => path.join(dir, item.name));
}

function getFiles(dir, extension) {
    return fs.readdirSync(dir, { withFileTypes: true })
        .filter(item => item.isFile() && item.name.endsWith(extension))
        .map(item => path.join(dir, item.name));
}

function getAnims(dir) {
    let gr2_anims = [];
    // Pomijamy podkatalogi z końcówką "hair", gdyż tam często są modele,
    // a nie animacje (zgodnie z dotychczasowym założeniem).
    let subdirs = getSubdirectories(dir).filter(subdir => !subdir.endsWith("hair"));

    for (const subdir of subdirs) {
        gr2_anims = gr2_anims.concat(getFiles(subdir, ".gr2"));
    }
    return gr2_anims;
}

function createPythonAnimationsFile(gr2_anims, outputPyFileName = "animations.py") {
    // 1. Z każdej ścieżki wyodrębniamy ostatni katalog (czyli folder poprzedzający plik) i nazwę pliku (bez rozszerzenia).
    // 2. Tworzymy ciąg "katalog nazwa_pliku" - w przykładzie: "action angry".
    const animNames = gr2_anims.map(animPath => {
        // path.dirname(animPath) zwraca np. "d:\\ymir work\\pc\\warrior\\action"
        const dirName = path.basename(path.dirname(animPath)); 
        // path.parse(animPath).name zwraca nazwę pliku bez rozszerzenia, np. "angry"
        const fileBaseName = path.parse(animPath).name;        
        return `${dirName} ${fileBaseName}`;
    });

    // 3. Budujemy zawartość pliku .py w stylu:
    // anims = [
    //     "action angry",
    //     "inny_katalog inny_plik",
    //     ...
    // ]
    // Uwaga: żeby kod wyglądał czytelnie, robimy wielolinijkowy zapis.
    const pythonArrayContent = 
`anims = [
${animNames.map(name => `    "${name}"`).join(",\n")}
]`;

    // 4. Zapisujemy do pliku .py
    fs.writeFileSync(outputPyFileName, pythonArrayContent, "utf8");
    console.log(`File written: ${outputPyFileName}`);
}

function getModels(dir) {
    let gr2_models = getFiles(dir, ".gr2");

    // Usuwamy pliki zawierające "_lod_" i "_rabbit1_backup"
    gr2_models = gr2_models.filter(file => !file.includes("_lod_"));
    gr2_models = gr2_models.filter(file => !file.includes("_lod0"));
    gr2_models = gr2_models.filter(file => !file.includes("_rabbit1_backup"));
    gr2_models = gr2_models.filter(file => !file.includes("assassin.gr2"));
    

    // Jeśli istnieje podkatalog "hair", to tamte pliki .gr2 także traktujemy jako modele
    let hairDir = path.join(dir, "hair");
    if (fs.existsSync(hairDir)) {
        const hairFiles = getFiles(hairDir, ".gr2");
        gr2_models = gr2_models.concat(hairFiles);
    }

    return gr2_models;
}

function createCmdFile(baseDir, filePaths, parent, child, type) {
    // Każdy plik otaczamy -f "..."
    const wrappedPaths = filePaths.map(filePath => `-f "${filePath}"`).join(" ");

    // Katalog docelowy (domyślny "fbx" w bieżącej lokalizacji)
    // const fbxDir = path.join(process.cwd(), "fbx");

    // Zawartość pliku .cmd to lista argumentów + wskazanie katalogu
    const cmdContent = `${exe_path} ${wrappedPaths} -d "${out_dir}"`;

    // Tworzymy nazwę pliku .cmd.txt z sufiksem zależnym od typu
    const outputCmdFileName = `${parent}-${child}-${type}-cmd.txt`;
    const outputCmdFilePath = path.join(process.cwd(), outputCmdFileName);

    fs.writeFileSync(outputCmdFilePath, cmdContent, "utf8");
    console.log(`File written: ${outputCmdFileName}`);
}

function processDirectory(directoryObj) {
    const baseDir = directoryObj.path;
    const baseModel = directoryObj.base_model;

    // Pobieramy listę animacji i modeli
    const gr2_anims = getAnims(baseDir);
    let gr2_models = getModels(baseDir);

    // Szukamy w nazwach modeli ciągu baseModel
    let selectedModel = gr2_models.find(filePath => filePath.includes(baseModel));

    // Jeśli nie ma żadnego pasującego pliku, bierzemy pierwszy z listy (o ile istnieje)
    if (!selectedModel && gr2_models.length > 0) {
        selectedModel = gr2_models[0];
    }

    // Jeśli nic nie znaleziono i nic nie ma w gr2_models, selectedModel zostanie undefined,
    // ale poniżej i tak nic się nie wydarzy (dla bezpieczeństwa warto to sprawdzić).

    // Usuwamy wybrany model z listy gr2_models
    if (selectedModel) {
        gr2_models = gr2_models.filter(filePath => filePath !== selectedModel);
    }

    // Dodajemy wybrany model na koniec listy animacji
    // (zgodnie z instrukcją – "dodajemy do animacji jeden element ze zbioru modeli")
    const finalAnims = selectedModel 
        ? [...gr2_anims, selectedModel] 
        : [...gr2_anims];

    // Nazwy folderów do użycia w nazwach wyjściowych plików
    // (są to dwa ostatnie fragmenty ścieżki)
    const [parent, child] = baseDir.split(path.sep).slice(-2);

    // 1) Tworzymy plik z listą animacji
    const outputAnimsFileName = `${parent}-${child}-anims.txt`;
    const outputAnimsFilePath = path.join(process.cwd(), outputAnimsFileName);
    fs.writeFileSync(outputAnimsFilePath, finalAnims.join("\n"), "utf8");
    console.log(`File written: ${outputAnimsFileName}`);

    // 2) Tworzymy plik z listą modeli
    const outputModelsFileName = `${parent}-${child}-models.txt`;
    const outputModelsFilePath = path.join(process.cwd(), outputModelsFileName);
    fs.writeFileSync(outputModelsFilePath, gr2_models.join("\n"), "utf8");
    console.log(`File written: ${outputModelsFileName}`);

    // Wywołujemy createCmdFile dla animacji
    createCmdFile(baseDir, finalAnims, parent, child, "anims");

    // Wywołujemy createCmdFile dla modeli
    createCmdFile(baseDir, gr2_models, parent, child, "models");
    
    createPythonAnimationsFile(gr2_anims, `${parent}-${child}-animations.py`);
}

// Uruchamiamy przetwarzanie dla każdego elementu w tablicy directories
directories.forEach(processDirectory);
 

 

make_index_2.js

Spoiler

 

const fs = require("fs");
const path = require("path");

const exe_path = '. c:\\Users\\XD\\Desktop\\GrannyExporter.exe';
const out_dir = 'c:\\Users\\XD\\Desktop\\index_out';

// Teraz directories jest tablicą obiektów, 
// w których każdy obiekt zawiera ścieżkę 'path' oraz nazwę base_model.
const directories = [
    { path: "d:\\ymir work\\pc\\assassin", base_model: "assassin" },
    { path: "d:\\ymir work\\pc\\shaman", base_model: "shaman" },
    { path: "d:\\ymir work\\pc\\sura", base_model: "sura" },
    { path: "d:\\ymir work\\pc\\warrior", base_model: "warrior_nahan" },
    { path: "d:\\ymir work\\pc2\\assassin", base_model: "assassin" },
    { path: "d:\\ymir work\\pc2\\shaman", base_model: "shaman" },
    { path: "d:\\ymir work\\pc2\\sura", base_model: "sura" },
    { path: "d:\\ymir work\\pc2\\warrior", base_model: "warrior" }
];

function getSubdirectories(dir) {
    return fs.readdirSync(dir, { withFileTypes: true })
        .filter(item => item.isDirectory())
        .map(item => path.join(dir, item.name));
}

function getFiles(dir, extension) {
    return fs.readdirSync(dir, { withFileTypes: true })
        .filter(item => item.isFile() && item.name.endsWith(extension))
        .map(item => path.join(dir, item.name));
}

function getAnims(dir) {
    let gr2_anims = [];
    // Pomijamy podkatalogi z końcówką "hair", gdyż tam często są modele,
    // a nie animacje (zgodnie z dotychczasowym założeniem).
    let subdirs = getSubdirectories(dir).filter(subdir => !subdir.endsWith("hair"));

    for (const subdir of subdirs) {
        gr2_anims = gr2_anims.concat(getFiles(subdir, ".gr2"));
    }
    return gr2_anims;
}

function getModels(dir) {
    let gr2_models = getFiles(dir, ".gr2");

    // Usuwamy pliki zawierające "_lod_" i "_rabbit1_backup"
    gr2_models = gr2_models.filter(file => !file.includes("_lod_"));
    gr2_models = gr2_models.filter(file => !file.includes("_rabbit1_backup"));

    // Jeśli istnieje podkatalog "hair", to tamte pliki .gr2 także traktujemy jako modele
    let hairDir = path.join(dir, "hair");
    if (fs.existsSync(hairDir)) {
        const hairFiles = getFiles(hairDir, ".gr2");
        gr2_models = gr2_models.concat(hairFiles);
    }

    return gr2_models;
}

function createCmdFile(baseDir, filePaths, parent, child, type) {
    // Każdy plik otaczamy -f "..."
    const wrappedPaths = filePaths.map(filePath => `-f "${filePath}"`).join(" ");

    // Katalog docelowy (domyślny "fbx" w bieżącej lokalizacji)
    // const fbxDir = path.join(process.cwd(), "fbx");

    // Zawartość pliku .cmd to lista argumentów + wskazanie katalogu
    const cmdContent = `${exe_path} ${wrappedPaths} -d "${out_dir}"`;

    // Tworzymy nazwę pliku .cmd.txt z sufiksem zależnym od typu
    const outputCmdFileName = `${parent}-${child}-${type}-cmd.txt`;
    const outputCmdFilePath = path.join(process.cwd(), outputCmdFileName);

    fs.writeFileSync(outputCmdFilePath, cmdContent, "utf8");
    console.log(`File written: ${outputCmdFileName}`);
}

function processDirectory(directoryObj) {
    const baseDir = directoryObj.path;
    const baseModel = directoryObj.base_model;

    // Pobieramy listę animacji i modeli
    const gr2_anims = getAnims(baseDir);
    let gr2_models = getModels(baseDir);

    // Szukamy w nazwach modeli ciągu baseModel
    let selectedModel = gr2_models.find(filePath => filePath.includes(baseModel));

    // Jeśli nie ma żadnego pasującego pliku, bierzemy pierwszy z listy (o ile istnieje)
    if (!selectedModel && gr2_models.length > 0) {
        selectedModel = gr2_models[0];
    }

    // Jeśli nic nie znaleziono i nic nie ma w gr2_models, selectedModel zostanie undefined,
    // ale poniżej i tak nic się nie wydarzy (dla bezpieczeństwa warto to sprawdzić).

    // Usuwamy wybrany model z listy gr2_models
    if (selectedModel) {
        gr2_models = gr2_models.filter(filePath => filePath !== selectedModel);
    }

    // Dodajemy wybrany model na koniec listy animacji
    // (zgodnie z instrukcją – "dodajemy do animacji jeden element ze zbioru modeli")
    const finalAnims = selectedModel 
        ? [...gr2_anims, selectedModel] 
        : [...gr2_anims];

    // Nazwy folderów do użycia w nazwach wyjściowych plików
    // (są to dwa ostatnie fragmenty ścieżki)
    const [parent, child] = baseDir.split(path.sep).slice(-2);

    // 1) Tworzymy plik z listą animacji
    const outputAnimsFileName = `${parent}-${child}-anims.txt`;
    const outputAnimsFilePath = path.join(process.cwd(), outputAnimsFileName);
    fs.writeFileSync(outputAnimsFilePath, finalAnims.join("\n"), "utf8");
    console.log(`File written: ${outputAnimsFileName}`);

    // 2) Tworzymy plik z listą modeli
    const outputModelsFileName = `${parent}-${child}-models.txt`;
    const outputModelsFilePath = path.join(process.cwd(), outputModelsFileName);
    fs.writeFileSync(outputModelsFilePath, gr2_models.join("\n"), "utf8");
    console.log(`File written: ${outputModelsFileName}`);

    // Wywołujemy createCmdFile dla animacji
    createCmdFile(baseDir, finalAnims, parent, child, "anims");

    // Wywołujemy createCmdFile dla modeli
    createCmdFile(baseDir, gr2_models, parent, child, "models");
}

// Uruchamiamy przetwarzanie dla każdego elementu w tablicy directories
directories.forEach(processDirectory);
 

 

make_index.js

Spoiler

 

const fs = require("fs");
const path = require("path");

const directories = [
    "d:\\ymir work\\pc\\assassin",
    "d:\\ymir work\\pc\\shaman",
    "d:\\ymir work\\pc\\sura",
    "d:\\ymir work\\pc\\warrior",
    "d:\\ymir work\\pc2\\assassin",
    "d:\\ymir work\\pc2\\shaman",
    "d:\\ymir work\\pc2\\sura",
    "d:\\ymir work\\pc2\\warrior"
];

function getSubdirectories(dir) {
    return fs.readdirSync(dir, { withFileTypes: true })
        .filter(item => item.isDirectory())
        .map(item => path.join(dir, item.name));
}

function getFiles(dir, extension) {
    return fs.readdirSync(dir, { withFileTypes: true })
        .filter(item => item.isFile() && item.name.endsWith(extension))
        .map(item => path.join(dir, item.name));
}

function getAnims(dir) {
    let gr2_anims = [];
    let subdirs = getSubdirectories(dir).filter(subdir => !subdir.endsWith("hair"));

    for (const subdir of subdirs) {
        gr2_anims = gr2_anims.concat(getFiles(subdir, ".gr2"));
    }

    return gr2_anims;
}

function getModels(dir) {
    let gr2_models = getFiles(dir, ".gr2");
    gr2_models = gr2_models.filter(file => !file.includes("_lod_"));
    gr2_models = gr2_models.filter(file => !file.includes("_rabbit1_backup"));
    
    let hairDir = path.join(dir, "hair");
    if (fs.existsSync(hairDir)) {
        const hairFiles = getFiles(hairDir, ".gr2");
        gr2_models = hairFiles.concat(gr2_models);
    }

    return gr2_models;
}

function createCmdFile(baseDir, combined, parent, child) {
    const wrappedPaths = combined.map(filePath => `-f "${filePath}"`).join(" ");
    const fbxDir = path.join(process.cwd(), "fbx");
    const cmdContent = `${wrappedPaths} -d "${fbxDir}"`;
    const outputCmdFileName = `${parent}-${child}-cmd.txt`;
    const outputCmdFilePath = path.join(process.cwd(), outputCmdFileName);

    fs.writeFileSync(outputCmdFilePath, cmdContent, "utf8");
    console.log(`File written: ${outputCmdFileName}`);
}

function processDirectory(baseDir) {
    const gr2_anims = getAnims(baseDir);
    const gr2_models = getModels(baseDir);

    const combined = [...gr2_anims, ...gr2_models];
    const [parent, child] = baseDir.split(path.sep).slice(-2);

    // Create the .txt file with paths
    const outputFileName = `${parent}-${child}.txt`;
    const outputFilePath = path.join(process.cwd(), outputFileName);
    fs.writeFileSync(outputFilePath, combined.join("\n"), "utf8");
    console.log(`File written: ${outputFileName}`);

    // Create the .cmd.txt file
    createCmdFile(baseDir, combined, parent, child);
}

// Main Execution
directories.forEach(processDirectory);
 

 

monster.js

Spoiler

 

// script.js

const fs = require('fs');
const path = require('path');

/**
 * Sprawdza, czy dany path jest katalogiem.
 */
function isDirectory(source) {
  return fs.lstatSync(source).isDirectory();
}

/**
 * Główna funkcja skryptu.
 */
function main() {
  // 1. Dwa katalogi bazowe
  const baseDirs = [
    'D:\\ymir work\\monster',
    'D:\\ymir work\\monster2'
  ];

  // 2. Zbiorcza lista subfolderów z obu katalogów
  let allSubfolders = [];

  for (const baseDir of baseDirs) {
    if (!fs.existsSync(baseDir)) {
      console.warn(`Baza katalogów "${baseDir}" nie istnieje! Pomijam...`);
      continue;
    }

    const entries = fs.readdirSync(baseDir);
    const directories = entries.filter(entry => {
      const fullPath = path.join(baseDir, entry);
      return isDirectory(fullPath);
    });

    directories.forEach(dir => {
      allSubfolders.push(path.join(baseDir, dir));
    });
  }

  // 3. Iterujemy po wszystkich subfolderach
  for (const folderPath of allSubfolders) {
    // Nazwa subfolderu, np. 'wolf'
    const subfolderName = path.basename(folderPath);

    // Tworzymy lokalny katalog wyjściowy (tam, gdzie jest uruchamiany skrypt)
    // __dirname to ścieżka do folderu, w którym znajduje się skrypt
    const localOutputFolder = path.join(__dirname, 'monster', subfolderName);

    // Jeśli katalog nie istnieje, tworzymy go
    if (!fs.existsSync(localOutputFolder)) {
      fs.mkdirSync(localOutputFolder, { recursive: true });
      // console.log(`Utworzono lokalny katalog: ${localOutputFolder}`);
    }

    // Plik motlist.txt w danym subfolderze
    const motlistPath = path.join(folderPath, 'motlist.txt');
    if (!fs.existsSync(motlistPath)) {
      console.warn(`Brak pliku "motlist.txt" w katalogu: ${folderPath} - pomijam, ale ostrzegam!`);
      continue;  // Przechodzimy do kolejnego subfolderu
    }

    // 4. Wczytujemy zawartość pliku motlist.txt
    const motlistContent = fs.readFileSync(motlistPath, 'utf8');
    const motlistLines = motlistContent.split(/\r?\n/).filter(line => line.trim() !== '');

    // Tablica do przechowywania danych o animacjach
    const animData = [];

    // 5. Parsowanie każdej linii motlist.txt
    for (const line of motlistLines) {
      // if (!line.includes('GENERAL')) {
      //   // Na wszelki wypadek ignorujemy linie, które nie zawierają słowa GENERAL
      //   continue;
      // }

      const parts = line.split(/\s+/);
      if (parts.length < 4) {
        console.warn(`Linia uszkodzona lub niepełna: "${line}" w katalogu: ${folderPath}`);
        continue;
      }

      // zgodnie z opisem:
      // parts[0] = 'GENERAL'
      // parts[1] = nazwa animacji
      // parts[2] = nazwa pliku .msa
      // parts[3] = ilość klatek

      const animationName = parts[1].toLowerCase(); 
      const msaFilename = parts[2];
      const frames = parts[3];

      // Podmiana .msa -> .gr2
      const gr2Filename = msaFilename.replace('.msa', '.gr2');
      const gr2FilePath = path.join(folderPath, gr2Filename);

      // Sprawdzamy, czy taki plik .gr2 istnieje
      if (!fs.existsSync(gr2FilePath)) {
        console.warn(`Plik .gr2 o nazwie "${gr2Filename}" nie istnieje w katalogu: ${folderPath}`);
      }

      // Zapamiętujemy dane o animacji
      animData.push({
        animationName,
        gr2Filename,
        frames
      });
    }

    // 6. Wyszukujemy wszystkie pliki .gr2 w bieżącym subfolderze
    const dirFiles = fs.readdirSync(folderPath);
    const allGr2Files = dirFiles.filter(file => file.toLowerCase().endsWith('.gr2'));

    // 7. Usuwamy z allGr2Files te, które zostały wykorzystane w motlist.txt
    const usedGr2Filenames = animData.map(item => item.gr2Filename);
    let remainingGr2Files = allGr2Files.filter(file => !usedGr2Filenames.includes(file));

    // 8. Dla pozostałych sprawdzamy warunki (numeryczna nazwa, "_lod")
    remainingGr2Files = remainingGr2Files.filter(file => {
      // Pomijamy pliki z "_lod"
      if (file.toLowerCase().includes('_lod')) {
        return false; 
      }
      // Ostrzeżenie, jeśli nazwa jest numeryczna
      const fileBaseName = path.parse(file).name; 
      if (!isNaN(fileBaseName)) {
        console.warn(`Plik .gr2 "${file}" w katalogu ${folderPath} ma numeryczną nazwę!`);
      }
      return true;
    });

    // 9. Przygotowujemy ścieżki do plików .gr2  
    const gr2PathsFromMotlist = animData.map(item => path.join(folderPath, item.gr2Filename));
    const additionalGr2Paths = remainingGr2Files.map(file => path.join(folderPath, file));

    // 10. Tworzenie pliku export.txt (w lokalnym katalogu)
    let exportContent = '';
    exportContent += gr2PathsFromMotlist.join('\n') + '\n';
    if (additionalGr2Paths.length > 0) {
      exportContent += additionalGr2Paths.join('\n') + '\n';
    }

    const exportTxtPath = path.join(localOutputFolder, 'export.txt');
    fs.writeFileSync(exportTxtPath, exportContent, 'utf8');
    // console.log(`Zapisano plik: ${exportTxtPath}`);

    // 11. Tworzenie pliku export_anims.py (również w lokalnym katalogu)
    //     Format: anim_list = [
    //       ("wait", 65),
    //       ("run", 100),
    //       ...
    //     ]
    let pythonAnimList = 'anim_list = [\n';
    animData.forEach(item => {
      pythonAnimList += `    ("${item.animationName}", ${item.frames}),\n`;
    });
    pythonAnimList += ']\n';

    const exportAnimsPyPath = path.join(localOutputFolder, 'export_anims.py');
    fs.writeFileSync(exportAnimsPyPath, pythonAnimList, 'utf8');
    // console.log(`Zapisano plik: ${exportAnimsPyPath}`);
  }

  // console.log("Operacja zakończona!");
}

// Uruchamiamy funkcję główną
main();
 

 

 

Edited by MeQQs
  • Metin2 Dev 1
  • Love 1
  • 2 weeks later...
On 12/29/2024 at 5:00 AM, MeQQs said:

Hello,

I've exported all the character models from a 2014 client, including animations and textures. I've created convenient .blend files, which might be useful for someone. You can use these for rendering purposes, or use the skeletons to create your own animations or other interesting projects.

You can also use these files to import all the models into another engine, such as Unity or Unreal.

I have tried to correct all the errors I found. However, it must be taken into account that these models and animations were created using Autodesk software. Consequently, there are certain differences that complicate the import process. One of these differences is the skeleton itself; we can notice that at the ends of fingers, toes, and other bones, there are small blocks often named with "nub." This is one of the differences in how the skeleton is constructed in Maya compared to Blender. These can be removed, and the animations will still work, but I left them just in case. It's always easier to remove something than to add it later.

All textures are in PNG format, and the alpha channel is preserved. Metin encodes information about, I believe, the metallness of the material in the alpha channel. I'm not sure about this. Therefore, most materials are set to ignore the alpha channel.

All textures are packed in the .blend file; however, I am providing these textures separately in a folder for easy access.

There is no reason why I exported these models. I just found a tool here on our forum and had to test it. I'm sharing the result in case someone finds it useful.

I have always wanted to export a character from Metin, but the possibilities were limited. It's great that it is now possible.

 

Hidden Content

  • Give reaction to this post to see the hidden content.

 

Hidden Content

  • Give reaction to this post to see the hidden content.

 

 

spacer.png

spacer.png

Quick and dirty render from blender:

spacer.pngspacer.png

spacer.pngspacer.png

 

I love it !

  • Love 3
  • 3 months later...
  • 1 month later...

I am also trying to gat that root motion problem sorted but i dont have much luck 😞 maybe someone could give a little more info on how to do it or upload the correct files. 

Thanks in advance.

  • 2 weeks later...
On 12/28/2024 at 11:00 PM, MeQQs said:

Hello,

I've exported all the character models from a 2014 client, including animations and textures. I've created convenient .blend files, which might be useful for someone. You can use these for rendering purposes, or use the skeletons to create your own animations or other interesting projects.

You can also use these files to import all the models into another engine, such as Unity or Unreal.

I have tried to correct all the errors I found. However, it must be taken into account that these models and animations were created using Autodesk software. Consequently, there are certain differences that complicate the import process. One of these differences is the skeleton itself; we can notice that at the ends of fingers, toes, and other bones, there are small blocks often named with "nub." This is one of the differences in how the skeleton is constructed in Maya compared to Blender. These can be removed, and the animations will still work, but I left them just in case. It's always easier to remove something than to add it later.

All textures are in PNG format, and the alpha channel is preserved. Metin encodes information about, I believe, the metallness of the material in the alpha channel. I'm not sure about this. Therefore, most materials are set to ignore the alpha channel.

All textures are packed in the .blend file; however, I am providing these textures separately in a folder for easy access.

There is no reason why I exported these models. I just found a tool here on our forum and had to test it. I'm sharing the result in case someone finds it useful.

I have always wanted to export a character from Metin, but the possibilities were limited. It's great that it is now possible.

 

Hidden Content

  • Give reaction to this post to see the hidden content.

 

Hidden Content

  • Give reaction to this post to see the hidden content.

 

 

spacer.png

spacer.png

Quick and dirty render from blender:

spacer.pngspacer.png

spacer.pngspacer.png

 

awesome, bro!

  • Love 1
  • 1 month later...
  • 4 weeks later...

Don't use any images from : imgur, turkmmop, freakgamers, inforge, hizliresim... Or your content will be deleted without notice...
Use : https://metin2.download/media/add/

Please use https://metin2.download/ when uploading files smaller than 100MB, otherwise the approval will take longer due to manual upload.

Please sign in to comment

You will be able to leave a comment after signing in



Sign In Now
×
×
  • Create New...

Important Information

Terms of Use / Privacy Policy / Guidelines / We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue.