解神者Live2D导出问题

如下图,在AS(net472.v0.16.47)中打开,没有路径位置,所以没法直接用UnityLive2DExtractor这工具导出
如果有佬知道的话请在下面告诉我,感激不尽

2 个赞

他不是加密了吗?

大佬细说

巧了,我也遇到这个问题,去年想提取解神者,发现每个角色的live2d文件每部分都散乱的单独打包,只能提spine,我觉得这个要把资源目录的文件给解了,看看它是怎么放的才行

@Akaset
去Unitypy的群开贴问了一下,大佬给我了一些Dump出来的dll,需要指定它们来读取MonoBehaviour资源,我也上传到这里一起研究好了
DummyDll.zip (6.0 MB)

找别的佬又看了一下,模型加密了,要反编译看代码才知道怎么解,或者从内存中dump

更新
它所有的 ab 的 container 路径都是由一个叫 globalgamemanagers的文件来控制 (?),如果去除这个文件去加载全部文件,那么剩下的文件路径都不会加载出来,这文件的前面是所有data名,后面是一些container路径,最后还有一堆数据

找到一个笨方法可以提取解神者的moc3文件,虽然文件十分混乱还是可以通过尝试来找出live2d的moc3与贴图部分,具体思路如下:
1.先把live2d的尸块图片都通过as提取出来
2.根据wiki或者其他网站找出人物的图片,这里就拿楼主的示意图片来举例,通过尸块最后猜测出图片可能是莉莉丝#神格皮肤2的,网站网址https://wiki.biligame.com/x2/%E8%8E%89%E8%8E%89%E4%B8%9D#%E7%A5%9E%E6%A0%BC%E7%9A%AE%E8%82%A42
3.在as中搜索有关线索,这里是莉莉丝,经过尝试是搜索lilisu,type选择mono文件


4.将所有可能的文件右击搜索它的原始位置,并将其重命名
5.将文件放入hex编辑器里并搜索moc这样的关键字,如图

发现moc3二进制部分,使用python将这部分截取下来作为moc3文件
6.python代码部分,图省事就ChatGPT来完成了,记得修改路径位置

7.使用live2dexviewer进行尝试组合,如果不成功或者图片碎了可能是贴图找错了
8.最终效果

2 个赞

还能酱紫玩?奇妙
成了,现在就差动作了吧,鼠标追踪感觉是自己实现的?

动作你搜idle这类关键字也能搜到,带@的应该就是二进制的动作文件 as里面无法浏览内容 我没试能不能直接截取然后当动作用 方法应该和moc3的一样 那个live2dextractor 好像可以把这个动作转为json格式的 我不知道怎么转 所以也没有去尝试提取

完全不知道怎么下手。。

将我之前的python代码中的 moc_index = content.find(b’MOC’) 这一行的MOC换成这个idle的名称 这里是TianShu@Idle03就可以截取了 记得修改后缀 不过byte形式的动作我不知道live2dexviewer识别的是什么后缀 估计live2dexviewer可能读不出来 我逆向水平有限 只能提供点思路

不太会python。使用assetstudio(就云图那个)导出anim文件,跑一下就行了。我试过了,是没有问题的。这个是在昨天发布的云图上改的,稳定性会更好。
搞这个anim 转换为 json一开始就想着简单,早知道一开始就用正则了。然后转换格式。写了三个小时,麻了。有两个小时在调bug。
解牛者的物理文件转换代码还没写。不想搞了

import fs from 'fs'
import path from 'path'

// 每层空格多少个
const spaceCeil = 2
const numRexg = /^-?\d+\.?\d*$/
const CubismPhysicsControllerRexg = /CubismPhysicsController/

function hangle(lines, index, ceil, falg) {
  let data = {}
  let arrayData = []
  for (let i = index; i < lines.length; i++) {
    let line = lines[i];
    // 确认层级
    let spaceNum = 0
    for (let j = 0; j < line.length && (line[j] === ' ' || line[j] === '-'); j++) {
      spaceNum++
    }
    if (spaceNum / spaceCeil < ceil) {
      return {
        value: arrayData.length ? arrayData : Object.keys(data).length ? data : [],
        index: i - 1
      }
    }
    if (line.trim() === '') {
      continue;
    }
    const parts = line.split(':');
    let key = parts[0].trim();
    if (key.startsWith('- ')) {
      // key = key.split("-")[1].trim()
      // arrayData.push(data)
      // line[ceil * spaceCeil] = ' '
      if (falg) {
        return {
          value: data,
          index: i - 1
        }
      } else {
        lines[i] = line = line.substring(0, ceil * spaceCeil - spaceCeil) + ' ' + line.substring(ceil * spaceCeil + 1 - spaceCeil)
        let result = hangle(lines, i, ceil, true)
        arrayData.push(result.value)
        i = result.index
      }
      continue
    }
    let value = parts[1]?.trim()
    if (value.length === 0) {
      // console.log(key);
      let result = hangle(lines, i + 1, ceil + 1, false)
      // console.log(result.value);
      i = result.index
      value = result.value
      // console.log(key + ":" + JSON.stringify(value));
    } else if (numRexg.test(value)) {
      value = Number(value)
    } else if (value === "[]") {
      value = []
    }
    data[key] = value;
  }
  return {
    value: arrayData.length ? arrayData : Object.keys(data).length ? data : [],
    index: lines.length
  }
}

function processFadeFiles(dirPath) {
  const files = fs.readdirSync(dirPath);
  for (const file of files) {
    const filePath = path.join(dirPath, file);
    const stat = fs.statSync(filePath);
    if (stat.isDirectory()) {
      processFadeFiles(filePath);
    } else if (file.endsWith('.fade.json')) {
      try {
        const fileName = path.basename(file, '.fade.json');
        const data = fs.readFileSync(filePath, 'utf8');
        const obj = JSON.parse(data);
        const motion3Json = {
          'Version': 3,
          "Meta": {
            "Duration": 0.000,
            "Fps": 60.0,
            "Loop": true,
            "AreBeziersRestricted": true,
            "CurveCount": 0,
            "TotalSegmentCount": 0,
            "TotalPointCount": 0,
            "UserDataCount": 1,
            "TotalUserDataSize": 0
          },
          "Curves": [],
          "UserData": [
            {
              "Time": 0.0,
              "Value": ""
            }
          ]
        };
        // motion3Json.Meta.TotalSegmentCount = obj.ParameterIds * 10
        // motion3Json.Meta.TotalSegmentCount = obj.ParameterIds * 15
        let TotalSegmentCount = 0
        let maxTime = 0.0
        for (let i = 0; i < obj.ParameterCurves.length; i++) {
          let Segments = []
          for (let j = 0; j < obj.ParameterCurves[i].m_Curve.length; j++) {
            TotalSegmentCount++;
            Segments.push(obj.ParameterCurves[i].m_Curve[j].time ?? 0)
            Segments.push(obj.ParameterCurves[i].m_Curve[j].value ?? 0)
            Segments.push(obj.ParameterCurves[i].m_Curve[j].weightedMode ?? 0)
            maxTime = maxTime > obj.ParameterCurves[i].m_Curve[j].time ? maxTime : obj.ParameterCurves[i].m_Curve[j].time
          }
          Segments.pop()
          motion3Json.Curves.push({
            "Target": "Parameter",
            "Id": obj.ParameterIds[i],
            "Segments": Segments
          })
        }
        motion3Json.Meta.CurveCount = obj.ParameterIds.length
        motion3Json.Meta.Duration = maxTime
        motion3Json.Meta.TotalSegmentCount = TotalSegmentCount
        motion3Json.Meta.TotalPointCount = obj.ParameterIds.length + TotalSegmentCount
        fs.writeFileSync(path.join(dirPath, `${fileName}.motion3.json`), JSON.stringify(motion3Json, '\t'));
        console.log(path.join(dirPath, `${fileName}.motion3.json`) + "已生成");
      } catch (e) {
        console.log(e);
        console.log(path.join(dirPath, `${fileName}.motion3.json`) + "转化失败");
      }
    } else if (CubismPhysicsControllerRexg.test(file)) {
      try {
        const data = fs.readFileSync(filePath, 'utf8');
        const obj = JSON.parse(data);
        let physicsJson = {
          "Version": 3,
          "Meta": {
            "PhysicsSettingCount": 0,
            "TotalInputCount": 0,
            "TotalOutputCount": 0,
            "VertexCount": 0,
            "Fps": 0,
            "EffectiveForces": {
            },
            "PhysicsDictionary": [
            ]
          },
          "PhysicsSettings": []
        }
        physicsJson.Meta.EffectiveForces.Gravity = obj?._rig?.Gravity
        physicsJson.Meta.EffectiveForces.Wind = obj?._rig?.Wind
        physicsJson.Meta.Fps = obj._rig?.Fps ?? 60
        for (let i = 0; i < obj._rig?.SubRigs?.length ?? 0; i++) {
          let physicsSetting = {
            "Id": "PhysicsSetting",
            "Input": [
            ],
            "Output": [
            ],
            "Vertices": [
            ],
            "Normalization": {
            }
          }
          let rig = obj._rig.SubRigs[i]
          physicsSetting.Id = physicsSetting.Id + (i + 1)
          physicsJson.Meta.PhysicsDictionary.push({
            "Id": physicsSetting.Id,
            "Name": i + 1 + ""
          })
          for (let j = 0; j < rig?.Input.length ?? 0; j++) {
            physicsSetting.Input.push({
              "Source": {
                "Target": "Parameter",
                "Id": rig.Input[j].SourceId
              },
              "Weight": rig.Input[j].Weight,
              "Type": rig.Input[j].AngleScale || rig.Input[j].AngleScale === 0 ? "Angle" : "X",
              "Reflect": false
            })
          }
          for (let j = 0; j < rig?.Output.length ?? 0; j++) {
            physicsSetting.Output.push({
              "Destination": {
                "Target": "Parameter",
                "Id": rig.Output[j].DestinationId
              },
              "VertexIndex": 1,
              "Scale": rig.Output[j].AngleScale ?? 1,
              "Weight": rig.Output[j].Weight,
              "Type": rig.Output[j].AngleScale || rig.Output[j].AngleScale === 0 ? "Angle" : "X",
              "Reflect": false
            })
          }
          for (let j = 0; j < rig?.Particles?.length; j++) {
            physicsSetting.Vertices.push({
              "Position": rig?.Particles[j].InitialPosition,
              "Mobility": rig?.Particles[j].Mobility,
              "Delay": rig?.Particles[j].Delay,
              "Acceleration": rig?.Particles[j].Acceleration,
              "Radius": rig?.Particles[j].Radius
            })
          }
          physicsSetting.Normalization = rig.Normalization
          physicsJson.PhysicsSettings.push(physicsSetting)
        }
        fs.writeFileSync(path.join(dirPath, `l2d${Math.ceil(Math.random() * 100000)}.physics3.json`), JSON.stringify(physicsJson, '\t'));
        console.log(path.join(dirPath, `physics3.json`) + "已生成");
      } catch (e) {
        console.log(e);
        console.log(path.join(dirPath, `physics3.json`) + "转化失败");
      }
    } else if (file.endsWith(".anim")) {
      try {
        const animTxt = fs.readFileSync(filePath, 'utf8');
        let lines = animTxt.split("\n");
        // console.log(lines[0]);
        const animJson = hangle(lines, 3, 0).value
        const motion3Json = {
          'Version': 3,
          "Meta": {
            "Duration": 0.000,
            "Fps": 60.0,
            "Loop": true,
            "AreBeziersRestricted": true,
            "CurveCount": 0,
            "TotalSegmentCount": 0,
            "TotalPointCount": 0,
            "UserDataCount": 1,
            "TotalUserDataSize": 0
          },
          "Curves": [],
          "UserData": [
            {
              "Time": 0.0,
              "Value": ""
            }
          ]
        };
        const fileName = path.basename(file, '.anim');
        let TotalSegmentCount = 0
        let maxTime = 0
        for (let i = 0; i < animJson.AnimationClip.m_FloatCurves.length ?? 0; i++) {
          let Segments = []
          let curve = animJson.AnimationClip.m_FloatCurves[i].curve
          for (let j = 0; j < curve.m_Curve.length ?? 0; j++) {
            Segments.push(curve.m_Curve[j].time)
            Segments.push(curve.m_Curve[j].value)
            Segments.push(0)
            if (maxTime < curve.m_Curve[j].time) {
              maxTime = curve.m_Curve[j].time
            }
          }
          Segments.pop()
          TotalSegmentCount += curve.m_Curve.length
          if (Object.prototype.toString.call(animJson.AnimationClip.m_FloatCurves[i].path) === "[object String]") {
            // let Target, Id = animJson.AnimationClip.m_FloatCurves[i].path?.split("/")
            motion3Json.Curves.push({
              "Target": "Parameter",
              "Id": animJson.AnimationClip.m_FloatCurves[i].path?.split("/")[1],
              "Segments": Segments
            })
          }
        }
        motion3Json.Meta.Duration = maxTime
        motion3Json.Meta.TotalSegmentCount = TotalSegmentCount
        motion3Json.Meta.CurveCount = animJson.AnimationClip.m_FloatCurves.length
        motion3Json.Meta.TotalPointCount = motion3Json.Meta.TotalSegmentCount + motion3Json.Meta.CurveCount
        fs.writeFileSync(path.join(dirPath, `${fileName}.motion3.json`), JSON.stringify(motion3Json, '\t'))
        console.log(path.join(dirPath, `${fileName}.motion3.json`) + '已生成');
      } catch (err) {
        console.log(err);
        console.log(path.join(dirPath, file) + "转化失败");
      }
    }
  }
}
processFadeFiles("D:\\新建文件夹\\qq\\Output");
2 个赞

佬好强,我试试

物理文件,assetstudio导不出来。

是AnimationClip嘛
这是python脚本吧,我这边运行不起来。(

node.js脚本。你加我qq 1197158057我可以发你我跑完的(我不是卖片的)。python不是很熟,大二时python课都逃掉了。

笑死,刚提完,它就要似了

有提好的资源吗

上面的 muyi123 佬做好后传LiveViewerEX的工坊了

这里总览地写一下live2d的导出方法,希望对大家有所帮助

1.MOC3文件
一开始导入as里面怎么找也找不到moc3文件,再加上也没有容器路径,没法用UnityLive2DExtract这工具导出,然后看到楼上 bakaneko 佬的回帖,才知道moc3文件只是前面放了UnityFs的标头+一堆空字符+角色拼音名(有些甚至没有名字)+moc3头,直接切掉多余的头好了,至此,总算迈出第一步了

直接在资源文件里面找moc3脚本

import os
import shutil

def find_and_copy_moc3_files(input_folder, output_folder):
    if not os.path.exists(output_folder):
        os.makedirs(output_folder)

    # 16进制值"MOC3"对应的字节序列
    moc3_bytes = bytes.fromhex("4D 4F 43 33")

    for foldername, subfolders, filenames in os.walk(input_folder):
        for filename in filenames:
            file_path = os.path.join(foldername, filename)

            with open(file_path, "rb") as file:
                file_contents = file.read()
                if moc3_bytes in file_contents:
                    output_file_path = os.path.join(output_folder, filename)
                    shutil.copy(file_path, output_file_path)
                    print(f"Copied: {file_path} to {output_file_path}")

if __name__ == "__main__":
    input_folder = "/home/Neko/Downloads/Unpack Games/解神者_2.4/assets"
    output_folder = "moc3"
    find_and_copy_moc3_files(input_folder, output_folder)

切moc3标头前面的多余字符,并还原原本名称的脚本

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

function isNumAndAlphe(a) {
    if((a >= 97 && a <= 122) || (a >= 65 && a <= 90) || (a >= 48 && a <= 57) || a === '-'.charCodeAt() || a === '_'.charCodeAt()) {
        return true
    }
    return false
}

function deleteBinaryData1(filePath, directoryPath) {
    const data = fs.readFileSync(filePath);
    const mocIndex = data.indexOf('MOC');
    // const fileName = path.basename(filePath)
    let fileName = ''
    if (mocIndex !== -1) {
        // 寻找开始统计位
        let i
        for(i = mocIndex - 5; i >= 0 && !isNumAndAlphe(data[i]); i--) {
        }
        for(; i >= 0 && isNumAndAlphe(data[i]); i--) {
            fileName = String.fromCharCode(data[i]) + fileName;
        }
        const newData = data.slice(mocIndex);
        fs.writeFileSync(path.join(directoryPath, `${fileName}.moc3`), newData);
        console.log(filePath + "->" + path.join(directoryPath, `${fileName}.moc3`));
    }
}

function readDirectory1(directoryPath) {
    const files = fs.readdirSync(directoryPath);
    files.forEach((file) => {
        const filePath = path.join(directoryPath, file);
        const stats = fs.statSync(filePath);
        if (stats.isDirectory()) {
            // readDirectory1(filePath);
        }
        try{
            deleteBinaryData1(filePath, directoryPath);
        }catch(e) {
            console.log(e);
            console.log(filePath + "转换失败")
        }
    });
}

readDirectory1("moc3")

更新moc3文件更为方便的导出方法

这时候 muyi123 回帖,发了提取motions的脚本,我两加了QQ继续探讨后续

2.motions提取

这里用的是muyi123佬的提取脚本,但是代码的动画持续有点问题,用的是最晚时间减去最早时间,然后他在动画播放完到下一个动画播放前,会有一个卡顿
(尝试修正代码,但是反而时间变更大了,欢迎大佬们提出更好的动作提取方法,或者来修正代码)

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

// 每层空格多少个
const spaceCeil = 2
const numRexg = /^-?\d+\.?\d*$/
const CubismPhysicsControllerRexg = /CubismPhysicsController/

function hangle(lines, index, ceil, falg) {
    let data = {}
    let arrayData = []
    for (let i = index; i < lines.length; i++) {
        let line = lines[i];
        // 确认层级
        let spaceNum = 0
        for (let j = 0; j < line.length && (line[j] === ' ' || line[j] === '-'); j++) {
            spaceNum++
        }
        if (spaceNum / spaceCeil < ceil) {
            return {
                value: arrayData.length ? arrayData : Object.keys(data).length ? data : [],
                index: i - 1
            }
        }
        if (line.trim() === '') {
            continue;
        }
        const parts = line.split(':');
        let key = parts[0].trim();
        if (key.startsWith('- ')) {
            // key = key.split("-")[1].trim()
            // arrayData.push(data)
            // line[ceil * spaceCeil] = ' '
            if (falg) {
                return {
                    value: data,
                    index: i - 1
                }
            } else {
                lines[i] = line = line.substring(0, ceil * spaceCeil - spaceCeil) + ' ' + line.substring(ceil * spaceCeil + 1 - spaceCeil)
                let result = hangle(lines, i, ceil, true)
                arrayData.push(result.value)
                i = result.index
            }
            continue
        }
        let value = parts[1].trim()
        if (value.length === 0) {
            // console.log(key);
            let result = hangle(lines, i + 1, ceil + 1, false)
            // console.log(result.value);
            i = result.index
            value = result.value
            // console.log(key + ":" + JSON.stringify(value));
        } else if (numRexg.test(value)) {
            value = Number(value)
        } else if (value === "[]") {
            value = []
        }
        data[key] = value;
    }
    return {
        value: arrayData.length ? arrayData : Object.keys(data).length ? data : [],
        index: lines.length
    }
}

function processFadeFiles(dirPath) {
    const files = fs.readdirSync(dirPath);
    for (const file of files) {
        const filePath = path.join(dirPath, file);
        const stat = fs.statSync(filePath);
        if (stat.isDirectory()) {
            processFadeFiles(filePath);
        } else if (file.endsWith('.fade.json')) {
            const fileName = path.basename(file, '.fade.json');
            const data = fs.readFileSync(filePath, 'utf8');
            const obj = JSON.parse(data);
            const motion3Json = {
                'Version': 3,
                "Meta": {
                    "Duration": 0.000,
                    "Fps": 60.0,
                    "Loop": true,
                    "AreBeziersRestricted": true,
                    "CurveCount": 0,
                    "TotalSegmentCount": 0,
                    "TotalPointCount": 0,
                    "UserDataCount": 1,
                    "TotalUserDataSize": 0
                },
                "Curves": [],
                "UserData": [
                    {
                        "Time": 0.0,
                        "Value": ""
                    }
                ]
            };
            // motion3Json.Meta.TotalSegmentCount = obj.ParameterIds * 10
            // motion3Json.Meta.TotalSegmentCount = obj.ParameterIds * 15
            let TotalSegmentCount = 0
            let maxTime = 0.0
            for (let i = 0; i < obj.ParameterCurves.length; i++) {
                let Segments = []
                for (let j = 0; j < obj.ParameterCurves[i].m_Curve.length; j++) {
                    TotalSegmentCount++;
                    Segments.push(obj.ParameterCurves[i].m_Curve[j].time ?? 0)
                    Segments.push(obj.ParameterCurves[i].m_Curve[j].value ?? 0)
                    Segments.push(obj.ParameterCurves[i].m_Curve[j].weightedMode ?? 0)
                    maxTime = maxTime > obj.ParameterCurves[i].m_Curve[j].time ? maxTime : obj.ParameterCurves[i].m_Curve[j].time
                }
                Segments.pop()
                motion3Json.Curves.push({
                    "Target": "Parameter",
                    "Id": obj.ParameterIds[i],
                    "Segments": Segments
                })
            }
            motion3Json.Meta.CurveCount = obj.ParameterIds.length
            motion3Json.Meta.Duration = maxTime
            motion3Json.Meta.TotalSegmentCount = TotalSegmentCount
            motion3Json.Meta.TotalPointCount = obj.ParameterIds.length + TotalSegmentCount
            fs.writeFileSync(path.join(dirPath, `${fileName}.motion3.json`), JSON.stringify(motion3Json, '\t'));
            console.log(path.join(dirPath, `${fileName}.motion3.json`) + "已生成");
        } else if (CubismPhysicsControllerRexg.test(file)) {
            const data = fs.readFileSync(filePath, 'utf8');
            const obj = JSON.parse(data);
            let physicsJson = {
                "Version": 3,
                "Meta": {
                    "PhysicsSettingCount": 0,
                    "TotalInputCount": 0,
                    "TotalOutputCount": 0,
                    "VertexCount": 0,
                    "Fps": 0,
                    "EffectiveForces": {
                    },
                    "PhysicsDictionary": [
                    ]
                },
                "PhysicsSettings": []
            }
            physicsJson.Meta.EffectiveForces.Gravity = obj?._rig?.Gravity
            physicsJson.Meta.EffectiveForces.Wind = obj?._rig?.Wind
            physicsJson.Meta.Fps = obj._rig?.Fps ?? 60
            for (let i = 0; i < obj._rig?.SubRigs?.length ?? 0; i++) {
                let physicsSetting = {
                    "Id": "PhysicsSetting",
                    "Input": [
                    ],
                    "Output": [
                    ],
                    "Vertices": [
                    ],
                    "Normalization": {
                    }
                }
                let rig = obj._rig.SubRigs[i]
                physicsSetting.Id = physicsSetting.Id + (i + 1)
                physicsJson.Meta.PhysicsDictionary.push({
                    "Id": physicsSetting.Id,
                    "Name": i + 1 + ""
                })
                for (let j = 0; j < rig?.Input.length ?? 0; j++) {
                    physicsSetting.Input.push({
                        "Source": {
                            "Target": "Parameter",
                            "Id": rig.Input[j].SourceId
                        },
                        "Weight": rig.Input[j].Weight,
                        "Type": rig.Input[j].AngleScale || rig.Input[j].AngleScale === 0 ? "Angle" : "X",
                        "Reflect": false
                    })
                }
                for (let j = 0; j < rig?.Output.length ?? 0; j++) {
                    physicsSetting.Output.push({
                        "Destination": {
                            "Target": "Parameter",
                            "Id": rig.Output[j].DestinationId
                        },
                        "VertexIndex": 1,
                        "Scale": rig.Output[j].AngleScale ?? 1,
                        "Weight": rig.Output[j].Weight,
                        "Type": rig.Output[j].AngleScale || rig.Output[j].AngleScale === 0 ? "Angle" : "X",
                        "Reflect": false
                    })
                }
                for (let j = 0; j < rig?.Particles?.length; j++) {
                    physicsSetting.Vertices.push({
                        "Position": rig?.Particles[j].InitialPosition,
                        "Mobility": rig?.Particles[j].Mobility,
                        "Delay": rig?.Particles[j].Delay,
                        "Acceleration": rig?.Particles[j].Acceleration,
                        "Radius": rig?.Particles[j].Radius
                    })
                }
                physicsSetting.Normalization = rig.Normalization
                physicsJson.PhysicsSettings.push(physicsSetting)
            }
            fs.writeFileSync(path.join(dirPath, `l2d${Math.ceil(Math.random() * 100000)}.physics3.json`), JSON.stringify(physicsJson, '\t'));
            console.log(path.join(dirPath, `physics3.json`) + "已生成");
        } else if (file.endsWith(".anim")) {
            try {
                const animTxt = fs.readFileSync(filePath, 'utf8');
                let lines = animTxt.split("\n");
                // console.log(lines[0]);
                const animJson = hangle(lines, 3, 0).value
                const motion3Json = {
                    'Version': 3,
                    "Meta": {
                        "Duration": 0.000,
                        "Fps": 60.0,
                        "Loop": true,
                        "AreBeziersRestricted": true,
                        "CurveCount": 0,
                        "TotalSegmentCount": 0,
                        "TotalPointCount": 0,
                        "UserDataCount": 1,
                        "TotalUserDataSize": 0
                    },
                    "Curves": [],
                    "UserData": [
                        {
                            "Time": 0.0,
                            "Value": ""
                        }
                    ]
                };
                const fileName = path.basename(file, '.anim');
                let TotalSegmentCount = 0
                for (let i = 0; i < animJson.AnimationClip.m_FloatCurves.length ?? 0; i++) {
                    let Segments = []
                    let curve = animJson.AnimationClip.m_FloatCurves[i].curve
                    for (let j = 0; j < curve.m_Curve.length ?? 0; j++) {
                        Segments.push(curve.m_Curve[j].time)
                        Segments.push(curve.m_Curve[j].value)
                        Segments.push(0)
                    }
                    Segments.pop()
                    TotalSegmentCount += curve.m_Curve.length
                    if (Object.prototype.toString.call(animJson.AnimationClip.m_FloatCurves[i].path) === "[object String]") {
                        // let Target, Id = animJson.AnimationClip.m_FloatCurves[i].path?.split("/")
                        motion3Json.Curves.push({
                            "Target": "Parameter",
                            "Id": animJson.AnimationClip.m_FloatCurves[i].path?.split("/")[1],
                            "Segments": Segments
                        })
                    }
                }
                motion3Json.Meta.Duration = animJson.AnimationClip.m_AnimationClipSettings.m_StopTime - animJson.AnimationClip.m_AnimationClipSettings.m_StartTime
                motion3Json.Meta.TotalSegmentCount = TotalSegmentCount
                motion3Json.Meta.CurveCount = animJson.AnimationClip.m_FloatCurves.length
                motion3Json.Meta.TotalPointCount = motion3Json.Meta.TotalSegmentCount + motion3Json.Meta.CurveCount
                fs.writeFileSync(path.join(dirPath, `${fileName}.motion3.json`), JSON.stringify(motion3Json, '\t'))
                console.log(`${fileName}.motion3.json已生成`);
            }catch(err) {
                console.log(err);
                console.log(file + "转化失败");
            }
        }
    }
}




processFadeFiles("tianshu");

每个角色的live2d,只有三个motion,以 角色名+@Idlexx 来命名,如下图天书的例子

然后就是写一个model3.json文件来加载,我这里拿其他游戏直接套的,也可以写个脚本来自动套

{
	"Version": 3,
	"FileReferences": {
		"Moc": "tianshu_model.moc3",
		"Textures": [
			"textures/texture_1014.png"
		],
		"Motions": {
			"Idle": [
				{
					"File": "motions/[email protected]",
					"FadeInTime": 0,
					"FadeOutTime": 0
				}
			],
			"Tap": [
				{
					"File": "motions/[email protected]"
				},
				{
					"File": "motions/[email protected]"
				}
			]
		}
	},
	"HitAreas": []
}

3.结尾
至此应该就差不多了,和 muyi123 佬一起拼了20多个,这里感谢回帖的两个佬,还有QQ帮忙认角色的兄弟,后面 muyi123 佬上传了Live2DViewerEX的工坊,还对着B站的视频用vits切了语音(肝帝),大家可以去工坊订阅去支持他

这里用B的视频是因为包里面的音频资源拆出来不全,而且我们只有一个安装包的资源,不知道是不是缺了,大家可以看看自己的游戏资源有多大,如果有大佬愿意提供完整资源的话真的不胜感激

研究过程中用到的全部资源都在这了,感兴趣的可以自己下来研究

最后的最后忘了说这游戏是live2d+spine混杂(
扔一个我自己用来分类的脚本,as选择TextAssetsTextures,导出后直接用这个脚本就能自动分类+贴图分辨率修正

#!/bin/bash

# 遍历TextAsset文件夹中的文件
for file in ./TextAsset/*; do
  # 检查文件名是否包含".atlas"或".skel"
  if [[ $file == *".atlas"* || $file == *".skel"* ]]; then
    # 提取文件名和扩展名
    base_name=$(basename "$file")
    new_name=""

    # 如果文件名包含".atlas",只保留".atlas"作为后缀
    if [[ $base_name == *".atlas"* ]]; then
      new_name="${base_name%%.atlas*}.atlas"
    fi

    # 如果文件名包含".skel",只保留".skel"作为后缀
    if [[ $base_name == *".skel"* ]]; then
      new_name="${base_name%%.skel*}.skel"
    fi

    # 如果新文件名与旧文件名不同,进行重命名
    if [ "$base_name" != "$new_name" ]; then
      mv "$file" "./TextAsset/$new_name"
      echo "重命名文件: $file 为 $new_name"
    else
      echo "跳过文件: $file,不需要重命名"
    fi
  fi
done

# 创建Spine文件夹(如果不存在)
mkdir -p ./Spine

# 遍历TextAsset文件夹中的以.atlas为后缀的文本文件
for atlas_file in ./TextAsset/*.atlas; do
  # 提取文件名(不包括扩展名)
  atlas_base_name=$(basename -s .atlas "$atlas_file")

  # 检查是否存在对应的.png和size数据
  png_files=()
  size_data="./TextAsset/$atlas_base_name.atlas"

  if [ -f "$size_data" ]; then
    while IFS= read -r line; do
      if [[ $line == *".png" ]]; then
        png_file="./Texture2D/$line"
        if [ -f "$png_file" ]; then
          png_files+=("$png_file")
        fi
      fi
    done < "$size_data"

    if [ "${#png_files[@]}" -gt 0 ]; then
      # 从size数据中提取分辨率信息
      resolution=$(grep -oP 'size: \K[\d,]+' "$size_data")

      # 移动.atlas文件、对应的.png文件和匹配的.skel文件到Spine文件夹下的匹配文件夹
      target_folder="./Spine/$atlas_base_name"
      mkdir -p "$target_folder"
      cp "$atlas_file" "$target_folder"
      for png_file in "${png_files[@]}"; do
        cp "$png_file" "$target_folder"
      done
      cp "./TextAsset/$atlas_base_name.skel"* "$target_folder"
      echo "复制文件到 $target_folder"
    else
      echo "跳过 $atlas_file,没有找到对应的.png文件"
    fi
  else
    echo "跳过 $atlas_file,缺少对应的size数据文件"
  fi
done

# 处理图片缩放,确保与atlas中的一致
python <<EOF
import os
import re
from PIL import Image

def resize_image(image_path, new_size, output_path):
    image = Image.open(image_path)
    resized_image = image.resize(new_size, Image.LANCZOS)  # LANCZOS插值
    resized_image.save(output_path)

spine_folder = "Spine"  # 改为你的Spine文件夹路径
atlas_files = []

for root, dirs, files in os.walk(spine_folder):
    for file in files:
        if file.endswith(".atlas"):
            atlas_files.append(os.path.join(root, file))

for atlas_file in atlas_files:
    with open(atlas_file, "r") as file:
        lines = file.readlines()

    current_image = None
    correct_size = None

    image_pattern = re.compile(r'([^#]+)\.png')
    size_pattern = re.compile(r'size:\s*(\d+),\s*(\d+)')

    for line in lines:
        image_match = image_pattern.search(line)
        size_match = size_pattern.search(line)

        if image_match:
            current_image = image_match.group(1) + ".png"
        elif size_match:
            if current_image and not correct_size:
                width, height = map(int, size_match.groups())
                correct_size = (width, height)
            elif current_image and correct_size:
                width, height = map(int, size_match.groups())
                if (width, height) != correct_size:
                    image_path = os.path.join(os.path.dirname(atlas_file), current_image)
                    if Image.open(image_path).size != correct_size:
                        print(f"Resizing {image_path} to {correct_size}...")
                        resize_image(image_path, correct_size, image_path)
                current_image = None
                correct_size = None

print("操作完成。")

EOF

1 个赞