ProjectRR — нативный прототип на ассетах ProjectR
- Все 189 скелетов Spine сконвертированы в нативные Godot Skeleton2D (.tscn), без Spine в рантайме - Меню + сцены: подземелье (карта комнат), просмотр спрайтов, нативные анимации - Кастомизация цвета (кожа/волосы/глаза) через шейдер - Вытащенные текстуры/UI/шрифты + нарезка спрайтов из атласа - Датасет комнат для LoRA, офлайн-инструменты конвертации в tools/ Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using Spine;
|
||||
|
||||
// Офлайн-экспортёр: читает Spine .skel.bytes+.atlas через spine-csharp и
|
||||
// выгружает геометрию (кости setup + привязки с мировыми вершинами/UV/треуг/весами) в свой JSON.
|
||||
// Использование: spineconv <base_path_без_расширения> <out.json>
|
||||
|
||||
class NullTex : TextureLoader
|
||||
{
|
||||
public void Load(AtlasPage page, string path) { }
|
||||
public void Unload(object texture) { }
|
||||
}
|
||||
|
||||
class Program
|
||||
{
|
||||
static void Main(string[] args)
|
||||
{
|
||||
string baseP = args[0];
|
||||
string outPath = args[1];
|
||||
|
||||
var atlas = new Atlas(baseP + ".atlas.txt", new NullTex());
|
||||
var loader = new AtlasAttachmentLoader(atlas);
|
||||
var binary = new SkeletonBinary(loader) { Scale = 1f };
|
||||
SkeletonData data = binary.ReadSkeletonData(baseP + ".skel.bytes");
|
||||
|
||||
var skeleton = new Skeleton(data);
|
||||
skeleton.SetToSetupPose();
|
||||
skeleton.UpdateWorldTransform(Skeleton.Physics.Update);
|
||||
|
||||
var boneIndex = new Dictionary<BoneData, int>();
|
||||
var bonesOut = new List<object>();
|
||||
var bones = data.Bones;
|
||||
for (int i = 0; i < bones.Count; i++)
|
||||
{
|
||||
var b = bones.Items[i];
|
||||
boneIndex[b] = i;
|
||||
bonesOut.Add(new
|
||||
{
|
||||
name = b.Name,
|
||||
parent = b.Parent != null ? b.Parent.Name : null,
|
||||
x = b.X, y = b.Y, rotation = b.Rotation,
|
||||
scaleX = b.ScaleX, scaleY = b.ScaleY,
|
||||
shearX = b.ShearX, shearY = b.ShearY, length = b.Length
|
||||
});
|
||||
}
|
||||
|
||||
var attsOut = new List<object>();
|
||||
var pages = new HashSet<string>();
|
||||
var drawOrder = skeleton.DrawOrder;
|
||||
for (int d = 0; d < drawOrder.Count; d++)
|
||||
{
|
||||
Slot slot = drawOrder.Items[d];
|
||||
Attachment att = slot.Attachment;
|
||||
if (att == null) continue;
|
||||
int sBone = boneIndex[slot.Data.BoneData];
|
||||
var sd = slot.Data;
|
||||
float[] color = { sd.R, sd.G, sd.B, sd.A };
|
||||
|
||||
if (att is RegionAttachment ra)
|
||||
{
|
||||
float[] wv = new float[8];
|
||||
ra.ComputeWorldVertices(slot, wv, 0, 2);
|
||||
var reg = (AtlasRegion)ra.Region;
|
||||
pages.Add(reg.page.name);
|
||||
var weights = new List<object>();
|
||||
for (int k = 0; k < 4; k++)
|
||||
weights.Add(new object[] { new object[] { sBone, 1f } });
|
||||
attsOut.Add(new
|
||||
{
|
||||
slot = sd.Name, name = ra.Name, type = "region", page = reg.page.name,
|
||||
world = wv, uv = ra.UVs, triangles = new int[] { 0, 1, 2, 2, 3, 0 },
|
||||
weights, color
|
||||
});
|
||||
}
|
||||
else if (att is MeshAttachment ma)
|
||||
{
|
||||
int n = ma.WorldVerticesLength;
|
||||
float[] wv = new float[n];
|
||||
ma.ComputeWorldVertices(slot, 0, n, wv, 0, 2);
|
||||
var reg = (AtlasRegion)ma.Region;
|
||||
pages.Add(reg.page.name);
|
||||
attsOut.Add(new
|
||||
{
|
||||
slot = sd.Name, name = ma.Name, type = "mesh", page = reg.page.name,
|
||||
world = wv, uv = ma.UVs, triangles = ma.Triangles,
|
||||
weights = ParseWeights(ma, sBone), color
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var outObj = new { bones = bonesOut, pages = new List<string>(pages), attachments = attsOut };
|
||||
File.WriteAllText(outPath, JsonSerializer.Serialize(outObj));
|
||||
int meshes = 0, regions = 0, weighted = 0;
|
||||
foreach (var s in drawOrder)
|
||||
{
|
||||
if (s.Attachment is MeshAttachment m) { meshes++; if (m.Bones != null) weighted++; }
|
||||
else if (s.Attachment is RegionAttachment) regions++;
|
||||
}
|
||||
Console.WriteLine($"OK bones={bonesOut.Count} regions={regions} meshes={meshes} (weighted={weighted}) pages={pages.Count}");
|
||||
}
|
||||
|
||||
static List<object> ParseWeights(MeshAttachment ma, int fallbackBone)
|
||||
{
|
||||
var res = new List<object>();
|
||||
int verts = ma.WorldVerticesLength / 2;
|
||||
if (ma.Bones == null)
|
||||
{
|
||||
for (int i = 0; i < verts; i++)
|
||||
res.Add(new object[] { new object[] { fallbackBone, 1f } });
|
||||
return res;
|
||||
}
|
||||
int[] bs = ma.Bones;
|
||||
float[] vs = ma.Vertices;
|
||||
int vb = 0, vv = 0;
|
||||
for (int i = 0; i < verts; i++)
|
||||
{
|
||||
int cnt = bs[vb++];
|
||||
var list = new List<object>();
|
||||
for (int k = 0; k < cnt; k++)
|
||||
{
|
||||
int bi = bs[vb++];
|
||||
float x = vs[vv++], y = vs[vv++], w = vs[vv++];
|
||||
list.Add(new object[] { bi, w });
|
||||
}
|
||||
res.Add(list);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://c18wfng0fuglx
|
||||
@@ -0,0 +1,33 @@
|
||||
# spineconv — экспорт геометрии Spine → JSON (офлайн)
|
||||
|
||||
Читает `.skel.bytes` + `.atlas.txt` через свободный исходник **spine-csharp 4.2** и
|
||||
выгружает геометрию рига (кости setup + привязки: мировые вершины, UV, треугольники,
|
||||
веса) в компактный JSON, который потребляет нативный сборщик (`src/NativeCharacter.cs`).
|
||||
Нужен потому, что рантайм spine-godot НЕ отдаёт геометрию привязок.
|
||||
|
||||
## Как собрать и запустить
|
||||
|
||||
```bash
|
||||
# 1) исходники spine-csharp 4.2 (рядом с этой папкой)
|
||||
git clone --depth 1 --branch 4.2 --filter=blob:none --sparse \
|
||||
https://github.com/EsotericSoftware/spine-runtimes.git spine-rt
|
||||
cd spine-rt && git sparse-checkout set spine-csharp/src && cd ..
|
||||
# (csproj ссылается на ..\spine-rt\spine-csharp\src\**\*.cs — поправь путь при необходимости)
|
||||
|
||||
# 2) собрать
|
||||
dotnet build -c Release
|
||||
|
||||
# 3) экспорт геометрии персонажа
|
||||
dotnet bin/Release/net8.0/spineconv.dll <path>/elf1_combat tools/rig/elf1_combat.json
|
||||
```
|
||||
|
||||
## Пайплайн Spine → нативный Godot (Вариант B)
|
||||
|
||||
1. **spineconv** → `tools/rig/<name>.json` (геометрия рига).
|
||||
2. **tools/bake_anim.gd** (в редакторе spine-godot, headless) → `tools/rig/<name>_anims.json`
|
||||
(запекает локальные трансформы костей покадрово; прогоняет Spine-рантайм офлайн).
|
||||
3. **src/NativeCharacter.cs** — в рантайме собирает `Skeleton2D` + взвешенные `Polygon2D`
|
||||
из геометрии и `AnimationPlayer` из запечённых костей. **Spine в рантайме не используется.**
|
||||
|
||||
Итоговый персонаж (`NativeCharacter.cs` + два JSON + атлас-PNG) переносится в обычный
|
||||
Godot без модуля Spine.
|
||||
@@ -0,0 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>disable</Nullable>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<AssemblyName>spineconv</AssemblyName>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="..\spine-rt\spine-csharp\src\**\*.cs" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
Reference in New Issue
Block a user